@liveblocks/react
Advanced tools
Comparing version 0.9.0 to 0.10.0
@@ -152,3 +152,2 @@ import { Client, RecordData, Others, Presence, Record, InitialStorageFactory, Room, User } from "@liveblocks/client"; | ||
export declare function useStorageActions(): StorageActions; | ||
export { createClient } from "@liveblocks/client"; | ||
export type { Record, Client, List } from "@liveblocks/client"; | ||
export {}; |
1328
lib/index.js
@@ -47,559 +47,2 @@ Object.defineProperty(exports, '__esModule', { value: true }); | ||
const min = 32; | ||
const max = 127; | ||
function makePosition(before, after) { | ||
// No children | ||
if (before == null && after == null) { | ||
return pos([min + 1]); | ||
} | ||
// Insert at the end | ||
if (before != null && after == null) { | ||
return getNextPosition(before); | ||
} | ||
// Insert at the start | ||
if (before == null && after != null) { | ||
return getPreviousPosition(after); | ||
} | ||
return pos(makePositionFromCodes(posCodes(before), posCodes(after))); | ||
} | ||
function getPreviousPosition(after) { | ||
const result = []; | ||
const afterCodes = posCodes(after); | ||
for (let i = 0; i < afterCodes.length; i++) { | ||
const code = afterCodes[i]; | ||
if (code <= min + 1) { | ||
result.push(min); | ||
if (afterCodes.length - 1 === i) { | ||
result.push(max - 1); | ||
break; | ||
} | ||
} | ||
else { | ||
result.push(code - 1); | ||
break; | ||
} | ||
} | ||
return pos(result); | ||
} | ||
function getNextPosition(before) { | ||
const result = []; | ||
const beforeCodes = posCodes(before); | ||
for (let i = 0; i < beforeCodes.length; i++) { | ||
const code = beforeCodes[i]; | ||
if (code === max - 1) { | ||
result.push(code); | ||
if (beforeCodes.length - 1 === i) { | ||
result.push(min + 1); | ||
break; | ||
} | ||
} | ||
else { | ||
result.push(code + 1); | ||
break; | ||
} | ||
} | ||
return pos(result); | ||
} | ||
function makePositionFromCodes(before, after) { | ||
let index = 0; | ||
const result = []; | ||
while (true) { | ||
const beforeDigit = before[index] || min; | ||
const afterDigit = after[index] || max; | ||
if (beforeDigit > afterDigit) { | ||
throw new Error(`Impossible to generate position between ${before} and ${after}`); | ||
} | ||
if (beforeDigit === afterDigit) { | ||
result.push(beforeDigit); | ||
index++; | ||
continue; | ||
} | ||
if (afterDigit - beforeDigit === 1) { | ||
result.push(beforeDigit); | ||
result.push(...makePositionFromCodes(before.slice(index + 1), [])); | ||
break; | ||
} | ||
const mid = beforeDigit + Math.floor((afterDigit - beforeDigit) / 2); | ||
result.push(mid); | ||
break; | ||
} | ||
return result; | ||
} | ||
function posCodes(str) { | ||
const codes = []; | ||
for (let i = 0; i < str.length; i++) { | ||
codes.push(str.charCodeAt(i)); | ||
} | ||
return codes; | ||
} | ||
function pos(codes) { | ||
return String.fromCharCode(...codes); | ||
} | ||
function compare(itemA, itemB) { | ||
const aCodes = posCodes(itemA.position); | ||
const bCodes = posCodes(itemB.position); | ||
const maxLength = Math.max(aCodes.length, bCodes.length); | ||
for (let i = 0; i < maxLength; i++) { | ||
const a = aCodes[i] == null ? min : aCodes[i]; | ||
const b = bCodes[i] == null ? min : bCodes[i]; | ||
if (a === b) { | ||
continue; | ||
} | ||
else { | ||
return a - b; | ||
} | ||
} | ||
throw new Error(`Impossible to compare similar position "${itemA.position}" and "${itemB.position}"`); | ||
} | ||
const RECORD = Symbol("liveblocks.record"); | ||
const LIST = Symbol("liveblocks.list"); | ||
function createRecord(id, data) { | ||
return Object.assign({ id, $$type: RECORD }, data); | ||
} | ||
function createList(id, items = []) { | ||
return { | ||
id, | ||
$$type: LIST, | ||
length: items.length, | ||
toArray: () => items, | ||
map: (callback) => items.map(callback), | ||
}; | ||
} | ||
function noop() { } | ||
class Doc { | ||
constructor(root, _cache, _emit) { | ||
this.root = root; | ||
this._cache = _cache; | ||
this._emit = _emit; | ||
} | ||
static empty(id = "root", emit = noop) { | ||
const root = { | ||
id, | ||
$$type: RECORD, | ||
}; | ||
return new Doc(root, { links: new Map(), listCache: new Map() }, emit); | ||
} | ||
static createFromRoot(data, id = "root", emit = noop) { | ||
let doc = Doc.empty(id, emit); | ||
doc = doc.updateRecord(doc.root.id, data); | ||
return doc; | ||
} | ||
static load(root, emit = noop) { | ||
let doc = Doc.empty(root.id, emit); | ||
return doc.dispatch({ | ||
type: OpType.RecordUpdate, | ||
id: root.id, | ||
data: root.data, | ||
}); | ||
} | ||
get data() { | ||
return this.root; | ||
} | ||
dispatch(op, shouldEmit = false) { | ||
if (shouldEmit) { | ||
this._emit(op); | ||
} | ||
if (op.id === this.root.id) { | ||
const node = dispatch(this.root, op, this._cache, []); | ||
return new Doc(node, this._cache, this._emit); | ||
} | ||
else { | ||
const links = getAllLinks(op.id, this.root.id, this._cache.links); | ||
const node = dispatch(this.root, op, this._cache, links); | ||
return new Doc(node, this._cache, this._emit); | ||
} | ||
} | ||
getChild(id) { | ||
if (id === this.root.id) { | ||
return this.root; | ||
} | ||
const allLinks = getAllLinks(id, this.root.id, this._cache.links); | ||
return getChildDeep(this.root, id, allLinks, this._cache); | ||
} | ||
updateRecord(id, overrides) { | ||
const currentRecord = this.getChild(id); | ||
if (currentRecord == null) { | ||
throw new Error(`Record with id "${id}" does not exist`); | ||
} | ||
let data = {}; | ||
for (const key in overrides) { | ||
const value = overrides[key]; | ||
data[key] = serialize(value); | ||
} | ||
const op = { | ||
id: currentRecord.id, | ||
type: OpType.RecordUpdate, | ||
data, | ||
}; | ||
return this.dispatch(op, true); | ||
} | ||
pushItem(id, item) { | ||
const list = this.getChild(id); | ||
if (list == null) { | ||
throw new Error(`List with id "${id}" does not exist`); | ||
} | ||
if (list.$$type !== LIST) { | ||
throw new Error(`Node with id "${id}" is not a list`); | ||
} | ||
if (!isRecord(item)) { | ||
throw new Error("List can't only have Record as children"); | ||
} | ||
const data = serialize(item); | ||
if (list.length === 0) { | ||
return this.dispatch({ | ||
type: OpType.ListInsert, | ||
id: list.id, | ||
position: makePosition(), | ||
data, | ||
}, true); | ||
} | ||
const items = sortedListItems(getListItems(this._cache, id)); | ||
const [tailPosition] = items[items.length - 1]; | ||
const position = makePosition(tailPosition); | ||
const operation = { | ||
type: OpType.ListInsert, | ||
id: list.id, | ||
position, | ||
data, | ||
}; | ||
return this.dispatch(operation, true); | ||
} | ||
moveItem(id, index, targetIndex) { | ||
const list = this.getChild(id); | ||
if (list == null) { | ||
throw new Error(`List with id "${id}" does not exist`); | ||
} | ||
if (list.$$type !== LIST) { | ||
throw new Error(`Node with id "${id}" is not a list`); | ||
} | ||
const items = sortedListItems(getListItems(this._cache, id)); | ||
if (targetIndex < 0) { | ||
throw new Error("targetIndex cannot be less than 0"); | ||
} | ||
if (targetIndex >= items.length) { | ||
throw new Error("targetIndex cannot be greater or equal than the list length"); | ||
} | ||
if (index < 0) { | ||
throw new Error("index cannot be less than 0"); | ||
} | ||
if (index >= items.length) { | ||
throw new Error("index cannot be greater or equal than the list length"); | ||
} | ||
if (index === targetIndex) { | ||
return this; | ||
} | ||
let beforePosition = null; | ||
let afterPosition = null; | ||
if (index < targetIndex) { | ||
afterPosition = | ||
targetIndex === items.length - 1 | ||
? undefined | ||
: items[targetIndex + 1][0]; | ||
beforePosition = items[targetIndex][0]; | ||
} | ||
else { | ||
afterPosition = items[targetIndex][0]; | ||
beforePosition = | ||
targetIndex === 0 ? undefined : items[targetIndex - 1][0]; | ||
} | ||
const position = makePosition(beforePosition, afterPosition); | ||
const [, item] = items[index]; | ||
return this.dispatch({ | ||
type: OpType.ListMove, | ||
id: list.id, | ||
itemId: item.id, | ||
position, | ||
}, true); | ||
} | ||
deleteItem(id, index) { | ||
const list = this.getChild(id); | ||
if (list == null) { | ||
throw new Error(`List with id "${id}" does not exist`); | ||
} | ||
if (list.$$type !== LIST) { | ||
throw new Error(`Node with id "${id}" is not a list`); | ||
} | ||
const items = sortedListItems(getListItems(this._cache, id)); | ||
const [, item] = items[index]; | ||
return this.dispatch({ | ||
type: OpType.ListRemove, | ||
id: list.id, | ||
itemId: item.id, | ||
}, true); | ||
} | ||
deleteItemById(id, itemId) { | ||
const list = this.getChild(id); | ||
if (list == null) { | ||
throw new Error(`List with id "${id}" does not exist`); | ||
} | ||
if (list.$$type !== LIST) { | ||
throw new Error(`Node with id "${id}" is not a list`); | ||
} | ||
const itemsMap = getListItems(this._cache, id); | ||
let item = null; | ||
for (const [, crdt] of itemsMap) { | ||
if (crdt.id === itemId) { | ||
item = crdt; | ||
break; | ||
} | ||
} | ||
if (item == null) { | ||
throw new Error(`List with id "${id}" does not have an item with id "${itemId}"`); | ||
} | ||
return this.dispatch({ | ||
type: OpType.ListRemove, | ||
id: list.id, | ||
itemId: item.id, | ||
}, true); | ||
} | ||
} | ||
function getAllLinks(id, rootId, links) { | ||
let currentId = id; | ||
const result = []; | ||
do { | ||
const link = links.get(currentId); | ||
if (link == null) { | ||
throw new Error(`Can't find link for id "${currentId}"`); | ||
} | ||
currentId = link.parentId; | ||
result.push(link); | ||
} while (currentId !== rootId); | ||
return result; | ||
} | ||
function deserializeList(serialized, cache) { | ||
const listItems = new Map(); | ||
for (const position in serialized.data) { | ||
const item = deserialize(serialized.data[position], cache); | ||
if (!isRecord(item)) { | ||
throw new Error("TODO"); | ||
} | ||
listItems.set(position, item); | ||
cache.links.set(item.id, { parentId: serialized.id, parentKey: position }); | ||
} | ||
cache.listCache.set(serialized.id, listItems); | ||
return createList(serialized.id, listItemsToArray(listItems)); | ||
} | ||
function getListItems(cache, listId) { | ||
const items = cache.listCache.get(listId); | ||
if (items == null) { | ||
throw new Error(`Can't find list cache for id "${listId}"`); | ||
} | ||
return items; | ||
} | ||
function deserializeRecord(serialized, cache) { | ||
const result = { | ||
id: serialized.id, | ||
$$type: RECORD, | ||
}; | ||
for (const key in serialized.data) { | ||
const item = deserialize(serialized.data[key], cache); | ||
if (isCrdt(item)) { | ||
cache.links.set(item.id, { | ||
parentId: serialized.id, | ||
parentKey: key, | ||
}); | ||
} | ||
result[key] = item; | ||
} | ||
return result; | ||
} | ||
function deserialize(serialized, cache) { | ||
switch (serialized.type) { | ||
case CrdtType.Register: { | ||
return serialized.data; | ||
} | ||
case CrdtType.Record: { | ||
return deserializeRecord(serialized, cache); | ||
} | ||
case CrdtType.List: { | ||
return deserializeList(serialized, cache); | ||
} | ||
default: { | ||
throw new Error("TODO"); | ||
} | ||
} | ||
} | ||
function dispatchOnRecord(record, op, cache, links) { | ||
if (links.length === 0) { | ||
if (record.id !== op.id) { | ||
throw new Error("TODO"); | ||
} | ||
switch (op.type) { | ||
case OpType.RecordUpdate: { | ||
return updateRecord(record, op, cache); | ||
} | ||
default: { | ||
console.warn("Unsupported operation"); | ||
return record; | ||
} | ||
} | ||
} | ||
const currentLink = links.pop(); | ||
const child = record[currentLink.parentKey]; | ||
const newNode = dispatch(child, op, cache, links); | ||
return Object.assign(Object.assign({}, record), { [currentLink.parentKey]: newNode }); | ||
} | ||
function dispatchOnList(list, op, cache, links) { | ||
if (links.length === 0) { | ||
if (list.id !== op.id) { | ||
throw new Error("TODO"); | ||
} | ||
switch (op.type) { | ||
case OpType.ListInsert: { | ||
return listInsert(list, op, cache); | ||
} | ||
case OpType.ListMove: { | ||
return listMove(list, op, cache); | ||
} | ||
case OpType.ListRemove: { | ||
return listDelete(list, op, cache); | ||
} | ||
default: { | ||
console.warn("Unsupported operation"); | ||
return list; | ||
} | ||
} | ||
} | ||
const currentLink = links.pop(); | ||
const position = currentLink.parentKey; | ||
const items = getListItems(cache, list.id); | ||
const item = items.get(position); | ||
if (item == null) { | ||
throw new Error("TODO"); | ||
} | ||
const newItem = dispatch(item, op, cache, links); | ||
items.set(position, newItem); | ||
return createList(list.id, listItemsToArray(items)); | ||
} | ||
function dispatch(node, op, cache, links) { | ||
switch (node.$$type) { | ||
case RECORD: | ||
return dispatchOnRecord(node, op, cache, links); | ||
case LIST: | ||
return dispatchOnList(node, op, cache, links); | ||
default: { | ||
throw new Error("Unknown CRDT"); | ||
} | ||
} | ||
} | ||
function updateRecord(node, op, cache) { | ||
const result = Object.assign({}, node); | ||
for (const key in op.data) { | ||
const value = op.data[key]; | ||
const item = deserialize(value, cache); | ||
if (isCrdt(item)) { | ||
cache.links.set(item.id, { parentId: node.id, parentKey: key }); | ||
} | ||
result[key] = item; | ||
} | ||
return result; | ||
} | ||
function listInsert(list, op, cache) { | ||
const items = getListItems(cache, list.id); | ||
const item = deserialize(op.data, cache); | ||
if (isCrdt(item)) { | ||
items.set(op.position, item); | ||
cache.links.set(item.id, { parentId: list.id, parentKey: op.position }); | ||
} | ||
return createList(list.id, listItemsToArray(items)); | ||
} | ||
function listMove(list, op, cache) { | ||
const items = getListItems(cache, list.id); | ||
const link = getLinkOrThrow(cache, op.itemId); | ||
const item = items.get(link.parentKey); | ||
if (item == null) { | ||
throw new Error("TODO"); | ||
} | ||
// Delete old position cache entry | ||
items.delete(link.parentKey); | ||
// Insert new position in cache | ||
items.set(op.position, item); | ||
// Update link | ||
cache.links.set(op.itemId, { parentId: list.id, parentKey: op.position }); | ||
return createList(list.id, listItemsToArray(items)); | ||
} | ||
function getLinkOrThrow(cache, id) { | ||
const link = cache.links.get(id); | ||
if (link == null) { | ||
throw new Error(`Can't find link with id "${id}"`); | ||
} | ||
return link; | ||
} | ||
function listDelete(list, op, cache) { | ||
const items = getListItems(cache, list.id); | ||
const link = getLinkOrThrow(cache, op.itemId); | ||
items.delete(link.parentKey); | ||
cache.links.delete(op.itemId); | ||
return createList(list.id, listItemsToArray(items)); | ||
} | ||
function listItemsToArray(items) { | ||
return sortedListItems(items).map((entry) => entry[1]); | ||
} | ||
function sortedListItems(items) { | ||
return Array.from(items.entries()).sort((entryA, entryB) => compare({ position: entryA[0] }, { position: entryB[0] })); | ||
} | ||
function getChildDeep(node, id, links, cache) { | ||
let currentNode = node; | ||
while (currentNode.id !== id) { | ||
const link = links.pop(); | ||
if (link == null || link.parentId !== currentNode.id) { | ||
throw new Error("TODO"); | ||
} | ||
if (currentNode.$$type === RECORD) { | ||
currentNode = currentNode[link.parentKey]; | ||
} | ||
else { | ||
const listItems = getListItems(cache, currentNode.id); | ||
const item = listItems.get(link.parentKey); | ||
if (item == null) { | ||
throw new Error("TODO"); | ||
} | ||
currentNode = item; | ||
} | ||
} | ||
return currentNode; | ||
} | ||
function isRecord(value) { | ||
return value != null && typeof value === "object" && value.$$type === RECORD; | ||
} | ||
function isList(value) { | ||
return value != null && typeof value === "object" && value.$$type === LIST; | ||
} | ||
function isCrdt(value) { | ||
return isRecord(value) || isList(value); | ||
} | ||
function serializeRecord(record) { | ||
const serializedData = {}; | ||
for (const key in record) { | ||
if (key !== "id" && key !== "$$type") { | ||
const value = record[key]; // TODO: Find out why typescript does not like that | ||
serializedData[key] = serialize(value); | ||
} | ||
} | ||
return { | ||
id: record.id, | ||
type: CrdtType.Record, | ||
data: serializedData, | ||
}; | ||
} | ||
function serializeList(list) { | ||
return { | ||
id: list.id, | ||
type: CrdtType.List, | ||
data: {}, | ||
}; | ||
} | ||
function serialize(value) { | ||
if (isRecord(value)) { | ||
return serializeRecord(value); | ||
} | ||
else if (isList(value)) { | ||
return serializeList(value); | ||
} | ||
else { | ||
return { type: CrdtType.Register, data: value }; | ||
} | ||
} | ||
var LiveStorageState; | ||
@@ -612,12 +55,3 @@ (function (LiveStorageState) { | ||
function remove(array, item) { | ||
for (let i = 0; i < array.length; i++) { | ||
if (array[i] === item) { | ||
array.splice(i, 1); | ||
break; | ||
} | ||
} | ||
} | ||
var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
(undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
@@ -631,58 +65,4 @@ return new (P || (P = Promise))(function (resolve, reject) { | ||
}; | ||
function fetchAuthorize(endpoint, room) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const res = yield fetch(endpoint, { | ||
method: "POST", | ||
headers: { | ||
"Content-Type": "application/json", | ||
}, | ||
body: JSON.stringify({ | ||
room, | ||
}), | ||
}); | ||
if (!res.ok) { | ||
throw new AuthenticationError(`Authentication error. Liveblocks could not parse the response of your authentication "${endpoint}"`); | ||
} | ||
let authResponse = null; | ||
try { | ||
authResponse = yield res.json(); | ||
} | ||
catch (er) { | ||
throw new AuthenticationError(`Authentication error. Liveblocks could not parse the response of your authentication "${endpoint}"`); | ||
} | ||
if (typeof authResponse.token !== "string") { | ||
throw new AuthenticationError(`Authentication error. Liveblocks could not parse the response of your authentication "${endpoint}"`); | ||
} | ||
return authResponse.token; | ||
}); | ||
} | ||
function auth(endpoint, room) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
if (typeof endpoint === "string") { | ||
return fetchAuthorize(endpoint, room); | ||
} | ||
if (typeof endpoint === "function") { | ||
return endpoint(room); | ||
} | ||
throw new Error("Authentication error. Liveblocks could not parse the response of your authentication endpoint"); | ||
}); | ||
} | ||
class AuthenticationError extends Error { | ||
constructor(message) { | ||
super(message); | ||
} | ||
} | ||
function parseToken(token) { | ||
const tokenParts = token.split("."); | ||
if (tokenParts.length !== 3) { | ||
throw new AuthenticationError(`Authentication error. Liveblocks could not parse the response of your authentication endpoint`); | ||
} | ||
const data = JSON.parse(atob(tokenParts[1])); | ||
if (typeof data.actor !== "number") { | ||
throw new AuthenticationError(`Authentication error. Liveblocks could not parse the response of your authentication endpoint`); | ||
} | ||
return data; | ||
} | ||
var __awaiter$1 = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
(undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
@@ -696,706 +76,3 @@ return new (P || (P = Promise))(function (resolve, reject) { | ||
}; | ||
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 === "storage" || | ||
value === "my-presence" || | ||
value === "others" || | ||
value === "event" || | ||
value === "error" || | ||
value === "connection"); | ||
} | ||
function makeIdFactory(connectionId) { | ||
let count = 0; | ||
return () => `${connectionId}:${count++}`; | ||
} | ||
function makeOthers(presenceMap) { | ||
const array = Object.values(presenceMap); | ||
return { | ||
get count() { | ||
return array.length; | ||
}, | ||
map(callback) { | ||
return array.map(callback); | ||
}, | ||
toArray() { | ||
return array; | ||
}, | ||
}; | ||
} | ||
function makeStateMachine(state, context, mockedEffects) { | ||
const effects = mockedEffects || { | ||
authenticate() { | ||
return __awaiter$1(this, void 0, void 0, function* () { | ||
try { | ||
const token = yield auth(context.authEndpoint, context.room); | ||
const parsedToken = parseToken(token); | ||
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(parsedToken, 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); | ||
}, | ||
}; | ||
function subscribe(type, listener) { | ||
if (!isValidRoomEventType(type)) { | ||
throw new Error(`"${type}" is not a valid event name`); | ||
} | ||
state.listeners[type].push(listener); | ||
} | ||
function unsubscribe(event, callback) { | ||
if (!isValidRoomEventType(event)) { | ||
throw new Error(`"${event}" is not a valid event name`); | ||
} | ||
const callbacks = state.listeners[event]; | ||
remove(callbacks, callback); | ||
} | ||
function getConnectionState() { | ||
return state.connection.state; | ||
} | ||
function getCurrentUser() { | ||
return state.connection.state === "open" || | ||
state.connection.state === "connecting" | ||
? { | ||
connectionId: state.connection.id, | ||
id: state.connection.userId, | ||
info: state.connection.userInfo, | ||
presence: getPresence(), | ||
} | ||
: null; | ||
} | ||
function connect() { | ||
if (typeof window === "undefined") { | ||
return; | ||
} | ||
if (state.connection.state !== "closed" && | ||
state.connection.state !== "unavailable") { | ||
return null; | ||
} | ||
updateConnection({ state: "authenticating" }); | ||
effects.authenticate(); | ||
} | ||
function updatePresence(overrides) { | ||
const newPresence = Object.assign(Object.assign({}, state.me), overrides); | ||
if (state.flushData.presence == null) { | ||
state.flushData.presence = overrides; | ||
} | ||
else { | ||
for (const key in overrides) { | ||
state.flushData.presence[key] = overrides[key]; | ||
} | ||
} | ||
state.me = newPresence; | ||
tryFlushing(); | ||
for (const listener of state.listeners["my-presence"]) { | ||
listener(state.me); | ||
} | ||
} | ||
function authenticationSuccess(token, socket) { | ||
updateConnection({ | ||
state: "connecting", | ||
id: token.actor, | ||
userInfo: token.info, | ||
userId: token.id, | ||
}); | ||
state.idFactory = makeIdFactory(token.actor); | ||
state.socket = socket; | ||
} | ||
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") { | ||
heartbeat(); | ||
} | ||
} | ||
function onUpdatePresenceMessage(message) { | ||
const user = state.users[message.actor]; | ||
if (user == null) { | ||
state.users[message.actor] = { | ||
connectionId: message.actor, | ||
presence: message.data, | ||
}; | ||
} | ||
else { | ||
state.users[message.actor] = { | ||
id: user.id, | ||
info: user.info, | ||
connectionId: message.actor, | ||
presence: Object.assign(Object.assign({}, user.presence), message.data), | ||
}; | ||
} | ||
updateUsers({ | ||
type: "update", | ||
updates: message.data, | ||
user: state.users[message.actor], | ||
}); | ||
} | ||
function updateUsers(event) { | ||
state.others = makeOthers(state.users); | ||
for (const listener of state.listeners["others"]) { | ||
listener(state.others, event); | ||
} | ||
} | ||
function onUserLeftMessage(message) { | ||
const userLeftMessage = message; | ||
const user = state.users[userLeftMessage.actor]; | ||
if (user) { | ||
delete state.users[userLeftMessage.actor]; | ||
updateUsers({ type: "leave", user }); | ||
} | ||
} | ||
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, | ||
}; | ||
} | ||
state.users = newUsers; | ||
updateUsers({ type: "reset" }); | ||
} | ||
function onNavigatorOnline() { | ||
if (state.connection.state === "unavailable") { | ||
reconnect(); | ||
} | ||
} | ||
function onEvent(message) { | ||
for (const listener of state.listeners.event) { | ||
listener({ connectionId: message.actor, event: message.event }); | ||
} | ||
} | ||
function onUserJoinedMessage(message) { | ||
state.users[message.actor] = { | ||
connectionId: message.actor, | ||
info: message.info, | ||
id: message.id, | ||
}; | ||
updateUsers({ type: "enter", user: state.users[message.actor] }); | ||
if (state.me) { | ||
// Send current presence to new user | ||
// TODO: Consider storing it on the backend | ||
state.flushData.messages.push({ | ||
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); | ||
switch (message.type) { | ||
case ServerMessageType.InitialStorageState: { | ||
onInitialStorageState(message); | ||
break; | ||
} | ||
case ServerMessageType.UpdateStorage: { | ||
onStorageUpdates(message); | ||
break; | ||
} | ||
case ServerMessageType.UserJoined: { | ||
onUserJoinedMessage(message); | ||
break; | ||
} | ||
case ServerMessageType.UpdatePresence: { | ||
onUpdatePresenceMessage(message); | ||
break; | ||
} | ||
case ServerMessageType.Event: { | ||
onEvent(message); | ||
break; | ||
} | ||
case ServerMessageType.UserLeft: { | ||
onUserLeftMessage(message); | ||
break; | ||
} | ||
case ServerMessageType.RoomState: { | ||
onRoomStateMessage(message); | ||
break; | ||
} | ||
} | ||
} | ||
// 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); | ||
state.users = {}; | ||
updateUsers({ type: "reset" }); | ||
if (event.code >= 4000 && event.code <= 4100) { | ||
updateConnection({ state: "failed" }); | ||
const error = new LiveblocksError(event.reason, event.code); | ||
for (const listener of state.listeners.error) { | ||
listener(error); | ||
} | ||
} | ||
else if (event.wasClean === false) { | ||
updateConnection({ state: "unavailable" }); | ||
state.numberOfRetry++; | ||
state.timeoutHandles.reconnect = effects.scheduleReconnect(getRetryDelay()); | ||
} | ||
else { | ||
updateConnection({ state: "closed" }); | ||
} | ||
} | ||
function updateConnection(connection) { | ||
state.connection = connection; | ||
for (const listener of state.listeners.connection) { | ||
listener(connection.state); | ||
} | ||
} | ||
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(); | ||
if (state.connection.state === "connecting") { | ||
updateConnection(Object.assign(Object.assign({}, state.connection), { state: "open" })); | ||
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; | ||
} | ||
clearTimeout(state.timeoutHandles.pongTimeout); | ||
state.timeoutHandles.pongTimeout = effects.schedulePongTimeout(); | ||
if (state.socket.readyState === WebSocket.OPEN) { | ||
state.socket.send("ping"); | ||
} | ||
} | ||
function pongTimeout() { | ||
reconnect(); | ||
} | ||
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; | ||
} | ||
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(); | ||
const elapsedTime = now - state.lastFlushTime; | ||
if (elapsedTime > context.throttleDelay) { | ||
const messages = flushDataToMessages(state); | ||
if (messages.length === 0) { | ||
return; | ||
} | ||
effects.send(messages); | ||
state.flushData = { | ||
messages: [], | ||
storageOperations: [], | ||
presence: null, | ||
}; | ||
state.lastFlushTime = now; | ||
} | ||
else { | ||
if (state.timeoutHandles.flush != null) { | ||
clearTimeout(state.timeoutHandles.flush); | ||
} | ||
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" }); | ||
if (state.timeoutHandles.flush) { | ||
clearTimeout(state.timeoutHandles.flush); | ||
} | ||
clearTimeout(state.timeoutHandles.reconnect); | ||
clearTimeout(state.timeoutHandles.pongTimeout); | ||
clearInterval(state.intervalHandles.heartbeat); | ||
state.users = {}; | ||
updateUsers({ type: "reset" }); | ||
clearListeners(); | ||
} | ||
function clearListeners() { | ||
for (const key in state.listeners) { | ||
state.listeners[key] = []; | ||
} | ||
} | ||
function getPresence() { | ||
return state.me; | ||
} | ||
function getOthers() { | ||
return state.others; | ||
} | ||
function broadcastEvent(event) { | ||
if (state.socket == null) { | ||
return; | ||
} | ||
state.flushData.messages.push({ | ||
type: ClientMessageType.ClientEvent, | ||
event, | ||
}); | ||
tryFlushing(); | ||
} | ||
/** | ||
* STORAGE | ||
*/ | ||
function onStorageUpdates(message) { | ||
if (state.doc == null) { | ||
// TODO: Cache updates in case they are coming while root is queried | ||
return; | ||
} | ||
updateDoc(message.ops.reduce((doc, op) => doc.dispatch(op), state.doc)); | ||
} | ||
function updateDoc(doc) { | ||
state.doc = doc; | ||
if (doc) { | ||
for (const listener of state.listeners.storage) { | ||
listener(getStorage()); | ||
} | ||
} | ||
} | ||
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$1(data), | ||
createList: () => createList$1(), | ||
}))); | ||
} | ||
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$1(data) { | ||
return createRecord(makeId(), data); | ||
} | ||
function createList$1() { | ||
return createList(makeId()); | ||
} | ||
function fetchStorage(initialStorageFactory) { | ||
state.initialStorageFactory = initialStorageFactory; | ||
state.storageState = LiveStorageState.Loading; | ||
state.flushData.messages.push({ type: ClientMessageType.FetchStorage }); | ||
tryFlushing(); | ||
} | ||
function updateRecord(record, overrides) { | ||
updateDoc(state.doc.updateRecord(record.id, overrides)); | ||
} | ||
function pushItem(list, item) { | ||
updateDoc(state.doc.pushItem(list.id, item)); | ||
} | ||
function deleteItem(list, index) { | ||
updateDoc(state.doc.deleteItem(list.id, index)); | ||
} | ||
function deleteItemById(list, itemId) { | ||
updateDoc(state.doc.deleteItemById(list.id, itemId)); | ||
} | ||
function moveItem(list, index, targetIndex) { | ||
updateDoc(state.doc.moveItem(list.id, index, targetIndex)); | ||
} | ||
return { | ||
// Internal | ||
onOpen, | ||
onClose, | ||
onMessage, | ||
authenticationSuccess, | ||
heartbeat, | ||
onNavigatorOnline, | ||
// onWakeUp, | ||
onVisibilityChange, | ||
// Core | ||
connect, | ||
disconnect, | ||
subscribe, | ||
unsubscribe, | ||
// Presence | ||
updatePresence, | ||
broadcastEvent, | ||
// Storage | ||
fetchStorage, | ||
createRecord: createRecord$1, | ||
updateRecord, | ||
createList: createList$1, | ||
pushItem, | ||
deleteItem, | ||
deleteItemById, | ||
moveItem, | ||
selectors: { | ||
// Core | ||
getConnectionState, | ||
getCurrentUser, | ||
// Presence | ||
getPresence, | ||
getOthers, | ||
// Storage | ||
getStorage, | ||
}, | ||
}; | ||
} | ||
function defaultState(me) { | ||
return { | ||
connection: { state: "closed" }, | ||
socket: null, | ||
listeners: { | ||
storage: [], | ||
event: [], | ||
others: [], | ||
"my-presence": [], | ||
error: [], | ||
connection: [], | ||
}, | ||
numberOfRetry: 0, | ||
lastFlushTime: 0, | ||
timeoutHandles: { | ||
flush: null, | ||
reconnect: 0, | ||
pongTimeout: 0, | ||
}, | ||
flushData: { | ||
presence: me == null ? {} : me, | ||
messages: [], | ||
storageOperations: [], | ||
}, | ||
intervalHandles: { | ||
heartbeat: 0, | ||
}, | ||
me: me == null ? {} : me, | ||
users: {}, | ||
others: makeOthers({}), | ||
storageState: LiveStorageState.NotInitialized, | ||
initialStorageFactory: null, | ||
doc: null, | ||
idFactory: null, | ||
}; | ||
} | ||
function createRoom(name, options) { | ||
const throttleDelay = options.throttle || 100; | ||
const liveblocksServer = options.liveblocksServer || "wss://liveblocks.net"; | ||
const authEndpoint = options.authEndpoint; | ||
const state = defaultState(options.initialPresence); | ||
const machine = makeStateMachine(state, { | ||
throttleDelay, | ||
liveblocksServer, | ||
authEndpoint, | ||
room: name, | ||
}); | ||
const room = { | ||
///////////// | ||
// Core // | ||
///////////// | ||
getConnectionState: machine.selectors.getConnectionState, | ||
getCurrentUser: machine.selectors.getCurrentUser, | ||
subscribe: machine.subscribe, | ||
unsubscribe: machine.unsubscribe, | ||
///////////// | ||
// Storage // | ||
///////////// | ||
getStorage: machine.selectors.getStorage, | ||
fetchStorage: machine.fetchStorage, | ||
createRecord: machine.createRecord, | ||
createList: machine.createList, | ||
updateRecord: machine.updateRecord, | ||
pushItem: machine.pushItem, | ||
deleteItem: machine.deleteItem, | ||
deleteItemById: machine.deleteItemById, | ||
moveItem: machine.moveItem, | ||
////////////// | ||
// Presence // | ||
////////////// | ||
getPresence: machine.selectors.getPresence, | ||
updatePresence: machine.updatePresence, | ||
getOthers: machine.selectors.getOthers, | ||
broadcastEvent: machine.broadcastEvent, | ||
}; | ||
return { | ||
connect: machine.connect, | ||
disconnect: machine.disconnect, | ||
onNavigatorOnline: machine.onNavigatorOnline, | ||
onVisibilityChange: machine.onVisibilityChange, | ||
room, | ||
}; | ||
} | ||
class LiveblocksError extends Error { | ||
constructor(message, code) { | ||
super(message); | ||
this.code = code; | ||
} | ||
} | ||
/** | ||
* Create a client that will be responsible to communicate with liveblocks servers. | ||
* | ||
* ### Example | ||
* ``` | ||
* const client = createClient({ | ||
* authEndpoint: "/api/auth" | ||
* }) | ||
* ``` | ||
*/ | ||
function createClient(options) { | ||
if (typeof options.throttle === "number") { | ||
if (options.throttle < 80 || options.throttle > 1000) { | ||
throw new Error("Liveblocks client throttle should be between 80 and 1000 ms"); | ||
} | ||
} | ||
const rooms = new Map(); | ||
function getRoom(roomId) { | ||
const internalRoom = rooms.get(roomId); | ||
return internalRoom ? internalRoom.room : null; | ||
} | ||
function enter(roomId, initialPresence) { | ||
let internalRoom = rooms.get(roomId); | ||
if (internalRoom) { | ||
return internalRoom.room; | ||
} | ||
internalRoom = createRoom(roomId, Object.assign(Object.assign({}, options), { initialPresence })); | ||
rooms.set(roomId, internalRoom); | ||
internalRoom.connect(); | ||
return internalRoom.room; | ||
} | ||
function leave(roomId) { | ||
let room = rooms.get(roomId); | ||
if (room) { | ||
room.disconnect(); | ||
rooms.delete(roomId); | ||
} | ||
} | ||
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 { | ||
getRoom, | ||
enter, | ||
leave, | ||
}; | ||
} | ||
var ClientContext = React.createContext(null); | ||
@@ -1691,3 +368,2 @@ var RoomContext = React.createContext(null); | ||
exports.RoomProvider = RoomProvider; | ||
exports.createClient = createClient; | ||
exports.useBroadcastEvent = useBroadcastEvent; | ||
@@ -1694,0 +370,0 @@ exports.useCurrentUser = useCurrentUser; |
{ | ||
"name": "@liveblocks/react", | ||
"version": "0.9.0", | ||
"version": "0.10.0", | ||
"description": "", | ||
@@ -25,6 +25,4 @@ "main": "./lib/index.js", | ||
"license": "Apache-2.0", | ||
"dependencies": { | ||
"@liveblocks/client": "0.9.0" | ||
}, | ||
"peerDependencies": { | ||
"@liveblocks/client": "0.10.0", | ||
"react": "^16.14.0 || ^17" | ||
@@ -31,0 +29,0 @@ }, |
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
72967
519
+ Added@liveblocks/client@0.10.0(transitive)
- Removed@liveblocks/client@0.9.0
- Removed@liveblocks/client@0.9.0(transitive)