@casual-simulation/aux-common
Version:
Common library for AUX projects
637 lines • 18 kB
JavaScript
/* 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