@liveblocks/client
Advanced tools
Comparing version 0.6.0-beta.3 to 0.6.0-beta.4
@@ -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, |
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", |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
127587
3581