@liveblocks/core
Version:
Private internals for Liveblocks. DO NOT import directly from this package!
1,424 lines (1,386 loc) • 179 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;
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;
};
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;
};
declare enum Permission {
Read = "room:read",
Write = "room:write",
PresenceWrite = "room:presence:write",
CommentsWrite = "comments:write",
CommentsRead = "comments:read"
}
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>;
getItemState: (input: I) => AsyncResult<O> | undefined;
invalidate: (inputs?: I[]) => void;
};
declare const kTrigger: unique symbol;
/**
* Runs a callback function that is allowed to change multiple signals. At the
* end of the batch, all changed signals will be notified (at most once).
*
* Nesting batches is supported.
*/
declare function batch(callback: Callback<void>): void;
type SignalType<S extends ISignal<any>> = S extends ISignal<infer T> ? T : never;
interface ISignal<T> {
get(): T;
subscribe(callback: Callback<void>): UnsubscribeCallback;
addSink(sink: DerivedSignal<unknown>): void;
removeSink(sink: DerivedSignal<unknown>): void;
}
/**
* Base functionality every Signal implementation needs.
*/
declare abstract class AbstractSignal<T> implements ISignal<T>, Observable<void> {
#private;
constructor(equals?: (a: T, b: T) => boolean);
dispose(): void;
abstract get(): T;
get hasWatchers(): boolean;
[kTrigger](): void;
subscribe(callback: Callback<void>): UnsubscribeCallback;
subscribeOnce(callback: Callback<void>): UnsubscribeCallback;
waitUntil(): never;
markSinksDirty(): void;
addSink(sink: DerivedSignal<unknown>): void;
removeSink(sink: DerivedSignal<unknown>): void;
asReadonly(): ISignal<T>;
}
declare class Signal<T> extends AbstractSignal<T> {
#private;
constructor(value: T, equals?: (a: T, b: T) => boolean);
dispose(): void;
get(): T;
set(newValue: T | ((oldValue: T) => T)): void;
}
declare class DerivedSignal<T> extends AbstractSignal<T> {
#private;
static from<Ts extends unknown[], V>(...args: [...signals: {
[K in keyof Ts]: ISignal<Ts[K]>;
}, transform: (...values: Ts) => V]): DerivedSignal<V>;
static from<Ts extends unknown[], V>(...args: [...signals: {
[K in keyof Ts]: ISignal<Ts[K]>;
}, transform: (...values: Ts) => V, equals: (a: V, b: V) => boolean]): DerivedSignal<V>;
private constructor();
dispose(): void;
get isDirty(): boolean;
markDirty(): void;
get(): T;
/**
* Called by the Signal system if one or more of the dependent signals have
* changed. In the case of a DerivedSignal, we'll only want to re-evaluate
* the actual value if it's being watched, or any of their sinks are being
* watched actively.
*/
[kTrigger](): void;
}
/**
* A MutableSignal is a bit like Signal, except its state is managed by
* a single value whose reference does not change but is mutated.
*
* Similar to how useSyncExternalState() works in React, there is a way to read
* the current state at any point in time synchronously, and a way to update
* its reference.
*/
declare class MutableSignal<T extends object> extends AbstractSignal<T> {
#private;
constructor(initialState: T);
dispose(): void;
get(): T;
/**
* Invokes a callback function that is allowed to mutate the given state
* value. Do not change the value outside of the callback.
*
* If the callback explicitly returns `false`, it's assumed that the state
* was not changed.
*/
mutate(callback?: (state: T) => void | boolean): 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 enum OpCode {
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
}
/**
* These operations are the payload for {@link UpdateStorageServerMsg} messages
* only.
*/
type Op = AckOp | 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 intent?: "set";
readonly deletedId?: string;
readonly type: OpCode.CREATE_OBJECT;
readonly parentId: string;
readonly parentKey: string;
readonly data: JsonObject;
};
type CreateListOp = {
readonly opId?: string;
readonly id: string;
readonly intent?: "set";
readonly deletedId?: string;
readonly type: OpCode.CREATE_LIST;
readonly parentId: string;
readonly parentKey: string;
};
type CreateMapOp = {
readonly opId?: string;
readonly id: string;
readonly intent?: "set";
readonly deletedId?: string;
readonly type: OpCode.CREATE_MAP;
readonly parentId: string;
readonly parentKey: string;
};
type CreateRegisterOp = {
readonly opId?: string;
readonly id: string;
readonly intent?: "set";
readonly deletedId?: string;
readonly type: OpCode.CREATE_REGISTER;
readonly parentId: string;
readonly parentKey: string;
readonly data: Json;
};
type DeleteCrdtOp = {
readonly opId?: string;
readonly id: string;
readonly type: OpCode.DELETE_CRDT;
};
type AckOp = {
readonly type: OpCode.DELETE_CRDT;
readonly id: "ACK";
readonly opId: string;
};
/**
* Create an Op that can be used as an acknowledgement for the given opId, to
* send back to the originating client in cases where the server decided to
* ignore the Op and not forward it.
*
* Why?
* It's important for the client to receive an acknowledgement for this, so
* that it can correctly update its own unacknowledged Ops administration.
* Otherwise it could get in "synchronizing" state indefinitely.
*
* CLEVER HACK
* Introducing a new Op type for this would not be backward-compatible as
* receiving such Op would crash old clients :(
* So the clever backward-compatible hack pulled here is that we codify the
* acknowledgement as a "deletion Op" for the non-existing node id "ACK". In
* old clients such Op is accepted, but will effectively be a no-op as that
* node does not exist, but as a side-effect the Op will get acknowledged.
*/
declare function ackOp(opId: string): AckOp;
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;
};
declare enum ClientMsgCode {
UPDATE_PRESENCE = 100,
BROADCAST_EVENT = 103,
FETCH_STORAGE = 200,
UPDATE_STORAGE = 201,
FETCH_YDOC = 300,
UPDATE_YDOC = 301
}
/**
* Messages that can be sent from the client to the server.
*/
type ClientMsg<P extends JsonObject, E extends Json> = BroadcastEventClientMsg<E> | UpdatePresenceClientMsg<P> | UpdateStorageClientMsg | FetchStorageClientMsg | FetchYDocClientMsg | UpdateYDocClientMsg;
type BroadcastEventClientMsg<E extends Json> = {
type: ClientMsgCode.BROADCAST_EVENT;
event: E;
};
type UpdatePresenceClientMsg<P extends JsonObject> = {
readonly type: ClientMsgCode.UPDATE_PRESENCE;
/**
* Set this to any number to signify that this is a Full Presence™
* update, not a patch.
*
* The numeric value itself no longer has specific meaning. Historically,
* this field was intended so that clients could ignore these broadcasted
* full presence messages, but it turned out that getting a full presence
* "keyframe" from time to time was useful.
*
* So nowadays, the presence (pun intended) of this `targetActor` field
* is a backward-compatible way of expressing that the `data` contains
* all presence fields, and isn't a partial "patch".
*/
readonly targetActor: number;
readonly data: P;
} | {
readonly type: ClientMsgCode.UPDATE_PRESENCE;
/**
* Absence of the `targetActor` field signifies that this is a Partial
* Presence™ "patch".
*/
readonly targetActor?: undefined;
readonly data: Partial<P>;
};
type UpdateStorageClientMsg = {
readonly type: ClientMsgCode.UPDATE_STORAGE;
readonly ops: Op[];
};
type FetchStorageClientMsg = {
readonly type: ClientMsgCode.FETCH_STORAGE;
};
type FetchYDocClientMsg = {
readonly type: ClientMsgCode.FETCH_YDOC;
readonly vector: string;
readonly guid?: string;
readonly v2?: boolean;
};
type UpdateYDocClientMsg = {
readonly type: ClientMsgCode.UPDATE_YDOC;
readonly update: string;
readonly guid?: string;
readonly v2?: boolean;
};
/**
* Represents an indefinitely deep arbitrary immutable data
* structure, as returned by the .toImmutable().
*/
type Immutable = Scalar | ImmutableList | ImmutableObject | ImmutableMap;
type Scalar = string | number | boolean | null;
type ImmutableList = readonly Immutable[];
type ImmutableObject = {
readonly [key: string]: Immutable | undefined;
};
type ImmutableMap = ReadonlyMap<string, Immutable>;
type UpdateDelta = {
type: "update";
} | {
type: "delete";
};
/**
* "Plain LSON" is a JSON-based format that's used when serializing Live structures
* to send them over HTTP (e.g. in the API endpoint to let users upload their initial
* Room storage, in the API endpoint to fetch a Room's storage, ...).
*
* In the client, you would typically create LSON values using:
*
* new LiveObject({ x: 0, y: 0 })
*
* But over HTTP, this has to be serialized somehow. The "Plain LSON" format
* is what's used in the POST /init-storage-new endpoint, to allow users to
* control which parts of their data structure should be considered "Live"
* objects, and which parts are "normal" objects.
*
* So if they have a structure like:
*
* { x: 0, y: 0 }
*
* And want to make it a Live object, they can serialize it by wrapping it in
* a special "annotation":
*
* {
* "liveblocksType": "LiveObject",
* "data": { x: 0, y: 0 },
* }
*
* This "Plain LSON" data format defines exactly those wrappings.
*
* To summarize:
*
* LSON value | Plain LSON equivalent
* ----------------------+----------------------------------------------
* 42 | 42
* [1, 2, 3] | [1, 2, 3]
* { x: 0, y: 0 } | { x: 0, y: 0 }
* ----------------------+----------------------------------------------
* new LiveList(...) | { liveblocksType: "LiveList", data: ... }
* new LiveMap(...) | { liveblocksType: "LiveMap", data: ... }
* new LiveObject(...) | { liveblocksType: "LiveObject", data: ... }
*
*/
type PlainLsonFields = Record<string, PlainLson>;
type PlainLsonObject = {
liveblocksType: "LiveObject";
data: PlainLsonFields;
};
type PlainLsonMap = {
liveblocksType: "LiveMap";
data: PlainLsonFields;
};
type PlainLsonList = {
liveblocksType: "LiveList";
data: PlainLson[];
};
type PlainLson = PlainLsonObject | PlainLsonMap | PlainLsonList | Json;
type IdTuple<T> = [id: string, value: T];
declare enum CrdtType {
OBJECT = 0,
LIST = 1,
MAP = 2,
REGISTER = 3
}
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;
};
declare function isRootCrdt(crdt: SerializedCrdt): crdt is SerializedRootObject;
declare function isChildCrdt(crdt: SerializedCrdt): crdt is SerializedChild;
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>(items: IdTuple<SerializedCrdt>[], pool: ManagedPool): LiveObject<O>;
constructor(obj?: O);
/**
* Transform the LiveObject into a javascript object
*/
toObject(): O;
/**
* 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;
/**
* 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;
toImmutable(): ToImmutable<O>;
clone(): LiveObject<O>;
}
/**
* Helper type to convert any valid Lson type to the equivalent Json type.
*
* Examples:
*
* ToImmutable<42> // 42
* ToImmutable<'hi'> // 'hi'
* ToImmutable<number> // number
* ToImmutable<string> // string
* ToImmutable<string | LiveList<number>> // string | readonly number[]
* ToImmutable<LiveMap<string, LiveList<number>>>
* // ReadonlyMap<string, readonly number[]>
* ToImmutable<LiveObject<{ a: number, b: LiveList<string>, c?: number }>>
* // { readonly a: null, readonly b: readonly string[], readonly c?: number }
*
*/
type ToImmutable<L extends Lson | LsonObject> = L extends LiveList<infer I> ? readonly ToImmutable<I>[] : L extends LiveObject<infer O> ? ToImmutable<O> : L extends LiveMap<infer K, infer V> ? ReadonlyMap<K, ToImmutable<V>> : L extends LsonObject ? {
readonly [K in keyof L]: ToImmutable<Exclude<L[K], undefined>> | (undefined extends L[K] ? undefined : never);
} : L extends Json ? L : never;
/**
* Returns PlainLson for a given Json or LiveStructure, suitable for calling the storage init api
*/
declare function toPlainLson(lson: Lson): PlainLson;
/**
* 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;
toImmutable(): ReadonlyMap<TKey, ToImmutable<TValue>>;
clone(): LiveMap<TKey, TValue>;
}
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;
/**
* 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 roomId: string;
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: Op[], 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;
}
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: Op[], 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;
};
/**
* @private Private API, never use this API directly.
*/
declare function createManagedPool(roomId: string, options: CreateManagedPoolOptions): ManagedPool;
declare abstract class AbstractCrdt {
#private;
get roomId(): string | null;
/**
* Return an immutable snapshot of this Live node and its children.
*/
toImmutable(): Immutable;
/**
* 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;
/**
* Returns an Array of all the elements in the LiveList.
*/
toArray(): TItem[];
/**
* 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 LiveLsit is searched backwards, starting at fromIndex.
* @param searchElement Element to locate.
* @param fromIndex The index at which to start searching backwards.
* @returns
*/
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>;
toImmutable(): readonly ToImmutable<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 = {
[key: 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 | number[]
* ToJson<LiveMap<string, LiveList<number>>>
* // { [key: string]: number[] }
* ToJson<LiveObject<{ a: number, b: LiveList<string>, c?: number }>>
* // { a: null, b: string[], c?: number }
*
*/
type ToJson<T extends Lson | LsonObject> = T extends Json ? T : T extends LsonObject ? {
[K in keyof T]: ToJson<Exclude<T[K], undefined>> | (undefined extends T[K] ? undefined : never);
} : T extends LiveList<infer I> ? ToJson<I>[] : T extends LiveObject<infer O> ? ToJson<O> : T extends LiveMap<infer KS, infer V> ? {
[K in KS]: ToJson<V>;
} : 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 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;
};
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 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" | "RoomInfo" | "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 DM = GetOverride<"ThreadMetadata", BaseMetadata>;
type DRI = GetOverride<"RoomInfo", BaseRoomInfo>;
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 = {
type: "comment";
id: string;
threadId: string;
roomId: string;
userId: string;
createdAt: Date;
editedAt?: Date;
reactions: CommentReaction[];
attachments: CommentAttachment[];
} & Relax<{
body: CommentBody;
} | {
deletedAt: Date;
}>;
type CommentDataPlain = Omit<DateToString<CommentData>, "reactions" | "body"> & {
reactions: DateToString<CommentReaction>[];
} & 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>;
type CommentBodyUserMention = {
type: "mention";
kind: "user";
id: string;
};
type CommentBodyLink = {
type: "link";
url: string;
text?: string;
};
type CommentBodyText = {
bold?: boolean;
italic?: boolean;
strikethrough?: boolean;
code?: boolean;
text: string;
};
type CommentBody = {
version: 1;
content: CommentBodyBlockElement[];
};
type CommentUserReaction = {
emoji: string;
createdAt: Date;
userId: string;
};
type CommentUserReactionPlain = DateToString<CommentUserReaction>;
/**
* Represents a thread of comments.
*/
type ThreadData<M extends BaseMetadata = DM> = {
type: "thread";
id: string;
roomId: string;
createdAt: Date;
updatedAt: Date;
comments: CommentData[];
metadata: M;
resolved: boolean;
};
interface ThreadDataWithDeleteInfo<M extends BaseMetadata = DM> extends ThreadData<M> {
deletedAt?: Date;
}
type ThreadDataPlain<M extends BaseMetadata> = Omit<DateToString<ThreadData<M>>, "comments" | "metadata"> & {
comments: CommentDataPlain[];
metadata: M;
};
type ThreadDeleteInfo = {
type: "deletedThread";
id: string;
roomId: string;
deletedAt: Date;
};
type StringOperators<T> = T | {
startsWith: string;
};
/**
* This type can be used to build a metadata query string (compatible
* with `@liveblocks/query-parser`) through a type-safe API.
*
* In addition to exact values (`:` in query string), it adds:
* - to strings:
* - `startsWith` (`^` in query string)
*/
type QueryMetadata<M extends BaseMetadata> = {
[K in keyof M]: (string extends M[K] ? StringOperators<M[K]> : M[K]) | null;
};
/**
* Pre-defined notification channels support list.
*/
type NotificationChannel = "email" | "slack" | "teams" | "webPush";
/**
* `K` represents custom notification kinds
* defined in the augmentation `ActivitiesData` (e.g `liveblocks.config.ts`).
* It means the type `NotificationKind` will be shaped like:
* thread | textMention | $customKind1 | $customKind2 | ...
*/
type NotificationKind<K extends keyof DAD = keyof DAD> = "thread" | "textMention" | K;
/**
* A notification channel settings is a set of notification kinds.
* One setting can have multiple kinds (+ augmentation)
*/
type NotificationChannelSettings = {
[K in NotificationKind]: boolean;
};
/**
* @private
*
* Base definition of notification settings.
* Plain means it's a simple object coming from the remote backend.
*
* It's the raw settings object where somme channels cannot exists
* because there are no notification kinds enabled on the dashboard.
* And this object isn't yet proxied by the creator factory `createNotificationSettings`.
*/
type NotificationSettingsPlain = {
[C in NotificationChannel]?: NotificationChannelSettings;
};
/**
* Notification settings.
* One channel for one set of settings.
*/
type NotificationSettings = {
[C in NotificationChannel]: NotificationChannelSettings | null;
};
/**
* It creates a deep partial specific for `NotificationSettings`
* to offer a nice DX when updating the settings (e.g not being forced to define every keys)
* and at the same the some preserver the augmentation for custom kinds (e.g `liveblocks.config.ts`).
*/
type DeepPartialWithAugmentation<T> = T extends object ? {
[P in keyof T]?: T[P] extends {
[K in NotificationKind]: boolean;
} ? Partial<T[P]> & {
[K in keyof DAD]?: boolean;
} : DeepPartialWithAugmentation<T[P]>;
} : T;
/**
* Partial notification settings with augmentation preserved gracefully.
* It means you can update the settings without being forced to define every keys.
* Useful when implementing update functions.
*/
type PartialNotificationSettings = DeepPartialWithAugmentation<NotificationSettingsPlain>;
/**
* @private
*
* Creates a `NotificationSettings` object with the given initial plain settings.
* It defines a getter for each channel to access the settings and returns `null` with an error log
* in case the required channel isn't enabled in the dashboard.
*
* You can see this function as `Proxy` like around `NotificationSettingsPlain` type.
* We can't predict what will be enabled on the dashboard or not, so it's important
* provide a good DX to developers by returning `null` completed by an error log
* when they try to access a channel that isn't enabled in the dashboard.
*/
declare function createNotificationSettings(plain: NotificationSettingsPlain): NotificationSettings;
/**
* @private
*
* Patch a `NotificationSettings` object by applying notification kind updates
* coming from a `PartialNotificationSettings` object.
*/
declare function patchNotificationSettings(existing: NotificationSettings, patch: PartialNotificationSettings): NotificationSettings;
/**
*
* Utility to check if a notification channel settings
* is enabled for every notification kinds.
*
* Usage:
* ```ts
* const isEmailChannelEnabled = isNotificationChannelEnabled(settings.email);
* ```
*/
declare function isNotificationChannelEnabled(settings: NotificationChannelSettings | null): boolean;
type RoomThreadsSubscriptionSettings = "all" | "replies_and_mentions" | "none";
type RoomTextMentionsSubscriptionSettings = "mine" | "none";
type RoomSubscriptionSettings = {
threads: RoomThreadsSubscriptionSettings;
textMentions: RoomTextMentionsSubscriptionSettings;
};
type UserRoomSubscriptionSettings = {
roomId: string;
} & RoomSubscriptionSettings;
type SubscriptionData<K extends keyof DAD = keyof DAD> = {
kind: NotificationKind<K>;
subjectId: string;
createdAt: Date;
};
type SubscriptionDataPlain = DateToString<SubscriptionData>;
type UserSubscriptionData<K extends keyof DAD = keyof DAD> = SubscriptionData<K> & {
userId: string;
};
type UserSubscriptionDataPlain = DateToString<UserSubscriptionData>;
type SubscriptionDeleteInfo = {
type: "deletedSubscription";
kind: NotificationKind;
subjectId: string;
deletedAt: Date;
};
type SubscriptionDeleteInfoPlain = DateToString<SubscriptionDeleteInfo>;
type SubscriptionKey = `${NotificationKind}:${string}`;
declare function getSubscriptionKey(subscription: SubscriptionData | SubscriptionDeleteInfo): SubscriptionKey;
declare function getSubscriptionKey(kind: NotificationKind, subjectId: string): SubscriptionKey;
/**
* Represents a user connected in a room. Treated as immutable.
*/
type User<P extends JsonObject = DP, U extends BaseUserMeta = DU> = {
/**
* 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: U["id"];
/**
* Additional user information that has been set in the authentication endpoint.
*/
readonly info: U["info"];
/**
* The user’s presence data.
*/
readonly presence: P;
/**
* True if the user can mutate the Room’s Storage and/or YDoc, false if they
* can only read but not mutate it.
*/
readonly canWrite: boolean;
/**
* True if the user can comment on a thread
*/
readonly canComment: boolean;
};
type InternalOthersEvent<P extends JsonObject, U exten