Socket
Socket
Sign inDemoInstall

@liveblocks/client

Package Overview
Dependencies
Maintainers
3
Versions
379
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@liveblocks/client - npm Package Compare versions

Comparing version 0.12.3 to 0.13.0-beta.1

lib/cjs/AbstractCrdt.d.ts

4

lib/cjs/index.d.ts

@@ -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.
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc