Skip to content

Commit 034a230

Browse files
9larsonsdaniellockyer
authored andcommitted
Added alpha feature to demonstrate websockets
refs TryGhost/Product#2561 - added simple socket-io implementation to Ghost server - added alpha flag for websockets - added route in admin to test websockets using a simple counter stored in server local memory (refreshes on reboot)
1 parent e52efb3 commit 034a230

File tree

16 files changed

+359
-5
lines changed

16 files changed

+359
-5
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<div class="gh-expandable-header">
2+
<div>
3+
<h4 class="gh-expandable-title">Counter</h4>
4+
<p class="gh-expandable-description">Current counter value: <strong>{{this.counter}}</strong></p>
5+
<p class="gh-expandable-description">This counter will reset when Ghost reboots.</p>
6+
</div>
7+
<button type="button" class="gh-btn" {{on "click" this.handleClick}} data-test-button="delete-all"><span>Add One</span></button>
8+
</div>
+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import Component from '@glimmer/component';
2+
import {action} from '@ember/object';
3+
import {inject as service} from '@ember/service';
4+
import {tracked} from '@glimmer/tracking';
5+
6+
export default class Websockets extends Component {
7+
@service('socket-io') socketIOService;
8+
9+
constructor(...args) {
10+
super(...args);
11+
// initialize connection
12+
13+
// TODO: ensure this works with subdirectories
14+
let origin = window.location.origin; // this gives us host:port
15+
let socket = this.socketIOService.socketFor(origin);
16+
// add listener
17+
socket.on('addCount', (value) => {
18+
this.counter = value;
19+
});
20+
}
21+
22+
// button counter
23+
@tracked counter = 0;
24+
25+
// handle button/event
26+
@action handleClick() {
27+
let socket = this.socketIOService.socketFor(origin);
28+
this.counter = 1 + this.counter;
29+
socket.emit('addCount', this.counter);
30+
}
31+
}
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import Controller from '@ember/controller';
2+
/* eslint-disable ghost/ember/alias-model-in-controller */
3+
import classic from 'ember-classic-decorator';
4+
import {inject as service} from '@ember/service';
5+
6+
@classic
7+
export default class WebsocketsController extends Controller {
8+
@service feature;
9+
10+
init() {
11+
super.init(...arguments);
12+
}
13+
}

ghost/admin/app/router.js

+3
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ Router.map(function () {
5555
this.route('settings.code-injection', {path: '/settings/code-injection'});
5656
this.route('settings.history', {path: '/settings/history'});
5757
this.route('settings.analytics', {path: '/settings/analytics'});
58+
59+
// testing websockets
60+
this.route('websockets');
5861

5962
// redirect from old /settings/members-email to /settings/newsletters
6063
this.route('settings.members-email', {path: '/settings/members-email'});

ghost/admin/app/routes/websockets.js

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import AuthenticatedRoute from './authenticated';
2+
import {inject as service} from '@ember/service';
3+
4+
// need this to be authenticated
5+
export default class WebsocketRoute extends AuthenticatedRoute {
6+
@service session;
7+
8+
beforeModel() {
9+
super.beforeModel(...arguments);
10+
11+
const user = this.session.user;
12+
13+
if (!user.isAdmin) {
14+
return this.transitionTo('settings.staff.user', user);
15+
}
16+
}
17+
}

ghost/admin/app/services/feature.js

+1
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export default class FeatureService extends Service {
7171
@feature('outboundLinkTagging') outboundLinkTagging;
7272
@feature('emailErrors') emailErrors;
7373
@feature('milestoneEmails') milestoneEmails;
74+
@feature('websockets') websockets;
7475

7576
_user = null;
7677

ghost/admin/app/templates/settings/labs.hbs

+13
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,19 @@
252252
</div>
253253
</div>
254254
</div>
255+
<div class="gh-expandable-block">
256+
<div class="gh-expandable-header">
257+
<div>
258+
<h4 class="gh-expandable-title">Websockets</h4>
259+
<p class="gh-expandable-description">
260+
Test out Websockets functionality at <code>/ghost/#/websockets</code>.
261+
</p>
262+
</div>
263+
<div class="for-switch">
264+
<GhFeatureFlag @flag="websockets" />
265+
</div>
266+
</div>
267+
</div>
255268
</div>
256269
</div>
257270
{{/if}}
+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<section class="gh-canvas">
2+
<GhCanvasHeader class="gh-canvas-header">
3+
<div class="flex flex-column">
4+
<h2 class="gh-canvas-title" data-test-screen-title>
5+
Testing Websockets
6+
</h2>
7+
</div>
8+
</GhCanvasHeader>
9+
{{#if (feature 'websockets')}}
10+
<section class="view-container settings-debug">
11+
<p class="gh-box gh-box-tip">{{svg-jar "idea"}}This is a testing ground for new or experimental features. They
12+
may change, break or inexplicably disappear at any time.</p>
13+
<div class="gh-main-section">
14+
<h4 class="gh-main-section-header small bn">Secrets</h4>
15+
<div class="gh-expandable">
16+
<div class="gh-expandable-block">
17+
<Websockets />
18+
</div>
19+
</div>
20+
</div>
21+
</section>
22+
{{else}}
23+
<section class="view-container settings-debug">
24+
<p class="gh-box gh-box-alert">{{svg-jar "warning-stroke"}}This is a testing ground for new or experimental features. You need developer experiments
25+
enabled to see the content here.
26+
</p>
27+
</section>
28+
{{/if}}
29+
</section>

ghost/admin/config/environment.js

+4
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ module.exports = function (environment) {
3333

3434
'ember-simple-auth': { },
3535

36+
'ember-websockets': {
37+
socketIO: true
38+
},
39+
3640
'@sentry/ember': {
3741
disablePerformance: true,
3842
sentry: {}

ghost/admin/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@
119119
"ember-test-selectors": "6.0.0",
120120
"ember-tooltips": "3.6.0",
121121
"ember-truth-helpers": "3.1.1",
122+
"ember-websockets": "10.2.1",
122123
"eslint": "8.34.0",
123124
"eslint-plugin-babel": "5.3.1",
124125
"eslint-plugin-react": "7.32.2",
@@ -181,4 +182,4 @@
181182
"path-browserify": "1.0.1",
182183
"webpack": "5.75.0"
183184
}
184-
}
185+
}

ghost/core/core/boot.js

+7
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,13 @@ async function bootGhost({backend = true, frontend = true, server = true} = {})
473473
await initDynamicRouting();
474474
}
475475

476+
// TODO: move this to the correct place once we figure out where that is
477+
if (ghostServer) {
478+
// NOTE: changes in this labs setting requires server reboot since we don't re-init services after changes a labs flag
479+
const websockets = require('./server/services/websockets');
480+
await websockets.init(ghostServer);
481+
}
482+
476483
await initServices({config});
477484
debug('End: Load Ghost Services & Apps');
478485

Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require('./service');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
const {Server} = require('socket.io');
2+
const debug = require('@tryghost/debug')('websockets');
3+
const logging = require('@tryghost/logging');
4+
5+
const labs = require('../../../shared/labs');
6+
7+
module.exports = {
8+
async init(ghostServer) {
9+
debug(`[Websockets] Is labs set? ${labs.isSet('websockets')}`);
10+
11+
if (labs.isSet('websockets')) {
12+
logging.info(`Starting websockets service`);
13+
14+
const io = new Server(ghostServer.httpServer);
15+
let count = 0;
16+
17+
io.on(`connection`, (socket) => {
18+
debug(`[Websockets] Client connected`);
19+
// on connect, send current value
20+
socket.emit('addCount', count);
21+
// listen to to changes in value from client
22+
socket.on('addCount', () => {
23+
count = count + 1;
24+
debug(`[Websockets] received addCount from client, count is now ${count}`);
25+
socket.broadcast.emit('addCount', count);
26+
});
27+
});
28+
29+
ghostServer.registerCleanupTask(async () => {
30+
logging.warn(`Stopping websockets service`);
31+
await new Promise((resolve) => {
32+
io.close(resolve);
33+
});
34+
});
35+
}
36+
}
37+
};

ghost/core/core/shared/labs.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ const ALPHA_FEATURES = [
3636
'urlCache',
3737
'beforeAfterCard',
3838
'lexicalEditor',
39-
'outboundLinkTagging'
39+
'outboundLinkTagging',
40+
'websockets'
4041
];
4142

4243
module.exports.GA_KEYS = [...GA_FEATURES];

ghost/core/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@
196196
"rss": "1.2.2",
197197
"sanitize-html": "2.9.0",
198198
"semver": "7.3.8",
199+
"socket.io": "4.6.0",
199200
"stoppable": "1.1.0",
200201
"uuid": "9.0.0",
201202
"xml": "1.0.1"

0 commit comments

Comments
 (0)