Socket
Socket
Sign inDemoInstall

@liveblocks/client

Package Overview
Dependencies
Maintainers
2
Versions
376
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.6.0-beta.3 to 0.6.0-beta.4

15

lib/cjs/client.js

@@ -44,2 +44,17 @@ "use strict";

}
if (typeof window !== "undefined") {
// TODO: Expose a way to clear these
window.addEventListener("online", () => {
for (const [, room] of rooms) {
room._onNavigatorOnline();
}
});
}
if (typeof document !== "undefined") {
document.addEventListener("visibilitychange", () => {
for (const [, room] of rooms) {
room._onVisibilityChange(document.visibilityState);
}
});
}
return {

@@ -46,0 +61,0 @@ addEventListener,

2

lib/cjs/index.d.ts
export type { Record, RecordData, List } from "./doc";
export { createClient } from "./client";
export { RoomState, LiveStorageState } from "./types";
export { LiveStorageState } from "./types";
export type { Others, Presence, Room, InitialStorageFactory, Client, LiveStorage, } from "./types";
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.LiveStorageState = exports.RoomState = exports.createClient = void 0;
exports.LiveStorageState = exports.createClient = void 0;
var client_1 = require("./client");
Object.defineProperty(exports, "createClient", { enumerable: true, get: function () { return client_1.createClient; } });
var types_1 = require("./types");
Object.defineProperty(exports, "RoomState", { enumerable: true, get: function () { return types_1.RoomState; } });
Object.defineProperty(exports, "LiveStorageState", { enumerable: true, get: function () { return types_1.LiveStorageState; } });

@@ -135,4 +135,3 @@ import { Presence } from "./types";

MAX_NUMBER_OF_CONCURRENT_CONNECTIONS = 4003,
MAX_NUMBER_OF_MESSAGES_PER_DAY_PER_APP = 4004,
INTERNAL_ERROR = 4005
MAX_NUMBER_OF_MESSAGES_PER_DAY_PER_APP = 4004
}

@@ -43,3 +43,2 @@ "use strict";

WebsocketCloseCodes[WebsocketCloseCodes["MAX_NUMBER_OF_MESSAGES_PER_DAY_PER_APP"] = 4004] = "MAX_NUMBER_OF_MESSAGES_PER_DAY_PER_APP";
WebsocketCloseCodes[WebsocketCloseCodes["INTERNAL_ERROR"] = 4005] = "INTERNAL_ERROR";
})(WebsocketCloseCodes = exports.WebsocketCloseCodes || (exports.WebsocketCloseCodes = {}));

@@ -1,4 +0,102 @@

import { ClientOptions, Room } from "./types";
import { RecordData, List } from ".";
import { Doc, Record } from "./doc";
import { Others, Presence, ClientOptions, Room, InitialStorageFactory, MyPresenceEventCallback, OthersPresenceEventCallback, StorageEventCallback, AuthEndpoint, LiveStorageState, LiveStorage, EventEventCallback, User, Connection, Serializable } from "./types";
import { ClientMessage, Op } from "./live";
declare type IdFactory = () => string;
export declare type State = {
connection: Connection;
socket: WebSocket | null;
lastFlushTime: number;
flushData: {
presence: Presence | null;
messages: ClientMessage[];
storageOperations: Op[];
};
timeoutHandles: {
flush: number | null;
reconnect: number;
pongTimeout: number;
};
intervalHandles: {
heartbeat: number;
};
listeners: {
storage: StorageEventCallback[];
event: EventEventCallback[];
"others-presence": OthersPresenceEventCallback[];
"my-presence": MyPresenceEventCallback[];
};
me: Presence | null;
others: Others;
users: {
[connectionId: number]: User;
};
idFactory: IdFactory | null;
numberOfRetry: number;
doc: Doc<any> | null;
storageState: LiveStorageState;
initialStorageFactory: InitialStorageFactory | null;
};
export declare type Effects = {
authenticate(): void;
send(messages: ClientMessage[]): void;
delayFlush(delay: number): number;
startHeartbeatInterval(): number;
schedulePongTimeout(): number;
scheduleReconnect(delay: number): number;
};
declare type Context = {
room: string;
authEndpoint: AuthEndpoint;
liveblocksServer: string;
throttleDelay: number;
onError: (error: Error) => void;
};
export declare function makeStateMachine(state: State, context: Context, mockedEffects?: Effects): {
onOpen: () => void;
onClose: (event: {
code: number;
wasClean: boolean;
reason: any;
}) => void;
onMessage: (event: MessageEvent) => void;
authenticationSuccess: (connectionId: number, socket: WebSocket) => void;
heartbeat: () => void;
onNavigatorOnline: () => void;
onVisibilityChange: (visibilityState: VisibilityState) => void;
connect: () => null | undefined;
disconnect: () => void;
addEventListener: {
<T extends Serializable>(type: "my-presence", listener: MyPresenceEventCallback<T>): void;
<T_1 extends Serializable>(type: "others-presence", listener: OthersPresenceEventCallback<T_1>): void;
(type: "event", listener: EventEventCallback): void;
<T_2 extends RecordData>(type: "storage", listener: StorageEventCallback<T_2>): void;
};
removeEventListener: {
<T_3 extends Serializable>(type: "my-presence", listener: MyPresenceEventCallback<T_3>): void;
<T_4 extends Serializable>(type: "others-presence", listener: OthersPresenceEventCallback<T_4>): void;
(type: "event", listener: EventEventCallback): void;
<T_5 extends RecordData>(type: "storage", listener: StorageEventCallback<T_5>): void;
};
updatePresence: <T_6 extends Serializable>(overrides: Partial<T_6>) => void;
broadcastEvent: (event: any) => void;
fetchStorage: (initialStorageFactory: InitialStorageFactory) => void;
createRecord: <T_7 extends RecordData>(data: any) => Record<T_7>;
updateRecord: <T_8 extends RecordData>(record: Record<T_8>, overrides: Partial<T_8>) => void;
createList: <T_9 extends RecordData>() => List<Record<T_9>>;
pushItem: <T_10 extends RecordData>(list: List<Record<T_10>>, item: Record<T_10>) => void;
deleteItem: <T_11 extends RecordData>(list: List<Record<T_11>>, index: number) => void;
moveItem: <T_12 extends RecordData>(list: List<Record<T_12>>, index: number, targetIndex: number) => void;
selectors: {
getListenersCount: () => number;
getConnectionState: () => Connection;
getPresence: <T_13 extends Serializable>() => T_13 | null;
getOthers: <T_14 extends Serializable>() => Others<T_14>;
getStorage: () => LiveStorage;
};
};
export declare function defaultState(): State;
export declare function createRoom(name: string, options: ClientOptions & {
onError: (error: Error) => void;
}): Room;
export {};

@@ -42,3 +42,3 @@ "use strict";

Object.defineProperty(exports, "__esModule", { value: true });
exports.createRoom = void 0;
exports.createRoom = exports.defaultState = exports.makeStateMachine = void 0;
const doc_1 = require("./doc");

@@ -50,21 +50,11 @@ const types_1 = require("./types");

const live_1 = require("./live");
const BACKOFF_RETRY_DELAYS = [
250,
500,
1000,
2000,
4000,
8000,
10000,
10000,
10000,
10000,
];
const BACKOFF_RETRY_DELAYS = [250, 500, 1000, 2000, 4000, 8000, 10000];
const HEARTBEAT_INTERVAL = 30000;
// const WAKE_UP_CHECK_INTERVAL = 2000;
const PONG_TIMEOUT = 2000;
function isValidRoomEventType(value) {
return (value === "open" ||
value === "storage" ||
return (value === "storage" ||
value === "my-presence" ||
value === "others-presence" ||
value === "event" ||
value === "close");
value === "event");
}

@@ -89,137 +79,109 @@ function makeIdFactory(connectionId) {

}
function createRoom(name, options) {
const throttleDelay = options.throttle || 100;
const liveblocksServer = options.liveblocksServer || "wss://live.liveblocks.io";
const authEndpoint = options.authEndpoint;
const _listeners = {
open: [],
storage: [],
event: [],
"others-presence": [],
"my-presence": [],
close: [],
};
let _idFactory = null;
let _doc = null;
let _storageState = types_1.LiveStorageState.NotInitialized;
let _initialStorageFactory = null;
const _state = {
me: null,
socket: null,
lastFlushTime: 0,
flushTimeout: null,
flushData: {
presence: null,
messages: [],
storageOperations: [],
function log(...params) {
return;
console.log(...params, new Date().toString());
}
function makeStateMachine(state, context, mockedEffects) {
const effects = mockedEffects || {
authenticate() {
return __awaiter(this, void 0, void 0, function* () {
try {
const token = yield authentication_1.default(context.authEndpoint, context.room);
const connectionId = authentication_1.parseToken(token).actor;
const socket = new WebSocket(`${context.liveblocksServer}/?token=${token}`);
socket.addEventListener("message", onMessage);
socket.addEventListener("open", onOpen);
socket.addEventListener("close", onClose);
socket.addEventListener("error", onError);
authenticationSuccess(connectionId, socket);
}
catch (er) {
authenticationFailure(er);
}
});
},
send(messageOrMessages) {
if (state.socket == null) {
throw new Error("Can't send message if socket is null");
}
state.socket.send(JSON.stringify(messageOrMessages));
},
delayFlush(delay) {
return setTimeout(tryFlushing, delay);
},
startHeartbeatInterval() {
return setInterval(heartbeat, HEARTBEAT_INTERVAL);
},
schedulePongTimeout() {
return setTimeout(pongTimeout, PONG_TIMEOUT);
},
scheduleReconnect(delay) {
return setTimeout(connect, delay);
},
};
let _users = {};
let _others = makeOthers(_users);
let state = types_1.RoomState.Default;
let numberOfRetry = 0;
let retryTimeoutId = 0;
function send(messageOrMessages) {
if (_state.socket == null) {
throw new Error("Can't send message if socket is null");
function addEventListener(type, listener) {
if (!isValidRoomEventType(type)) {
throw new Error(`"${type}" is not a valid event name`);
}
_state.socket.send(JSON.stringify(messageOrMessages));
state.listeners[type].push(listener);
}
function makeId() {
if (_idFactory == null) {
throw new Error("Can't generate id. Id factory is missing.");
function removeEventListener(event, callback) {
if (!isValidRoomEventType(event)) {
throw new Error(`"${event}" is not a valid event name`);
}
return _idFactory();
const callbacks = state.listeners[event];
utils_1.remove(callbacks, callback);
}
function updateUsers(newUsers) {
_users = newUsers;
_others = makeOthers(newUsers);
for (const listener of _listeners["others-presence"]) {
listener(_others);
}
function getConnectionState() {
return state.connection;
}
function dispatch(op) {
_state.flushData.storageOperations.push(op);
tryFlushing();
function getListenersCount() {
return (state.listeners["my-presence"].length +
state.listeners["others-presence"].length +
state.listeners.storage.length +
state.listeners.event.length);
}
function getStorage() {
if (_storageState === types_1.LiveStorageState.Loaded) {
return {
state: _storageState,
root: _doc.root,
};
function connect() {
if (state.connection.state !== "closed" &&
state.connection.state !== "unavailable") {
return null;
}
return {
state: _storageState,
};
updateConnection({ state: "authenticating" });
effects.authenticate();
}
function fetchStorage(initialStorageFactory) {
_initialStorageFactory = initialStorageFactory;
_storageState = types_1.LiveStorageState.Loading;
_state.flushData.messages.push({ type: live_1.ClientMessageType.FetchStorage });
tryFlushing();
}
function updateDoc(doc) {
_doc = doc;
if (doc) {
for (const listener of _listeners.storage) {
listener(getStorage());
}
function updatePresence(overrides) {
const newPresence = Object.assign(Object.assign({}, state.me), overrides);
if (state.flushData.presence == null) {
state.flushData.presence = overrides;
}
}
function createRecord(data) {
return doc_2.createRecord(makeId(), data);
}
function createList() {
return doc_2.createList(makeId());
}
function onInitialStorageState(message) {
_storageState = types_1.LiveStorageState.Loaded;
if (message.root == null) {
const rootId = makeId();
_doc = doc_1.Doc.empty(rootId, (op) => dispatch(op));
updateDoc(_doc.updateRecord(rootId, _initialStorageFactory({
createRecord: (data) => createRecord(data),
createList: () => createList(),
})));
}
else {
updateDoc(doc_1.Doc.load(message.root, (op) => dispatch(op)));
for (const key in overrides) {
state.flushData.presence[key] = overrides[key];
}
}
}
function onStorageUpdates(message) {
if (_doc == null) {
// TODO: Cache updates in case they are coming while root is queried
return;
}
updateDoc(message.ops.reduce((doc, op) => doc.dispatch(op), _doc));
}
function onOpen() {
state = types_1.RoomState.Connected;
numberOfRetry = 0;
state.me = newPresence;
tryFlushing();
for (const callback of _listeners.open) {
callback();
for (const listener of state.listeners["my-presence"]) {
listener(state.me);
}
}
function onEvent(message) {
for (const listener of _listeners.event) {
listener({ connectionId: message.actor, event: message.event });
}
function authenticationSuccess(connectionId, socket) {
updateConnection({ state: "connecting", id: connectionId });
state.idFactory = makeIdFactory(connectionId);
state.socket = socket;
}
function onRoomStateMessage(message) {
const newUsers = {};
for (const key in message.users) {
const connectionId = Number.parseInt(key);
const user = message.users[key];
newUsers[connectionId] = {
connectionId,
info: user.info,
id: user.id,
};
function authenticationFailure(error) {
console.error(error);
updateConnection({ state: "unavailable" });
state.numberOfRetry++;
state.timeoutHandles.reconnect = effects.scheduleReconnect(getRetryDelay());
}
function onVisibilityChange(visibilityState) {
if (visibilityState === "visible" && state.connection.state === "open") {
log("Heartbeat after visibility change");
heartbeat();
}
updateUsers(newUsers);
}
function onUpdatePresenceMessage(message) {
const user = _users[message.actor];
const user = state.users[message.actor];
const newUser = user

@@ -236,11 +198,42 @@ ? {

};
updateUsers(Object.assign(Object.assign({}, _users), { [message.actor]: newUser }));
updateUsers(Object.assign(Object.assign({}, state.users), { [message.actor]: newUser }));
}
function updateUsers(newUsers) {
state.users = newUsers;
state.others = makeOthers(newUsers);
for (const listener of state.listeners["others-presence"]) {
listener(state.others);
}
}
function onUserLeftMessage(message) {
const userLeftMessage = message;
const _a = _users, _b = userLeftMessage.actor, notUsed = _a[_b], rest = __rest(_a, [typeof _b === "symbol" ? _b : _b + ""]);
const _a = state.users, _b = userLeftMessage.actor, notUsed = _a[_b], rest = __rest(_a, [typeof _b === "symbol" ? _b : _b + ""]);
updateUsers(rest);
}
function onRoomStateMessage(message) {
const newUsers = {};
for (const key in message.users) {
const connectionId = Number.parseInt(key);
const user = message.users[key];
newUsers[connectionId] = {
connectionId,
info: user.info,
id: user.id,
};
}
updateUsers(newUsers);
}
function onNavigatorOnline() {
if (state.connection.state === "unavailable") {
log("Try to reconnect after connectivity change");
reconnect();
}
}
function onEvent(message) {
for (const listener of state.listeners.event) {
listener({ connectionId: message.actor, event: message.event });
}
}
function onUserJoinedMessage(message) {
updateUsers(Object.assign(Object.assign({}, _users), { [message.actor]: {
updateUsers(Object.assign(Object.assign({}, state.users), { [message.actor]: {
connectionId: message.actor,

@@ -250,12 +243,18 @@ info: message.info,

} }));
// Send current presence to new user
// TODO: Consider storing it on the backend
_state.flushData.messages.push({
type: live_1.ClientMessageType.UpdatePresence,
data: _state.me,
targetActor: message.actor,
});
tryFlushing();
if (state.me) {
// Send current presence to new user
// TODO: Consider storing it on the backend
state.flushData.messages.push({
type: live_1.ClientMessageType.UpdatePresence,
data: state.me,
targetActor: message.actor,
});
tryFlushing();
}
}
function onMessage(event) {
if (event.data === "pong") {
clearTimeout(state.timeoutHandles.pongTimeout);
return;
}
const message = JSON.parse(event.data);

@@ -293,88 +292,98 @@ switch (message.type) {

}
// function onWakeUp() {
// // Sometimes, the browser can put the webpage on pause (computer is on sleep mode for example)
// // The client will not know that the server has probably close the connection even if the readyState is Open
// // One way to detect this kind of pause is to ensure that a setInterval is not taking more than the delay it was configured with
// if (state.connection.state === "open") {
// log("Try to reconnect after laptop wake up");
// reconnect();
// }
// }
function onClose(event) {
state.socket = null;
clearTimeout(state.timeoutHandles.pongTimeout);
clearInterval(state.intervalHandles.heartbeat);
if (state.timeoutHandles.flush) {
clearTimeout(state.timeoutHandles.flush);
}
clearTimeout(state.timeoutHandles.reconnect);
updateUsers({});
if (event.code >= 4000 && event.code <= 4100) {
options.onError(new Error(event.reason));
updateConnection({ state: "failed" });
context.onError(new Error(event.reason));
}
for (const listener of _listeners.close) {
listener();
else if (event.wasClean === false) {
updateConnection({ state: "unavailable" });
state.numberOfRetry++;
state.timeoutHandles.reconnect = effects.scheduleReconnect(getRetryDelay());
}
_state.socket = null;
updateUsers({});
if (event.wasClean === false) {
state = types_1.RoomState.Default;
numberOfRetry++;
retryTimeoutId = setTimeout(() => connect(), BACKOFF_RETRY_DELAYS[numberOfRetry < BACKOFF_RETRY_DELAYS.length
? numberOfRetry
: BACKOFF_RETRY_DELAYS.length - 1]);
else {
updateConnection({ state: "closed" });
}
}
function onError(event) { }
function connect() {
return __awaiter(this, void 0, void 0, function* () {
if (state === types_1.RoomState.Connected || state === types_1.RoomState.Connecting) {
return;
}
state = types_1.RoomState.Connecting;
let token = null;
let actor = null;
try {
token = yield authentication_1.default(authEndpoint, name);
actor = authentication_1.parseToken(token).actor;
}
catch (er) {
options.onError(er);
state = types_1.RoomState.Error;
return;
}
_idFactory = makeIdFactory(actor);
_state.socket = new WebSocket(`${liveblocksServer}/?token=${token}`);
_state.socket.addEventListener("message", onMessage);
_state.socket.addEventListener("open", onOpen);
_state.socket.addEventListener("close", onClose);
_state.socket.addEventListener("error", onError);
});
function updateConnection(connection) {
state.connection = connection;
}
function disconnect() {
if (_state.socket) {
_state.socket.removeEventListener("open", onOpen);
_state.socket.removeEventListener("message", onMessage);
_state.socket.removeEventListener("close", onClose);
_state.socket.removeEventListener("error", onError);
_state.socket.close();
_state.socket = null;
function getRetryDelay() {
return BACKOFF_RETRY_DELAYS[state.numberOfRetry < BACKOFF_RETRY_DELAYS.length
? state.numberOfRetry
: BACKOFF_RETRY_DELAYS.length - 1];
}
function onError() { }
function onOpen() {
clearInterval(state.intervalHandles.heartbeat);
state.intervalHandles.heartbeat = effects.startHeartbeatInterval();
updateConnection({ state: "open", id: state.connection.id });
state.numberOfRetry = 0;
tryFlushing();
}
function heartbeat() {
if (state.socket == null) {
// Should never happen, because we clear the pong timeout when the connection is dropped explictly
return;
}
state = types_1.RoomState.Default;
updateUsers({});
clearTimeout(retryTimeoutId);
_listeners.open = [];
_listeners["my-presence"] = [];
_listeners["others-presence"] = [];
_listeners.event = [];
_listeners.storage = [];
_listeners.close = [];
clearTimeout(state.timeoutHandles.pongTimeout);
state.timeoutHandles.pongTimeout = effects.schedulePongTimeout();
if (state.socket.readyState === WebSocket.OPEN) {
state.socket.send("ping");
}
}
//////////////
// Presence //
//////////////
function getPresence() {
return _state.me;
function pongTimeout() {
log("Pong timeout. Trying to reconnect.");
reconnect();
}
function getOthers() {
return _others;
}
function updatePresence(overrides) {
// Create new local presence right away and call listeners
const newPresence = Object.assign(Object.assign({}, _state.me), overrides);
_state.me = newPresence;
for (const listener of _listeners["my-presence"]) {
listener(_state.me);
function reconnect() {
if (state.socket) {
state.socket.removeEventListener("open", onOpen);
state.socket.removeEventListener("message", onMessage);
state.socket.removeEventListener("close", onClose);
state.socket.removeEventListener("error", onError);
state.socket.close();
state.socket = null;
}
updatePresenceToSend(_state, overrides);
tryFlushing();
updateConnection({ state: "unavailable" });
clearTimeout(state.timeoutHandles.pongTimeout);
if (state.timeoutHandles.flush) {
clearTimeout(state.timeoutHandles.flush);
}
clearTimeout(state.timeoutHandles.reconnect);
clearInterval(state.intervalHandles.heartbeat);
connect();
}
function tryFlushing() {
if (state.socket == null) {
return;
}
if (state.socket.readyState !== WebSocket.OPEN) {
return;
}
const now = Date.now();
if (canSend(now, _state, throttleDelay)) {
send(flushDataToMessages(_state));
_state.flushData = {
const elapsedTime = now - state.lastFlushTime;
if (elapsedTime > context.throttleDelay) {
const messages = flushDataToMessages(state);
if (messages.length === 0) {
return;
}
effects.send(messages);
state.flushData = {
messages: [],

@@ -384,28 +393,64 @@ storageOperations: [],

};
_state.lastFlushTime = Date.now();
state.lastFlushTime = now;
}
else {
if (_state.flushTimeout) {
clearTimeout(_state.flushTimeout);
_state.flushTimeout = null;
if (state.timeoutHandles.flush != null) {
clearTimeout(state.timeoutHandles.flush);
}
_state.flushTimeout = setTimeout(() => {
if (isSocketReady(_state)) {
flushDataToMessages(_state);
send(flushDataToMessages(_state));
_state.flushData = {
messages: [],
storageOperations: [],
presence: null,
};
_state.lastFlushTime = Date.now();
}
}, throttleDelay - (now - _state.lastFlushTime));
state.timeoutHandles.flush = effects.delayFlush(context.throttleDelay - (now - state.lastFlushTime));
}
}
function flushDataToMessages(state) {
const messages = [];
if (state.flushData.presence) {
messages.push({
type: live_1.ClientMessageType.UpdatePresence,
data: state.flushData.presence,
});
}
for (const event of state.flushData.messages) {
messages.push(event);
}
if (state.flushData.storageOperations.length > 0) {
messages.push({
type: live_1.ClientMessageType.UpdateStorage,
ops: state.flushData.storageOperations,
});
}
return messages;
}
function disconnect() {
if (state.socket) {
state.socket.removeEventListener("open", onOpen);
state.socket.removeEventListener("message", onMessage);
state.socket.removeEventListener("close", onClose);
state.socket.removeEventListener("error", onError);
state.socket.close();
state.socket = null;
}
updateConnection({ state: "closed" });
state.me = null;
if (state.timeoutHandles.flush) {
clearTimeout(state.timeoutHandles.flush);
}
clearTimeout(state.timeoutHandles.reconnect);
clearTimeout(state.timeoutHandles.pongTimeout);
clearInterval(state.intervalHandles.heartbeat);
updateUsers({});
state.listeners["my-presence"] = [];
state.listeners["others-presence"] = [];
state.listeners.event = [];
state.listeners.storage = [];
}
function getPresence() {
return state.me;
}
function getOthers() {
return state.others;
}
function broadcastEvent(event) {
if (!isSocketReady(_state)) {
if (state.socket == null) {
return;
}
_state.flushData.messages.push({
state.flushData.messages.push({
type: live_1.ClientMessageType.ClientEvent,

@@ -416,96 +461,198 @@ event,

}
function addEventListener(type, listener) {
if (!isValidRoomEventType(type)) {
throw new Error(`"${type}" is not a valid event name`);
/**
* STORAGE
*/
function onStorageUpdates(message) {
if (state.doc == null) {
// TODO: Cache updates in case they are coming while root is queried
return;
}
_listeners[type].push(listener);
updateDoc(message.ops.reduce((doc, op) => doc.dispatch(op), state.doc));
}
function removeEventListener(event, callback) {
if (!isValidRoomEventType(event)) {
throw new Error(`"${event}" is not a valid event name`);
function updateDoc(doc) {
state.doc = doc;
if (doc) {
for (const listener of state.listeners.storage) {
listener(getStorage());
}
}
const callbacks = _listeners[event];
utils_1.remove(callbacks, callback);
}
function getStorage() {
if (state.storageState === types_1.LiveStorageState.Loaded) {
return {
state: state.storageState,
root: state.doc.root,
};
}
return {
state: state.storageState,
};
}
function onInitialStorageState(message) {
state.storageState = types_1.LiveStorageState.Loaded;
if (message.root == null) {
const rootId = makeId();
state.doc = doc_1.Doc.empty(rootId, (op) => dispatch(op));
updateDoc(state.doc.updateRecord(rootId, state.initialStorageFactory({
createRecord: (data) => createRecord(data),
createList: () => createList(),
})));
}
else {
updateDoc(doc_1.Doc.load(message.root, (op) => dispatch(op)));
}
}
function makeId() {
if (state.idFactory == null) {
throw new Error("Can't generate id. Id factory is missing.");
}
return state.idFactory();
}
function dispatch(op) {
state.flushData.storageOperations.push(op);
tryFlushing();
}
function createRecord(data) {
return doc_2.createRecord(makeId(), data);
}
function createList() {
return doc_2.createList(makeId());
}
function fetchStorage(initialStorageFactory) {
state.initialStorageFactory = initialStorageFactory;
state.storageState = types_1.LiveStorageState.Loading;
state.flushData.messages.push({ type: live_1.ClientMessageType.FetchStorage });
tryFlushing();
}
function updateRecord(record, overrides) {
updateDoc(state.doc.updateRecord(record.id, overrides));
}
function pushItem(list, item) {
updateDoc(state.doc.pushItem(list.id, item));
}
function deleteItem(list, index) {
updateDoc(state.doc.deleteItem(list.id, index));
}
function moveItem(list, index, targetIndex) {
updateDoc(state.doc.moveItem(list.id, index, targetIndex));
}
return {
connect() {
connect();
},
// Internal
onOpen,
onClose,
onMessage,
authenticationSuccess,
heartbeat,
onNavigatorOnline,
// onWakeUp,
onVisibilityChange,
// Core
connect,
disconnect,
getListenersCount() {
return (_listeners.open.length +
_listeners["my-presence"].length +
_listeners["others-presence"].length +
_listeners.storage.length +
_listeners.close.length +
_listeners.event.length);
},
getState() {
return state;
},
addEventListener,
removeEventListener,
/////////////
// Storage //
/////////////
getStorage,
// Presence
updatePresence,
broadcastEvent,
// Storage
fetchStorage,
createRecord,
updateRecord,
createList,
updateRecord(record, overrides) {
updateDoc(_doc.updateRecord(record.id, overrides));
pushItem,
deleteItem,
moveItem,
selectors: {
// Core
getListenersCount,
getConnectionState,
// Presence
getPresence,
getOthers,
// Storage
getStorage,
},
pushItem(list, item) {
updateDoc(_doc.pushItem(list.id, item));
};
}
exports.makeStateMachine = makeStateMachine;
function defaultState() {
return {
connection: { state: "closed" },
socket: null,
listeners: {
storage: [],
event: [],
"others-presence": [],
"my-presence": [],
},
deleteItem(list, index) {
updateDoc(_doc.deleteItem(list.id, index));
numberOfRetry: 0,
lastFlushTime: 0,
timeoutHandles: {
flush: null,
reconnect: 0,
pongTimeout: 0,
},
moveItem(list, index, targetIndex) {
updateDoc(_doc.moveItem(list.id, index, targetIndex));
flushData: {
presence: null,
messages: [],
storageOperations: [],
},
intervalHandles: {
heartbeat: 0,
},
me: null,
users: {},
others: makeOthers({}),
storageState: types_1.LiveStorageState.NotInitialized,
initialStorageFactory: null,
doc: null,
idFactory: null,
};
}
exports.defaultState = defaultState;
function createRoom(name, options) {
const throttleDelay = options.throttle || 100;
const liveblocksServer = options.liveblocksServer || "wss://live.liveblocks.io";
const authEndpoint = options.authEndpoint;
const state = defaultState();
const machine = makeStateMachine(state, {
throttleDelay,
liveblocksServer,
authEndpoint,
onError: options.onError,
room: name,
});
const room = {
/////////////
// Core //
/////////////
connect: machine.connect,
disconnect: machine.disconnect,
getConnectionState: machine.selectors.getConnectionState,
getListenersCount: machine.selectors.getListenersCount,
addEventListener: machine.addEventListener,
removeEventListener: machine.removeEventListener,
/////////////
// Storage //
/////////////
getStorage: machine.selectors.getStorage,
fetchStorage: machine.fetchStorage,
createRecord: machine.createRecord,
createList: machine.createList,
updateRecord: machine.updateRecord,
pushItem: machine.pushItem,
deleteItem: machine.deleteItem,
moveItem: machine.moveItem,
//////////////
// Presence //
//////////////
getPresence,
updatePresence,
getOthers,
broadcastEvent,
getPresence: machine.selectors.getPresence,
updatePresence: machine.updatePresence,
getOthers: machine.selectors.getOthers,
broadcastEvent: machine.broadcastEvent,
};
room._onNavigatorOnline = machine.onNavigatorOnline;
room._onVisibilityChange = machine.onVisibilityChange;
return room;
}
exports.createRoom = createRoom;
function flushDataToMessages(state) {
const messages = [];
if (state.flushData.presence) {
messages.push({
type: live_1.ClientMessageType.UpdatePresence,
data: state.flushData.presence,
});
}
for (const event of state.flushData.messages) {
messages.push(event);
}
if (state.flushData.storageOperations.length > 0) {
messages.push({
type: live_1.ClientMessageType.UpdateStorage,
ops: state.flushData.storageOperations,
});
}
return messages;
}
function isSocketReady(state) {
return state.socket !== null && state.socket.readyState === WebSocket.OPEN;
}
function canSend(now, state, wait) {
return isSocketReady(state) && now - state.lastFlushTime > wait;
}
function updatePresenceToSend(state, overrides) {
if (state.flushData.presence == null) {
state.flushData.presence = overrides;
}
else {
for (const key in overrides) {
state.flushData.presence[key] = overrides[key];
}
}
}
import { RecordData, Record, List } from "./doc";
export declare type User<T extends Presence = Presence> = {
connectionId: number;
id?: string;
info?: any;
presence?: T;
};
/**
* Public
* Represents all the other users conencted in the room. Treated as immutable.
*/
export interface Others<T extends Presence = Presence> {
export interface Others<TPresence extends Presence = Presence> {
/**
* Number of other users in the room.
*/
readonly count: number;
toArray(): Array<User<T>>;
map<U>(callback: (user: User<T>) => U): Array<U>;
/**
* Returns the array of connected users in room.
*/
toArray(): User<TPresence>[];
/**
* This function let you map over the connected users in the room.
*/
map<U>(callback: (user: User<TPresence>) => U): U[];
}
/**
* Represents a user connected in a room. Treated as immutable.
*/
export declare type User<TPresence extends Presence = Presence> = {
/**
* The connection id of the user. It is unique and increment at every new connection.
*/
readonly connectionId: number;
/**
* The id of the user that has been set in the authentication endpoint.
* Useful to get additional information about the connected user.
*/
readonly id?: string;
/**
* Additional user information that has been set in the authentication endpoint.
*/
readonly info?: any;
/**
* The user presence.
*/
readonly presence?: TPresence;
};
export declare type Presence = Serializable;
export declare type SerializablePrimitive = boolean | string | number | null;
export declare type Serializable = {
[key: string]: SerializablePrimitive | Serializable | Array<SerializablePrimitive>;
[key: string]: SerializablePrimitive | Serializable | SerializablePrimitive[];
};

@@ -24,4 +49,7 @@ declare type AuthEndpointCallback = (room: string) => Promise<string>;

export declare type ClientOptions = {
/**
* The authentication endpoint that is called to ensure that the current user has access to a room.
* Can be an url or a callback if you need to attach additional headers.
*/
authEndpoint: AuthEndpoint;
liveblocksServer?: string;
throttle?: number;

@@ -43,7 +71,13 @@ };

};
export declare type Connection = {
state: "closed" | "authenticating" | "unavailable" | "failed";
} | {
state: "open" | "connecting";
id: number;
};
export declare type Room = {
connect(): void;
disconnect(): void;
getConnectionState(): Connection;
getListenersCount(): number;
getState(): RoomState;
addEventListener: {

@@ -54,4 +88,2 @@ <T extends Presence>(type: "my-presence", listener: MyPresenceEventCallback<T>): void;

<T extends RecordData>(type: "storage", listener: StorageEventCallback<T>): void;
(type: "open", listener: OpenEventCallback): void;
(type: "close", listener: CloseEventCallback): void;
};

@@ -63,4 +95,2 @@ removeEventListener: {

<T extends RecordData>(type: "storage", listener: StorageEventCallback<T>): void;
(type: "open", listener: OpenEventCallback): void;
(type: "close", listener: CloseEventCallback): void;
};

@@ -87,4 +117,2 @@ getStorage: () => LiveStorage;

}) => void;
export declare type OpenEventCallback = () => void;
export declare type CloseEventCallback = () => void;
export declare type CreateRecord = Room["createRecord"];

@@ -97,4 +125,2 @@ export declare type CreateList = Room["createList"];

export declare type RoomEventCallbackMap = {
open: OpenEventCallback;
close: CloseEventCallback;
storage: StorageEventCallback;

@@ -104,8 +130,2 @@ "my-presence": MyPresenceEventCallback;

};
export declare enum RoomState {
Default = 0,
Connecting = 1,
Connected = 2,
Error = 3
}
declare type ClientErrorCallback = (error: Error) => void;

@@ -112,0 +132,0 @@ declare type ClientEventCallbackMap = {

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RoomState = exports.LiveStorageState = void 0;
exports.LiveStorageState = void 0;
var LiveStorageState;

@@ -10,8 +10,1 @@ (function (LiveStorageState) {

})(LiveStorageState = exports.LiveStorageState || (exports.LiveStorageState = {}));
var RoomState;
(function (RoomState) {
RoomState[RoomState["Default"] = 0] = "Default";
RoomState[RoomState["Connecting"] = 1] = "Connecting";
RoomState[RoomState["Connected"] = 2] = "Connected";
RoomState[RoomState["Error"] = 3] = "Error";
})(RoomState = exports.RoomState || (exports.RoomState = {}));

@@ -41,2 +41,17 @@ import { createRoom } from "./room";

}
if (typeof window !== "undefined") {
// TODO: Expose a way to clear these
window.addEventListener("online", () => {
for (const [, room] of rooms) {
room._onNavigatorOnline();
}
});
}
if (typeof document !== "undefined") {
document.addEventListener("visibilitychange", () => {
for (const [, room] of rooms) {
room._onVisibilityChange(document.visibilityState);
}
});
}
return {

@@ -43,0 +58,0 @@ addEventListener,

export type { Record, RecordData, List } from "./doc";
export { createClient } from "./client";
export { RoomState, LiveStorageState } from "./types";
export { LiveStorageState } from "./types";
export type { Others, Presence, Room, InitialStorageFactory, Client, LiveStorage, } from "./types";
export { createClient } from "./client";
export { RoomState, LiveStorageState } from "./types";
export { LiveStorageState } from "./types";

@@ -135,4 +135,3 @@ import { Presence } from "./types";

MAX_NUMBER_OF_CONCURRENT_CONNECTIONS = 4003,
MAX_NUMBER_OF_MESSAGES_PER_DAY_PER_APP = 4004,
INTERNAL_ERROR = 4005
MAX_NUMBER_OF_MESSAGES_PER_DAY_PER_APP = 4004
}

@@ -40,3 +40,2 @@ export var ServerMessageType;

WebsocketCloseCodes[WebsocketCloseCodes["MAX_NUMBER_OF_MESSAGES_PER_DAY_PER_APP"] = 4004] = "MAX_NUMBER_OF_MESSAGES_PER_DAY_PER_APP";
WebsocketCloseCodes[WebsocketCloseCodes["INTERNAL_ERROR"] = 4005] = "INTERNAL_ERROR";
})(WebsocketCloseCodes || (WebsocketCloseCodes = {}));

@@ -1,4 +0,102 @@

import { ClientOptions, Room } from "./types";
import { RecordData, List } from ".";
import { Doc, Record } from "./doc";
import { Others, Presence, ClientOptions, Room, InitialStorageFactory, MyPresenceEventCallback, OthersPresenceEventCallback, StorageEventCallback, AuthEndpoint, LiveStorageState, LiveStorage, EventEventCallback, User, Connection, Serializable } from "./types";
import { ClientMessage, Op } from "./live";
declare type IdFactory = () => string;
export declare type State = {
connection: Connection;
socket: WebSocket | null;
lastFlushTime: number;
flushData: {
presence: Presence | null;
messages: ClientMessage[];
storageOperations: Op[];
};
timeoutHandles: {
flush: number | null;
reconnect: number;
pongTimeout: number;
};
intervalHandles: {
heartbeat: number;
};
listeners: {
storage: StorageEventCallback[];
event: EventEventCallback[];
"others-presence": OthersPresenceEventCallback[];
"my-presence": MyPresenceEventCallback[];
};
me: Presence | null;
others: Others;
users: {
[connectionId: number]: User;
};
idFactory: IdFactory | null;
numberOfRetry: number;
doc: Doc<any> | null;
storageState: LiveStorageState;
initialStorageFactory: InitialStorageFactory | null;
};
export declare type Effects = {
authenticate(): void;
send(messages: ClientMessage[]): void;
delayFlush(delay: number): number;
startHeartbeatInterval(): number;
schedulePongTimeout(): number;
scheduleReconnect(delay: number): number;
};
declare type Context = {
room: string;
authEndpoint: AuthEndpoint;
liveblocksServer: string;
throttleDelay: number;
onError: (error: Error) => void;
};
export declare function makeStateMachine(state: State, context: Context, mockedEffects?: Effects): {
onOpen: () => void;
onClose: (event: {
code: number;
wasClean: boolean;
reason: any;
}) => void;
onMessage: (event: MessageEvent) => void;
authenticationSuccess: (connectionId: number, socket: WebSocket) => void;
heartbeat: () => void;
onNavigatorOnline: () => void;
onVisibilityChange: (visibilityState: VisibilityState) => void;
connect: () => null | undefined;
disconnect: () => void;
addEventListener: {
<T extends Serializable>(type: "my-presence", listener: MyPresenceEventCallback<T>): void;
<T_1 extends Serializable>(type: "others-presence", listener: OthersPresenceEventCallback<T_1>): void;
(type: "event", listener: EventEventCallback): void;
<T_2 extends RecordData>(type: "storage", listener: StorageEventCallback<T_2>): void;
};
removeEventListener: {
<T_3 extends Serializable>(type: "my-presence", listener: MyPresenceEventCallback<T_3>): void;
<T_4 extends Serializable>(type: "others-presence", listener: OthersPresenceEventCallback<T_4>): void;
(type: "event", listener: EventEventCallback): void;
<T_5 extends RecordData>(type: "storage", listener: StorageEventCallback<T_5>): void;
};
updatePresence: <T_6 extends Serializable>(overrides: Partial<T_6>) => void;
broadcastEvent: (event: any) => void;
fetchStorage: (initialStorageFactory: InitialStorageFactory) => void;
createRecord: <T_7 extends RecordData>(data: any) => Record<T_7>;
updateRecord: <T_8 extends RecordData>(record: Record<T_8>, overrides: Partial<T_8>) => void;
createList: <T_9 extends RecordData>() => List<Record<T_9>>;
pushItem: <T_10 extends RecordData>(list: List<Record<T_10>>, item: Record<T_10>) => void;
deleteItem: <T_11 extends RecordData>(list: List<Record<T_11>>, index: number) => void;
moveItem: <T_12 extends RecordData>(list: List<Record<T_12>>, index: number, targetIndex: number) => void;
selectors: {
getListenersCount: () => number;
getConnectionState: () => Connection;
getPresence: <T_13 extends Serializable>() => T_13 | null;
getOthers: <T_14 extends Serializable>() => Others<T_14>;
getStorage: () => LiveStorage;
};
};
export declare function defaultState(): State;
export declare function createRoom(name: string, options: ClientOptions & {
onError: (error: Error) => void;
}): Room;
export {};

@@ -22,3 +22,3 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {

import { Doc } from "./doc";
import { RoomState, LiveStorageState, } from "./types";
import { LiveStorageState, } from "./types";
import { createRecord as innerCreateRecord, createList as innerCreateList, } from "./doc";

@@ -28,21 +28,11 @@ import { remove } from "./utils";

import { ClientMessageType, ServerMessageType, } from "./live";
const BACKOFF_RETRY_DELAYS = [
250,
500,
1000,
2000,
4000,
8000,
10000,
10000,
10000,
10000,
];
const BACKOFF_RETRY_DELAYS = [250, 500, 1000, 2000, 4000, 8000, 10000];
const HEARTBEAT_INTERVAL = 30000;
// const WAKE_UP_CHECK_INTERVAL = 2000;
const PONG_TIMEOUT = 2000;
function isValidRoomEventType(value) {
return (value === "open" ||
value === "storage" ||
return (value === "storage" ||
value === "my-presence" ||
value === "others-presence" ||
value === "event" ||
value === "close");
value === "event");
}

@@ -67,137 +57,109 @@ function makeIdFactory(connectionId) {

}
export function createRoom(name, options) {
const throttleDelay = options.throttle || 100;
const liveblocksServer = options.liveblocksServer || "wss://live.liveblocks.io";
const authEndpoint = options.authEndpoint;
const _listeners = {
open: [],
storage: [],
event: [],
"others-presence": [],
"my-presence": [],
close: [],
};
let _idFactory = null;
let _doc = null;
let _storageState = LiveStorageState.NotInitialized;
let _initialStorageFactory = null;
const _state = {
me: null,
socket: null,
lastFlushTime: 0,
flushTimeout: null,
flushData: {
presence: null,
messages: [],
storageOperations: [],
function log(...params) {
return;
console.log(...params, new Date().toString());
}
export function makeStateMachine(state, context, mockedEffects) {
const effects = mockedEffects || {
authenticate() {
return __awaiter(this, void 0, void 0, function* () {
try {
const token = yield auth(context.authEndpoint, context.room);
const connectionId = parseToken(token).actor;
const socket = new WebSocket(`${context.liveblocksServer}/?token=${token}`);
socket.addEventListener("message", onMessage);
socket.addEventListener("open", onOpen);
socket.addEventListener("close", onClose);
socket.addEventListener("error", onError);
authenticationSuccess(connectionId, socket);
}
catch (er) {
authenticationFailure(er);
}
});
},
send(messageOrMessages) {
if (state.socket == null) {
throw new Error("Can't send message if socket is null");
}
state.socket.send(JSON.stringify(messageOrMessages));
},
delayFlush(delay) {
return setTimeout(tryFlushing, delay);
},
startHeartbeatInterval() {
return setInterval(heartbeat, HEARTBEAT_INTERVAL);
},
schedulePongTimeout() {
return setTimeout(pongTimeout, PONG_TIMEOUT);
},
scheduleReconnect(delay) {
return setTimeout(connect, delay);
},
};
let _users = {};
let _others = makeOthers(_users);
let state = RoomState.Default;
let numberOfRetry = 0;
let retryTimeoutId = 0;
function send(messageOrMessages) {
if (_state.socket == null) {
throw new Error("Can't send message if socket is null");
function addEventListener(type, listener) {
if (!isValidRoomEventType(type)) {
throw new Error(`"${type}" is not a valid event name`);
}
_state.socket.send(JSON.stringify(messageOrMessages));
state.listeners[type].push(listener);
}
function makeId() {
if (_idFactory == null) {
throw new Error("Can't generate id. Id factory is missing.");
function removeEventListener(event, callback) {
if (!isValidRoomEventType(event)) {
throw new Error(`"${event}" is not a valid event name`);
}
return _idFactory();
const callbacks = state.listeners[event];
remove(callbacks, callback);
}
function updateUsers(newUsers) {
_users = newUsers;
_others = makeOthers(newUsers);
for (const listener of _listeners["others-presence"]) {
listener(_others);
}
function getConnectionState() {
return state.connection;
}
function dispatch(op) {
_state.flushData.storageOperations.push(op);
tryFlushing();
function getListenersCount() {
return (state.listeners["my-presence"].length +
state.listeners["others-presence"].length +
state.listeners.storage.length +
state.listeners.event.length);
}
function getStorage() {
if (_storageState === LiveStorageState.Loaded) {
return {
state: _storageState,
root: _doc.root,
};
function connect() {
if (state.connection.state !== "closed" &&
state.connection.state !== "unavailable") {
return null;
}
return {
state: _storageState,
};
updateConnection({ state: "authenticating" });
effects.authenticate();
}
function fetchStorage(initialStorageFactory) {
_initialStorageFactory = initialStorageFactory;
_storageState = LiveStorageState.Loading;
_state.flushData.messages.push({ type: ClientMessageType.FetchStorage });
tryFlushing();
}
function updateDoc(doc) {
_doc = doc;
if (doc) {
for (const listener of _listeners.storage) {
listener(getStorage());
}
function updatePresence(overrides) {
const newPresence = Object.assign(Object.assign({}, state.me), overrides);
if (state.flushData.presence == null) {
state.flushData.presence = overrides;
}
}
function createRecord(data) {
return innerCreateRecord(makeId(), data);
}
function createList() {
return innerCreateList(makeId());
}
function onInitialStorageState(message) {
_storageState = LiveStorageState.Loaded;
if (message.root == null) {
const rootId = makeId();
_doc = Doc.empty(rootId, (op) => dispatch(op));
updateDoc(_doc.updateRecord(rootId, _initialStorageFactory({
createRecord: (data) => createRecord(data),
createList: () => createList(),
})));
}
else {
updateDoc(Doc.load(message.root, (op) => dispatch(op)));
for (const key in overrides) {
state.flushData.presence[key] = overrides[key];
}
}
}
function onStorageUpdates(message) {
if (_doc == null) {
// TODO: Cache updates in case they are coming while root is queried
return;
}
updateDoc(message.ops.reduce((doc, op) => doc.dispatch(op), _doc));
}
function onOpen() {
state = RoomState.Connected;
numberOfRetry = 0;
state.me = newPresence;
tryFlushing();
for (const callback of _listeners.open) {
callback();
for (const listener of state.listeners["my-presence"]) {
listener(state.me);
}
}
function onEvent(message) {
for (const listener of _listeners.event) {
listener({ connectionId: message.actor, event: message.event });
}
function authenticationSuccess(connectionId, socket) {
updateConnection({ state: "connecting", id: connectionId });
state.idFactory = makeIdFactory(connectionId);
state.socket = socket;
}
function onRoomStateMessage(message) {
const newUsers = {};
for (const key in message.users) {
const connectionId = Number.parseInt(key);
const user = message.users[key];
newUsers[connectionId] = {
connectionId,
info: user.info,
id: user.id,
};
function authenticationFailure(error) {
console.error(error);
updateConnection({ state: "unavailable" });
state.numberOfRetry++;
state.timeoutHandles.reconnect = effects.scheduleReconnect(getRetryDelay());
}
function onVisibilityChange(visibilityState) {
if (visibilityState === "visible" && state.connection.state === "open") {
log("Heartbeat after visibility change");
heartbeat();
}
updateUsers(newUsers);
}
function onUpdatePresenceMessage(message) {
const user = _users[message.actor];
const user = state.users[message.actor];
const newUser = user

@@ -214,11 +176,42 @@ ? {

};
updateUsers(Object.assign(Object.assign({}, _users), { [message.actor]: newUser }));
updateUsers(Object.assign(Object.assign({}, state.users), { [message.actor]: newUser }));
}
function updateUsers(newUsers) {
state.users = newUsers;
state.others = makeOthers(newUsers);
for (const listener of state.listeners["others-presence"]) {
listener(state.others);
}
}
function onUserLeftMessage(message) {
const userLeftMessage = message;
const _a = _users, _b = userLeftMessage.actor, notUsed = _a[_b], rest = __rest(_a, [typeof _b === "symbol" ? _b : _b + ""]);
const _a = state.users, _b = userLeftMessage.actor, notUsed = _a[_b], rest = __rest(_a, [typeof _b === "symbol" ? _b : _b + ""]);
updateUsers(rest);
}
function onRoomStateMessage(message) {
const newUsers = {};
for (const key in message.users) {
const connectionId = Number.parseInt(key);
const user = message.users[key];
newUsers[connectionId] = {
connectionId,
info: user.info,
id: user.id,
};
}
updateUsers(newUsers);
}
function onNavigatorOnline() {
if (state.connection.state === "unavailable") {
log("Try to reconnect after connectivity change");
reconnect();
}
}
function onEvent(message) {
for (const listener of state.listeners.event) {
listener({ connectionId: message.actor, event: message.event });
}
}
function onUserJoinedMessage(message) {
updateUsers(Object.assign(Object.assign({}, _users), { [message.actor]: {
updateUsers(Object.assign(Object.assign({}, state.users), { [message.actor]: {
connectionId: message.actor,

@@ -228,12 +221,18 @@ info: message.info,

} }));
// Send current presence to new user
// TODO: Consider storing it on the backend
_state.flushData.messages.push({
type: ClientMessageType.UpdatePresence,
data: _state.me,
targetActor: message.actor,
});
tryFlushing();
if (state.me) {
// Send current presence to new user
// TODO: Consider storing it on the backend
state.flushData.messages.push({
type: ClientMessageType.UpdatePresence,
data: state.me,
targetActor: message.actor,
});
tryFlushing();
}
}
function onMessage(event) {
if (event.data === "pong") {
clearTimeout(state.timeoutHandles.pongTimeout);
return;
}
const message = JSON.parse(event.data);

@@ -271,88 +270,98 @@ switch (message.type) {

}
// function onWakeUp() {
// // Sometimes, the browser can put the webpage on pause (computer is on sleep mode for example)
// // The client will not know that the server has probably close the connection even if the readyState is Open
// // One way to detect this kind of pause is to ensure that a setInterval is not taking more than the delay it was configured with
// if (state.connection.state === "open") {
// log("Try to reconnect after laptop wake up");
// reconnect();
// }
// }
function onClose(event) {
state.socket = null;
clearTimeout(state.timeoutHandles.pongTimeout);
clearInterval(state.intervalHandles.heartbeat);
if (state.timeoutHandles.flush) {
clearTimeout(state.timeoutHandles.flush);
}
clearTimeout(state.timeoutHandles.reconnect);
updateUsers({});
if (event.code >= 4000 && event.code <= 4100) {
options.onError(new Error(event.reason));
updateConnection({ state: "failed" });
context.onError(new Error(event.reason));
}
for (const listener of _listeners.close) {
listener();
else if (event.wasClean === false) {
updateConnection({ state: "unavailable" });
state.numberOfRetry++;
state.timeoutHandles.reconnect = effects.scheduleReconnect(getRetryDelay());
}
_state.socket = null;
updateUsers({});
if (event.wasClean === false) {
state = RoomState.Default;
numberOfRetry++;
retryTimeoutId = setTimeout(() => connect(), BACKOFF_RETRY_DELAYS[numberOfRetry < BACKOFF_RETRY_DELAYS.length
? numberOfRetry
: BACKOFF_RETRY_DELAYS.length - 1]);
else {
updateConnection({ state: "closed" });
}
}
function onError(event) { }
function connect() {
return __awaiter(this, void 0, void 0, function* () {
if (state === RoomState.Connected || state === RoomState.Connecting) {
return;
}
state = RoomState.Connecting;
let token = null;
let actor = null;
try {
token = yield auth(authEndpoint, name);
actor = parseToken(token).actor;
}
catch (er) {
options.onError(er);
state = RoomState.Error;
return;
}
_idFactory = makeIdFactory(actor);
_state.socket = new WebSocket(`${liveblocksServer}/?token=${token}`);
_state.socket.addEventListener("message", onMessage);
_state.socket.addEventListener("open", onOpen);
_state.socket.addEventListener("close", onClose);
_state.socket.addEventListener("error", onError);
});
function updateConnection(connection) {
state.connection = connection;
}
function disconnect() {
if (_state.socket) {
_state.socket.removeEventListener("open", onOpen);
_state.socket.removeEventListener("message", onMessage);
_state.socket.removeEventListener("close", onClose);
_state.socket.removeEventListener("error", onError);
_state.socket.close();
_state.socket = null;
function getRetryDelay() {
return BACKOFF_RETRY_DELAYS[state.numberOfRetry < BACKOFF_RETRY_DELAYS.length
? state.numberOfRetry
: BACKOFF_RETRY_DELAYS.length - 1];
}
function onError() { }
function onOpen() {
clearInterval(state.intervalHandles.heartbeat);
state.intervalHandles.heartbeat = effects.startHeartbeatInterval();
updateConnection({ state: "open", id: state.connection.id });
state.numberOfRetry = 0;
tryFlushing();
}
function heartbeat() {
if (state.socket == null) {
// Should never happen, because we clear the pong timeout when the connection is dropped explictly
return;
}
state = RoomState.Default;
updateUsers({});
clearTimeout(retryTimeoutId);
_listeners.open = [];
_listeners["my-presence"] = [];
_listeners["others-presence"] = [];
_listeners.event = [];
_listeners.storage = [];
_listeners.close = [];
clearTimeout(state.timeoutHandles.pongTimeout);
state.timeoutHandles.pongTimeout = effects.schedulePongTimeout();
if (state.socket.readyState === WebSocket.OPEN) {
state.socket.send("ping");
}
}
//////////////
// Presence //
//////////////
function getPresence() {
return _state.me;
function pongTimeout() {
log("Pong timeout. Trying to reconnect.");
reconnect();
}
function getOthers() {
return _others;
}
function updatePresence(overrides) {
// Create new local presence right away and call listeners
const newPresence = Object.assign(Object.assign({}, _state.me), overrides);
_state.me = newPresence;
for (const listener of _listeners["my-presence"]) {
listener(_state.me);
function reconnect() {
if (state.socket) {
state.socket.removeEventListener("open", onOpen);
state.socket.removeEventListener("message", onMessage);
state.socket.removeEventListener("close", onClose);
state.socket.removeEventListener("error", onError);
state.socket.close();
state.socket = null;
}
updatePresenceToSend(_state, overrides);
tryFlushing();
updateConnection({ state: "unavailable" });
clearTimeout(state.timeoutHandles.pongTimeout);
if (state.timeoutHandles.flush) {
clearTimeout(state.timeoutHandles.flush);
}
clearTimeout(state.timeoutHandles.reconnect);
clearInterval(state.intervalHandles.heartbeat);
connect();
}
function tryFlushing() {
if (state.socket == null) {
return;
}
if (state.socket.readyState !== WebSocket.OPEN) {
return;
}
const now = Date.now();
if (canSend(now, _state, throttleDelay)) {
send(flushDataToMessages(_state));
_state.flushData = {
const elapsedTime = now - state.lastFlushTime;
if (elapsedTime > context.throttleDelay) {
const messages = flushDataToMessages(state);
if (messages.length === 0) {
return;
}
effects.send(messages);
state.flushData = {
messages: [],

@@ -362,28 +371,64 @@ storageOperations: [],

};
_state.lastFlushTime = Date.now();
state.lastFlushTime = now;
}
else {
if (_state.flushTimeout) {
clearTimeout(_state.flushTimeout);
_state.flushTimeout = null;
if (state.timeoutHandles.flush != null) {
clearTimeout(state.timeoutHandles.flush);
}
_state.flushTimeout = setTimeout(() => {
if (isSocketReady(_state)) {
flushDataToMessages(_state);
send(flushDataToMessages(_state));
_state.flushData = {
messages: [],
storageOperations: [],
presence: null,
};
_state.lastFlushTime = Date.now();
}
}, throttleDelay - (now - _state.lastFlushTime));
state.timeoutHandles.flush = effects.delayFlush(context.throttleDelay - (now - state.lastFlushTime));
}
}
function flushDataToMessages(state) {
const messages = [];
if (state.flushData.presence) {
messages.push({
type: ClientMessageType.UpdatePresence,
data: state.flushData.presence,
});
}
for (const event of state.flushData.messages) {
messages.push(event);
}
if (state.flushData.storageOperations.length > 0) {
messages.push({
type: ClientMessageType.UpdateStorage,
ops: state.flushData.storageOperations,
});
}
return messages;
}
function disconnect() {
if (state.socket) {
state.socket.removeEventListener("open", onOpen);
state.socket.removeEventListener("message", onMessage);
state.socket.removeEventListener("close", onClose);
state.socket.removeEventListener("error", onError);
state.socket.close();
state.socket = null;
}
updateConnection({ state: "closed" });
state.me = null;
if (state.timeoutHandles.flush) {
clearTimeout(state.timeoutHandles.flush);
}
clearTimeout(state.timeoutHandles.reconnect);
clearTimeout(state.timeoutHandles.pongTimeout);
clearInterval(state.intervalHandles.heartbeat);
updateUsers({});
state.listeners["my-presence"] = [];
state.listeners["others-presence"] = [];
state.listeners.event = [];
state.listeners.storage = [];
}
function getPresence() {
return state.me;
}
function getOthers() {
return state.others;
}
function broadcastEvent(event) {
if (!isSocketReady(_state)) {
if (state.socket == null) {
return;
}
_state.flushData.messages.push({
state.flushData.messages.push({
type: ClientMessageType.ClientEvent,

@@ -394,95 +439,195 @@ event,

}
function addEventListener(type, listener) {
if (!isValidRoomEventType(type)) {
throw new Error(`"${type}" is not a valid event name`);
/**
* STORAGE
*/
function onStorageUpdates(message) {
if (state.doc == null) {
// TODO: Cache updates in case they are coming while root is queried
return;
}
_listeners[type].push(listener);
updateDoc(message.ops.reduce((doc, op) => doc.dispatch(op), state.doc));
}
function removeEventListener(event, callback) {
if (!isValidRoomEventType(event)) {
throw new Error(`"${event}" is not a valid event name`);
function updateDoc(doc) {
state.doc = doc;
if (doc) {
for (const listener of state.listeners.storage) {
listener(getStorage());
}
}
const callbacks = _listeners[event];
remove(callbacks, callback);
}
function getStorage() {
if (state.storageState === LiveStorageState.Loaded) {
return {
state: state.storageState,
root: state.doc.root,
};
}
return {
state: state.storageState,
};
}
function onInitialStorageState(message) {
state.storageState = LiveStorageState.Loaded;
if (message.root == null) {
const rootId = makeId();
state.doc = Doc.empty(rootId, (op) => dispatch(op));
updateDoc(state.doc.updateRecord(rootId, state.initialStorageFactory({
createRecord: (data) => createRecord(data),
createList: () => createList(),
})));
}
else {
updateDoc(Doc.load(message.root, (op) => dispatch(op)));
}
}
function makeId() {
if (state.idFactory == null) {
throw new Error("Can't generate id. Id factory is missing.");
}
return state.idFactory();
}
function dispatch(op) {
state.flushData.storageOperations.push(op);
tryFlushing();
}
function createRecord(data) {
return innerCreateRecord(makeId(), data);
}
function createList() {
return innerCreateList(makeId());
}
function fetchStorage(initialStorageFactory) {
state.initialStorageFactory = initialStorageFactory;
state.storageState = LiveStorageState.Loading;
state.flushData.messages.push({ type: ClientMessageType.FetchStorage });
tryFlushing();
}
function updateRecord(record, overrides) {
updateDoc(state.doc.updateRecord(record.id, overrides));
}
function pushItem(list, item) {
updateDoc(state.doc.pushItem(list.id, item));
}
function deleteItem(list, index) {
updateDoc(state.doc.deleteItem(list.id, index));
}
function moveItem(list, index, targetIndex) {
updateDoc(state.doc.moveItem(list.id, index, targetIndex));
}
return {
connect() {
connect();
},
// Internal
onOpen,
onClose,
onMessage,
authenticationSuccess,
heartbeat,
onNavigatorOnline,
// onWakeUp,
onVisibilityChange,
// Core
connect,
disconnect,
getListenersCount() {
return (_listeners.open.length +
_listeners["my-presence"].length +
_listeners["others-presence"].length +
_listeners.storage.length +
_listeners.close.length +
_listeners.event.length);
},
getState() {
return state;
},
addEventListener,
removeEventListener,
/////////////
// Storage //
/////////////
getStorage,
// Presence
updatePresence,
broadcastEvent,
// Storage
fetchStorage,
createRecord,
updateRecord,
createList,
updateRecord(record, overrides) {
updateDoc(_doc.updateRecord(record.id, overrides));
pushItem,
deleteItem,
moveItem,
selectors: {
// Core
getListenersCount,
getConnectionState,
// Presence
getPresence,
getOthers,
// Storage
getStorage,
},
pushItem(list, item) {
updateDoc(_doc.pushItem(list.id, item));
};
}
export function defaultState() {
return {
connection: { state: "closed" },
socket: null,
listeners: {
storage: [],
event: [],
"others-presence": [],
"my-presence": [],
},
deleteItem(list, index) {
updateDoc(_doc.deleteItem(list.id, index));
numberOfRetry: 0,
lastFlushTime: 0,
timeoutHandles: {
flush: null,
reconnect: 0,
pongTimeout: 0,
},
moveItem(list, index, targetIndex) {
updateDoc(_doc.moveItem(list.id, index, targetIndex));
flushData: {
presence: null,
messages: [],
storageOperations: [],
},
intervalHandles: {
heartbeat: 0,
},
me: null,
users: {},
others: makeOthers({}),
storageState: LiveStorageState.NotInitialized,
initialStorageFactory: null,
doc: null,
idFactory: null,
};
}
export function createRoom(name, options) {
const throttleDelay = options.throttle || 100;
const liveblocksServer = options.liveblocksServer || "wss://live.liveblocks.io";
const authEndpoint = options.authEndpoint;
const state = defaultState();
const machine = makeStateMachine(state, {
throttleDelay,
liveblocksServer,
authEndpoint,
onError: options.onError,
room: name,
});
const room = {
/////////////
// Core //
/////////////
connect: machine.connect,
disconnect: machine.disconnect,
getConnectionState: machine.selectors.getConnectionState,
getListenersCount: machine.selectors.getListenersCount,
addEventListener: machine.addEventListener,
removeEventListener: machine.removeEventListener,
/////////////
// Storage //
/////////////
getStorage: machine.selectors.getStorage,
fetchStorage: machine.fetchStorage,
createRecord: machine.createRecord,
createList: machine.createList,
updateRecord: machine.updateRecord,
pushItem: machine.pushItem,
deleteItem: machine.deleteItem,
moveItem: machine.moveItem,
//////////////
// Presence //
//////////////
getPresence,
updatePresence,
getOthers,
broadcastEvent,
getPresence: machine.selectors.getPresence,
updatePresence: machine.updatePresence,
getOthers: machine.selectors.getOthers,
broadcastEvent: machine.broadcastEvent,
};
room._onNavigatorOnline = machine.onNavigatorOnline;
room._onVisibilityChange = machine.onVisibilityChange;
return room;
}
function flushDataToMessages(state) {
const messages = [];
if (state.flushData.presence) {
messages.push({
type: ClientMessageType.UpdatePresence,
data: state.flushData.presence,
});
}
for (const event of state.flushData.messages) {
messages.push(event);
}
if (state.flushData.storageOperations.length > 0) {
messages.push({
type: ClientMessageType.UpdateStorage,
ops: state.flushData.storageOperations,
});
}
return messages;
}
function isSocketReady(state) {
return state.socket !== null && state.socket.readyState === WebSocket.OPEN;
}
function canSend(now, state, wait) {
return isSocketReady(state) && now - state.lastFlushTime > wait;
}
function updatePresenceToSend(state, overrides) {
if (state.flushData.presence == null) {
state.flushData.presence = overrides;
}
else {
for (const key in overrides) {
state.flushData.presence[key] = overrides[key];
}
}
}
import { RecordData, Record, List } from "./doc";
export declare type User<T extends Presence = Presence> = {
connectionId: number;
id?: string;
info?: any;
presence?: T;
};
/**
* Public
* Represents all the other users conencted in the room. Treated as immutable.
*/
export interface Others<T extends Presence = Presence> {
export interface Others<TPresence extends Presence = Presence> {
/**
* Number of other users in the room.
*/
readonly count: number;
toArray(): Array<User<T>>;
map<U>(callback: (user: User<T>) => U): Array<U>;
/**
* Returns the array of connected users in room.
*/
toArray(): User<TPresence>[];
/**
* This function let you map over the connected users in the room.
*/
map<U>(callback: (user: User<TPresence>) => U): U[];
}
/**
* Represents a user connected in a room. Treated as immutable.
*/
export declare type User<TPresence extends Presence = Presence> = {
/**
* The connection id of the user. It is unique and increment at every new connection.
*/
readonly connectionId: number;
/**
* The id of the user that has been set in the authentication endpoint.
* Useful to get additional information about the connected user.
*/
readonly id?: string;
/**
* Additional user information that has been set in the authentication endpoint.
*/
readonly info?: any;
/**
* The user presence.
*/
readonly presence?: TPresence;
};
export declare type Presence = Serializable;
export declare type SerializablePrimitive = boolean | string | number | null;
export declare type Serializable = {
[key: string]: SerializablePrimitive | Serializable | Array<SerializablePrimitive>;
[key: string]: SerializablePrimitive | Serializable | SerializablePrimitive[];
};

@@ -24,4 +49,7 @@ declare type AuthEndpointCallback = (room: string) => Promise<string>;

export declare type ClientOptions = {
/**
* The authentication endpoint that is called to ensure that the current user has access to a room.
* Can be an url or a callback if you need to attach additional headers.
*/
authEndpoint: AuthEndpoint;
liveblocksServer?: string;
throttle?: number;

@@ -43,7 +71,13 @@ };

};
export declare type Connection = {
state: "closed" | "authenticating" | "unavailable" | "failed";
} | {
state: "open" | "connecting";
id: number;
};
export declare type Room = {
connect(): void;
disconnect(): void;
getConnectionState(): Connection;
getListenersCount(): number;
getState(): RoomState;
addEventListener: {

@@ -54,4 +88,2 @@ <T extends Presence>(type: "my-presence", listener: MyPresenceEventCallback<T>): void;

<T extends RecordData>(type: "storage", listener: StorageEventCallback<T>): void;
(type: "open", listener: OpenEventCallback): void;
(type: "close", listener: CloseEventCallback): void;
};

@@ -63,4 +95,2 @@ removeEventListener: {

<T extends RecordData>(type: "storage", listener: StorageEventCallback<T>): void;
(type: "open", listener: OpenEventCallback): void;
(type: "close", listener: CloseEventCallback): void;
};

@@ -87,4 +117,2 @@ getStorage: () => LiveStorage;

}) => void;
export declare type OpenEventCallback = () => void;
export declare type CloseEventCallback = () => void;
export declare type CreateRecord = Room["createRecord"];

@@ -97,4 +125,2 @@ export declare type CreateList = Room["createList"];

export declare type RoomEventCallbackMap = {
open: OpenEventCallback;
close: CloseEventCallback;
storage: StorageEventCallback;

@@ -104,8 +130,2 @@ "my-presence": MyPresenceEventCallback;

};
export declare enum RoomState {
Default = 0,
Connecting = 1,
Connected = 2,
Error = 3
}
declare type ClientErrorCallback = (error: Error) => void;

@@ -112,0 +132,0 @@ declare type ClientEventCallbackMap = {

@@ -7,8 +7,1 @@ export var LiveStorageState;

})(LiveStorageState || (LiveStorageState = {}));
export var RoomState;
(function (RoomState) {
RoomState[RoomState["Default"] = 0] = "Default";
RoomState[RoomState["Connecting"] = 1] = "Connecting";
RoomState[RoomState["Connected"] = 2] = "Connected";
RoomState[RoomState["Error"] = 3] = "Error";
})(RoomState || (RoomState = {}));
{
"name": "@liveblocks/client",
"version": "0.6.0-beta.3",
"version": "0.6.0-beta.4",
"description": "",

@@ -5,0 +5,0 @@ "main": "./lib/cjs/index.js",

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