From 3d783ed8b66bcf8979fd8088e02c48540888710c Mon Sep 17 00:00:00 2001 From: deathg0d Date: Mon, 2 Sep 2024 01:19:58 +0545 Subject: [PATCH] Cache (#3) * - Replaced provider.destroyHandler with provider.destroy - Emit connection closed information from peer * Edited Readme with destroy handler changes * incremented version to 2.0.0 added idb-keyval to project * Added indexedDB support, protect data from unexpected provider destroys * re-build distribution files --- README.md | 3 ++- dist/provider.d.ts | 6 +++-- dist/provider.d.ts.map | 2 +- dist/provider.js | 56 ++++++++++++++++++++++++++++++++------- dist/webrtc.d.ts | 11 ++++++++ dist/webrtc.d.ts.map | 2 +- dist/webrtc.js | 19 +++++++------ package-lock.json | 10 +++++-- package.json | 11 +++++--- src/provider.ts | 60 +++++++++++++++++++++++++++++++++--------- src/webrtc.ts | 24 ++++++++++++----- 11 files changed, 157 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 220594c..e10d793 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,8 @@ new FireProvider({ #### Methods -- **destroyHandler**: Destroys the y-fire instance. You may want to destroy the y-fire instance when navigating out of the page to avoid the initialization of duplicate instances. Use `provider.destroyHandler();` to destroy the instance. +- **destroy**: Destroys the y-fire instance. You may want to destroy the y-fire instance when navigating out of the page to avoid the initialization of duplicate instances. Use `provider.destroy();` to destroy the instance. +- ~~**destroyHandler**: Destroys the y-fire instance. You may want to destroy the y-fire instance when navigating out of the page to avoid the initialization of duplicate instances. Use `provider.destroyHandler();` to destroy the instance.~~ (Replaced with **destroy**) #### Events diff --git a/dist/provider.d.ts b/dist/provider.d.ts index 630dbc2..7a81faf 100644 --- a/dist/provider.d.ts +++ b/dist/provider.d.ts @@ -60,8 +60,10 @@ export declare class FireProvider extends ObservableV2 { onReady: () => void; onDeleted: () => void; onSaving: (status: boolean) => void; - private destroyHandler; init: () => Promise; + syncLocal: () => Promise; + saveToLocal: () => Promise; + deleteLocal: () => Promise; initiateHandler: () => void; trackData: () => void; trackMesh: () => void; @@ -73,7 +75,7 @@ export declare class FireProvider extends ObservableV2 { message: unknown; data: Uint8Array | null; }) => void; - saveToFirestore: () => void; + saveToFirestore: () => Promise; sendToFirestoreQueue: () => void; sendCache: (from: string) => void; sendToQueue: ({ from, update }: { diff --git a/dist/provider.d.ts.map b/dist/provider.d.ts.map index 7b0c0d0..f0fce07 100644 --- a/dist/provider.d.ts.map +++ b/dist/provider.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/provider.js b/dist/provider.js index fd3db28..94014e6 100644 --- a/dist/provider.js +++ b/dist/provider.js @@ -12,6 +12,7 @@ import { collection } from "firebase/firestore"; import * as Y from "yjs"; import { ObservableV2 } from "lib0/observable"; import * as awarenessProtocol from "y-protocols/awareness"; +import { get as getLocal, set as setLocal, del as delLocal } from "idb-keyval"; import { deleteInstance, initiateInstance, refreshPeers } from "./utils"; import { WebRtc } from "./webrtc"; import { createGraph } from "./graph"; @@ -61,6 +62,33 @@ export class FireProvider extends ObservableV2 { this.kill(true); // destroy provider but keep the read-only stream alive } }); + this.syncLocal = () => __awaiter(this, void 0, void 0, function* () { + try { + const local = yield getLocal(this.documentPath); + if (local) + Y.applyUpdate(this.doc, local, { key: "local-sync" }); + } + catch (e) { + this.consoleHandler("get local error", e); + } + }); + this.saveToLocal = () => __awaiter(this, void 0, void 0, function* () { + try { + const currentDoc = Y.encodeStateAsUpdate(this.doc); + setLocal(this.documentPath, currentDoc); + } + catch (e) { + this.consoleHandler("set local error", e); + } + }); + this.deleteLocal = () => __awaiter(this, void 0, void 0, function* () { + try { + delLocal(this.documentPath); + } + catch (e) { + this.consoleHandler("del local error", e); + } + }); this.initiateHandler = () => { this.consoleHandler("FireProvider initiated!"); this.awareness.on("update", this.awarenessUpdateHandler); @@ -68,6 +96,7 @@ export class FireProvider extends ObservableV2 { // keep track of selected peers this.trackMesh(); this.doc.on("update", this.updateHandler); + this.syncLocal(); // if there's any data in indexedDb, get and apply }; this.trackData = () => { // Whenever there are changes to the firebase document @@ -121,7 +150,7 @@ export class FireProvider extends ObservableV2 { if (this.recreateTimeout) clearTimeout(this.recreateTimeout); this.recreateTimeout = setTimeout(() => __awaiter(this, void 0, void 0, function* () { - this.consoleHandler("triggering reconnect"); + this.consoleHandler("triggering reconnect", this.uid); this.destroy(); this.init(); }), 200); @@ -204,11 +233,12 @@ export class FireProvider extends ObservableV2 { } } }; - this.saveToFirestore = () => { + this.saveToFirestore = () => __awaiter(this, void 0, void 0, function* () { try { // current document to firestore const ref = doc(this.db, this.documentPath); - setDoc(ref, { content: Bytes.fromUint8Array(Y.encodeStateAsUpdate(this.doc)) }, { merge: true }); + yield setDoc(ref, { content: Bytes.fromUint8Array(Y.encodeStateAsUpdate(this.doc)) }, { merge: true }); + this.deleteLocal(); // We have successfully saved to Firestore, empty indexedDb for now } catch (error) { this.consoleHandler("error saving to firestore", error); @@ -217,7 +247,7 @@ export class FireProvider extends ObservableV2 { if (this.onSaving) this.onSaving(false); } - }; + }); this.sendToFirestoreQueue = () => { // if cache settles down, save document to firebase if (this.firestoreTimeout) @@ -276,14 +306,25 @@ export class FireProvider extends ObservableV2 { } }; this.updateHandler = (update, origin) => { + // Origin can be of the following types + // 1. User typed something -> origin: object + // 2. User loaded something from local store -> origin: object + // 3. User received update from a peer -> origin: string = peer uid + // 4. User received update from Firestore -> origin: string = 'origin:firebase/update' + // 5. Update triggered because user applied updates from the above sources -> origin: string = uid if (origin !== this.uid) { - // Only allow updates typed by the user, and updates sent by peers - // Disallow repeat updates that were sent back by the peers + // We will not allow no. 5. to propagate any further + // Apply updates received from no. 1 to 4. -> triggers no. 5 Y.applyUpdate(this.doc, update, this.uid); // the third parameter sets the transaction-origin + // Convert no. 1 and 2 to uid, because we want these to eventually trigger 'save' to Firestore + // sendToQueue method will either: + // 1. save origin:uid to Firestore (and send to peers through WebRtc) + // 2. send updates from other origins through WebRtc only this.sendToQueue({ from: typeof origin === "string" ? origin : this.uid, update, }); + this.saveToLocal(); // save data to local indexedDb } }; this.awarenessUpdateHandler = ({ added, updated, removed, }, origin) => { @@ -350,8 +391,5 @@ export class FireProvider extends ObservableV2 { this.awareness = new awarenessProtocol.Awareness(this.doc); // Initialize the provider const init = this.init(); - this.destroyHandler = () => { - this.destroy(); - }; } } diff --git a/dist/webrtc.d.ts b/dist/webrtc.d.ts index 6fa8f12..c27f266 100644 --- a/dist/webrtc.d.ts +++ b/dist/webrtc.d.ts @@ -34,10 +34,21 @@ export declare class WebRtc extends ObservableV2 { peerKey: CryptoKey; connection: string; clock: string | number | NodeJS.Timeout; + idleThreshold: number; constructor({ firebaseApp, ydoc, awareness, instanceConnection, documentPath, uid, peerUid, isCaller, }: Parameters); initPeer: () => void; startInitClock: () => void; createKey: () => Promise; + createPeer: (config: { + initiator: boolean; + config: { + iceServers: { + urls: string; + }[]; + }; + trickle: boolean; + channelName?: string; + }) => void; callPeer: () => void; replyPeer: () => void; handshake: () => void; diff --git a/dist/webrtc.d.ts.map b/dist/webrtc.d.ts.map index 6e11862..f951e4b 100644 --- a/dist/webrtc.d.ts.map +++ b/dist/webrtc.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"webrtc.d.ts","sourceRoot":"","sources":["../src/webrtc.ts"],"names":[],"mappings":";AAAA,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;AACzB,OAAO,KAAK,iBAAiB,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAIL,SAAS,EAMV,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,UAAU,MAAM,mBAAmB,CAAC;AAU3C,UAAU,UAAU;IAClB,WAAW,EAAE,WAAW,CAAC;IACzB,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC;IACZ,SAAS,EAAE,iBAAiB,CAAC,SAAS,CAAC;IACvC,kBAAkB,EAAE,YAAY,CAAC,GAAG,CAAC,CAAC;IACtC,YAAY,EAAE,MAAM,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAMD,qBAAa,MAAO,SAAQ,YAAY,CAAC,GAAG,CAAC;IAC3C,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC;IACpB,SAAS,EAAE,iBAAiB,CAAC,SAAS,CAAC;IACvC,kBAAkB,EAAE,YAAY,CAAC,GAAG,CAAC,CAAC;IACtC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,UAAU,CAAC,QAAQ,CAAC;IAC1B,QAAQ,CAAC,EAAE,EAAE,SAAS,CAAC;IACvB,OAAO,CAAC,oBAAoB,CAAC,CAAc;IAC3C,QAAQ,EAAE,OAAO,CAAC;IAClB,GAAG;;;;MAKD;IACF,OAAO,EAAE,SAAS,CAAC;IACnB,UAAU,EAAE,MAAM,CAAgB;IAClC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC;gBAE5B,EACV,WAAW,EACX,IAAI,EACJ,SAAS,EACT,kBAAkB,EAClB,YAAY,EACZ,GAAG,EACH,OAAO,EACP,QAAgB,GACjB,EAAE,UAAU;IAkBb,QAAQ,aAaN;IAEF,cAAc,aAcZ;IAEF,SAAS,sBAOP;IAEF,QAAQ,aA4BN;IAEF,SAAS,aA2BP;IAEF,SAAS,aAmBP;IAEF,cAAc,aAKZ;IAEF,OAAO,0CAQL;IAEF,aAAa,aAoBX;IAEF,iBAAiB,aAIf;IAEF,aAAa,aAKX;IAEF,QAAQ;iBAIG,OAAO;cACV,UAAU,GAAG,IAAI;wBAUvB;IAEF,mBAAmB,SAAgB,GAAG,mBAqBpC;IAEF,cAAc,YAAa,GAAG,SAAQ,GAAG,UASvC;IAEF,YAAY,UAAW,GAAG,UAExB;IAEI,OAAO;CAQd"} \ No newline at end of file +{"version":3,"file":"webrtc.d.ts","sourceRoot":"","sources":["../src/webrtc.ts"],"names":[],"mappings":";AAAA,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;AACzB,OAAO,KAAK,iBAAiB,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAIL,SAAS,EAMV,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,UAAU,MAAM,mBAAmB,CAAC;AAU3C,UAAU,UAAU;IAClB,WAAW,EAAE,WAAW,CAAC;IACzB,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC;IACZ,SAAS,EAAE,iBAAiB,CAAC,SAAS,CAAC;IACvC,kBAAkB,EAAE,YAAY,CAAC,GAAG,CAAC,CAAC;IACtC,YAAY,EAAE,MAAM,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAMD,qBAAa,MAAO,SAAQ,YAAY,CAAC,GAAG,CAAC;IAC3C,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC;IACpB,SAAS,EAAE,iBAAiB,CAAC,SAAS,CAAC;IACvC,kBAAkB,EAAE,YAAY,CAAC,GAAG,CAAC,CAAC;IACtC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,UAAU,CAAC,QAAQ,CAAC;IAC1B,QAAQ,CAAC,EAAE,EAAE,SAAS,CAAC;IACvB,OAAO,CAAC,oBAAoB,CAAC,CAAc;IAC3C,QAAQ,EAAE,OAAO,CAAC;IAClB,GAAG;;;;MAKD;IACF,OAAO,EAAE,SAAS,CAAC;IACnB,UAAU,EAAE,MAAM,CAAgB;IAClC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC;IACxC,aAAa,EAAE,MAAM,CAAS;gBAElB,EACV,WAAW,EACX,IAAI,EACJ,SAAS,EACT,kBAAkB,EAClB,YAAY,EACZ,GAAG,EACH,OAAO,EACP,QAAgB,GACjB,EAAE,UAAU;IAkBb,QAAQ,aAaN;IAEF,cAAc,aAcZ;IAEF,SAAS,sBAOP;IAEF,UAAU;mBACG,OAAO;gBACV;YAAE,UAAU,EAAE;gBAAE,IAAI,EAAE,MAAM,CAAA;aAAE,EAAE,CAAA;SAAE;iBACjC,OAAO;sBACF,MAAM;eAGpB;IAEF,QAAQ,aA4BN;IAEF,SAAS,aA2BP;IAEF,SAAS,aAmBP;IAEF,cAAc,aAKZ;IAEF,OAAO,0CAQL;IAEF,aAAa,aAoBX;IAEF,iBAAiB,aAIf;IAEF,aAAa,aAKX;IAEF,QAAQ;iBAIG,OAAO;cACV,UAAU,GAAG,IAAI;wBAUvB;IAEF,mBAAmB,SAAgB,GAAG,mBAqBpC;IAEF,cAAc,YAAa,GAAG,SAAQ,GAAG,UASvC;IAEF,YAAY,UAAW,GAAG,UAExB;IAEI,OAAO;CAQd"} \ No newline at end of file diff --git a/dist/webrtc.js b/dist/webrtc.js index dc17cca..1b92973 100644 --- a/dist/webrtc.js +++ b/dist/webrtc.js @@ -23,6 +23,7 @@ export class WebRtc extends ObservableV2 { ], }; this.connection = "connecting"; + this.idleThreshold = 20000; this.initPeer = () => { this.createKey(); if (this.isCaller) { @@ -40,7 +41,7 @@ export class WebRtc extends ObservableV2 { this.startInitClock = () => { /** * We will track how long it takes to connect to this peer - * if it takes longer than 30s, we assume that we are + * if it takes longer than idleThreshold, we assume that we are * connected to a zombie peer. Thus we will attempt to * kill the zombie instance */ @@ -49,10 +50,9 @@ export class WebRtc extends ObservableV2 { this.clock = setTimeout(() => { if (this.connection !== "connected") { killZombie(this.db, this.documentPath, this.uid, this.peerUid); - if (this.peer) - this.peer.destroy(); + this.handleOnClose(); } - }, 30 * 1000); + }, this.idleThreshold); }; this.createKey = () => __awaiter(this, void 0, void 0, function* () { this.peerKey = yield generateKey(this.isCaller ? this.uid : this.peerUid, this.isCaller ? this.peerUid : this.uid); @@ -60,8 +60,11 @@ export class WebRtc extends ObservableV2 { if (!this.peerKey) this.destroy(); }); + this.createPeer = (config) => { + this.peer = new SimplePeer(config); + }; this.callPeer = () => { - this.peer = new SimplePeer({ + this.createPeer({ initiator: true, config: this.ice, trickle: false, @@ -77,7 +80,7 @@ export class WebRtc extends ObservableV2 { yield setDoc(callRef, { signal }); setTimeout(() => { deleteDoc(callRef); - }, 30 * 1000); // delete call after 30 seconds, if handshake hasn't deleted it yet + }, this.idleThreshold); // delete call after defined miliseconds, if handshake hasn't deleted it yet } catch (error) { this.errorHandler(error); @@ -85,7 +88,7 @@ export class WebRtc extends ObservableV2 { })); }; this.replyPeer = () => { - this.peer = new SimplePeer({ + this.createPeer({ initiator: false, config: this.ice, trickle: false, @@ -100,7 +103,7 @@ export class WebRtc extends ObservableV2 { yield setDoc(answerRef, { signal }); setTimeout(() => { deleteDoc(answerRef); - }, 30 * 1000); // delete call after 30 seconds, if handshake hasn't deleted it yet + }, this.idleThreshold); // delete call after defined miliseconds, if handshake hasn't deleted it yet } catch (error) { this.errorHandler(error); diff --git a/package-lock.json b/package-lock.json index 2761b1a..5eff51a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,15 @@ { "name": "y-fire", - "version": "1.0.0", + "version": "1.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "y-fire", - "version": "1.0.0", + "version": "1.0.3", "license": "MIT", "dependencies": { + "idb-keyval": "^6.2.1", "simple-peer-light": "^9.10.0" }, "devDependencies": { @@ -807,6 +808,11 @@ "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", "peer": true }, + "node_modules/idb-keyval": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz", + "integrity": "sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==" + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", diff --git a/package.json b/package.json index 8e6145c..8116907 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,12 @@ { "name": "y-fire", - "version": "1.0.2", + "version": "2.0.0", "description": "A firebase (firestore) provider for Yjs", "main": "dist/index.js", "types": "dist/index.d.ts", - "files": ["dist"], + "files": [ + "dist" + ], "scripts": { "build": "tsc", "test": "echo \"Error: no test specified\" && exit 1" @@ -30,11 +32,12 @@ "typescript": "^5.3.3" }, "dependencies": { + "idb-keyval": "^6.2.1", "simple-peer-light": "^9.10.0" }, "peerDependencies": { "firebase": "^10.7.1", - "yjs": "^13.6.12", - "y-protocols": "^1.0.6" + "y-protocols": "^1.0.6", + "yjs": "^13.6.12" } } diff --git a/src/provider.ts b/src/provider.ts index 7a432a6..df3f394 100644 --- a/src/provider.ts +++ b/src/provider.ts @@ -12,6 +12,7 @@ import { collection } from "firebase/firestore"; import * as Y from "yjs"; import { ObservableV2 } from "lib0/observable"; import * as awarenessProtocol from "y-protocols/awareness"; +import { get as getLocal, set as setLocal, del as delLocal } from "idb-keyval"; import { deleteInstance, initiateInstance, refreshPeers } from "./utils"; import { WebRtc } from "./webrtc"; import { createGraph } from "./graph"; @@ -87,9 +88,6 @@ export class FireProvider extends ObservableV2 { public onDeleted: () => void; public onSaving: (status: boolean) => void; - // private initiateHandler: () => void; - private destroyHandler: () => void; - init = async () => { this.trackData(); // initiate this before creating instance, so that users with read permissions can also view the document try { @@ -105,6 +103,32 @@ export class FireProvider extends ObservableV2 { } }; + syncLocal = async () => { + try { + const local = await getLocal(this.documentPath); + if (local) Y.applyUpdate(this.doc, local, { key: "local-sync" }); + } catch (e) { + this.consoleHandler("get local error", e); + } + }; + + saveToLocal = async () => { + try { + const currentDoc = Y.encodeStateAsUpdate(this.doc); + setLocal(this.documentPath, currentDoc); + } catch (e) { + this.consoleHandler("set local error", e); + } + }; + + deleteLocal = async () => { + try { + delLocal(this.documentPath); + } catch (e) { + this.consoleHandler("del local error", e); + } + }; + initiateHandler = () => { this.consoleHandler("FireProvider initiated!"); this.awareness.on("update", this.awarenessUpdateHandler); @@ -112,6 +136,7 @@ export class FireProvider extends ObservableV2 { // keep track of selected peers this.trackMesh(); this.doc.on("update", this.updateHandler); + this.syncLocal(); // if there's any data in indexedDb, get and apply }; trackData = () => { @@ -184,7 +209,7 @@ export class FireProvider extends ObservableV2 { reconnect = () => { if (this.recreateTimeout) clearTimeout(this.recreateTimeout); this.recreateTimeout = setTimeout(async () => { - this.consoleHandler("triggering reconnect"); + this.consoleHandler("triggering reconnect", this.uid); this.destroy(); this.init(); }, 200); @@ -279,15 +304,16 @@ export class FireProvider extends ObservableV2 { } }; - saveToFirestore = () => { + saveToFirestore = async () => { try { // current document to firestore const ref = doc(this.db, this.documentPath); - setDoc( + await setDoc( ref, { content: Bytes.fromUint8Array(Y.encodeStateAsUpdate(this.doc)) }, { merge: true } ); + this.deleteLocal(); // We have successfully saved to Firestore, empty indexedDb for now } catch (error) { this.consoleHandler("error saving to firestore", error); } finally { @@ -354,15 +380,29 @@ export class FireProvider extends ObservableV2 { }; updateHandler = (update: Uint8Array, origin: any) => { + // Origin can be of the following types + // 1. User typed something -> origin: object + // 2. User loaded something from local store -> origin: object + // 3. User received update from a peer -> origin: string = peer uid + // 4. User received update from Firestore -> origin: string = 'origin:firebase/update' + // 5. Update triggered because user applied updates from the above sources -> origin: string = uid + if (origin !== this.uid) { - // Only allow updates typed by the user, and updates sent by peers - // Disallow repeat updates that were sent back by the peers + // We will not allow no. 5. to propagate any further + + // Apply updates received from no. 1 to 4. -> triggers no. 5 Y.applyUpdate(this.doc, update, this.uid); // the third parameter sets the transaction-origin + // Convert no. 1 and 2 to uid, because we want these to eventually trigger 'save' to Firestore + // sendToQueue method will either: + // 1. save origin:uid to Firestore (and send to peers through WebRtc) + // 2. send updates from other origins through WebRtc only this.sendToQueue({ from: typeof origin === "string" ? origin : this.uid, update, }); + + this.saveToLocal(); // save data to local indexedDb } }; @@ -459,9 +499,5 @@ export class FireProvider extends ObservableV2 { // Initialize the provider const init = this.init(); - - this.destroyHandler = () => { - this.destroy(); - }; } } diff --git a/src/webrtc.ts b/src/webrtc.ts index 1eff897..70d370a 100644 --- a/src/webrtc.ts +++ b/src/webrtc.ts @@ -58,6 +58,7 @@ export class WebRtc extends ObservableV2 { peerKey: CryptoKey; connection: string = "connecting"; clock: string | number | NodeJS.Timeout; + idleThreshold: number = 20000; constructor({ firebaseApp, @@ -104,7 +105,7 @@ export class WebRtc extends ObservableV2 { startInitClock = () => { /** * We will track how long it takes to connect to this peer - * if it takes longer than 30s, we assume that we are + * if it takes longer than idleThreshold, we assume that we are * connected to a zombie peer. Thus we will attempt to * kill the zombie instance */ @@ -112,9 +113,9 @@ export class WebRtc extends ObservableV2 { this.clock = setTimeout(() => { if (this.connection !== "connected") { killZombie(this.db, this.documentPath, this.uid, this.peerUid); - if (this.peer) this.peer.destroy(); + this.handleOnClose(); } - }, 30 * 1000); + }, this.idleThreshold); }; createKey = async () => { @@ -126,8 +127,17 @@ export class WebRtc extends ObservableV2 { if (!this.peerKey) this.destroy(); }; + createPeer = (config: { + initiator: boolean; + config: { iceServers: { urls: string }[] }; + trickle: boolean; + channelName?: string; + }) => { + this.peer = new SimplePeer(config); + }; + callPeer = () => { - this.peer = new SimplePeer({ + this.createPeer({ initiator: true, config: this.ice, trickle: false, @@ -149,7 +159,7 @@ export class WebRtc extends ObservableV2 { setTimeout(() => { deleteDoc(callRef); - }, 30 * 1000); // delete call after 30 seconds, if handshake hasn't deleted it yet + }, this.idleThreshold); // delete call after defined miliseconds, if handshake hasn't deleted it yet } catch (error) { this.errorHandler(error); } @@ -157,7 +167,7 @@ export class WebRtc extends ObservableV2 { }; replyPeer = () => { - this.peer = new SimplePeer({ + this.createPeer({ initiator: false, config: this.ice, trickle: false, @@ -178,7 +188,7 @@ export class WebRtc extends ObservableV2 { setTimeout(() => { deleteDoc(answerRef); - }, 30 * 1000); // delete call after 30 seconds, if handshake hasn't deleted it yet + }, this.idleThreshold); // delete call after defined miliseconds, if handshake hasn't deleted it yet } catch (error) { this.errorHandler(error); }