diff --git a/excalidraw-app/App.tsx b/excalidraw-app/App.tsx index ca3bb47157..369408e731 100644 --- a/excalidraw-app/App.tsx +++ b/excalidraw-app/App.tsx @@ -139,6 +139,7 @@ import type { ElementsChange } from "../packages/excalidraw/change"; import Slider from "rc-slider"; import "rc-slider/assets/index.css"; +import { SyncClient } from "../packages/excalidraw/sync/client"; polyfill(); @@ -388,7 +389,7 @@ const ExcalidrawWrapper = () => { syncAPI?.connect(); return () => { - syncAPI?.disconnect(); + syncAPI?.disconnect(SyncClient.NORMAL_CLOSURE); clearInterval(interval); }; }, [syncAPI]); @@ -885,9 +886,11 @@ const ExcalidrawWrapper = () => { max={acknowledgedIncrements.length} value={nextVersion === -1 ? acknowledgedIncrements.length : nextVersion} onChange={(value) => { - if (value !== acknowledgedIncrements.length - 1) { + // CFDO: should be disabled when offline! (later we could have speculative changes in the versioning log as well) + // CFDO: in safari the whole canvas gets selected when dragging + if (value !== acknowledgedIncrements.length) { // don't listen to updates in the detached mode - syncAPI?.disconnect(); + syncAPI?.disconnect(SyncClient.NORMAL_CLOSURE); } else { // reconnect once we're back to the latest version syncAPI?.connect(); diff --git a/packages/excalidraw/cloudflare/repository.ts b/packages/excalidraw/cloudflare/repository.ts index b441cf58a0..491db83087 100644 --- a/packages/excalidraw/cloudflare/repository.ts +++ b/packages/excalidraw/cloudflare/repository.ts @@ -8,7 +8,7 @@ import type { export class DurableIncrementsRepository implements IncrementsRepository { constructor(private storage: DurableObjectStorage) { // #region DEV ONLY - this.storage.sql.exec(`DROP TABLE IF EXISTS increments;`); + // this.storage.sql.exec(`DROP TABLE IF EXISTS increments;`); // #endregion this.storage.sql.exec(`CREATE TABLE IF NOT EXISTS increments( diff --git a/packages/excalidraw/package.json b/packages/excalidraw/package.json index e2a13ae61e..fae33ef5cd 100644 --- a/packages/excalidraw/package.json +++ b/packages/excalidraw/package.json @@ -106,7 +106,6 @@ "@testing-library/jest-dom": "5.16.2", "@testing-library/react": "16.0.0", "@types/async-lock": "^1.4.2", - "@types/lodash.debounce": "4.0.9", "@types/pako": "1.0.3", "@types/pica": "5.1.3", "@types/resize-observer-browser": "0.1.7", diff --git a/packages/excalidraw/sync/client.ts b/packages/excalidraw/sync/client.ts index 9bab1c8ecd..866b1fad56 100644 --- a/packages/excalidraw/sync/client.ts +++ b/packages/excalidraw/sync/client.ts @@ -1,5 +1,4 @@ /* eslint-disable no-console */ -import debounce from "lodash.debounce"; import throttle from "lodash.throttle"; import ReconnectingWebSocket, { type Event, @@ -19,11 +18,18 @@ import type { PUSH_PAYLOAD, SERVER_INCREMENT, } from "./protocol"; +import { debounce } from "../utils"; -const NO_STATUS_RECEIVED_ERROR_CODE = 1005; -const ABNORMAL_CLOSURE_ERROR_CODE = 1006; +interface AcknowledgedIncrement { + increment: StoreIncrement; + version: number; +} export class SyncClient { + public static readonly NORMAL_CLOSURE = 1000; + public static readonly NO_STATUS_RECEIVED_ERROR_CODE = 1005; + public static readonly ABNORMAL_CLOSURE_ERROR_CODE = 1006; + private static readonly HOST_URL = import.meta.env.DEV ? "ws://localhost:8787" : "https://excalidraw-sync.marcel-529.workers.dev"; @@ -38,11 +44,15 @@ export class SyncClient { private readonly repository: MetadataRepository; // CFDO: shouldn't be stateful, only request / response - private readonly acknowledgedIncrementsMap: Map = - new Map(); + private readonly acknowledgedIncrementsMap: Map< + string, + AcknowledgedIncrement + > = new Map(); public get acknowledgedIncrements() { - return Array.from(this.acknowledgedIncrementsMap.values()); + return Array.from(this.acknowledgedIncrementsMap.values()) + .sort((a, b) => (a.version < b.version ? -1 : 1)) + .map((x) => x.increment); } private readonly roomId: string; @@ -87,7 +97,6 @@ export class SyncClient { }); } - // CFDO I: throttle does not work that well here (after some period it tries to reconnect too often) public connect = throttle( () => { if (this.server && this.server.readyState !== this.server.CLOSED) { @@ -121,7 +130,7 @@ export class SyncClient { ); public disconnect = throttle( - (code?: number, reason?: string) => { + (code: number, reason?: string) => { if (!this.server) { return; } @@ -134,7 +143,9 @@ export class SyncClient { } console.log( - `Disconnecting from the sync server with code "${code}" and reason "${reason}"...`, + `Disconnecting from the sync server with code "${code}"${ + reason ? ` and reason "${reason}".` : "." + }`, ); this.server.removeEventListener("message", this.onMessage); this.server.removeEventListener("open", this.onOpen); @@ -153,7 +164,7 @@ export class SyncClient { private onClose = (event: CloseEvent) => { this.disconnect( - event.code || NO_STATUS_RECEIVED_ERROR_CODE, + event.code || SyncClient.NO_STATUS_RECEIVED_ERROR_CODE, event.reason || "Connection closed without a reason", ); }; @@ -161,8 +172,8 @@ export class SyncClient { private onError = (event: Event) => { this.disconnect( event.type === "error" - ? ABNORMAL_CLOSURE_ERROR_CODE - : NO_STATUS_RECEIVED_ERROR_CODE, + ? SyncClient.ABNORMAL_CLOSURE_ERROR_CODE + : SyncClient.NO_STATUS_RECEIVED_ERROR_CODE, `Received "${event.type}" on the sync connection`, ); }; @@ -227,12 +238,9 @@ export class SyncClient { }); } - // CFDO: should be flushed once regular push / pull goes through - private schedulePush = (ms: number = 1000) => - debounce(this.push, ms, { leading: true, trailing: false }); - - private schedulePull = (ms: number = 1000) => - debounce(this.pull, ms, { leading: true, trailing: false }); + // CFDO: could be flushed once regular push / pull goes through + private schedulePush = debounce(() => this.push(), 1000); + private schedulePull = debounce(() => this.pull(), 1000); // CFDO: refactor by applying all operations to store, not to the elements private handleAcknowledged(payload: { increments: Array }) { @@ -246,11 +254,12 @@ export class SyncClient { const { increments: remoteIncrements } = payload; // apply remote increments - for (const { id, version, payload } of remoteIncrements.sort((a, b) => - a.version <= b.version ? -1 : 1, - )) { + for (const { id, version, payload } of remoteIncrements) { // CFDO: temporary to load all increments on init - this.acknowledgedIncrementsMap.set(id, StoreIncrement.load(payload)); + this.acknowledgedIncrementsMap.set(id, { + increment: StoreIncrement.load(payload), + version, + }); // local increment shall not have to be applied again if (this.queue.has(id)) { @@ -301,11 +310,11 @@ export class SyncClient { this.lastAcknowledgedVersion = nextAcknowledgedVersion; } catch (e) { console.error("Failed to apply acknowledged increments:", e); - this.schedulePull().call(this); + this.schedulePull(); return; } - this.schedulePush().call(this); + this.schedulePush(); } private handleRejected(payload: { ids: Array; message: string }) { diff --git a/yarn.lock b/yarn.lock index 8c78fe9785..f9b190b362 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3383,13 +3383,6 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/lodash.debounce@4.0.9": - version "4.0.9" - resolved "https://registry.yarnpkg.com/@types/lodash.debounce/-/lodash.debounce-4.0.9.tgz#0f5f21c507bce7521b5e30e7a24440975ac860a5" - integrity sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ== - dependencies: - "@types/lodash" "*" - "@types/lodash.throttle@4.1.7": version "4.1.7" resolved "https://registry.yarnpkg.com/@types/lodash.throttle/-/lodash.throttle-4.1.7.tgz#4ef379eb4f778068022310ef166625f420b6ba58" @@ -7971,7 +7964,7 @@ lodash.camelcase@^4.3.0: resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== -lodash.debounce@4.0.8, lodash.debounce@^4.0.8: +lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== @@ -9409,6 +9402,11 @@ rechoir@^0.7.0: dependencies: resolve "^1.9.0" +reconnecting-websocket@4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz#3b0e5b96ef119e78a03135865b8bb0af1b948783" + integrity sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng== + redent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f"