@liveblocks/client
Advanced tools
Comparing version 0.11.0 to 0.12.0-beta.1
@@ -5,4 +5,3 @@ import { ClientOptions, Client } from "./types"; | ||
* | ||
* ### Example | ||
* ``` | ||
* @example | ||
* const client = createClient({ | ||
@@ -28,4 +27,3 @@ * authEndpoint: "/api/auth" | ||
* }); | ||
* ``` | ||
*/ | ||
export declare function createClient(options: ClientOptions): Client; |
@@ -8,4 +8,3 @@ "use strict"; | ||
* | ||
* ### Example | ||
* ``` | ||
* @example | ||
* const client = createClient({ | ||
@@ -31,7 +30,7 @@ * authEndpoint: "/api/auth" | ||
* }); | ||
* ``` | ||
*/ | ||
function createClient(options) { | ||
if (typeof options.throttle === "number") { | ||
if (options.throttle < 80 || options.throttle > 1000) { | ||
const clientOptions = options; | ||
if (typeof clientOptions.throttle === "number") { | ||
if (clientOptions.throttle < 80 || clientOptions.throttle > 1000) { | ||
throw new Error("Liveblocks client throttle should be between 80 and 1000 ms"); | ||
@@ -45,3 +44,3 @@ } | ||
} | ||
function enter(roomId, initialPresence) { | ||
function enter(roomId, options = {}) { | ||
let internalRoom = rooms.get(roomId); | ||
@@ -51,3 +50,3 @@ if (internalRoom) { | ||
} | ||
internalRoom = room_1.createRoom(roomId, Object.assign(Object.assign({}, options), { initialPresence })); | ||
internalRoom = room_1.createRoom(roomId, Object.assign(Object.assign({}, clientOptions), options)); | ||
rooms.set(roomId, internalRoom); | ||
@@ -54,0 +53,0 @@ internalRoom.connect(); |
@@ -1,51 +0,97 @@ | ||
import { Op, SerializedRecord } from "./live"; | ||
import { Serializable, SerializablePrimitive } from "./types"; | ||
declare type Link = { | ||
parentId: string; | ||
parentKey: string; | ||
}; | ||
declare type Links = Map<string, Link>; | ||
declare type ListCache = Map<string, Map<string, Crdt>>; | ||
declare type Cache = { | ||
links: Links; | ||
listCache: ListCache; | ||
}; | ||
declare type Crdt = Record | List<any>; | ||
declare const RECORD: unique symbol; | ||
declare const LIST: unique symbol; | ||
export declare function createRecord<T extends RecordData>(id: string, data: T): Record<T>; | ||
export declare function createList<T>(id: string, items?: T[]): List<T>; | ||
export declare type RecordData = { | ||
[key: string]: RecordValue; | ||
}; | ||
declare type RecordValue = SerializablePrimitive | Array<SerializablePrimitive> | Serializable | Record<any> | List<any>; | ||
export declare type Record<T extends RecordData = RecordData> = { | ||
readonly id: string; | ||
readonly $$type: typeof RECORD; | ||
} & T; | ||
export declare type List<T> = { | ||
readonly id: string; | ||
readonly $$type: typeof LIST; | ||
toArray(): Array<T>; | ||
map<U>(callback: (value: T, index: number) => U): U[]; | ||
readonly length: number; | ||
}; | ||
declare type Emit = (op: Op) => void; | ||
export declare class Doc<T extends RecordData> { | ||
root: Record<T>; | ||
private _cache; | ||
private _emit; | ||
constructor(root: Record<T>, _cache: Cache, _emit: Emit); | ||
static empty<T extends RecordData>(id?: string, emit?: Emit): Doc<T>; | ||
static createFromRoot<T extends RecordData>(data: T, id?: string, emit?: Emit): Doc<T>; | ||
static load<T extends RecordData>(root: SerializedRecord, emit?: Emit): Doc<T>; | ||
get data(): Record<T>; | ||
dispatch(op: Op, shouldEmit?: boolean): Doc<T>; | ||
private getChild; | ||
updateRecord<TRecord>(id: string, overrides: Partial<TRecord>): Doc<T>; | ||
pushItem<TItem>(id: string, item: TItem): Doc<T>; | ||
moveItem(id: string, index: number, targetIndex: number): Doc<T>; | ||
deleteItem(id: string, index: number): Doc<T>; | ||
deleteItemById(id: string, itemId: string): Doc<T>; | ||
import { Op, SerializedCrdtWithId, SerializedList } from "./live"; | ||
declare const INTERNAL: unique symbol; | ||
declare type Dispatch = (ops: Op[]) => void; | ||
declare type Crdt = LiveRecord | LiveList; | ||
export declare type RecordData = Record<string, any>; | ||
export declare class Doc<T extends RecordData = RecordData> { | ||
private _root; | ||
private actor; | ||
private _dispatch; | ||
private _clock; | ||
private _items; | ||
private constructor(); | ||
static from<T>(root: T, actor?: number, dispatch?: Dispatch): Doc<T>; | ||
static load<T>(items: SerializedCrdtWithId[], actor: number, dispatch?: Dispatch): Doc<T>; | ||
dispatch(ops: Op[]): void; | ||
addItem(id: string, item: Crdt): void; | ||
deleteItem(id: string): void; | ||
apply(op: Op): void; | ||
private applyDeleteRecordKey; | ||
private applyUpdateRecord; | ||
private applyCreateRecord; | ||
private applyDeleteRecord; | ||
private applySetParentKey; | ||
get root(): LiveRecord<T>; | ||
count(): number; | ||
generateId(): string; | ||
} | ||
export declare class LiveRecord<T extends RecordData = RecordData> { | ||
private _map; | ||
private _listeners; | ||
private _ctx?; | ||
constructor(object?: T); | ||
static deserialize([id, item]: SerializedCrdtWithId, parentToChildren: Map<string, SerializedCrdtWithId[]>, doc: Doc): LiveRecord<{ | ||
[key: string]: any; | ||
}>; | ||
get [INTERNAL](): { | ||
ctx: { | ||
id: string; | ||
doc: Doc<Record<string, any>>; | ||
parentId?: string | undefined; | ||
} | undefined; | ||
attachChild: (key: keyof T, child: Crdt) => void; | ||
detachChild: (child: Crdt) => void; | ||
detach: () => void; | ||
attach: (id: string, doc: Doc<Record<string, any>>, parentId?: string | undefined, parentKey?: string | undefined) => Op[]; | ||
apply: (op: Op) => void; | ||
}; | ||
private attach; | ||
private attachChild; | ||
private detachChild; | ||
private detach; | ||
private apply; | ||
private notify; | ||
toObject(): T; | ||
set<TKey extends keyof T>(key: TKey, value: T[TKey]): void; | ||
get<TKey extends keyof T>(key: TKey): T[TKey]; | ||
delete<TKey extends keyof T>(key: TKey): void; | ||
update(overrides: Partial<T>): void; | ||
subscribe(listener: () => void): void; | ||
unsubscribe(listener: () => void): void; | ||
} | ||
export declare class LiveList<T extends LiveRecord = LiveRecord> { | ||
private _listeners; | ||
private _ctx?; | ||
private _items; | ||
constructor(items?: T[]); | ||
static deserialize([id, item]: [id: string, item: SerializedList], parentToChildren: Map<string, SerializedCrdtWithId[]>, doc: Doc): LiveList<never>; | ||
get [INTERNAL](): { | ||
ctx: { | ||
id: string; | ||
parentId: string; | ||
doc: Doc<Record<string, any>>; | ||
} | undefined; | ||
attachChild: (key: string, child: LiveRecord<Record<string, any>>) => void; | ||
detachChild: (child: Crdt) => void; | ||
attach: (id: string, doc: Doc<Record<string, any>>, parentId: string, parentKey: string) => Op[]; | ||
detach: () => void; | ||
apply: (op: Op) => void; | ||
setChildKey: (key: string, child: Crdt) => void; | ||
}; | ||
private attach; | ||
private detach; | ||
private attachChild; | ||
private detachChild; | ||
private setChildKey; | ||
private apply; | ||
private notify; | ||
push(item: T): void; | ||
insert(item: T, index: number): void; | ||
move(index: number, targetIndex: number): void; | ||
delete(index: number): void; | ||
toArray(): T[]; | ||
get(index: number): T; | ||
subscribe(listener: () => void): void; | ||
unsubscribe(listener: () => void): void; | ||
} | ||
export {}; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.Doc = exports.createList = exports.createRecord = void 0; | ||
exports.LiveList = exports.LiveRecord = exports.Doc = void 0; | ||
const utils_1 = require("./utils"); | ||
const live_1 = require("./live"); | ||
const position_1 = require("./position"); | ||
const RECORD = Symbol("liveblocks.record"); | ||
const LIST = Symbol("liveblocks.list"); | ||
function createRecord(id, data) { | ||
return Object.assign({ id, $$type: RECORD }, data); | ||
} | ||
exports.createRecord = createRecord; | ||
function createList(id, items = []) { | ||
return { | ||
id, | ||
$$type: LIST, | ||
length: items.length, | ||
toArray: () => items, | ||
map: (callback) => items.map(callback), | ||
}; | ||
} | ||
exports.createList = createList; | ||
function noop() { } | ||
const INTERNAL = Symbol("liveblocks.internal"); | ||
function noOp() { } | ||
class Doc { | ||
constructor(root, _cache, _emit) { | ||
this.root = root; | ||
this._cache = _cache; | ||
this._emit = _emit; | ||
constructor(_root, actor = 0, _dispatch = noOp) { | ||
this._root = _root; | ||
this.actor = actor; | ||
this._dispatch = _dispatch; | ||
this._clock = 0; | ||
this._items = new Map(); | ||
} | ||
static empty(id = "root", emit = noop) { | ||
const root = { | ||
id, | ||
$$type: RECORD, | ||
}; | ||
return new Doc(root, { links: new Map(), listCache: new Map() }, emit); | ||
static from(root, actor = 0, dispatch = noOp) { | ||
const rootRecord = new LiveRecord(root); | ||
const storage = new Doc(rootRecord, actor, dispatch); | ||
const ops = rootRecord[INTERNAL].attach(storage.generateId(), storage); | ||
storage.dispatch(ops); | ||
return storage; | ||
} | ||
static createFromRoot(data, id = "root", emit = noop) { | ||
let doc = Doc.empty(id, emit); | ||
doc = doc.updateRecord(doc.root.id, data); | ||
static load(items, actor, dispatch = noOp) { | ||
if (items.length === 0) { | ||
throw new Error("Internal error: cannot load storage without items"); | ||
} | ||
const parentToChildren = new Map(); | ||
let root = null; | ||
for (const tuple of items) { | ||
const parentId = tuple[1].parentId; | ||
if (parentId == null) { | ||
root = tuple; | ||
} | ||
else { | ||
const children = parentToChildren.get(parentId); | ||
if (children != null) { | ||
children.push(tuple); | ||
} | ||
else { | ||
parentToChildren.set(parentId, [tuple]); | ||
} | ||
} | ||
} | ||
if (root == null) { | ||
throw new Error("Root can't be null"); | ||
} | ||
const doc = new Doc(null, actor, dispatch); | ||
doc._root = LiveRecord.deserialize(root, parentToChildren, doc); | ||
return doc; | ||
} | ||
static load(root, emit = noop) { | ||
let doc = Doc.empty(root.id, emit); | ||
return doc.dispatch({ | ||
type: live_1.OpType.RecordUpdate, | ||
id: root.id, | ||
data: root.data, | ||
}); | ||
dispatch(ops) { | ||
this._dispatch(ops); | ||
} | ||
get data() { | ||
return this.root; | ||
addItem(id, item) { | ||
this._items.set(id, item); | ||
} | ||
dispatch(op, shouldEmit = false) { | ||
if (shouldEmit) { | ||
this._emit(op); | ||
deleteItem(id) { | ||
this._items.delete(id); | ||
} | ||
apply(op) { | ||
switch (op.type) { | ||
case live_1.OpType.UpdateRecord: { | ||
this.applyUpdateRecord(op); | ||
break; | ||
} | ||
case live_1.OpType.CreateRecord: { | ||
this.applyCreateRecord(op); | ||
break; | ||
} | ||
case live_1.OpType.DeleteRecord: { | ||
this.applyDeleteRecord(op); | ||
break; | ||
} | ||
case live_1.OpType.SetParentKey: { | ||
this.applySetParentKey(op); | ||
break; | ||
} | ||
case live_1.OpType.DeleteRecordKey: { | ||
this.applyDeleteRecordKey(op); | ||
break; | ||
} | ||
} | ||
if (op.id === this.root.id) { | ||
const node = dispatch(this.root, op, this._cache, []); | ||
return new Doc(node, this._cache, this._emit); | ||
} | ||
applyDeleteRecordKey(op) { | ||
const item = this._items.get(op.id); | ||
if (item) { | ||
item[INTERNAL].apply(op); | ||
} | ||
else { | ||
const links = getAllLinks(op.id, this.root.id, this._cache.links); | ||
const node = dispatch(this.root, op, this._cache, links); | ||
return new Doc(node, this._cache, this._emit); | ||
} | ||
applyUpdateRecord(op) { | ||
const item = this._items.get(op.id); | ||
if (item) { | ||
item[INTERNAL].apply(op); | ||
} | ||
} | ||
getChild(id) { | ||
if (id === this.root.id) { | ||
return this.root; | ||
applyCreateRecord(op) { | ||
const newRecord = new LiveRecord(op.data); | ||
newRecord[INTERNAL].attach(op.id, this, op.parentId, op.parentKey); | ||
if (op.parentId && op.parentKey) { | ||
const parent = this._items.get(op.parentId); | ||
if (parent == null) { | ||
throw new Error("Parent is missing"); | ||
} | ||
parent[INTERNAL].attachChild(op.parentKey, newRecord); | ||
} | ||
const allLinks = getAllLinks(id, this.root.id, this._cache.links); | ||
return getChildDeep(this.root, id, allLinks, this._cache); | ||
} | ||
updateRecord(id, overrides) { | ||
const currentRecord = this.getChild(id); | ||
if (currentRecord == null) { | ||
throw new Error(`Record with id "${id}" does not exist`); | ||
applyDeleteRecord(op) { | ||
const item = this._items.get(op.id); | ||
if (item == null) { | ||
return; | ||
} | ||
let data = {}; | ||
for (const key in overrides) { | ||
const value = overrides[key]; | ||
data[key] = serialize(value); | ||
const parentId = item[INTERNAL].ctx.parentId; | ||
if (parentId == null) { | ||
return; | ||
} | ||
const op = { | ||
id: currentRecord.id, | ||
type: live_1.OpType.RecordUpdate, | ||
data, | ||
}; | ||
return this.dispatch(op, true); | ||
const parent = this._items.get(parentId); | ||
if (parent) { | ||
parent[INTERNAL].detachChild(item); | ||
} | ||
} | ||
pushItem(id, item) { | ||
const list = this.getChild(id); | ||
if (list == null) { | ||
throw new Error(`List with id "${id}" does not exist`); | ||
applySetParentKey(op) { | ||
const item = this._items.get(op.id); | ||
if (item == null) { | ||
return; | ||
} | ||
if (list.$$type !== LIST) { | ||
throw new Error(`Node with id "${id}" is not a list`); | ||
const parentId = item[INTERNAL].ctx.parentId; | ||
if (parentId == null) { | ||
return; | ||
} | ||
if (!isRecord(item)) { | ||
throw new Error("List can't only have Record as children"); | ||
const parent = this._items.get(parentId); | ||
if (parent && parent instanceof LiveList) { | ||
parent[INTERNAL].setChildKey(op.parentKey, item); | ||
} | ||
const data = serialize(item); | ||
if (list.length === 0) { | ||
return this.dispatch({ | ||
type: live_1.OpType.ListInsert, | ||
id: list.id, | ||
position: position_1.makePosition(), | ||
data, | ||
}, true); | ||
} | ||
const items = sortedListItems(getListItems(this._cache, id)); | ||
const [tailPosition] = items[items.length - 1]; | ||
const position = position_1.makePosition(tailPosition); | ||
const operation = { | ||
type: live_1.OpType.ListInsert, | ||
id: list.id, | ||
position, | ||
data, | ||
}; | ||
return this.dispatch(operation, true); | ||
} | ||
moveItem(id, index, targetIndex) { | ||
const list = this.getChild(id); | ||
if (list == null) { | ||
throw new Error(`List with id "${id}" does not exist`); | ||
get root() { | ||
return this._root; | ||
} | ||
count() { | ||
return this._items.size; | ||
} | ||
generateId() { | ||
return `${this.actor}:${this._clock++}`; | ||
} | ||
} | ||
exports.Doc = Doc; | ||
class LiveRecord { | ||
constructor(object = {}) { | ||
this._listeners = []; | ||
this._map = new Map(Object.entries(object)); | ||
} | ||
static deserialize([id, item], parentToChildren, doc) { | ||
if (item.type !== live_1.CrdtType.Record) { | ||
throw new Error(`Tried to deserialize a record but item type is "${item.type}"`); | ||
} | ||
if (list.$$type !== LIST) { | ||
throw new Error(`Node with id "${id}" is not a list`); | ||
const record = new LiveRecord(item.data); | ||
record.attach(id, doc, item.parentId, item.parentKey); | ||
const children = parentToChildren.get(id); | ||
if (children == null) { | ||
return record; | ||
} | ||
const items = sortedListItems(getListItems(this._cache, id)); | ||
if (targetIndex < 0) { | ||
throw new Error("targetIndex cannot be less than 0"); | ||
for (const entry of children) { | ||
const crdt = entry[1]; | ||
if (crdt.parentKey == null) { | ||
throw new Error("Tried to deserialize a crdt but it does not have a parentKey and is not the root"); | ||
} | ||
const child = deserialize(entry, parentToChildren, doc); | ||
record._map.set(crdt.parentKey, child); | ||
} | ||
if (targetIndex >= items.length) { | ||
throw new Error("targetIndex cannot be greater or equal than the list length"); | ||
return record; | ||
} | ||
get [INTERNAL]() { | ||
return { | ||
ctx: this._ctx, | ||
attachChild: this.attachChild.bind(this), | ||
detachChild: this.detachChild.bind(this), | ||
detach: this.detach.bind(this), | ||
attach: this.attach.bind(this), | ||
apply: this.apply.bind(this), | ||
}; | ||
} | ||
attach(id, doc, parentId, parentKey) { | ||
if (this._ctx) { | ||
throw new Error("LiveRecord is already part of the storage!"); | ||
} | ||
if (index < 0) { | ||
throw new Error("index cannot be less than 0"); | ||
doc.addItem(id, this); | ||
this._ctx = { | ||
id, | ||
doc: doc, | ||
parentId, | ||
}; | ||
const ops = []; | ||
const createOp = { | ||
id: this._ctx.id, | ||
type: live_1.OpType.CreateRecord, | ||
parentId, | ||
parentKey, | ||
data: {}, | ||
}; | ||
ops.push(createOp); | ||
for (const [key, value] of this._map) { | ||
if (value instanceof LiveRecord) { | ||
ops.push(...value.attach(doc.generateId(), doc, this._ctx.id, key)); | ||
} | ||
else if (value instanceof LiveList) { | ||
ops.push(...value[INTERNAL].attach(doc.generateId(), doc, this._ctx.id, key)); | ||
} | ||
else { | ||
createOp.data[key] = value; | ||
} | ||
} | ||
if (index >= items.length) { | ||
throw new Error("index cannot be greater or equal than the list length"); | ||
return ops; | ||
} | ||
attachChild(key, child) { | ||
this._map.set(key, child); | ||
this.notify(); | ||
} | ||
detachChild(child) { | ||
for (const [key, value] of this._map) { | ||
if (value === child) { | ||
this._map.delete(key); | ||
} | ||
} | ||
if (index === targetIndex) { | ||
return this; | ||
if (child instanceof LiveRecord) { | ||
child.detach(); | ||
} | ||
let beforePosition = null; | ||
let afterPosition = null; | ||
if (index < targetIndex) { | ||
afterPosition = | ||
targetIndex === items.length - 1 | ||
? undefined | ||
: items[targetIndex + 1][0]; | ||
beforePosition = items[targetIndex][0]; | ||
this.notify(); | ||
} | ||
detach() { | ||
if (this._ctx == null) { | ||
return; | ||
} | ||
else { | ||
afterPosition = items[targetIndex][0]; | ||
beforePosition = | ||
targetIndex === 0 ? undefined : items[targetIndex - 1][0]; | ||
this._ctx.doc.deleteItem(this._ctx.id); | ||
for (const [, value] of this._map) { | ||
if (value instanceof LiveRecord) { | ||
value.detach(); | ||
} | ||
} | ||
const position = position_1.makePosition(beforePosition, afterPosition); | ||
const [, item] = items[index]; | ||
return this.dispatch({ | ||
type: live_1.OpType.ListMove, | ||
id: list.id, | ||
itemId: item.id, | ||
position, | ||
}, true); | ||
} | ||
deleteItem(id, index) { | ||
const list = this.getChild(id); | ||
if (list == null) { | ||
throw new Error(`List with id "${id}" does not exist`); | ||
apply(op) { | ||
if (op.type === live_1.OpType.UpdateRecord) { | ||
for (const key in op.data) { | ||
const oldValue = this._map.get(key); | ||
if (oldValue instanceof LiveRecord) { | ||
oldValue.detach(); | ||
} | ||
const value = op.data[key]; | ||
this._map.set(key, value); | ||
} | ||
this.notify(); | ||
} | ||
if (list.$$type !== LIST) { | ||
throw new Error(`Node with id "${id}" is not a list`); | ||
else if (op.type === live_1.OpType.DeleteRecordKey) { | ||
const key = op.key; | ||
const oldValue = this._map.get(key); | ||
if (oldValue instanceof LiveRecord) { | ||
oldValue.detach(); | ||
} | ||
this._map.delete(key); | ||
this.notify(); | ||
} | ||
const items = sortedListItems(getListItems(this._cache, id)); | ||
const [, item] = items[index]; | ||
return this.dispatch({ | ||
type: live_1.OpType.ListRemove, | ||
id: list.id, | ||
itemId: item.id, | ||
}, true); | ||
} | ||
deleteItemById(id, itemId) { | ||
const list = this.getChild(id); | ||
if (list == null) { | ||
throw new Error(`List with id "${id}" does not exist`); | ||
notify() { | ||
for (const listener of this._listeners) { | ||
listener(); | ||
} | ||
if (list.$$type !== LIST) { | ||
throw new Error(`Node with id "${id}" is not a list`); | ||
} | ||
const itemsMap = getListItems(this._cache, id); | ||
let item = null; | ||
for (const [, crdt] of itemsMap) { | ||
if (crdt.id === itemId) { | ||
item = crdt; | ||
break; | ||
} | ||
toObject() { | ||
return Object.fromEntries(this._map); | ||
} | ||
set(key, value) { | ||
// TODO: Find out why typescript complains | ||
this.update({ [key]: value }); | ||
} | ||
get(key) { | ||
return this._map.get(key); | ||
} | ||
delete(key) { | ||
if (this._ctx) { | ||
const ops = []; | ||
const item = this._map.get(key); | ||
if (item instanceof LiveRecord) { | ||
item.detach(); | ||
} | ||
this._ctx.doc.dispatch([ | ||
{ type: live_1.OpType.DeleteRecordKey, id: this._ctx.id, key: key }, | ||
]); | ||
} | ||
if (item == null) { | ||
throw new Error(`List with id "${id}" does not have an item with id "${itemId}"`); | ||
} | ||
return this.dispatch({ | ||
type: live_1.OpType.ListRemove, | ||
id: list.id, | ||
itemId: item.id, | ||
}, true); | ||
this._map.delete(key); | ||
this.notify(); | ||
} | ||
} | ||
exports.Doc = Doc; | ||
function getAllLinks(id, rootId, links) { | ||
let currentId = id; | ||
const result = []; | ||
do { | ||
const link = links.get(currentId); | ||
if (link == null) { | ||
throw new Error(`Can't find link for id "${currentId}"`); | ||
update(overrides) { | ||
if (this._ctx) { | ||
const ops = []; | ||
const updateOperation = { | ||
id: this._ctx.id, | ||
type: live_1.OpType.UpdateRecord, | ||
data: {}, | ||
}; | ||
ops.push(updateOperation); | ||
for (const key in overrides) { | ||
const oldValue = this._map.get(key); | ||
if (oldValue instanceof LiveRecord) { | ||
oldValue.detach(); | ||
} | ||
const value = overrides[key]; | ||
if (value instanceof LiveRecord) { | ||
ops.push(...value.attach(this._ctx.doc.generateId(), this._ctx.doc, this._ctx.id, key)); | ||
} | ||
else { | ||
updateOperation.data[key] = value; | ||
} | ||
this._map.set(key, value); | ||
} | ||
this._ctx.doc.dispatch(ops); | ||
this.notify(); | ||
} | ||
currentId = link.parentId; | ||
result.push(link); | ||
} while (currentId !== rootId); | ||
return result; | ||
} | ||
function deserializeList(serialized, cache) { | ||
const listItems = new Map(); | ||
for (const position in serialized.data) { | ||
const item = deserialize(serialized.data[position], cache); | ||
if (!isRecord(item)) { | ||
throw new Error("TODO"); | ||
else { | ||
for (const key in overrides) { | ||
const value = overrides[key]; | ||
this._map.set(key, value); | ||
} | ||
this.notify(); | ||
} | ||
listItems.set(position, item); | ||
cache.links.set(item.id, { parentId: serialized.id, parentKey: position }); | ||
} | ||
cache.listCache.set(serialized.id, listItems); | ||
return createList(serialized.id, listItemsToArray(listItems)); | ||
} | ||
function getListItems(cache, listId) { | ||
const items = cache.listCache.get(listId); | ||
if (items == null) { | ||
throw new Error(`Can't find list cache for id "${listId}"`); | ||
subscribe(listener) { | ||
this._listeners.push(listener); | ||
} | ||
return items; | ||
unsubscribe(listener) { | ||
utils_1.remove(this._listeners, listener); | ||
} | ||
} | ||
function deserializeRecord(serialized, cache) { | ||
const result = { | ||
id: serialized.id, | ||
$$type: RECORD, | ||
}; | ||
for (const key in serialized.data) { | ||
const item = deserialize(serialized.data[key], cache); | ||
if (isCrdt(item)) { | ||
cache.links.set(item.id, { | ||
parentId: serialized.id, | ||
parentKey: key, | ||
}); | ||
exports.LiveRecord = LiveRecord; | ||
class LiveList { | ||
constructor(items = []) { | ||
this._listeners = []; | ||
// TODO: Find a better data structure | ||
this._items = []; | ||
let position = undefined; | ||
for (let i = 0; i < items.length; i++) { | ||
const newPosition = position_1.makePosition(position); | ||
this._items.push([items[i], newPosition]); | ||
position = newPosition; | ||
} | ||
result[key] = item; | ||
} | ||
return result; | ||
} | ||
function deserialize(serialized, cache) { | ||
switch (serialized.type) { | ||
case live_1.CrdtType.Register: { | ||
return serialized.data; | ||
static deserialize([id, item], parentToChildren, doc) { | ||
const list = new LiveList([]); | ||
list.attach(id, doc, item.parentId, item.parentKey); | ||
const children = parentToChildren.get(id); | ||
if (children == null) { | ||
return list; | ||
} | ||
case live_1.CrdtType.Record: { | ||
return deserializeRecord(serialized, cache); | ||
for (const entry of children) { | ||
const child = LiveRecord.deserialize(entry, parentToChildren, doc); | ||
list.attachChild(entry[1].parentKey, child); | ||
} | ||
case live_1.CrdtType.List: { | ||
return deserializeList(serialized, cache); | ||
} | ||
default: { | ||
throw new Error("TODO"); | ||
} | ||
return list; | ||
} | ||
} | ||
function dispatchOnRecord(record, op, cache, links) { | ||
if (links.length === 0) { | ||
if (record.id !== op.id) { | ||
throw new Error("TODO"); | ||
get [INTERNAL]() { | ||
return { | ||
ctx: this._ctx, | ||
attachChild: this.attachChild.bind(this), | ||
detachChild: this.detachChild.bind(this), | ||
attach: this.attach.bind(this), | ||
detach: this.detach.bind(this), | ||
apply: this.apply.bind(this), | ||
setChildKey: this.setChildKey.bind(this), | ||
}; | ||
} | ||
attach(id, doc, parentId, parentKey) { | ||
if (this._ctx) { | ||
throw new Error("LiveList is already part of the storage!"); | ||
} | ||
switch (op.type) { | ||
case live_1.OpType.RecordUpdate: { | ||
return updateRecord(record, op, cache); | ||
} | ||
default: { | ||
console.warn("Unsupported operation"); | ||
return record; | ||
} | ||
doc.addItem(id, this); | ||
this._ctx = { | ||
doc: doc, | ||
id: id, | ||
parentId: parentId, | ||
}; | ||
const ops = []; | ||
const createOp = { | ||
id: this._ctx.id, | ||
type: live_1.OpType.CreateList, | ||
parentId, | ||
parentKey, | ||
}; | ||
ops.push(createOp); | ||
for (const [item, position] of this._items) { | ||
ops.push(...item[INTERNAL].attach(doc.generateId(), doc, this._ctx.id, position)); | ||
} | ||
return ops; | ||
} | ||
const currentLink = links.pop(); | ||
const child = record[currentLink.parentKey]; | ||
const newNode = dispatch(child, op, cache, links); | ||
return Object.assign(Object.assign({}, record), { [currentLink.parentKey]: newNode }); | ||
} | ||
function dispatchOnList(list, op, cache, links) { | ||
if (links.length === 0) { | ||
if (list.id !== op.id) { | ||
throw new Error("TODO"); | ||
detach() { | ||
if (this._ctx == null) { | ||
return; | ||
} | ||
switch (op.type) { | ||
case live_1.OpType.ListInsert: { | ||
return listInsert(list, op, cache); | ||
} | ||
case live_1.OpType.ListMove: { | ||
return listMove(list, op, cache); | ||
} | ||
case live_1.OpType.ListRemove: { | ||
return listDelete(list, op, cache); | ||
} | ||
default: { | ||
console.warn("Unsupported operation"); | ||
return list; | ||
} | ||
} | ||
this._ctx.doc.deleteItem(this._ctx.id); | ||
} | ||
const currentLink = links.pop(); | ||
const position = currentLink.parentKey; | ||
const items = getListItems(cache, list.id); | ||
const item = items.get(position); | ||
if (item == null) { | ||
throw new Error("TODO"); | ||
attachChild(key, child) { | ||
this._items.push([child, key]); | ||
this._items.sort((itemA, itemB) => position_1.compare({ position: itemA[1] }, { position: itemB[1] })); | ||
this.notify(); | ||
} | ||
const newItem = dispatch(item, op, cache, links); | ||
items.set(position, newItem); | ||
return createList(list.id, listItemsToArray(items)); | ||
} | ||
function dispatch(node, op, cache, links) { | ||
switch (node.$$type) { | ||
case RECORD: | ||
return dispatchOnRecord(node, op, cache, links); | ||
case LIST: | ||
return dispatchOnList(node, op, cache, links); | ||
default: { | ||
throw new Error("Unknown CRDT"); | ||
detachChild(child) { | ||
const indexToDelete = this._items.findIndex((item) => item[0] === child); | ||
this._items.splice(indexToDelete); | ||
if (child instanceof LiveRecord) { | ||
child[INTERNAL].detach(); | ||
} | ||
this.notify(); | ||
} | ||
} | ||
function updateRecord(node, op, cache) { | ||
const result = Object.assign({}, node); | ||
for (const key in op.data) { | ||
const value = op.data[key]; | ||
const item = deserialize(value, cache); | ||
if (isCrdt(item)) { | ||
cache.links.set(item.id, { parentId: node.id, parentKey: key }); | ||
setChildKey(key, child) { | ||
const item = this._items.find((item) => item[0] === child); | ||
if (item) { | ||
item[1] = key; | ||
} | ||
result[key] = item; | ||
this._items.sort((itemA, itemB) => position_1.compare({ position: itemA[1] }, { position: itemB[1] })); | ||
this.notify(); | ||
} | ||
return result; | ||
} | ||
function listInsert(list, op, cache) { | ||
const items = getListItems(cache, list.id); | ||
const item = deserialize(op.data, cache); | ||
if (isCrdt(item)) { | ||
items.set(op.position, item); | ||
cache.links.set(item.id, { parentId: list.id, parentKey: op.position }); | ||
apply(op) { } | ||
notify() { | ||
for (const listener of this._listeners) { | ||
listener(); | ||
} | ||
} | ||
return createList(list.id, listItemsToArray(items)); | ||
} | ||
function listMove(list, op, cache) { | ||
const items = getListItems(cache, list.id); | ||
const link = getLinkOrThrow(cache, op.itemId); | ||
const item = items.get(link.parentKey); | ||
if (item == null) { | ||
throw new Error("TODO"); | ||
push(item) { | ||
const position = this._items.length === 0 | ||
? position_1.makePosition() | ||
: position_1.makePosition(this._items[this._items.length - 1][1]); | ||
this._items.push([item, position]); | ||
this.notify(); | ||
if (this._ctx) { | ||
const ops = item[INTERNAL].attach(this._ctx.doc.generateId(), this._ctx.doc, this._ctx.id, position); | ||
this._ctx.doc.dispatch(ops); | ||
} | ||
} | ||
// Delete old position cache entry | ||
items.delete(link.parentKey); | ||
// Insert new position in cache | ||
items.set(op.position, item); | ||
// Update link | ||
cache.links.set(op.itemId, { parentId: list.id, parentKey: op.position }); | ||
return createList(list.id, listItemsToArray(items)); | ||
} | ||
function getLinkOrThrow(cache, id) { | ||
const link = cache.links.get(id); | ||
if (link == null) { | ||
throw new Error(`Can't find link with id "${id}"`); | ||
insert(item, index) { | ||
if (index < 0 || index > this._items.length) { | ||
throw new Error(`Cannot delete list item at index "${index}". index should be between 0 and ${this._items.length}`); | ||
} | ||
let before = this._items[index - 1] ? this._items[index - 1][1] : undefined; | ||
let after = this._items[index] ? this._items[index][1] : undefined; | ||
const position = position_1.makePosition(before, after); | ||
this._items.push([item, position]); | ||
this._items.sort((itemA, itemB) => position_1.compare({ position: itemA[1] }, { position: itemB[1] })); | ||
this.notify(); | ||
if (this._ctx) { | ||
const ops = item[INTERNAL].attach(this._ctx.doc.generateId(), this._ctx.doc, this._ctx.id, position); | ||
this._ctx.doc.dispatch(ops); | ||
} | ||
} | ||
return link; | ||
} | ||
function listDelete(list, op, cache) { | ||
const items = getListItems(cache, list.id); | ||
const link = getLinkOrThrow(cache, op.itemId); | ||
items.delete(link.parentKey); | ||
cache.links.delete(op.itemId); | ||
return createList(list.id, listItemsToArray(items)); | ||
} | ||
function listItemsToArray(items) { | ||
return sortedListItems(items).map((entry) => entry[1]); | ||
} | ||
function sortedListItems(items) { | ||
return Array.from(items.entries()).sort((entryA, entryB) => position_1.compare({ position: entryA[0] }, { position: entryB[0] })); | ||
} | ||
function getChildDeep(node, id, links, cache) { | ||
let currentNode = node; | ||
while (currentNode.id !== id) { | ||
const link = links.pop(); | ||
if (link == null || link.parentId !== currentNode.id) { | ||
throw new Error("TODO"); | ||
move(index, targetIndex) { | ||
if (targetIndex < 0) { | ||
throw new Error("targetIndex cannot be less than 0"); | ||
} | ||
if (currentNode.$$type === RECORD) { | ||
currentNode = currentNode[link.parentKey]; | ||
if (targetIndex >= this._items.length) { | ||
throw new Error("targetIndex cannot be greater or equal than the list length"); | ||
} | ||
if (index < 0) { | ||
throw new Error("index cannot be less than 0"); | ||
} | ||
if (index >= this._items.length) { | ||
throw new Error("index cannot be greater or equal than the list length"); | ||
} | ||
let beforePosition = null; | ||
let afterPosition = null; | ||
if (index < targetIndex) { | ||
afterPosition = | ||
targetIndex === this._items.length - 1 | ||
? undefined | ||
: this._items[targetIndex + 1][1]; | ||
beforePosition = this._items[targetIndex][1]; | ||
} | ||
else { | ||
const listItems = getListItems(cache, currentNode.id); | ||
const item = listItems.get(link.parentKey); | ||
if (item == null) { | ||
throw new Error("TODO"); | ||
} | ||
currentNode = item; | ||
afterPosition = this._items[targetIndex][1]; | ||
beforePosition = | ||
targetIndex === 0 ? undefined : this._items[targetIndex - 1][1]; | ||
} | ||
const position = position_1.makePosition(beforePosition, afterPosition); | ||
const item = this._items[index]; | ||
item[1] = position; | ||
this._items.sort((itemA, itemB) => position_1.compare({ position: itemA[1] }, { position: itemB[1] })); | ||
this.notify(); | ||
if (this._ctx) { | ||
this._ctx.doc.dispatch([ | ||
{ | ||
type: live_1.OpType.SetParentKey, | ||
id: item[0][INTERNAL].ctx.id, | ||
parentKey: position, | ||
}, | ||
]); | ||
} | ||
} | ||
return currentNode; | ||
} | ||
function isRecord(value) { | ||
return value != null && typeof value === "object" && value.$$type === RECORD; | ||
} | ||
function isList(value) { | ||
return value != null && typeof value === "object" && value.$$type === LIST; | ||
} | ||
function isCrdt(value) { | ||
return isRecord(value) || isList(value); | ||
} | ||
function serializeRecord(record) { | ||
const serializedData = {}; | ||
for (const key in record) { | ||
if (key !== "id" && key !== "$$type") { | ||
const value = record[key]; // TODO: Find out why typescript does not like that | ||
serializedData[key] = serialize(value); | ||
delete(index) { | ||
if (index < 0 || index >= this._items.length) { | ||
throw new Error(`Cannot delete list item at index "${index}". index should be between 0 and ${this._items.length - 1}`); | ||
} | ||
const item = this._items[index]; | ||
this._items.splice(index, 1); | ||
if (this._ctx) { | ||
const childRecord = item[0]; | ||
this._ctx.doc.dispatch([ | ||
{ | ||
id: childRecord[INTERNAL].ctx.id, | ||
type: live_1.OpType.DeleteRecord, | ||
}, | ||
]); | ||
childRecord[INTERNAL].detach(); | ||
} | ||
this.notify(); | ||
} | ||
return { | ||
id: record.id, | ||
type: live_1.CrdtType.Record, | ||
data: serializedData, | ||
}; | ||
} | ||
function serializeList(list) { | ||
return { | ||
id: list.id, | ||
type: live_1.CrdtType.List, | ||
data: {}, | ||
}; | ||
} | ||
function serialize(value) { | ||
if (isRecord(value)) { | ||
return serializeRecord(value); | ||
toArray() { | ||
return this._items.map((entry) => entry[0]); | ||
} | ||
else if (isList(value)) { | ||
return serializeList(value); | ||
get(index) { | ||
return this._items[index][0]; | ||
} | ||
else { | ||
return { type: live_1.CrdtType.Register, data: value }; | ||
subscribe(listener) { | ||
this._listeners.push(listener); | ||
} | ||
unsubscribe(listener) { | ||
utils_1.remove(this._listeners, listener); | ||
} | ||
} | ||
exports.LiveList = LiveList; | ||
function deserialize(entry, parentToChildren, doc) { | ||
switch (entry[1].type) { | ||
case live_1.CrdtType.Record: { | ||
return LiveRecord.deserialize(entry, parentToChildren, doc); | ||
} | ||
case live_1.CrdtType.List: { | ||
return LiveList.deserialize(entry, parentToChildren, doc); | ||
} | ||
default: { | ||
throw new Error("Unexpected CRDT type"); | ||
} | ||
} | ||
} |
@@ -1,4 +0,3 @@ | ||
export type { Record, RecordData, List } from "./doc"; | ||
export { LiveRecord, LiveList, RecordData } from "./doc"; | ||
export type { Others, Presence, Room, Client, User } from "./types"; | ||
export { createClient } from "./client"; | ||
export { LiveStorageState } from "./types"; | ||
export type { Others, Presence, Room, InitialStorageFactory, Client, LiveStorage, User, } from "./types"; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.LiveStorageState = exports.createClient = void 0; | ||
exports.createClient = exports.LiveList = exports.LiveRecord = void 0; | ||
var doc_1 = require("./doc"); | ||
Object.defineProperty(exports, "LiveRecord", { enumerable: true, get: function () { return doc_1.LiveRecord; } }); | ||
Object.defineProperty(exports, "LiveList", { enumerable: true, get: function () { return doc_1.LiveList; } }); | ||
var client_1 = require("./client"); | ||
Object.defineProperty(exports, "createClient", { enumerable: true, get: function () { return client_1.createClient; } }); | ||
var types_1 = require("./types"); | ||
Object.defineProperty(exports, "LiveStorageState", { enumerable: true, get: function () { return types_1.LiveStorageState; } }); |
@@ -41,5 +41,6 @@ import { Presence } from "./types"; | ||
}; | ||
export declare type SerializedCrdtWithId = [id: string, crdt: SerializedCrdt]; | ||
export declare type InitialDocumentStateMessage = { | ||
type: ServerMessageType.InitialStorageState; | ||
root: SerializedRecord | null; | ||
items: SerializedCrdtWithId[]; | ||
}; | ||
@@ -75,57 +76,64 @@ export declare type UpdateStorageMessage = { | ||
Record = 0, | ||
List = 1, | ||
Register = 2 | ||
List = 1 | ||
} | ||
export declare type SerializedRecord = { | ||
id: string; | ||
type: CrdtType.Record; | ||
parentId?: string; | ||
parentKey?: string; | ||
data: { | ||
[key: string]: SerializedCrdt; | ||
[key: string]: any; | ||
}; | ||
}; | ||
export declare type SerializedList = { | ||
id: string; | ||
type: CrdtType.List; | ||
data: { | ||
[position: string]: SerializedCrdt; | ||
}; | ||
parentId: string; | ||
parentKey: string; | ||
}; | ||
export declare type SerializedRegister = { | ||
id?: string; | ||
type: CrdtType.Register; | ||
data: any; | ||
}; | ||
export declare type SerializedCrdt = SerializedRecord | SerializedList | SerializedRegister; | ||
export declare type SerializedCrdt = SerializedRecord | SerializedList; | ||
export declare enum OpType { | ||
Init = 100, | ||
ListInsert = 200, | ||
ListMove = 201, | ||
ListRemove = 202, | ||
RecordUpdate = 300 | ||
Init = 0, | ||
SetParentKey = 1, | ||
CreateList = 2, | ||
UpdateRecord = 3, | ||
CreateRecord = 4, | ||
DeleteRecord = 5, | ||
DeleteRecordKey = 6 | ||
} | ||
export declare type Op = RecordUpdateOp | ListInsertOp | ListDeleteOp | ListMoveOp; | ||
export declare type Op = CreateRecordOp | RecordUpdateOp | DeleteRecordOp | CreateListOp | SetParentKeyOp | DeleteRecordKeyOp; | ||
export declare type RecordUpdateOp = { | ||
id: string; | ||
type: OpType.RecordUpdate; | ||
type: OpType.UpdateRecord; | ||
data: { | ||
[key: string]: SerializedCrdt; | ||
[key: string]: any; | ||
}; | ||
}; | ||
export declare type ListInsertOp = { | ||
export declare type CreateRecordOp = { | ||
id: string; | ||
type: OpType.ListInsert; | ||
position: string; | ||
data: SerializedCrdt; | ||
type: OpType.CreateRecord; | ||
parentId?: string; | ||
parentKey?: string; | ||
data: { | ||
[key: string]: any; | ||
}; | ||
}; | ||
export declare type ListMoveOp = { | ||
export declare type CreateListOp = { | ||
id: string; | ||
type: OpType.ListMove; | ||
itemId: string; | ||
position: string; | ||
type: OpType.CreateList; | ||
parentId: string; | ||
parentKey: string; | ||
}; | ||
export declare type ListDeleteOp = { | ||
export declare type DeleteRecordOp = { | ||
id: string; | ||
type: OpType.ListRemove; | ||
itemId: string; | ||
type: OpType.DeleteRecord; | ||
}; | ||
export declare type SetParentKeyOp = { | ||
id: string; | ||
type: OpType.SetParentKey; | ||
parentKey: string; | ||
}; | ||
export declare type DeleteRecordKeyOp = { | ||
id: string; | ||
type: OpType.DeleteRecordKey; | ||
key: string; | ||
}; | ||
export declare enum WebsocketCloseCodes { | ||
@@ -132,0 +140,0 @@ CLOSE_ABNORMAL = 1006, |
@@ -25,11 +25,12 @@ "use strict"; | ||
CrdtType[CrdtType["List"] = 1] = "List"; | ||
CrdtType[CrdtType["Register"] = 2] = "Register"; | ||
})(CrdtType = exports.CrdtType || (exports.CrdtType = {})); | ||
var OpType; | ||
(function (OpType) { | ||
OpType[OpType["Init"] = 100] = "Init"; | ||
OpType[OpType["ListInsert"] = 200] = "ListInsert"; | ||
OpType[OpType["ListMove"] = 201] = "ListMove"; | ||
OpType[OpType["ListRemove"] = 202] = "ListRemove"; | ||
OpType[OpType["RecordUpdate"] = 300] = "RecordUpdate"; | ||
OpType[OpType["Init"] = 0] = "Init"; | ||
OpType[OpType["SetParentKey"] = 1] = "SetParentKey"; | ||
OpType[OpType["CreateList"] = 2] = "CreateList"; | ||
OpType[OpType["UpdateRecord"] = 3] = "UpdateRecord"; | ||
OpType[OpType["CreateRecord"] = 4] = "CreateRecord"; | ||
OpType[OpType["DeleteRecord"] = 5] = "DeleteRecord"; | ||
OpType[OpType["DeleteRecordKey"] = 6] = "DeleteRecordKey"; | ||
})(OpType = exports.OpType || (exports.OpType = {})); | ||
@@ -36,0 +37,0 @@ var WebsocketCloseCodes; |
@@ -1,4 +0,3 @@ | ||
import { RecordData, List } from "."; | ||
import { Doc, Record } from "./doc"; | ||
import { Others, Presence, ClientOptions, Room, InitialStorageFactory, MyPresenceCallback, OthersEventCallback, StorageCallback, AuthEndpoint, LiveStorageState, LiveStorage, EventCallback, User, Connection, Serializable, ErrorCallback, AuthenticationToken, ConnectionCallback } from "./types"; | ||
import { RecordData } from "./doc"; | ||
import { Others, Presence, ClientOptions, Room, MyPresenceCallback, OthersEventCallback, AuthEndpoint, EventCallback, User, Connection, ErrorCallback, AuthenticationToken, ConnectionCallback } from "./types"; | ||
import { ClientMessage, Op } from "./live"; | ||
@@ -24,3 +23,2 @@ declare type IdFactory = () => string; | ||
listeners: { | ||
storage: StorageCallback[]; | ||
event: EventCallback[]; | ||
@@ -39,5 +37,5 @@ others: OthersEventCallback[]; | ||
numberOfRetry: number; | ||
doc: Doc<any> | null; | ||
storageState: LiveStorageState; | ||
initialStorageFactory: InitialStorageFactory | null; | ||
defaultStorageRoot?: { | ||
[key: string]: any; | ||
}; | ||
}; | ||
@@ -73,6 +71,5 @@ export declare type Effects = { | ||
subscribe: { | ||
<T extends Serializable>(type: "my-presence", listener: MyPresenceCallback<T>): void; | ||
<T_1 extends Serializable>(type: "others", listener: OthersEventCallback<T_1>): void; | ||
<T extends Presence>(type: "my-presence", listener: MyPresenceCallback<T>): void; | ||
<T_1 extends Presence>(type: "others", listener: OthersEventCallback<T_1>): void; | ||
(type: "event", listener: EventCallback): void; | ||
<T_2 extends RecordData>(type: "storage", listener: StorageCallback<T_2>): void; | ||
(type: "error", listener: ErrorCallback): void; | ||
@@ -82,28 +79,23 @@ (type: "connection", listener: ConnectionCallback): void; | ||
unsubscribe: { | ||
<T_3 extends Serializable>(type: "my-presence", listener: MyPresenceCallback<T_3>): void; | ||
<T_4 extends Serializable>(type: "others", listener: OthersEventCallback<T_4>): void; | ||
<T_2 extends Presence>(type: "my-presence", listener: MyPresenceCallback<T_2>): void; | ||
<T_3 extends Presence>(type: "others", listener: OthersEventCallback<T_3>): void; | ||
(type: "event", listener: EventCallback): void; | ||
<T_5 extends RecordData>(type: "storage", listener: StorageCallback<T_5>): void; | ||
(type: "error", listener: ErrorCallback): void; | ||
(type: "connection", listener: ConnectionCallback): void; | ||
}; | ||
updatePresence: <T_6 extends Serializable>(overrides: Partial<T_6>) => void; | ||
updatePresence: <T_4 extends Presence>(overrides: Partial<T_4>) => void; | ||
broadcastEvent: (event: any) => void; | ||
fetchStorage: (initialStorageFactory: InitialStorageFactory) => void; | ||
createRecord: <T_7 extends RecordData>(data: any) => Record<T_7>; | ||
updateRecord: <T_8 extends RecordData>(record: Record<T_8>, overrides: Partial<T_8>) => void; | ||
createList: <T_9 extends RecordData>() => List<Record<T_9>>; | ||
pushItem: <T_10 extends RecordData>(list: List<Record<T_10>>, item: Record<T_10>) => void; | ||
deleteItem: <T_11 extends RecordData>(list: List<Record<T_11>>, index: number) => void; | ||
deleteItemById: <T_12 extends RecordData>(list: List<Record<T_12>>, itemId: string) => void; | ||
moveItem: <T_13 extends RecordData>(list: List<Record<T_13>>, index: number, targetIndex: number) => void; | ||
getStorage: <TRoot>() => Promise<{ | ||
root: import("./doc").LiveRecord<TRoot>; | ||
}>; | ||
selectors: { | ||
getConnectionState: () => "failed" | "closed" | "open" | "connecting" | "authenticating" | "unavailable"; | ||
getSelf: <TPresence extends Serializable = Serializable>() => User<TPresence> | null; | ||
getPresence: <T_14 extends Serializable>() => T_14; | ||
getOthers: <T_15 extends Serializable>() => Others<T_15>; | ||
getStorage: () => LiveStorage; | ||
getSelf: <TPresence extends Presence = Presence>() => User<TPresence> | null; | ||
getPresence: <T_5 extends Presence>() => T_5; | ||
getOthers: <T_6 extends Presence>() => Others<T_6>; | ||
}; | ||
}; | ||
export declare function defaultState(me?: Presence): State; | ||
export declare function defaultState(me?: Presence, defaultStorageRoot?: { | ||
[key: string]: any; | ||
}): State; | ||
export declare type InternalRoom = { | ||
@@ -117,4 +109,5 @@ room: Room; | ||
export declare function createRoom(name: string, options: ClientOptions & { | ||
initialPresence?: Presence; | ||
defaultPresence?: Presence; | ||
defaultStorageRoot?: RecordData; | ||
}): InternalRoom; | ||
export {}; |
@@ -30,10 +30,11 @@ "use strict"; | ||
}; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.createRoom = exports.defaultState = exports.makeStateMachine = void 0; | ||
const doc_1 = require("./doc"); | ||
const types_1 = require("./types"); | ||
const doc_2 = require("./doc"); | ||
const utils_1 = require("./utils"); | ||
const authentication_1 = __importStar(require("./authentication")); | ||
const live_1 = require("./live"); | ||
const storage_1 = __importDefault(require("./storage")); | ||
const BACKOFF_RETRY_DELAYS = [250, 500, 1000, 2000, 4000, 8000, 10000]; | ||
@@ -44,4 +45,3 @@ const HEARTBEAT_INTERVAL = 30000; | ||
function isValidRoomEventType(value) { | ||
return (value === "storage" || | ||
value === "my-presence" || | ||
return (value === "my-presence" || | ||
value === "others" || | ||
@@ -274,10 +274,2 @@ value === "event" || | ||
switch (message.type) { | ||
case live_1.ServerMessageType.InitialStorageState: { | ||
onInitialStorageState(message); | ||
break; | ||
} | ||
case live_1.ServerMessageType.UpdateStorage: { | ||
onStorageUpdates(message); | ||
break; | ||
} | ||
case live_1.ServerMessageType.UserJoined: { | ||
@@ -304,2 +296,3 @@ onUserJoinedMessage(message); | ||
} | ||
storage.onMessage(message); | ||
} | ||
@@ -487,82 +480,29 @@ // function onWakeUp() { | ||
} | ||
/** | ||
* STORAGE | ||
*/ | ||
function onStorageUpdates(message) { | ||
if (state.doc == null) { | ||
// TODO: Cache updates in case they are coming while root is queried | ||
return; | ||
} | ||
updateDoc(message.ops.reduce((doc, op) => doc.dispatch(op), state.doc)); | ||
function dispatch(ops) { | ||
state.flushData.storageOperations.push(...ops); | ||
tryFlushing(); | ||
} | ||
function updateDoc(doc) { | ||
state.doc = doc; | ||
if (doc) { | ||
for (const listener of state.listeners.storage) { | ||
listener(getStorage()); | ||
const storage = new storage_1.default({ | ||
fetchStorage: () => { | ||
state.flushData.messages.push({ type: live_1.ClientMessageType.FetchStorage }); | ||
tryFlushing(); | ||
}, | ||
dispatch, | ||
getConnectionId: () => { | ||
const me = getSelf(); | ||
if (me) { | ||
return me.connectionId; | ||
} | ||
} | ||
} | ||
throw new Error("Unexpected"); | ||
}, | ||
defaultRoot: state.defaultStorageRoot, | ||
}); | ||
function getStorage() { | ||
if (state.storageState === types_1.LiveStorageState.Loaded) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const doc = yield storage.getDocument(); | ||
return { | ||
state: state.storageState, | ||
root: state.doc.root, | ||
root: doc.root, | ||
}; | ||
} | ||
return { | ||
state: state.storageState, | ||
}; | ||
}); | ||
} | ||
function onInitialStorageState(message) { | ||
state.storageState = types_1.LiveStorageState.Loaded; | ||
if (message.root == null) { | ||
const rootId = makeId(); | ||
state.doc = doc_1.Doc.empty(rootId, (op) => dispatch(op)); | ||
updateDoc(state.doc.updateRecord(rootId, state.initialStorageFactory({ | ||
createRecord: (data) => createRecord(data), | ||
createList: () => createList(), | ||
}))); | ||
} | ||
else { | ||
updateDoc(doc_1.Doc.load(message.root, (op) => dispatch(op))); | ||
} | ||
} | ||
function makeId() { | ||
if (state.idFactory == null) { | ||
throw new Error("Can't generate id. Id factory is missing."); | ||
} | ||
return state.idFactory(); | ||
} | ||
function dispatch(op) { | ||
state.flushData.storageOperations.push(op); | ||
tryFlushing(); | ||
} | ||
function createRecord(data) { | ||
return doc_2.createRecord(makeId(), data); | ||
} | ||
function createList() { | ||
return doc_2.createList(makeId()); | ||
} | ||
function fetchStorage(initialStorageFactory) { | ||
state.initialStorageFactory = initialStorageFactory; | ||
state.storageState = types_1.LiveStorageState.Loading; | ||
state.flushData.messages.push({ type: live_1.ClientMessageType.FetchStorage }); | ||
tryFlushing(); | ||
} | ||
function updateRecord(record, overrides) { | ||
updateDoc(state.doc.updateRecord(record.id, overrides)); | ||
} | ||
function pushItem(list, item) { | ||
updateDoc(state.doc.pushItem(list.id, item)); | ||
} | ||
function deleteItem(list, index) { | ||
updateDoc(state.doc.deleteItem(list.id, index)); | ||
} | ||
function deleteItemById(list, itemId) { | ||
updateDoc(state.doc.deleteItemById(list.id, itemId)); | ||
} | ||
function moveItem(list, index, targetIndex) { | ||
updateDoc(state.doc.moveItem(list.id, index, targetIndex)); | ||
} | ||
return { | ||
@@ -586,11 +526,3 @@ // Internal | ||
broadcastEvent, | ||
// Storage | ||
fetchStorage, | ||
createRecord, | ||
updateRecord, | ||
createList, | ||
pushItem, | ||
deleteItem, | ||
deleteItemById, | ||
moveItem, | ||
getStorage, | ||
selectors: { | ||
@@ -603,4 +535,2 @@ // Core | ||
getOthers, | ||
// Storage | ||
getStorage, | ||
}, | ||
@@ -610,3 +540,3 @@ }; | ||
exports.makeStateMachine = makeStateMachine; | ||
function defaultState(me) { | ||
function defaultState(me, defaultStorageRoot) { | ||
return { | ||
@@ -616,3 +546,2 @@ connection: { state: "closed" }, | ||
listeners: { | ||
storage: [], | ||
event: [], | ||
@@ -642,5 +571,3 @@ others: [], | ||
others: makeOthers({}), | ||
storageState: types_1.LiveStorageState.NotInitialized, | ||
initialStorageFactory: null, | ||
doc: null, | ||
defaultStorageRoot, | ||
idFactory: null, | ||
@@ -652,5 +579,5 @@ }; | ||
const throttleDelay = options.throttle || 100; | ||
const liveblocksServer = options.liveblocksServer || "wss://liveblocks.net"; | ||
const liveblocksServer = options.liveblocksServer || "wss://liveblocks.net/v2"; | ||
const authEndpoint = options.authEndpoint; | ||
const state = defaultState(options.initialPresence); | ||
const state = defaultState(options.defaultPresence, options.defaultStorageRoot); | ||
const machine = makeStateMachine(state, { | ||
@@ -670,14 +597,2 @@ throttleDelay, | ||
unsubscribe: machine.unsubscribe, | ||
///////////// | ||
// Storage // | ||
///////////// | ||
getStorage: machine.selectors.getStorage, | ||
fetchStorage: machine.fetchStorage, | ||
createRecord: machine.createRecord, | ||
createList: machine.createList, | ||
updateRecord: machine.updateRecord, | ||
pushItem: machine.pushItem, | ||
deleteItem: machine.deleteItem, | ||
deleteItemById: machine.deleteItemById, | ||
moveItem: machine.moveItem, | ||
////////////// | ||
@@ -690,2 +605,3 @@ // Presence // | ||
broadcastEvent: machine.broadcastEvent, | ||
getStorage: machine.getStorage, | ||
}; | ||
@@ -692,0 +608,0 @@ return { |
@@ -1,3 +0,2 @@ | ||
import { RecordData, Record, List } from "./doc"; | ||
export declare type StorageCallback<T extends RecordData = RecordData> = (storage: LiveStorage<T>) => void; | ||
import { LiveRecord, RecordData } from "./doc"; | ||
export declare type MyPresenceCallback<T extends Presence = Presence> = (me: T) => void; | ||
@@ -12,3 +11,2 @@ export declare type OthersEventCallback<T extends Presence = Presence> = (others: Others<T>, event: OthersEvent<T>) => void; | ||
export declare type RoomEventCallbackMap = { | ||
storage: StorageCallback; | ||
"my-presence": MyPresenceCallback; | ||
@@ -20,8 +18,2 @@ others: OthersEventCallback; | ||
}; | ||
export declare type CreateRecord = Room["createRecord"]; | ||
export declare type CreateList = Room["createList"]; | ||
export declare type InitialStorageFactory<TRoot = RecordData> = (factories: { | ||
createRecord: CreateRecord; | ||
createList: CreateList; | ||
}) => TRoot; | ||
export declare type Client = { | ||
@@ -39,3 +31,6 @@ /** | ||
*/ | ||
enter(roomId: string, defaultPresence?: Presence): Room; | ||
enter<TStorageRoot = RecordData>(roomId: string, options?: { | ||
defaultPresence?: Presence; | ||
defaultStorageRoot?: TStorageRoot; | ||
}): Room; | ||
/** | ||
@@ -91,6 +86,4 @@ * Leaves a room. | ||
}; | ||
export declare type Presence = Serializable; | ||
export declare type SerializablePrimitive = boolean | string | number | null; | ||
export declare type Serializable = { | ||
[key: string]: SerializablePrimitive | Serializable | SerializablePrimitive[]; | ||
export declare type Presence = { | ||
[key: string]: any; | ||
}; | ||
@@ -112,13 +105,2 @@ declare type AuthEndpointCallback = (room: string) => Promise<{ | ||
}; | ||
export declare enum LiveStorageState { | ||
NotInitialized = 0, | ||
Loading = 1, | ||
Loaded = 2 | ||
} | ||
export declare type LiveStorage<T extends RecordData = RecordData> = { | ||
state: LiveStorageState.Loading | LiveStorageState.NotInitialized; | ||
} | { | ||
state: LiveStorageState.Loaded; | ||
root: Record<T>; | ||
}; | ||
declare type ConnectionState = "closed" | "authenticating" | "unavailable" | "failed" | "open" | "connecting"; | ||
@@ -154,8 +136,6 @@ export declare type Connection = { | ||
* | ||
* ### Example | ||
* ``` typescript | ||
* @example | ||
* room.subscribe("my-presence", (presence) => { | ||
* // Do something | ||
* }); | ||
* ``` | ||
*/ | ||
@@ -168,8 +148,6 @@ <T extends Presence>(type: "my-presence", listener: MyPresenceCallback<T>): void; | ||
* | ||
* ### Example | ||
* ``` typescript | ||
* @example | ||
* room.subscribe("others", (others) => { | ||
* // Do something | ||
* }); | ||
* ``` | ||
*/ | ||
@@ -182,11 +160,8 @@ <T extends Presence>(type: "others", listener: OthersEventCallback<T>): void; | ||
* | ||
* ### Example | ||
* ``` typescript | ||
* @example | ||
* room.subscribe("event", ({ event, connectionId }) => { | ||
* // Do something | ||
* }); | ||
* ``` | ||
*/ | ||
(type: "event", listener: EventCallback): void; | ||
<T extends RecordData>(type: "storage", listener: StorageCallback<T>): void; | ||
/** | ||
@@ -207,8 +182,6 @@ * Subscribe to errors thrown in the room. | ||
* | ||
* ### Example | ||
* ``` typescript | ||
* @example | ||
* const onPresenceChange = (presence) => { }; | ||
* room.subscribe("my-presence", onPresenceChange); | ||
* room.unsubscribe("my-presence", onPresenceChange); | ||
* ``` | ||
*/ | ||
@@ -221,8 +194,6 @@ <T extends Presence>(type: "my-presence", listener: MyPresenceCallback<T>): void; | ||
* | ||
* ### Example | ||
* ``` typescript | ||
* @example | ||
* const onOthersChange = (presence) => { }; | ||
* room.subscribe("others", onOthersChange); | ||
* room.unsubscribe("others", onOthersChange); | ||
* ``` | ||
*/ | ||
@@ -235,11 +206,8 @@ <T extends Presence>(type: "others", listener: OthersEventCallback<T>): void; | ||
* | ||
* ### Example | ||
* ``` typescript | ||
* @example | ||
* const onEvent = ({ event, connectionId }) => { }; | ||
* room.subscribe("event", onEvent); | ||
* room.unsubscribe("event", onEvent); | ||
* ``` | ||
*/ | ||
(type: "event", listener: EventCallback): void; | ||
<T extends RecordData>(type: "storage", listener: StorageCallback<T>): void; | ||
/** | ||
@@ -258,6 +226,4 @@ * Unsubscribe to errors thrown in the room. | ||
* | ||
* ### Example | ||
* ``` typescript | ||
* @example | ||
* const user = room.getSelf(); | ||
* ``` | ||
*/ | ||
@@ -268,6 +234,4 @@ getSelf<TPresence extends Presence = Presence>(): User<TPresence> | null; | ||
* | ||
* ### Example | ||
* ``` typescript | ||
* @example | ||
* const presence = room.getPresence(); | ||
* ``` | ||
*/ | ||
@@ -278,6 +242,4 @@ getPresence: <T extends Presence>() => T; | ||
* | ||
* ### Example | ||
* ``` typescript | ||
* @example | ||
* const others = room.getOthers(); | ||
* ``` | ||
*/ | ||
@@ -287,6 +249,5 @@ getOthers: <T extends Presence>() => Others<T>; | ||
* Updates the presence of the current user. Only pass the properties you want to update. No need to send the full presence. | ||
* @param {Partial<T>} overrides A partial object that contains the properties you want to update. | ||
* @param {Partial<T>} overrides - A partial object that contains the properties you want to update. | ||
* | ||
* ### Example | ||
* ``` typescript | ||
* @example | ||
* room.updatePresence({ x: 0 }); | ||
@@ -297,3 +258,2 @@ * room.updatePresence({ y: 0 }); | ||
* // presence is equivalent to { x: 0, y: 0 } | ||
* ``` | ||
*/ | ||
@@ -303,7 +263,5 @@ updatePresence: <T extends Presence>(overrides: Partial<T>) => void; | ||
* Broadcast an event to other users in the room. Event broadcasted to the room can be listened with {@link Room.subscribe}("event"). | ||
* @param {any} event the event to broadcast. Should be serializable to JSON | ||
* @param {any} event - the event to broadcast. Should be serializable to JSON | ||
* | ||
* ### Example | ||
* ``` typescript | ||
* | ||
* @example | ||
* // On client A | ||
@@ -318,15 +276,8 @@ * room.broadcastEvent({ type: "EMOJI", emoji: "🔥" }); | ||
* }); | ||
* ``` | ||
*/ | ||
broadcastEvent: (event: any) => void; | ||
getStorage: () => LiveStorage; | ||
fetchStorage(initialStorageFactory: InitialStorageFactory): void; | ||
createRecord: <T extends RecordData>(data: T) => Record<T>; | ||
createList: <T extends RecordData>() => List<Record<T>>; | ||
updateRecord<T extends RecordData>(record: Record<T>, overrides: Partial<T>): void; | ||
pushItem<T extends RecordData>(list: List<Record<T>>, item: Record<T>): void; | ||
deleteItem<T extends RecordData>(list: List<Record<T>>, index: number): void; | ||
deleteItemById<T extends RecordData>(list: List<Record<T>>, itemId: string): void; | ||
moveItem<T extends RecordData>(list: List<Record<T>>, index: number, targetIndex: number): void; | ||
getStorage: <TRoot>() => Promise<{ | ||
root: LiveRecord<TRoot>; | ||
}>; | ||
}; | ||
export {}; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.LiveStorageState = void 0; | ||
var LiveStorageState; | ||
(function (LiveStorageState) { | ||
LiveStorageState[LiveStorageState["NotInitialized"] = 0] = "NotInitialized"; | ||
LiveStorageState[LiveStorageState["Loading"] = 1] = "Loading"; | ||
LiveStorageState[LiveStorageState["Loaded"] = 2] = "Loaded"; | ||
})(LiveStorageState = exports.LiveStorageState || (exports.LiveStorageState = {})); |
@@ -5,4 +5,3 @@ import { ClientOptions, Client } from "./types"; | ||
* | ||
* ### Example | ||
* ``` | ||
* @example | ||
* const client = createClient({ | ||
@@ -28,4 +27,3 @@ * authEndpoint: "/api/auth" | ||
* }); | ||
* ``` | ||
*/ | ||
export declare function createClient(options: ClientOptions): Client; |
@@ -5,4 +5,3 @@ import { createRoom } from "./room"; | ||
* | ||
* ### Example | ||
* ``` | ||
* @example | ||
* const client = createClient({ | ||
@@ -28,7 +27,7 @@ * authEndpoint: "/api/auth" | ||
* }); | ||
* ``` | ||
*/ | ||
export function createClient(options) { | ||
if (typeof options.throttle === "number") { | ||
if (options.throttle < 80 || options.throttle > 1000) { | ||
const clientOptions = options; | ||
if (typeof clientOptions.throttle === "number") { | ||
if (clientOptions.throttle < 80 || clientOptions.throttle > 1000) { | ||
throw new Error("Liveblocks client throttle should be between 80 and 1000 ms"); | ||
@@ -42,3 +41,3 @@ } | ||
} | ||
function enter(roomId, initialPresence) { | ||
function enter(roomId, options = {}) { | ||
let internalRoom = rooms.get(roomId); | ||
@@ -48,3 +47,3 @@ if (internalRoom) { | ||
} | ||
internalRoom = createRoom(roomId, Object.assign(Object.assign({}, options), { initialPresence })); | ||
internalRoom = createRoom(roomId, Object.assign(Object.assign({}, clientOptions), options)); | ||
rooms.set(roomId, internalRoom); | ||
@@ -51,0 +50,0 @@ internalRoom.connect(); |
@@ -1,51 +0,97 @@ | ||
import { Op, SerializedRecord } from "./live"; | ||
import { Serializable, SerializablePrimitive } from "./types"; | ||
declare type Link = { | ||
parentId: string; | ||
parentKey: string; | ||
}; | ||
declare type Links = Map<string, Link>; | ||
declare type ListCache = Map<string, Map<string, Crdt>>; | ||
declare type Cache = { | ||
links: Links; | ||
listCache: ListCache; | ||
}; | ||
declare type Crdt = Record | List<any>; | ||
declare const RECORD: unique symbol; | ||
declare const LIST: unique symbol; | ||
export declare function createRecord<T extends RecordData>(id: string, data: T): Record<T>; | ||
export declare function createList<T>(id: string, items?: T[]): List<T>; | ||
export declare type RecordData = { | ||
[key: string]: RecordValue; | ||
}; | ||
declare type RecordValue = SerializablePrimitive | Array<SerializablePrimitive> | Serializable | Record<any> | List<any>; | ||
export declare type Record<T extends RecordData = RecordData> = { | ||
readonly id: string; | ||
readonly $$type: typeof RECORD; | ||
} & T; | ||
export declare type List<T> = { | ||
readonly id: string; | ||
readonly $$type: typeof LIST; | ||
toArray(): Array<T>; | ||
map<U>(callback: (value: T, index: number) => U): U[]; | ||
readonly length: number; | ||
}; | ||
declare type Emit = (op: Op) => void; | ||
export declare class Doc<T extends RecordData> { | ||
root: Record<T>; | ||
private _cache; | ||
private _emit; | ||
constructor(root: Record<T>, _cache: Cache, _emit: Emit); | ||
static empty<T extends RecordData>(id?: string, emit?: Emit): Doc<T>; | ||
static createFromRoot<T extends RecordData>(data: T, id?: string, emit?: Emit): Doc<T>; | ||
static load<T extends RecordData>(root: SerializedRecord, emit?: Emit): Doc<T>; | ||
get data(): Record<T>; | ||
dispatch(op: Op, shouldEmit?: boolean): Doc<T>; | ||
private getChild; | ||
updateRecord<TRecord>(id: string, overrides: Partial<TRecord>): Doc<T>; | ||
pushItem<TItem>(id: string, item: TItem): Doc<T>; | ||
moveItem(id: string, index: number, targetIndex: number): Doc<T>; | ||
deleteItem(id: string, index: number): Doc<T>; | ||
deleteItemById(id: string, itemId: string): Doc<T>; | ||
import { Op, SerializedCrdtWithId, SerializedList } from "./live"; | ||
declare const INTERNAL: unique symbol; | ||
declare type Dispatch = (ops: Op[]) => void; | ||
declare type Crdt = LiveRecord | LiveList; | ||
export declare type RecordData = Record<string, any>; | ||
export declare class Doc<T extends RecordData = RecordData> { | ||
private _root; | ||
private actor; | ||
private _dispatch; | ||
private _clock; | ||
private _items; | ||
private constructor(); | ||
static from<T>(root: T, actor?: number, dispatch?: Dispatch): Doc<T>; | ||
static load<T>(items: SerializedCrdtWithId[], actor: number, dispatch?: Dispatch): Doc<T>; | ||
dispatch(ops: Op[]): void; | ||
addItem(id: string, item: Crdt): void; | ||
deleteItem(id: string): void; | ||
apply(op: Op): void; | ||
private applyDeleteRecordKey; | ||
private applyUpdateRecord; | ||
private applyCreateRecord; | ||
private applyDeleteRecord; | ||
private applySetParentKey; | ||
get root(): LiveRecord<T>; | ||
count(): number; | ||
generateId(): string; | ||
} | ||
export declare class LiveRecord<T extends RecordData = RecordData> { | ||
private _map; | ||
private _listeners; | ||
private _ctx?; | ||
constructor(object?: T); | ||
static deserialize([id, item]: SerializedCrdtWithId, parentToChildren: Map<string, SerializedCrdtWithId[]>, doc: Doc): LiveRecord<{ | ||
[key: string]: any; | ||
}>; | ||
get [INTERNAL](): { | ||
ctx: { | ||
id: string; | ||
doc: Doc<Record<string, any>>; | ||
parentId?: string | undefined; | ||
} | undefined; | ||
attachChild: (key: keyof T, child: Crdt) => void; | ||
detachChild: (child: Crdt) => void; | ||
detach: () => void; | ||
attach: (id: string, doc: Doc<Record<string, any>>, parentId?: string | undefined, parentKey?: string | undefined) => Op[]; | ||
apply: (op: Op) => void; | ||
}; | ||
private attach; | ||
private attachChild; | ||
private detachChild; | ||
private detach; | ||
private apply; | ||
private notify; | ||
toObject(): T; | ||
set<TKey extends keyof T>(key: TKey, value: T[TKey]): void; | ||
get<TKey extends keyof T>(key: TKey): T[TKey]; | ||
delete<TKey extends keyof T>(key: TKey): void; | ||
update(overrides: Partial<T>): void; | ||
subscribe(listener: () => void): void; | ||
unsubscribe(listener: () => void): void; | ||
} | ||
export declare class LiveList<T extends LiveRecord = LiveRecord> { | ||
private _listeners; | ||
private _ctx?; | ||
private _items; | ||
constructor(items?: T[]); | ||
static deserialize([id, item]: [id: string, item: SerializedList], parentToChildren: Map<string, SerializedCrdtWithId[]>, doc: Doc): LiveList<never>; | ||
get [INTERNAL](): { | ||
ctx: { | ||
id: string; | ||
parentId: string; | ||
doc: Doc<Record<string, any>>; | ||
} | undefined; | ||
attachChild: (key: string, child: LiveRecord<Record<string, any>>) => void; | ||
detachChild: (child: Crdt) => void; | ||
attach: (id: string, doc: Doc<Record<string, any>>, parentId: string, parentKey: string) => Op[]; | ||
detach: () => void; | ||
apply: (op: Op) => void; | ||
setChildKey: (key: string, child: Crdt) => void; | ||
}; | ||
private attach; | ||
private detach; | ||
private attachChild; | ||
private detachChild; | ||
private setChildKey; | ||
private apply; | ||
private notify; | ||
push(item: T): void; | ||
insert(item: T, index: number): void; | ||
move(index: number, targetIndex: number): void; | ||
delete(index: number): void; | ||
toArray(): T[]; | ||
get(index: number): T; | ||
subscribe(listener: () => void): void; | ||
unsubscribe(listener: () => void): void; | ||
} | ||
export {}; |
@@ -1,451 +0,531 @@ | ||
import { OpType, CrdtType, } from "./live"; | ||
import { remove } from "./utils"; | ||
import { CrdtType, OpType, } from "./live"; | ||
import { compare, makePosition } from "./position"; | ||
const RECORD = Symbol("liveblocks.record"); | ||
const LIST = Symbol("liveblocks.list"); | ||
export function createRecord(id, data) { | ||
return Object.assign({ id, $$type: RECORD }, data); | ||
} | ||
export function createList(id, items = []) { | ||
return { | ||
id, | ||
$$type: LIST, | ||
length: items.length, | ||
toArray: () => items, | ||
map: (callback) => items.map(callback), | ||
}; | ||
} | ||
function noop() { } | ||
const INTERNAL = Symbol("liveblocks.internal"); | ||
function noOp() { } | ||
export class Doc { | ||
constructor(root, _cache, _emit) { | ||
this.root = root; | ||
this._cache = _cache; | ||
this._emit = _emit; | ||
constructor(_root, actor = 0, _dispatch = noOp) { | ||
this._root = _root; | ||
this.actor = actor; | ||
this._dispatch = _dispatch; | ||
this._clock = 0; | ||
this._items = new Map(); | ||
} | ||
static empty(id = "root", emit = noop) { | ||
const root = { | ||
id, | ||
$$type: RECORD, | ||
}; | ||
return new Doc(root, { links: new Map(), listCache: new Map() }, emit); | ||
static from(root, actor = 0, dispatch = noOp) { | ||
const rootRecord = new LiveRecord(root); | ||
const storage = new Doc(rootRecord, actor, dispatch); | ||
const ops = rootRecord[INTERNAL].attach(storage.generateId(), storage); | ||
storage.dispatch(ops); | ||
return storage; | ||
} | ||
static createFromRoot(data, id = "root", emit = noop) { | ||
let doc = Doc.empty(id, emit); | ||
doc = doc.updateRecord(doc.root.id, data); | ||
static load(items, actor, dispatch = noOp) { | ||
if (items.length === 0) { | ||
throw new Error("Internal error: cannot load storage without items"); | ||
} | ||
const parentToChildren = new Map(); | ||
let root = null; | ||
for (const tuple of items) { | ||
const parentId = tuple[1].parentId; | ||
if (parentId == null) { | ||
root = tuple; | ||
} | ||
else { | ||
const children = parentToChildren.get(parentId); | ||
if (children != null) { | ||
children.push(tuple); | ||
} | ||
else { | ||
parentToChildren.set(parentId, [tuple]); | ||
} | ||
} | ||
} | ||
if (root == null) { | ||
throw new Error("Root can't be null"); | ||
} | ||
const doc = new Doc(null, actor, dispatch); | ||
doc._root = LiveRecord.deserialize(root, parentToChildren, doc); | ||
return doc; | ||
} | ||
static load(root, emit = noop) { | ||
let doc = Doc.empty(root.id, emit); | ||
return doc.dispatch({ | ||
type: OpType.RecordUpdate, | ||
id: root.id, | ||
data: root.data, | ||
}); | ||
dispatch(ops) { | ||
this._dispatch(ops); | ||
} | ||
get data() { | ||
return this.root; | ||
addItem(id, item) { | ||
this._items.set(id, item); | ||
} | ||
dispatch(op, shouldEmit = false) { | ||
if (shouldEmit) { | ||
this._emit(op); | ||
deleteItem(id) { | ||
this._items.delete(id); | ||
} | ||
apply(op) { | ||
switch (op.type) { | ||
case OpType.UpdateRecord: { | ||
this.applyUpdateRecord(op); | ||
break; | ||
} | ||
case OpType.CreateRecord: { | ||
this.applyCreateRecord(op); | ||
break; | ||
} | ||
case OpType.DeleteRecord: { | ||
this.applyDeleteRecord(op); | ||
break; | ||
} | ||
case OpType.SetParentKey: { | ||
this.applySetParentKey(op); | ||
break; | ||
} | ||
case OpType.DeleteRecordKey: { | ||
this.applyDeleteRecordKey(op); | ||
break; | ||
} | ||
} | ||
if (op.id === this.root.id) { | ||
const node = dispatch(this.root, op, this._cache, []); | ||
return new Doc(node, this._cache, this._emit); | ||
} | ||
applyDeleteRecordKey(op) { | ||
const item = this._items.get(op.id); | ||
if (item) { | ||
item[INTERNAL].apply(op); | ||
} | ||
else { | ||
const links = getAllLinks(op.id, this.root.id, this._cache.links); | ||
const node = dispatch(this.root, op, this._cache, links); | ||
return new Doc(node, this._cache, this._emit); | ||
} | ||
applyUpdateRecord(op) { | ||
const item = this._items.get(op.id); | ||
if (item) { | ||
item[INTERNAL].apply(op); | ||
} | ||
} | ||
getChild(id) { | ||
if (id === this.root.id) { | ||
return this.root; | ||
applyCreateRecord(op) { | ||
const newRecord = new LiveRecord(op.data); | ||
newRecord[INTERNAL].attach(op.id, this, op.parentId, op.parentKey); | ||
if (op.parentId && op.parentKey) { | ||
const parent = this._items.get(op.parentId); | ||
if (parent == null) { | ||
throw new Error("Parent is missing"); | ||
} | ||
parent[INTERNAL].attachChild(op.parentKey, newRecord); | ||
} | ||
const allLinks = getAllLinks(id, this.root.id, this._cache.links); | ||
return getChildDeep(this.root, id, allLinks, this._cache); | ||
} | ||
updateRecord(id, overrides) { | ||
const currentRecord = this.getChild(id); | ||
if (currentRecord == null) { | ||
throw new Error(`Record with id "${id}" does not exist`); | ||
applyDeleteRecord(op) { | ||
const item = this._items.get(op.id); | ||
if (item == null) { | ||
return; | ||
} | ||
let data = {}; | ||
for (const key in overrides) { | ||
const value = overrides[key]; | ||
data[key] = serialize(value); | ||
const parentId = item[INTERNAL].ctx.parentId; | ||
if (parentId == null) { | ||
return; | ||
} | ||
const op = { | ||
id: currentRecord.id, | ||
type: OpType.RecordUpdate, | ||
data, | ||
}; | ||
return this.dispatch(op, true); | ||
const parent = this._items.get(parentId); | ||
if (parent) { | ||
parent[INTERNAL].detachChild(item); | ||
} | ||
} | ||
pushItem(id, item) { | ||
const list = this.getChild(id); | ||
if (list == null) { | ||
throw new Error(`List with id "${id}" does not exist`); | ||
applySetParentKey(op) { | ||
const item = this._items.get(op.id); | ||
if (item == null) { | ||
return; | ||
} | ||
if (list.$$type !== LIST) { | ||
throw new Error(`Node with id "${id}" is not a list`); | ||
const parentId = item[INTERNAL].ctx.parentId; | ||
if (parentId == null) { | ||
return; | ||
} | ||
if (!isRecord(item)) { | ||
throw new Error("List can't only have Record as children"); | ||
const parent = this._items.get(parentId); | ||
if (parent && parent instanceof LiveList) { | ||
parent[INTERNAL].setChildKey(op.parentKey, item); | ||
} | ||
const data = serialize(item); | ||
if (list.length === 0) { | ||
return this.dispatch({ | ||
type: OpType.ListInsert, | ||
id: list.id, | ||
position: makePosition(), | ||
data, | ||
}, true); | ||
} | ||
const items = sortedListItems(getListItems(this._cache, id)); | ||
const [tailPosition] = items[items.length - 1]; | ||
const position = makePosition(tailPosition); | ||
const operation = { | ||
type: OpType.ListInsert, | ||
id: list.id, | ||
position, | ||
data, | ||
}; | ||
return this.dispatch(operation, true); | ||
} | ||
moveItem(id, index, targetIndex) { | ||
const list = this.getChild(id); | ||
if (list == null) { | ||
throw new Error(`List with id "${id}" does not exist`); | ||
get root() { | ||
return this._root; | ||
} | ||
count() { | ||
return this._items.size; | ||
} | ||
generateId() { | ||
return `${this.actor}:${this._clock++}`; | ||
} | ||
} | ||
export class LiveRecord { | ||
constructor(object = {}) { | ||
this._listeners = []; | ||
this._map = new Map(Object.entries(object)); | ||
} | ||
static deserialize([id, item], parentToChildren, doc) { | ||
if (item.type !== CrdtType.Record) { | ||
throw new Error(`Tried to deserialize a record but item type is "${item.type}"`); | ||
} | ||
if (list.$$type !== LIST) { | ||
throw new Error(`Node with id "${id}" is not a list`); | ||
const record = new LiveRecord(item.data); | ||
record.attach(id, doc, item.parentId, item.parentKey); | ||
const children = parentToChildren.get(id); | ||
if (children == null) { | ||
return record; | ||
} | ||
const items = sortedListItems(getListItems(this._cache, id)); | ||
if (targetIndex < 0) { | ||
throw new Error("targetIndex cannot be less than 0"); | ||
for (const entry of children) { | ||
const crdt = entry[1]; | ||
if (crdt.parentKey == null) { | ||
throw new Error("Tried to deserialize a crdt but it does not have a parentKey and is not the root"); | ||
} | ||
const child = deserialize(entry, parentToChildren, doc); | ||
record._map.set(crdt.parentKey, child); | ||
} | ||
if (targetIndex >= items.length) { | ||
throw new Error("targetIndex cannot be greater or equal than the list length"); | ||
return record; | ||
} | ||
get [INTERNAL]() { | ||
return { | ||
ctx: this._ctx, | ||
attachChild: this.attachChild.bind(this), | ||
detachChild: this.detachChild.bind(this), | ||
detach: this.detach.bind(this), | ||
attach: this.attach.bind(this), | ||
apply: this.apply.bind(this), | ||
}; | ||
} | ||
attach(id, doc, parentId, parentKey) { | ||
if (this._ctx) { | ||
throw new Error("LiveRecord is already part of the storage!"); | ||
} | ||
if (index < 0) { | ||
throw new Error("index cannot be less than 0"); | ||
doc.addItem(id, this); | ||
this._ctx = { | ||
id, | ||
doc: doc, | ||
parentId, | ||
}; | ||
const ops = []; | ||
const createOp = { | ||
id: this._ctx.id, | ||
type: OpType.CreateRecord, | ||
parentId, | ||
parentKey, | ||
data: {}, | ||
}; | ||
ops.push(createOp); | ||
for (const [key, value] of this._map) { | ||
if (value instanceof LiveRecord) { | ||
ops.push(...value.attach(doc.generateId(), doc, this._ctx.id, key)); | ||
} | ||
else if (value instanceof LiveList) { | ||
ops.push(...value[INTERNAL].attach(doc.generateId(), doc, this._ctx.id, key)); | ||
} | ||
else { | ||
createOp.data[key] = value; | ||
} | ||
} | ||
if (index >= items.length) { | ||
throw new Error("index cannot be greater or equal than the list length"); | ||
return ops; | ||
} | ||
attachChild(key, child) { | ||
this._map.set(key, child); | ||
this.notify(); | ||
} | ||
detachChild(child) { | ||
for (const [key, value] of this._map) { | ||
if (value === child) { | ||
this._map.delete(key); | ||
} | ||
} | ||
if (index === targetIndex) { | ||
return this; | ||
if (child instanceof LiveRecord) { | ||
child.detach(); | ||
} | ||
let beforePosition = null; | ||
let afterPosition = null; | ||
if (index < targetIndex) { | ||
afterPosition = | ||
targetIndex === items.length - 1 | ||
? undefined | ||
: items[targetIndex + 1][0]; | ||
beforePosition = items[targetIndex][0]; | ||
this.notify(); | ||
} | ||
detach() { | ||
if (this._ctx == null) { | ||
return; | ||
} | ||
else { | ||
afterPosition = items[targetIndex][0]; | ||
beforePosition = | ||
targetIndex === 0 ? undefined : items[targetIndex - 1][0]; | ||
this._ctx.doc.deleteItem(this._ctx.id); | ||
for (const [, value] of this._map) { | ||
if (value instanceof LiveRecord) { | ||
value.detach(); | ||
} | ||
} | ||
const position = makePosition(beforePosition, afterPosition); | ||
const [, item] = items[index]; | ||
return this.dispatch({ | ||
type: OpType.ListMove, | ||
id: list.id, | ||
itemId: item.id, | ||
position, | ||
}, true); | ||
} | ||
deleteItem(id, index) { | ||
const list = this.getChild(id); | ||
if (list == null) { | ||
throw new Error(`List with id "${id}" does not exist`); | ||
apply(op) { | ||
if (op.type === OpType.UpdateRecord) { | ||
for (const key in op.data) { | ||
const oldValue = this._map.get(key); | ||
if (oldValue instanceof LiveRecord) { | ||
oldValue.detach(); | ||
} | ||
const value = op.data[key]; | ||
this._map.set(key, value); | ||
} | ||
this.notify(); | ||
} | ||
if (list.$$type !== LIST) { | ||
throw new Error(`Node with id "${id}" is not a list`); | ||
else if (op.type === OpType.DeleteRecordKey) { | ||
const key = op.key; | ||
const oldValue = this._map.get(key); | ||
if (oldValue instanceof LiveRecord) { | ||
oldValue.detach(); | ||
} | ||
this._map.delete(key); | ||
this.notify(); | ||
} | ||
const items = sortedListItems(getListItems(this._cache, id)); | ||
const [, item] = items[index]; | ||
return this.dispatch({ | ||
type: OpType.ListRemove, | ||
id: list.id, | ||
itemId: item.id, | ||
}, true); | ||
} | ||
deleteItemById(id, itemId) { | ||
const list = this.getChild(id); | ||
if (list == null) { | ||
throw new Error(`List with id "${id}" does not exist`); | ||
notify() { | ||
for (const listener of this._listeners) { | ||
listener(); | ||
} | ||
if (list.$$type !== LIST) { | ||
throw new Error(`Node with id "${id}" is not a list`); | ||
} | ||
const itemsMap = getListItems(this._cache, id); | ||
let item = null; | ||
for (const [, crdt] of itemsMap) { | ||
if (crdt.id === itemId) { | ||
item = crdt; | ||
break; | ||
} | ||
toObject() { | ||
return Object.fromEntries(this._map); | ||
} | ||
set(key, value) { | ||
// TODO: Find out why typescript complains | ||
this.update({ [key]: value }); | ||
} | ||
get(key) { | ||
return this._map.get(key); | ||
} | ||
delete(key) { | ||
if (this._ctx) { | ||
const ops = []; | ||
const item = this._map.get(key); | ||
if (item instanceof LiveRecord) { | ||
item.detach(); | ||
} | ||
this._ctx.doc.dispatch([ | ||
{ type: OpType.DeleteRecordKey, id: this._ctx.id, key: key }, | ||
]); | ||
} | ||
if (item == null) { | ||
throw new Error(`List with id "${id}" does not have an item with id "${itemId}"`); | ||
} | ||
return this.dispatch({ | ||
type: OpType.ListRemove, | ||
id: list.id, | ||
itemId: item.id, | ||
}, true); | ||
this._map.delete(key); | ||
this.notify(); | ||
} | ||
} | ||
function getAllLinks(id, rootId, links) { | ||
let currentId = id; | ||
const result = []; | ||
do { | ||
const link = links.get(currentId); | ||
if (link == null) { | ||
throw new Error(`Can't find link for id "${currentId}"`); | ||
update(overrides) { | ||
if (this._ctx) { | ||
const ops = []; | ||
const updateOperation = { | ||
id: this._ctx.id, | ||
type: OpType.UpdateRecord, | ||
data: {}, | ||
}; | ||
ops.push(updateOperation); | ||
for (const key in overrides) { | ||
const oldValue = this._map.get(key); | ||
if (oldValue instanceof LiveRecord) { | ||
oldValue.detach(); | ||
} | ||
const value = overrides[key]; | ||
if (value instanceof LiveRecord) { | ||
ops.push(...value.attach(this._ctx.doc.generateId(), this._ctx.doc, this._ctx.id, key)); | ||
} | ||
else { | ||
updateOperation.data[key] = value; | ||
} | ||
this._map.set(key, value); | ||
} | ||
this._ctx.doc.dispatch(ops); | ||
this.notify(); | ||
} | ||
currentId = link.parentId; | ||
result.push(link); | ||
} while (currentId !== rootId); | ||
return result; | ||
} | ||
function deserializeList(serialized, cache) { | ||
const listItems = new Map(); | ||
for (const position in serialized.data) { | ||
const item = deserialize(serialized.data[position], cache); | ||
if (!isRecord(item)) { | ||
throw new Error("TODO"); | ||
else { | ||
for (const key in overrides) { | ||
const value = overrides[key]; | ||
this._map.set(key, value); | ||
} | ||
this.notify(); | ||
} | ||
listItems.set(position, item); | ||
cache.links.set(item.id, { parentId: serialized.id, parentKey: position }); | ||
} | ||
cache.listCache.set(serialized.id, listItems); | ||
return createList(serialized.id, listItemsToArray(listItems)); | ||
} | ||
function getListItems(cache, listId) { | ||
const items = cache.listCache.get(listId); | ||
if (items == null) { | ||
throw new Error(`Can't find list cache for id "${listId}"`); | ||
subscribe(listener) { | ||
this._listeners.push(listener); | ||
} | ||
return items; | ||
unsubscribe(listener) { | ||
remove(this._listeners, listener); | ||
} | ||
} | ||
function deserializeRecord(serialized, cache) { | ||
const result = { | ||
id: serialized.id, | ||
$$type: RECORD, | ||
}; | ||
for (const key in serialized.data) { | ||
const item = deserialize(serialized.data[key], cache); | ||
if (isCrdt(item)) { | ||
cache.links.set(item.id, { | ||
parentId: serialized.id, | ||
parentKey: key, | ||
}); | ||
export class LiveList { | ||
constructor(items = []) { | ||
this._listeners = []; | ||
// TODO: Find a better data structure | ||
this._items = []; | ||
let position = undefined; | ||
for (let i = 0; i < items.length; i++) { | ||
const newPosition = makePosition(position); | ||
this._items.push([items[i], newPosition]); | ||
position = newPosition; | ||
} | ||
result[key] = item; | ||
} | ||
return result; | ||
} | ||
function deserialize(serialized, cache) { | ||
switch (serialized.type) { | ||
case CrdtType.Register: { | ||
return serialized.data; | ||
static deserialize([id, item], parentToChildren, doc) { | ||
const list = new LiveList([]); | ||
list.attach(id, doc, item.parentId, item.parentKey); | ||
const children = parentToChildren.get(id); | ||
if (children == null) { | ||
return list; | ||
} | ||
case CrdtType.Record: { | ||
return deserializeRecord(serialized, cache); | ||
for (const entry of children) { | ||
const child = LiveRecord.deserialize(entry, parentToChildren, doc); | ||
list.attachChild(entry[1].parentKey, child); | ||
} | ||
case CrdtType.List: { | ||
return deserializeList(serialized, cache); | ||
} | ||
default: { | ||
throw new Error("TODO"); | ||
} | ||
return list; | ||
} | ||
} | ||
function dispatchOnRecord(record, op, cache, links) { | ||
if (links.length === 0) { | ||
if (record.id !== op.id) { | ||
throw new Error("TODO"); | ||
get [INTERNAL]() { | ||
return { | ||
ctx: this._ctx, | ||
attachChild: this.attachChild.bind(this), | ||
detachChild: this.detachChild.bind(this), | ||
attach: this.attach.bind(this), | ||
detach: this.detach.bind(this), | ||
apply: this.apply.bind(this), | ||
setChildKey: this.setChildKey.bind(this), | ||
}; | ||
} | ||
attach(id, doc, parentId, parentKey) { | ||
if (this._ctx) { | ||
throw new Error("LiveList is already part of the storage!"); | ||
} | ||
switch (op.type) { | ||
case OpType.RecordUpdate: { | ||
return updateRecord(record, op, cache); | ||
} | ||
default: { | ||
console.warn("Unsupported operation"); | ||
return record; | ||
} | ||
doc.addItem(id, this); | ||
this._ctx = { | ||
doc: doc, | ||
id: id, | ||
parentId: parentId, | ||
}; | ||
const ops = []; | ||
const createOp = { | ||
id: this._ctx.id, | ||
type: OpType.CreateList, | ||
parentId, | ||
parentKey, | ||
}; | ||
ops.push(createOp); | ||
for (const [item, position] of this._items) { | ||
ops.push(...item[INTERNAL].attach(doc.generateId(), doc, this._ctx.id, position)); | ||
} | ||
return ops; | ||
} | ||
const currentLink = links.pop(); | ||
const child = record[currentLink.parentKey]; | ||
const newNode = dispatch(child, op, cache, links); | ||
return Object.assign(Object.assign({}, record), { [currentLink.parentKey]: newNode }); | ||
} | ||
function dispatchOnList(list, op, cache, links) { | ||
if (links.length === 0) { | ||
if (list.id !== op.id) { | ||
throw new Error("TODO"); | ||
detach() { | ||
if (this._ctx == null) { | ||
return; | ||
} | ||
switch (op.type) { | ||
case OpType.ListInsert: { | ||
return listInsert(list, op, cache); | ||
} | ||
case OpType.ListMove: { | ||
return listMove(list, op, cache); | ||
} | ||
case OpType.ListRemove: { | ||
return listDelete(list, op, cache); | ||
} | ||
default: { | ||
console.warn("Unsupported operation"); | ||
return list; | ||
} | ||
} | ||
this._ctx.doc.deleteItem(this._ctx.id); | ||
} | ||
const currentLink = links.pop(); | ||
const position = currentLink.parentKey; | ||
const items = getListItems(cache, list.id); | ||
const item = items.get(position); | ||
if (item == null) { | ||
throw new Error("TODO"); | ||
attachChild(key, child) { | ||
this._items.push([child, key]); | ||
this._items.sort((itemA, itemB) => compare({ position: itemA[1] }, { position: itemB[1] })); | ||
this.notify(); | ||
} | ||
const newItem = dispatch(item, op, cache, links); | ||
items.set(position, newItem); | ||
return createList(list.id, listItemsToArray(items)); | ||
} | ||
function dispatch(node, op, cache, links) { | ||
switch (node.$$type) { | ||
case RECORD: | ||
return dispatchOnRecord(node, op, cache, links); | ||
case LIST: | ||
return dispatchOnList(node, op, cache, links); | ||
default: { | ||
throw new Error("Unknown CRDT"); | ||
detachChild(child) { | ||
const indexToDelete = this._items.findIndex((item) => item[0] === child); | ||
this._items.splice(indexToDelete); | ||
if (child instanceof LiveRecord) { | ||
child[INTERNAL].detach(); | ||
} | ||
this.notify(); | ||
} | ||
} | ||
function updateRecord(node, op, cache) { | ||
const result = Object.assign({}, node); | ||
for (const key in op.data) { | ||
const value = op.data[key]; | ||
const item = deserialize(value, cache); | ||
if (isCrdt(item)) { | ||
cache.links.set(item.id, { parentId: node.id, parentKey: key }); | ||
setChildKey(key, child) { | ||
const item = this._items.find((item) => item[0] === child); | ||
if (item) { | ||
item[1] = key; | ||
} | ||
result[key] = item; | ||
this._items.sort((itemA, itemB) => compare({ position: itemA[1] }, { position: itemB[1] })); | ||
this.notify(); | ||
} | ||
return result; | ||
} | ||
function listInsert(list, op, cache) { | ||
const items = getListItems(cache, list.id); | ||
const item = deserialize(op.data, cache); | ||
if (isCrdt(item)) { | ||
items.set(op.position, item); | ||
cache.links.set(item.id, { parentId: list.id, parentKey: op.position }); | ||
apply(op) { } | ||
notify() { | ||
for (const listener of this._listeners) { | ||
listener(); | ||
} | ||
} | ||
return createList(list.id, listItemsToArray(items)); | ||
} | ||
function listMove(list, op, cache) { | ||
const items = getListItems(cache, list.id); | ||
const link = getLinkOrThrow(cache, op.itemId); | ||
const item = items.get(link.parentKey); | ||
if (item == null) { | ||
throw new Error("TODO"); | ||
push(item) { | ||
const position = this._items.length === 0 | ||
? makePosition() | ||
: makePosition(this._items[this._items.length - 1][1]); | ||
this._items.push([item, position]); | ||
this.notify(); | ||
if (this._ctx) { | ||
const ops = item[INTERNAL].attach(this._ctx.doc.generateId(), this._ctx.doc, this._ctx.id, position); | ||
this._ctx.doc.dispatch(ops); | ||
} | ||
} | ||
// Delete old position cache entry | ||
items.delete(link.parentKey); | ||
// Insert new position in cache | ||
items.set(op.position, item); | ||
// Update link | ||
cache.links.set(op.itemId, { parentId: list.id, parentKey: op.position }); | ||
return createList(list.id, listItemsToArray(items)); | ||
} | ||
function getLinkOrThrow(cache, id) { | ||
const link = cache.links.get(id); | ||
if (link == null) { | ||
throw new Error(`Can't find link with id "${id}"`); | ||
insert(item, index) { | ||
if (index < 0 || index > this._items.length) { | ||
throw new Error(`Cannot delete list item at index "${index}". index should be between 0 and ${this._items.length}`); | ||
} | ||
let before = this._items[index - 1] ? this._items[index - 1][1] : undefined; | ||
let after = this._items[index] ? this._items[index][1] : undefined; | ||
const position = makePosition(before, after); | ||
this._items.push([item, position]); | ||
this._items.sort((itemA, itemB) => compare({ position: itemA[1] }, { position: itemB[1] })); | ||
this.notify(); | ||
if (this._ctx) { | ||
const ops = item[INTERNAL].attach(this._ctx.doc.generateId(), this._ctx.doc, this._ctx.id, position); | ||
this._ctx.doc.dispatch(ops); | ||
} | ||
} | ||
return link; | ||
} | ||
function listDelete(list, op, cache) { | ||
const items = getListItems(cache, list.id); | ||
const link = getLinkOrThrow(cache, op.itemId); | ||
items.delete(link.parentKey); | ||
cache.links.delete(op.itemId); | ||
return createList(list.id, listItemsToArray(items)); | ||
} | ||
function listItemsToArray(items) { | ||
return sortedListItems(items).map((entry) => entry[1]); | ||
} | ||
function sortedListItems(items) { | ||
return Array.from(items.entries()).sort((entryA, entryB) => compare({ position: entryA[0] }, { position: entryB[0] })); | ||
} | ||
function getChildDeep(node, id, links, cache) { | ||
let currentNode = node; | ||
while (currentNode.id !== id) { | ||
const link = links.pop(); | ||
if (link == null || link.parentId !== currentNode.id) { | ||
throw new Error("TODO"); | ||
move(index, targetIndex) { | ||
if (targetIndex < 0) { | ||
throw new Error("targetIndex cannot be less than 0"); | ||
} | ||
if (currentNode.$$type === RECORD) { | ||
currentNode = currentNode[link.parentKey]; | ||
if (targetIndex >= this._items.length) { | ||
throw new Error("targetIndex cannot be greater or equal than the list length"); | ||
} | ||
if (index < 0) { | ||
throw new Error("index cannot be less than 0"); | ||
} | ||
if (index >= this._items.length) { | ||
throw new Error("index cannot be greater or equal than the list length"); | ||
} | ||
let beforePosition = null; | ||
let afterPosition = null; | ||
if (index < targetIndex) { | ||
afterPosition = | ||
targetIndex === this._items.length - 1 | ||
? undefined | ||
: this._items[targetIndex + 1][1]; | ||
beforePosition = this._items[targetIndex][1]; | ||
} | ||
else { | ||
const listItems = getListItems(cache, currentNode.id); | ||
const item = listItems.get(link.parentKey); | ||
if (item == null) { | ||
throw new Error("TODO"); | ||
} | ||
currentNode = item; | ||
afterPosition = this._items[targetIndex][1]; | ||
beforePosition = | ||
targetIndex === 0 ? undefined : this._items[targetIndex - 1][1]; | ||
} | ||
const position = makePosition(beforePosition, afterPosition); | ||
const item = this._items[index]; | ||
item[1] = position; | ||
this._items.sort((itemA, itemB) => compare({ position: itemA[1] }, { position: itemB[1] })); | ||
this.notify(); | ||
if (this._ctx) { | ||
this._ctx.doc.dispatch([ | ||
{ | ||
type: OpType.SetParentKey, | ||
id: item[0][INTERNAL].ctx.id, | ||
parentKey: position, | ||
}, | ||
]); | ||
} | ||
} | ||
return currentNode; | ||
} | ||
function isRecord(value) { | ||
return value != null && typeof value === "object" && value.$$type === RECORD; | ||
} | ||
function isList(value) { | ||
return value != null && typeof value === "object" && value.$$type === LIST; | ||
} | ||
function isCrdt(value) { | ||
return isRecord(value) || isList(value); | ||
} | ||
function serializeRecord(record) { | ||
const serializedData = {}; | ||
for (const key in record) { | ||
if (key !== "id" && key !== "$$type") { | ||
const value = record[key]; // TODO: Find out why typescript does not like that | ||
serializedData[key] = serialize(value); | ||
delete(index) { | ||
if (index < 0 || index >= this._items.length) { | ||
throw new Error(`Cannot delete list item at index "${index}". index should be between 0 and ${this._items.length - 1}`); | ||
} | ||
const item = this._items[index]; | ||
this._items.splice(index, 1); | ||
if (this._ctx) { | ||
const childRecord = item[0]; | ||
this._ctx.doc.dispatch([ | ||
{ | ||
id: childRecord[INTERNAL].ctx.id, | ||
type: OpType.DeleteRecord, | ||
}, | ||
]); | ||
childRecord[INTERNAL].detach(); | ||
} | ||
this.notify(); | ||
} | ||
return { | ||
id: record.id, | ||
type: CrdtType.Record, | ||
data: serializedData, | ||
}; | ||
} | ||
function serializeList(list) { | ||
return { | ||
id: list.id, | ||
type: CrdtType.List, | ||
data: {}, | ||
}; | ||
} | ||
function serialize(value) { | ||
if (isRecord(value)) { | ||
return serializeRecord(value); | ||
toArray() { | ||
return this._items.map((entry) => entry[0]); | ||
} | ||
else if (isList(value)) { | ||
return serializeList(value); | ||
get(index) { | ||
return this._items[index][0]; | ||
} | ||
else { | ||
return { type: CrdtType.Register, data: value }; | ||
subscribe(listener) { | ||
this._listeners.push(listener); | ||
} | ||
unsubscribe(listener) { | ||
remove(this._listeners, listener); | ||
} | ||
} | ||
function deserialize(entry, parentToChildren, doc) { | ||
switch (entry[1].type) { | ||
case CrdtType.Record: { | ||
return LiveRecord.deserialize(entry, parentToChildren, doc); | ||
} | ||
case CrdtType.List: { | ||
return LiveList.deserialize(entry, parentToChildren, doc); | ||
} | ||
default: { | ||
throw new Error("Unexpected CRDT type"); | ||
} | ||
} | ||
} |
@@ -1,4 +0,3 @@ | ||
export type { Record, RecordData, List } from "./doc"; | ||
export { LiveRecord, LiveList, RecordData } from "./doc"; | ||
export type { Others, Presence, Room, Client, User } from "./types"; | ||
export { createClient } from "./client"; | ||
export { LiveStorageState } from "./types"; | ||
export type { Others, Presence, Room, InitialStorageFactory, Client, LiveStorage, User, } from "./types"; |
@@ -0,2 +1,2 @@ | ||
export { LiveRecord, LiveList } from "./doc"; | ||
export { createClient } from "./client"; | ||
export { LiveStorageState } from "./types"; |
@@ -41,5 +41,6 @@ import { Presence } from "./types"; | ||
}; | ||
export declare type SerializedCrdtWithId = [id: string, crdt: SerializedCrdt]; | ||
export declare type InitialDocumentStateMessage = { | ||
type: ServerMessageType.InitialStorageState; | ||
root: SerializedRecord | null; | ||
items: SerializedCrdtWithId[]; | ||
}; | ||
@@ -75,57 +76,64 @@ export declare type UpdateStorageMessage = { | ||
Record = 0, | ||
List = 1, | ||
Register = 2 | ||
List = 1 | ||
} | ||
export declare type SerializedRecord = { | ||
id: string; | ||
type: CrdtType.Record; | ||
parentId?: string; | ||
parentKey?: string; | ||
data: { | ||
[key: string]: SerializedCrdt; | ||
[key: string]: any; | ||
}; | ||
}; | ||
export declare type SerializedList = { | ||
id: string; | ||
type: CrdtType.List; | ||
data: { | ||
[position: string]: SerializedCrdt; | ||
}; | ||
parentId: string; | ||
parentKey: string; | ||
}; | ||
export declare type SerializedRegister = { | ||
id?: string; | ||
type: CrdtType.Register; | ||
data: any; | ||
}; | ||
export declare type SerializedCrdt = SerializedRecord | SerializedList | SerializedRegister; | ||
export declare type SerializedCrdt = SerializedRecord | SerializedList; | ||
export declare enum OpType { | ||
Init = 100, | ||
ListInsert = 200, | ||
ListMove = 201, | ||
ListRemove = 202, | ||
RecordUpdate = 300 | ||
Init = 0, | ||
SetParentKey = 1, | ||
CreateList = 2, | ||
UpdateRecord = 3, | ||
CreateRecord = 4, | ||
DeleteRecord = 5, | ||
DeleteRecordKey = 6 | ||
} | ||
export declare type Op = RecordUpdateOp | ListInsertOp | ListDeleteOp | ListMoveOp; | ||
export declare type Op = CreateRecordOp | RecordUpdateOp | DeleteRecordOp | CreateListOp | SetParentKeyOp | DeleteRecordKeyOp; | ||
export declare type RecordUpdateOp = { | ||
id: string; | ||
type: OpType.RecordUpdate; | ||
type: OpType.UpdateRecord; | ||
data: { | ||
[key: string]: SerializedCrdt; | ||
[key: string]: any; | ||
}; | ||
}; | ||
export declare type ListInsertOp = { | ||
export declare type CreateRecordOp = { | ||
id: string; | ||
type: OpType.ListInsert; | ||
position: string; | ||
data: SerializedCrdt; | ||
type: OpType.CreateRecord; | ||
parentId?: string; | ||
parentKey?: string; | ||
data: { | ||
[key: string]: any; | ||
}; | ||
}; | ||
export declare type ListMoveOp = { | ||
export declare type CreateListOp = { | ||
id: string; | ||
type: OpType.ListMove; | ||
itemId: string; | ||
position: string; | ||
type: OpType.CreateList; | ||
parentId: string; | ||
parentKey: string; | ||
}; | ||
export declare type ListDeleteOp = { | ||
export declare type DeleteRecordOp = { | ||
id: string; | ||
type: OpType.ListRemove; | ||
itemId: string; | ||
type: OpType.DeleteRecord; | ||
}; | ||
export declare type SetParentKeyOp = { | ||
id: string; | ||
type: OpType.SetParentKey; | ||
parentKey: string; | ||
}; | ||
export declare type DeleteRecordKeyOp = { | ||
id: string; | ||
type: OpType.DeleteRecordKey; | ||
key: string; | ||
}; | ||
export declare enum WebsocketCloseCodes { | ||
@@ -132,0 +140,0 @@ CLOSE_ABNORMAL = 1006, |
@@ -22,11 +22,12 @@ export var ServerMessageType; | ||
CrdtType[CrdtType["List"] = 1] = "List"; | ||
CrdtType[CrdtType["Register"] = 2] = "Register"; | ||
})(CrdtType || (CrdtType = {})); | ||
export var OpType; | ||
(function (OpType) { | ||
OpType[OpType["Init"] = 100] = "Init"; | ||
OpType[OpType["ListInsert"] = 200] = "ListInsert"; | ||
OpType[OpType["ListMove"] = 201] = "ListMove"; | ||
OpType[OpType["ListRemove"] = 202] = "ListRemove"; | ||
OpType[OpType["RecordUpdate"] = 300] = "RecordUpdate"; | ||
OpType[OpType["Init"] = 0] = "Init"; | ||
OpType[OpType["SetParentKey"] = 1] = "SetParentKey"; | ||
OpType[OpType["CreateList"] = 2] = "CreateList"; | ||
OpType[OpType["UpdateRecord"] = 3] = "UpdateRecord"; | ||
OpType[OpType["CreateRecord"] = 4] = "CreateRecord"; | ||
OpType[OpType["DeleteRecord"] = 5] = "DeleteRecord"; | ||
OpType[OpType["DeleteRecordKey"] = 6] = "DeleteRecordKey"; | ||
})(OpType || (OpType = {})); | ||
@@ -33,0 +34,0 @@ export var WebsocketCloseCodes; |
@@ -1,4 +0,3 @@ | ||
import { RecordData, List } from "."; | ||
import { Doc, Record } from "./doc"; | ||
import { Others, Presence, ClientOptions, Room, InitialStorageFactory, MyPresenceCallback, OthersEventCallback, StorageCallback, AuthEndpoint, LiveStorageState, LiveStorage, EventCallback, User, Connection, Serializable, ErrorCallback, AuthenticationToken, ConnectionCallback } from "./types"; | ||
import { RecordData } from "./doc"; | ||
import { Others, Presence, ClientOptions, Room, MyPresenceCallback, OthersEventCallback, AuthEndpoint, EventCallback, User, Connection, ErrorCallback, AuthenticationToken, ConnectionCallback } from "./types"; | ||
import { ClientMessage, Op } from "./live"; | ||
@@ -24,3 +23,2 @@ declare type IdFactory = () => string; | ||
listeners: { | ||
storage: StorageCallback[]; | ||
event: EventCallback[]; | ||
@@ -39,5 +37,5 @@ others: OthersEventCallback[]; | ||
numberOfRetry: number; | ||
doc: Doc<any> | null; | ||
storageState: LiveStorageState; | ||
initialStorageFactory: InitialStorageFactory | null; | ||
defaultStorageRoot?: { | ||
[key: string]: any; | ||
}; | ||
}; | ||
@@ -73,6 +71,5 @@ export declare type Effects = { | ||
subscribe: { | ||
<T extends Serializable>(type: "my-presence", listener: MyPresenceCallback<T>): void; | ||
<T_1 extends Serializable>(type: "others", listener: OthersEventCallback<T_1>): void; | ||
<T extends Presence>(type: "my-presence", listener: MyPresenceCallback<T>): void; | ||
<T_1 extends Presence>(type: "others", listener: OthersEventCallback<T_1>): void; | ||
(type: "event", listener: EventCallback): void; | ||
<T_2 extends RecordData>(type: "storage", listener: StorageCallback<T_2>): void; | ||
(type: "error", listener: ErrorCallback): void; | ||
@@ -82,28 +79,23 @@ (type: "connection", listener: ConnectionCallback): void; | ||
unsubscribe: { | ||
<T_3 extends Serializable>(type: "my-presence", listener: MyPresenceCallback<T_3>): void; | ||
<T_4 extends Serializable>(type: "others", listener: OthersEventCallback<T_4>): void; | ||
<T_2 extends Presence>(type: "my-presence", listener: MyPresenceCallback<T_2>): void; | ||
<T_3 extends Presence>(type: "others", listener: OthersEventCallback<T_3>): void; | ||
(type: "event", listener: EventCallback): void; | ||
<T_5 extends RecordData>(type: "storage", listener: StorageCallback<T_5>): void; | ||
(type: "error", listener: ErrorCallback): void; | ||
(type: "connection", listener: ConnectionCallback): void; | ||
}; | ||
updatePresence: <T_6 extends Serializable>(overrides: Partial<T_6>) => void; | ||
updatePresence: <T_4 extends Presence>(overrides: Partial<T_4>) => void; | ||
broadcastEvent: (event: any) => void; | ||
fetchStorage: (initialStorageFactory: InitialStorageFactory) => void; | ||
createRecord: <T_7 extends RecordData>(data: any) => Record<T_7>; | ||
updateRecord: <T_8 extends RecordData>(record: Record<T_8>, overrides: Partial<T_8>) => void; | ||
createList: <T_9 extends RecordData>() => List<Record<T_9>>; | ||
pushItem: <T_10 extends RecordData>(list: List<Record<T_10>>, item: Record<T_10>) => void; | ||
deleteItem: <T_11 extends RecordData>(list: List<Record<T_11>>, index: number) => void; | ||
deleteItemById: <T_12 extends RecordData>(list: List<Record<T_12>>, itemId: string) => void; | ||
moveItem: <T_13 extends RecordData>(list: List<Record<T_13>>, index: number, targetIndex: number) => void; | ||
getStorage: <TRoot>() => Promise<{ | ||
root: import("./doc").LiveRecord<TRoot>; | ||
}>; | ||
selectors: { | ||
getConnectionState: () => "failed" | "closed" | "open" | "connecting" | "authenticating" | "unavailable"; | ||
getSelf: <TPresence extends Serializable = Serializable>() => User<TPresence> | null; | ||
getPresence: <T_14 extends Serializable>() => T_14; | ||
getOthers: <T_15 extends Serializable>() => Others<T_15>; | ||
getStorage: () => LiveStorage; | ||
getSelf: <TPresence extends Presence = Presence>() => User<TPresence> | null; | ||
getPresence: <T_5 extends Presence>() => T_5; | ||
getOthers: <T_6 extends Presence>() => Others<T_6>; | ||
}; | ||
}; | ||
export declare function defaultState(me?: Presence): State; | ||
export declare function defaultState(me?: Presence, defaultStorageRoot?: { | ||
[key: string]: any; | ||
}): State; | ||
export declare type InternalRoom = { | ||
@@ -117,4 +109,5 @@ room: Room; | ||
export declare function createRoom(name: string, options: ClientOptions & { | ||
initialPresence?: Presence; | ||
defaultPresence?: Presence; | ||
defaultStorageRoot?: RecordData; | ||
}): InternalRoom; | ||
export {}; |
@@ -10,8 +10,6 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
}; | ||
import { Doc } from "./doc"; | ||
import { LiveStorageState, } from "./types"; | ||
import { createRecord as innerCreateRecord, createList as innerCreateList, } from "./doc"; | ||
import { remove } from "./utils"; | ||
import auth, { parseToken } from "./authentication"; | ||
import { ClientMessageType, ServerMessageType, } from "./live"; | ||
import Storage from "./storage"; | ||
const BACKOFF_RETRY_DELAYS = [250, 500, 1000, 2000, 4000, 8000, 10000]; | ||
@@ -22,4 +20,3 @@ const HEARTBEAT_INTERVAL = 30000; | ||
function isValidRoomEventType(value) { | ||
return (value === "storage" || | ||
value === "my-presence" || | ||
return (value === "my-presence" || | ||
value === "others" || | ||
@@ -252,10 +249,2 @@ value === "event" || | ||
switch (message.type) { | ||
case ServerMessageType.InitialStorageState: { | ||
onInitialStorageState(message); | ||
break; | ||
} | ||
case ServerMessageType.UpdateStorage: { | ||
onStorageUpdates(message); | ||
break; | ||
} | ||
case ServerMessageType.UserJoined: { | ||
@@ -282,2 +271,3 @@ onUserJoinedMessage(message); | ||
} | ||
storage.onMessage(message); | ||
} | ||
@@ -465,82 +455,29 @@ // function onWakeUp() { | ||
} | ||
/** | ||
* STORAGE | ||
*/ | ||
function onStorageUpdates(message) { | ||
if (state.doc == null) { | ||
// TODO: Cache updates in case they are coming while root is queried | ||
return; | ||
} | ||
updateDoc(message.ops.reduce((doc, op) => doc.dispatch(op), state.doc)); | ||
function dispatch(ops) { | ||
state.flushData.storageOperations.push(...ops); | ||
tryFlushing(); | ||
} | ||
function updateDoc(doc) { | ||
state.doc = doc; | ||
if (doc) { | ||
for (const listener of state.listeners.storage) { | ||
listener(getStorage()); | ||
const storage = new Storage({ | ||
fetchStorage: () => { | ||
state.flushData.messages.push({ type: ClientMessageType.FetchStorage }); | ||
tryFlushing(); | ||
}, | ||
dispatch, | ||
getConnectionId: () => { | ||
const me = getSelf(); | ||
if (me) { | ||
return me.connectionId; | ||
} | ||
} | ||
} | ||
throw new Error("Unexpected"); | ||
}, | ||
defaultRoot: state.defaultStorageRoot, | ||
}); | ||
function getStorage() { | ||
if (state.storageState === LiveStorageState.Loaded) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const doc = yield storage.getDocument(); | ||
return { | ||
state: state.storageState, | ||
root: state.doc.root, | ||
root: doc.root, | ||
}; | ||
} | ||
return { | ||
state: state.storageState, | ||
}; | ||
}); | ||
} | ||
function onInitialStorageState(message) { | ||
state.storageState = LiveStorageState.Loaded; | ||
if (message.root == null) { | ||
const rootId = makeId(); | ||
state.doc = Doc.empty(rootId, (op) => dispatch(op)); | ||
updateDoc(state.doc.updateRecord(rootId, state.initialStorageFactory({ | ||
createRecord: (data) => createRecord(data), | ||
createList: () => createList(), | ||
}))); | ||
} | ||
else { | ||
updateDoc(Doc.load(message.root, (op) => dispatch(op))); | ||
} | ||
} | ||
function makeId() { | ||
if (state.idFactory == null) { | ||
throw new Error("Can't generate id. Id factory is missing."); | ||
} | ||
return state.idFactory(); | ||
} | ||
function dispatch(op) { | ||
state.flushData.storageOperations.push(op); | ||
tryFlushing(); | ||
} | ||
function createRecord(data) { | ||
return innerCreateRecord(makeId(), data); | ||
} | ||
function createList() { | ||
return innerCreateList(makeId()); | ||
} | ||
function fetchStorage(initialStorageFactory) { | ||
state.initialStorageFactory = initialStorageFactory; | ||
state.storageState = LiveStorageState.Loading; | ||
state.flushData.messages.push({ type: ClientMessageType.FetchStorage }); | ||
tryFlushing(); | ||
} | ||
function updateRecord(record, overrides) { | ||
updateDoc(state.doc.updateRecord(record.id, overrides)); | ||
} | ||
function pushItem(list, item) { | ||
updateDoc(state.doc.pushItem(list.id, item)); | ||
} | ||
function deleteItem(list, index) { | ||
updateDoc(state.doc.deleteItem(list.id, index)); | ||
} | ||
function deleteItemById(list, itemId) { | ||
updateDoc(state.doc.deleteItemById(list.id, itemId)); | ||
} | ||
function moveItem(list, index, targetIndex) { | ||
updateDoc(state.doc.moveItem(list.id, index, targetIndex)); | ||
} | ||
return { | ||
@@ -564,11 +501,3 @@ // Internal | ||
broadcastEvent, | ||
// Storage | ||
fetchStorage, | ||
createRecord, | ||
updateRecord, | ||
createList, | ||
pushItem, | ||
deleteItem, | ||
deleteItemById, | ||
moveItem, | ||
getStorage, | ||
selectors: { | ||
@@ -581,8 +510,6 @@ // Core | ||
getOthers, | ||
// Storage | ||
getStorage, | ||
}, | ||
}; | ||
} | ||
export function defaultState(me) { | ||
export function defaultState(me, defaultStorageRoot) { | ||
return { | ||
@@ -592,3 +519,2 @@ connection: { state: "closed" }, | ||
listeners: { | ||
storage: [], | ||
event: [], | ||
@@ -618,5 +544,3 @@ others: [], | ||
others: makeOthers({}), | ||
storageState: LiveStorageState.NotInitialized, | ||
initialStorageFactory: null, | ||
doc: null, | ||
defaultStorageRoot, | ||
idFactory: null, | ||
@@ -627,5 +551,5 @@ }; | ||
const throttleDelay = options.throttle || 100; | ||
const liveblocksServer = options.liveblocksServer || "wss://liveblocks.net"; | ||
const liveblocksServer = options.liveblocksServer || "wss://liveblocks.net/v2"; | ||
const authEndpoint = options.authEndpoint; | ||
const state = defaultState(options.initialPresence); | ||
const state = defaultState(options.defaultPresence, options.defaultStorageRoot); | ||
const machine = makeStateMachine(state, { | ||
@@ -645,14 +569,2 @@ throttleDelay, | ||
unsubscribe: machine.unsubscribe, | ||
///////////// | ||
// Storage // | ||
///////////// | ||
getStorage: machine.selectors.getStorage, | ||
fetchStorage: machine.fetchStorage, | ||
createRecord: machine.createRecord, | ||
createList: machine.createList, | ||
updateRecord: machine.updateRecord, | ||
pushItem: machine.pushItem, | ||
deleteItem: machine.deleteItem, | ||
deleteItemById: machine.deleteItemById, | ||
moveItem: machine.moveItem, | ||
////////////// | ||
@@ -665,2 +577,3 @@ // Presence // | ||
broadcastEvent: machine.broadcastEvent, | ||
getStorage: machine.getStorage, | ||
}; | ||
@@ -667,0 +580,0 @@ return { |
@@ -1,3 +0,2 @@ | ||
import { RecordData, Record, List } from "./doc"; | ||
export declare type StorageCallback<T extends RecordData = RecordData> = (storage: LiveStorage<T>) => void; | ||
import { LiveRecord, RecordData } from "./doc"; | ||
export declare type MyPresenceCallback<T extends Presence = Presence> = (me: T) => void; | ||
@@ -12,3 +11,2 @@ export declare type OthersEventCallback<T extends Presence = Presence> = (others: Others<T>, event: OthersEvent<T>) => void; | ||
export declare type RoomEventCallbackMap = { | ||
storage: StorageCallback; | ||
"my-presence": MyPresenceCallback; | ||
@@ -20,8 +18,2 @@ others: OthersEventCallback; | ||
}; | ||
export declare type CreateRecord = Room["createRecord"]; | ||
export declare type CreateList = Room["createList"]; | ||
export declare type InitialStorageFactory<TRoot = RecordData> = (factories: { | ||
createRecord: CreateRecord; | ||
createList: CreateList; | ||
}) => TRoot; | ||
export declare type Client = { | ||
@@ -39,3 +31,6 @@ /** | ||
*/ | ||
enter(roomId: string, defaultPresence?: Presence): Room; | ||
enter<TStorageRoot = RecordData>(roomId: string, options?: { | ||
defaultPresence?: Presence; | ||
defaultStorageRoot?: TStorageRoot; | ||
}): Room; | ||
/** | ||
@@ -91,6 +86,4 @@ * Leaves a room. | ||
}; | ||
export declare type Presence = Serializable; | ||
export declare type SerializablePrimitive = boolean | string | number | null; | ||
export declare type Serializable = { | ||
[key: string]: SerializablePrimitive | Serializable | SerializablePrimitive[]; | ||
export declare type Presence = { | ||
[key: string]: any; | ||
}; | ||
@@ -112,13 +105,2 @@ declare type AuthEndpointCallback = (room: string) => Promise<{ | ||
}; | ||
export declare enum LiveStorageState { | ||
NotInitialized = 0, | ||
Loading = 1, | ||
Loaded = 2 | ||
} | ||
export declare type LiveStorage<T extends RecordData = RecordData> = { | ||
state: LiveStorageState.Loading | LiveStorageState.NotInitialized; | ||
} | { | ||
state: LiveStorageState.Loaded; | ||
root: Record<T>; | ||
}; | ||
declare type ConnectionState = "closed" | "authenticating" | "unavailable" | "failed" | "open" | "connecting"; | ||
@@ -154,8 +136,6 @@ export declare type Connection = { | ||
* | ||
* ### Example | ||
* ``` typescript | ||
* @example | ||
* room.subscribe("my-presence", (presence) => { | ||
* // Do something | ||
* }); | ||
* ``` | ||
*/ | ||
@@ -168,8 +148,6 @@ <T extends Presence>(type: "my-presence", listener: MyPresenceCallback<T>): void; | ||
* | ||
* ### Example | ||
* ``` typescript | ||
* @example | ||
* room.subscribe("others", (others) => { | ||
* // Do something | ||
* }); | ||
* ``` | ||
*/ | ||
@@ -182,11 +160,8 @@ <T extends Presence>(type: "others", listener: OthersEventCallback<T>): void; | ||
* | ||
* ### Example | ||
* ``` typescript | ||
* @example | ||
* room.subscribe("event", ({ event, connectionId }) => { | ||
* // Do something | ||
* }); | ||
* ``` | ||
*/ | ||
(type: "event", listener: EventCallback): void; | ||
<T extends RecordData>(type: "storage", listener: StorageCallback<T>): void; | ||
/** | ||
@@ -207,8 +182,6 @@ * Subscribe to errors thrown in the room. | ||
* | ||
* ### Example | ||
* ``` typescript | ||
* @example | ||
* const onPresenceChange = (presence) => { }; | ||
* room.subscribe("my-presence", onPresenceChange); | ||
* room.unsubscribe("my-presence", onPresenceChange); | ||
* ``` | ||
*/ | ||
@@ -221,8 +194,6 @@ <T extends Presence>(type: "my-presence", listener: MyPresenceCallback<T>): void; | ||
* | ||
* ### Example | ||
* ``` typescript | ||
* @example | ||
* const onOthersChange = (presence) => { }; | ||
* room.subscribe("others", onOthersChange); | ||
* room.unsubscribe("others", onOthersChange); | ||
* ``` | ||
*/ | ||
@@ -235,11 +206,8 @@ <T extends Presence>(type: "others", listener: OthersEventCallback<T>): void; | ||
* | ||
* ### Example | ||
* ``` typescript | ||
* @example | ||
* const onEvent = ({ event, connectionId }) => { }; | ||
* room.subscribe("event", onEvent); | ||
* room.unsubscribe("event", onEvent); | ||
* ``` | ||
*/ | ||
(type: "event", listener: EventCallback): void; | ||
<T extends RecordData>(type: "storage", listener: StorageCallback<T>): void; | ||
/** | ||
@@ -258,6 +226,4 @@ * Unsubscribe to errors thrown in the room. | ||
* | ||
* ### Example | ||
* ``` typescript | ||
* @example | ||
* const user = room.getSelf(); | ||
* ``` | ||
*/ | ||
@@ -268,6 +234,4 @@ getSelf<TPresence extends Presence = Presence>(): User<TPresence> | null; | ||
* | ||
* ### Example | ||
* ``` typescript | ||
* @example | ||
* const presence = room.getPresence(); | ||
* ``` | ||
*/ | ||
@@ -278,6 +242,4 @@ getPresence: <T extends Presence>() => T; | ||
* | ||
* ### Example | ||
* ``` typescript | ||
* @example | ||
* const others = room.getOthers(); | ||
* ``` | ||
*/ | ||
@@ -287,6 +249,5 @@ getOthers: <T extends Presence>() => Others<T>; | ||
* Updates the presence of the current user. Only pass the properties you want to update. No need to send the full presence. | ||
* @param {Partial<T>} overrides A partial object that contains the properties you want to update. | ||
* @param {Partial<T>} overrides - A partial object that contains the properties you want to update. | ||
* | ||
* ### Example | ||
* ``` typescript | ||
* @example | ||
* room.updatePresence({ x: 0 }); | ||
@@ -297,3 +258,2 @@ * room.updatePresence({ y: 0 }); | ||
* // presence is equivalent to { x: 0, y: 0 } | ||
* ``` | ||
*/ | ||
@@ -303,7 +263,5 @@ updatePresence: <T extends Presence>(overrides: Partial<T>) => void; | ||
* Broadcast an event to other users in the room. Event broadcasted to the room can be listened with {@link Room.subscribe}("event"). | ||
* @param {any} event the event to broadcast. Should be serializable to JSON | ||
* @param {any} event - the event to broadcast. Should be serializable to JSON | ||
* | ||
* ### Example | ||
* ``` typescript | ||
* | ||
* @example | ||
* // On client A | ||
@@ -318,15 +276,8 @@ * room.broadcastEvent({ type: "EMOJI", emoji: "🔥" }); | ||
* }); | ||
* ``` | ||
*/ | ||
broadcastEvent: (event: any) => void; | ||
getStorage: () => LiveStorage; | ||
fetchStorage(initialStorageFactory: InitialStorageFactory): void; | ||
createRecord: <T extends RecordData>(data: T) => Record<T>; | ||
createList: <T extends RecordData>() => List<Record<T>>; | ||
updateRecord<T extends RecordData>(record: Record<T>, overrides: Partial<T>): void; | ||
pushItem<T extends RecordData>(list: List<Record<T>>, item: Record<T>): void; | ||
deleteItem<T extends RecordData>(list: List<Record<T>>, index: number): void; | ||
deleteItemById<T extends RecordData>(list: List<Record<T>>, itemId: string): void; | ||
moveItem<T extends RecordData>(list: List<Record<T>>, index: number, targetIndex: number): void; | ||
getStorage: <TRoot>() => Promise<{ | ||
root: LiveRecord<TRoot>; | ||
}>; | ||
}; | ||
export {}; |
@@ -1,6 +0,1 @@ | ||
export var LiveStorageState; | ||
(function (LiveStorageState) { | ||
LiveStorageState[LiveStorageState["NotInitialized"] = 0] = "NotInitialized"; | ||
LiveStorageState[LiveStorageState["Loading"] = 1] = "Loading"; | ||
LiveStorageState[LiveStorageState["Loaded"] = 2] = "Loaded"; | ||
})(LiveStorageState || (LiveStorageState = {})); | ||
export {}; |
{ | ||
"name": "@liveblocks/client", | ||
"version": "0.11.0", | ||
"version": "0.12.0-beta.1", | ||
"description": "", | ||
@@ -5,0 +5,0 @@ "main": "./lib/cjs/index.js", |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
42
4378
152633