@liveblocks/node 2.21.0-emails3 → 2.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3,22 +3,91 @@ import { detectDupes } from "@liveblocks/core";
3
3
 
4
4
  // src/version.ts
5
5
  var PKG_NAME = "@liveblocks/node";
6
- var PKG_VERSION = "2.21.0-emails3";
6
+ var PKG_VERSION = "2.22.0";
7
7
  var PKG_FORMAT = "esm";
8
8
 
9
9
  // src/client.ts
10
10
  import {
11
+ checkBounds,
12
+ ClientMsgCode,
11
13
  convertToCommentData,
12
14
  convertToCommentUserReaction,
13
15
  convertToInboxNotificationData,
14
16
  convertToThreadData,
17
+ createManagedPool,
15
18
  createUserNotificationSettings,
19
+ LiveObject,
20
+ makeAbortController,
16
21
  objectToQuery,
17
22
  tryParseJson,
18
23
  url as url2,
19
24
  urljoin
20
25
  } from "@liveblocks/core";
21
26
 
27
+ // src/lib/itertools.ts
28
+ async function asyncConsume(iterable) {
29
+ const result = [];
30
+ for await (const item of iterable) {
31
+ result.push(item);
32
+ }
33
+ return result;
34
+ }
35
+ async function runConcurrently(iterable, fn, concurrency) {
36
+ const queue = /* @__PURE__ */ new Set();
37
+ for await (const item of iterable) {
38
+ if (queue.size >= concurrency) {
39
+ await Promise.race(queue);
40
+ }
41
+ const promise = (async () => {
42
+ try {
43
+ await fn(item);
44
+ } finally {
45
+ queue.delete(promise);
46
+ }
47
+ })();
48
+ queue.add(promise);
49
+ }
50
+ if (queue.size > 0) {
51
+ await Promise.all(queue);
52
+ }
53
+ }
54
+
55
+ // src/lib/ndjson.ts
56
+ var LineStream = class extends TransformStream {
57
+ constructor() {
58
+ let buffer = "";
59
+ super({
60
+ transform(chunk, controller) {
61
+ buffer += chunk;
62
+ if (buffer.includes("\n")) {
63
+ const lines = buffer.split("\n");
64
+ for (let i = 0; i < lines.length - 1; i++) {
65
+ if (lines[i].length > 0) {
66
+ controller.enqueue(lines[i]);
67
+ }
68
+ }
69
+ buffer = lines[lines.length - 1];
70
+ }
71
+ },
72
+ flush(controller) {
73
+ if (buffer.length > 0) {
74
+ controller.enqueue(buffer);
75
+ }
76
+ }
77
+ });
78
+ }
79
+ };
80
+ var NdJsonStream = class extends TransformStream {
81
+ constructor() {
82
+ super({
83
+ transform(line, controller) {
84
+ const json = JSON.parse(line);
85
+ controller.enqueue(json);
86
+ }
87
+ });
88
+ }
89
+ };
90
+
22
91
  // src/Session.ts
23
92
  import { url } from "@liveblocks/core";
24
93
 
@@ -407,8 +476,8 @@ var Liveblocks = class {
407
476
  if (!res.ok) {
408
477
  throw await LiveblocksError.from(res);
409
478
  }
410
- const data = await res.json();
411
- const rooms = data.data.map((room) => {
479
+ const page = await res.json();
480
+ const rooms = page.data.map((room) => {
412
481
  const lastConnectionAt = room.lastConnectionAt ? new Date(room.lastConnectionAt) : void 0;
413
482
  const createdAt = new Date(room.createdAt);
414
483
  return {
@@ -418,10 +487,42 @@ var Liveblocks = class {
418
487
  };
419
488
  });
420
489
  return {
421
- ...data,
490
+ ...page,
422
491
  data: rooms
423
492
  };
424
493
  }
494
+ /**
495
+ * Iterates over all rooms that match the given criteria.
496
+ *
497
+ * The difference with .getRooms() is that pagination will happen
498
+ * automatically under the hood, using the given `pageSize`.
499
+ *
500
+ * @param criteria.userId (optional) A filter on users accesses.
501
+ * @param criteria.groupIds (optional) A filter on groups accesses. Multiple groups can be used.
502
+ * @param criteria.query.roomId (optional) A filter by room ID.
503
+ * @param criteria.query.metadata (optional) A filter by metadata.
504
+ *
505
+ * @param options.pageSize (optional) The page size to use for each request.
506
+ * @param options.signal (optional) An abort signal to cancel the request.
507
+ */
508
+ async *iterRooms(criteria, options) {
509
+ const { signal } = options ?? {};
510
+ const pageSize = checkBounds("pageSize", options?.pageSize ?? 40, 20);
511
+ let cursor = void 0;
512
+ while (true) {
513
+ const { nextCursor, data } = await this.getRooms(
514
+ { ...criteria, startingAfter: cursor, limit: pageSize },
515
+ { signal }
516
+ );
517
+ for (const item of data) {
518
+ yield item;
519
+ }
520
+ if (!nextCursor) {
521
+ break;
522
+ }
523
+ cursor = nextCursor;
524
+ }
525
+ }
425
526
  /**
426
527
  * Creates a new room with the given id.
427
528
  * @param roomId The id of the room to create.
@@ -457,6 +558,51 @@ var Liveblocks = class {
457
558
  createdAt
458
559
  };
459
560
  }
561
+ /**
562
+ * Returns a room with the given id, or creates one with the given creation
563
+ * options if it doesn't exist yet.
564
+ *
565
+ * @param roomId The id of the room.
566
+ * @param params.defaultAccesses The default accesses for the room if the room will be created.
567
+ * @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.
568
+ * @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.
569
+ * @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.
570
+ * @param options.signal (optional) An abort signal to cancel the request.
571
+ * @returns The room.
572
+ */
573
+ async getOrCreateRoom(roomId, params, options) {
574
+ try {
575
+ return await this.createRoom(roomId, params, options);
576
+ } catch (err) {
577
+ if (err instanceof LiveblocksError && err.status === 409) {
578
+ return await this.getRoom(roomId, options);
579
+ } else {
580
+ throw err;
581
+ }
582
+ }
583
+ }
584
+ /**
585
+ * Updates or creates a new room with the given properties.
586
+ *
587
+ * @param roomId The id of the room to update or create.
588
+ * @param params.defaultAccesses The default accesses for the room.
589
+ * @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.
590
+ * @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.
591
+ * @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.
592
+ * @param options.signal (optional) An abort signal to cancel the request.
593
+ * @returns The room.
594
+ */
595
+ async upsertRoom(roomId, params, options) {
596
+ try {
597
+ return await this.createRoom(roomId, params, options);
598
+ } catch (err) {
599
+ if (err instanceof LiveblocksError && err.status === 409) {
600
+ return await this.updateRoom(roomId, params, options);
601
+ } else {
602
+ throw err;
603
+ }
604
+ }
605
+ }
460
606
  /**
461
607
  * Returns a room with the given id.
462
608
  * @param roomId The id of the room to return.
@@ -567,6 +713,27 @@ var Liveblocks = class {
567
713
  }
568
714
  return await res.json();
569
715
  }
716
+ async #requestStorageMutation(roomId, options) {
717
+ const resp = await this.#post(
718
+ url2`/v2/rooms/${roomId}/request-storage-mutation`,
719
+ {},
720
+ options
721
+ );
722
+ if (!resp.ok) {
723
+ throw await LiveblocksError.from(resp);
724
+ }
725
+ if (resp.headers.get("content-type") !== "application/x-ndjson") {
726
+ throw new Error("Unexpected response content type");
727
+ }
728
+ if (resp.body === null) {
729
+ throw new Error("Unexpected null body in response");
730
+ }
731
+ const stream = resp.body.pipeThrough(new TextDecoderStream()).pipeThrough(new LineStream()).pipeThrough(new NdJsonStream());
732
+ const iter = stream[Symbol.asyncIterator]();
733
+ const { actor } = await iter.next();
734
+ const nodes = await asyncConsume(iter);
735
+ return { actor, nodes };
736
+ }
570
737
  /**
571
738
  * Initializes a room’s Storage. The room must already exist and have an empty Storage.
572
739
  * Calling this endpoint will disconnect all users from the room if there are any.
@@ -1016,7 +1183,7 @@ var Liveblocks = class {
1016
1183
  const { roomId, threadId } = params;
1017
1184
  const res = await this.#post(
1018
1185
  url2`/v2/rooms/${roomId}/threads/${threadId}/mark-as-resolved`,
1019
- {},
1186
+ { userId: params.data.userId },
1020
1187
  options
1021
1188
  );
1022
1189
  if (!res.ok) {
@@ -1036,7 +1203,7 @@ var Liveblocks = class {
1036
1203
  const { roomId, threadId } = params;
1037
1204
  const res = await this.#post(
1038
1205
  url2`/v2/rooms/${roomId}/threads/${threadId}/mark-as-unresolved`,
1039
- {},
1206
+ { userId: params.data.userId },
1040
1207
  options
1041
1208
  );
1042
1209
  if (!res.ok) {
@@ -1156,17 +1323,51 @@ var Liveblocks = class {
1156
1323
  }
1157
1324
  const res = await this.#get(
1158
1325
  url2`/v2/users/${userId}/inbox-notifications`,
1159
- { query },
1326
+ {
1327
+ query,
1328
+ limit: params?.limit,
1329
+ startingAfter: params?.startingAfter
1330
+ },
1160
1331
  options
1161
1332
  );
1162
1333
  if (!res.ok) {
1163
1334
  throw await LiveblocksError.from(res);
1164
1335
  }
1165
- const { data } = await res.json();
1336
+ const page = await res.json();
1166
1337
  return {
1167
- data: data.map(convertToInboxNotificationData)
1338
+ ...page,
1339
+ data: page.data.map(convertToInboxNotificationData)
1168
1340
  };
1169
1341
  }
1342
+ /**
1343
+ * Iterates over all inbox notifications for a user.
1344
+ *
1345
+ * The difference with .getInboxNotifications() is that pagination will
1346
+ * happen automatically under the hood, using the given `pageSize`.
1347
+ *
1348
+ * @param criteria.userId The user ID to get the inbox notifications from.
1349
+ * @param criteria.query The query to filter inbox notifications by. It is based on our query language and can filter by unread.
1350
+ * @param options.pageSize (optional) The page size to use for each request.
1351
+ * @param options.signal (optional) An abort signal to cancel the request.
1352
+ */
1353
+ async *iterInboxNotifications(criteria, options) {
1354
+ const { signal } = options ?? {};
1355
+ const pageSize = checkBounds("pageSize", options?.pageSize ?? 50, 10);
1356
+ let cursor = void 0;
1357
+ while (true) {
1358
+ const { nextCursor, data } = await this.getInboxNotifications(
1359
+ { ...criteria, startingAfter: cursor, limit: pageSize },
1360
+ { signal }
1361
+ );
1362
+ for (const item of data) {
1363
+ yield item;
1364
+ }
1365
+ if (!nextCursor) {
1366
+ break;
1367
+ }
1368
+ cursor = nextCursor;
1369
+ }
1370
+ }
1170
1371
  /**
1171
1372
  * Gets the user's room notification settings.
1172
1373
  * @param params.userId The user ID to get the room notifications from.
@@ -1338,6 +1539,121 @@ var Liveblocks = class {
1338
1539
  throw await LiveblocksError.from(res);
1339
1540
  }
1340
1541
  }
1542
+ /**
1543
+ * Retrieves the current Storage contents for the given room ID and calls the
1544
+ * provided callback function, in which you can mutate the Storage contents
1545
+ * at will.
1546
+ *
1547
+ * If you need to run the same mutation across multiple rooms, prefer using
1548
+ * `.massMutateStorage()` instead of looping over room IDs yourself.
1549
+ */
1550
+ async mutateStorage(roomId, callback, options) {
1551
+ return this.#_mutateOneRoom(roomId, void 0, callback, options);
1552
+ }
1553
+ /**
1554
+ * Retrieves the Storage contents for each room that matches the given
1555
+ * criteria and calls the provided callback function, in which you can mutate
1556
+ * the Storage contents at will.
1557
+ *
1558
+ * You can use the `criteria` parameter to select which rooms to process by
1559
+ * their metadata. If you pass `{}` (empty object), all rooms will be
1560
+ * selected and processed.
1561
+ *
1562
+ * This method will execute mutations in parallel, using the specified
1563
+ * `concurrency` value. If you which to run the mutations serially, set
1564
+ * `concurrency` to 1.
1565
+ */
1566
+ async massMutateStorage(criteria, callback, massOptions) {
1567
+ const concurrency = checkBounds(
1568
+ "concurrency",
1569
+ massOptions?.concurrency ?? 8,
1570
+ 1,
1571
+ 20
1572
+ );
1573
+ const pageSize = Math.max(20, concurrency * 4);
1574
+ const { signal } = massOptions ?? {};
1575
+ const rooms = this.iterRooms(criteria, { pageSize, signal });
1576
+ const options = { signal };
1577
+ await runConcurrently(
1578
+ rooms,
1579
+ (roomData) => this.#_mutateOneRoom(roomData.id, roomData, callback, options),
1580
+ concurrency
1581
+ );
1582
+ }
1583
+ async #_mutateOneRoom(roomId, room, callback, options) {
1584
+ const debounceInterval = 200;
1585
+ const { signal, abort } = makeAbortController(options?.signal);
1586
+ let opsBuffer = [];
1587
+ let outstandingFlush$ = void 0;
1588
+ let lastFlush = performance.now();
1589
+ const flushIfNeeded = (force) => {
1590
+ if (opsBuffer.length === 0)
1591
+ return;
1592
+ if (outstandingFlush$) {
1593
+ return;
1594
+ }
1595
+ const now = performance.now();
1596
+ if (!(force || now - lastFlush > debounceInterval)) {
1597
+ return;
1598
+ }
1599
+ lastFlush = now;
1600
+ const ops = opsBuffer;
1601
+ opsBuffer = [];
1602
+ outstandingFlush$ = this.#sendMessage(
1603
+ roomId,
1604
+ [{ type: ClientMsgCode.UPDATE_STORAGE, ops }],
1605
+ { signal }
1606
+ ).catch((err) => {
1607
+ abort(err);
1608
+ }).finally(() => {
1609
+ outstandingFlush$ = void 0;
1610
+ });
1611
+ };
1612
+ try {
1613
+ const resp = await this.#requestStorageMutation(roomId, { signal });
1614
+ const { actor, nodes } = resp;
1615
+ const pool = createManagedPool(roomId, {
1616
+ getCurrentConnectionId: () => actor,
1617
+ onDispatch: (ops, _reverse, _storageUpdates) => {
1618
+ if (ops.length === 0) return;
1619
+ for (const op of ops) {
1620
+ opsBuffer.push(op);
1621
+ }
1622
+ flushIfNeeded(
1623
+ /* force */
1624
+ false
1625
+ );
1626
+ }
1627
+ });
1628
+ const root = LiveObject._fromItems(nodes, pool);
1629
+ const callback$ = callback({ room, root });
1630
+ flushIfNeeded(
1631
+ /* force */
1632
+ true
1633
+ );
1634
+ await callback$;
1635
+ } catch (e) {
1636
+ abort();
1637
+ throw e;
1638
+ } finally {
1639
+ await outstandingFlush$;
1640
+ flushIfNeeded(
1641
+ /* force */
1642
+ true
1643
+ );
1644
+ await outstandingFlush$;
1645
+ }
1646
+ }
1647
+ async #sendMessage(roomId, messages, options) {
1648
+ const res = await this.#post(
1649
+ url2`/v2/rooms/${roomId}/send-message`,
1650
+ { messages },
1651
+ { signal: options?.signal }
1652
+ );
1653
+ if (!res.ok) {
1654
+ throw await LiveblocksError.from(res);
1655
+ }
1656
+ }
1341
1657
  };
1342
1658
  var LiveblocksError = class _LiveblocksError extends Error {
1343
1659
  status;