UNPKG

json-crdt-repo

Version:

JSON CRDT server and syncing local-first browser client

290 lines (289 loc) 12.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.EditSession = void 0; const tslib_1 = require("tslib"); const Log_1 = require("json-joy/lib/json-crdt/log/Log"); const json_crdt_1 = require("json-joy/lib/json-crdt"); const concurrencyDecorator_1 = require("thingies/lib/concurrencyDecorator"); const createRace_1 = require("thingies/lib/createRace"); const constants_1 = require("json-joy/lib/json-crdt-patch/constants"); const rxjs_1 = require("rxjs"); const operators_1 = require("rxjs/operators"); let EditSession = (() => { var _a; let _instanceExtraInitializers = []; let _sync_decorators; return _a = class EditSession { get model() { return this.log.end; } constructor(repo, id, start, cursor = undefined, session = Math.floor(Math.random() * 0x7fffffff)) { this.repo = (tslib_1.__runInitializers(this, _instanceExtraInitializers), repo); this.id = id; this.start = start; this.cursor = cursor; this.session = session; this._stopped = false; this._stop$ = new rxjs_1.Subject(); this._syncRace = (0, createRace_1.createRace)(); this.saveInProgress = false; // ------------------------------------------------ reactive event processing this.events = []; this.onEvent = (event) => { if (this._stopped) return; if (event.merge) { const cursor = event.cursor; if (cursor !== undefined) { this.cursor = cursor; } } if (event.rebase) { if (event.session === this.session) return; } this.events.push(event); this.drainEvents(); }; this.log = new Log_1.Log(() => this.start.clone()); const api = this.log.end.api; const flushUnsubscribe = api.onFlush.listen(() => { this.syncLog(); }); const patchUnsubscribe = api.onPatch.listen((patch) => { const id = patch.getId(); if (!id) return; const clock = this.model.clock; if (id.sid === clock.sid && clock.time >= id.time) { this.syncLog(); } }); this._stop$.pipe((0, operators_1.first)()).subscribe(() => { flushUnsubscribe(); patchUnsubscribe(); }); this.repo.change$(this.id).pipe((0, operators_1.takeUntil)(this._stop$)).subscribe(this.onEvent); } dispose() { if (this._stopped) return; this._stopped = true; this._stop$.next(); } clear() { const { start, log } = this; const empty = json_crdt_1.Model.create(undefined, start.clock.sid); start.reset(empty); log.patches.clear(); log.end.reset(empty); } /** * Push (persist) any in-memory changes and pull (load) the latest state * from the local repo. */ async sync() { if (this._stopped) return null; const log = this.log; const api = log.end.api; api.flush(); this.saveInProgress = true; try { const patches = []; // biome-ignore lint: for loop not possible here log.patches.forEach((patch) => { patches.push(patch.v); }); const length = patches.length; // TODO: After async call check that sync state is still valid. New patches, might have been added. if (length || this.cursor === undefined) { const time = this.start.clock.time - 1; const res = await this.repo.sync({ id: this.id, patches, time, cursor: this.cursor, session: this.session, }); if (this._stopped) return null; // TODO: After sync call succeeds, remove the patches from the log. if (length) { const last = patches[length - 1]; const lastId = last.getId(); if (lastId) this.log.advanceTo(lastId); this.start.applyBatch(patches); } if (res.model) { this._syncRace(() => { this.reset(res.model); }); } else if (res.merge && res.merge) { this._syncRace(() => { this.merge(res.merge); }); } const cursorBehind = this.cursor !== res.cursor; if (cursorBehind) { setTimeout(async () => { if (this._stopped) return; const get = await this.repo.getIf({ id: this.id, cursor: this.cursor, }); if (this._stopped) return; if (!get) return; this.reset(get.model); }, 50); } return { remote: res.remote }; } else { const res = await this.repo.getIf({ id: this.id, time: this.model.clock.time - 1, cursor: this.cursor }); if (this._stopped) return null; if (res) { this.reset(res.model); this.cursor = res.cursor; } return null; } } finally { this.saveInProgress = false; this.drainEvents(); } } syncLog() { if (!this.log.patches.size()) return; this._syncRace(() => { this.sync() .then((error) => { this.onsyncerror?.(error); }) .catch((error) => { this.onsyncerror?.(error); }); }); } async del() { this.clear(); this.dispose(); await this.repo.del(this.id); } /** * Load latest state from the local repo. */ async load() { const { model } = await this.repo.get({ id: this.id }); if (model.clock.time > this.start.clock.time) this.reset(model); } loadSilent() { this.load().catch(() => { }); } // ------------------------------------------------------- change integration reset(model) { this.start = model.clone(); const log = this.log; const end = log.end; if (end.api.builder.patch.ops.length) end.api.flush(); end.reset(model); // biome-ignore lint: for loop not possible here log.patches.forEach((patch) => end.applyPatch(patch.v)); } rebase(patches) { if (patches.length === 0) return; const log = this.log; const end = log.end; if (end.api.builder.patch.ops.length) end.api.flush(); if (log.patches.size() === 0) { this.merge(patches); return; } this.start.applyBatch(patches); const newEnd = this.start.clone(); const lastPatch = patches[patches.length - 1]; let nextTick = lastPatch.getId().time + lastPatch.span(); const rebased = []; // biome-ignore lint: for loop not possible here log.patches.forEach(({ v }) => { const patch = v.rebase(nextTick); rebased.push(patch); newEnd.applyPatch(patch); nextTick += patch.span(); }); log.patches.clear(); for (const patch of rebased) log.patches.set(patch.getId(), patch); log.end.reset(newEnd); } merge(patches) { if (!patches.length) return; const start = this.start; const log = this.log; const end = log.end; const sid = end.clock.sid; for (const patch of patches) { const patchId = patch.getId(); if (!patchId) continue; const patchSid = patchId.sid; if (patchSid === constants_1.SESSION.GLOBAL) continue; if (patchSid === sid && patchId.time < end.clock.time) continue; end.applyPatch(patch); start.applyPatch(patch); log.patches.del(patchId); } } drainEvents() { if (this.saveInProgress || this._stopped) return; this._syncRace(() => { const events = this.events; const length = events.length; for (let i = 0; i < length; i++) { const event = events[i]; try { if (event.reset) this.reset(event.reset); else if (event.rebase) this.rebase(event.rebase); else if (event.merge) this.merge(event.merge); else if (event.del) { this.clear(); break; } } catch (error) { // tslint:disable-next-line no-console console.error('Failed to apply event', event, error); } } this.events = []; }); } }, (() => { const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0; _sync_decorators = [(0, concurrencyDecorator_1.concurrency)(1)]; tslib_1.__esDecorate(_a, null, _sync_decorators, { kind: "method", name: "sync", static: false, private: false, access: { has: obj => "sync" in obj, get: obj => obj.sync }, metadata: _metadata }, null, _instanceExtraInitializers); if (_metadata) Object.defineProperty(_a, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata }); })(), _a; })(); exports.EditSession = EditSession;