UNPKG

@casual-simulation/aux-common

Version:
637 lines 18 kB
/* CasualOS is a set of web-based tools designed to facilitate the creation of real-time, multi-user, context-aware interactive experiences. * * Copyright (c) 2019-2025 Casual Simulation, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ import { BehaviorSubject, Observable, startWith, Subject, Subscription, } from 'rxjs'; import { AbstractType as YType, Map as YMap, Array as YArray, Text as YText, YMapEvent, YArrayEvent, YTextEvent, createRelativePositionFromTypeIndex, createAbsolutePositionFromRelativePosition, Doc, applyUpdate, encodeStateAsUpdate, } from 'yjs'; import { fromByteArray, toByteArray } from 'base64-js'; import { YjsIndexedDBPersistence } from '../yjs/YjsIndexedDBPersistence'; export const APPLY_UPDATES_TO_INST_TRANSACTION_ORIGIN = '__apply_updates_to_inst'; /** * Creates a new YJS shared document. * @param config The config for the document. */ export function createYjsSharedDocument(config) { return new YjsSharedDocument(config); } /** * Defines a shared document that is backed by a YJS document. */ export class YjsSharedDocument { get recordName() { return this._recordName; } get address() { return this._inst; } get branch() { return this._branch; } get clientId() { return this._doc.clientID; } get closed() { return this._sub.closed; } get onVersionUpdated() { return this._onVersionUpdated; } get onError() { return this._onError; } get onEvents() { return this._onEvents; } get onClientError() { return this._onClientError; } get onStatusUpdated() { return this._onStatusUpdated; } get site() { return this._currentSite; } get onUpdates() { return this._onUpdates.pipe(startWith([fromByteArray(encodeStateAsUpdate(this._doc))])); } get doc() { return this._doc; } get _remoteSite() { return this._remoteId.toString(); } get _currentSite() { return this._localId.toString(); } unsubscribe() { this._sub.unsubscribe(); } constructor(config) { this._onUpdates = new Subject(); this._onError = new Subject(); this._onEvents = new Subject(); this._onStatusUpdated = new Subject(); this._onClientError = new Subject(); this._sub = new Subscription(); this._doc = new Doc(); this._isLocalTransaction = true; this._isRemoteUpdate = false; this._maps = new Map(); this._arrays = new Map(); this._texts = new Map(); Object.defineProperty(this._doc, '__sharedDoc', { value: this, enumerable: false, writable: false, }); this._branch = config.branch; this._persistence = config.localPersistence; this._localId = this._doc.clientID; this._remoteId = new Doc().clientID; this._currentVersion = { currentSite: this._localId.toString(), remoteSite: this._remoteId.toString(), vector: {}, }; this._onVersionUpdated = new BehaviorSubject(this._currentVersion); this._onUpdates = new Subject(); } getMap(name) { let map = this._maps.get(name); if (!map) { map = new YjsSharedMap(this._doc.getMap(name)); this._maps.set(name, map); } return map; } getArray(name) { let array = this._arrays.get(name); if (!array) { array = new YjsSharedArray(this._doc.getArray(name)); this._arrays.set(name, array); } return array; } getText(name) { let text = this._texts.get(name); if (!text) { text = new YjsSharedText(this._doc.getText(name)); this._texts.set(name, text); } return text; } createMap() { return new YjsSharedMap(new YMap()); } createArray() { return new YjsSharedArray(new YArray()); } async init() { } connect() { var _a; if (((_a = this._persistence) === null || _a === void 0 ? void 0 : _a.saveToIndexedDb) && this._branch) { console.log('[YjsPartition] Using IndexedDB persistence'); this._indexeddb = new YjsIndexedDBPersistence(this._branch, this._doc, { broadcastChanges: true }); } this._onStatusUpdated.next({ type: 'connection', connected: true, }); this._onStatusUpdated.next({ type: 'authentication', authenticated: true, }); this._onStatusUpdated.next({ type: 'authorization', authorized: true, }); if (this._indexeddb) { // wait to send the initial sync event until the persistence is ready this._indexeddb.waitForInit().then(() => { this._onStatusUpdated.next({ type: 'sync', synced: true, }); }); } else { this._onStatusUpdated.next({ type: 'sync', synced: true, }); } } transact(callback) { return this._doc.transact(callback); } getStateUpdate() { const update = { id: 0, timestamp: Date.now(), update: fromByteArray(encodeStateAsUpdate(this._doc)), }; return update; } applyStateUpdates(updates) { this._applyUpdates(updates.map((u) => u.update), APPLY_UPDATES_TO_INST_TRANSACTION_ORIGIN); } applyUpdates(updates) { this._applyUpdates(updates, APPLY_UPDATES_TO_INST_TRANSACTION_ORIGIN); } /** * Applies the given updates to the YJS document. * @param updates The updates to apply. * @param transactionOrigin The origin of the transaction. */ _applyUpdates(updates, transactionOrigin) { try { this._isRemoteUpdate = true; for (let updateBase64 of updates) { const update = toByteArray(updateBase64); applyUpdate(this._doc, update, transactionOrigin); } } finally { this._isRemoteUpdate = false; } } } function convertEvent(event) { if (event instanceof YMapEvent) { return { type: 'map', target: event.target.__sharedType, changes: event.changes.keys, }; } else if (event instanceof YArrayEvent) { return { type: 'array', target: event.target.__sharedType, delta: convertArrayDelta(event.delta), }; } else if (event instanceof YTextEvent) { return { type: 'text', target: event.target.__sharedType, delta: convertTextDelta(event.delta), }; } return null; } function convertArrayDelta(delta) { let ops = []; for (let op of delta) { if (op.insert) { ops.push({ type: 'insert', values: op.insert, }); } else if (op.delete) { ops.push({ type: 'delete', count: op.delete, }); } else { ops.push({ type: 'preserve', count: op.retain, }); } } return ops; } function convertTextDelta(delta) { let ops = []; for (let op of delta) { if (op.insert) { ops.push({ type: 'insert', text: op.insert, attributes: op.attributes, }); } else if (op.delete) { ops.push({ type: 'delete', count: op.delete, }); } else { ops.push({ type: 'preserve', count: op.retain, }); } } return ops; } function changesObservable(type) { return new Observable((observer) => { const f = (event) => { observer.next(convertEvent(event)); }; type.observe(f); return () => { // Unsubscribe type.unobserve(f); }; }); } function deepChangesObservable(type) { return new Observable((observer) => { const f = (event) => { observer.next(event.map(convertEvent)); }; type.observeDeep(f); return () => { // Unsubscribe type.unobserveDeep(f); }; }); } export class YjsSharedType { get type() { return this._type; } get doc() { var _a; return (_a = this._type.doc) === null || _a === void 0 ? void 0 : _a.__sharedDoc; } get parent() { var _a; return (_a = this._type.parent) === null || _a === void 0 ? void 0 : _a.__sharedType; } get changes() { return this._changes; } get deepChanges() { return this._deepChanges; } constructor(type) { this._type = type; Object.defineProperty(this._type, '__sharedType', { value: this, enumerable: false, writable: false, }); this._changes = changesObservable(this._type); this._deepChanges = deepChangesObservable(this._type); } } export class YjsSharedMap extends YjsSharedType { constructor(map) { let ymap; if (map instanceof YMap) { ymap = map; } else { ymap = new YMap(map); } super(ymap); } get size() { return this.type.size; } set(key, value) { if (value instanceof YjsSharedType) { if (value.doc) { throw new Error('Cannot set a top-level map inside another map.'); } value = value.type; } this.type.set(key, value); } get(key) { const val = this.type.get(key); return valueOrSharedType(val); } delete(key) { this.type.delete(key); } has(key) { return this.type.has(key); } clear() { this.type.clear(); } clone() { return new YjsSharedMap(this.type.clone()); } toJSON() { return this.type.toJSON(); } forEach(callback) { return this.type.forEach((value, key) => callback(value, key, this)); } entries() { return this.type.entries(); } keys() { return this.type.keys(); } values() { return this.type.values(); } [Symbol.iterator]() { return this.type[Symbol.iterator](); } } export class YjsSharedArray extends YjsSharedType { get length() { return this.type.length; } get size() { return this.type.length; } constructor(arr) { let yarray; if (arr instanceof YArray) { yarray = arr; } else { yarray = YArray.from(arr); } super(yarray); } insert(index, items) { this.type.insert(index, this._mapItems(items)); } delete(index, count) { this.type.delete(index, count); } applyDelta(delta) { let index = 0; for (let op of delta) { if (op.type === 'preserve') { index += op.count; } else if (op.type === 'insert') { this.type.insert(index, op.values); index += op.values.length; } else if (op.type === 'delete') { this.type.delete(index, op.count); } } } push(...items) { this.type.push(this._mapItems(items)); } pop() { let lastIndex = this.type.length - 1; if (lastIndex < 0) { return undefined; } else { const lastItem = this.type.get(lastIndex); this.type.delete(lastIndex, 1); return lastItem; } } unshift(...items) { this.type.unshift(this._mapItems(items)); } shift() { if (this.type.length <= 0) { return undefined; } else { const firstItem = this.type.get(0); this.type.delete(0, 1); return firstItem; } } get(index) { return valueOrSharedType(this.type.get(index)); } slice(start, end) { return this.type.slice(start, end); } splice(start, deleteCount, ...items) { if (this.type.length <= 0) { if (items.length > 0) { this.push(...items); } return []; } const len = this.type.length; if (start < -len) { start = 0; } else if (-len <= start && start < 0) { start = len + start; } else if (start >= len) { start = len; } if (start >= len) { deleteCount = 0; } else if (typeof deleteCount === 'undefined') { deleteCount = 0; } else if (deleteCount >= len - start) { deleteCount = len - start; } else if (deleteCount < 0) { deleteCount = 0; } let deleted = []; if (deleteCount > 0) { deleted = this.type.slice(start, start + deleteCount); this.delete(start, deleteCount); } if (items.length > 0) { this.insert(start, items); } return deleted; } toArray() { return this.type.toArray(); } toJSON() { return this.type.toJSON(); } forEach(callback) { this.type.forEach((value, index) => callback(value, index, this)); } map(callback) { return this.type.map((value, index) => callback(value, index, this)); } filter(predicate) { let arr = []; for (let i = 0; i < this.type.length; i++) { const val = this.type.get(i); if (predicate(val, i, this)) { arr.push(val); } } return arr; } clone() { return new YjsSharedArray(this.type.clone()); } [Symbol.iterator]() { return this.type[Symbol.iterator](); } _mapItems(items) { let containsSharedType = false; for (let i of items) { if (i instanceof YjsSharedType) { if (i.doc) { throw new Error('Cannot push a top-level array inside another array.'); } containsSharedType = true; break; } } if (containsSharedType) { items = items.map((i) => (i instanceof YjsSharedType ? i.type : i)); } return items; } } export class YjsSharedText { get doc() { var _a; return (_a = this._text.doc) === null || _a === void 0 ? void 0 : _a.__sharedDoc; } get parent() { var _a; return (_a = this._text.parent) === null || _a === void 0 ? void 0 : _a.__sharedType; } get length() { return this._text.length; } get size() { return this._text.length; } get changes() { return this._changes; } get deepChanges() { return this._deepChanges; } constructor(text) { if (text instanceof YText) { this._text = text; } else { this._text = new YText(text); } Object.defineProperty(this._text, '__sharedType', { value: this, enumerable: false, writable: false, }); this._changes = changesObservable(this._text); this._deepChanges = deepChangesObservable(this._text); } insert(index, text, attribtues) { this._text.insert(index, text, attribtues); } delete(index, count) { this._text.delete(index, count); } applyDelta(delta) { let d = []; for (let op of delta) { if (op.type === 'preserve') { d.push({ retain: op.count }); } else if (op.type === 'insert') { d.push({ insert: op.text, attributes: op.attributes }); } else if (op.type === 'delete') { d.push({ delete: op.count }); } } this._text.applyDelta(d); } toDelta() { return convertTextDelta(this._text.toDelta()); } encodeRelativePosition(index, assoc) { return createRelativePositionFromTypeIndex(this._text, index, assoc); } decodeRelativePosition(position) { const pos = createAbsolutePositionFromRelativePosition(position, this._text.doc); return pos.index; } slice(start, end) { return this._text.toString().slice(start, end); } toString() { return this._text.toString(); } toJSON() { return this._text.toJSON(); } clone() { return new YjsSharedText(this._text.clone()); } } function valueOrSharedType(val) { if (val instanceof YType) { return val.__sharedType; } return val; } //# sourceMappingURL=YjsSharedDocument.js.map