mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-30 22:34:01 -05:00
Removed Lexical multiplayer experiment
fix https://linear.app/ghost/issue/ENG-1804/remove-lexicalmultiplayer-code - this experiment never went anywhere and we'll probably change our plan in the future, so this commit cleans up all the relevant code in this repo for the feature, in order to cut down on the code we have
This commit is contained in:
parent
6fb5047601
commit
f6d774ee93
12 changed files with 7 additions and 473 deletions
|
@ -7,10 +7,6 @@ const features = [{
|
|||
title: 'URL cache',
|
||||
description: 'Enable URL Caching',
|
||||
flag: 'urlCache'
|
||||
},{
|
||||
title: 'Lexical multiplayer',
|
||||
description: 'Enables multiplayer editing in the lexical editor.',
|
||||
flag: 'lexicalMultiplayer'
|
||||
},{
|
||||
title: 'Webmentions',
|
||||
description: 'Allows viewing received mentions on the dashboard.',
|
||||
|
|
|
@ -668,28 +668,14 @@ export default class KoenigLexicalEditor extends Component {
|
|||
return {progress, isLoading, upload, errors, filesNumber};
|
||||
};
|
||||
|
||||
// TODO: react component isn't re-rendered when its props are changed meaning we don't transition
|
||||
// to enabling multiplayer when a new post is saved and it gets an ID we can use for a YJS doc
|
||||
// - figure out how to re-render the component when its props change
|
||||
// - figure out some other mechanism for handling posts that don't really exist yet with multiplayer
|
||||
const enableMultiplayer = this.feature.lexicalMultiplayer && !cardConfig.post.isNew;
|
||||
const multiplayerWsProtocol = window.location.protocol === 'https:' ? `wss:` : `ws:`;
|
||||
const multiplayerEndpoint = multiplayerWsProtocol + window.location.host + this.ghostPaths.url.api('posts', 'multiplayer');
|
||||
const multiplayerDocId = cardConfig.post.id;
|
||||
const multiplayerUsername = this.session.user.name;
|
||||
|
||||
const KGEditorComponent = ({isInitInstance}) => {
|
||||
return (
|
||||
<div data-secondary-instance={isInitInstance ? true : false} style={isInitInstance ? {display: 'none'} : {}}>
|
||||
<KoenigComposer
|
||||
editorResource={this.editorResource}
|
||||
cardConfig={cardConfig}
|
||||
enableMultiplayer={enableMultiplayer}
|
||||
fileUploader={{useFileUpload, fileTypes}}
|
||||
initialEditorState={this.args.lexical}
|
||||
multiplayerUsername={multiplayerUsername}
|
||||
multiplayerDocId={multiplayerDocId}
|
||||
multiplayerEndpoint={multiplayerEndpoint}
|
||||
onError={this.onError}
|
||||
darkMode={this.feature.nightShift}
|
||||
isTKEnabled={true}
|
||||
|
|
|
@ -60,7 +60,6 @@ export default class FeatureService extends Service {
|
|||
|
||||
// labs flags
|
||||
@feature('urlCache') urlCache;
|
||||
@feature('lexicalMultiplayer') lexicalMultiplayer;
|
||||
@feature('audienceFeedback') audienceFeedback;
|
||||
@feature('webmentions') webmentions;
|
||||
@feature('stripeAutomaticTax') stripeAutomaticTax;
|
||||
|
|
|
@ -574,7 +574,7 @@ async function bootGhost({backend = true, frontend = true, server = true} = {})
|
|||
if (prometheusClient) {
|
||||
prometheusClient.instrumentKnex(connection);
|
||||
}
|
||||
|
||||
|
||||
const {dataService} = await initServicesForFrontend({bootLogger});
|
||||
|
||||
if (frontend) {
|
||||
|
@ -587,13 +587,6 @@ async function bootGhost({backend = true, frontend = true, server = true} = {})
|
|||
await initAppService();
|
||||
}
|
||||
|
||||
// TODO: move this to the correct place once we figure out where that is
|
||||
if (ghostServer) {
|
||||
const lexicalMultiplayer = require('./server/services/lexical-multiplayer');
|
||||
await lexicalMultiplayer.init(ghostServer);
|
||||
await lexicalMultiplayer.enable();
|
||||
}
|
||||
|
||||
await initServices({config});
|
||||
|
||||
// Gate the NestJS framework behind an env var to prevent it from being loaded (and slowing down boot)
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
module.exports = require('./service');
|
|
@ -1,141 +0,0 @@
|
|||
const debug = require('@tryghost/debug')('lexical-multiplayer'); // eslint-disable-line no-unused-vars
|
||||
const logging = require('@tryghost/logging');
|
||||
const {getSession} = require('../auth/session/express-session');
|
||||
const models = require('../../models');
|
||||
const labs = require('../../../shared/labs');
|
||||
|
||||
let wss;
|
||||
|
||||
const onSocketError = (error) => {
|
||||
logging.error(error);
|
||||
};
|
||||
|
||||
const onUnauthorized = (socket) => {
|
||||
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
||||
socket.destroy();
|
||||
};
|
||||
|
||||
const handleUpgrade = async (request, socket, head) => {
|
||||
socket.on('error', onSocketError);
|
||||
|
||||
// make sure the request is on the supported path
|
||||
// TODO: check handling of subdirectories
|
||||
if (!request.url.startsWith('/ghost/api/admin/posts/multiplayer/')) {
|
||||
socket.write('HTTP/1.1 404 Not Found\r\n\r\n');
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
// grab the session from the request
|
||||
const session = await getSession(request, {});
|
||||
if (!session || !session.user_id) {
|
||||
return onUnauthorized(socket);
|
||||
}
|
||||
|
||||
// fetch the session's user from the db
|
||||
const user = await models.User.findOne({id: session.user_id});
|
||||
if (!user) {
|
||||
return onUnauthorized(socket);
|
||||
}
|
||||
request.user = user;
|
||||
|
||||
// TODO: check if user has access to the post
|
||||
|
||||
// TODO: (elsewhere) close websocket connections on logout
|
||||
// - probably need to create a map of sockets to users?
|
||||
|
||||
socket.removeListener('error', onSocketError);
|
||||
|
||||
wss.handleUpgrade(request, socket, head, (ws) => {
|
||||
wss.emit('connection', ws, request);
|
||||
});
|
||||
};
|
||||
|
||||
let _enable;
|
||||
let _disable;
|
||||
let _isClosing = false;
|
||||
let _closePromise;
|
||||
|
||||
module.exports = {
|
||||
async init(ghostServer) {
|
||||
_enable = async () => {
|
||||
if (_isClosing) {
|
||||
logging.info('Waiting for previous Lexical multiplayer websockets service to close');
|
||||
await _closePromise;
|
||||
}
|
||||
|
||||
if (wss) {
|
||||
logging.info('Lexical multiplayer websockets service already started');
|
||||
return;
|
||||
}
|
||||
|
||||
if (labs.isSet('lexicalMultiplayer')) {
|
||||
logging.info('Starting lexical multiplayer websockets service');
|
||||
|
||||
// TODO: can we use or adapt patterns from https://github.com/HenningM/express-ws?
|
||||
const WS = require('ws');
|
||||
wss = new WS.Server({noServer: true});
|
||||
const {setupWSConnection} = require('./y-websocket');
|
||||
|
||||
wss.on('connection', (socket, request) => {
|
||||
socket.on('error', onSocketError);
|
||||
|
||||
// TODO: better method for extracting doc name from URL
|
||||
const docName = request.url.replace('/ghost/api/admin/posts/multiplayer/', '');
|
||||
setupWSConnection(socket, request, {docName});
|
||||
});
|
||||
|
||||
// TODO: this should probably be at a higher level, especially if we
|
||||
// want to support multiple websocket services
|
||||
ghostServer.httpServer.on('upgrade', handleUpgrade);
|
||||
}
|
||||
};
|
||||
|
||||
_disable = async () => {
|
||||
logging.info('Stopping lexical multiplayer websockets service');
|
||||
ghostServer.httpServer?.off('upgrade', handleUpgrade);
|
||||
|
||||
if (wss) {
|
||||
_isClosing = true;
|
||||
_closePromise = new Promise((resolve) => {
|
||||
// first sweep, soft close
|
||||
wss.clients.forEach((socket) => {
|
||||
socket.close();
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
// second sweep, hard close
|
||||
wss.clients.forEach((socket) => {
|
||||
if ([socket.OPEN, socket.CLOSING].includes(socket.readyState)) {
|
||||
socket.terminate();
|
||||
}
|
||||
});
|
||||
|
||||
resolve();
|
||||
}, 5000);
|
||||
}).finally(() => {
|
||||
wss = null;
|
||||
_isClosing = false;
|
||||
});
|
||||
|
||||
return _closePromise;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
async enable() {
|
||||
if (!_enable) {
|
||||
logging.error('Lexical multiplayer service must be initialized before it can be enabled/disabled');
|
||||
return;
|
||||
}
|
||||
return _enable();
|
||||
},
|
||||
|
||||
async disable() {
|
||||
if (!_enable) {
|
||||
logging.error('Lexical multiplayer service must be initialized before it can be enabled/disabled');
|
||||
return;
|
||||
}
|
||||
return _disable();
|
||||
}
|
||||
};
|
|
@ -1,244 +0,0 @@
|
|||
// based on https://github.com/yjs/y-websocket/blob/master/bin/utils.js
|
||||
|
||||
const Y = require('yjs');
|
||||
const syncProtocol = require('y-protocols/dist/sync.cjs');
|
||||
const awarenessProtocol = require('y-protocols/dist/awareness.cjs');
|
||||
|
||||
const encoding = require('lib0/dist/encoding.cjs');
|
||||
const decoding = require('lib0/dist/decoding.cjs');
|
||||
const map = require('lib0/dist/map.cjs');
|
||||
|
||||
const wsReadyStateConnecting = 0;
|
||||
const wsReadyStateOpen = 1;
|
||||
const wsReadyStateClosing = 2 // eslint-disable-line
|
||||
const wsReadyStateClosed = 3 // eslint-disable-line
|
||||
|
||||
/**
|
||||
* @type {Map<string,WSSharedDoc>}
|
||||
*/
|
||||
const docs = new Map();
|
||||
module.exports.docs = docs;
|
||||
|
||||
const messageSync = 0;
|
||||
const messageAwareness = 1;
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} update
|
||||
* @param {any} origin
|
||||
* @param {WSSharedDoc} doc
|
||||
*/
|
||||
const updateHandler = (update, origin, doc) => {
|
||||
const encoder = encoding.createEncoder();
|
||||
encoding.writeVarUint(encoder, messageSync);
|
||||
syncProtocol.writeUpdate(encoder, update);
|
||||
const message = encoding.toUint8Array(encoder);
|
||||
doc.conns.forEach((_, conn) => send(doc, conn, message));
|
||||
};
|
||||
|
||||
class WSSharedDoc extends Y.Doc {
|
||||
/**
|
||||
* @param {string} name
|
||||
*/
|
||||
constructor(name) {
|
||||
super({gc: true});
|
||||
this.name = name;
|
||||
/**
|
||||
* Maps from conn to set of controlled user ids. Delete all user ids from awareness when this conn is closed
|
||||
* @type {Map<Object, Set<number>>}
|
||||
*/
|
||||
this.conns = new Map();
|
||||
/**
|
||||
* @type {awarenessProtocol.Awareness}
|
||||
*/
|
||||
this.awareness = new awarenessProtocol.Awareness(this);
|
||||
this.awareness.setLocalState(null);
|
||||
/**
|
||||
* @param {{ added: Array<number>, updated: Array<number>, removed: Array<number> }} changes
|
||||
* @param {Object | null} conn Origin is the connection that made the change
|
||||
*/
|
||||
const awarenessChangeHandler = ({added, updated, removed}, conn) => {
|
||||
const changedClients = added.concat(updated, removed);
|
||||
if (conn !== null) {
|
||||
const connControlledIDs = /** @type {Set<number>} */ (this.conns.get(conn));
|
||||
if (connControlledIDs !== undefined) {
|
||||
added.forEach((clientID) => {
|
||||
connControlledIDs.add(clientID);
|
||||
});
|
||||
removed.forEach((clientID) => {
|
||||
connControlledIDs.delete(clientID);
|
||||
});
|
||||
}
|
||||
}
|
||||
// broadcast awareness update
|
||||
const encoder = encoding.createEncoder();
|
||||
encoding.writeVarUint(encoder, messageAwareness);
|
||||
encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(this.awareness, changedClients));
|
||||
const buff = encoding.toUint8Array(encoder);
|
||||
this.conns.forEach((_, c) => {
|
||||
send(this, c, buff);
|
||||
});
|
||||
};
|
||||
this.awareness.on('update', awarenessChangeHandler);
|
||||
this.on('update', updateHandler);
|
||||
// if (isCallbackSet) {
|
||||
// this.on('update', debounce(
|
||||
// callbackHandler,
|
||||
// CALLBACK_DEBOUNCE_WAIT,
|
||||
// { maxWait: CALLBACK_DEBOUNCE_MAXWAIT }
|
||||
// ))
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a Y.Doc by name, whether in memory or on disk
|
||||
*
|
||||
* @param {string} docname - the name of the Y.Doc to find or create
|
||||
* @param {boolean} gc - whether to allow gc on the doc (applies only when created)
|
||||
* @return {WSSharedDoc}
|
||||
*/
|
||||
const getYDoc = (docname, gc = true) => map.setIfUndefined(docs, docname, () => {
|
||||
const doc = new WSSharedDoc(docname);
|
||||
doc.gc = gc;
|
||||
// if (persistence !== null) {
|
||||
// persistence.bindState(docname, doc);
|
||||
// }
|
||||
docs.set(docname, doc);
|
||||
return doc;
|
||||
});
|
||||
|
||||
module.exports.getYDoc = getYDoc;
|
||||
|
||||
/**
|
||||
* @param {any} conn
|
||||
* @param {WSSharedDoc} doc
|
||||
* @param {Uint8Array} message
|
||||
*/
|
||||
const messageListener = (conn, doc, message) => {
|
||||
try {
|
||||
const encoder = encoding.createEncoder();
|
||||
const decoder = decoding.createDecoder(message);
|
||||
const messageType = decoding.readVarUint(decoder);
|
||||
switch (messageType) {
|
||||
case messageSync:
|
||||
encoding.writeVarUint(encoder, messageSync);
|
||||
syncProtocol.readSyncMessage(decoder, encoder, doc, conn);
|
||||
|
||||
// If the `encoder` only contains the type of reply message and no
|
||||
// message, there is no need to send the message. When `encoder` only
|
||||
// contains the type of reply, its length is 1.
|
||||
if (encoding.length(encoder) > 1) {
|
||||
send(doc, conn, encoding.toUint8Array(encoder));
|
||||
}
|
||||
break;
|
||||
case messageAwareness: {
|
||||
awarenessProtocol.applyAwarenessUpdate(doc.awareness, decoding.readVarUint8Array(decoder), conn);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
doc.emit('error', [err]);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {WSSharedDoc} doc
|
||||
* @param {any} conn
|
||||
*/
|
||||
const closeConn = (doc, conn) => {
|
||||
if (doc.conns.has(conn)) {
|
||||
/**
|
||||
* @type {Set<number>}
|
||||
*/
|
||||
const controlledIds = doc.conns.get(conn);
|
||||
doc.conns.delete(conn);
|
||||
awarenessProtocol.removeAwarenessStates(doc.awareness, Array.from(controlledIds), null);
|
||||
if (doc.conns.size === 0/* && persistence !== null*/) {
|
||||
// if persisted, we store state and destroy ydocument
|
||||
// persistence.writeState(doc.name, doc).then(() => {
|
||||
// doc.destroy();
|
||||
// });
|
||||
docs.delete(doc.name);
|
||||
}
|
||||
}
|
||||
conn.close();
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {WSSharedDoc} doc
|
||||
* @param {any} conn
|
||||
* @param {Uint8Array} m
|
||||
*/
|
||||
const send = (doc, conn, m) => {
|
||||
if (conn.readyState !== wsReadyStateConnecting && conn.readyState !== wsReadyStateOpen) {
|
||||
closeConn(doc, conn);
|
||||
}
|
||||
try {
|
||||
conn.send(m, (err) => {
|
||||
if (err !== null && err !== undefined) {
|
||||
closeConn(doc, conn);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
closeConn(doc, conn);
|
||||
}
|
||||
};
|
||||
|
||||
const pingTimeout = 30000;
|
||||
|
||||
/**
|
||||
* @param {any} conn
|
||||
* @param {any} req
|
||||
* @param {any} opts
|
||||
*/
|
||||
module.exports.setupWSConnection = (conn, req, {docName = req.url.slice(1).split('?')[0], gc = true} = {}) => {
|
||||
conn.binaryType = 'arraybuffer';
|
||||
// get doc, initialize if it does not exist yet
|
||||
const doc = getYDoc(docName, gc);
|
||||
doc.conns.set(conn, new Set());
|
||||
// listen and reply to events
|
||||
conn.on('message', /** @param {ArrayBuffer} message */ message => messageListener(conn, doc, new Uint8Array(message)));
|
||||
|
||||
// Check if connection is still alive
|
||||
let pongReceived = true;
|
||||
const pingInterval = setInterval(() => {
|
||||
if (!pongReceived) {
|
||||
if (doc.conns.has(conn)) {
|
||||
closeConn(doc, conn);
|
||||
}
|
||||
clearInterval(pingInterval);
|
||||
} else if (doc.conns.has(conn)) {
|
||||
pongReceived = false;
|
||||
try {
|
||||
conn.ping();
|
||||
} catch (e) {
|
||||
closeConn(doc, conn);
|
||||
clearInterval(pingInterval);
|
||||
}
|
||||
}
|
||||
}, pingTimeout);
|
||||
conn.on('close', () => {
|
||||
closeConn(doc, conn);
|
||||
clearInterval(pingInterval);
|
||||
});
|
||||
conn.on('pong', () => {
|
||||
pongReceived = true;
|
||||
});
|
||||
// put the following in a variables in a block so the interval handlers don't keep in in
|
||||
// scope
|
||||
{
|
||||
// send sync step 1
|
||||
const syncEncoder = encoding.createEncoder();
|
||||
encoding.writeVarUint(syncEncoder, messageSync);
|
||||
syncProtocol.writeSyncStep1(syncEncoder, doc);
|
||||
send(doc, conn, encoding.toUint8Array(syncEncoder));
|
||||
|
||||
const awarenessStates = doc.awareness.getStates();
|
||||
if (awarenessStates.size > 0) {
|
||||
const awarenessEncoder = encoding.createEncoder();
|
||||
encoding.writeVarUint(awarenessEncoder, messageAwareness);
|
||||
encoding.writeVarUint8Array(awarenessEncoder, awarenessProtocol.encodeAwarenessUpdate(doc.awareness, Array.from(awarenessStates.keys())));
|
||||
send(doc, conn, encoding.toUint8Array(awarenessEncoder));
|
||||
}
|
||||
}
|
||||
};
|
|
@ -227,21 +227,6 @@ class SettingsBREADService {
|
|||
const {filteredSettings: refilteredSettings, emailsToVerify} = await this.prepSettingsForEmailVerification(filteredSettings, getSetting);
|
||||
|
||||
const modelArray = await this.SettingsModel.edit(refilteredSettings, options).then((result) => {
|
||||
// TODO: temporary fix for starting/stopping lexicalMultiplayer service when labs flag is changed
|
||||
// this should be removed along with the flag, or set up in a more generic way
|
||||
const labsSetting = result.find(setting => setting.get('key') === 'labs');
|
||||
if (labsSetting) {
|
||||
const lexicalMultiplayer = require('../lexical-multiplayer');
|
||||
const previous = JSON.parse(labsSetting.previousAttributes().value);
|
||||
const current = JSON.parse(labsSetting.get('value'));
|
||||
|
||||
if (!previous.lexicalMultiplayer && current.lexicalMultiplayer) {
|
||||
lexicalMultiplayer.enable();
|
||||
} else if (previous.lexicalMultiplayer && !current.lexicalMultiplayer) {
|
||||
lexicalMultiplayer.disable();
|
||||
}
|
||||
}
|
||||
|
||||
return this._formatBrowse(_.keyBy(_.invokeMap(result, 'toJSON'), 'key'), options.context);
|
||||
});
|
||||
|
||||
|
|
|
@ -39,7 +39,6 @@ const BETA_FEATURES = [
|
|||
const ALPHA_FEATURES = [
|
||||
'NestPlayground',
|
||||
'urlCache',
|
||||
'lexicalMultiplayer',
|
||||
'emailCustomization',
|
||||
'mailEvents',
|
||||
'collectionsCard',
|
||||
|
|
|
@ -204,7 +204,6 @@
|
|||
"keypair": "1.0.4",
|
||||
"knex": "2.4.2",
|
||||
"knex-migrator": "5.2.1",
|
||||
"lib0": "0.2.94",
|
||||
"lodash": "4.17.21",
|
||||
"luxon": "3.5.0",
|
||||
"moment": "2.24.0",
|
||||
|
@ -221,10 +220,7 @@
|
|||
"stoppable": "1.1.0",
|
||||
"superagent": "5.1.0",
|
||||
"superagent-throttle": "1.0.1",
|
||||
"ws": "8.18.0",
|
||||
"xml": "1.0.1",
|
||||
"y-protocols": "1.0.6",
|
||||
"yjs": "13.6.20"
|
||||
"xml": "1.0.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@sentry/profiling-node": "7.119.2",
|
||||
|
|
|
@ -25,7 +25,6 @@ Object {
|
|||
"i18n": true,
|
||||
"importMemberTier": true,
|
||||
"lexicalIndicators": true,
|
||||
"lexicalMultiplayer": true,
|
||||
"mailEvents": true,
|
||||
"members": true,
|
||||
"newEmailAddresses": true,
|
||||
|
|
43
yarn.lock
43
yarn.lock
|
@ -20492,11 +20492,6 @@ isobject@^3.0.0, isobject@^3.0.1:
|
|||
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
|
||||
integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==
|
||||
|
||||
isomorphic.js@^0.2.4:
|
||||
version "0.2.5"
|
||||
resolved "https://registry.yarnpkg.com/isomorphic.js/-/isomorphic.js-0.2.5.tgz#13eecf36f2dba53e85d355e11bf9d4208c6f7f88"
|
||||
integrity sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==
|
||||
|
||||
isostring@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/isostring/-/isostring-0.0.1.tgz#ddb608efbfc89cda86db9cb16be090a788134c7f"
|
||||
|
@ -21824,20 +21819,6 @@ lexical@0.13.1:
|
|||
resolved "https://registry.yarnpkg.com/lexical/-/lexical-0.13.1.tgz#0abffe9bc05a7a9da8a6128ea478bf08c11654db"
|
||||
integrity sha512-jaqRYzVEfBKbX4FwYpd/g+MyOjRaraAel0iQsTrwvx3hyN0bswUZuzb6H6nGlFSjcdrc77wKpyKwoWj4aUd+Bw==
|
||||
|
||||
lib0@0.2.94:
|
||||
version "0.2.94"
|
||||
resolved "https://registry.yarnpkg.com/lib0/-/lib0-0.2.94.tgz#fc28b4b65f816599f1e2f59d3401e231709535b3"
|
||||
integrity sha512-hZ3p54jL4Wpu7IOg26uC7dnEWiMyNlUrb9KoG7+xYs45WkQwpVvKFndVq2+pqLYKe1u8Fp3+zAfZHVvTK34PvQ==
|
||||
dependencies:
|
||||
isomorphic.js "^0.2.4"
|
||||
|
||||
lib0@^0.2.85, lib0@^0.2.98:
|
||||
version "0.2.98"
|
||||
resolved "https://registry.yarnpkg.com/lib0/-/lib0-0.2.98.tgz#fe55203b8586512c1837248d5f309d7dfd566f5d"
|
||||
integrity sha512-XteTiNO0qEXqqweWx+b21p/fBnNHUA1NwAtJNJek1oPrewEZs2uiT4gWivHKr9GqCjDPAhchz0UQO8NwU3bBNA==
|
||||
dependencies:
|
||||
isomorphic.js "^0.2.4"
|
||||
|
||||
lie@3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e"
|
||||
|
@ -31913,11 +31894,6 @@ write-file-atomic@^5.0.1:
|
|||
imurmurhash "^0.1.4"
|
||||
signal-exit "^4.0.1"
|
||||
|
||||
ws@8.18.0, ws@^8.18.0, ws@^8.2.3:
|
||||
version "8.18.0"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc"
|
||||
integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==
|
||||
|
||||
ws@^6.1.0:
|
||||
version "6.2.2"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.2.tgz#dd5cdbd57a9979916097652d78f1cc5faea0c32e"
|
||||
|
@ -31930,6 +31906,11 @@ ws@^7.4.6:
|
|||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591"
|
||||
integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==
|
||||
|
||||
ws@^8.18.0, ws@^8.2.3:
|
||||
version "8.18.0"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc"
|
||||
integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==
|
||||
|
||||
ws@~8.11.0:
|
||||
version "8.11.0"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143"
|
||||
|
@ -31995,13 +31976,6 @@ xtend@~3.0.0:
|
|||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-3.0.0.tgz#5cce7407baf642cba7becda568111c493f59665a"
|
||||
integrity sha512-sp/sT9OALMjRW1fKDlPeuSZlDQpkqReA0pyJukniWbTGoEKefHxhGJynE3PNhUMlcM8qWIjPwecwCw4LArS5Eg==
|
||||
|
||||
y-protocols@1.0.6:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/y-protocols/-/y-protocols-1.0.6.tgz#66dad8a95752623443e8e28c0e923682d2c0d495"
|
||||
integrity sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==
|
||||
dependencies:
|
||||
lib0 "^0.2.85"
|
||||
|
||||
y18n@^4.0.0:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"
|
||||
|
@ -32104,13 +32078,6 @@ yauzl@^2.10.0:
|
|||
buffer-crc32 "~0.2.3"
|
||||
fd-slicer "~1.1.0"
|
||||
|
||||
yjs@13.6.20:
|
||||
version "13.6.20"
|
||||
resolved "https://registry.yarnpkg.com/yjs/-/yjs-13.6.20.tgz#da878412688f107dc03faa4fc3cff37736fe5dfa"
|
||||
integrity sha512-Z2YZI+SYqK7XdWlloI3lhMiKnCdFCVC4PchpdO+mCYwtiTwncjUbnRK9R1JmkNfdmHyDXuWN3ibJAt0wsqTbLQ==
|
||||
dependencies:
|
||||
lib0 "^0.2.98"
|
||||
|
||||
yn@3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
|
||||
|
|
Loading…
Reference in a new issue