From 2412b13fd8fee648d9f39cb841fc72a66d7bbfe0 Mon Sep 17 00:00:00 2001 From: pubuzhixing8 Date: Thu, 1 Aug 2024 17:40:10 +0800 Subject: [PATCH 01/12] feat: init y-websocket in app and utils in backend --- backend/src/index.ts | 4 +- backend/src/utils.ts | 314 +++++++++++++++ packages/grid/src/core/action/general.ts | 4 +- src/app/share/provider.ts | 3 +- src/app/share/y-websocket.ts | 485 +++++++++++++++++++++++ tsconfig.json | 4 +- 6 files changed, 807 insertions(+), 7 deletions(-) create mode 100644 backend/src/utils.ts create mode 100644 src/app/share/y-websocket.ts diff --git a/backend/src/index.ts b/backend/src/index.ts index 35cc6ff5..3a27fc48 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,9 +1,7 @@ import * as Y from 'yjs'; import * as WebSocket from 'ws'; import http, { Server } from 'http'; -const ywsUtils = require('y-websocket/bin/utils'); -const setupWSConnection = ywsUtils.setupWSConnection; -const docs = ywsUtils.docs as Map }>; +import { docs, setupWSConnection } from './utils'; const port = (process.env['PORT'] || 3000) as number; const server: Server = http.createServer((request, response) => { diff --git a/backend/src/utils.ts b/backend/src/utils.ts new file mode 100644 index 00000000..b03e55cf --- /dev/null +++ b/backend/src/utils.ts @@ -0,0 +1,314 @@ +import * as Y from 'yjs'; +const syncProtocol = require('y-protocols/sync'); +const awarenessProtocol = require('y-protocols/awareness'); + +const encoding = require('lib0/encoding'); +const decoding = require('lib0/decoding'); +const map = require('lib0/map'); + +const debounce = require('lodash.debounce'); + +// const callbackHandler = require('./callback.cjs').callbackHandler; +// const isCallbackSet = require('./callback.cjs').isCallbackSet; + +const CALLBACK_DEBOUNCE_WAIT = parseInt(process.env['CALLBACK_DEBOUNCE_WAIT'] || '2000'); +const CALLBACK_DEBOUNCE_MAXWAIT = parseInt(process.env['CALLBACK_DEBOUNCE_MAXWAIT'] || '10000'); + +const wsReadyStateConnecting = 0; +const wsReadyStateOpen = 1; +const wsReadyStateClosing = 2; // eslint-disable-line +const wsReadyStateClosed = 3; // eslint-disable-line + +// disable gc when using snapshots! +const gcEnabled = process.env['GC'] !== 'false' && process.env['GC'] !== '0'; +const persistenceDir = process.env['YPERSISTENCE']; +/** + * @type {{bindState: function(string,WSSharedDoc):void, writeState:function(string,WSSharedDoc):Promise, provider: any}|null} + */ +let persistence = null; +if (typeof persistenceDir === 'string') { + console.info('Persisting documents to "' + persistenceDir + '"'); + // @ts-ignore + const LeveldbPersistence = require('y-leveldb').LeveldbPersistence; + const ldb = new LeveldbPersistence(persistenceDir); + persistence = { + provider: ldb, + bindState: async (docName, ydoc) => { + const persistedYdoc = await ldb.getYDoc(docName); + const newUpdates = Y.encodeStateAsUpdate(ydoc); + ldb.storeUpdate(docName, newUpdates); + Y.applyUpdate(ydoc, Y.encodeStateAsUpdate(persistedYdoc)); + ydoc.on('update', (update) => { + ldb.storeUpdate(docName, update); + }); + }, + writeState: async (_docName, _ydoc) => {} + }; +} + +/** + * @param {{bindState: function(string,WSSharedDoc):void, + * writeState:function(string,WSSharedDoc):Promise,provider:any}|null} persistence_ + */ +exports.setPersistence = (persistence_) => { + persistence = persistence_; +}; + +/** + * @return {null|{bindState: function(string,WSSharedDoc):void, + * writeState:function(string,WSSharedDoc):Promise}|null} used persistence layer + */ +exports.getPersistence = () => persistence; + +/** + * @type {Map} + */ +export const docs = new Map(); + +const messageSync = 0; +const messageAwareness = 1; +// const messageAuth = 2 + +/** + * @param {Uint8Array} update + * @param {any} _origin + * @param {WSSharedDoc} doc + * @param {any} _tr + */ +const updateHandler = (update, _origin, doc, _tr) => { + 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)); +}; + +/** + * @type {(ydoc: Y.Doc) => Promise} + */ +let contentInitializor = (_ydoc) => Promise.resolve(); + +/** + * This function is called once every time a Yjs document is created. You can + * use it to pull data from an external source or initialize content. + * + * @param {(ydoc: Y.Doc) => Promise} f + */ +exports.setContentInitializor = (f) => { + contentInitializor = f; +}; + +class WSSharedDoc extends Y.Doc { + name: any; + conns: any; + awareness: any; + whenInitialized: any; + /** + * @param {string} name + */ + constructor(name) { + super({ gc: gcEnabled }); + 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>} + */ + this.conns = new Map(); + /** + * @type {awarenessProtocol.Awareness} + */ + this.awareness = new awarenessProtocol.Awareness(this); + this.awareness.setLocalState(null); + /** + * @param {{ added: Array, updated: Array, removed: Array }} 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} */ 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', /** @type {any} */ updateHandler); + // if (isCallbackSet) { + // this.on('update', /** @type {any} */ debounce(callbackHandler, CALLBACK_DEBOUNCE_WAIT, { maxWait: CALLBACK_DEBOUNCE_MAXWAIT })); + // } + this.whenInitialized = contentInitializor(this); + } +} + +exports.WSSharedDoc = WSSharedDoc; + +/** + * 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; + }); + +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) { + console.error(err); + // @ts-ignore + doc.emit('error', [err]); + } +}; + +/** + * @param {WSSharedDoc} doc + * @param {any} conn + */ +const closeConn = (doc, conn) => { + if (doc.conns.has(conn)) { + /** + * @type {Set} + */ + // @ts-ignore + 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 {import('ws').WebSocket} conn + * @param {Uint8Array} m + */ +const send = (doc, conn, m) => { + if (conn.readyState !== wsReadyStateConnecting && conn.readyState !== wsReadyStateOpen) { + closeConn(doc, conn); + } + try { + conn.send(m, {}, (err) => { + err != null && closeConn(doc, conn); + }); + } catch (e) { + closeConn(doc, conn); + } +}; + +const pingTimeout = 30000; + +/** + * @param {import('ws').WebSocket} conn + * @param {import('http').IncomingMessage} req + * @param {any} opts + */ +export const 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 encoder = encoding.createEncoder(); + encoding.writeVarUint(encoder, messageSync); + syncProtocol.writeSyncStep1(encoder, doc); + send(doc, conn, encoding.toUint8Array(encoder)); + const awarenessStates = doc.awareness.getStates(); + if (awarenessStates.size > 0) { + const encoder = encoding.createEncoder(); + encoding.writeVarUint(encoder, messageAwareness); + encoding.writeVarUint8Array( + encoder, + awarenessProtocol.encodeAwarenessUpdate(doc.awareness, Array.from(awarenessStates.keys())) + ); + send(doc, conn, encoding.toUint8Array(encoder)); + } + } +}; diff --git a/packages/grid/src/core/action/general.ts b/packages/grid/src/core/action/general.ts index b4149464..82e5d027 100644 --- a/packages/grid/src/core/action/general.ts +++ b/packages/grid/src/core/action/general.ts @@ -39,7 +39,7 @@ const apply = (aiTable: AITable, records: AITableRecords, fields: AITableFields, } case ActionName.MoveRecord: { if (isPathEqual(options.path, options.newPath)) { - return; + // return; } const record = records[options.path[0]]; records.splice(options.path[0], 1); @@ -48,7 +48,7 @@ const apply = (aiTable: AITable, records: AITableRecords, fields: AITableFields, } case ActionName.MoveField: { if (isPathEqual(options.path, options.newPath)) { - return; + // return; } const field = fields[options.path[0]]; fields.splice(options.path[0], 1); diff --git a/src/app/share/provider.ts b/src/app/share/provider.ts index 6a585655..6ba5799d 100644 --- a/src/app/share/provider.ts +++ b/src/app/share/provider.ts @@ -1,5 +1,6 @@ -import { WebsocketProvider } from 'y-websocket'; +// import { WebsocketProvider } from 'y-websocket'; import * as Y from 'yjs'; +import { WebsocketProvider } from './y-websocket'; export const getProvider = (doc: Y.Doc, room: string, isDev: boolean) => { // 在线地址:wss://demos.yjs.dev/ws diff --git a/src/app/share/y-websocket.ts b/src/app/share/y-websocket.ts new file mode 100644 index 00000000..61d7cc2a --- /dev/null +++ b/src/app/share/y-websocket.ts @@ -0,0 +1,485 @@ +/* +Unlike stated in the LICENSE file, it is not necessary to include the copyright notice and permission notice when you copy code from this file. +*/ + +/** + * @module provider/websocket + */ + +/* eslint-env browser */ +import * as Y from 'yjs'; // eslint-disable-line +import * as bc from 'lib0/broadcastchannel'; +import * as time from 'lib0/time'; +import * as encoding from 'lib0/encoding'; +import * as decoding from 'lib0/decoding'; +import * as syncProtocol from 'y-protocols/sync'; +import * as authProtocol from 'y-protocols/auth'; +import * as awarenessProtocol from 'y-protocols/awareness'; +import * as mutex from 'lib0/mutex'; +import { Observable } from 'lib0/observable'; +import * as math from 'lib0/math'; +import * as url from 'lib0/url'; +import { Observable as RxjsObservable } from 'rxjs'; + +const messageSync = 0; +const messageQueryAwareness = 3; +const messageAwareness = 1; +const messageAuth = 2; + +/** + * encoder, decoder, provider, emitSynced, messageType + * @type {Array} + */ +const messageHandlers = []; + +messageHandlers[messageSync] = (encoder, decoder, provider, emitSynced, messageType) => { + encoding.writeVarUint(encoder, messageSync); + const syncMessageType = syncProtocol.readSyncMessage(decoder, encoder, provider.doc, provider); + if (emitSynced && syncMessageType === syncProtocol.messageYjsSyncStep2 && !provider.synced) { + provider.synced = true; + } +}; + +messageHandlers[messageQueryAwareness] = (encoder, decoder, provider, emitSynced, messageType) => { + encoding.writeVarUint(encoder, messageAwareness); + encoding.writeVarUint8Array( + encoder, + awarenessProtocol.encodeAwarenessUpdate(provider.awareness, Array.from(provider.awareness.getStates().keys())) + ); +}; + +messageHandlers[messageAwareness] = (encoder, decoder, provider, emitSynced, messageType) => { + awarenessProtocol.applyAwarenessUpdate(provider.awareness, decoding.readVarUint8Array(decoder), provider); +}; + +messageHandlers[messageAuth] = (encoder, decoder, provider, emitSynced, messageType) => { + authProtocol.readAuthMessage(decoder, provider.doc, (doc, reason) => { + permissionDeniedHandler(provider, reason); + }); +}; + +const reconnectTimeoutBase = 1200; +const maxReconnectTimeout = 12000; +// @todo - this should depend on awareness.outdatedTime +const messageReconnectTimeout = 60000; + +/** + * @param {WebsocketProvider} provider + * @param {string} reason + */ +const permissionDeniedHandler = (provider: WebsocketProvider, reason) => { + console.warn(`Permission denied to access ${provider.url}.\n${reason}`); +}; + +/** + * @param {WebsocketProvider} provider + * @param {Uint8Array} buf + * @param {boolean} emitSynced + * @return {encoding.Encoder} + */ +const readMessage = (provider, buf, emitSynced) => { + const decoder = decoding.createDecoder(buf); + const encoder = encoding.createEncoder(); + const messageType = decoding.readVarUint(decoder); + const messageHandler = provider.messageHandlers[messageType]; + if (/** @type {any} */ messageHandler) { + messageHandler(encoder, decoder, provider, emitSynced, messageType); + } else { + console.error('Unable to compute message'); + } + return encoder; +}; + +/** + * @param {WebsocketProvider} provider + */ +const setupWS = (provider) => { + if (provider.shouldConnect && provider.ws === null) { + const websocket = new provider._WS(provider.url); + websocket.binaryType = 'arraybuffer'; + provider.ws = websocket; + provider.wsconnecting = true; + provider.wsconnected = false; + provider.synced = false; + + websocket.onmessage = (event) => { + const { data } = event; + if (typeof data === 'object') { + provider.wsLastMessageReceived = time.getUnixTime(); + const encoder = readMessage(provider, new Uint8Array(event.data), true); + if (encoding.length(encoder) > 1) { + websocket.send(encoding.toUint8Array(encoder)); + } + } + }; + websocket.onclose = () => { + provider.ws = null; + provider.wsconnecting = false; + if (provider.wsconnected) { + provider.wsconnected = false; + provider.synced = false; + // update awareness (all users except local left) + awarenessProtocol.removeAwarenessStates( + provider.awareness, + Array.from(provider.awareness.getStates().keys() as number[]).filter( + (client: number) => client !== provider.doc.clientID + ), + provider + ); + provider.emit('status', [ + { + status: 'disconnected' + } + ]); + } else { + provider.wsUnsuccessfulReconnects++; + } + // Start with no reconnect timeout and increase timeout by + // log10(wsUnsuccessfulReconnects). + // The idea is to increase reconnect timeout slowly and have no reconnect + // timeout at the beginning (log(1) = 0) + setTimeout( + setupWS, + math.min(math.log10(provider.wsUnsuccessfulReconnects + 1) * reconnectTimeoutBase, maxReconnectTimeout), + provider + ); + }; + websocket.onopen = () => { + provider.wsLastMessageReceived = time.getUnixTime(); + provider.wsconnecting = false; + provider.wsconnected = true; + provider.wsUnsuccessfulReconnects = 0; + provider.emit('status', [ + { + status: 'connected' + } + ]); + // sync ydoc + syncYDoc(provider, websocket); + // sync local awareness state + syncAwareness(provider, websocket); + // resend step1 and awareness + const _syncInterval = setInterval(() => { + if (!provider.synced && provider.wsconnected) { + syncYDoc(provider, websocket); + syncAwareness(provider, websocket); + } else { + clearInterval(_syncInterval); + } + }, 1000); + }; + + provider.emit('status', [ + { + status: 'connecting' + } + ]); + } +}; + +const syncYDoc = (provider, websocket) => { + // always send sync step 1 when connected + const encoder = encoding.createEncoder(); + encoding.writeVarUint(encoder, messageSync); + syncProtocol.writeSyncStep1(encoder, provider.doc); + websocket.send(encoding.toUint8Array(encoder)); +}; + +const syncAwareness = (provider, websocket) => { + if (provider.awareness.getLocalState() !== null) { + const encoderAwarenessState = encoding.createEncoder(); + encoding.writeVarUint(encoderAwarenessState, messageAwareness); + encoding.writeVarUint8Array( + encoderAwarenessState, + awarenessProtocol.encodeAwarenessUpdate(provider.awareness, [provider.doc.clientID]) + ); + websocket.send(encoding.toUint8Array(encoderAwarenessState)); + } +}; + +/** + * @param {WebsocketProvider} provider + * @param {ArrayBuffer} buf + */ +const broadcastMessage = (provider, buf) => { + if (provider.wsconnected) { + /** @type {WebSocket} */ provider.ws.send(buf); + } + if (provider.bcconnected) { + provider.mux(() => { + bc.publish(provider.bcChannel, buf); + }); + } +}; + +/** + * Websocket Provider for Yjs. Creates a websocket connection to sync the shared document. + * The document name is attached to the provided url. I.e. the following example + * creates a websocket connection to http://localhost:1234/my-document-name + * + * @example + * import * as Y from 'yjs' + * import { WebsocketProvider } from 'y-websocket' + * const doc = new Y.Doc() + * const provider = new WebsocketProvider('http://localhost:1234', 'my-document-name', doc) + * + * @extends {Observable} + */ +export class WebsocketProvider extends Observable { + bcChannel: string; + roomname: any; + serverUrl: string; + doc: any; + params: any; + _WS: { + new (url: string, protocols?: string | string[]): WebSocket; + prototype: WebSocket; + readonly CLOSED: number; + readonly CLOSING: number; + readonly CONNECTING: number; + readonly OPEN: number; + }; + awareness: awarenessProtocol.Awareness; + wsconnected: boolean; + wsconnecting: boolean; + bcconnected: boolean; + wsUnsuccessfulReconnects: number; + messageHandlers: any[]; + mux: mutex.mutex; + _synced: boolean; + ws: any; + wsLastMessageReceived: number; + shouldConnect: boolean; + _resyncInterval: number; + _bcSubscriber: (data: any) => void; + _updateHandler: (update: any, origin: any) => void; + _awarenessUpdateHandler: ({ added, updated, removed }: { added: any; updated: any; removed: any }, origin: any) => void; + _beforeUnloadHandler: () => void; + _checkInterval: any; + /** + * @param {string} serverUrl + * @param {string} roomname + * @param {Y.Doc} doc + * @param {object} [opts] + * @param {boolean} [opts.connect] + * @param {awarenessProtocol.Awareness} [opts.awareness] + * @param {Object} [opts.params] + * @param {typeof WebSocket} [opts.WebSocketPolyfill] Optionall provide a WebSocket polyfill + * @param {number} [opts.resyncInterval] Request server state every `resyncInterval` milliseconds + */ + constructor( + serverUrl, + roomname, + doc, + { + connect = true, + awareness = new awarenessProtocol.Awareness(doc), + params = {}, + WebSocketPolyfill = WebSocket, + resyncInterval = -1 + } = {} + ) { + super(); + // ensure that url is always ends with / + while (serverUrl[serverUrl.length - 1] === '/') { + serverUrl = serverUrl.slice(0, serverUrl.length - 1); + } + this.roomname = roomname; + this.serverUrl = serverUrl; + this.bcChannel = serverUrl + '/' + roomname; + this.doc = doc; + this.params = params + this._WS = WebSocketPolyfill; + this.awareness = awareness; + this.wsconnected = false; + this.wsconnecting = false; + this.bcconnected = false; + this.wsUnsuccessfulReconnects = 0; + this.messageHandlers = messageHandlers.slice(); + this.mux = mutex.createMutex(); + /** + * @type {boolean} + */ + this._synced = false; + /** + * @type {WebSocket?} + */ + this.ws = null; + this.wsLastMessageReceived = 0; + /** + * Whether to connect to other peers or not + * @type {boolean} + */ + this.shouldConnect = connect; + + /** + * @type {number} + */ + this._resyncInterval = 0; + if (resyncInterval > 0) { + this._resyncInterval = /** @type {any} */ setInterval(() => { + if (this.ws) { + // resend sync step 1 + const encoder = encoding.createEncoder(); + encoding.writeVarUint(encoder, messageSync); + + syncProtocol.writeSyncStep1(encoder, doc); + this.ws.send(encoding.toUint8Array(encoder)); + } + }, resyncInterval) as any; + } + + /** + * @param {ArrayBuffer} data + */ + this._bcSubscriber = (data) => { + this.mux(() => { + const encoder = readMessage(this, new Uint8Array(data), false); + if (encoding.length(encoder) > 1) { + bc.publish(this.bcChannel, encoding.toUint8Array(encoder)); + } + }); + }; + /** + * Listens to Yjs updates and sends them to remote peers (ws and broadcastchannel) + * @param {Uint8Array} update + * @param {any} origin + */ + this._updateHandler = (update, origin) => { + if (origin !== this) { + const encoder = encoding.createEncoder(); + encoding.writeVarUint(encoder, messageSync); + syncProtocol.writeUpdate(encoder, update); + broadcastMessage(this, encoding.toUint8Array(encoder)); + } + }; + this.doc.on('update', this._updateHandler); + /** + * @param {any} changed + * @param {any} origin + */ + this._awarenessUpdateHandler = ({ added, updated, removed }, origin) => { + const changedClients = added.concat(updated).concat(removed); + const encoder = encoding.createEncoder(); + encoding.writeVarUint(encoder, messageAwareness); + encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(awareness, changedClients)); + broadcastMessage(this, encoding.toUint8Array(encoder)); + }; + this._beforeUnloadHandler = () => { + awarenessProtocol.removeAwarenessStates(this.awareness, [doc.clientID], 'window unload'); + }; + // if (typeof window !== 'undefined') { + // window.addEventListener('beforeunload', this._beforeUnloadHandler); + // } else if (typeof process !== 'undefined') { + // process.on('exit', () => this._beforeUnloadHandler); + // } + awareness.on('update', this._awarenessUpdateHandler); + this._checkInterval = /** @type {any} */ setInterval(() => { + if (this.wsconnected && messageReconnectTimeout < time.getUnixTime() - this.wsLastMessageReceived) { + // no message received in a long time - not even your own awareness + // updates (which are updated every 15 seconds) + /** @type {WebSocket} */ this.ws.close(); + } + }, messageReconnectTimeout / 10); + if (connect) { + this.connect(); + } + } + + get url() { + const encodedParams = url.encodeQueryParams(this.params); + return this.serverUrl + '/' + this.roomname + (encodedParams.length === 0 ? '' : '?' + encodedParams); + } + + /** + * @type {boolean} + */ + get synced() { + return this._synced; + } + + set synced(state) { + if (this._synced !== state) { + this._synced = state; + this.emit('synced', [state]); + this.emit('sync', [state]); + } + } + + override destroy() { + if (this._resyncInterval !== 0) { + clearInterval(this._resyncInterval); + } + clearInterval(this._checkInterval); + this.disconnect(); + // if (typeof window !== 'undefined') { + // window.removeEventListener('beforeunload', this._beforeUnloadHandler); + // } else if (typeof process !== 'undefined') { + // process.off('exit', () => this._beforeUnloadHandler); + // } + this.awareness.off('update', this._awarenessUpdateHandler); + this.doc.off('update', this._updateHandler); + this.doc.destroy(); + super.destroy(); + } + + connectBc() { + if (!this.bcconnected) { + bc.subscribe(this.bcChannel, this._bcSubscriber); + this.bcconnected = true; + } + // send sync step1 to bc + this.mux(() => { + // write sync step 1 + const encoderSync = encoding.createEncoder(); + encoding.writeVarUint(encoderSync, messageSync); + syncProtocol.writeSyncStep1(encoderSync, this.doc); + bc.publish(this.bcChannel, encoding.toUint8Array(encoderSync)); + // broadcast local state + const encoderState = encoding.createEncoder(); + encoding.writeVarUint(encoderState, messageSync); + syncProtocol.writeSyncStep2(encoderState, this.doc); + bc.publish(this.bcChannel, encoding.toUint8Array(encoderState)); + // write queryAwareness + const encoderAwarenessQuery = encoding.createEncoder(); + encoding.writeVarUint(encoderAwarenessQuery, messageQueryAwareness); + bc.publish(this.bcChannel, encoding.toUint8Array(encoderAwarenessQuery)); + // broadcast local awareness state + const encoderAwarenessState = encoding.createEncoder(); + encoding.writeVarUint(encoderAwarenessState, messageAwareness); + encoding.writeVarUint8Array( + encoderAwarenessState, + awarenessProtocol.encodeAwarenessUpdate(this.awareness, [this.doc.clientID]) + ); + bc.publish(this.bcChannel, encoding.toUint8Array(encoderAwarenessState)); + }); + } + + disconnectBc() { + // broadcast message with local awareness state set to null (indicating disconnect) + const encoder = encoding.createEncoder(); + encoding.writeVarUint(encoder, messageAwareness); + encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(this.awareness, [this.doc.clientID], new Map())); + broadcastMessage(this, encoding.toUint8Array(encoder)); + if (this.bcconnected) { + bc.unsubscribe(this.bcChannel, this._bcSubscriber); + this.bcconnected = false; + } + } + + disconnect() { + this.shouldConnect = false; + this.disconnectBc(); + if (this.ws !== null) { + this.ws.close(); + } + } + + connect() { + this.shouldConnect = true; + if (!this.wsconnected && this.ws === null) { + setupWS(this); + this.connectBc(); + } + } +} diff --git a/tsconfig.json b/tsconfig.json index 9425457b..a57f312b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,9 @@ "compileOnSave": false, "compilerOptions": { "outDir": "./dist/out-tsc", - "strict": true, + "strict": false, // 关闭所有严格类型检查选项 + "noImplicitAny": false, // 允许隐式的 any 类型 + "strictPropertyInitialization": false, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, From ae343e7507a994f2059293b2a1278110b8cd3a13 Mon Sep 17 00:00:00 2001 From: pubuzhixing8 Date: Fri, 2 Aug 2024 13:43:01 +0800 Subject: [PATCH 02/12] feat: improve shared type initialization --- backend/src/utils.ts | 2 +- src/app/share/apply-to-yjs/add-record.ts | 1 - src/app/share/y-websocket.ts | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/src/utils.ts b/backend/src/utils.ts index b03e55cf..900c7fb0 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -181,7 +181,7 @@ exports.getYDoc = getYDoc; * @param {WSSharedDoc} doc * @param {Uint8Array} message */ -const messageListener = (conn, doc, message) => { +const messageListener = (conn, doc: WSSharedDoc, message) => { try { const encoder = encoding.createEncoder(); const decoder = decoding.createDecoder(message); diff --git a/src/app/share/apply-to-yjs/add-record.ts b/src/app/share/apply-to-yjs/add-record.ts index 0c905f73..4a77790e 100644 --- a/src/app/share/apply-to-yjs/add-record.ts +++ b/src/app/share/apply-to-yjs/add-record.ts @@ -8,6 +8,5 @@ export default function addRecord(sharedType: SharedType, action: AddRecordActio const path = action.path[0]; records.insert(path, [toRecordSyncElement(action.record as DemoAIRecord)]); } - return sharedType; } diff --git a/src/app/share/y-websocket.ts b/src/app/share/y-websocket.ts index 61d7cc2a..5f471b39 100644 --- a/src/app/share/y-websocket.ts +++ b/src/app/share/y-websocket.ts @@ -288,7 +288,7 @@ export class WebsocketProvider extends Observable { this.serverUrl = serverUrl; this.bcChannel = serverUrl + '/' + roomname; this.doc = doc; - this.params = params + this.params = params; this._WS = WebSocketPolyfill; this.awareness = awareness; this.wsconnected = false; From 882a70b70fedce192796a4dbbc6d613bee85b0bb Mon Sep 17 00:00:00 2001 From: pubuzhixing8 Date: Fri, 2 Aug 2024 15:21:57 +0800 Subject: [PATCH 03/12] feat: add live-block-provider for frontend --- src/app/share/live-block-provider.ts | 128 +++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 src/app/share/live-block-provider.ts diff --git a/src/app/share/live-block-provider.ts b/src/app/share/live-block-provider.ts new file mode 100644 index 00000000..b275a888 --- /dev/null +++ b/src/app/share/live-block-provider.ts @@ -0,0 +1,128 @@ +/* +Unlike stated in the LICENSE file, it is not necessary to include the copyright notice and permission notice when you copy code from this file. +*/ + +/** + * @module provider/websocket + */ + +/* eslint-env browser */ +import * as Y from 'yjs'; // eslint-disable-line +import * as encoding from 'lib0/encoding'; +import * as decoding from 'lib0/decoding'; +import * as syncProtocol from 'y-protocols/sync'; +import * as authProtocol from 'y-protocols/auth'; +import * as awarenessProtocol from 'y-protocols/awareness'; +import { Observable } from 'lib0/observable'; + +const messageSync = 0; +const messageQueryAwareness = 3; +const messageAwareness = 1; +const messageAuth = 2; + +/** + * encoder, decoder, provider, emitSynced, messageType + * @type {Array} + */ +const messageHandlers = []; + +messageHandlers[messageSync] = (encoder, decoder, provider: LiveBlockProvider, emitSynced, messageType) => { + encoding.writeVarUint(encoder, messageSync); + encoding.writeVarString(encoder, provider.doc.guid); + const syncMessageType = syncProtocol.readSyncMessage(decoder, encoder, provider.doc, provider); + if (emitSynced && syncMessageType === syncProtocol.messageYjsSyncStep2 && !provider.synced) { + provider.synced = true; + } + return syncMessageType; +}; + +messageHandlers[messageQueryAwareness] = (encoder, decoder, provider, emitSynced, messageType) => { + encoding.writeVarUint(encoder, messageAwareness); + encoding.writeVarUint8Array( + encoder, + awarenessProtocol.encodeAwarenessUpdate(provider.awareness, Array.from(provider.awareness.getStates().keys())) + ); +}; + +messageHandlers[messageAwareness] = (encoder, decoder, provider, emitSynced, messageType) => { + awarenessProtocol.applyAwarenessUpdate(provider.awareness, decoding.readVarUint8Array(decoder), provider); +}; + +messageHandlers[messageAuth] = (encoder, decoder, provider, emitSynced, messageType) => { + authProtocol.readAuthMessage(decoder, provider.doc, (doc, reason) => { + permissionDeniedHandler(provider, reason); + }); +}; + +const permissionDeniedHandler = (provider: LiveBlockProvider, reason) => { + console.warn(`Permission denied to access ${provider.doc.guid}.\n${reason}`); +}; + +const syncLiveBlock = (doc: Y.Doc, websocket) => { + // always send sync step 1 when connected + const encoder = encoding.createEncoder(); + encoding.writeVarUint(encoder, messageSync); + encoding.writeVarString(encoder, doc.guid); + syncProtocol.writeSyncStep1(encoder, doc); + websocket.send(encoding.toUint8Array(encoder)); +}; + +export class LiveBlockProvider extends Observable { + doc: Y.Doc; + sharedType: Y.Array; + ws: WebSocket; + #synced = false; + + constructor(guid: string, ws: WebSocket, doc?: Y.Doc) { + super(); + if (doc) { + this.doc = doc; + this.sharedType = doc.getArray(); + } else { + this.doc = new Y.Doc({ guid }); + this.sharedType = this.doc.getArray(); + } + this.ws = ws; + this.sharedType.observeDeep((events) => { + this.emit('onChange', events); + }); + this.doc.on('update', (update, origin) => { + if (origin !== this) { + const encoder = encoding.createEncoder(); + encoding.writeVarUint(encoder, messageSync); + encoding.writeVarString(encoder, this.doc.guid); + syncProtocol.writeUpdate(encoder, update); + } + }); + } + + sync() { + syncLiveBlock(this.doc, this.ws); + } + + readMessage(messageType: number, encoder: encoding.Encoder, decoder: decoding.Decoder) { + const messageHandler = messageHandlers[messageType]; + if (messageHandler) { + const result = messageHandler(encoder, decoder, this, true, messageType); + // 只要是 step1 回复(yjs 给出的逻辑是,step1 并且有不同修改时回复) + // 因为加了 guid 不好判定是否有不同修改,暂时全同步 + if (syncProtocol.messageYjsSyncStep1 === result) { + this.ws.send(encoding.toUint8Array(encoder)); + } + } + } + + /** + * @type {boolean} + */ + get synced() { + return this.#synced; + } + + set synced(state) { + if (this.#synced !== state) { + this.#synced = state; + this.emit('synced', [state]); + } + } +} From 1e875c750949db9c25715803863144eff8257206 Mon Sep 17 00:00:00 2001 From: pubuzhixing8 Date: Mon, 5 Aug 2024 10:06:12 +0800 Subject: [PATCH 04/12] chore: add note --- src/app/share/live-block-provider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/share/live-block-provider.ts b/src/app/share/live-block-provider.ts index b275a888..a3e79c52 100644 --- a/src/app/share/live-block-provider.ts +++ b/src/app/share/live-block-provider.ts @@ -104,7 +104,7 @@ export class LiveBlockProvider extends Observable { const messageHandler = messageHandlers[messageType]; if (messageHandler) { const result = messageHandler(encoder, decoder, this, true, messageType); - // 只要是 step1 回复(yjs 给出的逻辑是,step1 并且有不同修改时回复) + // TODO: 只要是 step1 回复(yjs 给出的逻辑是,step1 并且有不同修改时回复) // 因为加了 guid 不好判定是否有不同修改,暂时全同步 if (syncProtocol.messageYjsSyncStep1 === result) { this.ws.send(encoding.toUint8Array(encoder)); From 926fae51572b69aec6e339e6046cc3ccfc254efc Mon Sep 17 00:00:00 2001 From: pubuzhixing8 Date: Mon, 5 Aug 2024 11:32:06 +0800 Subject: [PATCH 05/12] feat: add guid for all doc and sync message --- backend/src/utils.ts | 22 +++-- src/app/share/live-block-provider.ts | 1 + src/app/share/shared.ts | 2 +- src/app/share/y-websocket.ts | 124 ++++++++------------------- 4 files changed, 50 insertions(+), 99 deletions(-) diff --git a/backend/src/utils.ts b/backend/src/utils.ts index 900c7fb0..b50fc7ca 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -75,9 +75,10 @@ const messageAwareness = 1; * @param {WSSharedDoc} doc * @param {any} _tr */ -const updateHandler = (update, _origin, doc, _tr) => { +const updateHandler = (update, _origin, doc: WSSharedDoc, _tr) => { const encoder = encoding.createEncoder(); encoding.writeVarUint(encoder, messageSync); + encoding.writeVarString(encoder, doc.guid); syncProtocol.writeUpdate(encoder, update); const message = encoding.toUint8Array(encoder); doc.conns.forEach((_, conn) => send(doc, conn, message)); @@ -107,7 +108,7 @@ class WSSharedDoc extends Y.Doc { * @param {string} name */ constructor(name) { - super({ gc: gcEnabled }); + super({ gc: gcEnabled, guid: name }); this.name = name; /** * Maps from conn to set of controlled user ids. Delete all user ids from awareness when this conn is closed @@ -139,6 +140,7 @@ class WSSharedDoc extends Y.Doc { // broadcast awareness update const encoder = encoding.createEncoder(); encoding.writeVarUint(encoder, messageAwareness); + encoding.writeVarString(encoder, this.guid); encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(this.awareness, changedClients)); const buff = encoding.toUint8Array(encoder); this.conns.forEach((_, c) => { @@ -163,8 +165,8 @@ exports.WSSharedDoc = WSSharedDoc; * @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 getYDoc = (docname, gc = true) => { + return map.setIfUndefined(docs, docname, () => { const doc = new WSSharedDoc(docname); doc.gc = gc; if (persistence !== null) { @@ -172,9 +174,9 @@ const getYDoc = (docname, gc = true) => } docs.set(docname, doc); return doc; - }); + }) as WSSharedDoc; +}; -exports.getYDoc = getYDoc; /** * @param {any} conn @@ -186,15 +188,17 @@ const messageListener = (conn, doc: WSSharedDoc, message) => { const encoder = encoding.createEncoder(); const decoder = decoding.createDecoder(message); const messageType = decoding.readVarUint(decoder); + const guid = decoding.readVarString(decoder); switch (messageType) { case messageSync: encoding.writeVarUint(encoder, messageSync); - syncProtocol.readSyncMessage(decoder, encoder, doc, conn); + encoding.writeVarString(encoder, guid); + const syncMessageType = 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) { + if (syncMessageType === 0) { send(doc, conn, encoding.toUint8Array(encoder)); } break; @@ -298,12 +302,14 @@ export const setupWSConnection = (conn, req, { docName = (req.url || '').slice(1 // send sync step 1 const encoder = encoding.createEncoder(); encoding.writeVarUint(encoder, messageSync); + encoding.writeVarString(encoder, doc.guid); syncProtocol.writeSyncStep1(encoder, doc); send(doc, conn, encoding.toUint8Array(encoder)); const awarenessStates = doc.awareness.getStates(); if (awarenessStates.size > 0) { const encoder = encoding.createEncoder(); encoding.writeVarUint(encoder, messageAwareness); + encoding.writeVarString(encoder, doc.guid); encoding.writeVarUint8Array( encoder, awarenessProtocol.encodeAwarenessUpdate(doc.awareness, Array.from(awarenessStates.keys())) diff --git a/src/app/share/live-block-provider.ts b/src/app/share/live-block-provider.ts index a3e79c52..9a9b15a9 100644 --- a/src/app/share/live-block-provider.ts +++ b/src/app/share/live-block-provider.ts @@ -38,6 +38,7 @@ messageHandlers[messageSync] = (encoder, decoder, provider: LiveBlockProvider, e messageHandlers[messageQueryAwareness] = (encoder, decoder, provider, emitSynced, messageType) => { encoding.writeVarUint(encoder, messageAwareness); + encoding.writeVarString(encoder, provider.doc.guid); encoding.writeVarUint8Array( encoder, awarenessProtocol.encodeAwarenessUpdate(provider.awareness, Array.from(provider.awareness.getStates().keys())) diff --git a/src/app/share/shared.ts b/src/app/share/shared.ts index 12541910..7c1b0cff 100644 --- a/src/app/share/shared.ts +++ b/src/app/share/shared.ts @@ -9,7 +9,7 @@ export type SyncElement = Y.Array; export type SharedType = Y.Map; export const createSharedType = () => { - const doc = new Y.Doc(); + const doc = new Y.Doc({ guid: 'room-1' }); const sharedType = doc.getMap('ai-table'); return sharedType; }; diff --git a/src/app/share/y-websocket.ts b/src/app/share/y-websocket.ts index 5f471b39..c2423ec1 100644 --- a/src/app/share/y-websocket.ts +++ b/src/app/share/y-websocket.ts @@ -20,6 +20,7 @@ import { Observable } from 'lib0/observable'; import * as math from 'lib0/math'; import * as url from 'lib0/url'; import { Observable as RxjsObservable } from 'rxjs'; +import { LiveBlockProvider } from './live-block-provider'; const messageSync = 0; const messageQueryAwareness = 3; @@ -32,16 +33,19 @@ const messageAuth = 2; */ const messageHandlers = []; -messageHandlers[messageSync] = (encoder, decoder, provider, emitSynced, messageType) => { +messageHandlers[messageSync] = (encoder, decoder, provider: WebsocketProvider, emitSynced, messageType) => { encoding.writeVarUint(encoder, messageSync); + encoding.writeVarString(encoder, provider.doc.guid); const syncMessageType = syncProtocol.readSyncMessage(decoder, encoder, provider.doc, provider); if (emitSynced && syncMessageType === syncProtocol.messageYjsSyncStep2 && !provider.synced) { provider.synced = true; } + return syncMessageType; }; messageHandlers[messageQueryAwareness] = (encoder, decoder, provider, emitSynced, messageType) => { encoding.writeVarUint(encoder, messageAwareness); + encoding.writeVarString(encoder, provider.doc.guid); encoding.writeVarUint8Array( encoder, awarenessProtocol.encodeAwarenessUpdate(provider.awareness, Array.from(provider.awareness.getStates().keys())) @@ -73,27 +77,8 @@ const permissionDeniedHandler = (provider: WebsocketProvider, reason) => { /** * @param {WebsocketProvider} provider - * @param {Uint8Array} buf - * @param {boolean} emitSynced - * @return {encoding.Encoder} */ -const readMessage = (provider, buf, emitSynced) => { - const decoder = decoding.createDecoder(buf); - const encoder = encoding.createEncoder(); - const messageType = decoding.readVarUint(decoder); - const messageHandler = provider.messageHandlers[messageType]; - if (/** @type {any} */ messageHandler) { - messageHandler(encoder, decoder, provider, emitSynced, messageType); - } else { - console.error('Unable to compute message'); - } - return encoder; -}; - -/** - * @param {WebsocketProvider} provider - */ -const setupWS = (provider) => { +const setupWS = (provider: WebsocketProvider) => { if (provider.shouldConnect && provider.ws === null) { const websocket = new provider._WS(provider.url); websocket.binaryType = 'arraybuffer'; @@ -106,9 +91,25 @@ const setupWS = (provider) => { const { data } = event; if (typeof data === 'object') { provider.wsLastMessageReceived = time.getUnixTime(); - const encoder = readMessage(provider, new Uint8Array(event.data), true); - if (encoding.length(encoder) > 1) { - websocket.send(encoding.toUint8Array(encoder)); + const buf = new Uint8Array(event.data); + const decoder = decoding.createDecoder(buf); + const encoder = encoding.createEncoder(); + const messageType = decoding.readVarUint(decoder); + const guid = decoding.readVarString(decoder); + console.log('on message from guid: ', guid); + if (guid !== provider.doc.guid) { + console.log('子文档') + const liveBlock = provider.liveBlocks.get(guid); + return; + } + const messageHandler = provider.messageHandlers[messageType]; + if (/** @type {any} */ messageHandler) { + const syncMessageType = messageHandler(encoder, decoder, provider, true, messageType); + if (syncMessageType === syncProtocol.messageYjsSyncStep2) { + websocket.send(encoding.toUint8Array(encoder)); + } + } else { + console.error('Unable to compute message'); } } }; @@ -121,7 +122,7 @@ const setupWS = (provider) => { // update awareness (all users except local left) awarenessProtocol.removeAwarenessStates( provider.awareness, - Array.from(provider.awareness.getStates().keys() as number[]).filter( + Array.from(provider.awareness.getStates().keys() as unknown as number[]).filter( (client: number) => client !== provider.doc.clientID ), provider @@ -177,18 +178,20 @@ const setupWS = (provider) => { } }; -const syncYDoc = (provider, websocket) => { +const syncYDoc = (provider: WebsocketProvider, websocket) => { // always send sync step 1 when connected const encoder = encoding.createEncoder(); encoding.writeVarUint(encoder, messageSync); + encoding.writeVarString(encoder, provider.doc.guid); syncProtocol.writeSyncStep1(encoder, provider.doc); websocket.send(encoding.toUint8Array(encoder)); }; -const syncAwareness = (provider, websocket) => { +const syncAwareness = (provider: WebsocketProvider, websocket) => { if (provider.awareness.getLocalState() !== null) { const encoderAwarenessState = encoding.createEncoder(); encoding.writeVarUint(encoderAwarenessState, messageAwareness); + encoding.writeVarString(encoderAwarenessState, provider.doc.guid); encoding.writeVarUint8Array( encoderAwarenessState, awarenessProtocol.encodeAwarenessUpdate(provider.awareness, [provider.doc.clientID]) @@ -205,11 +208,6 @@ const broadcastMessage = (provider, buf) => { if (provider.wsconnected) { /** @type {WebSocket} */ provider.ws.send(buf); } - if (provider.bcconnected) { - provider.mux(() => { - bc.publish(provider.bcChannel, buf); - }); - } }; /** @@ -229,7 +227,8 @@ export class WebsocketProvider extends Observable { bcChannel: string; roomname: any; serverUrl: string; - doc: any; + doc: Y.Doc; + liveBlocks: Map = new Map(); params: any; _WS: { new (url: string, protocols?: string | string[]): WebSocket; @@ -329,17 +328,6 @@ export class WebsocketProvider extends Observable { }, resyncInterval) as any; } - /** - * @param {ArrayBuffer} data - */ - this._bcSubscriber = (data) => { - this.mux(() => { - const encoder = readMessage(this, new Uint8Array(data), false); - if (encoding.length(encoder) > 1) { - bc.publish(this.bcChannel, encoding.toUint8Array(encoder)); - } - }); - }; /** * Listens to Yjs updates and sends them to remote peers (ws and broadcastchannel) * @param {Uint8Array} update @@ -349,6 +337,7 @@ export class WebsocketProvider extends Observable { if (origin !== this) { const encoder = encoding.createEncoder(); encoding.writeVarUint(encoder, messageSync); + encoding.writeVarString(encoder, this.doc.guid); syncProtocol.writeUpdate(encoder, update); broadcastMessage(this, encoding.toUint8Array(encoder)); } @@ -362,6 +351,7 @@ export class WebsocketProvider extends Observable { const changedClients = added.concat(updated).concat(removed); const encoder = encoding.createEncoder(); encoding.writeVarUint(encoder, messageAwareness); + encoding.writeVarString(encoder, this.doc.guid); encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(awareness, changedClients)); broadcastMessage(this, encoding.toUint8Array(encoder)); }; @@ -423,53 +413,8 @@ export class WebsocketProvider extends Observable { super.destroy(); } - connectBc() { - if (!this.bcconnected) { - bc.subscribe(this.bcChannel, this._bcSubscriber); - this.bcconnected = true; - } - // send sync step1 to bc - this.mux(() => { - // write sync step 1 - const encoderSync = encoding.createEncoder(); - encoding.writeVarUint(encoderSync, messageSync); - syncProtocol.writeSyncStep1(encoderSync, this.doc); - bc.publish(this.bcChannel, encoding.toUint8Array(encoderSync)); - // broadcast local state - const encoderState = encoding.createEncoder(); - encoding.writeVarUint(encoderState, messageSync); - syncProtocol.writeSyncStep2(encoderState, this.doc); - bc.publish(this.bcChannel, encoding.toUint8Array(encoderState)); - // write queryAwareness - const encoderAwarenessQuery = encoding.createEncoder(); - encoding.writeVarUint(encoderAwarenessQuery, messageQueryAwareness); - bc.publish(this.bcChannel, encoding.toUint8Array(encoderAwarenessQuery)); - // broadcast local awareness state - const encoderAwarenessState = encoding.createEncoder(); - encoding.writeVarUint(encoderAwarenessState, messageAwareness); - encoding.writeVarUint8Array( - encoderAwarenessState, - awarenessProtocol.encodeAwarenessUpdate(this.awareness, [this.doc.clientID]) - ); - bc.publish(this.bcChannel, encoding.toUint8Array(encoderAwarenessState)); - }); - } - - disconnectBc() { - // broadcast message with local awareness state set to null (indicating disconnect) - const encoder = encoding.createEncoder(); - encoding.writeVarUint(encoder, messageAwareness); - encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(this.awareness, [this.doc.clientID], new Map())); - broadcastMessage(this, encoding.toUint8Array(encoder)); - if (this.bcconnected) { - bc.unsubscribe(this.bcChannel, this._bcSubscriber); - this.bcconnected = false; - } - } - disconnect() { this.shouldConnect = false; - this.disconnectBc(); if (this.ws !== null) { this.ws.close(); } @@ -479,7 +424,6 @@ export class WebsocketProvider extends Observable { this.shouldConnect = true; if (!this.wsconnected && this.ws === null) { setupWS(this); - this.connectBc(); } } } From 0545249ca1664b5696face68b892d045d5493f54 Mon Sep 17 00:00:00 2001 From: pubuzhixing8 Date: Mon, 5 Aug 2024 16:42:55 +0800 Subject: [PATCH 06/12] chore: correct types --- src/app/action/view.ts | 2 +- src/app/service/table.service.ts | 2 +- src/app/share/apply-to-table/map-event.ts | 4 ++-- src/app/share/provider.ts | 1 - 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/app/action/view.ts b/src/app/action/view.ts index 9d2b0d34..5e789e41 100644 --- a/src/app/action/view.ts +++ b/src/app/action/view.ts @@ -9,7 +9,7 @@ export function setView(aiTable: AIViewTable, value: Partial, path: const oldView: Partial = {}; const newView: Partial = {}; for (const key in value) { - const k = key as keyof AITableView; + const k = key; if (JSON.stringify(view[k]) !== JSON.stringify(value[k])) { if (view.hasOwnProperty(key)) { oldView[k] = view[k] as any; diff --git a/src/app/service/table.service.ts b/src/app/service/table.service.ts index f410648d..d8017dad 100644 --- a/src/app/service/table.service.ts +++ b/src/app/service/table.service.ts @@ -1,6 +1,5 @@ import { computed, Injectable, isDevMode, signal, WritableSignal } from '@angular/core'; import { createSharedType, initSharedType, SharedType } from '../share/shared'; -import { WebsocketProvider } from 'y-websocket'; import { getProvider } from '../share/provider'; import { DemoAIField, DemoAIRecord } from '../types'; import { getDefaultValue, sortDataByView } from '../utils/utils'; @@ -10,6 +9,7 @@ import { YjsAITable } from '../share/yjs-table'; import { AITable } from '@ai-table/grid'; import { AITableView } from '../types/view'; import { createDraft, finishDraft } from 'immer'; +import { WebsocketProvider } from '../share/y-websocket'; @Injectable() export class TableService { diff --git a/src/app/share/apply-to-table/map-event.ts b/src/app/share/apply-to-table/map-event.ts index d06167aa..2992e0b9 100644 --- a/src/app/share/apply-to-table/map-event.ts +++ b/src/app/share/apply-to-table/map-event.ts @@ -26,14 +26,14 @@ export default function translateMapEvent( }) for (const [key, value] of entries) { - const k = key as keyof AITableView; + const k = key; newProperties[k] = value } const oldEntries = keyChanges.map(([key]) => [key, (targetElement as any)[key]]) for (const [key, value] of oldEntries) { - const k = key as keyof AITableView; + const k = key; properties[k] = value } diff --git a/src/app/share/provider.ts b/src/app/share/provider.ts index 6ba5799d..7fd266fe 100644 --- a/src/app/share/provider.ts +++ b/src/app/share/provider.ts @@ -1,4 +1,3 @@ -// import { WebsocketProvider } from 'y-websocket'; import * as Y from 'yjs'; import { WebsocketProvider } from './y-websocket'; From 98b51355e59f091b0ef80d84207bcd0b05372d34 Mon Sep 17 00:00:00 2001 From: pubuzhixing8 Date: Mon, 5 Aug 2024 16:50:53 +0800 Subject: [PATCH 07/12] chore: update room name --- src/app/service/table.service.ts | 2 +- src/app/share/shared.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/service/table.service.ts b/src/app/service/table.service.ts index d8017dad..f626753b 100644 --- a/src/app/service/table.service.ts +++ b/src/app/service/table.service.ts @@ -68,7 +68,7 @@ export class TableService { let isInitialized = false; if (!this.sharedType) { - this.sharedType = createSharedType(); + this.sharedType = createSharedType(room); this.sharedType.observeDeep((events: any) => { if (!YjsAITable.isLocal(this.aiTable)) { if (!isInitialized) { diff --git a/src/app/share/shared.ts b/src/app/share/shared.ts index 7c1b0cff..64aff7a0 100644 --- a/src/app/share/shared.ts +++ b/src/app/share/shared.ts @@ -8,8 +8,8 @@ export type SyncArrayElement = Y.Array>; export type SyncElement = Y.Array; export type SharedType = Y.Map; -export const createSharedType = () => { - const doc = new Y.Doc({ guid: 'room-1' }); +export const createSharedType = (room: string) => { + const doc = new Y.Doc({ guid: room }); const sharedType = doc.getMap('ai-table'); return sharedType; }; From 6b8cf2670f94b3e0ea88e22f12f8ec04a28f6a2b Mon Sep 17 00:00:00 2001 From: pubuzhixing8 Date: Mon, 5 Aug 2024 17:18:05 +0800 Subject: [PATCH 08/12] feat: update error condition --- package-lock.json | 1403 ++++---------------------- package.json | 2 +- src/app/component/table.component.ts | 2 +- src/app/share/y-websocket.ts | 4 +- 4 files changed, 185 insertions(+), 1226 deletions(-) diff --git a/package-lock.json b/package-lock.json index aa0475e5..dc2ea326 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@tethys/icons": "^1.4.62", "date-fns": "^3.6.0", "immer": "^10.0.3", - "ngx-tethys": "^17.0.8", + "ngx-tethys": "^17.0.14", "npm": "^10.8.1", "rxjs": "~7.8.0", "tslib": "^2.6.3", @@ -51,7 +51,7 @@ "ts-node": "^10.9.2", "typescript": "~5.4.2", "ws": "8.0.0", - "y-websocket": "^2.0.3", + "y-protocols": "^1.0.6", "yjs": "^13.6.16" } }, @@ -230,22 +230,6 @@ "node": ">=12" } }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/darwin-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@angular-devkit/build-angular/node_modules/@types/node": { "version": "22.0.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.0.0.tgz", @@ -524,22 +508,6 @@ "node": ">=12" } }, - "node_modules/@angular/build/node_modules/@esbuild/darwin-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@angular/build/node_modules/@types/node": { "version": "22.0.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.0.0.tgz", @@ -2914,542 +2882,191 @@ "node": ">= 10.0.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", - "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.3", "cpu": [ - "ppc64" + "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ - "aix" + "darwin" ], "engines": { "node": ">=12" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", - "cpu": [ - "arm" - ], + "node_modules/@hutson/parse-repository-url": { + "version": "3.0.2", "dev": true, - "optional": true, - "os": [ - "android" - ], + "license": "Apache-2.0", "engines": { - "node": ">=12" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", - "cpu": [ - "arm64" - ], + "node_modules/@inquirer/figures": { + "version": "1.0.3", "dev": true, - "optional": true, - "os": [ - "android" - ], + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", - "cpu": [ - "x64" - ], + "node_modules/@isaacs/cliui": { + "version": "8.0.2", "dev": true, - "optional": true, - "os": [ - "android" - ], + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, "engines": { "node": ">=12" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.3", - "cpu": [ - "arm64" - ], + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.3", - "cpu": [ - "x64" - ], + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", - "cpu": [ - "arm64" - ], + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } + "license": "MIT" }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", - "cpu": [ - "x64" - ], + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", "dev": true, - "optional": true, - "os": [ - "freebsd" - ], + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, "engines": { "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", - "cpu": [ - "arm" - ], + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, "engines": { "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", - "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", - "cpu": [ - "arm64" - ], + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, "engines": { "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", - "cpu": [ - "ia32" - ], + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, "engines": { - "node": ">=12" + "node": ">=8" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", - "cpu": [ - "loong64" - ], + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=8" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", - "cpu": [ - "mips64el" - ], + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, "engines": { - "node": ">=12" + "node": ">=6.0.0" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", - "cpu": [ - "ppc64" - ], + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=6.0.0" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", - "cpu": [ - "riscv64" - ], + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=6.0.0" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", - "cpu": [ - "s390x" - ], + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", - "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", - "cpu": [ - "x64" - ], + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", - "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@hutson/parse-repository-url": { - "version": "3.0.2", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@inquirer/figures": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "dev": true, - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "dev": true, - "license": "ISC", - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "dev": true, - "license": "MIT" + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", @@ -3552,18 +3169,6 @@ "darwin" ] }, - "node_modules/@lmdb/lmdb-darwin-x64": { - "version": "3.0.8", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { "version": "3.0.3", "cpu": [ @@ -3576,18 +3181,6 @@ "darwin" ] }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { - "version": "3.0.3", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, "node_modules/@ngtools/webpack": { "version": "18.0.3", "dev": true, @@ -3975,19 +3568,7 @@ "darwin" ] }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.18.0", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/wasm-node": { + "node_modules/@rollup/wasm-node": { "version": "4.18.0", "dev": true, "license": "MIT", @@ -4720,23 +4301,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/abstract-leveldown": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-6.2.3.tgz", - "integrity": "sha512-BsLm5vFMRUrrLeCcRc+G0t2qOaTzpoJQLOubq2XM72eNpjF5UdU5o/5NvlNhx95XHcAvcl8OMXr4mlg/fRgUXQ==", - "dev": true, - "optional": true, - "dependencies": { - "buffer": "^5.5.0", - "immediate": "^3.2.3", - "level-concat-iterator": "~2.0.0", - "level-supports": "~1.0.0", - "xtend": "~4.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/accepts": { "version": "1.3.8", "dev": true, @@ -5055,13 +4619,6 @@ ], "license": "MIT" }, - "node_modules/async-limiter": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", - "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", - "dev": true, - "optional": true - }, "node_modules/asynckit": { "version": "0.4.0", "dev": true, @@ -8041,20 +7598,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/deferred-leveldown": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/deferred-leveldown/-/deferred-leveldown-5.3.0.tgz", - "integrity": "sha512-a59VOT+oDy7vtAbLRCZwWgxu2BaCfd5Hk7wxJd48ei7I+nsg8Orlb9CLG0PMZienk9BSUKgeAqkO2+Lw+1+Ukw==", - "dev": true, - "optional": true, - "dependencies": { - "abstract-leveldown": "~6.2.1", - "inherits": "^2.0.3" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/define-data-property": { "version": "1.1.4", "dev": true, @@ -8375,22 +7918,6 @@ "iconv-lite": "^0.6.2" } }, - "node_modules/encoding-down": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/encoding-down/-/encoding-down-6.3.0.tgz", - "integrity": "sha512-QKrV0iKR6MZVJV08QY0wp1e7vF6QbhnbQhb07bwpEyuz4uZiZgPlEGdkCROuFkUwdxlFaiPIhjyarH1ee/3vhw==", - "dev": true, - "optional": true, - "dependencies": { - "abstract-leveldown": "^6.2.1", - "inherits": "^2.0.3", - "level-codec": "^9.0.0", - "level-errors": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/encoding/node_modules/iconv-lite": { "version": "0.6.3", "dev": true, @@ -8493,430 +8020,94 @@ "dev": true, "license": "MIT" }, - "node_modules/errno": { - "version": "0.1.8", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "prr": "~1.0.1" - }, - "bin": { - "errno": "cli.js" - } - }, - "node_modules/error-ex": { - "version": "1.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-define-property": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-module-lexer": { - "version": "1.5.3", - "dev": true, - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.21.3", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.3", - "@esbuild/android-arm": "0.21.3", - "@esbuild/android-arm64": "0.21.3", - "@esbuild/android-x64": "0.21.3", - "@esbuild/darwin-arm64": "0.21.3", - "@esbuild/darwin-x64": "0.21.3", - "@esbuild/freebsd-arm64": "0.21.3", - "@esbuild/freebsd-x64": "0.21.3", - "@esbuild/linux-arm": "0.21.3", - "@esbuild/linux-arm64": "0.21.3", - "@esbuild/linux-ia32": "0.21.3", - "@esbuild/linux-loong64": "0.21.3", - "@esbuild/linux-mips64el": "0.21.3", - "@esbuild/linux-ppc64": "0.21.3", - "@esbuild/linux-riscv64": "0.21.3", - "@esbuild/linux-s390x": "0.21.3", - "@esbuild/linux-x64": "0.21.3", - "@esbuild/netbsd-x64": "0.21.3", - "@esbuild/openbsd-x64": "0.21.3", - "@esbuild/sunos-x64": "0.21.3", - "@esbuild/win32-arm64": "0.21.3", - "@esbuild/win32-ia32": "0.21.3", - "@esbuild/win32-x64": "0.21.3" - } - }, - "node_modules/esbuild-wasm": { - "version": "0.21.3", - "dev": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/aix-ppc64": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.3.tgz", - "integrity": "sha512-yTgnwQpFVYfvvo4SvRFB0SwrW8YjOxEoT7wfMT7Ol5v7v5LDNvSGo67aExmxOb87nQNeWPVvaGBNfQ7BXcrZ9w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/android-arm": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.3.tgz", - "integrity": "sha512-bviJOLMgurLJtF1/mAoJLxDZDL6oU5/ztMHnJQRejbJrSc9FFu0QoUoFhvi6qSKJEw9y5oGyvr9fuDtzJ30rNQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/android-arm64": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.3.tgz", - "integrity": "sha512-c+ty9necz3zB1Y+d/N+mC6KVVkGUUOcm4ZmT5i/Fk5arOaY3i6CA3P5wo/7+XzV8cb4GrI/Zjp8NuOQ9Lfsosw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/android-x64": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.3.tgz", - "integrity": "sha512-JReHfYCRK3FVX4Ra+y5EBH1b9e16TV2OxrPAvzMsGeES0X2Ndm9ImQRI4Ket757vhc5XBOuGperw63upesclRw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.3.tgz", - "integrity": "sha512-fsNAAl5pU6wmKHq91cHWQT0Fz0vtyE1JauMzKotrwqIKAswwP5cpHUCxZNSTuA/JlqtScq20/5KZ+TxQdovU/g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/freebsd-x64": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.3.tgz", - "integrity": "sha512-tci+UJ4zP5EGF4rp8XlZIdq1q1a/1h9XuronfxTMCNBslpCtmk97Q/5qqy1Mu4zIc0yswN/yP/BLX+NTUC1bXA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/linux-arm": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.3.tgz", - "integrity": "sha512-f6kz2QpSuyHHg01cDawj0vkyMwuIvN62UAguQfnNVzbge2uWLhA7TCXOn83DT0ZvyJmBI943MItgTovUob36SQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/linux-arm64": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.3.tgz", - "integrity": "sha512-vvG6R5g5ieB4eCJBQevyDMb31LMHthLpXTc2IGkFnPWS/GzIFDnaYFp558O+XybTmYrVjxnryru7QRleJvmZ6Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/linux-ia32": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.3.tgz", - "integrity": "sha512-HjCWhH7K96Na+66TacDLJmOI9R8iDWDDiqe17C7znGvvE4sW1ECt9ly0AJ3dJH62jHyVqW9xpxZEU1jKdt+29A==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/linux-loong64": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.3.tgz", - "integrity": "sha512-BGpimEccmHBZRcAhdlRIxMp7x9PyJxUtj7apL2IuoG9VxvU/l/v1z015nFs7Si7tXUwEsvjc1rOJdZCn4QTU+Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/linux-mips64el": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.3.tgz", - "integrity": "sha512-5rMOWkp7FQGtAH3QJddP4w3s47iT20hwftqdm7b+loe95o8JU8ro3qZbhgMRy0VuFU0DizymF1pBKkn3YHWtsw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/linux-ppc64": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.3.tgz", - "integrity": "sha512-h0zj1ldel89V5sjPLo5H1SyMzp4VrgN1tPkN29TmjvO1/r0MuMRwJxL8QY05SmfsZRs6TF0c/IDH3u7XYYmbAg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/linux-riscv64": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.3.tgz", - "integrity": "sha512-dkAKcTsTJ+CRX6bnO17qDJbLoW37npd5gSNtSzjYQr0svghLJYGYB0NF1SNcU1vDcjXLYS5pO4qOW4YbFama4A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/linux-s390x": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.3.tgz", - "integrity": "sha512-vnD1YUkovEdnZWEuMmy2X2JmzsHQqPpZElXx6dxENcIwTu+Cu5ERax6+Ke1QsE814Zf3c6rxCfwQdCTQ7tPuXA==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/linux-x64": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.3.tgz", - "integrity": "sha512-IOXOIm9WaK7plL2gMhsWJd+l2bfrhfilv0uPTptoRoSb2p09RghhQQp9YY6ZJhk/kqmeRt6siRdMSLLwzuT0KQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/netbsd-x64": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.3.tgz", - "integrity": "sha512-uTgCwsvQ5+vCQnqM//EfDSuomo2LhdWhFPS8VL8xKf+PKTCrcT/2kPPoWMTs22aB63MLdGMJiE3f1PHvCDmUOw==", - "cpu": [ - "x64" - ], + "node_modules/errno": { + "version": "0.1.8", "dev": true, + "license": "MIT", "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" } }, - "node_modules/esbuild/node_modules/@esbuild/openbsd-x64": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.3.tgz", - "integrity": "sha512-vNAkR17Ub2MgEud2Wag/OE4HTSI6zlb291UYzHez/psiKarp0J8PKGDnAhMBcHFoOHMXHfExzmjMojJNbAStrQ==", - "cpu": [ - "x64" - ], + "node_modules/error-ex": { + "version": "1.3.2", "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" } }, - "node_modules/esbuild/node_modules/@esbuild/sunos-x64": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.3.tgz", - "integrity": "sha512-W8H9jlGiSBomkgmouaRoTXo49j4w4Kfbl6I1bIdO/vT0+0u4f20ko3ELzV3hPI6XV6JNBVX+8BC+ajHkvffIJA==", - "cpu": [ - "x64" - ], + "node_modules/es-define-property": { + "version": "1.0.0", "dev": true, - "optional": true, - "os": [ - "sunos" - ], + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, "engines": { - "node": ">=12" + "node": ">= 0.4" } }, - "node_modules/esbuild/node_modules/@esbuild/win32-arm64": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.3.tgz", - "integrity": "sha512-EjEomwyLSCg8Ag3LDILIqYCZAq/y3diJ04PnqGRgq8/4O3VNlXyMd54j/saShaN4h5o5mivOjAzmU6C3X4v0xw==", - "cpu": [ - "arm64" - ], + "node_modules/es-errors": { + "version": "1.3.0", "dev": true, - "optional": true, - "os": [ - "win32" - ], + "license": "MIT", "engines": { - "node": ">=12" + "node": ">= 0.4" } }, - "node_modules/esbuild/node_modules/@esbuild/win32-ia32": { + "node_modules/es-module-lexer": { + "version": "1.5.3", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { "version": "0.21.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.3.tgz", - "integrity": "sha512-WGiE/GgbsEwR33++5rzjiYsKyHywE8QSZPF7Rfx9EBfK3Qn3xyR6IjyCr5Uk38Kg8fG4/2phN7sXp4NPWd3fcw==", - "cpu": [ - "ia32" - ], "dev": true, - "optional": true, - "os": [ - "win32" - ], + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, "engines": { "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.3", + "@esbuild/android-arm": "0.21.3", + "@esbuild/android-arm64": "0.21.3", + "@esbuild/android-x64": "0.21.3", + "@esbuild/darwin-arm64": "0.21.3", + "@esbuild/darwin-x64": "0.21.3", + "@esbuild/freebsd-arm64": "0.21.3", + "@esbuild/freebsd-x64": "0.21.3", + "@esbuild/linux-arm": "0.21.3", + "@esbuild/linux-arm64": "0.21.3", + "@esbuild/linux-ia32": "0.21.3", + "@esbuild/linux-loong64": "0.21.3", + "@esbuild/linux-mips64el": "0.21.3", + "@esbuild/linux-ppc64": "0.21.3", + "@esbuild/linux-riscv64": "0.21.3", + "@esbuild/linux-s390x": "0.21.3", + "@esbuild/linux-x64": "0.21.3", + "@esbuild/netbsd-x64": "0.21.3", + "@esbuild/openbsd-x64": "0.21.3", + "@esbuild/sunos-x64": "0.21.3", + "@esbuild/win32-arm64": "0.21.3", + "@esbuild/win32-ia32": "0.21.3", + "@esbuild/win32-x64": "0.21.3" } }, - "node_modules/esbuild/node_modules/@esbuild/win32-x64": { + "node_modules/esbuild-wasm": { "version": "0.21.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.3.tgz", - "integrity": "sha512-xRxC0jaJWDLYvcUvjQmHCJSfMrgmUuvsoXgDeU/wTorQ1ngDdUBuFtgY3W1Pc5sprGAvZBtWdJX7RPg/iZZUqA==", - "cpu": [ - "x64" - ], "dev": true, - "optional": true, - "os": [ - "win32" - ], + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, "engines": { "node": ">=12" } @@ -10684,13 +9875,6 @@ "node": ">=0.10.0" } }, - "node_modules/immediate": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.3.0.tgz", - "integrity": "sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==", - "dev": true, - "optional": true - }, "node_modules/immer": { "version": "10.1.1", "license": "MIT", @@ -11838,161 +11022,6 @@ "node": ">=0.10.0" } }, - "node_modules/level": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/level/-/level-6.0.1.tgz", - "integrity": "sha512-psRSqJZCsC/irNhfHzrVZbmPYXDcEYhA5TVNwr+V92jF44rbf86hqGp8fiT702FyiArScYIlPSBTDUASCVNSpw==", - "dev": true, - "optional": true, - "dependencies": { - "level-js": "^5.0.0", - "level-packager": "^5.1.0", - "leveldown": "^5.4.0" - }, - "engines": { - "node": ">=8.6.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/level" - } - }, - "node_modules/level-codec": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/level-codec/-/level-codec-9.0.2.tgz", - "integrity": "sha512-UyIwNb1lJBChJnGfjmO0OR+ezh2iVu1Kas3nvBS/BzGnx79dv6g7unpKIDNPMhfdTEGoc7mC8uAu51XEtX+FHQ==", - "dev": true, - "optional": true, - "dependencies": { - "buffer": "^5.6.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/level-concat-iterator": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/level-concat-iterator/-/level-concat-iterator-2.0.1.tgz", - "integrity": "sha512-OTKKOqeav2QWcERMJR7IS9CUo1sHnke2C0gkSmcR7QuEtFNLLzHQAvnMw8ykvEcv0Qtkg0p7FOwP1v9e5Smdcw==", - "dev": true, - "optional": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/level-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/level-errors/-/level-errors-2.0.1.tgz", - "integrity": "sha512-UVprBJXite4gPS+3VznfgDSU8PTRuVX0NXwoWW50KLxd2yw4Y1t2JUR5In1itQnudZqRMT9DlAM3Q//9NCjCFw==", - "dev": true, - "optional": true, - "dependencies": { - "errno": "~0.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/level-iterator-stream": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/level-iterator-stream/-/level-iterator-stream-4.0.2.tgz", - "integrity": "sha512-ZSthfEqzGSOMWoUGhTXdX9jv26d32XJuHz/5YnuHZzH6wldfWMOVwI9TBtKcya4BKTyTt3XVA0A3cF3q5CY30Q==", - "dev": true, - "optional": true, - "dependencies": { - "inherits": "^2.0.4", - "readable-stream": "^3.4.0", - "xtend": "^4.0.2" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/level-js": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/level-js/-/level-js-5.0.2.tgz", - "integrity": "sha512-SnBIDo2pdO5VXh02ZmtAyPP6/+6YTJg2ibLtl9C34pWvmtMEmRTWpra+qO/hifkUtBTOtfx6S9vLDjBsBK4gRg==", - "dev": true, - "optional": true, - "dependencies": { - "abstract-leveldown": "~6.2.3", - "buffer": "^5.5.0", - "inherits": "^2.0.3", - "ltgt": "^2.1.2" - } - }, - "node_modules/level-packager": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/level-packager/-/level-packager-5.1.1.tgz", - "integrity": "sha512-HMwMaQPlTC1IlcwT3+swhqf/NUO+ZhXVz6TY1zZIIZlIR0YSn8GtAAWmIvKjNY16ZkEg/JcpAuQskxsXqC0yOQ==", - "dev": true, - "optional": true, - "dependencies": { - "encoding-down": "^6.3.0", - "levelup": "^4.3.2" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/level-supports": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/level-supports/-/level-supports-1.0.1.tgz", - "integrity": "sha512-rXM7GYnW8gsl1vedTJIbzOrRv85c/2uCMpiiCzO2fndd06U/kUXEEU9evYn4zFggBOg36IsBW8LzqIpETwwQzg==", - "dev": true, - "optional": true, - "dependencies": { - "xtend": "^4.0.2" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/leveldown": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/leveldown/-/leveldown-5.6.0.tgz", - "integrity": "sha512-iB8O/7Db9lPaITU1aA2txU/cBEXAt4vWwKQRrrWuS6XDgbP4QZGj9BL2aNbwb002atoQ/lIotJkfyzz+ygQnUQ==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "dependencies": { - "abstract-leveldown": "~6.2.1", - "napi-macros": "~2.0.0", - "node-gyp-build": "~4.1.0" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/leveldown/node_modules/node-gyp-build": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.1.1.tgz", - "integrity": "sha512-dSq1xmcPDKPZ2EED2S6zw/b9NKsqzXRE6dVr8TVQnI3FJOTteUMuqF3Qqs6LZg+mLGYJWqQzMbIjMtJqTv87nQ==", - "dev": true, - "optional": true, - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } - }, - "node_modules/levelup": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/levelup/-/levelup-4.4.0.tgz", - "integrity": "sha512-94++VFO3qN95cM/d6eBXvd894oJE0w3cInq9USsyQzzoJxmiYzPAocNcuGCPGGjoXqDVJcr3C1jzt1TSjyaiLQ==", - "dev": true, - "optional": true, - "dependencies": { - "deferred-leveldown": "~5.3.0", - "level-errors": "~2.0.0", - "level-iterator-stream": "~4.0.0", - "level-supports": "~1.0.0", - "xtend": "~4.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/lib0": { "version": "0.2.94", "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.94.tgz", @@ -12245,13 +11274,6 @@ "yallist": "^3.0.2" } }, - "node_modules/ltgt": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ltgt/-/ltgt-2.2.1.tgz", - "integrity": "sha512-AI2r85+4MquTw9ZYqabu4nMwy9Oftlfa/e/52t9IjtfG+mGBbTNdAoZ3RQKLHR6r0wQnwZnPIEh/Ya6XTWAKNA==", - "dev": true, - "optional": true - }, "node_modules/magic-string": { "version": "0.30.10", "dev": true, @@ -12874,13 +11896,6 @@ "node": ">=0.10.0" } }, - "node_modules/napi-macros": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.0.0.tgz", - "integrity": "sha512-A0xLykHtARfueITVDernsAWdtIMbOJgKgcluwENp3AlsKN/PloyO10HtmoqnFAQAcxPkgZN7wdfPfEd0zNGxbg==", - "dev": true, - "optional": true - }, "node_modules/needle": { "version": "3.3.1", "dev": true, @@ -13034,7 +12049,9 @@ } }, "node_modules/ngx-tethys": { - "version": "17.0.8", + "version": "17.0.17", + "resolved": "https://registry.npmjs.org/ngx-tethys/-/ngx-tethys-17.0.17.tgz", + "integrity": "sha512-BvGwPmJsPUBdK+TKpGY2pHEihC6coC3A3QocPp+v1BwA6rbbsdMbL4d+MaEiZVpLsV4duf7Pu/qWbgTdq9+HdA==", "dependencies": { "tslib": "^2.3.0" }, @@ -19913,24 +18930,6 @@ "node": ">=0.4" } }, - "node_modules/y-leveldb": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/y-leveldb/-/y-leveldb-0.1.2.tgz", - "integrity": "sha512-6ulEn5AXfXJYi89rXPEg2mMHAyyw8+ZfeMMdOtBbV8FJpQ1NOrcgi6DTAcXof0dap84NjHPT2+9d0rb6cFsjEg==", - "dev": true, - "optional": true, - "dependencies": { - "level": "^6.0.1", - "lib0": "^0.2.31" - }, - "funding": { - "type": "GitHub Sponsors ❤", - "url": "https://github.com/sponsors/dmonad" - }, - "peerDependencies": { - "yjs": "^13.0.0" - } - }, "node_modules/y-protocols": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.6.tgz", @@ -19951,46 +18950,6 @@ "yjs": "^13.0.0" } }, - "node_modules/y-websocket": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/y-websocket/-/y-websocket-2.0.4.tgz", - "integrity": "sha512-UbrkOU4GPNFFTDlJYAxAmzZhia8EPxHkngZ6qjrxgIYCN3gI2l+zzLzA9p4LQJ0IswzpioeIgmzekWe7HoBBjg==", - "dev": true, - "dependencies": { - "lib0": "^0.2.52", - "lodash.debounce": "^4.0.8", - "y-protocols": "^1.0.5" - }, - "bin": { - "y-websocket": "bin/server.cjs", - "y-websocket-server": "bin/server.cjs" - }, - "engines": { - "node": ">=16.0.0", - "npm": ">=8.0.0" - }, - "funding": { - "type": "GitHub Sponsors ❤", - "url": "https://github.com/sponsors/dmonad" - }, - "optionalDependencies": { - "ws": "^6.2.1", - "y-leveldb": "^0.1.0" - }, - "peerDependencies": { - "yjs": "^13.5.6" - } - }, - "node_modules/y-websocket/node_modules/ws": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", - "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", - "dev": true, - "optional": true, - "dependencies": { - "async-limiter": "~1.0.0" - } - }, "node_modules/y18n": { "version": "5.0.8", "dev": true, @@ -20083,7 +19042,7 @@ }, "packages/grid": { "name": "@ai-table/grid", - "version": "0.0.3", + "version": "0.0.4", "dependencies": { "tslib": "^2.3.0" }, diff --git a/package.json b/package.json index 69f5906c..b34af1f8 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "ts-node": "^10.9.2", "typescript": "~5.4.2", "ws": "8.0.0", - "y-websocket": "^2.0.3", + "y-protocols": "^1.0.6", "yjs": "^13.6.16" } } diff --git a/src/app/component/table.component.ts b/src/app/component/table.component.ts index 66333e73..c73c2b03 100644 --- a/src/app/component/table.component.ts +++ b/src/app/component/table.component.ts @@ -1,6 +1,5 @@ import { Component, OnDestroy, OnInit, inject } from '@angular/core'; import { Router, RouterOutlet } from '@angular/router'; -import { WebsocketProvider } from 'y-websocket'; import { ThyAction } from 'ngx-tethys/action'; import { AITableGrid } from '@ai-table/grid'; import { FormsModule } from '@angular/forms'; @@ -8,6 +7,7 @@ import { ThyPopoverModule } from 'ngx-tethys/popover'; import { ThyTabs, ThyTab } from 'ngx-tethys/tabs'; import { ThyInputDirective } from 'ngx-tethys/input'; import { TableService } from '../service/table.service'; +import { WebsocketProvider } from '../share/y-websocket'; const initViews = [ { _id: 'view1', name: '表格视图1', isActive: true }, diff --git a/src/app/share/y-websocket.ts b/src/app/share/y-websocket.ts index c2423ec1..dbd76c08 100644 --- a/src/app/share/y-websocket.ts +++ b/src/app/share/y-websocket.ts @@ -105,7 +105,7 @@ const setupWS = (provider: WebsocketProvider) => { const messageHandler = provider.messageHandlers[messageType]; if (/** @type {any} */ messageHandler) { const syncMessageType = messageHandler(encoder, decoder, provider, true, messageType); - if (syncMessageType === syncProtocol.messageYjsSyncStep2) { + if (syncMessageType === syncProtocol.messageYjsSyncStep1) { websocket.send(encoding.toUint8Array(encoder)); } } else { @@ -321,7 +321,7 @@ export class WebsocketProvider extends Observable { // resend sync step 1 const encoder = encoding.createEncoder(); encoding.writeVarUint(encoder, messageSync); - + encoding.writeVarString(encoder, this.doc.guid); syncProtocol.writeSyncStep1(encoder, doc); this.ws.send(encoding.toUint8Array(encoder)); } From 9e963830d8f6bca1c5f9c6ccbafddaa6246640ef Mon Sep 17 00:00:00 2001 From: pubuzhixing8 Date: Mon, 5 Aug 2024 19:15:48 +0800 Subject: [PATCH 09/12] feat: add subdocs subscribe --- backend/src/utils.ts | 2 +- src/app/service/table.service.ts | 41 ++++++++++++++++++++------------ src/app/share/shared.ts | 32 ++++++++++++++++++++----- src/app/share/y-websocket.ts | 1 - 4 files changed, 53 insertions(+), 23 deletions(-) diff --git a/backend/src/utils.ts b/backend/src/utils.ts index b50fc7ca..59fc241e 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -198,7 +198,7 @@ const messageListener = (conn, doc: WSSharedDoc, message) => { // 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 (syncMessageType === 0) { + if (syncMessageType === syncProtocol.messageYjsSyncStep1) { send(doc, conn, encoding.toUint8Array(encoder)); } break; diff --git a/src/app/service/table.service.ts b/src/app/service/table.service.ts index f626753b..45ffb183 100644 --- a/src/app/service/table.service.ts +++ b/src/app/service/table.service.ts @@ -10,6 +10,7 @@ import { AITable } from '@ai-table/grid'; import { AITableView } from '../types/view'; import { createDraft, finishDraft } from 'immer'; import { WebsocketProvider } from '../share/y-websocket'; +import { Transaction } from 'yjs'; @Injectable() export class TableService { @@ -69,19 +70,25 @@ export class TableService { let isInitialized = false; if (!this.sharedType) { this.sharedType = createSharedType(room); - this.sharedType.observeDeep((events: any) => { - if (!YjsAITable.isLocal(this.aiTable)) { - if (!isInitialized) { - const data = translateSharedTypeToTable(this.sharedType!); - this.buildRenderRecords(data.records); - this.buildRenderFields(data.fields); - this.views.set(data.views); - isInitialized = true; - } else { - applyYjsEvents(this.aiTable, events); + this.sharedType.observeDeep((events: any, transaction: Transaction) => { + if (transaction.origin !== this.provider.doc) { + if (!YjsAITable.isLocal(this.aiTable)) { + if (!isInitialized) { + const data = translateSharedTypeToTable(this.sharedType!); + this.buildRenderRecords(data.records); + this.buildRenderFields(data.fields); + this.views.set(data.views); + isInitialized = true; + } else { + applyYjsEvents(this.aiTable, events); + } } } }); + this.sharedType.doc.on('subdocs', (subdocs) => { + console.log('subdocs', subdocs); + // [Array.from(subdocs.added).map(x => x.guid), Array.from(subdocs.removed).map(x => x.guid), Array.from(subdocs.loaded).map(x => x.guid)] + }); } this.provider = getProvider(this.sharedType.doc!, room, isDevMode()); this.provider.connect(); @@ -89,11 +96,15 @@ export class TableService { if (this.provider!.synced && [...this.sharedType!.doc!.store.clients.keys()].length === 0) { console.log('init shared type'); const value = getDefaultValue(); - initSharedType(this.sharedType!.doc!, { - records: value.records, - fields: value.fields, - views: this.views() - }); + initSharedType( + this.sharedType!.doc!, + { + records: value.records, + fields: value.fields, + views: this.views() + }, + this.provider + ); } }); } diff --git a/src/app/share/shared.ts b/src/app/share/shared.ts index 64aff7a0..c3984eb3 100644 --- a/src/app/share/shared.ts +++ b/src/app/share/shared.ts @@ -20,10 +20,11 @@ export const initSharedType = ( fields: DemoAIField[]; records: DemoAIRecord[]; views: AITableView[]; - } + }, + origin: any ) => { const sharedType = doc.getMap('ai-table'); - toSharedType(sharedType, initializeValue); + toSharedType(sharedType, initializeValue, origin); return sharedType; }; @@ -33,21 +34,22 @@ export function toSharedType( fields: DemoAIField[]; records: DemoAIRecord[]; views: AITableView[]; - } + }, + origin: any ): void { sharedType.doc!.transact(() => { const fieldSharedType = new Y.Array(); fieldSharedType.insert(0, data.fields.map(toSyncElement)); sharedType.set('fields', fieldSharedType); - const recordSharedType = new Y.Array>(); + const recordSharedType = new Y.Array(); sharedType.set('records', recordSharedType); recordSharedType.insert(0, data.records.map(toRecordSyncElement)); const viewsSharedType = new Y.Array(); sharedType.set('views', viewsSharedType); viewsSharedType.insert(0, data.views.map(toSyncElement)); - }); + }, sharedType.doc); } export function toSyncElement(node: any): SyncMapElement { @@ -78,10 +80,28 @@ export function toRecordSyncElement(record: DemoAIRecord): Y.Array> for (const fieldId in record['values']) { editableFields.push(record['values'][fieldId]); } - editableArray.insert(0, [...editableFields, record['positions']]); + editableArray.insert(0, [...editableFields, record['positions'] || {}]); // To save memory, convert map to array. const element = new Y.Array>(); element.insert(0, [nonEditableArray, editableArray]); return element; } + +export function recordToLive(record: DemoAIRecord): Y.Doc { + const subDoc = new Y.Doc({ guid: record._id }); + const yArray = subDoc.getArray(); + + const nonEditableArray = new Y.Array(); + nonEditableArray.insert(0, [record['_id']]); + + const editableArray = new Y.Array(); + const editableFields = []; + for (const fieldId in record['values']) { + editableFields.push(record['values'][fieldId]); + } + editableArray.insert(0, [...editableFields, record['positions']]); + // To save memory, convert map to array. + yArray.insert(0, [nonEditableArray, editableArray]); + return subDoc; +} diff --git a/src/app/share/y-websocket.ts b/src/app/share/y-websocket.ts index dbd76c08..998a07e8 100644 --- a/src/app/share/y-websocket.ts +++ b/src/app/share/y-websocket.ts @@ -96,7 +96,6 @@ const setupWS = (provider: WebsocketProvider) => { const encoder = encoding.createEncoder(); const messageType = decoding.readVarUint(decoder); const guid = decoding.readVarString(decoder); - console.log('on message from guid: ', guid); if (guid !== provider.doc.guid) { console.log('子文档') const liveBlock = provider.liveBlocks.get(guid); From 60580c3ec90e3f1c1cdc7155568917a3e1fee1b9 Mon Sep 17 00:00:00 2001 From: pubuzhixing8 Date: Thu, 8 Aug 2024 17:25:59 +0800 Subject: [PATCH 10/12] feat: support sync updateFieldValue by liveBlock --- backend/src/utils.ts | 88 ++++++++++++++----- src/app/service/table.service.ts | 35 +++++++- src/app/share/apply-to-table/array-event.ts | 42 ++++++++- src/app/share/apply-to-table/index.ts | 39 ++++++-- src/app/share/apply-to-yjs/add-field.ts | 4 +- src/app/share/apply-to-yjs/add-record.ts | 4 +- src/app/share/apply-to-yjs/index.ts | 4 +- src/app/share/apply-to-yjs/set-view.ts | 3 +- .../share/apply-to-yjs/update-field-value.ts | 24 +++-- src/app/share/live-block-provider.ts | 4 + src/app/share/shared.ts | 29 +++--- src/app/share/utils/translate-to-table.ts | 10 +-- src/app/share/y-websocket.ts | 4 + 13 files changed, 214 insertions(+), 76 deletions(-) diff --git a/backend/src/utils.ts b/backend/src/utils.ts index 59fc241e..3c452777 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -69,21 +69,6 @@ const messageSync = 0; const messageAwareness = 1; // const messageAuth = 2 -/** - * @param {Uint8Array} update - * @param {any} _origin - * @param {WSSharedDoc} doc - * @param {any} _tr - */ -const updateHandler = (update, _origin, doc: WSSharedDoc, _tr) => { - const encoder = encoding.createEncoder(); - encoding.writeVarUint(encoder, messageSync); - encoding.writeVarString(encoder, doc.guid); - syncProtocol.writeUpdate(encoder, update); - const message = encoding.toUint8Array(encoder); - doc.conns.forEach((_, conn) => send(doc, conn, message)); -}; - /** * @type {(ydoc: Y.Doc) => Promise} */ @@ -104,6 +89,9 @@ class WSSharedDoc extends Y.Doc { conns: any; awareness: any; whenInitialized: any; + + liveBlocks = new Map(); + /** * @param {string} name */ @@ -148,7 +136,17 @@ class WSSharedDoc extends Y.Doc { }); }; this.awareness.on('update', awarenessChangeHandler); - this.on('update', /** @type {any} */ updateHandler); + this.on( + 'update', + /** @type {any} */ (update, _origin, doc: WSSharedDoc, _tr) => { + const encoder = encoding.createEncoder(); + encoding.writeVarUint(encoder, messageSync); + encoding.writeVarString(encoder, doc.guid); + syncProtocol.writeUpdate(encoder, update); + const message = encoding.toUint8Array(encoder); + doc.conns.forEach((_, conn) => send(doc, conn, message)); + } + ); // if (isCallbackSet) { // this.on('update', /** @type {any} */ debounce(callbackHandler, CALLBACK_DEBOUNCE_WAIT, { maxWait: CALLBACK_DEBOUNCE_MAXWAIT })); // } @@ -156,6 +154,34 @@ class WSSharedDoc extends Y.Doc { } } +class LiveBlock extends Y.Doc { + rootDoc: WSSharedDoc; + constructor(guid: string, rootDoc: WSSharedDoc) { + super({ gc: gcEnabled, guid, autoLoad: true }); + this.on( + 'update', + /** @type {any} */ (update, _origin, doc: WSSharedDoc, _tr) => { + const encoder = encoding.createEncoder(); + encoding.writeVarUint(encoder, messageSync); + encoding.writeVarString(encoder, doc.guid); + syncProtocol.writeUpdate(encoder, update); + const message = encoding.toUint8Array(encoder); + rootDoc.conns.forEach((_, conn) => send(this, conn, message)); + } + ); + this.rootDoc = rootDoc; + } + + sync(conn) { + // send sync step 1 + const encoder = encoding.createEncoder(); + encoding.writeVarUint(encoder, messageSync); + encoding.writeVarString(encoder, this.guid); + syncProtocol.writeSyncStep1(encoder, this); + send(this, conn, encoding.toUint8Array(encoder)); + } +} + exports.WSSharedDoc = WSSharedDoc; /** @@ -177,13 +203,12 @@ const getYDoc = (docname, gc = true) => { }) as WSSharedDoc; }; - /** * @param {any} conn * @param {WSSharedDoc} doc * @param {Uint8Array} message */ -const messageListener = (conn, doc: WSSharedDoc, message) => { +const messageListener = (conn, rootDoc: WSSharedDoc, message) => { try { const encoder = encoding.createEncoder(); const decoder = decoding.createDecoder(message); @@ -193,17 +218,36 @@ const messageListener = (conn, doc: WSSharedDoc, message) => { case messageSync: encoding.writeVarUint(encoder, messageSync); encoding.writeVarString(encoder, guid); - const syncMessageType = syncProtocol.readSyncMessage(decoder, encoder, doc, conn); + let targetDoc: Y.Doc = null; + if (guid === rootDoc.guid) { + targetDoc = rootDoc; + } else { + let liveBlock = rootDoc.liveBlocks.get(guid); + if (!liveBlock) { + liveBlock = new LiveBlock(guid, rootDoc); + rootDoc.liveBlocks.set(guid, liveBlock); + liveBlock.sync(conn); + } + targetDoc = liveBlock; + } + + const syncMessageType = syncProtocol.readSyncMessage(decoder, encoder, targetDoc, conn); + + if (guid === rootDoc.guid) { + console.log(`root doc(${guid}): ${JSON.stringify(rootDoc.getMap('ai-table').toJSON())}`); + } else { + console.log(`sub doc(${guid}): ${JSON.stringify(targetDoc.getArray('').toJSON())}`); + } // 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 (syncMessageType === syncProtocol.messageYjsSyncStep1) { - send(doc, conn, encoding.toUint8Array(encoder)); + send(targetDoc, conn, encoding.toUint8Array(encoder)); } break; case messageAwareness: { - awarenessProtocol.applyAwarenessUpdate(doc.awareness, decoding.readVarUint8Array(decoder), conn); + awarenessProtocol.applyAwarenessUpdate(rootDoc.awareness, decoding.readVarUint8Array(decoder), conn); break; } } @@ -263,7 +307,7 @@ const pingTimeout = 30000; * @param {import('http').IncomingMessage} req * @param {any} opts */ -export const setupWSConnection = (conn, req, { docName = (req.url || '').slice(1).split('?')[0], gc = true } = {}) => { +export const setupWSConnection = (conn: any, 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); diff --git a/src/app/service/table.service.ts b/src/app/service/table.service.ts index 45ffb183..630c2bd6 100644 --- a/src/app/service/table.service.ts +++ b/src/app/service/table.service.ts @@ -3,14 +3,15 @@ import { createSharedType, initSharedType, SharedType } from '../share/shared'; import { getProvider } from '../share/provider'; import { DemoAIField, DemoAIRecord } from '../types'; import { getDefaultValue, sortDataByView } from '../utils/utils'; -import { applyYjsEvents } from '../share/apply-to-table'; -import { translateSharedTypeToTable } from '../share/utils/translate-to-table'; +import { applySubDocEvents, applyYjsEvents } from '../share/apply-to-table'; +import { translateRecord, translateSharedTypeToTable } from '../share/utils/translate-to-table'; import { YjsAITable } from '../share/yjs-table'; import { AITable } from '@ai-table/grid'; import { AITableView } from '../types/view'; import { createDraft, finishDraft } from 'immer'; import { WebsocketProvider } from '../share/y-websocket'; import { Transaction } from 'yjs'; +import { LiveBlockProvider } from '../share/live-block-provider'; @Injectable() export class TableService { @@ -75,7 +76,7 @@ export class TableService { if (!YjsAITable.isLocal(this.aiTable)) { if (!isInitialized) { const data = translateSharedTypeToTable(this.sharedType!); - this.buildRenderRecords(data.records); + this.buildRenderRecords([]); this.buildRenderFields(data.fields); this.views.set(data.views); isInitialized = true; @@ -87,7 +88,33 @@ export class TableService { }); this.sharedType.doc.on('subdocs', (subdocs) => { console.log('subdocs', subdocs); - // [Array.from(subdocs.added).map(x => x.guid), Array.from(subdocs.removed).map(x => x.guid), Array.from(subdocs.loaded).map(x => x.guid)] + for (const doc of subdocs.added) { + if (!this.provider.liveBlocks.get(doc.guid)) { + const liveBlock = new LiveBlockProvider(doc.guid, this.provider.ws, doc); + this.provider.liveBlocks.set(doc.guid, liveBlock); + liveBlock.sync(); + liveBlock.on('synced', () => { + const recordOfYArray = liveBlock.doc.getArray(); + const formatRecord = recordOfYArray.toJSON(); + const [nonEditableArray, editableArray] = formatRecord; + const record = { + _id: nonEditableArray[0], + positions: editableArray[editableArray.length - 1], + values: translateRecord(editableArray.slice(0, editableArray.length - 1), this.fields()) + }; + const newRecords = [...this.records(), record]; + this.buildRenderRecords(newRecords); + console.log('synced', record); + }); + liveBlock.sharedType.observeDeep((events: any) => { + if (liveBlock.synced) { + if (!YjsAITable.isLocal(this.aiTable)) { + applySubDocEvents(liveBlock, this.aiTable, events); + } + } + }); + } + } }); } this.provider = getProvider(this.sharedType.doc!, room, isDevMode()); diff --git a/src/app/share/apply-to-table/array-event.ts b/src/app/share/apply-to-table/array-event.ts index abf73a78..c19e20e5 100644 --- a/src/app/share/apply-to-table/array-event.ts +++ b/src/app/share/apply-to-table/array-event.ts @@ -2,6 +2,7 @@ import { ActionName, AIFieldValuePath, AITable, AITableAction, AITableField, AIT import * as Y from 'yjs'; import { toTablePath, translateRecord } from '../utils/translate-to-table'; import { isArray } from 'ngx-tethys/util'; +import { LiveBlockProvider } from '../live-block-provider'; export default function translateArrayEvent(aiTable: AITable, event: Y.YEvent): AITableAction[] { const actions: AITableAction[] = []; @@ -9,7 +10,7 @@ export default function translateArrayEvent(aiTable: AITable, event: Y.YEvent { if ('retain' in delta) { offset += delta.retain ?? 0; @@ -60,7 +61,44 @@ export default function translateArrayEvent(aiTable: AITable, event: Y.YEvent): AITableAction[] { + const actions: AITableAction[] = []; + let offset = 0; + let targetPath = toTablePath(event.path); + + console.log(event); + + event.changes.delta.forEach((delta) => { + if ('retain' in delta) { + offset += delta.retain ?? 0; + } + if ('insert' in delta) { + if (isArray(delta.insert)) { + if (targetPath.length) { + try { + delta.insert?.map((item: any) => { + // liveBlock + const recordIndex = aiTable.records().findIndex(((record) => record._id === liveBlock.doc.guid)); + const path = [recordIndex, offset] as AIFieldValuePath; + const fieldValue = AITableQueries.getFieldValue(aiTable, path); + // To exclude insert triggered by field inserts. + if (fieldValue !== item) { + actions.push({ + type: ActionName.UpdateFieldValue, + path, + fieldValue, + newFieldValue: item + }); + } + }); + } catch (error) {} + } } } }); diff --git a/src/app/share/apply-to-table/index.ts b/src/app/share/apply-to-table/index.ts index c453e799..ee974687 100644 --- a/src/app/share/apply-to-table/index.ts +++ b/src/app/share/apply-to-table/index.ts @@ -1,9 +1,10 @@ import * as Y from 'yjs'; import { AITable, AITableAction } from '@ai-table/grid'; -import translateArrayEvent from './array-event'; +import translateArrayEvent, { translateSubArrayEvent } from './array-event'; import { YjsAITable } from '../yjs-table'; import { AIViewAction, AIViewTable } from '../../types/view'; import translateMapEvent from './map-event'; +import { LiveBlockProvider } from '../live-block-provider'; export function translateYjsEvent(aiTable: AITable, event: Y.YEvent): AITableAction[] | AIViewAction[] { if (event instanceof Y.YArrayEvent) { @@ -15,25 +16,45 @@ export function translateYjsEvent(aiTable: AITable, event: Y.YEvent): AITab return []; } -export function applyEvents(aiTable: AITable, events: Y.YEvent[]){ +export function applyEvents(aiTable: AITable, events: Y.YEvent[]) { events.forEach((event) => - translateYjsEvent(aiTable, event).forEach((item: AIViewAction| AITableAction) => { - if(item.type === 'set_view'){ - (aiTable as AIViewTable).viewApply(item) - }else { + translateYjsEvent(aiTable, event).forEach((item: AIViewAction | AITableAction) => { + if (item.type === 'set_view') { + (aiTable as AIViewTable).viewApply(item); + } else { aiTable.apply(item); } - }) ); } export function applyYjsEvents(aiTable: AITable, events: Y.YEvent[]): void { if (YjsAITable.isUndo(aiTable)) { - applyEvents(aiTable, events) + applyEvents(aiTable, events); } else { YjsAITable.asRemote(aiTable, () => { - applyEvents(aiTable, events) + applyEvents(aiTable, events); + }); + } +} + +export function applySubDocEvents(liveBlock: LiveBlockProvider, aiTable: AITable, events: Y.YEvent[]): void { + if (YjsAITable.isUndo(aiTable)) { + applyEvents(aiTable, events); + } else { + YjsAITable.asRemote(aiTable, () => { + events.forEach((event) => { + if (event instanceof Y.YArrayEvent) { + const actions = translateSubArrayEvent(liveBlock, aiTable, event); + actions.forEach((action: AIViewAction | AITableAction) => { + if (action.type === 'set_view') { + (aiTable as AIViewTable).viewApply(action); + } else { + aiTable.apply(action); + } + }); + } + }); }); } } diff --git a/src/app/share/apply-to-yjs/add-field.ts b/src/app/share/apply-to-yjs/add-field.ts index 2072f839..b2b17277 100644 --- a/src/app/share/apply-to-yjs/add-field.ts +++ b/src/app/share/apply-to-yjs/add-field.ts @@ -1,7 +1,7 @@ import { SharedType, SyncArrayElement, toSyncElement } from '../shared'; -import { AddFieldAction, getDefaultFieldValue } from '@ai-table/grid'; +import { AddFieldAction, AITable, getDefaultFieldValue } from '@ai-table/grid'; -export default function addField(sharedType: SharedType, action: AddFieldAction): SharedType { +export default function addField(sharedType: SharedType, action: AddFieldAction, aiTable: AITable): SharedType { const fields = sharedType.get('fields'); const path = action.path[0]; if (fields) { diff --git a/src/app/share/apply-to-yjs/add-record.ts b/src/app/share/apply-to-yjs/add-record.ts index 4a77790e..fff0b771 100644 --- a/src/app/share/apply-to-yjs/add-record.ts +++ b/src/app/share/apply-to-yjs/add-record.ts @@ -1,8 +1,8 @@ import { DemoAIRecord } from '../../types'; import { SharedType, toRecordSyncElement } from '../shared'; -import { AddRecordAction } from '@ai-table/grid'; +import { AddRecordAction, AITable } from '@ai-table/grid'; -export default function addRecord(sharedType: SharedType, action: AddRecordAction): SharedType { +export default function addRecord(sharedType: SharedType, action: AddRecordAction, aiTable: AITable): SharedType { const records = sharedType.get('records'); if (records) { const path = action.path[0]; diff --git a/src/app/share/apply-to-yjs/index.ts b/src/app/share/apply-to-yjs/index.ts index 0163ff02..79bbf023 100644 --- a/src/app/share/apply-to-yjs/index.ts +++ b/src/app/share/apply-to-yjs/index.ts @@ -9,7 +9,7 @@ export type ActionMapper = { [K in O['type']]: O extends { type: K } ? ApplyFunc : never; }; -export type ApplyFunc = (sharedType: SharedType, op: O) => SharedType; +export type ApplyFunc = (sharedType: SharedType, op: O, aiTable: AITable) => SharedType; export const actionMappers: Partial> = { update_field_value: updateFieldValue, @@ -24,7 +24,7 @@ export default function applyActionOps(sharedType: SharedType, actions: AITableA actions.forEach((action) => { const apply = actionMappers[action.type] as ApplyFunc; if (apply) { - return apply(sharedType, action); + return apply(sharedType, action, aiTable); } return null; }); diff --git a/src/app/share/apply-to-yjs/set-view.ts b/src/app/share/apply-to-yjs/set-view.ts index 3ed0e2dd..1acec3a4 100644 --- a/src/app/share/apply-to-yjs/set-view.ts +++ b/src/app/share/apply-to-yjs/set-view.ts @@ -2,8 +2,9 @@ import { isObject } from "ngx-tethys/util"; import { AIViewAction } from "../../types/view"; import { SharedType, toSyncElement } from "../shared"; +import { AITable } from "@ai-table/grid"; -export default function setView(sharedType: SharedType, action: AIViewAction): SharedType { +export default function setView(sharedType: SharedType, action: AIViewAction, aiTable: AITable): SharedType { const views = sharedType.get('views'); if (views) { const index = action.path[0]; diff --git a/src/app/share/apply-to-yjs/update-field-value.ts b/src/app/share/apply-to-yjs/update-field-value.ts index d7f78955..67e2f028 100644 --- a/src/app/share/apply-to-yjs/update-field-value.ts +++ b/src/app/share/apply-to-yjs/update-field-value.ts @@ -1,15 +1,21 @@ +import { Array } from 'yjs'; +import { LiveBlockProvider } from '../live-block-provider'; import { SharedType, SyncArrayElement } from '../shared'; -import { UpdateFieldValueAction } from '@ai-table/grid'; +import { AITable, getRecordOrField, UpdateFieldValueAction } from '@ai-table/grid'; -export default function updateFieldValue(sharedType: SharedType, action: UpdateFieldValueAction): SharedType { - const records = sharedType.get('records'); - if (records) { - const record = records?.get(action.path[0]) as SyncArrayElement; - const customField = record.get(1); +export default function updateFieldValue(sharedType: SharedType, action: UpdateFieldValueAction, aiTable: AITable): SharedType { + // const records = sharedType.get('records'); + const record = aiTable.records()[action.path[0]] + if (record && sharedType.doc['liveBlocks']) { + const liveBlock = sharedType.doc['liveBlocks'].get(record._id) as LiveBlockProvider; + const recordArray = liveBlock.doc.getArray(); + // const record = records?.get(action.path[0]) as SyncArrayElement; + const customField = recordArray.get(1) as Array; const index = action.path[1]; - customField.delete(index); - customField.insert(index, [action.newFieldValue]); + liveBlock.doc.transact(() => { + customField.delete(index); + customField.insert(index, [action.newFieldValue]); + }); } - return sharedType; } diff --git a/src/app/share/live-block-provider.ts b/src/app/share/live-block-provider.ts index 9a9b15a9..c01cf7ca 100644 --- a/src/app/share/live-block-provider.ts +++ b/src/app/share/live-block-provider.ts @@ -93,6 +93,10 @@ export class LiveBlockProvider extends Observable { encoding.writeVarUint(encoder, messageSync); encoding.writeVarString(encoder, this.doc.guid); syncProtocol.writeUpdate(encoder, update); + if (this.ws) { + /** @type {WebSocket} */ ws.send(encoding.toUint8Array(encoder)); + console.log('send sub doc updates'); + } } }); } diff --git a/src/app/share/shared.ts b/src/app/share/shared.ts index c3984eb3..bdcdb53a 100644 --- a/src/app/share/shared.ts +++ b/src/app/share/shared.ts @@ -44,7 +44,7 @@ export function toSharedType( const recordSharedType = new Y.Array(); sharedType.set('records', recordSharedType); - recordSharedType.insert(0, data.records.map(toRecordSyncElement)); + recordSharedType.insert(0, data.records.map(recordToLive)); const viewsSharedType = new Y.Array(); sharedType.set('views', viewsSharedType); @@ -89,19 +89,20 @@ export function toRecordSyncElement(record: DemoAIRecord): Y.Array> } export function recordToLive(record: DemoAIRecord): Y.Doc { - const subDoc = new Y.Doc({ guid: record._id }); - const yArray = subDoc.getArray(); + const subDoc = new Y.Doc({ guid: record._id, autoLoad: true }); + subDoc.transact(() => { + const yArray = subDoc.getArray(); + const nonEditableArray = new Y.Array(); + nonEditableArray.insert(0, [record['_id']]); - const nonEditableArray = new Y.Array(); - nonEditableArray.insert(0, [record['_id']]); - - const editableArray = new Y.Array(); - const editableFields = []; - for (const fieldId in record['values']) { - editableFields.push(record['values'][fieldId]); - } - editableArray.insert(0, [...editableFields, record['positions']]); - // To save memory, convert map to array. - yArray.insert(0, [nonEditableArray, editableArray]); + const editableArray = new Y.Array(); + const editableFields = []; + for (const fieldId in record['values']) { + editableFields.push(record['values'][fieldId]); + } + editableArray.insert(0, [...editableFields, record['positions']]); + // To save memory, convert map to array. + yArray.insert(0, [nonEditableArray, editableArray]); + }); return subDoc; } diff --git a/src/app/share/utils/translate-to-table.ts b/src/app/share/utils/translate-to-table.ts index 9e68541b..624eb345 100644 --- a/src/app/share/utils/translate-to-table.ts +++ b/src/app/share/utils/translate-to-table.ts @@ -1,6 +1,7 @@ import { AITableFields, Path } from '@ai-table/grid'; import { SharedType } from '../shared'; import { DemoAIField, DemoAIRecord } from '../../types'; +import { Doc } from 'yjs'; export const translateRecord = (arrayRecord: any[], fields: AITableFields) => { const fieldIds = fields.map((item) => item._id); @@ -14,17 +15,8 @@ export const translateRecord = (arrayRecord: any[], fields: AITableFields) => { export const translateSharedTypeToTable = (sharedType: SharedType) => { const data = sharedType.toJSON(); const fields: DemoAIField[] = data['fields']; - const records: DemoAIRecord[] = data['records'].map((record: any) => { - const [nonEditableArray, editableArray] = record; - return { - _id: nonEditableArray[0], - positions: editableArray[editableArray.length - 1], - values: translateRecord(editableArray.slice(0, editableArray.length - 1), fields) - }; - }); const views = data['views']; return { - records, fields, views }; diff --git a/src/app/share/y-websocket.ts b/src/app/share/y-websocket.ts index 998a07e8..b813ff99 100644 --- a/src/app/share/y-websocket.ts +++ b/src/app/share/y-websocket.ts @@ -99,6 +99,9 @@ const setupWS = (provider: WebsocketProvider) => { if (guid !== provider.doc.guid) { console.log('子文档') const liveBlock = provider.liveBlocks.get(guid); + if (liveBlock) { + liveBlock.readMessage(messageType, encoder, decoder) + } return; } const messageHandler = provider.messageHandlers[messageType]; @@ -286,6 +289,7 @@ export class WebsocketProvider extends Observable { this.serverUrl = serverUrl; this.bcChannel = serverUrl + '/' + roomname; this.doc = doc; + this.doc['liveBlocks'] = this.liveBlocks; this.params = params; this._WS = WebSocketPolyfill; this.awareness = awareness; From 9f2e66089f24d3c5724cf085d6262075f989880d Mon Sep 17 00:00:00 2001 From: pubuzhixing8 Date: Mon, 12 Aug 2024 16:50:38 +0800 Subject: [PATCH 11/12] chore: xxx --- backend/src/utils.ts | 2 +- src/app/service/table.service.ts | 12 ++++++++--- src/app/share/apply-to-table/array-event.ts | 24 ++++++++++----------- src/app/share/apply-to-yjs/add-record.ts | 7 +++--- src/app/share/live-block-provider.ts | 1 + 5 files changed, 27 insertions(+), 19 deletions(-) diff --git a/backend/src/utils.ts b/backend/src/utils.ts index 3c452777..1aeaa03f 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -236,7 +236,7 @@ const messageListener = (conn, rootDoc: WSSharedDoc, message) => { if (guid === rootDoc.guid) { console.log(`root doc(${guid}): ${JSON.stringify(rootDoc.getMap('ai-table').toJSON())}`); } else { - console.log(`sub doc(${guid}): ${JSON.stringify(targetDoc.getArray('').toJSON())}`); + console.log(`sub doc(${guid})-${syncMessageType}: ${JSON.stringify(targetDoc.getArray('').toJSON())}`); } // If the `encoder` only contains the type of reply message and no diff --git a/src/app/service/table.service.ts b/src/app/service/table.service.ts index 630c2bd6..f79455a4 100644 --- a/src/app/service/table.service.ts +++ b/src/app/service/table.service.ts @@ -97,17 +97,23 @@ export class TableService { const recordOfYArray = liveBlock.doc.getArray(); const formatRecord = recordOfYArray.toJSON(); const [nonEditableArray, editableArray] = formatRecord; + if (!nonEditableArray || !editableArray) { + return; + } const record = { _id: nonEditableArray[0], positions: editableArray[editableArray.length - 1], values: translateRecord(editableArray.slice(0, editableArray.length - 1), this.fields()) }; - const newRecords = [...this.records(), record]; - this.buildRenderRecords(newRecords); - console.log('synced', record); + if (this.records().findIndex((_record) => _record._id === record._id) === -1) { + const newRecords = [...this.records(), record]; + this.buildRenderRecords(newRecords); + console.log('synced', record); + } }); liveBlock.sharedType.observeDeep((events: any) => { if (liveBlock.synced) { + console.log(events, 'subdoc changed'); if (!YjsAITable.isLocal(this.aiTable)) { applySubDocEvents(liveBlock, this.aiTable, events); } diff --git a/src/app/share/apply-to-table/array-event.ts b/src/app/share/apply-to-table/array-event.ts index c19e20e5..ce7beede 100644 --- a/src/app/share/apply-to-table/array-event.ts +++ b/src/app/share/apply-to-table/array-event.ts @@ -35,18 +35,18 @@ export default function translateArrayEvent(aiTable: AITable, event: Y.YEvent, index) => { - const data = item.toJSON(); - const [fixedField, customField] = data; - actions.push({ - type: ActionName.AddRecord, - path: [offset + index], - record: { - _id: fixedField[0], - values: translateRecord(customField, aiTable.fields()) - } - }); - }); + // delta.insert?.map((item: Y.Array, index) => { + // const data = item.toJSON(); + // const [fixedField, customField] = data; + // actions.push({ + // type: ActionName.AddRecord, + // path: [offset + index], + // record: { + // _id: fixedField[0], + // values: translateRecord(customField, aiTable.fields()) + // } + // }); + // }); } } if (isFieldsTranslate) { diff --git a/src/app/share/apply-to-yjs/add-record.ts b/src/app/share/apply-to-yjs/add-record.ts index fff0b771..8eb2d205 100644 --- a/src/app/share/apply-to-yjs/add-record.ts +++ b/src/app/share/apply-to-yjs/add-record.ts @@ -1,12 +1,13 @@ +import { Array } from 'yjs'; import { DemoAIRecord } from '../../types'; -import { SharedType, toRecordSyncElement } from '../shared'; +import { recordToLive, SharedType } from '../shared'; import { AddRecordAction, AITable } from '@ai-table/grid'; export default function addRecord(sharedType: SharedType, action: AddRecordAction, aiTable: AITable): SharedType { - const records = sharedType.get('records'); + const records = sharedType.get('records') as Array; if (records) { const path = action.path[0]; - records.insert(path, [toRecordSyncElement(action.record as DemoAIRecord)]); + records.insert(path, [recordToLive(action.record as DemoAIRecord)]); } return sharedType; } diff --git a/src/app/share/live-block-provider.ts b/src/app/share/live-block-provider.ts index c01cf7ca..ccd1f4d1 100644 --- a/src/app/share/live-block-provider.ts +++ b/src/app/share/live-block-provider.ts @@ -61,6 +61,7 @@ const permissionDeniedHandler = (provider: LiveBlockProvider, reason) => { const syncLiveBlock = (doc: Y.Doc, websocket) => { // always send sync step 1 when connected + console.log(`send step 1: ${doc.guid}`) const encoder = encoding.createEncoder(); encoding.writeVarUint(encoder, messageSync); encoding.writeVarString(encoder, doc.guid); From 526db8d458759a9a98abaca813ddb2b4553dfe2f Mon Sep 17 00:00:00 2001 From: pubuzhixing8 Date: Mon, 19 Aug 2024 14:53:24 +0800 Subject: [PATCH 12/12] live-feed --- src/app/share/live-feed/feed-object.ts | 64 +++++++ src/app/share/live-feed/provider.ts | 243 +++++++++++++++++++++++++ 2 files changed, 307 insertions(+) create mode 100644 src/app/share/live-feed/feed-object.ts create mode 100644 src/app/share/live-feed/provider.ts diff --git a/src/app/share/live-feed/feed-object.ts b/src/app/share/live-feed/feed-object.ts new file mode 100644 index 00000000..066f1040 --- /dev/null +++ b/src/app/share/live-feed/feed-object.ts @@ -0,0 +1,64 @@ +import * as Y from 'yjs'; +import { Observable } from 'lib0/observable'; + +// 尽量让使用方感受不到任何 subdoc 的概念 + +/** + * 可协同的对象 + */ +export class LiveFeedObject extends Y.Doc { + typeName: string; + + constructor(options: { guid: string; typeName: string }) { + super({ guid: options.guid }); + this.typeName = options.typeName; + } + + // 1.存储数据 + // 2.本地数据变化后同步给协同方 + // 3.远程数据变化后应用到本地数据 +} + +export class LiveFeedRoom extends Observable { + objects: Map = new Map(); + guid: string; + + constructor(options: { guid: string; objects: LiveFeedObject[] }) { + super(); + this.guid = options.guid; + } + + initObjects(objects: LiveFeedObject[]) { + objects.forEach((object: LiveFeedObject) => { + this.addObject(object); + }); + } + + addObject(object: LiveFeedObject) { + this.objects.set(object.guid, object); + object.get(object.typeName).observeDeep((events: Array>, transaction: Y.Transaction) => { + this.emit('change', [ + { + events, + transaction, + guid: object.guid + } + ]); + }); + object.on('update', (update: Uint8Array, arg1: any, doc: Y.Doc, transaction: Y.Transaction) => { + this.emit('update', [ + { + update, + transaction, + guid: object.guid + } + ]); + }); + } + + removeObject(object: LiveFeedObject) { + this.objects.delete(object.guid); + // object.off('update', ); + object.destroy(); + } +} diff --git a/src/app/share/live-feed/provider.ts b/src/app/share/live-feed/provider.ts new file mode 100644 index 00000000..4c7268f7 --- /dev/null +++ b/src/app/share/live-feed/provider.ts @@ -0,0 +1,243 @@ +/* +Unlike stated in the LICENSE file, it is not necessary to include the copyright notice and permission notice when you copy code from this file. +*/ + +/** + * @module provider/websocket + */ + +/* eslint-env browser */ +import * as Y from 'yjs'; // eslint-disable-line +import * as bc from 'lib0/broadcastchannel'; +import * as time from 'lib0/time'; +import * as encoding from 'lib0/encoding'; +import * as syncProtocol from 'y-protocols/sync'; +import * as awarenessProtocol from 'y-protocols/awareness'; +import * as mutex from 'lib0/mutex'; +import { Observable } from 'lib0/observable'; +import * as math from 'lib0/math'; +import * as url from 'lib0/url'; +import { LiveFeedObject, LiveFeedRoom } from './feed-object'; + +const messageSync = 0; +const messageQueryAwareness = 3; +const messageAwareness = 1; +const messageAuth = 2; + +const reconnectTimeoutBase = 1200; +const maxReconnectTimeout = 12000; +const messageReconnectTimeout = 60000; + +const setupWS = (provider: LiveFeedProvider) => { + if (provider.shouldConnect && provider.ws === null) { + const websocket = new provider._WS(provider.url); + websocket.binaryType = 'arraybuffer'; + provider.ws = websocket; + provider.wsconnecting = true; + provider.wsconnected = false; + provider.synced = false; + + websocket.onmessage = (event) => { + const { data } = event; + if (typeof data === 'object') { + + } + }; + websocket.onclose = () => { + provider.ws = null; + provider.wsconnecting = false; + if (provider.wsconnected) { + provider.wsconnected = false; + provider.synced = false; + provider.emit('status', [ + { + status: 'disconnected' + } + ]); + } else { + provider.wsUnsuccessfulReconnects++; + } + // Start with no reconnect timeout and increase timeout by + // log10(wsUnsuccessfulReconnects). + // The idea is to increase reconnect timeout slowly and have no reconnect + // timeout at the beginning (log(1) = 0) + setTimeout( + setupWS, + math.min(math.log10(provider.wsUnsuccessfulReconnects + 1) * reconnectTimeoutBase, maxReconnectTimeout), + provider + ); + }; + websocket.onopen = () => { + provider.wsLastMessageReceived = time.getUnixTime(); + provider.wsconnecting = false; + provider.wsconnected = true; + provider.wsUnsuccessfulReconnects = 0; + provider.emit('status', [ + { + status: 'connected' + } + ]); + // sync ydoc + const _syncInterval = setInterval(() => { + if (!provider.synced && provider.wsconnected) { + // sync ydoc + } else { + clearInterval(_syncInterval); + } + }, 1000); + }; + + provider.emit('status', [ + { + status: 'connecting' + } + ]); + } +}; + +export class LiveFeedProvider extends Observable { + roomname: any; + serverUrl: string; + params: any; + _WS: { + new (url: string, protocols?: string | string[]): WebSocket; + prototype: WebSocket; + readonly CLOSED: number; + readonly CLOSING: number; + readonly CONNECTING: number; + readonly OPEN: number; + }; + awareness: awarenessProtocol.Awareness; + wsconnected: boolean; + wsconnecting: boolean; + bcconnected: boolean; + wsUnsuccessfulReconnects: number; + messageHandlers: any[]; + mux: mutex.mutex; + _synced: boolean; + ws: any; + wsLastMessageReceived: number; + shouldConnect: boolean; + _resyncInterval: number; + _bcSubscriber: (data: any) => void; + _updateHandler: (update: any, origin: any) => void; + _awarenessUpdateHandler: ({ added, updated, removed }: { added: any; updated: any; removed: any }, origin: any) => void; + _beforeUnloadHandler: () => void; + _checkInterval: any; + room: LiveFeedRoom; + /** + * @param {string} serverUrl + * @param {string} roomname + * @param {Y.Doc} doc + * @param {object} [opts] + * @param {boolean} [opts.connect] + * @param {awarenessProtocol.Awareness} [opts.awareness] + * @param {Object} [opts.params] + * @param {typeof WebSocket} [opts.WebSocketPolyfill] Optionall provide a WebSocket polyfill + * @param {number} [opts.resyncInterval] Request server state every `resyncInterval` milliseconds + */ + constructor( + serverUrl, + roomname, + { + connect = true, + params = {}, + WebSocketPolyfill = WebSocket, + resyncInterval = -1 + } = {} + ) { + super(); + // ensure that url is always ends with / + while (serverUrl[serverUrl.length - 1] === '/') { + serverUrl = serverUrl.slice(0, serverUrl.length - 1); + } + this.roomname = roomname; + this.serverUrl = serverUrl; + this.params = params; + this._WS = WebSocketPolyfill; + this.wsconnected = false; + this.wsconnecting = false; + this.bcconnected = false; + this.wsUnsuccessfulReconnects = 0; + this.mux = mutex.createMutex(); + /** + * @type {boolean} + */ + this._synced = false; + /** + * @type {WebSocket?} + */ + this.ws = null; + this.wsLastMessageReceived = 0; + /** + * Whether to connect to other peers or not + * @type {boolean} + */ + this.shouldConnect = connect; + + /** + * @type {number} + */ + this._resyncInterval = 0; + if (resyncInterval > 0) { + this._resyncInterval = /** @type {any} */ setInterval(() => { + if (this.ws) { + // resend sync step 1 + } + }, resyncInterval) as any; + } + this._checkInterval = /** @type {any} */ setInterval(() => { + if (this.wsconnected && messageReconnectTimeout < time.getUnixTime() - this.wsLastMessageReceived) { + // no message received in a long time - not even your own awareness + // updates (which are updated every 15 seconds) + /** @type {WebSocket} */ this.ws.close(); + } + }, messageReconnectTimeout / 10); + if (connect) { + this.connect(); + } + } + + get url() { + const encodedParams = url.encodeQueryParams(this.params); + return this.serverUrl + '/' + this.roomname + (encodedParams.length === 0 ? '' : '?' + encodedParams); + } + + /** + * @type {boolean} + */ + get synced() { + return this._synced; + } + + set synced(state) { + if (this._synced !== state) { + this._synced = state; + this.emit('synced', [state]); + this.emit('sync', [state]); + } + } + + override destroy() { + if (this._resyncInterval !== 0) { + clearInterval(this._resyncInterval); + } + clearInterval(this._checkInterval); + this.disconnect(); + super.destroy(); + } + + disconnect() { + this.shouldConnect = false; + if (this.ws !== null) { + this.ws.close(); + } + } + + connect() { + this.shouldConnect = true; + if (!this.wsconnected && this.ws === null) { + setupWS(this); + } + } +} \ No newline at end of file