@liveblocks/client
Advanced tools
Comparing version 0.12.3 to 0.13.0-beta.1
@@ -1,3 +0,5 @@ | ||
export { LiveObject, LiveList, LiveMap } from "./doc"; | ||
export { LiveObject } from "./LiveObject"; | ||
export { LiveMap } from "./LiveMap"; | ||
export { LiveList } from "./LiveList"; | ||
export type { Others, Presence, Room, Client, User } from "./types"; | ||
export { createClient } from "./client"; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.createClient = exports.LiveMap = exports.LiveList = exports.LiveObject = void 0; | ||
var doc_1 = require("./doc"); | ||
Object.defineProperty(exports, "LiveObject", { enumerable: true, get: function () { return doc_1.LiveObject; } }); | ||
Object.defineProperty(exports, "LiveList", { enumerable: true, get: function () { return doc_1.LiveList; } }); | ||
Object.defineProperty(exports, "LiveMap", { enumerable: true, get: function () { return doc_1.LiveMap; } }); | ||
exports.createClient = exports.LiveList = exports.LiveMap = exports.LiveObject = void 0; | ||
var LiveObject_1 = require("./LiveObject"); | ||
Object.defineProperty(exports, "LiveObject", { enumerable: true, get: function () { return LiveObject_1.LiveObject; } }); | ||
var LiveMap_1 = require("./LiveMap"); | ||
Object.defineProperty(exports, "LiveMap", { enumerable: true, get: function () { return LiveMap_1.LiveMap; } }); | ||
var LiveList_1 = require("./LiveList"); | ||
Object.defineProperty(exports, "LiveList", { enumerable: true, get: function () { return LiveList_1.LiveList; } }); | ||
var client_1 = require("./client"); | ||
Object.defineProperty(exports, "createClient", { enumerable: true, get: function () { return client_1.createClient; } }); |
@@ -1,3 +0,11 @@ | ||
import { Others, Presence, ClientOptions, Room, MyPresenceCallback, OthersEventCallback, AuthEndpoint, EventCallback, User, Connection, ErrorCallback, AuthenticationToken, ConnectionCallback } from "./types"; | ||
import { Others, Presence, ClientOptions, Room, MyPresenceCallback, OthersEventCallback, AuthEndpoint, EventCallback, User, Connection, ErrorCallback, AuthenticationToken, ConnectionCallback, StorageCallback, StorageUpdate } from "./types"; | ||
import { ClientMessage, Op } from "./live"; | ||
import { LiveMap } from "./LiveMap"; | ||
import { LiveObject } from "./LiveObject"; | ||
import { LiveList } from "./LiveList"; | ||
import { AbstractCrdt } from "./AbstractCrdt"; | ||
declare type HistoryItem = Array<Op | { | ||
type: "presence"; | ||
data: Presence; | ||
}>; | ||
declare type IdFactory = () => string; | ||
@@ -8,3 +16,3 @@ export declare type State = { | ||
lastFlushTime: number; | ||
flushData: { | ||
buffer: { | ||
presence: Presence | null; | ||
@@ -28,2 +36,3 @@ messages: ClientMessage[]; | ||
connection: ConnectionCallback[]; | ||
storage: StorageCallback[]; | ||
}; | ||
@@ -40,2 +49,20 @@ me: Presence; | ||
}; | ||
clock: number; | ||
opClock: number; | ||
items: Map<string, AbstractCrdt>; | ||
root: LiveObject | undefined; | ||
undoStack: HistoryItem[]; | ||
redoStack: HistoryItem[]; | ||
isHistoryPaused: boolean; | ||
pausedHistory: HistoryItem; | ||
isBatching: boolean; | ||
batch: { | ||
ops: Op[]; | ||
reverseOps: HistoryItem; | ||
updates: { | ||
others: []; | ||
presence: boolean; | ||
nodes: Set<AbstractCrdt>; | ||
}; | ||
}; | ||
}; | ||
@@ -69,10 +96,19 @@ export declare type Effects = { | ||
onVisibilityChange: (visibilityState: VisibilityState) => void; | ||
getUndoStack: () => HistoryItem[]; | ||
getItemsCount: () => number; | ||
connect: () => null | undefined; | ||
disconnect: () => void; | ||
subscribe: { | ||
<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; | ||
(type: "error", listener: ErrorCallback): void; | ||
(type: "connection", listener: ConnectionCallback): void; | ||
(callback: (updates: StorageUpdate) => void): () => void; | ||
<TKey extends string, TValue>(liveMap: LiveMap<TKey, TValue>, callback: (liveMap: LiveMap<TKey, TValue>) => void): () => void; | ||
<TData>(liveObject: LiveObject<TData>, callback: (liveObject: LiveObject<TData>) => void): () => void; | ||
<TItem>(liveList: LiveList<TItem>, callback: (liveList: LiveList<TItem>) => void): () => void; | ||
<TItem_1 extends AbstractCrdt>(node: TItem_1, callback: (updates: StorageUpdate[]) => void, options: { | ||
isDeep: true; | ||
}): () => 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; | ||
(type: "error", listener: ErrorCallback): () => void; | ||
(type: "connection", listener: ConnectionCallback): () => void; | ||
}; | ||
@@ -86,8 +122,13 @@ unsubscribe: { | ||
}; | ||
updatePresence: <T_4 extends Presence>(overrides: Partial<T_4>) => void; | ||
updatePresence: <T_4 extends Presence>(overrides: Partial<T_4>, options?: { | ||
addToHistory: boolean; | ||
} | undefined) => void; | ||
broadcastEvent: (event: any) => void; | ||
batch: (callback: () => void) => void; | ||
undo: () => void; | ||
redo: () => void; | ||
pauseHistory: () => void; | ||
resumeHistory: () => void; | ||
getStorage: <TRoot>() => Promise<{ | ||
root: import("./doc").LiveObject<TRoot>; | ||
root: LiveObject<TRoot>; | ||
}>; | ||
@@ -94,0 +135,0 @@ selectors: { |
@@ -30,5 +30,2 @@ "use strict"; | ||
}; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
@@ -39,3 +36,7 @@ exports.createRoom = exports.defaultState = exports.makeStateMachine = void 0; | ||
const live_1 = require("./live"); | ||
const storage_1 = __importDefault(require("./storage")); | ||
const LiveMap_1 = require("./LiveMap"); | ||
const LiveObject_1 = require("./LiveObject"); | ||
const LiveList_1 = require("./LiveList"); | ||
const AbstractCrdt_1 = require("./AbstractCrdt"); | ||
const LiveRegister_1 = require("./LiveRegister"); | ||
const BACKOFF_RETRY_DELAYS = [250, 500, 1000, 2000, 4000, 8000, 10000]; | ||
@@ -112,9 +113,271 @@ const HEARTBEAT_INTERVAL = 30000; | ||
}; | ||
function subscribe(type, listener) { | ||
if (!isValidRoomEventType(type)) { | ||
throw new Error(`"${type}" is not a valid event name`); | ||
function genericSubscribe(callback) { | ||
state.listeners.storage.push(callback); | ||
return () => (0, utils_1.remove)(state.listeners.storage, callback); | ||
} | ||
function crdtSubscribe(crdt, innerCallback, options) { | ||
const cb = (updates) => { | ||
const relatedUpdates = []; | ||
for (const update of updates) { | ||
if ((options === null || options === void 0 ? void 0 : options.isDeep) && (0, utils_1.isSameNodeOrChildOf)(update.node, crdt)) { | ||
relatedUpdates.push(update); | ||
} | ||
else if (update.node._id === crdt._id) { | ||
innerCallback(update.node); | ||
} | ||
} | ||
if ((options === null || options === void 0 ? void 0 : options.isDeep) && relatedUpdates.length > 0) { | ||
innerCallback(relatedUpdates); | ||
} | ||
}; | ||
return genericSubscribe(cb); | ||
} | ||
function createRootFromMessage(message) { | ||
state.root = load(message.items); | ||
for (const key in state.defaultStorageRoot) { | ||
if (state.root.get(key) == null) { | ||
state.root.set(key, state.defaultStorageRoot[key]); | ||
} | ||
} | ||
state.listeners[type].push(listener); | ||
} | ||
function load(items) { | ||
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"); | ||
} | ||
return LiveObject_1.LiveObject._deserialize(root, parentToChildren, { | ||
addItem, | ||
deleteItem, | ||
generateId, | ||
generateOpId, | ||
dispatch: storageDispatch, | ||
}); | ||
} | ||
function addItem(id, item) { | ||
state.items.set(id, item); | ||
} | ||
function deleteItem(id) { | ||
state.items.delete(id); | ||
} | ||
function getItem(id) { | ||
return state.items.get(id); | ||
} | ||
function addToUndoStack(historyItem) { | ||
// If undo stack is too large, we remove the older item | ||
if (state.undoStack.length >= 50) { | ||
state.undoStack.shift(); | ||
} | ||
if (state.isHistoryPaused) { | ||
state.pausedHistory.unshift(...historyItem); | ||
} | ||
else { | ||
state.undoStack.push(historyItem); | ||
} | ||
} | ||
function storageDispatch(ops, reverse, modified) { | ||
if (state.isBatching) { | ||
state.batch.ops.push(...ops); | ||
for (const item of modified) { | ||
state.batch.updates.nodes.add(item); | ||
} | ||
state.batch.reverseOps.push(...reverse); | ||
} | ||
else { | ||
addToUndoStack(reverse); | ||
state.redoStack = []; | ||
dispatch(ops); | ||
notify({ nodes: new Set(modified) }); | ||
} | ||
} | ||
function notify({ nodes = new Set(), presence = false, others = [], }) { | ||
if (others.length > 0) { | ||
state.others = makeOthers(state.users); | ||
for (const event of others) { | ||
for (const listener of state.listeners["others"]) { | ||
listener(state.others, event); | ||
} | ||
} | ||
} | ||
if (presence) { | ||
for (const listener of state.listeners["my-presence"]) { | ||
listener(state.me); | ||
} | ||
} | ||
if (nodes.size > 0) { | ||
for (const subscriber of state.listeners.storage) { | ||
subscriber(Array.from(nodes).map((m) => { | ||
if (m instanceof LiveObject_1.LiveObject) { | ||
return { | ||
type: "LiveObject", | ||
node: m, | ||
}; | ||
} | ||
else if (m instanceof LiveList_1.LiveList) { | ||
return { | ||
type: "LiveList", | ||
node: m, | ||
}; | ||
} | ||
else { | ||
return { | ||
type: "LiveMap", | ||
node: m, | ||
}; | ||
} | ||
})); | ||
} | ||
} | ||
} | ||
function getConnectionId() { | ||
if (state.connection.state === "open" || | ||
state.connection.state === "connecting") { | ||
return state.connection.id; | ||
} | ||
throw new Error("Internal. Tried to get connection id but connection is not open"); | ||
} | ||
function generateId() { | ||
return `${getConnectionId()}:${state.clock++}`; | ||
} | ||
function generateOpId() { | ||
return `${getConnectionId()}:${state.opClock++}`; | ||
} | ||
function apply(item) { | ||
const result = { | ||
reverse: [], | ||
updates: { nodes: new Set(), presence: false }, | ||
}; | ||
for (const op of item) { | ||
if (op.type === "presence") { | ||
const reverse = { | ||
type: "presence", | ||
data: {}, | ||
}; | ||
for (const key in op.data) { | ||
reverse.data[key] = state.me[key]; | ||
} | ||
state.me = Object.assign(Object.assign({}, state.me), op.data); | ||
if (state.buffer.presence == null) { | ||
state.buffer.presence = op.data; | ||
} | ||
else { | ||
for (const key in op.data) { | ||
state.buffer.presence[key] = op.data; | ||
} | ||
} | ||
result.reverse.unshift(reverse); | ||
result.updates.presence = true; | ||
} | ||
else { | ||
const applyOpResult = applyOp(op); | ||
if (applyOpResult.modified) { | ||
result.updates.nodes.add(applyOpResult.modified); | ||
result.reverse.unshift(...applyOpResult.reverse); | ||
} | ||
} | ||
} | ||
return result; | ||
} | ||
function applyOp(op) { | ||
switch (op.type) { | ||
case live_1.OpType.DeleteObjectKey: | ||
case live_1.OpType.UpdateObject: | ||
case live_1.OpType.DeleteCrdt: { | ||
const item = state.items.get(op.id); | ||
if (item == null) { | ||
return { modified: false }; | ||
} | ||
return item._apply(op); | ||
} | ||
case live_1.OpType.SetParentKey: { | ||
const item = state.items.get(op.id); | ||
if (item == null) { | ||
return { modified: false }; | ||
} | ||
if (item._parent instanceof LiveList_1.LiveList) { | ||
const previousKey = item._parentKey; | ||
item._parent._setChildKey(op.parentKey, item); | ||
return { | ||
reverse: [ | ||
{ | ||
type: live_1.OpType.SetParentKey, | ||
id: item._id, | ||
parentKey: previousKey, | ||
}, | ||
], | ||
modified: item._parent, | ||
}; | ||
} | ||
return { modified: false }; | ||
} | ||
case live_1.OpType.CreateObject: { | ||
const parent = state.items.get(op.parentId); | ||
if (parent == null || getItem(op.id) != null) { | ||
return { modified: false }; | ||
} | ||
return parent._attachChild(op.id, op.parentKey, new LiveObject_1.LiveObject(op.data)); | ||
} | ||
case live_1.OpType.CreateList: { | ||
const parent = state.items.get(op.parentId); | ||
if (parent == null || getItem(op.id) != null) { | ||
return { modified: false }; | ||
} | ||
return parent._attachChild(op.id, op.parentKey, new LiveList_1.LiveList()); | ||
} | ||
case live_1.OpType.CreateRegister: { | ||
const parent = state.items.get(op.parentId); | ||
if (parent == null || getItem(op.id) != null) { | ||
return { modified: false }; | ||
} | ||
return parent._attachChild(op.id, op.parentKey, new LiveRegister_1.LiveRegister(op.data)); | ||
} | ||
case live_1.OpType.CreateMap: { | ||
const parent = state.items.get(op.parentId); | ||
if (parent == null || getItem(op.id) != null) { | ||
return { modified: false }; | ||
} | ||
return parent._attachChild(op.id, op.parentKey, new LiveMap_1.LiveMap()); | ||
} | ||
} | ||
return { modified: false }; | ||
} | ||
function subscribe(firstParam, listener, options) { | ||
if (firstParam instanceof AbstractCrdt_1.AbstractCrdt) { | ||
return crdtSubscribe(firstParam, listener, options); | ||
} | ||
else if (typeof firstParam === "function") { | ||
return genericSubscribe(firstParam); | ||
} | ||
else if (!isValidRoomEventType(firstParam)) { | ||
throw new Error(`"${firstParam}" is not a valid event name`); | ||
} | ||
state.listeners[firstParam].push(listener); | ||
return () => { | ||
const callbacks = state.listeners[firstParam]; | ||
(0, utils_1.remove)(callbacks, listener); | ||
}; | ||
} | ||
function unsubscribe(event, callback) { | ||
console.warn(`unsubscribe is depreacted and will be removed in a future version. | ||
use the callback returned by subscribe instead. | ||
See v0.13 release notes for more information. | ||
`); | ||
if (!isValidRoomEventType(event)) { | ||
@@ -151,17 +414,25 @@ throw new Error(`"${event}" is not a valid event name`); | ||
} | ||
function updatePresence(overrides) { | ||
const newPresence = Object.assign(Object.assign({}, state.me), overrides); | ||
if (state.flushData.presence == null) { | ||
state.flushData.presence = overrides; | ||
function updatePresence(overrides, options) { | ||
const oldValues = {}; | ||
if (state.buffer.presence == null) { | ||
state.buffer.presence = {}; | ||
} | ||
for (const key in overrides) { | ||
state.buffer.presence[key] = overrides[key]; | ||
oldValues[key] = state.me[key]; | ||
} | ||
state.me = Object.assign(Object.assign({}, state.me), overrides); | ||
if (state.isBatching) { | ||
if (options === null || options === void 0 ? void 0 : options.addToHistory) { | ||
state.batch.reverseOps.push({ type: "presence", data: oldValues }); | ||
} | ||
state.batch.updates.presence = true; | ||
} | ||
else { | ||
for (const key in overrides) { | ||
state.flushData.presence[key] = overrides[key]; | ||
tryFlushing(); | ||
if (options === null || options === void 0 ? void 0 : options.addToHistory) { | ||
addToUndoStack([{ type: "presence", data: oldValues }]); | ||
} | ||
notify({ presence: true }); | ||
} | ||
state.me = newPresence; | ||
tryFlushing(); | ||
for (const listener of state.listeners["my-presence"]) { | ||
listener(state.me); | ||
} | ||
} | ||
@@ -206,14 +477,8 @@ function authenticationSuccess(token, socket) { | ||
} | ||
updateUsers({ | ||
return { | ||
type: "update", | ||
updates: message.data, | ||
user: state.users[message.actor], | ||
}); | ||
}; | ||
} | ||
function updateUsers(event) { | ||
state.others = makeOthers(state.users); | ||
for (const listener of state.listeners["others"]) { | ||
listener(state.others, event); | ||
} | ||
} | ||
function onUserLeftMessage(message) { | ||
@@ -224,4 +489,5 @@ const userLeftMessage = message; | ||
delete state.users[userLeftMessage.actor]; | ||
updateUsers({ type: "leave", user }); | ||
return { type: "leave", user }; | ||
} | ||
return null; | ||
} | ||
@@ -240,3 +506,3 @@ function onRoomStateMessage(message) { | ||
state.users = newUsers; | ||
updateUsers({ type: "reset" }); | ||
return { type: "reset" }; | ||
} | ||
@@ -260,7 +526,6 @@ function onNavigatorOnline() { | ||
}; | ||
updateUsers({ type: "enter", user: state.users[message.actor] }); | ||
if (state.me) { | ||
// Send current presence to new user | ||
// TODO: Consider storing it on the backend | ||
state.flushData.messages.push({ | ||
state.buffer.messages.push({ | ||
type: live_1.ClientMessageType.UpdatePresence, | ||
@@ -272,2 +537,3 @@ data: state.me, | ||
} | ||
return { type: "enter", user: state.users[message.actor] }; | ||
} | ||
@@ -280,25 +546,53 @@ function onMessage(event) { | ||
const message = JSON.parse(event.data); | ||
switch (message.type) { | ||
case live_1.ServerMessageType.UserJoined: { | ||
onUserJoinedMessage(message); | ||
break; | ||
let subMessages = []; | ||
if (Array.isArray(message)) { | ||
subMessages = message; | ||
} | ||
else { | ||
subMessages.push(message); | ||
} | ||
const updates = { | ||
nodes: new Set(), | ||
others: [], | ||
}; | ||
for (const subMessage of subMessages) { | ||
switch (subMessage.type) { | ||
case live_1.ServerMessageType.UserJoined: { | ||
updates.others.push(onUserJoinedMessage(message)); | ||
break; | ||
} | ||
case live_1.ServerMessageType.UpdatePresence: { | ||
updates.others.push(onUpdatePresenceMessage(subMessage)); | ||
break; | ||
} | ||
case live_1.ServerMessageType.Event: { | ||
onEvent(subMessage); | ||
break; | ||
} | ||
case live_1.ServerMessageType.UserLeft: { | ||
const event = onUserLeftMessage(subMessage); | ||
if (event) { | ||
updates.others.push(event); | ||
} | ||
break; | ||
} | ||
case live_1.ServerMessageType.RoomState: { | ||
updates.others.push(onRoomStateMessage(subMessage)); | ||
break; | ||
} | ||
case live_1.ServerMessageType.InitialStorageState: { | ||
createRootFromMessage(subMessage); | ||
_getInitialStateResolver === null || _getInitialStateResolver === void 0 ? void 0 : _getInitialStateResolver(); | ||
break; | ||
} | ||
case live_1.ServerMessageType.UpdateStorage: { | ||
const applyResult = apply(subMessage.ops); | ||
for (const node of applyResult.updates.nodes) { | ||
updates.nodes.add(node); | ||
} | ||
break; | ||
} | ||
} | ||
case live_1.ServerMessageType.UpdatePresence: { | ||
onUpdatePresenceMessage(message); | ||
break; | ||
} | ||
case live_1.ServerMessageType.Event: { | ||
onEvent(message); | ||
break; | ||
} | ||
case live_1.ServerMessageType.UserLeft: { | ||
onUserLeftMessage(message); | ||
break; | ||
} | ||
case live_1.ServerMessageType.RoomState: { | ||
onRoomStateMessage(message); | ||
break; | ||
} | ||
} | ||
storage.onMessage(message); | ||
notify(updates); | ||
} | ||
@@ -323,3 +617,3 @@ // function onWakeUp() { | ||
state.users = {}; | ||
updateUsers({ type: "reset" }); | ||
notify({ others: [{ type: "reset" }] }); | ||
if (event.code >= 4000 && event.code <= 4100) { | ||
@@ -413,3 +707,3 @@ updateConnection({ state: "failed" }); | ||
effects.send(messages); | ||
state.flushData = { | ||
state.buffer = { | ||
messages: [], | ||
@@ -430,15 +724,15 @@ storageOperations: [], | ||
const messages = []; | ||
if (state.flushData.presence) { | ||
if (state.buffer.presence) { | ||
messages.push({ | ||
type: live_1.ClientMessageType.UpdatePresence, | ||
data: state.flushData.presence, | ||
data: state.buffer.presence, | ||
}); | ||
} | ||
for (const event of state.flushData.messages) { | ||
for (const event of state.buffer.messages) { | ||
messages.push(event); | ||
} | ||
if (state.flushData.storageOperations.length > 0) { | ||
if (state.buffer.storageOperations.length > 0) { | ||
messages.push({ | ||
type: live_1.ClientMessageType.UpdateStorage, | ||
ops: state.flushData.storageOperations, | ||
ops: state.buffer.storageOperations, | ||
}); | ||
@@ -465,3 +759,3 @@ } | ||
state.users = {}; | ||
updateUsers({ type: "reset" }); | ||
notify({ others: [{ type: "reset" }] }); | ||
clearListeners(); | ||
@@ -484,3 +778,3 @@ } | ||
} | ||
state.flushData.messages.push({ | ||
state.buffer.messages.push({ | ||
type: live_1.ClientMessageType.ClientEvent, | ||
@@ -492,25 +786,22 @@ event, | ||
function dispatch(ops) { | ||
state.flushData.storageOperations.push(...ops); | ||
state.buffer.storageOperations.push(...ops); | ||
tryFlushing(); | ||
} | ||
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, | ||
}); | ||
let _getInitialStatePromise = null; | ||
let _getInitialStateResolver = null; | ||
function getStorage() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const doc = yield storage.getDocument(); | ||
if (state.root) { | ||
return { | ||
root: state.root, | ||
}; | ||
} | ||
if (_getInitialStatePromise == null) { | ||
state.buffer.messages.push({ type: live_1.ClientMessageType.FetchStorage }); | ||
tryFlushing(); | ||
_getInitialStatePromise = new Promise((resolve) => (_getInitialStateResolver = resolve)); | ||
} | ||
yield _getInitialStatePromise; | ||
return { | ||
root: doc.root, | ||
root: state.root, | ||
}; | ||
@@ -520,7 +811,77 @@ }); | ||
function undo() { | ||
storage.undo(); | ||
if (state.isBatching) { | ||
throw new Error("undo is not allowed during a batch"); | ||
} | ||
const historyItem = state.undoStack.pop(); | ||
if (historyItem == null) { | ||
return; | ||
} | ||
state.isHistoryPaused = false; | ||
const result = apply(historyItem); | ||
notify(result.updates); | ||
state.redoStack.push(result.reverse); | ||
for (const op of historyItem) { | ||
if (op.type !== "presence") { | ||
state.buffer.storageOperations.push(op); | ||
} | ||
} | ||
tryFlushing(); | ||
} | ||
function redo() { | ||
storage.redo(); | ||
if (state.isBatching) { | ||
throw new Error("redo is not allowed during a batch"); | ||
} | ||
const historyItem = state.redoStack.pop(); | ||
if (historyItem == null) { | ||
return; | ||
} | ||
state.isHistoryPaused = false; | ||
const result = apply(historyItem); | ||
notify(result.updates); | ||
state.undoStack.push(result.reverse); | ||
for (const op of historyItem) { | ||
if (op.type !== "presence") { | ||
state.buffer.storageOperations.push(op); | ||
} | ||
} | ||
tryFlushing(); | ||
} | ||
function batch(callback) { | ||
if (state.isBatching) { | ||
throw new Error("batch should not be called during a batch"); | ||
} | ||
state.isBatching = true; | ||
try { | ||
callback(); | ||
} | ||
finally { | ||
state.isBatching = false; | ||
addToUndoStack(state.batch.reverseOps); | ||
// Clear the redo stack because batch is always called from a local operation | ||
state.redoStack = []; | ||
dispatch(state.batch.ops); | ||
notify(state.batch.updates); | ||
state.batch = { | ||
ops: [], | ||
reverseOps: [], | ||
updates: { | ||
others: [], | ||
nodes: new Set(), | ||
presence: false, | ||
}, | ||
}; | ||
tryFlushing(); | ||
} | ||
} | ||
function pauseHistory() { | ||
state.pausedHistory = []; | ||
state.isHistoryPaused = true; | ||
} | ||
function resumeHistory() { | ||
state.isHistoryPaused = false; | ||
if (state.pausedHistory.length > 0) { | ||
addToUndoStack(state.pausedHistory); | ||
} | ||
state.pausedHistory = []; | ||
} | ||
return { | ||
@@ -536,2 +897,4 @@ // Internal | ||
onVisibilityChange, | ||
getUndoStack: () => state.undoStack, | ||
getItemsCount: () => state.items.size, | ||
// Core | ||
@@ -545,4 +908,7 @@ connect, | ||
broadcastEvent, | ||
batch, | ||
undo, | ||
redo, | ||
pauseHistory, | ||
resumeHistory, | ||
getStorage, | ||
@@ -570,2 +936,3 @@ selectors: { | ||
connection: [], | ||
storage: [], | ||
}, | ||
@@ -579,3 +946,3 @@ numberOfRetry: 0, | ||
}, | ||
flushData: { | ||
buffer: { | ||
presence: me == null ? {} : me, | ||
@@ -593,2 +960,17 @@ messages: [], | ||
idFactory: null, | ||
// Storage | ||
clock: 0, | ||
opClock: 0, | ||
items: new Map(), | ||
root: undefined, | ||
undoStack: [], | ||
redoStack: [], | ||
isHistoryPaused: false, | ||
pausedHistory: [], | ||
isBatching: false, | ||
batch: { | ||
ops: [], | ||
updates: { nodes: new Set(), presence: false, others: [] }, | ||
reverseOps: [], | ||
}, | ||
}; | ||
@@ -599,3 +981,3 @@ } | ||
const throttleDelay = options.throttle || 100; | ||
const liveblocksServer = options.liveblocksServer || "wss://liveblocks.net/v4"; | ||
const liveblocksServer = options.liveblocksServer || "wss://liveblocks.net/v5"; | ||
let authEndpoint; | ||
@@ -634,4 +1016,9 @@ if (options.authEndpoint) { | ||
getStorage: machine.getStorage, | ||
undo: machine.undo, | ||
redo: machine.redo, | ||
batch: machine.batch, | ||
history: { | ||
undo: machine.undo, | ||
redo: machine.redo, | ||
pause: machine.pauseHistory, | ||
resume: machine.resumeHistory, | ||
}, | ||
}; | ||
@@ -638,0 +1025,0 @@ return { |
@@ -1,2 +0,4 @@ | ||
import { LiveObject } from "./doc"; | ||
import type { LiveList } from "./LiveList"; | ||
import type { LiveMap } from "./LiveMap"; | ||
import type { LiveObject } from "./LiveObject"; | ||
export declare type MyPresenceCallback<T extends Presence = Presence> = (me: T) => void; | ||
@@ -17,2 +19,16 @@ export declare type OthersEventCallback<T extends Presence = Presence> = (others: Others<T>, event: OthersEvent<T>) => void; | ||
}; | ||
export declare type LiveMapUpdates<TKey extends string = string, TValue = any> = { | ||
type: "LiveMap"; | ||
node: LiveMap<TKey, TValue>; | ||
}; | ||
export declare type LiveObjectUpdates<TData = any> = { | ||
type: "LiveObject"; | ||
node: LiveObject<TData>; | ||
}; | ||
export declare type LiveListUpdates<TItem = any> = { | ||
type: "LiveList"; | ||
node: LiveList<TItem>; | ||
}; | ||
export declare type StorageUpdate = LiveMapUpdates | LiveObjectUpdates | LiveListUpdates; | ||
export declare type StorageCallback = (updates: StorageUpdate[]) => void; | ||
export declare type Client = { | ||
@@ -22,3 +38,3 @@ /** | ||
* | ||
* @param roomId - The id of the room | ||
* @param roomId The id of the room | ||
*/ | ||
@@ -28,4 +44,4 @@ getRoom(roomId: string): Room | null; | ||
* Enters a room and returns it. | ||
* @param roomId - The id of the room | ||
* @param defaultPresence - Optional. Should be serializable to JSON. If omitted, an empty object will be used. | ||
* @param roomId The id of the room | ||
* @param defaultPresence Optional. Should be serializable to JSON. If omitted, an empty object will be used. | ||
*/ | ||
@@ -38,3 +54,3 @@ enter<TStorageRoot extends Record<string, any> = Record<string, any>>(roomId: string, options?: { | ||
* Leaves a room. | ||
* @param roomId - The id of the room | ||
* @param roomId The id of the room | ||
*/ | ||
@@ -138,3 +154,3 @@ leave(roomId: string): void; | ||
* | ||
* @param listener - the callback that is called every time the current user presence is updated with {@link Room.updatePresence}. | ||
* @param listener the callback that is called every time the current user presence is updated with {@link Room.updatePresence}. | ||
* | ||
@@ -146,7 +162,7 @@ * @example | ||
*/ | ||
<T extends Presence>(type: "my-presence", listener: MyPresenceCallback<T>): void; | ||
<T extends Presence>(type: "my-presence", listener: MyPresenceCallback<T>): () => void; | ||
/** | ||
* Subscribe to the other users updates. | ||
* | ||
* @param listener - the callback that is called when a user enters or leaves the room or when a user update its presence. | ||
* @param listener the callback that is called when a user enters or leaves the room or when a user update its presence. | ||
* | ||
@@ -158,7 +174,7 @@ * @example | ||
*/ | ||
<T extends Presence>(type: "others", listener: OthersEventCallback<T>): void; | ||
<T extends Presence>(type: "others", listener: OthersEventCallback<T>): () => void; | ||
/** | ||
* Subscribe to events broadcasted by {@link Room.broadcastEvent} | ||
* | ||
* @param listener - the callback that is called when a user calls {@link Room.broadcastEvent} | ||
* @param listener the callback that is called when a user calls {@link Room.broadcastEvent} | ||
* | ||
@@ -170,52 +186,159 @@ * @example | ||
*/ | ||
(type: "event", listener: EventCallback): void; | ||
(type: "event", listener: EventCallback): () => void; | ||
/** | ||
* Subscribe to errors thrown in the room. | ||
*/ | ||
(type: "error", listener: ErrorCallback): void; | ||
(type: "error", listener: ErrorCallback): () => void; | ||
/** | ||
* Subscribe to connection state updates. | ||
*/ | ||
(type: "connection", listener: ConnectionCallback): void; | ||
}; | ||
unsubscribe: { | ||
(type: "connection", listener: ConnectionCallback): () => void; | ||
/** | ||
* Unsubscribe to the current user presence updates. | ||
* Subscribes to changes made on a {@link LiveMap}. Returns an unsubscribe function. | ||
* In a future version, we will also expose what exactly changed in the {@link LiveMap}. | ||
* | ||
* @param listener - the callback that has been used with {@link Room.subscribe}("my-presence"). | ||
* @param listener the callback this called when the {@link LiveMap} changes. | ||
* | ||
* @returns Unsubscribe function. | ||
* | ||
* @example | ||
* const onPresenceChange = (presence) => { }; | ||
* room.subscribe("my-presence", onPresenceChange); | ||
* room.unsubscribe("my-presence", onPresenceChange); | ||
* const liveMap = new LiveMap(); | ||
* const unsubscribe = room.subscribe(liveMap, (liveMap) => { }); | ||
* unsubscribe(); | ||
*/ | ||
<T extends Presence>(type: "my-presence", listener: MyPresenceCallback<T>): void; | ||
<TKey extends string, TValue>(liveMap: LiveMap<TKey, TValue>, listener: (liveMap: LiveMap<TKey, TValue>) => void): () => void; | ||
/** | ||
* Unsubscribe to the other users updates. | ||
* Subscribes to changes made on a {@link LiveObject}. Returns an unsubscribe function. | ||
* In a future version, we will also expose what exactly changed in the {@link LiveObject}. | ||
* | ||
* @param listener - the callback that has been used with {@link Room.subscribe}("others"). | ||
* @param listener the callback this called when the {@link LiveObject} changes. | ||
* | ||
* @returns Unsubscribe function. | ||
* | ||
* @example | ||
* const onOthersChange = (presence) => { }; | ||
* room.subscribe("others", onOthersChange); | ||
* room.unsubscribe("others", onOthersChange); | ||
* const liveObject = new LiveObject(); | ||
* const unsubscribe = room.subscribe(liveObject, (liveObject) => { }); | ||
* unsubscribe(); | ||
*/ | ||
<T extends Presence>(type: "others", listener: OthersEventCallback<T>): void; | ||
<TData>(liveObject: LiveObject<TData>, callback: (liveObject: LiveObject<TData>) => void): () => void; | ||
/** | ||
* Unsubscribe to events broadcasted by {@link Room.broadcastEvent} | ||
* Subscribes to changes made on a {@link LiveList}. Returns an unsubscribe function. | ||
* In a future version, we will also expose what exactly changed in the {@link LiveList}. | ||
* | ||
* @param listener - the callback that has been used with {@link Room.unsubscribe}("event"). | ||
* @param listener the callback this called when the {@link LiveList} changes. | ||
* | ||
* @returns Unsubscribe function. | ||
* | ||
* @example | ||
* const onEvent = ({ event, connectionId }) => { }; | ||
* room.subscribe("event", onEvent); | ||
* room.unsubscribe("event", onEvent); | ||
* const liveList = new LiveList(); | ||
* const unsubscribe = room.subscribe(liveList, (liveList) => { }); | ||
* unsubscribe(); | ||
*/ | ||
<TItem>(liveList: LiveList<TItem>, callback: (liveList: LiveList<TItem>) => void): () => void; | ||
/** | ||
* Subscribes to changes made on a {@link LiveMap} and all the nested data structures. Returns an unsubscribe function. | ||
* In a future version, we will also expose what exactly changed in the {@link LiveMap}. | ||
* | ||
* @param listener the callback this called when the {@link LiveMap} changes. | ||
* | ||
* @returns Unsubscribe function. | ||
* | ||
* @example | ||
* const liveMap = new LiveMap(); | ||
* const unsubscribe = room.subscribe(liveMap, (liveMap) => { }, { isDeep: true }); | ||
* unsubscribe(); | ||
*/ | ||
<TKey extends string, TValue>(liveMap: LiveMap<TKey, TValue>, callback: (updates: StorageUpdate[]) => void, options: { | ||
isDeep: true; | ||
}): () => void; | ||
/** | ||
* Subscribes to changes made on a {@link LiveObject} and all the nested data structures. Returns an unsubscribe function. | ||
* In a future version, we will also expose what exactly changed in the {@link LiveObject}. | ||
* | ||
* @param listener the callback this called when the {@link LiveObject} changes. | ||
* | ||
* @returns Unsubscribe function. | ||
* | ||
* @example | ||
* const liveObject = new LiveObject(); | ||
* const unsubscribe = room.subscribe(liveObject, (liveObject) => { }, { isDeep: true }); | ||
* unsubscribe(); | ||
*/ | ||
<TData>(liveObject: LiveObject<TData>, callback: (updates: StorageUpdate[]) => void, options: { | ||
isDeep: true; | ||
}): () => void; | ||
/** | ||
* Subscribes to changes made on a {@link LiveList} and all the nested data structures. Returns an unsubscribe function. | ||
* In a future version, we will also expose what exactly changed in the {@link LiveList}. | ||
* | ||
* @param listener the callback this called when the {@link LiveList} changes. | ||
* | ||
* @returns Unsubscribe function. | ||
* | ||
* @example | ||
* const liveList = new LiveList(); | ||
* const unsubscribe = room.subscribe(liveList, (liveList) => { }, { isDeep: true }); | ||
* unsubscribe(); | ||
*/ | ||
<TItem>(liveList: LiveList<TItem>, callback: (updates: StorageUpdate[]) => void, options: { | ||
isDeep: true; | ||
}): () => void; | ||
}; | ||
/** | ||
* Room's history contains function that let you undo and redo operation made on by the current client on the presence and storage. | ||
*/ | ||
history: { | ||
/** | ||
* Undoes the last operation executed by the current client. | ||
* It does not impact operations made by other clients. | ||
*/ | ||
undo: () => void; | ||
/** | ||
* Redoes the last operation executed by the current client. | ||
* It does not impact operations made by other clients. | ||
*/ | ||
redo: () => void; | ||
/** | ||
* All future modifications made on the Room will be merged together to create a single history item until resume is called. | ||
*/ | ||
pause: () => void; | ||
/** | ||
* Resumes history. Modifications made on the Room are not merged into a single history item anymore. | ||
*/ | ||
resume: () => void; | ||
}; | ||
/** | ||
* @deprecated use the callback returned by subscribe instead. | ||
* See v0.13 release notes for more information. | ||
* Will be removed in a future version. | ||
*/ | ||
unsubscribe: { | ||
/** | ||
* @deprecated use the callback returned by subscribe instead. | ||
* See v0.13 release notes for more information. | ||
* Will be removed in a future version. | ||
*/ | ||
<T extends Presence>(type: "my-presence", listener: MyPresenceCallback<T>): void; | ||
/** | ||
* @deprecated use the callback returned by subscribe instead. | ||
* See v0.13 release notes for more information. | ||
* Will be removed in a future version. | ||
*/ | ||
<T extends Presence>(type: "others", listener: OthersEventCallback<T>): void; | ||
/** | ||
* @deprecated use the callback returned by subscribe instead. | ||
* See v0.13 release notes for more information. | ||
* Will be removed in a future version. | ||
*/ | ||
(type: "event", listener: EventCallback): void; | ||
/** | ||
* Unsubscribe to errors thrown in the room. | ||
* @deprecated use the callback returned by subscribe instead. | ||
* See v0.13 release notes for more information. | ||
* Will be removed in a future version. | ||
*/ | ||
(type: "error", listener: ErrorCallback): void; | ||
/** | ||
* Unsubscribe to connection state updates. | ||
* @deprecated use the callback returned by subscribe instead. | ||
* See v0.13 release notes for more information. | ||
* Will be removed in a future version. | ||
*/ | ||
@@ -248,3 +371,4 @@ (type: "connection", listener: ConnectionCallback): void; | ||
* 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 overrides A partial object that contains the properties you want to update. | ||
* @param overrides Optional object to configure the behavior of updatePresence. | ||
* | ||
@@ -258,6 +382,11 @@ * @example | ||
*/ | ||
updatePresence: <T extends Presence>(overrides: Partial<T>) => void; | ||
updatePresence: <T extends Presence>(overrides: Partial<T>, options?: { | ||
/** | ||
* Whether or not the presence should have an impact on the undo/redo history. | ||
*/ | ||
addToHistory: boolean; | ||
}) => 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 | ||
* Broadcasts 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 | ||
* | ||
@@ -279,5 +408,10 @@ * @example | ||
}>; | ||
undo: () => void; | ||
redo: () => void; | ||
/** | ||
* Batches modifications made during the given function. | ||
* All the modifications are sent to other clients in a single message. | ||
* All the subscribers are called only after the batch is over. | ||
* All the modifications are merged in a single history item (undo/redo). | ||
*/ | ||
batch: (fn: () => void) => void; | ||
}; | ||
export {}; |
@@ -0,1 +1,8 @@ | ||
import { AbstractCrdt, Doc } from "./AbstractCrdt"; | ||
import { SerializedCrdtWithId } from "./live"; | ||
export declare function remove<T>(array: T[], item: T): void; | ||
export declare function isSameNodeOrChildOf(node: AbstractCrdt, parent: AbstractCrdt): boolean; | ||
export declare function deserialize(entry: SerializedCrdtWithId, parentToChildren: Map<string, SerializedCrdtWithId[]>, doc: Doc): AbstractCrdt; | ||
export declare function isCrdt(obj: any): obj is AbstractCrdt; | ||
export declare function selfOrRegisterValue(obj: AbstractCrdt): any; | ||
export declare function selfOrRegister(obj: any): AbstractCrdt; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.remove = void 0; | ||
exports.selfOrRegister = exports.selfOrRegisterValue = exports.isCrdt = exports.deserialize = exports.isSameNodeOrChildOf = exports.remove = void 0; | ||
const live_1 = require("./live"); | ||
const LiveList_1 = require("./LiveList"); | ||
const LiveMap_1 = require("./LiveMap"); | ||
const LiveObject_1 = require("./LiveObject"); | ||
const LiveRegister_1 = require("./LiveRegister"); | ||
function remove(array, item) { | ||
@@ -13,1 +18,59 @@ for (let i = 0; i < array.length; i++) { | ||
exports.remove = remove; | ||
function isSameNodeOrChildOf(node, parent) { | ||
if (node === parent) { | ||
return true; | ||
} | ||
if (node._parent) { | ||
return isSameNodeOrChildOf(node._parent, parent); | ||
} | ||
return false; | ||
} | ||
exports.isSameNodeOrChildOf = isSameNodeOrChildOf; | ||
function deserialize(entry, parentToChildren, doc) { | ||
switch (entry[1].type) { | ||
case live_1.CrdtType.Object: { | ||
return LiveObject_1.LiveObject._deserialize(entry, parentToChildren, doc); | ||
} | ||
case live_1.CrdtType.List: { | ||
return LiveList_1.LiveList._deserialize(entry, parentToChildren, doc); | ||
} | ||
case live_1.CrdtType.Map: { | ||
return LiveMap_1.LiveMap._deserialize(entry, parentToChildren, doc); | ||
} | ||
case live_1.CrdtType.Register: { | ||
return LiveRegister_1.LiveRegister._deserialize(entry, parentToChildren, doc); | ||
} | ||
default: { | ||
throw new Error("Unexpected CRDT type"); | ||
} | ||
} | ||
} | ||
exports.deserialize = deserialize; | ||
function isCrdt(obj) { | ||
return (obj instanceof LiveObject_1.LiveObject || | ||
obj instanceof LiveMap_1.LiveMap || | ||
obj instanceof LiveList_1.LiveList || | ||
obj instanceof LiveRegister_1.LiveRegister); | ||
} | ||
exports.isCrdt = isCrdt; | ||
function selfOrRegisterValue(obj) { | ||
if (obj instanceof LiveRegister_1.LiveRegister) { | ||
return obj.data; | ||
} | ||
return obj; | ||
} | ||
exports.selfOrRegisterValue = selfOrRegisterValue; | ||
function selfOrRegister(obj) { | ||
if (obj instanceof LiveObject_1.LiveObject || | ||
obj instanceof LiveMap_1.LiveMap || | ||
obj instanceof LiveList_1.LiveList) { | ||
return obj; | ||
} | ||
else if (obj instanceof LiveRegister_1.LiveRegister) { | ||
throw new Error("Internal error. LiveRegister should not be created from selfOrRegister"); | ||
} | ||
else { | ||
return new LiveRegister_1.LiveRegister(obj); | ||
} | ||
} | ||
exports.selfOrRegister = selfOrRegister; |
@@ -1,3 +0,5 @@ | ||
export { LiveObject, LiveList, LiveMap } from "./doc"; | ||
export { LiveObject } from "./LiveObject"; | ||
export { LiveMap } from "./LiveMap"; | ||
export { LiveList } from "./LiveList"; | ||
export type { Others, Presence, Room, Client, User } from "./types"; | ||
export { createClient } from "./client"; |
@@ -1,2 +0,4 @@ | ||
export { LiveObject, LiveList, LiveMap } from "./doc"; | ||
export { LiveObject } from "./LiveObject"; | ||
export { LiveMap } from "./LiveMap"; | ||
export { LiveList } from "./LiveList"; | ||
export { createClient } from "./client"; |
@@ -1,3 +0,11 @@ | ||
import { Others, Presence, ClientOptions, Room, MyPresenceCallback, OthersEventCallback, AuthEndpoint, EventCallback, User, Connection, ErrorCallback, AuthenticationToken, ConnectionCallback } from "./types"; | ||
import { Others, Presence, ClientOptions, Room, MyPresenceCallback, OthersEventCallback, AuthEndpoint, EventCallback, User, Connection, ErrorCallback, AuthenticationToken, ConnectionCallback, StorageCallback, StorageUpdate } from "./types"; | ||
import { ClientMessage, Op } from "./live"; | ||
import { LiveMap } from "./LiveMap"; | ||
import { LiveObject } from "./LiveObject"; | ||
import { LiveList } from "./LiveList"; | ||
import { AbstractCrdt } from "./AbstractCrdt"; | ||
declare type HistoryItem = Array<Op | { | ||
type: "presence"; | ||
data: Presence; | ||
}>; | ||
declare type IdFactory = () => string; | ||
@@ -8,3 +16,3 @@ export declare type State = { | ||
lastFlushTime: number; | ||
flushData: { | ||
buffer: { | ||
presence: Presence | null; | ||
@@ -28,2 +36,3 @@ messages: ClientMessage[]; | ||
connection: ConnectionCallback[]; | ||
storage: StorageCallback[]; | ||
}; | ||
@@ -40,2 +49,20 @@ me: Presence; | ||
}; | ||
clock: number; | ||
opClock: number; | ||
items: Map<string, AbstractCrdt>; | ||
root: LiveObject | undefined; | ||
undoStack: HistoryItem[]; | ||
redoStack: HistoryItem[]; | ||
isHistoryPaused: boolean; | ||
pausedHistory: HistoryItem; | ||
isBatching: boolean; | ||
batch: { | ||
ops: Op[]; | ||
reverseOps: HistoryItem; | ||
updates: { | ||
others: []; | ||
presence: boolean; | ||
nodes: Set<AbstractCrdt>; | ||
}; | ||
}; | ||
}; | ||
@@ -69,10 +96,19 @@ export declare type Effects = { | ||
onVisibilityChange: (visibilityState: VisibilityState) => void; | ||
getUndoStack: () => HistoryItem[]; | ||
getItemsCount: () => number; | ||
connect: () => null | undefined; | ||
disconnect: () => void; | ||
subscribe: { | ||
<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; | ||
(type: "error", listener: ErrorCallback): void; | ||
(type: "connection", listener: ConnectionCallback): void; | ||
(callback: (updates: StorageUpdate) => void): () => void; | ||
<TKey extends string, TValue>(liveMap: LiveMap<TKey, TValue>, callback: (liveMap: LiveMap<TKey, TValue>) => void): () => void; | ||
<TData>(liveObject: LiveObject<TData>, callback: (liveObject: LiveObject<TData>) => void): () => void; | ||
<TItem>(liveList: LiveList<TItem>, callback: (liveList: LiveList<TItem>) => void): () => void; | ||
<TItem_1 extends AbstractCrdt>(node: TItem_1, callback: (updates: StorageUpdate[]) => void, options: { | ||
isDeep: true; | ||
}): () => 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; | ||
(type: "error", listener: ErrorCallback): () => void; | ||
(type: "connection", listener: ConnectionCallback): () => void; | ||
}; | ||
@@ -86,8 +122,13 @@ unsubscribe: { | ||
}; | ||
updatePresence: <T_4 extends Presence>(overrides: Partial<T_4>) => void; | ||
updatePresence: <T_4 extends Presence>(overrides: Partial<T_4>, options?: { | ||
addToHistory: boolean; | ||
} | undefined) => void; | ||
broadcastEvent: (event: any) => void; | ||
batch: (callback: () => void) => void; | ||
undo: () => void; | ||
redo: () => void; | ||
pauseHistory: () => void; | ||
resumeHistory: () => void; | ||
getStorage: <TRoot>() => Promise<{ | ||
root: import("./doc").LiveObject<TRoot>; | ||
root: LiveObject<TRoot>; | ||
}>; | ||
@@ -94,0 +135,0 @@ selectors: { |
@@ -10,6 +10,10 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
}; | ||
import { remove } from "./utils"; | ||
import { isSameNodeOrChildOf, remove } from "./utils"; | ||
import auth, { parseToken } from "./authentication"; | ||
import { ClientMessageType, ServerMessageType, } from "./live"; | ||
import Storage from "./storage"; | ||
import { ClientMessageType, ServerMessageType, OpType, } from "./live"; | ||
import { LiveMap } from "./LiveMap"; | ||
import { LiveObject } from "./LiveObject"; | ||
import { LiveList } from "./LiveList"; | ||
import { AbstractCrdt } from "./AbstractCrdt"; | ||
import { LiveRegister } from "./LiveRegister"; | ||
const BACKOFF_RETRY_DELAYS = [250, 500, 1000, 2000, 4000, 8000, 10000]; | ||
@@ -86,9 +90,271 @@ const HEARTBEAT_INTERVAL = 30000; | ||
}; | ||
function subscribe(type, listener) { | ||
if (!isValidRoomEventType(type)) { | ||
throw new Error(`"${type}" is not a valid event name`); | ||
function genericSubscribe(callback) { | ||
state.listeners.storage.push(callback); | ||
return () => remove(state.listeners.storage, callback); | ||
} | ||
function crdtSubscribe(crdt, innerCallback, options) { | ||
const cb = (updates) => { | ||
const relatedUpdates = []; | ||
for (const update of updates) { | ||
if ((options === null || options === void 0 ? void 0 : options.isDeep) && isSameNodeOrChildOf(update.node, crdt)) { | ||
relatedUpdates.push(update); | ||
} | ||
else if (update.node._id === crdt._id) { | ||
innerCallback(update.node); | ||
} | ||
} | ||
if ((options === null || options === void 0 ? void 0 : options.isDeep) && relatedUpdates.length > 0) { | ||
innerCallback(relatedUpdates); | ||
} | ||
}; | ||
return genericSubscribe(cb); | ||
} | ||
function createRootFromMessage(message) { | ||
state.root = load(message.items); | ||
for (const key in state.defaultStorageRoot) { | ||
if (state.root.get(key) == null) { | ||
state.root.set(key, state.defaultStorageRoot[key]); | ||
} | ||
} | ||
state.listeners[type].push(listener); | ||
} | ||
function load(items) { | ||
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"); | ||
} | ||
return LiveObject._deserialize(root, parentToChildren, { | ||
addItem, | ||
deleteItem, | ||
generateId, | ||
generateOpId, | ||
dispatch: storageDispatch, | ||
}); | ||
} | ||
function addItem(id, item) { | ||
state.items.set(id, item); | ||
} | ||
function deleteItem(id) { | ||
state.items.delete(id); | ||
} | ||
function getItem(id) { | ||
return state.items.get(id); | ||
} | ||
function addToUndoStack(historyItem) { | ||
// If undo stack is too large, we remove the older item | ||
if (state.undoStack.length >= 50) { | ||
state.undoStack.shift(); | ||
} | ||
if (state.isHistoryPaused) { | ||
state.pausedHistory.unshift(...historyItem); | ||
} | ||
else { | ||
state.undoStack.push(historyItem); | ||
} | ||
} | ||
function storageDispatch(ops, reverse, modified) { | ||
if (state.isBatching) { | ||
state.batch.ops.push(...ops); | ||
for (const item of modified) { | ||
state.batch.updates.nodes.add(item); | ||
} | ||
state.batch.reverseOps.push(...reverse); | ||
} | ||
else { | ||
addToUndoStack(reverse); | ||
state.redoStack = []; | ||
dispatch(ops); | ||
notify({ nodes: new Set(modified) }); | ||
} | ||
} | ||
function notify({ nodes = new Set(), presence = false, others = [], }) { | ||
if (others.length > 0) { | ||
state.others = makeOthers(state.users); | ||
for (const event of others) { | ||
for (const listener of state.listeners["others"]) { | ||
listener(state.others, event); | ||
} | ||
} | ||
} | ||
if (presence) { | ||
for (const listener of state.listeners["my-presence"]) { | ||
listener(state.me); | ||
} | ||
} | ||
if (nodes.size > 0) { | ||
for (const subscriber of state.listeners.storage) { | ||
subscriber(Array.from(nodes).map((m) => { | ||
if (m instanceof LiveObject) { | ||
return { | ||
type: "LiveObject", | ||
node: m, | ||
}; | ||
} | ||
else if (m instanceof LiveList) { | ||
return { | ||
type: "LiveList", | ||
node: m, | ||
}; | ||
} | ||
else { | ||
return { | ||
type: "LiveMap", | ||
node: m, | ||
}; | ||
} | ||
})); | ||
} | ||
} | ||
} | ||
function getConnectionId() { | ||
if (state.connection.state === "open" || | ||
state.connection.state === "connecting") { | ||
return state.connection.id; | ||
} | ||
throw new Error("Internal. Tried to get connection id but connection is not open"); | ||
} | ||
function generateId() { | ||
return `${getConnectionId()}:${state.clock++}`; | ||
} | ||
function generateOpId() { | ||
return `${getConnectionId()}:${state.opClock++}`; | ||
} | ||
function apply(item) { | ||
const result = { | ||
reverse: [], | ||
updates: { nodes: new Set(), presence: false }, | ||
}; | ||
for (const op of item) { | ||
if (op.type === "presence") { | ||
const reverse = { | ||
type: "presence", | ||
data: {}, | ||
}; | ||
for (const key in op.data) { | ||
reverse.data[key] = state.me[key]; | ||
} | ||
state.me = Object.assign(Object.assign({}, state.me), op.data); | ||
if (state.buffer.presence == null) { | ||
state.buffer.presence = op.data; | ||
} | ||
else { | ||
for (const key in op.data) { | ||
state.buffer.presence[key] = op.data; | ||
} | ||
} | ||
result.reverse.unshift(reverse); | ||
result.updates.presence = true; | ||
} | ||
else { | ||
const applyOpResult = applyOp(op); | ||
if (applyOpResult.modified) { | ||
result.updates.nodes.add(applyOpResult.modified); | ||
result.reverse.unshift(...applyOpResult.reverse); | ||
} | ||
} | ||
} | ||
return result; | ||
} | ||
function applyOp(op) { | ||
switch (op.type) { | ||
case OpType.DeleteObjectKey: | ||
case OpType.UpdateObject: | ||
case OpType.DeleteCrdt: { | ||
const item = state.items.get(op.id); | ||
if (item == null) { | ||
return { modified: false }; | ||
} | ||
return item._apply(op); | ||
} | ||
case OpType.SetParentKey: { | ||
const item = state.items.get(op.id); | ||
if (item == null) { | ||
return { modified: false }; | ||
} | ||
if (item._parent instanceof LiveList) { | ||
const previousKey = item._parentKey; | ||
item._parent._setChildKey(op.parentKey, item); | ||
return { | ||
reverse: [ | ||
{ | ||
type: OpType.SetParentKey, | ||
id: item._id, | ||
parentKey: previousKey, | ||
}, | ||
], | ||
modified: item._parent, | ||
}; | ||
} | ||
return { modified: false }; | ||
} | ||
case OpType.CreateObject: { | ||
const parent = state.items.get(op.parentId); | ||
if (parent == null || getItem(op.id) != null) { | ||
return { modified: false }; | ||
} | ||
return parent._attachChild(op.id, op.parentKey, new LiveObject(op.data)); | ||
} | ||
case OpType.CreateList: { | ||
const parent = state.items.get(op.parentId); | ||
if (parent == null || getItem(op.id) != null) { | ||
return { modified: false }; | ||
} | ||
return parent._attachChild(op.id, op.parentKey, new LiveList()); | ||
} | ||
case OpType.CreateRegister: { | ||
const parent = state.items.get(op.parentId); | ||
if (parent == null || getItem(op.id) != null) { | ||
return { modified: false }; | ||
} | ||
return parent._attachChild(op.id, op.parentKey, new LiveRegister(op.data)); | ||
} | ||
case OpType.CreateMap: { | ||
const parent = state.items.get(op.parentId); | ||
if (parent == null || getItem(op.id) != null) { | ||
return { modified: false }; | ||
} | ||
return parent._attachChild(op.id, op.parentKey, new LiveMap()); | ||
} | ||
} | ||
return { modified: false }; | ||
} | ||
function subscribe(firstParam, listener, options) { | ||
if (firstParam instanceof AbstractCrdt) { | ||
return crdtSubscribe(firstParam, listener, options); | ||
} | ||
else if (typeof firstParam === "function") { | ||
return genericSubscribe(firstParam); | ||
} | ||
else if (!isValidRoomEventType(firstParam)) { | ||
throw new Error(`"${firstParam}" is not a valid event name`); | ||
} | ||
state.listeners[firstParam].push(listener); | ||
return () => { | ||
const callbacks = state.listeners[firstParam]; | ||
remove(callbacks, listener); | ||
}; | ||
} | ||
function unsubscribe(event, callback) { | ||
console.warn(`unsubscribe is depreacted and will be removed in a future version. | ||
use the callback returned by subscribe instead. | ||
See v0.13 release notes for more information. | ||
`); | ||
if (!isValidRoomEventType(event)) { | ||
@@ -125,17 +391,25 @@ throw new Error(`"${event}" is not a valid event name`); | ||
} | ||
function updatePresence(overrides) { | ||
const newPresence = Object.assign(Object.assign({}, state.me), overrides); | ||
if (state.flushData.presence == null) { | ||
state.flushData.presence = overrides; | ||
function updatePresence(overrides, options) { | ||
const oldValues = {}; | ||
if (state.buffer.presence == null) { | ||
state.buffer.presence = {}; | ||
} | ||
for (const key in overrides) { | ||
state.buffer.presence[key] = overrides[key]; | ||
oldValues[key] = state.me[key]; | ||
} | ||
state.me = Object.assign(Object.assign({}, state.me), overrides); | ||
if (state.isBatching) { | ||
if (options === null || options === void 0 ? void 0 : options.addToHistory) { | ||
state.batch.reverseOps.push({ type: "presence", data: oldValues }); | ||
} | ||
state.batch.updates.presence = true; | ||
} | ||
else { | ||
for (const key in overrides) { | ||
state.flushData.presence[key] = overrides[key]; | ||
tryFlushing(); | ||
if (options === null || options === void 0 ? void 0 : options.addToHistory) { | ||
addToUndoStack([{ type: "presence", data: oldValues }]); | ||
} | ||
notify({ presence: true }); | ||
} | ||
state.me = newPresence; | ||
tryFlushing(); | ||
for (const listener of state.listeners["my-presence"]) { | ||
listener(state.me); | ||
} | ||
} | ||
@@ -180,14 +454,8 @@ function authenticationSuccess(token, socket) { | ||
} | ||
updateUsers({ | ||
return { | ||
type: "update", | ||
updates: message.data, | ||
user: state.users[message.actor], | ||
}); | ||
}; | ||
} | ||
function updateUsers(event) { | ||
state.others = makeOthers(state.users); | ||
for (const listener of state.listeners["others"]) { | ||
listener(state.others, event); | ||
} | ||
} | ||
function onUserLeftMessage(message) { | ||
@@ -198,4 +466,5 @@ const userLeftMessage = message; | ||
delete state.users[userLeftMessage.actor]; | ||
updateUsers({ type: "leave", user }); | ||
return { type: "leave", user }; | ||
} | ||
return null; | ||
} | ||
@@ -214,3 +483,3 @@ function onRoomStateMessage(message) { | ||
state.users = newUsers; | ||
updateUsers({ type: "reset" }); | ||
return { type: "reset" }; | ||
} | ||
@@ -234,7 +503,6 @@ function onNavigatorOnline() { | ||
}; | ||
updateUsers({ type: "enter", user: state.users[message.actor] }); | ||
if (state.me) { | ||
// Send current presence to new user | ||
// TODO: Consider storing it on the backend | ||
state.flushData.messages.push({ | ||
state.buffer.messages.push({ | ||
type: ClientMessageType.UpdatePresence, | ||
@@ -246,2 +514,3 @@ data: state.me, | ||
} | ||
return { type: "enter", user: state.users[message.actor] }; | ||
} | ||
@@ -254,25 +523,53 @@ function onMessage(event) { | ||
const message = JSON.parse(event.data); | ||
switch (message.type) { | ||
case ServerMessageType.UserJoined: { | ||
onUserJoinedMessage(message); | ||
break; | ||
let subMessages = []; | ||
if (Array.isArray(message)) { | ||
subMessages = message; | ||
} | ||
else { | ||
subMessages.push(message); | ||
} | ||
const updates = { | ||
nodes: new Set(), | ||
others: [], | ||
}; | ||
for (const subMessage of subMessages) { | ||
switch (subMessage.type) { | ||
case ServerMessageType.UserJoined: { | ||
updates.others.push(onUserJoinedMessage(message)); | ||
break; | ||
} | ||
case ServerMessageType.UpdatePresence: { | ||
updates.others.push(onUpdatePresenceMessage(subMessage)); | ||
break; | ||
} | ||
case ServerMessageType.Event: { | ||
onEvent(subMessage); | ||
break; | ||
} | ||
case ServerMessageType.UserLeft: { | ||
const event = onUserLeftMessage(subMessage); | ||
if (event) { | ||
updates.others.push(event); | ||
} | ||
break; | ||
} | ||
case ServerMessageType.RoomState: { | ||
updates.others.push(onRoomStateMessage(subMessage)); | ||
break; | ||
} | ||
case ServerMessageType.InitialStorageState: { | ||
createRootFromMessage(subMessage); | ||
_getInitialStateResolver === null || _getInitialStateResolver === void 0 ? void 0 : _getInitialStateResolver(); | ||
break; | ||
} | ||
case ServerMessageType.UpdateStorage: { | ||
const applyResult = apply(subMessage.ops); | ||
for (const node of applyResult.updates.nodes) { | ||
updates.nodes.add(node); | ||
} | ||
break; | ||
} | ||
} | ||
case ServerMessageType.UpdatePresence: { | ||
onUpdatePresenceMessage(message); | ||
break; | ||
} | ||
case ServerMessageType.Event: { | ||
onEvent(message); | ||
break; | ||
} | ||
case ServerMessageType.UserLeft: { | ||
onUserLeftMessage(message); | ||
break; | ||
} | ||
case ServerMessageType.RoomState: { | ||
onRoomStateMessage(message); | ||
break; | ||
} | ||
} | ||
storage.onMessage(message); | ||
notify(updates); | ||
} | ||
@@ -297,3 +594,3 @@ // function onWakeUp() { | ||
state.users = {}; | ||
updateUsers({ type: "reset" }); | ||
notify({ others: [{ type: "reset" }] }); | ||
if (event.code >= 4000 && event.code <= 4100) { | ||
@@ -387,3 +684,3 @@ updateConnection({ state: "failed" }); | ||
effects.send(messages); | ||
state.flushData = { | ||
state.buffer = { | ||
messages: [], | ||
@@ -404,15 +701,15 @@ storageOperations: [], | ||
const messages = []; | ||
if (state.flushData.presence) { | ||
if (state.buffer.presence) { | ||
messages.push({ | ||
type: ClientMessageType.UpdatePresence, | ||
data: state.flushData.presence, | ||
data: state.buffer.presence, | ||
}); | ||
} | ||
for (const event of state.flushData.messages) { | ||
for (const event of state.buffer.messages) { | ||
messages.push(event); | ||
} | ||
if (state.flushData.storageOperations.length > 0) { | ||
if (state.buffer.storageOperations.length > 0) { | ||
messages.push({ | ||
type: ClientMessageType.UpdateStorage, | ||
ops: state.flushData.storageOperations, | ||
ops: state.buffer.storageOperations, | ||
}); | ||
@@ -439,3 +736,3 @@ } | ||
state.users = {}; | ||
updateUsers({ type: "reset" }); | ||
notify({ others: [{ type: "reset" }] }); | ||
clearListeners(); | ||
@@ -458,3 +755,3 @@ } | ||
} | ||
state.flushData.messages.push({ | ||
state.buffer.messages.push({ | ||
type: ClientMessageType.ClientEvent, | ||
@@ -466,25 +763,22 @@ event, | ||
function dispatch(ops) { | ||
state.flushData.storageOperations.push(...ops); | ||
state.buffer.storageOperations.push(...ops); | ||
tryFlushing(); | ||
} | ||
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, | ||
}); | ||
let _getInitialStatePromise = null; | ||
let _getInitialStateResolver = null; | ||
function getStorage() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const doc = yield storage.getDocument(); | ||
if (state.root) { | ||
return { | ||
root: state.root, | ||
}; | ||
} | ||
if (_getInitialStatePromise == null) { | ||
state.buffer.messages.push({ type: ClientMessageType.FetchStorage }); | ||
tryFlushing(); | ||
_getInitialStatePromise = new Promise((resolve) => (_getInitialStateResolver = resolve)); | ||
} | ||
yield _getInitialStatePromise; | ||
return { | ||
root: doc.root, | ||
root: state.root, | ||
}; | ||
@@ -494,7 +788,77 @@ }); | ||
function undo() { | ||
storage.undo(); | ||
if (state.isBatching) { | ||
throw new Error("undo is not allowed during a batch"); | ||
} | ||
const historyItem = state.undoStack.pop(); | ||
if (historyItem == null) { | ||
return; | ||
} | ||
state.isHistoryPaused = false; | ||
const result = apply(historyItem); | ||
notify(result.updates); | ||
state.redoStack.push(result.reverse); | ||
for (const op of historyItem) { | ||
if (op.type !== "presence") { | ||
state.buffer.storageOperations.push(op); | ||
} | ||
} | ||
tryFlushing(); | ||
} | ||
function redo() { | ||
storage.redo(); | ||
if (state.isBatching) { | ||
throw new Error("redo is not allowed during a batch"); | ||
} | ||
const historyItem = state.redoStack.pop(); | ||
if (historyItem == null) { | ||
return; | ||
} | ||
state.isHistoryPaused = false; | ||
const result = apply(historyItem); | ||
notify(result.updates); | ||
state.undoStack.push(result.reverse); | ||
for (const op of historyItem) { | ||
if (op.type !== "presence") { | ||
state.buffer.storageOperations.push(op); | ||
} | ||
} | ||
tryFlushing(); | ||
} | ||
function batch(callback) { | ||
if (state.isBatching) { | ||
throw new Error("batch should not be called during a batch"); | ||
} | ||
state.isBatching = true; | ||
try { | ||
callback(); | ||
} | ||
finally { | ||
state.isBatching = false; | ||
addToUndoStack(state.batch.reverseOps); | ||
// Clear the redo stack because batch is always called from a local operation | ||
state.redoStack = []; | ||
dispatch(state.batch.ops); | ||
notify(state.batch.updates); | ||
state.batch = { | ||
ops: [], | ||
reverseOps: [], | ||
updates: { | ||
others: [], | ||
nodes: new Set(), | ||
presence: false, | ||
}, | ||
}; | ||
tryFlushing(); | ||
} | ||
} | ||
function pauseHistory() { | ||
state.pausedHistory = []; | ||
state.isHistoryPaused = true; | ||
} | ||
function resumeHistory() { | ||
state.isHistoryPaused = false; | ||
if (state.pausedHistory.length > 0) { | ||
addToUndoStack(state.pausedHistory); | ||
} | ||
state.pausedHistory = []; | ||
} | ||
return { | ||
@@ -510,2 +874,4 @@ // Internal | ||
onVisibilityChange, | ||
getUndoStack: () => state.undoStack, | ||
getItemsCount: () => state.items.size, | ||
// Core | ||
@@ -519,4 +885,7 @@ connect, | ||
broadcastEvent, | ||
batch, | ||
undo, | ||
redo, | ||
pauseHistory, | ||
resumeHistory, | ||
getStorage, | ||
@@ -543,2 +912,3 @@ selectors: { | ||
connection: [], | ||
storage: [], | ||
}, | ||
@@ -552,3 +922,3 @@ numberOfRetry: 0, | ||
}, | ||
flushData: { | ||
buffer: { | ||
presence: me == null ? {} : me, | ||
@@ -566,2 +936,17 @@ messages: [], | ||
idFactory: null, | ||
// Storage | ||
clock: 0, | ||
opClock: 0, | ||
items: new Map(), | ||
root: undefined, | ||
undoStack: [], | ||
redoStack: [], | ||
isHistoryPaused: false, | ||
pausedHistory: [], | ||
isBatching: false, | ||
batch: { | ||
ops: [], | ||
updates: { nodes: new Set(), presence: false, others: [] }, | ||
reverseOps: [], | ||
}, | ||
}; | ||
@@ -571,3 +956,3 @@ } | ||
const throttleDelay = options.throttle || 100; | ||
const liveblocksServer = options.liveblocksServer || "wss://liveblocks.net/v4"; | ||
const liveblocksServer = options.liveblocksServer || "wss://liveblocks.net/v5"; | ||
let authEndpoint; | ||
@@ -606,4 +991,9 @@ if (options.authEndpoint) { | ||
getStorage: machine.getStorage, | ||
undo: machine.undo, | ||
redo: machine.redo, | ||
batch: machine.batch, | ||
history: { | ||
undo: machine.undo, | ||
redo: machine.redo, | ||
pause: machine.pauseHistory, | ||
resume: machine.resumeHistory, | ||
}, | ||
}; | ||
@@ -610,0 +1000,0 @@ return { |
@@ -1,2 +0,4 @@ | ||
import { LiveObject } from "./doc"; | ||
import type { LiveList } from "./LiveList"; | ||
import type { LiveMap } from "./LiveMap"; | ||
import type { LiveObject } from "./LiveObject"; | ||
export declare type MyPresenceCallback<T extends Presence = Presence> = (me: T) => void; | ||
@@ -17,2 +19,16 @@ export declare type OthersEventCallback<T extends Presence = Presence> = (others: Others<T>, event: OthersEvent<T>) => void; | ||
}; | ||
export declare type LiveMapUpdates<TKey extends string = string, TValue = any> = { | ||
type: "LiveMap"; | ||
node: LiveMap<TKey, TValue>; | ||
}; | ||
export declare type LiveObjectUpdates<TData = any> = { | ||
type: "LiveObject"; | ||
node: LiveObject<TData>; | ||
}; | ||
export declare type LiveListUpdates<TItem = any> = { | ||
type: "LiveList"; | ||
node: LiveList<TItem>; | ||
}; | ||
export declare type StorageUpdate = LiveMapUpdates | LiveObjectUpdates | LiveListUpdates; | ||
export declare type StorageCallback = (updates: StorageUpdate[]) => void; | ||
export declare type Client = { | ||
@@ -22,3 +38,3 @@ /** | ||
* | ||
* @param roomId - The id of the room | ||
* @param roomId The id of the room | ||
*/ | ||
@@ -28,4 +44,4 @@ getRoom(roomId: string): Room | null; | ||
* Enters a room and returns it. | ||
* @param roomId - The id of the room | ||
* @param defaultPresence - Optional. Should be serializable to JSON. If omitted, an empty object will be used. | ||
* @param roomId The id of the room | ||
* @param defaultPresence Optional. Should be serializable to JSON. If omitted, an empty object will be used. | ||
*/ | ||
@@ -38,3 +54,3 @@ enter<TStorageRoot extends Record<string, any> = Record<string, any>>(roomId: string, options?: { | ||
* Leaves a room. | ||
* @param roomId - The id of the room | ||
* @param roomId The id of the room | ||
*/ | ||
@@ -138,3 +154,3 @@ leave(roomId: string): void; | ||
* | ||
* @param listener - the callback that is called every time the current user presence is updated with {@link Room.updatePresence}. | ||
* @param listener the callback that is called every time the current user presence is updated with {@link Room.updatePresence}. | ||
* | ||
@@ -146,7 +162,7 @@ * @example | ||
*/ | ||
<T extends Presence>(type: "my-presence", listener: MyPresenceCallback<T>): void; | ||
<T extends Presence>(type: "my-presence", listener: MyPresenceCallback<T>): () => void; | ||
/** | ||
* Subscribe to the other users updates. | ||
* | ||
* @param listener - the callback that is called when a user enters or leaves the room or when a user update its presence. | ||
* @param listener the callback that is called when a user enters or leaves the room or when a user update its presence. | ||
* | ||
@@ -158,7 +174,7 @@ * @example | ||
*/ | ||
<T extends Presence>(type: "others", listener: OthersEventCallback<T>): void; | ||
<T extends Presence>(type: "others", listener: OthersEventCallback<T>): () => void; | ||
/** | ||
* Subscribe to events broadcasted by {@link Room.broadcastEvent} | ||
* | ||
* @param listener - the callback that is called when a user calls {@link Room.broadcastEvent} | ||
* @param listener the callback that is called when a user calls {@link Room.broadcastEvent} | ||
* | ||
@@ -170,52 +186,159 @@ * @example | ||
*/ | ||
(type: "event", listener: EventCallback): void; | ||
(type: "event", listener: EventCallback): () => void; | ||
/** | ||
* Subscribe to errors thrown in the room. | ||
*/ | ||
(type: "error", listener: ErrorCallback): void; | ||
(type: "error", listener: ErrorCallback): () => void; | ||
/** | ||
* Subscribe to connection state updates. | ||
*/ | ||
(type: "connection", listener: ConnectionCallback): void; | ||
}; | ||
unsubscribe: { | ||
(type: "connection", listener: ConnectionCallback): () => void; | ||
/** | ||
* Unsubscribe to the current user presence updates. | ||
* Subscribes to changes made on a {@link LiveMap}. Returns an unsubscribe function. | ||
* In a future version, we will also expose what exactly changed in the {@link LiveMap}. | ||
* | ||
* @param listener - the callback that has been used with {@link Room.subscribe}("my-presence"). | ||
* @param listener the callback this called when the {@link LiveMap} changes. | ||
* | ||
* @returns Unsubscribe function. | ||
* | ||
* @example | ||
* const onPresenceChange = (presence) => { }; | ||
* room.subscribe("my-presence", onPresenceChange); | ||
* room.unsubscribe("my-presence", onPresenceChange); | ||
* const liveMap = new LiveMap(); | ||
* const unsubscribe = room.subscribe(liveMap, (liveMap) => { }); | ||
* unsubscribe(); | ||
*/ | ||
<T extends Presence>(type: "my-presence", listener: MyPresenceCallback<T>): void; | ||
<TKey extends string, TValue>(liveMap: LiveMap<TKey, TValue>, listener: (liveMap: LiveMap<TKey, TValue>) => void): () => void; | ||
/** | ||
* Unsubscribe to the other users updates. | ||
* Subscribes to changes made on a {@link LiveObject}. Returns an unsubscribe function. | ||
* In a future version, we will also expose what exactly changed in the {@link LiveObject}. | ||
* | ||
* @param listener - the callback that has been used with {@link Room.subscribe}("others"). | ||
* @param listener the callback this called when the {@link LiveObject} changes. | ||
* | ||
* @returns Unsubscribe function. | ||
* | ||
* @example | ||
* const onOthersChange = (presence) => { }; | ||
* room.subscribe("others", onOthersChange); | ||
* room.unsubscribe("others", onOthersChange); | ||
* const liveObject = new LiveObject(); | ||
* const unsubscribe = room.subscribe(liveObject, (liveObject) => { }); | ||
* unsubscribe(); | ||
*/ | ||
<T extends Presence>(type: "others", listener: OthersEventCallback<T>): void; | ||
<TData>(liveObject: LiveObject<TData>, callback: (liveObject: LiveObject<TData>) => void): () => void; | ||
/** | ||
* Unsubscribe to events broadcasted by {@link Room.broadcastEvent} | ||
* Subscribes to changes made on a {@link LiveList}. Returns an unsubscribe function. | ||
* In a future version, we will also expose what exactly changed in the {@link LiveList}. | ||
* | ||
* @param listener - the callback that has been used with {@link Room.unsubscribe}("event"). | ||
* @param listener the callback this called when the {@link LiveList} changes. | ||
* | ||
* @returns Unsubscribe function. | ||
* | ||
* @example | ||
* const onEvent = ({ event, connectionId }) => { }; | ||
* room.subscribe("event", onEvent); | ||
* room.unsubscribe("event", onEvent); | ||
* const liveList = new LiveList(); | ||
* const unsubscribe = room.subscribe(liveList, (liveList) => { }); | ||
* unsubscribe(); | ||
*/ | ||
<TItem>(liveList: LiveList<TItem>, callback: (liveList: LiveList<TItem>) => void): () => void; | ||
/** | ||
* Subscribes to changes made on a {@link LiveMap} and all the nested data structures. Returns an unsubscribe function. | ||
* In a future version, we will also expose what exactly changed in the {@link LiveMap}. | ||
* | ||
* @param listener the callback this called when the {@link LiveMap} changes. | ||
* | ||
* @returns Unsubscribe function. | ||
* | ||
* @example | ||
* const liveMap = new LiveMap(); | ||
* const unsubscribe = room.subscribe(liveMap, (liveMap) => { }, { isDeep: true }); | ||
* unsubscribe(); | ||
*/ | ||
<TKey extends string, TValue>(liveMap: LiveMap<TKey, TValue>, callback: (updates: StorageUpdate[]) => void, options: { | ||
isDeep: true; | ||
}): () => void; | ||
/** | ||
* Subscribes to changes made on a {@link LiveObject} and all the nested data structures. Returns an unsubscribe function. | ||
* In a future version, we will also expose what exactly changed in the {@link LiveObject}. | ||
* | ||
* @param listener the callback this called when the {@link LiveObject} changes. | ||
* | ||
* @returns Unsubscribe function. | ||
* | ||
* @example | ||
* const liveObject = new LiveObject(); | ||
* const unsubscribe = room.subscribe(liveObject, (liveObject) => { }, { isDeep: true }); | ||
* unsubscribe(); | ||
*/ | ||
<TData>(liveObject: LiveObject<TData>, callback: (updates: StorageUpdate[]) => void, options: { | ||
isDeep: true; | ||
}): () => void; | ||
/** | ||
* Subscribes to changes made on a {@link LiveList} and all the nested data structures. Returns an unsubscribe function. | ||
* In a future version, we will also expose what exactly changed in the {@link LiveList}. | ||
* | ||
* @param listener the callback this called when the {@link LiveList} changes. | ||
* | ||
* @returns Unsubscribe function. | ||
* | ||
* @example | ||
* const liveList = new LiveList(); | ||
* const unsubscribe = room.subscribe(liveList, (liveList) => { }, { isDeep: true }); | ||
* unsubscribe(); | ||
*/ | ||
<TItem>(liveList: LiveList<TItem>, callback: (updates: StorageUpdate[]) => void, options: { | ||
isDeep: true; | ||
}): () => void; | ||
}; | ||
/** | ||
* Room's history contains function that let you undo and redo operation made on by the current client on the presence and storage. | ||
*/ | ||
history: { | ||
/** | ||
* Undoes the last operation executed by the current client. | ||
* It does not impact operations made by other clients. | ||
*/ | ||
undo: () => void; | ||
/** | ||
* Redoes the last operation executed by the current client. | ||
* It does not impact operations made by other clients. | ||
*/ | ||
redo: () => void; | ||
/** | ||
* All future modifications made on the Room will be merged together to create a single history item until resume is called. | ||
*/ | ||
pause: () => void; | ||
/** | ||
* Resumes history. Modifications made on the Room are not merged into a single history item anymore. | ||
*/ | ||
resume: () => void; | ||
}; | ||
/** | ||
* @deprecated use the callback returned by subscribe instead. | ||
* See v0.13 release notes for more information. | ||
* Will be removed in a future version. | ||
*/ | ||
unsubscribe: { | ||
/** | ||
* @deprecated use the callback returned by subscribe instead. | ||
* See v0.13 release notes for more information. | ||
* Will be removed in a future version. | ||
*/ | ||
<T extends Presence>(type: "my-presence", listener: MyPresenceCallback<T>): void; | ||
/** | ||
* @deprecated use the callback returned by subscribe instead. | ||
* See v0.13 release notes for more information. | ||
* Will be removed in a future version. | ||
*/ | ||
<T extends Presence>(type: "others", listener: OthersEventCallback<T>): void; | ||
/** | ||
* @deprecated use the callback returned by subscribe instead. | ||
* See v0.13 release notes for more information. | ||
* Will be removed in a future version. | ||
*/ | ||
(type: "event", listener: EventCallback): void; | ||
/** | ||
* Unsubscribe to errors thrown in the room. | ||
* @deprecated use the callback returned by subscribe instead. | ||
* See v0.13 release notes for more information. | ||
* Will be removed in a future version. | ||
*/ | ||
(type: "error", listener: ErrorCallback): void; | ||
/** | ||
* Unsubscribe to connection state updates. | ||
* @deprecated use the callback returned by subscribe instead. | ||
* See v0.13 release notes for more information. | ||
* Will be removed in a future version. | ||
*/ | ||
@@ -248,3 +371,4 @@ (type: "connection", listener: ConnectionCallback): void; | ||
* 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 overrides A partial object that contains the properties you want to update. | ||
* @param overrides Optional object to configure the behavior of updatePresence. | ||
* | ||
@@ -258,6 +382,11 @@ * @example | ||
*/ | ||
updatePresence: <T extends Presence>(overrides: Partial<T>) => void; | ||
updatePresence: <T extends Presence>(overrides: Partial<T>, options?: { | ||
/** | ||
* Whether or not the presence should have an impact on the undo/redo history. | ||
*/ | ||
addToHistory: boolean; | ||
}) => 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 | ||
* Broadcasts 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 | ||
* | ||
@@ -279,5 +408,10 @@ * @example | ||
}>; | ||
undo: () => void; | ||
redo: () => void; | ||
/** | ||
* Batches modifications made during the given function. | ||
* All the modifications are sent to other clients in a single message. | ||
* All the subscribers are called only after the batch is over. | ||
* All the modifications are merged in a single history item (undo/redo). | ||
*/ | ||
batch: (fn: () => void) => void; | ||
}; | ||
export {}; |
@@ -0,1 +1,8 @@ | ||
import { AbstractCrdt, Doc } from "./AbstractCrdt"; | ||
import { SerializedCrdtWithId } from "./live"; | ||
export declare function remove<T>(array: T[], item: T): void; | ||
export declare function isSameNodeOrChildOf(node: AbstractCrdt, parent: AbstractCrdt): boolean; | ||
export declare function deserialize(entry: SerializedCrdtWithId, parentToChildren: Map<string, SerializedCrdtWithId[]>, doc: Doc): AbstractCrdt; | ||
export declare function isCrdt(obj: any): obj is AbstractCrdt; | ||
export declare function selfOrRegisterValue(obj: AbstractCrdt): any; | ||
export declare function selfOrRegister(obj: any): AbstractCrdt; |
@@ -0,1 +1,6 @@ | ||
import { CrdtType, } from "./live"; | ||
import { LiveList } from "./LiveList"; | ||
import { LiveMap } from "./LiveMap"; | ||
import { LiveObject } from "./LiveObject"; | ||
import { LiveRegister } from "./LiveRegister"; | ||
export function remove(array, item) { | ||
@@ -9,1 +14,54 @@ for (let i = 0; i < array.length; i++) { | ||
} | ||
export function isSameNodeOrChildOf(node, parent) { | ||
if (node === parent) { | ||
return true; | ||
} | ||
if (node._parent) { | ||
return isSameNodeOrChildOf(node._parent, parent); | ||
} | ||
return false; | ||
} | ||
export function deserialize(entry, parentToChildren, doc) { | ||
switch (entry[1].type) { | ||
case CrdtType.Object: { | ||
return LiveObject._deserialize(entry, parentToChildren, doc); | ||
} | ||
case CrdtType.List: { | ||
return LiveList._deserialize(entry, parentToChildren, doc); | ||
} | ||
case CrdtType.Map: { | ||
return LiveMap._deserialize(entry, parentToChildren, doc); | ||
} | ||
case CrdtType.Register: { | ||
return LiveRegister._deserialize(entry, parentToChildren, doc); | ||
} | ||
default: { | ||
throw new Error("Unexpected CRDT type"); | ||
} | ||
} | ||
} | ||
export function isCrdt(obj) { | ||
return (obj instanceof LiveObject || | ||
obj instanceof LiveMap || | ||
obj instanceof LiveList || | ||
obj instanceof LiveRegister); | ||
} | ||
export function selfOrRegisterValue(obj) { | ||
if (obj instanceof LiveRegister) { | ||
return obj.data; | ||
} | ||
return obj; | ||
} | ||
export function selfOrRegister(obj) { | ||
if (obj instanceof LiveObject || | ||
obj instanceof LiveMap || | ||
obj instanceof LiveList) { | ||
return obj; | ||
} | ||
else if (obj instanceof LiveRegister) { | ||
throw new Error("Internal error. LiveRegister should not be created from selfOrRegister"); | ||
} | ||
else { | ||
return new LiveRegister(obj); | ||
} | ||
} |
{ | ||
"name": "@liveblocks/client", | ||
"version": "0.12.3", | ||
"version": "0.13.0-beta.1", | ||
"description": "", | ||
@@ -22,4 +22,4 @@ "main": "./lib/cjs/index.js", | ||
"build": "npm run build:esm && npm run build:cjs", | ||
"build:esm": "tsc -p tsconfig.json && rm ./lib/esm/*.test.js && rm ./lib/esm/*.test.d.ts", | ||
"build:cjs": "tsc -p tsconfig-cjs.json && rm ./lib/cjs/*.test.js && rm ./lib/cjs/*.test.d.ts", | ||
"build:esm": "tsc -p tsconfig-esm.json", | ||
"build:cjs": "tsc -p tsconfig-cjs.json", | ||
"test": "jest --watch" | ||
@@ -26,0 +26,0 @@ }, |
<p align="center"> | ||
<a href="https://liveblocks.io"> | ||
<img src="https://liveblocks.io/icon-192x192.png" height="96"> | ||
<img src="https://liveblocks.io/images/blog/introducing-liveblocks.png"> | ||
</a> | ||
</p> | ||
# [Liveblocks](https://liveblocks.io) · [![Twitter Follow](https://shields.io/twitter/follow/liveblocks?label=Follow)](https://twitter.com/liveblocks) | ||
# Liveblocks · [![Twitter Follow](https://shields.io/twitter/follow/liveblocks?label=Follow)](https://twitter.com/liveblocks) | ||
Liveblocks helps you create performant and reliable collaborative experiences. | ||
**At [Liveblocks](https://liveblocks.io), we’re building tools to help companies create world-class collaborative products that attract, engage and retain users.** This repository is a set of open-source packages for building performant and reliable multiplayer experiences. | ||
## [Documentation](https://liveblocks.io/docs) | ||
## Examples | ||
Try it live on [liveblocks.io](https://liveblocks.io/examples). | ||
- Browse our gallery of collaborative UI patterns. [View examples gallery](https://liveblocks.io/examples) | ||
- Explore and clone any of our open-source examples. [View code examples](https://github.com/liveblocks/liveblocks/tree/main/examples) | ||
Clone one of our [examples](https://github.com/liveblocks/liveblocks/tree/main/examples). | ||
## Packages | ||
- [@liveblocks/client](https://github.com/liveblocks/liveblocks/tree/main/packages/liveblocks) | ||
- [@liveblocks/react](https://github.com/liveblocks/liveblocks/tree/main/packages/liveblocks-react) | ||
- [@liveblocks/node](https://github.com/liveblocks/liveblocks/tree/main/packages/liveblocks-node) | ||
## Documentation | ||
[Read the documentation](https://liveblocks.io/docs) to start using Liveblocks. | ||
## Releases | ||
For changelog, visit [https://github.com/liveblocks/liveblocks/releases](https://github.com/liveblocks/liveblocks/releases). | ||
## Authors | ||
- Guillaume Salles ([@guillaume_slls](https://twitter.com/guillaume_slls)) - [Liveblocks](https://liveblocks.io) | ||
- Olivier Foucherot ([@ofoucherot](https://twitter.com/ofoucherot)) - [Liveblocks](https://liveblocks.io) | ||
- Steven Fabre ([@stevenfabre](https://twitter.com/stevenfabre)) - [Liveblocks](https://liveblocks.io) | ||
## Community | ||
- [Slack](https://join.slack.com/t/liveblocks-community/shared_invite/zt-qozwnk75-6RB0i1wk1lx470KX0YuZxQ) - To get involved with the Liveblocks community, ask questions and share tips. | ||
- [Twitter](https://twitter.com/liveblocks) - To receive updates, announcements, blog posts, and general Liveblocks tips. | ||
## License | ||
Licensed under the Apache License 2.0, Copyright © 2021-present [Liveblocks](https://liveblocks.io). | ||
See [LICENSE](./LICENSE) for more information. |
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
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
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
274792
54
7315
46
3