ghost 5.42.3 → 5.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/components/tryghost-adapter-cache-memory-ttl-5.44.0.tgz +0 -0
  2. package/components/tryghost-adapter-cache-redis-5.44.0.tgz +0 -0
  3. package/components/{tryghost-adapter-manager-5.42.3.tgz → tryghost-adapter-manager-5.44.0.tgz} +0 -0
  4. package/components/{tryghost-api-framework-5.42.3.tgz → tryghost-api-framework-5.44.0.tgz} +0 -0
  5. package/components/{tryghost-api-version-compatibility-service-5.42.3.tgz → tryghost-api-version-compatibility-service-5.44.0.tgz} +0 -0
  6. package/components/tryghost-audience-feedback-5.44.0.tgz +0 -0
  7. package/components/tryghost-bootstrap-socket-5.44.0.tgz +0 -0
  8. package/components/tryghost-constants-5.44.0.tgz +0 -0
  9. package/components/tryghost-custom-theme-settings-service-5.44.0.tgz +0 -0
  10. package/components/{tryghost-data-generator-5.42.3.tgz → tryghost-data-generator-5.44.0.tgz} +0 -0
  11. package/components/tryghost-domain-events-5.44.0.tgz +0 -0
  12. package/components/tryghost-dynamic-routing-events-5.44.0.tgz +0 -0
  13. package/components/tryghost-email-analytics-provider-mailgun-5.44.0.tgz +0 -0
  14. package/components/{tryghost-email-analytics-service-5.42.3.tgz → tryghost-email-analytics-service-5.44.0.tgz} +0 -0
  15. package/components/tryghost-email-content-generator-5.44.0.tgz +0 -0
  16. package/components/tryghost-email-events-5.44.0.tgz +0 -0
  17. package/components/{tryghost-email-service-5.42.3.tgz → tryghost-email-service-5.44.0.tgz} +0 -0
  18. package/components/tryghost-email-suppression-list-5.44.0.tgz +0 -0
  19. package/components/tryghost-event-aware-cache-wrapper-5.44.0.tgz +0 -0
  20. package/components/tryghost-express-dynamic-redirects-5.44.0.tgz +0 -0
  21. package/components/{tryghost-external-media-inliner-5.42.3.tgz → tryghost-external-media-inliner-5.44.0.tgz} +0 -0
  22. package/components/tryghost-extract-api-key-5.44.0.tgz +0 -0
  23. package/components/tryghost-html-to-plaintext-5.44.0.tgz +0 -0
  24. package/components/tryghost-i18n-5.44.0.tgz +0 -0
  25. package/components/tryghost-importer-handler-content-files-5.44.0.tgz +0 -0
  26. package/components/tryghost-importer-revue-5.44.0.tgz +0 -0
  27. package/components/{tryghost-job-manager-5.42.3.tgz → tryghost-job-manager-5.44.0.tgz} +0 -0
  28. package/components/{tryghost-link-redirects-5.42.3.tgz → tryghost-link-redirects-5.44.0.tgz} +0 -0
  29. package/components/{tryghost-link-replacer-5.42.3.tgz → tryghost-link-replacer-5.44.0.tgz} +0 -0
  30. package/components/{tryghost-link-tracking-5.42.3.tgz → tryghost-link-tracking-5.44.0.tgz} +0 -0
  31. package/components/tryghost-magic-link-5.44.0.tgz +0 -0
  32. package/components/{tryghost-mailgun-client-5.42.3.tgz → tryghost-mailgun-client-5.44.0.tgz} +0 -0
  33. package/components/{tryghost-member-attribution-5.42.3.tgz → tryghost-member-attribution-5.44.0.tgz} +0 -0
  34. package/components/tryghost-member-events-5.44.0.tgz +0 -0
  35. package/components/{tryghost-members-api-5.42.3.tgz → tryghost-members-api-5.44.0.tgz} +0 -0
  36. package/components/tryghost-members-csv-5.44.0.tgz +0 -0
  37. package/components/{tryghost-members-events-service-5.42.3.tgz → tryghost-members-events-service-5.44.0.tgz} +0 -0
  38. package/components/tryghost-members-importer-5.44.0.tgz +0 -0
  39. package/components/{tryghost-members-offers-5.42.3.tgz → tryghost-members-offers-5.44.0.tgz} +0 -0
  40. package/components/tryghost-members-payments-5.44.0.tgz +0 -0
  41. package/components/tryghost-members-ssr-5.44.0.tgz +0 -0
  42. package/components/{tryghost-members-stripe-service-5.42.3.tgz → tryghost-members-stripe-service-5.44.0.tgz} +0 -0
  43. package/components/{tryghost-mentions-email-report-5.42.3.tgz → tryghost-mentions-email-report-5.44.0.tgz} +0 -0
  44. package/components/{tryghost-milestones-5.42.3.tgz → tryghost-milestones-5.44.0.tgz} +0 -0
  45. package/components/{tryghost-minifier-5.42.3.tgz → tryghost-minifier-5.44.0.tgz} +0 -0
  46. package/components/tryghost-mw-api-version-mismatch-5.44.0.tgz +0 -0
  47. package/components/{tryghost-mw-cache-control-5.42.3.tgz → tryghost-mw-cache-control-5.44.0.tgz} +0 -0
  48. package/components/{tryghost-mw-error-handler-5.42.3.tgz → tryghost-mw-error-handler-5.44.0.tgz} +0 -0
  49. package/components/tryghost-mw-session-from-token-5.44.0.tgz +0 -0
  50. package/components/tryghost-mw-update-user-last-seen-5.44.0.tgz +0 -0
  51. package/components/tryghost-mw-version-match-5.44.0.tgz +0 -0
  52. package/components/tryghost-mw-vhost-5.44.0.tgz +0 -0
  53. package/components/tryghost-oembed-service-5.44.0.tgz +0 -0
  54. package/components/{tryghost-package-json-5.42.3.tgz → tryghost-package-json-5.44.0.tgz} +0 -0
  55. package/components/tryghost-posts-service-5.44.0.tgz +0 -0
  56. package/components/tryghost-referrers-5.44.0.tgz +0 -0
  57. package/components/tryghost-security-5.44.0.tgz +0 -0
  58. package/components/tryghost-session-service-5.44.0.tgz +0 -0
  59. package/components/tryghost-settings-path-manager-5.44.0.tgz +0 -0
  60. package/components/{tryghost-slack-notifications-5.42.3.tgz → tryghost-slack-notifications-5.44.0.tgz} +0 -0
  61. package/components/{tryghost-staff-service-5.42.3.tgz → tryghost-staff-service-5.44.0.tgz} +0 -0
  62. package/components/{tryghost-stats-service-5.42.3.tgz → tryghost-stats-service-5.44.0.tgz} +0 -0
  63. package/components/{tryghost-tiers-5.42.3.tgz → tryghost-tiers-5.44.0.tgz} +0 -0
  64. package/components/{tryghost-update-check-service-5.42.3.tgz → tryghost-update-check-service-5.44.0.tgz} +0 -0
  65. package/components/tryghost-verification-trigger-5.44.0.tgz +0 -0
  66. package/components/tryghost-version-notifications-data-service-5.44.0.tgz +0 -0
  67. package/components/{tryghost-webmentions-5.42.3.tgz → tryghost-webmentions-5.44.0.tgz} +0 -0
  68. package/content/themes/casper/assets/built/casper.js +1 -1
  69. package/content/themes/casper/assets/built/casper.js.map +1 -1
  70. package/content/themes/casper/assets/built/screen.css +1 -1
  71. package/content/themes/casper/assets/built/screen.css.map +1 -1
  72. package/content/themes/casper/assets/css/screen.css +0 -12
  73. package/content/themes/casper/assets/js/dropdown.js +5 -2
  74. package/content/themes/casper/package.json +1 -1
  75. package/core/boot.js +4 -0
  76. package/core/built/admin/assets/{chunk.234.b9c875c2897cfa768789.js → chunk.142.4b3c5e7ebee5433ac17e.js} +3179 -2665
  77. package/core/built/admin/assets/{chunk.234.b9c875c2897cfa768789.js.LICENSE.txt → chunk.142.4b3c5e7ebee5433ac17e.js.LICENSE.txt} +6 -0
  78. package/core/built/admin/assets/chunk.143.866856df015466a9d44c.js +35 -0
  79. package/core/built/admin/assets/{chunk.178.f6ada740ff9362d3ce09.js → chunk.178.8cd491189357ddcb427c.js} +4 -4
  80. package/core/built/admin/assets/{ghost-230f1ee812a92b7cd06bfa035994d093.js → ghost-1ce41f5b414ff115ff8167924199e7ba.js} +1237 -1157
  81. package/core/built/admin/assets/ghost-65a4349f579bcb6dcc5a0f0d7774f19a.css +1 -0
  82. package/core/built/admin/assets/ghost-dark-18bd18413fbf8e8b2f0ac21b32deb6ce.css +1 -0
  83. package/core/built/admin/assets/{vendor-da13470386c6ee05178d49830c64d7ab.js → vendor-f7817f9d354338ee94d5561277ea8056.js} +30 -26
  84. package/core/built/admin/index.html +6 -6
  85. package/core/server/api/endpoints/pages.js +50 -0
  86. package/core/server/api/endpoints/posts.js +7 -4
  87. package/core/server/api/endpoints/settings.js +1 -1
  88. package/core/server/api/endpoints/utils/serializers/input/pages.js +8 -0
  89. package/core/server/api/endpoints/utils/serializers/output/pages.js +20 -0
  90. package/core/server/data/migrations/versions/5.44/2023-04-14-04-17-add-snippets-lexical-column.js +8 -0
  91. package/core/server/data/schema/schema.js +1 -0
  92. package/core/server/models/base/plugins/bulk-operations.js +25 -9
  93. package/core/server/services/lexical-multiplayer/index.js +1 -0
  94. package/core/server/services/lexical-multiplayer/service.js +141 -0
  95. package/core/server/services/lexical-multiplayer/y-websocket.js +244 -0
  96. package/core/server/services/settings/settings-bread-service.js +15 -0
  97. package/core/server/web/api/endpoints/admin/routes.js +2 -0
  98. package/core/shared/config/defaults.json +1 -1
  99. package/core/shared/labs.js +1 -0
  100. package/package.json +136 -132
  101. package/yarn.lock +59 -33
  102. package/components/tryghost-adapter-cache-memory-ttl-5.42.3.tgz +0 -0
  103. package/components/tryghost-adapter-cache-redis-5.42.3.tgz +0 -0
  104. package/components/tryghost-audience-feedback-5.42.3.tgz +0 -0
  105. package/components/tryghost-bootstrap-socket-5.42.3.tgz +0 -0
  106. package/components/tryghost-constants-5.42.3.tgz +0 -0
  107. package/components/tryghost-custom-theme-settings-service-5.42.3.tgz +0 -0
  108. package/components/tryghost-domain-events-5.42.3.tgz +0 -0
  109. package/components/tryghost-dynamic-routing-events-5.42.3.tgz +0 -0
  110. package/components/tryghost-email-analytics-provider-mailgun-5.42.3.tgz +0 -0
  111. package/components/tryghost-email-content-generator-5.42.3.tgz +0 -0
  112. package/components/tryghost-email-events-5.42.3.tgz +0 -0
  113. package/components/tryghost-email-suppression-list-5.42.3.tgz +0 -0
  114. package/components/tryghost-event-aware-cache-wrapper-5.42.3.tgz +0 -0
  115. package/components/tryghost-express-dynamic-redirects-5.42.3.tgz +0 -0
  116. package/components/tryghost-extract-api-key-5.42.3.tgz +0 -0
  117. package/components/tryghost-html-to-plaintext-5.42.3.tgz +0 -0
  118. package/components/tryghost-i18n-5.42.3.tgz +0 -0
  119. package/components/tryghost-importer-handler-content-files-5.42.3.tgz +0 -0
  120. package/components/tryghost-importer-revue-5.42.3.tgz +0 -0
  121. package/components/tryghost-magic-link-5.42.3.tgz +0 -0
  122. package/components/tryghost-member-events-5.42.3.tgz +0 -0
  123. package/components/tryghost-members-csv-5.42.3.tgz +0 -0
  124. package/components/tryghost-members-importer-5.42.3.tgz +0 -0
  125. package/components/tryghost-members-payments-5.42.3.tgz +0 -0
  126. package/components/tryghost-members-ssr-5.42.3.tgz +0 -0
  127. package/components/tryghost-mw-api-version-mismatch-5.42.3.tgz +0 -0
  128. package/components/tryghost-mw-session-from-token-5.42.3.tgz +0 -0
  129. package/components/tryghost-mw-update-user-last-seen-5.42.3.tgz +0 -0
  130. package/components/tryghost-mw-version-match-5.42.3.tgz +0 -0
  131. package/components/tryghost-mw-vhost-5.42.3.tgz +0 -0
  132. package/components/tryghost-oembed-service-5.42.3.tgz +0 -0
  133. package/components/tryghost-posts-service-5.42.3.tgz +0 -0
  134. package/components/tryghost-referrers-5.42.3.tgz +0 -0
  135. package/components/tryghost-security-5.42.3.tgz +0 -0
  136. package/components/tryghost-session-service-5.42.3.tgz +0 -0
  137. package/components/tryghost-settings-path-manager-5.42.3.tgz +0 -0
  138. package/components/tryghost-verification-trigger-5.42.3.tgz +0 -0
  139. package/components/tryghost-version-notifications-data-service-5.42.3.tgz +0 -0
  140. package/core/built/admin/assets/chunk.143.d8ff82a24cae66011524.js +0 -35
  141. package/core/built/admin/assets/ghost-190bf0eacd6acd718615616770dc8a99.css +0 -1
  142. package/core/built/admin/assets/ghost-dark-79c98a102be37f72f679e924a51945bc.css +0 -1
@@ -0,0 +1,141 @@
1
+ const debug = require('@tryghost/debug')('lexical-multiplayer'); // eslint-disable-line no-unused-vars
2
+ const logging = require('@tryghost/logging');
3
+ const {getSession} = require('../auth/session/express-session');
4
+ const models = require('../../models');
5
+ const labs = require('../../../shared/labs');
6
+
7
+ let wss;
8
+
9
+ const onSocketError = (error) => {
10
+ logging.error(error);
11
+ };
12
+
13
+ const onUnauthorized = (socket) => {
14
+ socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
15
+ socket.destroy();
16
+ };
17
+
18
+ const handleUpgrade = async (request, socket, head) => {
19
+ socket.on('error', onSocketError);
20
+
21
+ // make sure the request is on the supported path
22
+ // TODO: check handling of subdirectories
23
+ if (!request.url.startsWith('/ghost/api/admin/posts/multiplayer/')) {
24
+ socket.write('HTTP/1.1 404 Not Found\r\n\r\n');
25
+ socket.destroy();
26
+ return;
27
+ }
28
+
29
+ // grab the session from the request
30
+ const session = await getSession(request, {});
31
+ if (!session || !session.user_id) {
32
+ return onUnauthorized(socket);
33
+ }
34
+
35
+ // fetch the session's user from the db
36
+ const user = await models.User.findOne({id: session.user_id});
37
+ if (!user) {
38
+ return onUnauthorized(socket);
39
+ }
40
+ request.user = user;
41
+
42
+ // TODO: check if user has access to the post
43
+
44
+ // TODO: (elsewhere) close websocket connections on logout
45
+ // - probably need to create a map of sockets to users?
46
+
47
+ socket.removeListener('error', onSocketError);
48
+
49
+ wss.handleUpgrade(request, socket, head, (ws) => {
50
+ wss.emit('connection', ws, request);
51
+ });
52
+ };
53
+
54
+ let _enable;
55
+ let _disable;
56
+ let _isClosing = false;
57
+ let _closePromise;
58
+
59
+ module.exports = {
60
+ async init(ghostServer) {
61
+ _enable = async () => {
62
+ if (_isClosing) {
63
+ logging.info('Waiting for previous Lexical multiplayer websockets service to close');
64
+ await _closePromise;
65
+ }
66
+
67
+ if (wss) {
68
+ logging.info('Lexical multiplayer websockets service already started');
69
+ return;
70
+ }
71
+
72
+ if (labs.isSet('lexicalMultiplayer')) {
73
+ logging.info('Starting lexical multiplayer websockets service');
74
+
75
+ // TODO: can we use or adapt patterns from https://github.com/HenningM/express-ws?
76
+ const WS = require('ws');
77
+ wss = new WS.Server({noServer: true});
78
+ const {setupWSConnection} = require('./y-websocket');
79
+
80
+ wss.on('connection', (socket, request) => {
81
+ socket.on('error', onSocketError);
82
+
83
+ // TODO: better method for extracting doc name from URL
84
+ const docName = request.url.replace('/ghost/api/admin/posts/multiplayer/', '');
85
+ setupWSConnection(socket, request, {docName});
86
+ });
87
+
88
+ // TODO: this should probably be at a higher level, especially if we
89
+ // want to support multiple websocket services
90
+ ghostServer.httpServer.on('upgrade', handleUpgrade);
91
+ }
92
+ };
93
+
94
+ _disable = async () => {
95
+ logging.info('Stopping lexical multiplayer websockets service');
96
+ ghostServer.httpServer.off('upgrade', handleUpgrade);
97
+
98
+ if (wss) {
99
+ _isClosing = true;
100
+ _closePromise = new Promise((resolve) => {
101
+ // first sweep, soft close
102
+ wss.clients.forEach((socket) => {
103
+ socket.close();
104
+ });
105
+
106
+ setTimeout(() => {
107
+ // second sweep, hard close
108
+ wss.clients.forEach((socket) => {
109
+ if ([socket.OPEN, socket.CLOSING].includes(socket.readyState)) {
110
+ socket.terminate();
111
+ }
112
+ });
113
+
114
+ resolve();
115
+ }, 5000);
116
+ }).finally(() => {
117
+ wss = null;
118
+ _isClosing = false;
119
+ });
120
+
121
+ return _closePromise;
122
+ }
123
+ };
124
+ },
125
+
126
+ async enable() {
127
+ if (!_enable) {
128
+ logging.error('Lexical multiplayer service must be initialized before it can be enabled/disabled');
129
+ return;
130
+ }
131
+ return _enable();
132
+ },
133
+
134
+ async disable() {
135
+ if (!_enable) {
136
+ logging.error('Lexical multiplayer service must be initialized before it can be enabled/disabled');
137
+ return;
138
+ }
139
+ return _disable();
140
+ }
141
+ };
@@ -0,0 +1,244 @@
1
+ // based on https://github.com/yjs/y-websocket/blob/master/bin/utils.js
2
+
3
+ const Y = require('yjs');
4
+ const syncProtocol = require('y-protocols/dist/sync.cjs');
5
+ const awarenessProtocol = require('y-protocols/dist/awareness.cjs');
6
+
7
+ const encoding = require('lib0/dist/encoding.cjs');
8
+ const decoding = require('lib0/dist/decoding.cjs');
9
+ const map = require('lib0/dist/map.cjs');
10
+
11
+ const wsReadyStateConnecting = 0;
12
+ const wsReadyStateOpen = 1;
13
+ const wsReadyStateClosing = 2 // eslint-disable-line
14
+ const wsReadyStateClosed = 3 // eslint-disable-line
15
+
16
+ /**
17
+ * @type {Map<string,WSSharedDoc>}
18
+ */
19
+ const docs = new Map();
20
+ module.exports.docs = docs;
21
+
22
+ const messageSync = 0;
23
+ const messageAwareness = 1;
24
+
25
+ /**
26
+ * @param {Uint8Array} update
27
+ * @param {any} origin
28
+ * @param {WSSharedDoc} doc
29
+ */
30
+ const updateHandler = (update, origin, doc) => {
31
+ const encoder = encoding.createEncoder();
32
+ encoding.writeVarUint(encoder, messageSync);
33
+ syncProtocol.writeUpdate(encoder, update);
34
+ const message = encoding.toUint8Array(encoder);
35
+ doc.conns.forEach((_, conn) => send(doc, conn, message));
36
+ };
37
+
38
+ class WSSharedDoc extends Y.Doc {
39
+ /**
40
+ * @param {string} name
41
+ */
42
+ constructor(name) {
43
+ super({gc: true});
44
+ this.name = name;
45
+ /**
46
+ * Maps from conn to set of controlled user ids. Delete all user ids from awareness when this conn is closed
47
+ * @type {Map<Object, Set<number>>}
48
+ */
49
+ this.conns = new Map();
50
+ /**
51
+ * @type {awarenessProtocol.Awareness}
52
+ */
53
+ this.awareness = new awarenessProtocol.Awareness(this);
54
+ this.awareness.setLocalState(null);
55
+ /**
56
+ * @param {{ added: Array<number>, updated: Array<number>, removed: Array<number> }} changes
57
+ * @param {Object | null} conn Origin is the connection that made the change
58
+ */
59
+ const awarenessChangeHandler = ({added, updated, removed}, conn) => {
60
+ const changedClients = added.concat(updated, removed);
61
+ if (conn !== null) {
62
+ const connControlledIDs = /** @type {Set<number>} */ (this.conns.get(conn));
63
+ if (connControlledIDs !== undefined) {
64
+ added.forEach((clientID) => {
65
+ connControlledIDs.add(clientID);
66
+ });
67
+ removed.forEach((clientID) => {
68
+ connControlledIDs.delete(clientID);
69
+ });
70
+ }
71
+ }
72
+ // broadcast awareness update
73
+ const encoder = encoding.createEncoder();
74
+ encoding.writeVarUint(encoder, messageAwareness);
75
+ encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(this.awareness, changedClients));
76
+ const buff = encoding.toUint8Array(encoder);
77
+ this.conns.forEach((_, c) => {
78
+ send(this, c, buff);
79
+ });
80
+ };
81
+ this.awareness.on('update', awarenessChangeHandler);
82
+ this.on('update', updateHandler);
83
+ // if (isCallbackSet) {
84
+ // this.on('update', debounce(
85
+ // callbackHandler,
86
+ // CALLBACK_DEBOUNCE_WAIT,
87
+ // { maxWait: CALLBACK_DEBOUNCE_MAXWAIT }
88
+ // ))
89
+ // }
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Gets a Y.Doc by name, whether in memory or on disk
95
+ *
96
+ * @param {string} docname - the name of the Y.Doc to find or create
97
+ * @param {boolean} gc - whether to allow gc on the doc (applies only when created)
98
+ * @return {WSSharedDoc}
99
+ */
100
+ const getYDoc = (docname, gc = true) => map.setIfUndefined(docs, docname, () => {
101
+ const doc = new WSSharedDoc(docname);
102
+ doc.gc = gc;
103
+ // if (persistence !== null) {
104
+ // persistence.bindState(docname, doc);
105
+ // }
106
+ docs.set(docname, doc);
107
+ return doc;
108
+ });
109
+
110
+ module.exports.getYDoc = getYDoc;
111
+
112
+ /**
113
+ * @param {any} conn
114
+ * @param {WSSharedDoc} doc
115
+ * @param {Uint8Array} message
116
+ */
117
+ const messageListener = (conn, doc, message) => {
118
+ try {
119
+ const encoder = encoding.createEncoder();
120
+ const decoder = decoding.createDecoder(message);
121
+ const messageType = decoding.readVarUint(decoder);
122
+ switch (messageType) {
123
+ case messageSync:
124
+ encoding.writeVarUint(encoder, messageSync);
125
+ syncProtocol.readSyncMessage(decoder, encoder, doc, conn);
126
+
127
+ // If the `encoder` only contains the type of reply message and no
128
+ // message, there is no need to send the message. When `encoder` only
129
+ // contains the type of reply, its length is 1.
130
+ if (encoding.length(encoder) > 1) {
131
+ send(doc, conn, encoding.toUint8Array(encoder));
132
+ }
133
+ break;
134
+ case messageAwareness: {
135
+ awarenessProtocol.applyAwarenessUpdate(doc.awareness, decoding.readVarUint8Array(decoder), conn);
136
+ break;
137
+ }
138
+ }
139
+ } catch (err) {
140
+ doc.emit('error', [err]);
141
+ }
142
+ };
143
+
144
+ /**
145
+ * @param {WSSharedDoc} doc
146
+ * @param {any} conn
147
+ */
148
+ const closeConn = (doc, conn) => {
149
+ if (doc.conns.has(conn)) {
150
+ /**
151
+ * @type {Set<number>}
152
+ */
153
+ const controlledIds = doc.conns.get(conn);
154
+ doc.conns.delete(conn);
155
+ awarenessProtocol.removeAwarenessStates(doc.awareness, Array.from(controlledIds), null);
156
+ if (doc.conns.size === 0/* && persistence !== null*/) {
157
+ // if persisted, we store state and destroy ydocument
158
+ // persistence.writeState(doc.name, doc).then(() => {
159
+ // doc.destroy();
160
+ // });
161
+ docs.delete(doc.name);
162
+ }
163
+ }
164
+ conn.close();
165
+ };
166
+
167
+ /**
168
+ * @param {WSSharedDoc} doc
169
+ * @param {any} conn
170
+ * @param {Uint8Array} m
171
+ */
172
+ const send = (doc, conn, m) => {
173
+ if (conn.readyState !== wsReadyStateConnecting && conn.readyState !== wsReadyStateOpen) {
174
+ closeConn(doc, conn);
175
+ }
176
+ try {
177
+ conn.send(m, (err) => {
178
+ if (err !== null && err !== undefined) {
179
+ closeConn(doc, conn);
180
+ }
181
+ });
182
+ } catch (e) {
183
+ closeConn(doc, conn);
184
+ }
185
+ };
186
+
187
+ const pingTimeout = 30000;
188
+
189
+ /**
190
+ * @param {any} conn
191
+ * @param {any} req
192
+ * @param {any} opts
193
+ */
194
+ module.exports.setupWSConnection = (conn, req, {docName = req.url.slice(1).split('?')[0], gc = true} = {}) => {
195
+ conn.binaryType = 'arraybuffer';
196
+ // get doc, initialize if it does not exist yet
197
+ const doc = getYDoc(docName, gc);
198
+ doc.conns.set(conn, new Set());
199
+ // listen and reply to events
200
+ conn.on('message', /** @param {ArrayBuffer} message */ message => messageListener(conn, doc, new Uint8Array(message)));
201
+
202
+ // Check if connection is still alive
203
+ let pongReceived = true;
204
+ const pingInterval = setInterval(() => {
205
+ if (!pongReceived) {
206
+ if (doc.conns.has(conn)) {
207
+ closeConn(doc, conn);
208
+ }
209
+ clearInterval(pingInterval);
210
+ } else if (doc.conns.has(conn)) {
211
+ pongReceived = false;
212
+ try {
213
+ conn.ping();
214
+ } catch (e) {
215
+ closeConn(doc, conn);
216
+ clearInterval(pingInterval);
217
+ }
218
+ }
219
+ }, pingTimeout);
220
+ conn.on('close', () => {
221
+ closeConn(doc, conn);
222
+ clearInterval(pingInterval);
223
+ });
224
+ conn.on('pong', () => {
225
+ pongReceived = true;
226
+ });
227
+ // put the following in a variables in a block so the interval handlers don't keep in in
228
+ // scope
229
+ {
230
+ // send sync step 1
231
+ const syncEncoder = encoding.createEncoder();
232
+ encoding.writeVarUint(syncEncoder, messageSync);
233
+ syncProtocol.writeSyncStep1(syncEncoder, doc);
234
+ send(doc, conn, encoding.toUint8Array(syncEncoder));
235
+
236
+ const awarenessStates = doc.awareness.getStates();
237
+ if (awarenessStates.size > 0) {
238
+ const awarenessEncoder = encoding.createEncoder();
239
+ encoding.writeVarUint(awarenessEncoder, messageAwareness);
240
+ encoding.writeVarUint8Array(awarenessEncoder, awarenessProtocol.encodeAwarenessUpdate(doc.awareness, Array.from(awarenessStates.keys())));
241
+ send(doc, conn, encoding.toUint8Array(awarenessEncoder));
242
+ }
243
+ }
244
+ };
@@ -214,6 +214,21 @@ class SettingsBREADService {
214
214
  const {filteredSettings: refilteredSettings, emailsToVerify} = await this.prepSettingsForEmailVerification(filteredSettings, getSetting);
215
215
 
216
216
  const modelArray = await this.SettingsModel.edit(refilteredSettings, options).then((result) => {
217
+ // TODO: temporary fix for starting/stopping lexicalMultiplayer service when labs flag is changed
218
+ // this should be removed along with the flag, or set up in a more generic way
219
+ const labsSetting = result.find(setting => setting.get('key') === 'labs');
220
+ if (labsSetting) {
221
+ const lexicalMultiplayer = require('../lexical-multiplayer');
222
+ const previous = JSON.parse(labsSetting.previousAttributes().value);
223
+ const current = JSON.parse(labsSetting.get('value'));
224
+
225
+ if (!previous.lexicalMultiplayer && current.lexicalMultiplayer) {
226
+ lexicalMultiplayer.enable();
227
+ } else if (previous.lexicalMultiplayer && !current.lexicalMultiplayer) {
228
+ lexicalMultiplayer.disable();
229
+ }
230
+ }
231
+
217
232
  return this._formatBrowse(_.keyBy(_.invokeMap(result, 'toJSON'), 'key'), options.context);
218
233
  });
219
234
 
@@ -42,6 +42,8 @@ module.exports = function apiRoutes() {
42
42
 
43
43
  // ## Pages
44
44
  router.get('/pages', mw.authAdminApi, http(api.pages.browse));
45
+ router.del('/pages', mw.authAdminApi, http(api.pages.bulkDestroy));
46
+ router.put('/pages/bulk', mw.authAdminApi, http(api.pages.bulkEdit));
45
47
  router.post('/pages', mw.authAdminApi, http(api.pages.add));
46
48
  router.get('/pages/:id', mw.authAdminApi, http(api.pages.read));
47
49
  router.get('/pages/slug/:slug', mw.authAdminApi, http(api.pages.read));
@@ -176,7 +176,7 @@
176
176
  },
177
177
  "portal": {
178
178
  "url": "https://cdn.jsdelivr.net/ghost/portal@~{version}/umd/portal.min.js",
179
- "version": "2.28"
179
+ "version": "2.29"
180
180
  },
181
181
  "sodoSearch": {
182
182
  "url": "https://cdn.jsdelivr.net/ghost/sodo-search@~{version}/umd/sodo-search.min.js",
@@ -36,6 +36,7 @@ const ALPHA_FEATURES = [
36
36
  'urlCache',
37
37
  'migrateApp',
38
38
  'lexicalEditor',
39
+ 'lexicalMultiplayer',
39
40
  'websockets',
40
41
  'stripeAutomaticTax',
41
42
  'makingItRain'