Various sync & time travel fixes

This commit is contained in:
Marcel Mraz 2024-12-23 16:48:45 +01:00
parent 6a17541713
commit 1abb901ec2
No known key found for this signature in database
GPG key ID: 4EBD6E62DC830CD2
5 changed files with 46 additions and 37 deletions

View file

@ -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();

View file

@ -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(

View file

@ -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",

View file

@ -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<string, StoreIncrement> =
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<SERVER_INCREMENT> }) {
@ -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<string>; message: string }) {

View file

@ -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"