@liveblocks/core
Version:
Private internals for Liveblocks. DO NOT import directly from this package!
1,369 lines (1,336 loc) • 216 kB
TypeScript
import { JSONSchema7 } from 'json-schema';
/**
* Throws an error if multiple copies of a Liveblocks package are being loaded
* at runtime. This likely indicates a packaging issue with the project.
*/
declare function detectDupes(pkgName: string, pkgVersion: string | false, // false if not built yet
pkgFormat: string | false): void;
/**
* This helper type is effectively a no-op, but will force TypeScript to
* "evaluate" any named helper types in its definition. This can sometimes make
* API signatures clearer in IDEs.
*
* For example, in:
*
* type Payload<T> = { data: T };
*
* let r1: Payload<string>;
* let r2: Resolve<Payload<string>>;
*
* The inferred type of `r1` is going to be `Payload<string>` which shows up in
* editor hints, and it may be unclear what's inside if you don't know the
* definition of `Payload`.
*
* The inferred type of `r2` is going to be `{ data: string }`, which may be
* more helpful.
*
* This trick comes from:
* https://effectivetypescript.com/2022/02/25/gentips-4-display/
*/
type Resolve<T> = T extends (...args: unknown[]) => unknown ? T : {
[K in keyof T]: T[K];
};
/**
* Relaxes a discriminated union type definition, by explicitly adding
* properties defined in any other member as 'never'.
*
* This makes accessing the members much more relaxed in TypeScript.
*
* For example:
* type MyUnion = Relax<
* | { foo: string }
* | { foo: number; bar: string; }
* | { qux: boolean }
* >;
*
* // With Relax, accessing is much easier:
* union.foo; // string | number | undefined
* union.bar; // string | undefined
* union.qux; // boolean
* union.whatever; // Error: Property 'whatever' does not exist on type 'MyUnion'
*
* // Without Relax, these would all be type errors:
* union.foo; // Error: Property 'foo' does not exist on type 'MyUnion'
* union.bar; // Error: Property 'bar' does not exist on type 'MyUnion'
* union.qux; // Error: Property 'qux' does not exist on type 'MyUnion'
*/
type Relax<T> = DistributiveRelax<T, T extends any ? keyof T : never>;
type DistributiveRelax<T, Ks extends string | number | symbol> = T extends any ? Resolve<{
[K in keyof T]: T[K];
} & {
[K in Exclude<Ks, keyof T>]?: never;
}> : never;
declare const Permission: {
/**
* Default permission for a room.
*/
readonly Read: "*:read";
readonly Write: "*:write";
/**
* Legacy aliases for default room permissions.
*/
readonly RoomWrite: "room:write";
readonly RoomRead: "room:read";
/**
* Storage
*/
readonly StorageRead: "storage:read";
readonly StorageWrite: "storage:write";
readonly StorageNone: "storage:none";
/**
* Comments
*/
readonly CommentsWrite: "comments:write";
readonly CommentsRead: "comments:read";
readonly CommentsNone: "comments:none";
readonly CommentsPublicWrite: "comments:public:write";
readonly CommentsPublicRead: "comments:public:read";
readonly CommentsPublicNone: "comments:public:none";
readonly CommentsPrivateWrite: "comments:private:write";
readonly CommentsPrivateRead: "comments:private:read";
readonly CommentsPrivateNone: "comments:private:none";
/**
* Feeds
*/
readonly FeedsRead: "feeds:read";
readonly FeedsWrite: "feeds:write";
readonly FeedsNone: "feeds:none";
/**
* Legacy
*/
readonly LegacyRoomPresenceWrite: "room:presence:write";
};
type Permission = (typeof Permission)[keyof typeof Permission];
declare const ACCESS_LEVELS: readonly ["none", "read", "write"];
type AccessLevel = (typeof ACCESS_LEVELS)[number];
type RequiredAccessLevel = "read" | "write";
type PermissionMatrix = {
room: AccessLevel;
storage: AccessLevel;
comments: AccessLevel;
"comments:public": AccessLevel;
"comments:private": AccessLevel;
feeds: AccessLevel;
personal: AccessLevel;
};
type PermissionResources = keyof PermissionMatrix;
type RoomPermissions = Permission[];
type RoomAccesses = Record<string, RoomPermissions>;
type UpdateRoomAccesses = Record<string, RoomPermissions | null>;
declare function permissionMatrixFromScopes(scopes: RoomPermissions): PermissionMatrix;
declare function hasPermissionAccess(matrix: Partial<PermissionMatrix>, resource: PermissionResources, requiredAccess: RequiredAccessLevel): boolean;
declare function normalizeRoomPermissions(permissions: string[] | readonly string[]): RoomPermissions;
declare function normalizeRoomAccesses(accesses: RoomAccesses | undefined): RoomAccesses | undefined;
declare function normalizeUpdateRoomAccesses(accesses: UpdateRoomAccesses | undefined): UpdateRoomAccesses | undefined;
/**
* Merges permission scopes from multiple sources, by priority: explicit user
* accesses override group accesses, which override the room defaults. Groups
* all share the same priority, so they are first merged together by taking
* the highest access level per feature (and base).
*/
declare function mergeRoomPermissionScopes({ defaultAccesses, groupsAccesses, userAccesses, }: {
defaultAccesses: RoomPermissions;
groupsAccesses: RoomPermissions[];
userAccesses: RoomPermissions;
}): RoomPermissions;
/**
* Validates a set of permissions:
* - every scope must be a known permission scope,
* - exactly one base permission is required (*:read, *:write, or the legacy
* aliases room:read, room:write),
* - at most one scope per feature (storage, comments, feeds, ...),
* - room:presence:write is accepted as an extra legacy scope.
*
* Returns `true` when the set is valid, or an error message otherwise.
*/
declare function validatePermissionsSet(scopes: readonly string[]): true | string;
type CustomAuthenticationResult = Relax<{
token: string;
} | {
error: "forbidden";
reason: string;
} | {
error: string;
reason: string;
}>;
/**
* Represents an indefinitely deep arbitrary JSON data structure. There are
* four types that make up the Json family:
*
* - Json any legal JSON value
* - JsonScalar any legal JSON leaf value (no lists or objects)
* - JsonArray a JSON value whose outer type is an array
* - JsonObject a JSON value whose outer type is an object
*
*/
type Json = JsonScalar | JsonArray | JsonObject;
type JsonScalar = string | number | boolean | null;
type JsonArray = Json[];
/**
* Any valid JSON object.
*/
type JsonObject = {
[key: string]: Json | undefined;
};
/**
* Like Json, but with readonly arrays and objects.
*/
type ReadonlyJson = JsonScalar | readonly ReadonlyJson[] | ReadonlyJsonObject;
/**
* Like JsonObject, but readonly.
*/
type ReadonlyJsonObject = {
readonly [key: string]: ReadonlyJson | undefined;
};
declare function isJsonScalar(data: Json): data is JsonScalar;
declare function isJsonArray(data: Json): data is JsonArray;
declare function isJsonObject(data: Json): data is JsonObject;
/**
* Represents some constraints for user info. Basically read this as: "any JSON
* object is fine, but _if_ it has a name field, it _must_ be a string."
* (Ditto for avatar.)
*/
type IUserInfo = {
[key: string]: Json | undefined;
name?: string;
avatar?: string;
};
/**
* This type is used by clients to define the metadata for a user.
*/
type BaseUserMeta = {
/**
* The id of the user that has been set in the authentication endpoint.
* Useful to get additional information about the connected user.
*/
id?: string;
/**
* Additional user information that has been set in the authentication endpoint.
*/
info?: IUserInfo;
};
type RenameDataField<T, TFieldName extends string> = T extends any ? {
[K in keyof T as K extends "data" ? TFieldName : K]: T[K];
} : never;
type AsyncLoading<F extends string = "data"> = RenameDataField<{
readonly isLoading: true;
readonly data?: never;
readonly error?: never;
}, F>;
type AsyncSuccess<T, F extends string = "data"> = RenameDataField<{
readonly isLoading: false;
readonly data: T;
readonly error?: never;
}, F>;
type AsyncError<F extends string = "data"> = RenameDataField<{
readonly isLoading: false;
readonly data?: never;
readonly error: Error;
}, F>;
type AsyncResult<T, F extends string = "data"> = AsyncLoading<F> | AsyncSuccess<T, F> | AsyncError<F>;
type Callback<T> = (event: T) => void;
type UnsubscribeCallback = () => void;
type Observable<T> = {
/**
* Register a callback function to be called whenever the event source emits
* an event.
*/
subscribe(callback: Callback<T>): UnsubscribeCallback;
/**
* Register a one-time callback function to be called whenever the event
* source emits an event. After the event fires, the callback is
* auto-unsubscribed.
*/
subscribeOnce(callback: Callback<T>): UnsubscribeCallback;
/**
* Returns a promise that will resolve when an event is emitted by this
* event source. Optionally, specify a predicate that has to match. The first
* event matching that predicate will then resolve the promise.
*/
waitUntil(predicate?: (event: T) => boolean): Promise<T>;
};
type EventSource<T> = Observable<T> & {
/**
* Notify all subscribers about the event. Will return `false` if there
* weren't any subscribers at the time the .notify() was called, or `true` if
* there was at least one subscriber.
*/
notify(event: T): boolean;
/**
* Returns the number of active subscribers.
*/
count(): number;
/**
* Observable instance, which can be used to subscribe to this event source
* in a readonly fashion. Safe to publicly expose.
*/
observable: Observable<T>;
/**
* Disposes of this event source.
*
* Will clears all registered event listeners. None of the registered
* functions will ever get called again.
*
* WARNING!
* Be careful when using this API, because the subscribers may not have any
* idea they won't be notified anymore.
*/
dispose(): void;
};
/**
* makeEventSource allows you to generate a subscribe/notify pair of functions
* to make subscribing easy and to get notified about events.
*
* The events are anonymous, so you can use it to define events, like so:
*
* const event1 = makeEventSource();
* const event2 = makeEventSource();
*
* event1.subscribe(foo);
* event1.subscribe(bar);
* event2.subscribe(qux);
*
* // Unsubscription is pretty standard
* const unsub = event2.subscribe(foo);
* unsub();
*
* event1.notify(); // Now foo and bar will get called
* event2.notify(); // Now qux will get called (but foo will not, since it's unsubscribed)
*
*/
declare function makeEventSource<T>(): EventSource<T>;
type BatchStore<O, I> = {
subscribe: (callback: Callback<void>) => UnsubscribeCallback;
enqueue: (input: I) => Promise<void>;
setData: (entries: [I, O][]) => void;
getItemState: (input: I) => AsyncResult<O> | undefined;
getData: (input: I) => O | undefined;
invalidate: (inputs?: I[]) => void;
};
type ContextualPromptResponse = Relax<{
type: "insert";
text: string;
} | {
type: "replace";
text: string;
} | {
type: "other";
text: string;
}>;
type ContextualPromptContext = {
beforeSelection: string;
selection: string;
afterSelection: string;
};
declare const brand: unique symbol;
type Brand<T, TBrand extends string> = T & {
[brand]: TBrand;
};
type ISODateString = Brand<string, "ISODateString">;
type DistributiveOmit<T, K extends PropertyKey> = T extends any ? Omit<T, K> : never;
type WithRequired<T, K extends keyof T> = T & {
[P in K]-?: T[P];
};
type WithOptional<T, K extends keyof T> = Omit<T, K> & {
[P in K]?: T[P];
};
/**
* Throw an error, but as an expression instead of a statement.
*/
declare function raise(msg: string): never;
/**
* Drop-in replacement for Object.entries() that retains better types.
*/
declare function entries<O extends {
[key: string]: unknown;
}, K extends keyof O>(obj: O): [K, O[K]][];
/**
* Drop-in replacement for Object.keys() that retains better types.
*/
declare function keys<O extends {
[key: string]: unknown;
}, K extends keyof O>(obj: O): K[];
/**
* Creates a new object by mapping a function over all values. Keys remain the
* same. Think Array.prototype.map(), but for values in an object.
*/
declare function mapValues<V, O extends Record<string, unknown>>(obj: O, mapFn: (value: O[keyof O], key: keyof O) => V): {
[K in keyof O]: V;
};
/**
* Alternative to JSON.parse() that will not throw in production. If the passed
* string cannot be parsed, this will return `undefined`.
*/
declare function tryParseJson(rawMessage: string): Json | undefined;
/**
* Decode base64 string.
*/
declare function b64decode(b64value: string): string;
type RemoveUndefinedValues<T> = {
[K in keyof T]-?: Exclude<T[K], undefined>;
};
/**
* Returns a new object instance where all explictly-undefined values are
* removed.
*/
declare function compactObject<O extends Record<string, unknown>>(obj: O): RemoveUndefinedValues<O>;
/**
* Returns a promise that resolves after the given number of milliseconds.
*/
declare function wait(millis: number): Promise<void>;
/**
* Returns whatever the given promise returns, but will be rejected with
* a "Timed out" error if the given promise does not return or reject within
* the given timeout period (in milliseconds).
*/
declare function withTimeout<T>(promise: Promise<T>, millis: number, errmsg: string): Promise<T>;
/**
* Memoize a promise factory, so that each subsequent call will return the same
* pending or success promise. If the promise rejects, will retain that failed
* promise for a small time period, after which the next attempt will reset the
* memoized value.
*/
declare function memoizeOnSuccess<T>(factoryFn: () => Promise<T>): () => Promise<T>;
/**
* Polyfill for Array.prototype.findLastIndex()
*/
declare function findLastIndex<T>(arr: T[], predicate: (value: T, index: number, obj: T[]) => boolean): number;
type OpCode = (typeof OpCode)[keyof typeof OpCode];
declare const OpCode: Readonly<{
INIT: 0;
SET_PARENT_KEY: 1;
CREATE_LIST: 2;
UPDATE_OBJECT: 3;
CREATE_OBJECT: 4;
DELETE_CRDT: 5;
DELETE_OBJECT_KEY: 6;
CREATE_MAP: 7;
CREATE_REGISTER: 8;
}>;
declare namespace OpCode {
type INIT = typeof OpCode.INIT;
type SET_PARENT_KEY = typeof OpCode.SET_PARENT_KEY;
type CREATE_LIST = typeof OpCode.CREATE_LIST;
type UPDATE_OBJECT = typeof OpCode.UPDATE_OBJECT;
type CREATE_OBJECT = typeof OpCode.CREATE_OBJECT;
type DELETE_CRDT = typeof OpCode.DELETE_CRDT;
type DELETE_OBJECT_KEY = typeof OpCode.DELETE_OBJECT_KEY;
type CREATE_MAP = typeof OpCode.CREATE_MAP;
type CREATE_REGISTER = typeof OpCode.CREATE_REGISTER;
}
/**
* These operations are the payload for {@link UpdateStorageServerMsg} messages
* only.
*/
type Op = CreateOp | UpdateObjectOp | DeleteCrdtOp | SetParentKeyOp | DeleteObjectKeyOp;
type CreateOp = CreateObjectOp | CreateRegisterOp | CreateMapOp | CreateListOp;
type UpdateObjectOp = {
readonly opId?: string;
readonly id: string;
readonly type: OpCode.UPDATE_OBJECT;
readonly data: Partial<JsonObject>;
};
type CreateObjectOp = {
readonly opId?: string;
readonly id: string;
readonly type: OpCode.CREATE_OBJECT;
readonly parentId: string;
readonly parentKey: string;
readonly data: JsonObject;
readonly intent?: "set" | "push";
readonly deletedId?: string;
};
type CreateListOp = {
readonly opId?: string;
readonly id: string;
readonly type: OpCode.CREATE_LIST;
readonly parentId: string;
readonly parentKey: string;
readonly intent?: "set" | "push";
readonly deletedId?: string;
};
type CreateMapOp = {
readonly opId?: string;
readonly id: string;
readonly type: OpCode.CREATE_MAP;
readonly parentId: string;
readonly parentKey: string;
readonly intent?: "set" | "push";
readonly deletedId?: string;
};
type CreateRegisterOp = {
readonly opId?: string;
readonly id: string;
readonly type: OpCode.CREATE_REGISTER;
readonly parentId: string;
readonly parentKey: string;
readonly data: Json;
readonly intent?: "set" | "push";
readonly deletedId?: string;
};
type DeleteCrdtOp = {
readonly opId?: string;
readonly id: string;
readonly type: OpCode.DELETE_CRDT;
};
type IgnoredOp = {
readonly type: OpCode.DELETE_CRDT;
readonly id: "ACK";
readonly opId: string;
};
type SetParentKeyOp = {
readonly opId?: string;
readonly id: string;
readonly type: OpCode.SET_PARENT_KEY;
readonly parentKey: string;
};
type DeleteObjectKeyOp = {
readonly opId?: string;
readonly id: string;
readonly type: OpCode.DELETE_OBJECT_KEY;
readonly key: string;
};
type HasOpId = {
opId: string;
};
/**
* Ops sent from client → server. Always includes an opId so the server can
* acknowledge the receipt.
*/
type ClientWireOp = Op & HasOpId;
type ClientWireCreateOp = CreateOp & HasOpId;
/**
* ServerWireOp: Ops sent from server → client. Three variants:
* 1. ClientWireOp — Full echo back of our own op, confirming it was applied
* 2. IgnoredOp — Our op was seen but intentionally ignored (still counts as ack)
* 3. Op without opId — Another client's op being forwarded to us
*/
type ServerWireOp = ClientWireOp | IgnoredOp | TheirOp;
type TheirOp = DistributiveOmit<Op, "opId"> & {
opId?: undefined;
};
type UpdateDelta = {
type: "update";
} | {
type: "delete";
deletedItem: Lson;
};
/**
* A LiveMap notification that is sent in-client to any subscribers whenever
* one or more of the values inside the LiveMap instance have changed.
*/
type LiveMapUpdates<TKey extends string, TValue extends Lson> = {
type: "LiveMap";
node: LiveMap<TKey, TValue>;
updates: {
[key: string]: UpdateDelta;
};
};
/**
* The LiveMap class is similar to a JavaScript Map that is synchronized on all clients.
* Keys should be a string, and values should be serializable to JSON.
* If multiple clients update the same property simultaneously, the last modification received by the Liveblocks servers is the winner.
*/
declare class LiveMap<TKey extends string, TValue extends Lson> extends AbstractCrdt {
#private;
constructor(entries?: readonly (readonly [TKey, TValue])[] | undefined);
/**
* Returns a specified element from the LiveMap.
* @param key The key of the element to return.
* @returns The element associated with the specified key, or undefined if the key can't be found in the LiveMap.
*/
get(key: TKey): TValue | undefined;
/**
* Adds or updates an element with a specified key and a value.
* @param key The key of the element to add. Should be a string.
* @param value The value of the element to add. Should be serializable to JSON.
*/
set(key: TKey, value: TValue): void;
/**
* Returns the number of elements in the LiveMap.
*/
get size(): number;
/**
* Returns a boolean indicating whether an element with the specified key exists or not.
* @param key The key of the element to test for presence.
*/
has(key: TKey): boolean;
/**
* Removes the specified element by key.
* @param key The key of the element to remove.
* @returns true if an element existed and has been removed, or false if the element does not exist.
*/
delete(key: TKey): boolean;
/**
* Returns a new Iterator object that contains the [key, value] pairs for each element.
*/
entries(): IterableIterator<[TKey, TValue]>;
/**
* Same function object as the initial value of the entries method.
*/
[Symbol.iterator](): IterableIterator<[TKey, TValue]>;
/**
* Returns a new Iterator object that contains the keys for each element.
*/
keys(): IterableIterator<TKey>;
/**
* Returns a new Iterator object that contains the values for each element.
*/
values(): IterableIterator<TValue>;
/**
* Executes a provided function once per each key/value pair in the Map object, in insertion order.
* @param callback Function to execute for each entry in the map.
*/
forEach(callback: (value: TValue, key: TKey, map: LiveMap<TKey, TValue>) => void): void;
toJSON(): {
readonly [P in TKey]: ToJson<TValue>;
};
clone(): LiveMap<TKey, TValue>;
}
type CrdtType = (typeof CrdtType)[keyof typeof CrdtType];
declare const CrdtType: Readonly<{
OBJECT: 0;
LIST: 1;
MAP: 2;
REGISTER: 3;
}>;
declare namespace CrdtType {
type OBJECT = typeof CrdtType.OBJECT;
type LIST = typeof CrdtType.LIST;
type MAP = typeof CrdtType.MAP;
type REGISTER = typeof CrdtType.REGISTER;
}
type SerializedCrdt = SerializedRootObject | SerializedChild;
type SerializedChild = SerializedObject | SerializedList | SerializedMap | SerializedRegister;
type SerializedRootObject = {
readonly type: CrdtType.OBJECT;
readonly data: JsonObject;
readonly parentId?: never;
readonly parentKey?: never;
};
type SerializedObject = {
readonly type: CrdtType.OBJECT;
readonly parentId: string;
readonly parentKey: string;
readonly data: JsonObject;
};
type SerializedList = {
readonly type: CrdtType.LIST;
readonly parentId: string;
readonly parentKey: string;
};
type SerializedMap = {
readonly type: CrdtType.MAP;
readonly parentId: string;
readonly parentKey: string;
};
type SerializedRegister = {
readonly type: CrdtType.REGISTER;
readonly parentId: string;
readonly parentKey: string;
readonly data: Json;
};
type StorageNode = RootStorageNode | ChildStorageNode;
type ChildStorageNode = ObjectStorageNode | ListStorageNode | MapStorageNode | RegisterStorageNode;
type RootStorageNode = [id: "root", value: SerializedRootObject];
type ObjectStorageNode = [id: string, value: SerializedObject];
type ListStorageNode = [id: string, value: SerializedList];
type MapStorageNode = [id: string, value: SerializedMap];
type RegisterStorageNode = [id: string, value: SerializedRegister];
type NodeMap = Map<string, SerializedCrdt>;
type NodeStream = Iterable<StorageNode>;
declare function isRootStorageNode(node: StorageNode): node is RootStorageNode;
declare function isObjectStorageNode(node: StorageNode): node is RootStorageNode | ObjectStorageNode;
declare function isListStorageNode(node: StorageNode): node is ListStorageNode;
declare function isMapStorageNode(node: StorageNode): node is MapStorageNode;
declare function isRegisterStorageNode(node: StorageNode): node is RegisterStorageNode;
type CompactNode = CompactRootNode | CompactChildNode;
type CompactChildNode = CompactObjectNode | CompactListNode | CompactMapNode | CompactRegisterNode;
type CompactRootNode = readonly [id: "root", data: JsonObject];
type CompactObjectNode = readonly [
id: string,
type: CrdtType.OBJECT,
parentId: string,
parentKey: string,
data: JsonObject
];
type CompactListNode = readonly [
id: string,
type: CrdtType.LIST,
parentId: string,
parentKey: string
];
type CompactMapNode = readonly [
id: string,
type: CrdtType.MAP,
parentId: string,
parentKey: string
];
type CompactRegisterNode = readonly [
id: string,
type: CrdtType.REGISTER,
parentId: string,
parentKey: string,
data: Json
];
declare function compactNodesToNodeStream(compactNodes: CompactNode[]): NodeStream;
declare function nodeStreamToCompactNodes(nodes: NodeStream): Iterable<CompactNode>;
/**
* Extracts only the explicitly-named string keys of a type, filtering out
* any index signature (e.g. `[key: string]: ...`).
*/
type KnownKeys<T> = keyof {
[K in keyof T as {} extends Record<K, 1> ? never : K]: true;
} & string;
/**
* Per-key sync configuration for node/edge `data` properties.
*
* true
* Sync this property to Liveblocks Storage. Arrays and objects in the value
* will be stored as LiveLists and LiveObjects, enabling fine-grained
* conflict-free merging. This is the default for all keys.
*
* false
* Don't sync this property. It stays local to the current client. This
* property will be `undefined` on other clients.
*
* "atomic"
* Sync this property, but treat it as an indivisible value. The entire value
* is replaced as a whole (last-writer-wins) instead of being recursively
* converted to LiveObjects/LiveLists. Use this when clients always replace
* the value entirely and never need concurrent sub-key merging.
*
* { ... }
* A nested config object for recursively configuring sub-keys of an object.
*
* @example
* ```ts
* const sync: SyncConfig = {
* label: true, // sync (default)
* createdAt: false, // local-only
* shape: "atomic", // replaced as a whole, no deep merge
* nested: { // recursive config
* deep: false,
* },
* };
* ```
*/
type SyncMode = boolean | "atomic" | SyncConfig;
type SyncConfig = {
[key: string]: SyncMode | undefined;
};
/**
* Deeply converts all nested lists to LiveLists, and all nested objects to
* LiveObjects.
*
* As such, the returned result will not contain any Json arrays or Json
* objects anymore.
*/
declare function deepLiveify(value: Json, config?: SyncMode): Lson;
/**
* Optional keys of O whose non-undefined type is plain Json (not a
* LiveStructure). These are the only keys eligible for setLocal().
* Uses KnownKeys to only consider explicitly-named keys, not index signatures.
* Checks optionality inline to avoid index signature pollution of OptionalKeys.
*/
type OptionalJsonKeys<O> = {
[K in KnownKeys<O>]: undefined extends O[K] ? Exclude<O[K], undefined> extends Json ? K : never : never;
}[KnownKeys<O>];
type LiveObjectUpdateDelta<O extends {
[key: string]: unknown;
}> = {
[K in keyof O]?: UpdateDelta | undefined;
};
/**
* A LiveObject notification that is sent in-client to any subscribers whenever
* one or more of the entries inside the LiveObject instance have changed.
*/
type LiveObjectUpdates<TData extends LsonObject> = {
type: "LiveObject";
node: LiveObject<TData>;
updates: LiveObjectUpdateDelta<TData>;
};
/**
* The LiveObject class is similar to a JavaScript object that is synchronized on all clients.
* Keys should be a string, and values should be serializable to JSON.
* If multiple clients update the same property simultaneously, the last modification received by the Liveblocks servers is the winner.
*/
declare class LiveObject<O extends LsonObject> extends AbstractCrdt {
#private;
/**
* Enable or disable detection of too large LiveObjects.
* When enabled, throws an error if LiveObject static data exceeds 128KB, which
* is the maximum value the server will be able to accept.
* By default, this behavior is disabled to avoid the runtime performance
* overhead on every LiveObject.set() or LiveObject.update() call.
*
* @experimental
*/
static detectLargeObjects: boolean;
/** @private Do not use this API directly */
static _fromItems<O extends LsonObject>(nodes: NodeStream, pool: ManagedPool): LiveObject<O>;
constructor(obj?: O);
/** @private */
keys(): Set<string>;
/**
* Adds or updates a property with a specified key and a value.
* @param key The key of the property to add
* @param value The value of the property to add
*/
set<TKey extends keyof O>(key: TKey, value: O[TKey]): void;
/**
* @experimental
*
* Sets a local-only property that is not synchronized over the wire. The
* value will be visible via get(), and toJSON() on this client only. Other
* clients and the server will see `undefined` for this key.
*
* Caveat: this method will not add changes to the undo/redo stack.
*/
setLocal<TKey extends OptionalJsonKeys<O>>(key: TKey, value: Extract<Exclude<O[TKey], undefined>, Json>): void;
/**
* Returns a specified property from the LiveObject.
* @param key The key of the property to get
*/
get<TKey extends keyof O>(key: TKey): O[TKey];
/**
* Deletes a key from the LiveObject
* @param key The key of the property to delete
*/
delete(key: keyof O): void;
/**
* Adds or updates multiple properties at once with an object.
* @param patch The object used to overrides properties
*/
update(patch: Partial<O>): void;
/**
* Creates a new LiveObject from a plain JSON object, recursively converting
* nested objects to LiveObjects and arrays to LiveLists.
*/
static from(obj: JsonObject): LiveObject<LsonObject>;
/** @private */
static from(obj: JsonObject, config?: SyncConfig): LiveObject<LsonObject>;
/**
* Reconciles this LiveObject tree to match the given JSON object. Only
* mutates keys that actually changed. Keys present on this LiveObject but
* absent from `jsonObj` will be deleted. Nested structures are recursively
* reconciled.
*/
reconcile(jsonObj: JsonObject): void;
/** @private */
reconcile(jsonObj: JsonObject, config?: SyncConfig): void;
/**
* Like reconcile(), but only touches the top-level keys present in
* `partialObj`. Keys on this LiveObject that are absent from `partialObj`
* are left untouched. Typically called on the storage root when
* reconciling a subset of keys without affecting other keys on the root.
*
* Note: the partial behavior only applies to the top-level keys of this
* object. Nested structures are always fully reconciled.
*
* @private
*/
reconcilePartially(partialObj: JsonObject, config?: SyncConfig): void;
toJSON(): ToJson<O>;
clone(): LiveObject<O>;
}
type StorageCallback = (updates: StorageUpdate[]) => void;
type LiveMapUpdate = LiveMapUpdates<string, Lson>;
type LiveObjectUpdate = LiveObjectUpdates<LsonObject>;
type LiveListUpdate = LiveListUpdates<Lson>;
/**
* The payload of notifications sent (in-client) when LiveStructures change.
* Messages of this kind are not originating from the network, but are 100%
* in-client.
*/
type StorageUpdate = LiveMapUpdate | LiveObjectUpdate | LiveListUpdate;
/**
* Read-only query surface over {@link UnacknowledgedOps}, handed to CRDTs so
* they can look up their own still-pending Create ops without being able to
* mutate the set (only the room adds/acks).
*/
interface ReadonlyUnacknowledgedOps {
/** Still-unacknowledged Create ops whose `parentId` is the given one. */
getByParentId(parentId: string): Iterable<ClientWireCreateOp>;
/**
* Still-unacknowledged Create ops whose `parentId` and `parentKey` are both
* the given ones (i.e. targeting one exact position).
*/
getByParentIdAndKey(parentId: string, parentKey: string): Iterable<ClientWireCreateOp>;
/**
* Whether the given pending op may already have been processed by the
* server. True for ops that were in flight when a connection died: the
* server may have stored them with the ack getting lost in the disconnect,
* or may never have received them. Until the (re-sent) op's ack arrives,
* the client cannot know which, so optimistic predictions that assume the
* op has not been processed yet are unsound for these ops.
*/
isPossiblyStored(opId: string): boolean;
}
/**
* The managed pool is a namespace registry (i.e. a context) that "owns" all
* the individual live nodes, ensuring each one has a unique ID, and holding on
* to live nodes before and after they are inter-connected.
*/
interface ManagedPool {
readonly nodes: ReadonlyMap<string, LiveNode>;
readonly generateId: () => string;
readonly generateOpId: () => string;
readonly getNode: (id: string) => LiveNode | undefined;
readonly addNode: (id: string, node: LiveNode) => void;
readonly deleteNode: (id: string) => void;
/**
* Dispatching has three responsibilities:
* - Sends serialized ops to the WebSocket servers
* - Add reverse operations to the undo/redo stack
* - Notify room subscribers with updates (in-client, no networking)
*/
dispatch: (ops: ClientWireOp[], reverseOps: Op[], storageUpdates: Map<string, StorageUpdate>) => void;
/**
* Ensures storage can be written to else throws an error.
* This is used to prevent writing to storage when the user does not have
* permission to do so.
* @throws {Error} if storage is not writable
* @returns {void}
*/
assertStorageIsWritable: () => void;
/**
* Read-only view of the client's still-unacknowledged ops (sent or
* pending-send, not yet confirmed by the server).
*/
readonly unacknowledgedOps: ReadonlyUnacknowledgedOps;
}
type CreateManagedPoolOptions = {
/**
* Returns the current connection ID. This is used to generate unique
* prefixes for nodes created by this client. This number is allowed to
* change over time (for example, when the client reconnects).
*/
getCurrentConnectionId(): number;
/**
* Will get invoked when any Live structure calls .dispatch() on the pool.
*/
onDispatch?: (ops: ClientWireOp[], reverse: Op[], storageUpdates: Map<string, StorageUpdate>) => void;
/**
* Will get invoked when any Live structure calls .assertStorageIsWritable()
* on the pool. Defaults to true when not provided. Return false if you want
* to prevent writes to the pool locally early, because you know they won't
* have an effect upstream.
*/
isStorageWritable?: () => boolean;
/**
* Read-only view of the client's still-unacknowledged ops. Used by CRDTs
* (e.g. LiveList) to know which of their optimistic mutations the server
* hasn't confirmed yet. Defaults to an empty view (e.g. server-side pools
* that dispatch-and-flush have no optimistic state to track).
*/
unacknowledgedOps?: ReadonlyUnacknowledgedOps;
};
/**
* @private Private API, never use this API directly.
*/
declare function createManagedPool(options: CreateManagedPoolOptions): ManagedPool;
declare abstract class AbstractCrdt {
#private;
/**
* @private
* Returns true if the cached JSON snapshot exists and is reference-equal
* to the given value. Does not trigger a recompute.
*/
hasCache(value: unknown): boolean;
/**
* Return a JSON-compatible snapshot of this Live node and its children.
* LiveObject values become plain objects, LiveList values become arrays,
* and LiveMap values also become plain objects (not Map instances).
* The result is cached and only recomputed when the contents change.
*/
toJSON(): ReadonlyJson;
/**
* Returns a deep clone of the current LiveStructure, suitable for insertion
* in the tree elsewhere.
*/
abstract clone(): Lson;
}
type LiveListUpdateDelta = {
type: "insert";
index: number;
item: Lson;
} | {
type: "delete";
index: number;
deletedItem: Lson;
} | {
type: "move";
index: number;
previousIndex: number;
item: Lson;
} | {
type: "set";
index: number;
item: Lson;
};
/**
* A LiveList notification that is sent in-client to any subscribers whenever
* one or more of the items inside the LiveList instance have changed.
*/
type LiveListUpdates<TItem extends Lson> = {
type: "LiveList";
node: LiveList<TItem>;
updates: LiveListUpdateDelta[];
};
/**
* The LiveList class represents an ordered collection of items that is synchronized across clients.
*/
declare class LiveList<TItem extends Lson> extends AbstractCrdt {
#private;
constructor(items: TItem[]);
/**
* Returns the number of elements.
*/
get length(): number;
/**
* Adds one element to the end of the LiveList.
* @param element The element to add to the end of the LiveList.
*/
push(element: TItem): void;
/**
* Inserts one element at a specified index.
* @param element The element to insert.
* @param index The index at which you want to insert the element.
*/
insert(element: TItem, index: number): void;
/**
* Move one element from one index to another.
* @param index The index of the element to move
* @param targetIndex The index where the element should be after moving.
*/
move(index: number, targetIndex: number): void;
/**
* Deletes an element at the specified index
* @param index The index of the element to delete
*/
delete(index: number): void;
clear(): void;
set(index: number, item: TItem): void;
/**
* Tests whether all elements pass the test implemented by the provided function.
* @param predicate Function to test for each element, taking two arguments (the element and its index).
* @returns true if the predicate function returns a truthy value for every element. Otherwise, false.
*/
every(predicate: (value: TItem, index: number) => unknown): boolean;
/**
* Creates an array with all elements that pass the test implemented by the provided function.
* @param predicate Function to test each element of the LiveList. Return a value that coerces to true to keep the element, or to false otherwise.
* @returns An array with the elements that pass the test.
*/
filter(predicate: (value: TItem, index: number) => unknown): TItem[];
/**
* Returns the first element that satisfies the provided testing function.
* @param predicate Function to execute on each value.
* @returns The value of the first element in the LiveList that satisfies the provided testing function. Otherwise, undefined is returned.
*/
find(predicate: (value: TItem, index: number) => unknown): TItem | undefined;
/**
* Returns the index of the first element in the LiveList that satisfies the provided testing function.
* @param predicate Function to execute on each value until the function returns true, indicating that the satisfying element was found.
* @returns The index of the first element in the LiveList that passes the test. Otherwise, -1.
*/
findIndex(predicate: (value: TItem, index: number) => unknown): number;
/**
* Executes a provided function once for each element.
* @param callbackfn Function to execute on each element.
*/
forEach(callbackfn: (value: TItem, index: number) => void): void;
/**
* Get the element at the specified index.
* @param index The index on the element to get.
* @returns The element at the specified index or undefined.
*/
get(index: number): TItem | undefined;
/**
* Returns the first index at which a given element can be found in the LiveList, or -1 if it is not present.
* @param searchElement Element to locate.
* @param fromIndex The index to start the search at.
* @returns The first index of the element in the LiveList; -1 if not found.
*/
indexOf(searchElement: TItem, fromIndex?: number): number;
/**
* Returns the last index at which a given element can be found in the LiveList, or -1 if it is not present. The LiveList is searched backwards, starting at fromIndex.
* @param searchElement Element to locate.
* @param fromIndex The index at which to start searching backwards.
* @returns The last index of the element in the LiveList; -1 if not found.
*/
lastIndexOf(searchElement: TItem, fromIndex?: number): number;
/**
* Creates an array populated with the results of calling a provided function on every element.
* @param callback Function that is called for every element.
* @returns An array with each element being the result of the callback function.
*/
map<U>(callback: (value: TItem, index: number) => U): U[];
/**
* Tests whether at least one element in the LiveList passes the test implemented by the provided function.
* @param predicate Function to test for each element.
* @returns true if the callback function returns a truthy value for at least one element. Otherwise, false.
*/
some(predicate: (value: TItem, index: number) => unknown): boolean;
[Symbol.iterator](): IterableIterator<TItem>;
toJSON(): readonly ToJson<TItem>[];
clone(): LiveList<TItem>;
}
/**
* INTERNAL
*/
declare class LiveRegister<TValue extends Json> extends AbstractCrdt {
#private;
constructor(data: TValue);
get data(): TValue;
clone(): TValue;
}
type LiveStructure = LiveObject<LsonObject> | LiveList<Lson> | LiveMap<string, Lson>;
/**
* Think of Lson as a sibling of the Json data tree, except that the nested
* data structure can contain a mix of Json values and LiveStructure instances.
*/
type Lson = Json | LiveStructure;
/**
* LiveNode is the internal tree for managing Live data structures. The key
* difference with Lson is that all the Json values get represented in
* a LiveRegister node.
*/
type LiveNode = LiveStructure | LiveRegister<Json>;
/**
* A mapping of keys to Lson values. A Lson value is any valid JSON
* value or a Live storage data structure (LiveMap, LiveList, etc.)
*/
type LsonObject = Record<string, Lson | undefined>;
/**
* Helper type to convert any valid Lson type to the equivalent Json type.
*
* Examples:
*
* ToJson<42> // 42
* ToJson<'hi'> // 'hi'
* ToJson<number> // number
* ToJson<string> // string
* ToJson<string | LiveList<number>> // string | readonly number[]
* ToJson<LiveMap<string, LiveList<number>>>
* // { readonly [key: string]: readonly number[] }
* ToJson<LiveObject<{ a: number, b: LiveList<string>, c?: number }>>
* // { readonly a: null, readonly b: readonly string[], readonly c?: number }
*/
type ToJson<L extends Lson | LsonObject> = L extends LiveList<infer I extends Lson> ? Lson extends I ? readonly ReadonlyJson[] : readonly ToJson<I>[] : L extends LiveObject<infer O extends LsonObject> ? LsonObject extends O ? ReadonlyJsonObject : {
readonly [K in keyof O]: ToJson<Exclude<O[K], undefined>> | (undefined extends O[K] ? undefined : never);
} : L extends LiveMap<infer KS extends string, infer V extends Lson> ? Lson extends V ? ReadonlyJsonObject : {
readonly [K in KS]: ToJson<V>;
} : L extends LsonObject ? string extends keyof L ? ReadonlyJsonObject : {
readonly [K in keyof L]: ToJson<Exclude<L[K], undefined>> | (undefined extends L[K] ? undefined : never);
} : L extends Json ? L : never;
type DateToString<T> = {
[P in keyof T]: T[P] extends Date ? string : T[P] extends Date | null ? string | null : T[P] extends Date | undefined ? string | undefined : T[P];
};
type MentionData = Relax<UserMentionData | GroupMentionData>;
type UserMentionData = {
kind: "user";
id: string;
};
type GroupMentionData = {
kind: "group";
id: string;
userIds?: string[];
};
type InboxNotificationThreadData = {
kind: "thread";
id: string;
roomId: string;
threadId: string;
notifiedAt: Date;
readAt: Date | null;
};
type InboxNotificationTextMentionData = {
kind: "textMention";
id: string;
roomId: string;
notifiedAt: Date;
readAt: Date | null;
createdBy: string;
mentionId: string;
mention: MentionData;
};
type InboxNotificationTextMentionDataPlain = DateToString<InboxNotificationTextMentionData>;
type ActivityData = Record<string, string | boolean | number | undefined>;
type InboxNotificationActivity<K extends keyof DAD = keyof DAD> = {
id: string;
createdAt: Date;
data: DAD[K];
};
type InboxNotificationCustomData<K extends keyof DAD = keyof DAD> = {
kind: K;
id: string;
roomId?: string;
subjectId: string;
notifiedAt: Date;
readAt: Date | null;
activities: InboxNotificationActivity<K>[];
};
type InboxNotificationData = InboxNotificationThreadData | InboxNotificationCustomData | InboxNotificationTextMentionData;
type InboxNotificationThreadDataPlain = DateToString<InboxNotificationThreadData>;
type InboxNotificationCustomDataPlain = Omit<DateToString<InboxNotificationCustomData>, "activities"> & {
activities: DateToString<InboxNotificationActivity>[];
};
type InboxNotificationDataPlain = InboxNotificationThreadDataPlain | InboxNotificationCustomDataPlain | InboxNotificationTextMentionDataPlain;
type InboxNotificationDeleteInfo = {
type: "deletedInboxNotification";
id: string;
roomId: string;
deletedAt: Date;
};
type BaseActivitiesData = {
[key: `$${string}`]: ActivityData;
};
type BaseGroupInfo = {
[key: string]: Json | undefined;
/**
* The name of the group.
*/
name?: string;
/**
* The avatar of the group.
*/
avatar?: string;
/**
* The description of the group.
*/
description?: string;
};
type BaseRoomInfo = {
[key: string]: Json | undefined;
/**
* The name of the room.
*/
name?: string;
/**
* The URL of the room.
*/
url?: string;
};
declare global {
/**
* Namespace for user-defined Liveblocks types.
*/
export interface Liveblocks {
[key: string]: unknown;
}
}
type ExtendableTypes = "Presence" | "Storage" | "UserMeta" | "RoomEvent" | "ThreadMetadata" | "CommentMetadata" | "FeedMetadata" | "FeedMessageData" | "RoomInfo" | "GroupInfo" | "ActivitiesData";
type MakeErrorString<K extends ExtendableTypes, Reason extends string = "does not match its requirements"> = `The type you provided for '${K}' ${Reason}. To learn how to fix this, see https://liveblocks.io/docs/errors/${K}`;
type GetOverride<K extends ExtendableTypes, B, Reason extends string = "does not match its requirements"> = GetOverrideOrErrorValue<K, B, MakeErrorString<K, Reason>>;
type GetOverrideOrErrorValue<K extends ExtendableTypes, B, ErrorType> = unknown extends Liveblocks[K] ? B : Liveblocks[K] extends B ? Liveblocks[K] : ErrorType;
type DP = GetOverride<"Presence", JsonObject, "is not a valid JSON object">;
type DS = GetOverride<"Storage", LsonObject, "is not a valid LSON value">;
type DU = GetOverrideOrErrorValue<"UserMeta", BaseUserMeta, Record<"id" | "info", MakeErrorString<"UserMeta">>>;
type DE = GetOverride<"RoomEvent", Json, "is not a valid JSON value">;
type DTM = GetOverride<"ThreadMetadata", BaseMetadata>;
type DCM = GetOverride<"CommentMetadata", BaseMetadata>;
type DFM = GetOverride<"FeedMetadata", Json, "is not a valid JSON value">;
type DFMD = GetOverride<"FeedMessageData", Json, "is not a valid JSON value">;
type DRI = GetOverride<"RoomInfo", BaseRoomInfo>;
type DGI = GetOverride<"GroupInfo", BaseGroupInfo>;
type DAD = GetOverrideOrErrorValue<"ActivitiesData", BaseActivitiesData, {
[K in keyof Liveblocks["ActivitiesData"]]: "At least one of the custom notification kinds you provided for 'ActivitiesData' does not match its requirements. To learn how to fix this, see https://liveblocks.io/docs/errors/ActivitiesData";
}>;
type KDAD = keyof DAD extends `$${string}` ? keyof DAD : "Custom notification kinds must start with '$' but your custom 'ActivitiesData' type contains at least one kind which doesn't. To learn how to fix this, see https://liveblocks.io/docs/errors/ActivitiesData";
type BaseMetadata = Record<string, string | boolean | number | undefined>;
type CommentReaction = {
emoji: string;
createdAt: Date;
users: {
id: string;
}[];
};
type CommentAttachment = {
type: "attachment";
id: string;
name: string;
size: number;
mimeType: string;
};
type CommentLocalAttachmentIdle = {
type: "localAttachment";
status: "idle";
id: string;
name: string;
size: number;
mimeType: string;
file: File;
};
type CommentLocalAttachmentUploading = {
type: "localAttachment";
status: "uploading";
id: string;
name: string;
size: number;
mimeType: string;
file: File;
};
type CommentLocalAttachmentUploaded = {
type: "localAttachment";
status: "uploaded";
id: string;
name: string;
size: number;
mimeType: string;
file: File;
};
type CommentLocalAttachmentError = {
type: "localAttachment";
status: "error";
id: string;
name: string;
size: number;
mimeType: string;
file: File;
error: Error;
};
type CommentLocalAttachment = CommentLocalAttachmentIdle | CommentLocalAttachmentUploading | CommentLocalAttachmentUploaded | CommentLocalAttachmentError;
type CommentMixedAttachment = CommentAttachment | CommentLocalAttachment;
/**
* Represents a comment.
*/
type CommentData<CM extends BaseMetadata = DCM> = {
type: "comment";
id: string;
threadId: string;
roomId: string;
userId: string;
createdAt: Date;
editedAt?: Date;
reactions: CommentReaction[];
attachments: CommentAttachment[];
metadata: CM;
} & Relax<{
body: CommentBody;
} | {
deletedAt: Date;
}>;
type CommentDataPlain<CM extends BaseMetadata = DCM> = Omit<DateToString<CommentData<CM>>, "reactions" | "body" | "metadata"> & {
reactions: DateToString<CommentReaction>[];
metadata: CM;
} & Relax<{
body: CommentBody;
} | {
deletedAt: string;
}>;
type CommentBodyBlockElement = CommentBodyParagraph;
type CommentBodyInlineElement = CommentBodyText | CommentBodyMention | CommentBodyLink;
type CommentBodyElement = CommentBodyBlockElement | CommentBodyInlineElement;
type CommentBodyParagraph = {
type: "paragraph";
children: CommentBodyInlineElement[];
};
type CommentBodyMention = Relax<CommentBodyUserMention | CommentBodyGroupMention>;
type CommentBodyUserMention = {
type: "mention";
kind: "user";
id: string;
};
type CommentBodyGroupMention =