@liveblocks/node
Version:
A server-side utility that lets you set up a Liveblocks authentication endpoint. Liveblocks is the all-in-one toolkit to build collaborative products like Figma, Notion, and more.
1,318 lines (1,289 loc) • 82.8 kB
JavaScript
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class;// src/index.ts
var _core = require('@liveblocks/core');
// src/version.ts
var PKG_NAME = "@liveblocks/node";
var PKG_VERSION = "3.11.0";
var PKG_FORMAT = "cjs";
// src/client.ts
// src/lib/itertools.ts
async function asyncConsume(iterable) {
const result = [];
for await (const item of iterable) {
result.push(item);
}
return result;
}
async function runConcurrently(iterable, fn, concurrency) {
const queue = /* @__PURE__ */ new Set();
for await (const item of iterable) {
if (queue.size >= concurrency) {
await Promise.race(queue);
}
const promise = (async () => {
try {
await fn(item);
} finally {
queue.delete(promise);
}
})();
queue.add(promise);
}
if (queue.size > 0) {
await Promise.all(queue);
}
}
// src/lib/ndjson.ts
var LineStream = class extends TransformStream {
constructor() {
let buffer = "";
super({
transform(chunk, controller) {
buffer += chunk;
if (buffer.includes("\n")) {
const lines = buffer.split("\n");
for (let i = 0; i < lines.length - 1; i++) {
if (lines[i].length > 0) {
controller.enqueue(lines[i]);
}
}
buffer = lines[lines.length - 1];
}
},
flush(controller) {
if (buffer.length > 0) {
controller.enqueue(buffer);
}
}
});
}
};
var NdJsonStream = class extends TransformStream {
constructor() {
super({
transform(line, controller) {
const json = JSON.parse(line);
controller.enqueue(json);
}
});
}
};
// src/Session.ts
// src/utils.ts
var DEFAULT_BASE_URL = "https://api.liveblocks.io";
var VALID_KEY_CHARS_REGEX = /^[\w-]+$/;
function getBaseUrl(baseUrl) {
if (typeof baseUrl === "string" && baseUrl.startsWith("http")) {
return baseUrl;
} else {
return DEFAULT_BASE_URL;
}
}
async function fetchPolyfill() {
return typeof globalThis.fetch !== "undefined" ? globalThis.fetch : (await Promise.resolve().then(() => _interopRequireWildcard(require("node-fetch")))).default;
}
function isString(value) {
return typeof value === "string";
}
function startsWith(value, prefix) {
return isString(value) && value.startsWith(prefix);
}
function isNonEmpty(value) {
return isString(value) && value.length > 0;
}
function assertNonEmpty(value, field) {
if (!isNonEmpty(value)) {
throw new Error(
`Invalid value for field '${field}'. Please provide a non-empty string. For more information: https://liveblocks.io/docs/api-reference/liveblocks-node#authorize`
);
}
}
function assertSecretKey(value, field) {
if (!startsWith(value, "sk_")) {
throw new Error(
`Invalid value for field '${field}'. Secret keys must start with 'sk_'. Please provide the secret key from your Liveblocks dashboard at https://liveblocks.io/dashboard/apikeys.`
);
}
if (!VALID_KEY_CHARS_REGEX.test(value)) {
throw new Error(
`Invalid chars found in field '${field}'. Please check that you correctly copied the secret key from your Liveblocks dashboard at https://liveblocks.io/dashboard/apikeys.`
);
}
}
function normalizeStatusCode(statusCode) {
if (statusCode >= 200 && statusCode < 300) {
return 200;
} else if (statusCode >= 500) {
return 503;
} else {
return statusCode;
}
}
// src/Session.ts
var ALL_PERMISSIONS = Object.freeze([
"room:write",
"room:read",
"room:presence:write",
"comments:write",
"comments:read"
]);
function isPermission(value) {
return ALL_PERMISSIONS.includes(value);
}
var MAX_PERMS_PER_SET = 10;
var READ_ACCESS = Object.freeze([
"room:read",
"room:presence:write",
"comments:read"
]);
var FULL_ACCESS = Object.freeze(["room:write", "comments:write"]);
var roomPatternRegex = /^([*]|[^*]{1,128}[*]?)$/;
var Session = (_class = class {
__init() {this.FULL_ACCESS = FULL_ACCESS}
__init2() {this.READ_ACCESS = READ_ACCESS}
#postFn;
#userId;
#userInfo;
#tenantId;
#sealed = false;
#permissions = /* @__PURE__ */ new Map();
/** @internal */
constructor(postFn, userId, userInfo, tenantId) {;_class.prototype.__init.call(this);_class.prototype.__init2.call(this);
assertNonEmpty(userId, "userId");
this.#postFn = postFn;
this.#userId = userId;
this.#userInfo = userInfo;
this.#tenantId = tenantId;
}
#getOrCreate(roomId) {
if (this.#sealed) {
throw new Error("You can no longer change these permissions.");
}
let perms = this.#permissions.get(roomId);
if (perms) {
return perms;
} else {
if (this.#permissions.size >= MAX_PERMS_PER_SET) {
throw new Error(
"You cannot add permissions for more than 10 rooms in a single token"
);
}
perms = /* @__PURE__ */ new Set();
this.#permissions.set(roomId, perms);
return perms;
}
}
allow(roomIdOrPattern, newPerms) {
if (typeof roomIdOrPattern !== "string") {
throw new Error("Room name or pattern must be a string");
}
if (!roomPatternRegex.test(roomIdOrPattern)) {
throw new Error("Invalid room name or pattern");
}
if (newPerms.length === 0) {
throw new Error("Permission list cannot be empty");
}
const existingPerms = this.#getOrCreate(roomIdOrPattern);
for (const perm of newPerms) {
if (!isPermission(perm)) {
throw new Error(`Not a valid permission: ${perm}`);
}
existingPerms.add(perm);
}
return this;
}
/** @internal - For unit tests only */
hasPermissions() {
return this.#permissions.size > 0;
}
/** @internal - For unit tests only */
seal() {
if (this.#sealed) {
throw new Error(
"You cannot reuse Session instances. Please create a new session every time."
);
}
this.#sealed = true;
}
/** @internal - For unit tests only */
serializePermissions() {
return Object.fromEntries(
Array.from(this.#permissions.entries()).map(([pat, perms]) => [
pat,
Array.from(perms)
])
);
}
/**
* Call this to authorize the session to access Liveblocks. Note that this
* will return a Liveblocks "access token". Anyone that obtains such access
* token will have access to the allowed resources.
*/
async authorize() {
this.seal();
if (!this.hasPermissions()) {
console.warn(
"Access tokens without any permission will not be supported soon, you should use wildcards when the client requests a token for resources outside a room. See https://liveblocks.io/docs/errors/liveblocks-client/access-tokens-not-enough-permissions"
);
}
try {
const resp = await this.#postFn(_core.url`/v2/authorize-user`, {
// Required
userId: this.#userId,
permissions: this.serializePermissions(),
// Optional metadata
userInfo: this.#userInfo,
tenantId: this.#tenantId
});
return {
status: normalizeStatusCode(resp.status),
body: await resp.text()
};
} catch (er) {
return {
status: 503,
body: 'Call to /v2/authorize-user failed. See "error" for more information.',
error: er
};
}
}
}, _class);
// src/client.ts
function inflateRoomData(room) {
const createdAt = new Date(room.createdAt);
const lastConnectionAt = room.lastConnectionAt ? new Date(room.lastConnectionAt) : void 0;
return {
...room,
createdAt,
lastConnectionAt
};
}
function inflateAiCopilot(copilot) {
return {
...copilot,
createdAt: new Date(copilot.createdAt),
updatedAt: new Date(copilot.updatedAt),
lastUsedAt: copilot.lastUsedAt ? new Date(copilot.lastUsedAt) : void 0
};
}
function inflateKnowledgeSource(source) {
return {
...source,
createdAt: new Date(source.createdAt),
updatedAt: new Date(source.updatedAt),
lastIndexedAt: new Date(source.lastIndexedAt)
};
}
function inflateWebKnowledgeSourceLink(link) {
return {
...link,
createdAt: new Date(link.createdAt),
lastIndexedAt: new Date(link.lastIndexedAt)
};
}
var Liveblocks = class {
#secret;
#baseUrl;
/**
* Interact with the Liveblocks API from your Node.js backend.
*/
constructor(options) {
const options_ = options;
const secret = options_.secret;
assertSecretKey(secret, "secret");
this.#secret = secret;
this.#baseUrl = new URL(getBaseUrl(options.baseUrl));
}
async #post(path, json, options) {
const url3 = _core.urljoin.call(void 0, this.#baseUrl, path);
const headers = {
Authorization: `Bearer ${this.#secret}`,
"Content-Type": "application/json"
};
const fetch = await fetchPolyfill();
const res = await fetch(url3, {
method: "POST",
headers,
body: JSON.stringify(json),
signal: _optionalChain([options, 'optionalAccess', _ => _.signal])
});
return res;
}
async #putBinary(path, body, params, options) {
const url3 = _core.urljoin.call(void 0, this.#baseUrl, path, params);
const headers = {
Authorization: `Bearer ${this.#secret}`,
"Content-Type": "application/octet-stream"
};
const fetch = await fetchPolyfill();
return await fetch(url3, {
method: "PUT",
headers,
body,
signal: _optionalChain([options, 'optionalAccess', _2 => _2.signal])
});
}
async #delete(path, params, options) {
const url3 = _core.urljoin.call(void 0, this.#baseUrl, path, params);
const headers = {
Authorization: `Bearer ${this.#secret}`
};
const fetch = await fetchPolyfill();
const res = await fetch(url3, {
method: "DELETE",
headers,
signal: _optionalChain([options, 'optionalAccess', _3 => _3.signal])
});
return res;
}
async #get(path, params, options) {
const url3 = _core.urljoin.call(void 0, this.#baseUrl, path, params);
const headers = {
Authorization: `Bearer ${this.#secret}`
};
const fetch = await fetchPolyfill();
const res = await fetch(url3, {
method: "GET",
headers,
signal: _optionalChain([options, 'optionalAccess', _4 => _4.signal])
});
return res;
}
/* -------------------------------------------------------------------------------------------------
* Authentication
* -----------------------------------------------------------------------------------------------*/
/**
* Prepares a new session to authorize a user to access Liveblocks.
*
* IMPORTANT:
* Always make sure that you trust the user making the request to your
* backend before calling .prepareSession()!
*
* @param userId Tell Liveblocks the user ID of the user to authorize. Must
* uniquely identify the user account in your system. The uniqueness of this
* value will determine how many MAUs will be counted/billed.
*
* @param tenantId (optional) The tenant ID to authorize the user for.
*
* @param options.userInfo Custom metadata to attach to this user. Data you
* add here will be visible to all other clients in the room, through the
* `other.info` property.
*
*/
prepareSession(userId, ...rest) {
const options = rest[0];
return new Session(
this.#post.bind(this),
userId,
_optionalChain([options, 'optionalAccess', _5 => _5.userInfo]),
_optionalChain([options, 'optionalAccess', _6 => _6.tenantId])
);
}
/**
* Call this to authenticate the user as an actor you want to allow to use
* Liveblocks.
*
* You should use this method only if you want to manage your permissions
* through the Liveblocks Permissions API. This method is more complicated to
* set up, but allows for finer-grained specification of permissions.
*
* Calling `.identifyUser()` only lets you securely identify a user (and what
* groups they belong to). What permissions this user will end up having is
* determined by whatever permissions you assign the user/group in your
* Liveblocks account, through the Permissions API:
* https://liveblocks.io/docs/rooms/permissions
*
* IMPORTANT:
* Always verify that you trust the user making the request before calling
* .identifyUser()!
*
* @param identity Tell Liveblocks the user ID of the user to authenticate.
* Must uniquely identify the user account in your system. The uniqueness of
* this value will determine how many MAUs will be counted/billed.
*
* If you also want to assign which groups this user belongs to, use the
* object form and specify the `groupIds` property. Those `groupIds` should
* match the groupIds you assigned permissions to via the Liveblocks
* Permissions API, see
* https://liveblocks.io/docs/rooms/permissions#permissions-levels-groups-accesses-example
*
* @param options.userInfo Custom metadata to attach to this user. Data you
* add here will be visible to all other clients in the room, through the
* `other.info` property.
*/
// These fields define the security identity of the user. Whatever you pass in here will define which
async identifyUser(identity, ...rest) {
const options = rest[0];
const path = _core.url`/v2/identify-user`;
const { userId, groupIds, tenantId } = typeof identity === "string" ? { userId: identity, groupIds: void 0, tenantId: void 0 } : identity;
assertNonEmpty(userId, "userId");
const body = {
userId,
groupIds,
tenantId,
userInfo: _optionalChain([options, 'optionalAccess', _7 => _7.userInfo])
};
try {
const resp = await this.#post(path, body);
return {
status: normalizeStatusCode(resp.status),
body: await resp.text()
};
} catch (er) {
return {
status: 503,
body: `Call to ${_core.urljoin.call(void 0,
this.#baseUrl,
path
)} failed. See "error" for more information.`,
error: er
};
}
}
/* -------------------------------------------------------------------------------------------------
* Room
* -----------------------------------------------------------------------------------------------*/
/**
* Returns a list of your rooms. The rooms are returned sorted by creation date, from newest to oldest. You can filter rooms by metadata, users accesses and groups accesses.
* @param params.limit (optional) A limit on the number of rooms to be returned. The limit can range between 1 and 100, and defaults to 20.
* @param params.startingAfter (optional) A cursor used for pagination. You get the value from the response of the previous page.
* @param params.userId (optional) A filter on users accesses.
* @param params.metadata (optional) A filter on metadata. Multiple metadata keys can be used to filter rooms.
* @param params.groupIds (optional) A filter on groups accesses. Multiple groups can be used.
* @param params.tenantId (optional) A filter on tenant ID.
* @param params.query (optional) A query to filter rooms by. It is based on our query language. You can filter by metadata and room ID.
* @param options.signal (optional) An abort signal to cancel the request.
* @returns A list of rooms.
*/
async getRooms(params = {}, options) {
const path = _core.url`/v2/rooms`;
let query;
if (typeof params.query === "string") {
query = params.query;
} else if (typeof params.query === "object") {
query = _core.objectToQuery.call(void 0, params.query);
}
const queryParams = {
limit: params.limit,
startingAfter: params.startingAfter,
userId: params.userId,
groupIds: params.groupIds ? params.groupIds.join(",") : void 0,
query
};
const res = await this.#get(path, queryParams, options);
if (!res.ok) {
throw await LiveblocksError.from(res);
}
const page = await res.json();
const rooms = page.data.map(inflateRoomData);
return {
...page,
data: rooms
};
}
/**
* Iterates over all rooms that match the given criteria.
*
* The difference with .getRooms() is that pagination will happen
* automatically under the hood, using the given `pageSize`.
*
* @param criteria.userId (optional) A filter on users accesses.
* @param criteria.groupIds (optional) A filter on groups accesses. Multiple groups can be used.
* @param criteria.query.roomId (optional) A filter by room ID.
* @param criteria.query.metadata (optional) A filter by metadata.
*
* @param options.pageSize (optional) The page size to use for each request.
* @param options.signal (optional) An abort signal to cancel the request.
*/
async *iterRooms(criteria, options) {
const { signal } = _nullishCoalesce(options, () => ( {}));
const pageSize = _core.checkBounds.call(void 0, "pageSize", _nullishCoalesce(_optionalChain([options, 'optionalAccess', _8 => _8.pageSize]), () => ( 40)), 20);
let cursor = void 0;
while (true) {
const { nextCursor, data } = await this.getRooms(
{ ...criteria, startingAfter: cursor, limit: pageSize },
{ signal }
);
for (const item of data) {
yield item;
}
if (!nextCursor) {
break;
}
cursor = nextCursor;
}
}
/**
* Creates a new room with the given id.
* @param roomId The id of the room to create.
* @param params.defaultAccesses The default accesses for the room.
* @param params.groupsAccesses (optional) The group accesses for the room. Can contain a maximum of 100 entries. Key length has a limit of 40 characters.
* @param params.usersAccesses (optional) The user accesses for the room. Can contain a maximum of 100 entries. Key length has a limit of 40 characters.
* @param params.metadata (optional) The metadata for the room. Supports upto a maximum of 50 entries. Key length has a limit of 40 characters. Value length has a limit of 256 characters.
* @param params.tenantId (optional) The tenant ID to create the room for.
* @param options.signal (optional) An abort signal to cancel the request.
* @returns The created room.
*/
async createRoom(roomId, params, options) {
const {
defaultAccesses,
groupsAccesses,
usersAccesses,
metadata,
tenantId
} = params;
const res = await this.#post(
_optionalChain([options, 'optionalAccess', _9 => _9.idempotent]) ? _core.url`/v2/rooms?idempotent` : _core.url`/v2/rooms`,
{
id: roomId,
defaultAccesses,
groupsAccesses,
usersAccesses,
tenantId,
metadata
},
options
);
if (!res.ok) {
throw await LiveblocksError.from(res);
}
const data = await res.json();
return inflateRoomData(data);
}
/**
* Returns a room with the given id, or creates one with the given creation
* options if it doesn't exist yet.
*
* @param roomId The id of the room.
* @param params.defaultAccesses The default accesses for the room if the room will be created.
* @param params.groupsAccesses (optional) The group accesses for the room if the room will be created. Can contain a maximum of 100 entries. Key length has a limit of 40 characters.
* @param params.usersAccesses (optional) The user accesses for the room if the room will be created. Can contain a maximum of 100 entries. Key length has a limit of 40 characters.
* @param params.metadata (optional) The metadata for the room if the room will be created. Supports upto a maximum of 50 entries. Key length has a limit of 40 characters. Value length has a limit of 256 characters.
* @param params.tenantId (optional) The tenant ID to create the room for.
* @param options.signal (optional) An abort signal to cancel the request.
* @returns The room.
*/
async getOrCreateRoom(roomId, params, options) {
return await this.createRoom(roomId, params, {
...options,
idempotent: true
});
}
/**
* Updates or creates a new room with the given properties.
*
* @param roomId The id of the room to update or create.
* @param update The fields to update. These values will be updated when the room exists, or set when the room does not exist and gets created. Must specify at least one key.
* @param create (optional) The fields to only use when the room does not exist and will be created. When the room already exists, these values are ignored.
* @param options.signal (optional) An abort signal to cancel the request.
* @returns The room.
*/
async upsertRoom(roomId, params, options) {
const res = await this.#post(
_core.url`/v2/rooms/${roomId}/upsert`,
params,
options
);
if (!res.ok) {
throw await LiveblocksError.from(res);
}
const data = await res.json();
return inflateRoomData(data);
}
/**
* Returns a room with the given id.
* @param roomId The id of the room to return.
* @returns The room with the given id.
* @param options.signal (optional) An abort signal to cancel the request.
*/
async getRoom(roomId, options) {
const res = await this.#get(_core.url`/v2/rooms/${roomId}`, void 0, options);
if (!res.ok) {
throw await LiveblocksError.from(res);
}
const data = await res.json();
return inflateRoomData(data);
}
/**
* Updates specific properties of a room. It’s not necessary to provide the entire room’s information.
* Setting a property to `null` means to delete this property.
* @param roomId The id of the room to update.
* @param params.defaultAccesses (optional) The default accesses for the room.
* @param params.groupsAccesses (optional) The group accesses for the room. Can contain a maximum of 100 entries. Key length has a limit of 40 characters.
* @param params.usersAccesses (optional) The user accesses for the room. Can contain a maximum of 100 entries. Key length has a limit of 40 characters.
* @param params.metadata (optional) The metadata for the room. Supports upto a maximum of 50 entries. Key length has a limit of 40 characters. Value length has a limit of 256 characters.
* @param options.signal (optional) An abort signal to cancel the request.
* @returns The updated room.
*/
async updateRoom(roomId, params, options) {
const { defaultAccesses, groupsAccesses, usersAccesses, metadata } = params;
const res = await this.#post(
_core.url`/v2/rooms/${roomId}`,
{
defaultAccesses,
groupsAccesses,
usersAccesses,
metadata
},
options
);
if (!res.ok) {
throw await LiveblocksError.from(res);
}
const data = await res.json();
return inflateRoomData(data);
}
/**
* Deletes a room with the given id. A deleted room is no longer accessible from the API or the dashboard and it cannot be restored.
* @param roomId The id of the room to delete.
* @param options.signal (optional) An abort signal to cancel the request.
*/
async deleteRoom(roomId, options) {
const res = await this.#delete(
_core.url`/v2/rooms/${roomId}`,
void 0,
options
);
if (!res.ok) {
throw await LiveblocksError.from(res);
}
}
/**
* Prepares a room for connectivity, making the eventual connection faster. Use this when you know you'll be loading a room but are not yet connected to it.
* @param roomId The id of the room to prewarm.
* @param options.signal (optional) An abort signal to cancel the request.
*/
async prewarmRoom(roomId, options) {
const res = await this.#get(
_core.url`/v2/rooms/${roomId}/prewarm`,
void 0,
options
);
if (!res.ok) {
throw await LiveblocksError.from(res);
}
}
/**
* Returns a list of users currently present in the requested room. For better performance, we recommand to call this endpoint every 10 seconds maximum. Duplicates can happen if a user is in the requested room with multiple browser tabs opened.
* @param roomId The id of the room to get the users from.
* @param options.signal (optional) An abort signal to cancel the request.
* @returns A list of users currently present in the requested room.
*/
async getActiveUsers(roomId, options) {
const res = await this.#get(
_core.url`/v2/rooms/${roomId}/active_users`,
void 0,
options
);
if (!res.ok) {
throw await LiveblocksError.from(res);
}
return await res.json();
}
/**
* Boadcasts an event to a room without having to connect to it via the client from @liveblocks/client. The connectionId passed to event listeners is -1 when using this API.
* @param roomId The id of the room to broadcast the event to.
* @param message The message to broadcast. It can be any JSON serializable value.
* @param options.signal (optional) An abort signal to cancel the request.
*/
async broadcastEvent(roomId, message, options) {
const res = await this.#post(
_core.url`/v2/rooms/${roomId}/broadcast_event`,
message,
options
);
if (!res.ok) {
throw await LiveblocksError.from(res);
}
}
async getStorageDocument(roomId, format = "plain-lson", options) {
const res = await this.#get(
_core.url`/v2/rooms/${roomId}/storage`,
{ format },
options
);
if (!res.ok) {
throw await LiveblocksError.from(res);
}
return await res.json();
}
async #requestStorageMutation(roomId, options) {
const resp = await this.#post(
_core.url`/v2/rooms/${roomId}/request-storage-mutation`,
{},
options
);
if (!resp.ok) {
throw await LiveblocksError.from(resp);
}
if (resp.headers.get("content-type") !== "application/x-ndjson") {
throw new Error("Unexpected response content type");
}
if (resp.body === null) {
throw new Error("Unexpected null body in response");
}
const stream = resp.body.pipeThrough(new TextDecoderStream()).pipeThrough(new LineStream()).pipeThrough(new NdJsonStream());
const iter = stream[Symbol.asyncIterator]();
const first = (await iter.next()).value;
if (!_core.isPlainObject.call(void 0, first) || typeof first.actor !== "number") {
throw new Error("Failed to obtain a unique session");
}
const nodes = await asyncConsume(iter);
return { actor: first.actor, nodes };
}
/**
* Initializes a room’s Storage. The room must already exist and have an empty Storage.
* Calling this endpoint will disconnect all users from the room if there are any.
*
* @param roomId The id of the room to initialize the storage from.
* @param document The document to initialize the storage with.
* @param options.signal (optional) An abort signal to cancel the request.
* @returns The initialized storage document. It is of the same format as the one passed in.
*/
async initializeStorageDocument(roomId, document, options) {
const res = await this.#post(
_core.url`/v2/rooms/${roomId}/storage`,
document,
options
);
if (!res.ok) {
throw await LiveblocksError.from(res);
}
return await res.json();
}
/**
* Deletes all of the room’s Storage data and disconnect all users from the room if there are any. Note that this does not delete the Yjs document in the room if one exists.
* @param roomId The id of the room to delete the storage from.
* @param options.signal (optional) An abort signal to cancel the request.
*/
async deleteStorageDocument(roomId, options) {
const res = await this.#delete(
_core.url`/v2/rooms/${roomId}/storage`,
void 0,
options
);
if (!res.ok) {
throw await LiveblocksError.from(res);
}
}
/* -------------------------------------------------------------------------------------------------
* Yjs
* -----------------------------------------------------------------------------------------------*/
/**
* Returns a JSON representation of the room’s Yjs document.
* @param roomId The id of the room to get the Yjs document from.
* @param params.format (optional) If true, YText will return formatting.
* @param params.key (optional) If provided, returns only a single key’s value, e.g. doc.get(key).toJSON().
* @param params.type (optional) Used with key to override the inferred type, i.e. "ymap" will return doc.get(key, Y.Map).
* @param options.signal (optional) An abort signal to cancel the request.
* @returns A JSON representation of the room’s Yjs document.
*/
async getYjsDocument(roomId, params = {}, options) {
const { format, key, type } = params;
const path = _core.url`v2/rooms/${roomId}/ydoc`;
const res = await this.#get(
path,
{ formatting: format ? "true" : void 0, key, type },
options
);
if (!res.ok) {
throw await LiveblocksError.from(res);
}
return await res.json();
}
/**
* Send a Yjs binary update to the room’s Yjs document. You can use this endpoint to initialize Yjs data for the room or to update the room’s Yjs document.
* @param roomId The id of the room to send the Yjs binary update to.
* @param update The Yjs update to send. Typically the result of calling `Yjs.encodeStateAsUpdate(doc)`. Read the [Yjs documentation](https://docs.yjs.dev/api/document-updates) to learn how to create a binary update.
* @param params.guid (optional) If provided, the binary update will be applied to the Yjs subdocument with the given guid. If not provided, the binary update will be applied to the root Yjs document.
* @param options.signal (optional) An abort signal to cancel the request.
*/
async sendYjsBinaryUpdate(roomId, update, params = {}, options) {
const res = await this.#putBinary(
_core.url`/v2/rooms/${roomId}/ydoc`,
update,
{ guid: params.guid },
options
);
if (!res.ok) {
throw await LiveblocksError.from(res);
}
}
/**
* Returns the room’s Yjs document encoded as a single binary update. This can be used by Y.applyUpdate(responseBody) to get a copy of the document in your backend.
* See [Yjs documentation](https://docs.yjs.dev/api/document-updates) for more information on working with updates.
* @param roomId The id of the room to get the Yjs document from.
* @param params.guid (optional) If provided, returns the binary update of the Yjs subdocument with the given guid. If not provided, returns the binary update of the root Yjs document.
* @param options.signal (optional) An abort signal to cancel the request.
* @returns The room’s Yjs document encoded as a single binary update.
*/
async getYjsDocumentAsBinaryUpdate(roomId, params = {}, options) {
const res = await this.#get(
_core.url`/v2/rooms/${roomId}/ydoc-binary`,
{ guid: params.guid },
options
);
if (!res.ok) {
throw await LiveblocksError.from(res);
}
return res.arrayBuffer();
}
/* -------------------------------------------------------------------------------------------------
* Comments
* -----------------------------------------------------------------------------------------------*/
/**
* Gets all the threads in a room.
*
* @param params.roomId The room ID to get the threads from.
* @param params.query The query to filter threads by. It is based on our query language and can filter by metadata.
* @param options.signal (optional) An abort signal to cancel the request.
* @returns A list of threads.
*/
async getThreads(params, options) {
const { roomId } = params;
let query;
if (typeof params.query === "string") {
query = params.query;
} else if (typeof params.query === "object") {
query = _core.objectToQuery.call(void 0, params.query);
}
const res = await this.#get(
_core.url`/v2/rooms/${roomId}/threads`,
{ query },
options
);
if (!res.ok) {
throw await LiveblocksError.from(res);
}
const { data } = await res.json();
return {
data: data.map((thread) => _core.convertToThreadData.call(void 0, thread))
};
}
/**
* Gets a thread.
*
* @param params.roomId The room ID to get the thread from.
* @param params.threadId The thread ID.
* @param options.signal (optional) An abort signal to cancel the request.
* @returns A thread.
*/
async getThread(params, options) {
const { roomId, threadId } = params;
const res = await this.#get(
_core.url`/v2/rooms/${roomId}/threads/${threadId}`,
void 0,
options
);
if (!res.ok) {
throw await LiveblocksError.from(res);
}
return _core.convertToThreadData.call(void 0, await res.json());
}
/**
* @deprecated Prefer using `getMentionsFromCommentBody` to extract mentions
* from comments and threads, or `Liveblocks.getThreadSubscriptions` to get
* the list of users who are subscribed to a thread.
*
* Gets a thread's participants.
*
* Participants are users who have commented on the thread
* or users that have been mentioned in a comment.
*
* @param params.roomId The room ID to get the thread participants from.
* @param params.threadId The thread ID to get the participants from.
* @param options.signal (optional) An abort signal to cancel the request.
* @returns An object containing an array of participant IDs.
*/
async getThreadParticipants(params, options) {
const { roomId, threadId } = params;
const res = await this.#get(
_core.url`/v2/rooms/${roomId}/threads/${threadId}/participants`,
void 0,
options
);
if (!res.ok) {
throw await LiveblocksError.from(res);
}
return await res.json();
}
/**
* Gets a thread's subscriptions.
*
* @param params.roomId The room ID to get the thread subscriptions from.
* @param params.threadId The thread ID to get the subscriptions from.
* @param options.signal (optional) An abort signal to cancel the request.
* @returns An array of subscriptions.
*/
async getThreadSubscriptions(params, options) {
const { roomId, threadId } = params;
const res = await this.#get(
_core.url`/v2/rooms/${roomId}/threads/${threadId}/subscriptions`,
void 0,
options
);
if (!res.ok) {
throw await LiveblocksError.from(res);
}
const { data } = await res.json();
return {
data: data.map(_core.convertToUserSubscriptionData)
};
}
/**
* Gets a thread's comment.
*
* @param params.roomId The room ID to get the comment from.
* @param params.threadId The thread ID to get the comment from.
* @param params.commentId The comment ID.
* @param options.signal (optional) An abort signal to cancel the request.
* @returns A comment.
*/
async getComment(params, options) {
const { roomId, threadId, commentId } = params;
const res = await this.#get(
_core.url`/v2/rooms/${roomId}/threads/${threadId}/comments/${commentId}`,
void 0,
options
);
if (!res.ok) {
throw await LiveblocksError.from(res);
}
return _core.convertToCommentData.call(void 0, await res.json());
}
/**
* Creates a comment.
*
* @param params.roomId The room ID to create the comment in.
* @param params.threadId The thread ID to create the comment in.
* @param params.data.userId The user ID of the user who is set to create the comment.
* @param params.data.createdAt (optional) The date the comment is set to be created.
* @param params.data.body The body of the comment.
* @param options.signal (optional) An abort signal to cancel the request.
* @returns The created comment.
*/
async createComment(params, options) {
const { roomId, threadId, data } = params;
const res = await this.#post(
_core.url`/v2/rooms/${roomId}/threads/${threadId}/comments`,
{
...data,
createdAt: _optionalChain([data, 'access', _10 => _10.createdAt, 'optionalAccess', _11 => _11.toISOString, 'call', _12 => _12()])
},
options
);
if (!res.ok) {
throw await LiveblocksError.from(res);
}
return _core.convertToCommentData.call(void 0, await res.json());
}
/**
* Edits a comment.
* @param params.roomId The room ID to edit the comment in.
* @param params.threadId The thread ID to edit the comment in.
* @param params.commentId The comment ID to edit.
* @param params.data.body The body of the comment.
* @param params.data.editedAt (optional) The date the comment was edited.
* @param options.signal (optional) An abort signal to cancel the request.
* @returns The edited comment.
*/
async editComment(params, options) {
const { roomId, threadId, commentId, data } = params;
const res = await this.#post(
_core.url`/v2/rooms/${roomId}/threads/${threadId}/comments/${commentId}`,
{ ...data, editedAt: _optionalChain([data, 'access', _13 => _13.editedAt, 'optionalAccess', _14 => _14.toISOString, 'call', _15 => _15()]) },
options
);
if (!res.ok) {
throw await LiveblocksError.from(res);
}
return _core.convertToCommentData.call(void 0, await res.json());
}
/**
* Deletes a comment. Deletes a comment. If there are no remaining comments in the thread, the thread is also deleted.
* @param params.roomId The room ID to delete the comment in.
* @param params.threadId The thread ID to delete the comment in.
* @param params.commentId The comment ID to delete.
* @param options.signal (optional) An abort signal to cancel the request.
*/
async deleteComment(params, options) {
const { roomId, threadId, commentId } = params;
const res = await this.#delete(
_core.url`/v2/rooms/${roomId}/threads/${threadId}/comments/${commentId}`,
void 0,
options
);
if (!res.ok) {
throw await LiveblocksError.from(res);
}
}
/**
* Creates a new thread. The thread will be created with the specified comment as its first comment.
* If the thread already exists, a `LiveblocksError` will be thrown with status code 409.
* @param params.roomId The room ID to create the thread in.
* @param params.thread.metadata (optional) The metadata for the thread. Supports upto a maximum of 10 entries. Value must be a string, boolean or number
* @param params.thread.comment.userId The user ID of the user who created the comment.
* @param params.thread.comment.createdAt (optional) The date the comment was created.
* @param params.thread.comment.body The body of the comment.
* @param options.signal (optional) An abort signal to cancel the request.
* @returns The created thread. The thread will be created with the specified comment as its first comment.
*/
async createThread(params, options) {
const { roomId, data } = params;
const res = await this.#post(
_core.url`/v2/rooms/${roomId}/threads`,
{
...data,
comment: {
...data.comment,
createdAt: _optionalChain([data, 'access', _16 => _16.comment, 'access', _17 => _17.createdAt, 'optionalAccess', _18 => _18.toISOString, 'call', _19 => _19()])
}
},
options
);
if (!res.ok) {
throw await LiveblocksError.from(res);
}
return _core.convertToThreadData.call(void 0, await res.json());
}
/**
* Deletes a thread and all of its comments.
* @param params.roomId The room ID to delete the thread in.
* @param params.threadId The thread ID to delete.
* @param options.signal (optional) An abort signal to cancel the request.
*/
async deleteThread(params, options) {
const { roomId, threadId } = params;
const res = await this.#delete(
_core.url`/v2/rooms/${roomId}/threads/${threadId}`,
void 0,
options
);
if (!res.ok) {
throw await LiveblocksError.from(res);
}
}
/**
* Mark a thread as resolved.
* @param params.roomId The room ID of the thread.
* @param params.threadId The thread ID to mark as resolved.
* @param params.data.userId The user ID of the user who marked the thread as resolved.
* @param options.signal (optional) An abort signal to cancel the request.
* @returns The thread marked as resolved.
*/
async markThreadAsResolved(params, options) {
const { roomId, threadId } = params;
const res = await this.#post(
_core.url`/v2/rooms/${roomId}/threads/${threadId}/mark-as-resolved`,
{ userId: params.data.userId },
options
);
if (!res.ok) {
throw await LiveblocksError.from(res);
}
return _core.convertToThreadData.call(void 0, await res.json());
}
/**
* Mark a thread as unresolved.
* @param params.roomId The room ID of the thread.
* @param params.threadId The thread ID to mark as unresolved.
* @param params.data.userId The user ID of the user who marked the thread as unresolved.
* @param options.signal (optional) An abort signal to cancel the request.
* @returns The thread marked as unresolved.
*/
async markThreadAsUnresolved(params, options) {
const { roomId, threadId } = params;
const res = await this.#post(
_core.url`/v2/rooms/${roomId}/threads/${threadId}/mark-as-unresolved`,
{ userId: params.data.userId },
options
);
if (!res.ok) {
throw await LiveblocksError.from(res);
}
return _core.convertToThreadData.call(void 0, await res.json());
}
/**
* Subscribes a user to a thread.
* @param params.roomId The room ID of the thread.
* @param params.threadId The thread ID to subscribe to.
* @param params.data.userId The user ID of the user to subscribe to the thread.
* @param options.signal (optional) An abort signal to cancel the request.
* @returns The thread subscription.
*/
async subscribeToThread(params, options) {
const { roomId, threadId } = params;
const res = await this.#post(
_core.url`/v2/rooms/${roomId}/threads/${threadId}/subscribe`,
{ userId: params.data.userId },
options
);
if (!res.ok) {
throw await LiveblocksError.from(res);
}
return _core.convertToSubscriptionData.call(void 0,
await res.json()
);
}
/**
* Unsubscribes a user from a thread.
* @param params.roomId The room ID of the thread.
* @param params.threadId The thread ID to unsubscribe from.
* @param params.data.userId The user ID of the user to unsubscribe from the thread.
* @param options.signal (optional) An abort signal to cancel the request.
*/
async unsubscribeFromThread(params, options) {
const { roomId, threadId } = params;
const res = await this.#post(
_core.url`/v2/rooms/${roomId}/threads/${threadId}/unsubscribe`,
{ userId: params.data.userId },
options
);
if (!res.ok) {
throw await LiveblocksError.from(res);
}
}
/**
* Updates the metadata of the specified thread in a room.
* @param params.roomId The room ID to update the thread in.
* @param params.threadId The thread ID to update.
* @param params.data.metadata The metadata for the thread. Value must be a string, boolean or number
* @param params.data.userId The user ID of the user who updated the thread.
* @param params.data.updatedAt (optional) The date the thread is set to be updated.
* @param options.signal (optional) An abort signal to cancel the request.
* @returns The updated thread metadata.
*/
async editThreadMetadata(params, options) {
const { roomId, threadId, data } = params;
const res = await this.#post(
_core.url`/v2/rooms/${roomId}/threads/${threadId}/metadata`,
{
...data,
updatedAt: _optionalChain([data, 'access', _20 => _20.updatedAt, 'optionalAccess', _21 => _21.toISOString, 'call', _22 => _22()])
},
options
);
if (!res.ok) {
throw await LiveblocksError.from(res);
}
return await res.json();
}
/**
* Adds a new comment reaction to a comment.
* @param params.roomId The room ID to add the comment reaction in.
* @param params.threadId The thread ID to add the comment reaction in.
* @param params.commentId The comment ID to add the reaction in.
* @param params.data.emoji The (emoji) reaction to add.
* @param params.data.userId The user ID of the user associated with the reaction.
* @param params.data.createdAt (optional) The date the reaction is set to be created.
* @param options.signal (optional) An abort signal to cancel the request.
* @returns The created comment reaction.
*/
async addCommentReaction(params, options) {
const { roomId, threadId, commentId, data } = params;
const res = await this.#post(
_core.url`/v2/rooms/${roomId}/threads/${threadId}/comments/${commentId}/add-reaction`,
{
...data,
createdAt: _optionalChain([data, 'access', _23 => _23.createdAt, 'optionalAccess', _24 => _24.toISOString, 'call', _25 => _25()])
},
options
);
if (!res.ok) {
throw await LiveblocksError.from(res);
}
const reaction = await res.json();
return _core.convertToCommentUserReaction.call(void 0, reaction);
}
/**
* Removes a reaction from a comment.
* @param params.roomId The room ID to remove the comment reaction from.
* @param params.threadId The thread ID to remove the comment reaction from.
* @param params.commentId The comment ID to remove the reaction from.
* @param params.data.emoji The (emoji) reaction to remove.
* @param params.data.userId The user ID of the user associated with the reaction.
* @param params.data.removedAt (optional) The date the reaction is set to be removed.
* @param options.signal (optional) An abort signal to cancel the request.
*/
async removeCommentReaction(params, options) {
const { roomId, threadId, data } = params;
const res = await this.#post(
_core.url`/v2/rooms/${roomId}/threads/${threadId}/comments/${params.commentId}/remove-reaction`,
{
...data,
removedAt: _optionalChain([data, 'access', _26 => _26.removedAt, 'optionalAccess', _27 => _27.toISOString, 'call', _28 => _28()])
},
options
);
if (!res.ok) {
throw await LiveblocksError.from(res);
}
}
/**
* Returns the inbox notifications for a user.
* @param params.userId The user ID to get the inbox notifications from.
* @param params.inboxNotificationId The ID of the inbox notification to get.
* @param options.signal (optional) An abort signal to cancel the request.
*/
async getInboxNotification(params, options) {
const { userId, inboxNotificationId } = params;
const res = await this.#get(
_core.url`/v2/users/${userId}/inbox-notifications/${inboxNotificationId}`,
void 0,
options
);
if (!res.ok) {
throw await LiveblocksError.from(res);
}
return _core.convertToInboxNotificationData.call(void 0,
await res.json()
);
}
/**
* Returns the inbox notifications for a user.
* @param params.userId The user ID to get the inbox notifications from.
* @param params.query The query to filter inbox notifications by. It is based on our query language and can filter by unread.
* @param params.tenantId (optional) The tenant ID to get the inbox notifications for.
* @param options.signal (optional) An abort signal to cancel the request.
*/
async getInboxNotifications(params, options) {
const { userId, tenantId, limit, startingAfter } = params;
let query;
if (typeof params.query === "string") {
query = params.query;
} else if (typeof params.query === "object") {
query = _core.objectToQuery.call(void 0, params.query);
}
const res = await this.#get(
_core.url`/v2/users/${userId}/inbox-notifications`,
{
query,
limit,
startingAfter,
tenantId
},
options
);
if (!res.ok) {
throw await LiveblocksError.from(res);
}
const page = await res.json();
return {
...page,
data: page.data.map(_core.convertToInboxNotificationData)
};
}
/**
* Iterates over all inbox notifications for a user.
*
* The difference with .getInboxNotifications() is that pagination will
* happen automatically under the hood, using the given `pageSize`.
*
* @param criteria.userId The user ID to get the inbox notifications from.
* @param criteria.query The query to filter inbox notifications by. It is based on our query language and can filter by unread.
* @param criteria.tenantId (optional) The tenant ID to get the inbox notifications for.
* @param options.pageSize (optional) The page size to use for each request.
* @param options.signal (optional) An abort signal to cancel the request.
*/
async *iterInboxNotifications(criteria, options) {
const { signal } = _nullishCoalesce(options, () => ( {}));
const pageSize = _core.checkBounds.call(void 0, "pageSize", _nullishCoalesce(_optionalChain([options, 'op