@liveblocks/node 2.21.0 → 2.22.1

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.cjs CHANGED
@@ -3,7 +3,7 @@ var _core = require('@liveblocks/core');
3
3
 
4
4
  // src/version.ts
5
5
  var PKG_NAME = "@liveblocks/node";
6
- var PKG_VERSION = "2.21.0";
6
+ var PKG_VERSION = "2.22.1";
7
7
  var PKG_FORMAT = "cjs";
8
8
 
9
9
  // src/client.ts
@@ -19,6 +19,75 @@ var PKG_FORMAT = "cjs";
19
19
 
20
20
 
21
21
 
22
+
23
+
24
+
25
+
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
 
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 } = _nullishCoalesce(options, () => ( {}));
510
+ const pageSize = _core.checkBounds.call(void 0, "pageSize", _nullishCoalesce(_optionalChain([options, 'optionalAccess', _8 => _8.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
+ _core.url`/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.
@@ -911,7 +1078,7 @@ var Liveblocks = class {
911
1078
  _core.url`/v2/rooms/${roomId}/threads/${threadId}/comments`,
912
1079
  {
913
1080
  ...data,
914
- createdAt: _optionalChain([data, 'access', _8 => _8.createdAt, 'optionalAccess', _9 => _9.toISOString, 'call', _10 => _10()])
1081
+ createdAt: _optionalChain([data, 'access', _9 => _9.createdAt, 'optionalAccess', _10 => _10.toISOString, 'call', _11 => _11()])
915
1082
  },
916
1083
  options
917
1084
  );
@@ -934,7 +1101,7 @@ var Liveblocks = class {
934
1101
  const { roomId, threadId, commentId, data } = params;
935
1102
  const res = await this.#post(
936
1103
  _core.url`/v2/rooms/${roomId}/threads/${threadId}/comments/${commentId}`,
937
- { ...data, editedAt: _optionalChain([data, 'access', _11 => _11.editedAt, 'optionalAccess', _12 => _12.toISOString, 'call', _13 => _13()]) },
1104
+ { ...data, editedAt: _optionalChain([data, 'access', _12 => _12.editedAt, 'optionalAccess', _13 => _13.toISOString, 'call', _14 => _14()]) },
938
1105
  options
939
1106
  );
940
1107
  if (!res.ok) {
@@ -978,7 +1145,7 @@ var Liveblocks = class {
978
1145
  ...data,
979
1146
  comment: {
980
1147
  ...data.comment,
981
- createdAt: _optionalChain([data, 'access', _14 => _14.comment, 'access', _15 => _15.createdAt, 'optionalAccess', _16 => _16.toISOString, 'call', _17 => _17()])
1148
+ createdAt: _optionalChain([data, 'access', _15 => _15.comment, 'access', _16 => _16.createdAt, 'optionalAccess', _17 => _17.toISOString, 'call', _18 => _18()])
982
1149
  }
983
1150
  },
984
1151
  options
@@ -1060,7 +1227,7 @@ var Liveblocks = class {
1060
1227
  _core.url`/v2/rooms/${roomId}/threads/${threadId}/metadata`,
1061
1228
  {
1062
1229
  ...data,
1063
- updatedAt: _optionalChain([data, 'access', _18 => _18.updatedAt, 'optionalAccess', _19 => _19.toISOString, 'call', _20 => _20()])
1230
+ updatedAt: _optionalChain([data, 'access', _19 => _19.updatedAt, 'optionalAccess', _20 => _20.toISOString, 'call', _21 => _21()])
1064
1231
  },
1065
1232
  options
1066
1233
  );
@@ -1086,7 +1253,7 @@ var Liveblocks = class {
1086
1253
  _core.url`/v2/rooms/${roomId}/threads/${threadId}/comments/${commentId}/add-reaction`,
1087
1254
  {
1088
1255
  ...data,
1089
- createdAt: _optionalChain([data, 'access', _21 => _21.createdAt, 'optionalAccess', _22 => _22.toISOString, 'call', _23 => _23()])
1256
+ createdAt: _optionalChain([data, 'access', _22 => _22.createdAt, 'optionalAccess', _23 => _23.toISOString, 'call', _24 => _24()])
1090
1257
  },
1091
1258
  options
1092
1259
  );
@@ -1112,7 +1279,7 @@ var Liveblocks = class {
1112
1279
  _core.url`/v2/rooms/${roomId}/threads/${threadId}/comments/${params.commentId}/remove-reaction`,
1113
1280
  {
1114
1281
  ...data,
1115
- removedAt: _optionalChain([data, 'access', _24 => _24.removedAt, 'optionalAccess', _25 => _25.toISOString, 'call', _26 => _26()])
1282
+ removedAt: _optionalChain([data, 'access', _25 => _25.removedAt, 'optionalAccess', _26 => _26.toISOString, 'call', _27 => _27()])
1116
1283
  },
1117
1284
  options
1118
1285
  );
@@ -1156,17 +1323,51 @@ var Liveblocks = class {
1156
1323
  }
1157
1324
  const res = await this.#get(
1158
1325
  _core.url`/v2/users/${userId}/inbox-notifications`,
1159
- { query },
1326
+ {
1327
+ query,
1328
+ limit: _optionalChain([params, 'optionalAccess', _28 => _28.limit]),
1329
+ startingAfter: _optionalChain([params, 'optionalAccess', _29 => _29.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(_core.convertToInboxNotificationData)
1338
+ ...page,
1339
+ data: page.data.map(_core.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 } = _nullishCoalesce(options, () => ( {}));
1355
+ const pageSize = _core.checkBounds.call(void 0, "pageSize", _nullishCoalesce(_optionalChain([options, 'optionalAccess', _30 => _30.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 = _core.checkBounds.call(void 0,
1568
+ "concurrency",
1569
+ _nullishCoalesce(_optionalChain([massOptions, 'optionalAccess', _31 => _31.concurrency]), () => ( 8)),
1570
+ 1,
1571
+ 20
1572
+ );
1573
+ const pageSize = Math.max(20, concurrency * 4);
1574
+ const { signal } = _nullishCoalesce(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 } = _core.makeAbortController.call(void 0, _optionalChain([options, 'optionalAccess', _32 => _32.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: _core.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 = _core.createManagedPool.call(void 0, 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 = _core.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
+ _core.url`/v2/rooms/${roomId}/send-message`,
1650
+ { messages },
1651
+ { signal: _optionalChain([options, 'optionalAccess', _33 => _33.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
 
@@ -1357,11 +1673,13 @@ ${this.details}`;
1357
1673
  return msg;
1358
1674
  }
1359
1675
  static async from(res) {
1676
+ const origErrLocation = new Error();
1677
+ Error.captureStackTrace(origErrLocation, _LiveblocksError.from);
1360
1678
  const FALLBACK = "An error happened without an error message";
1361
1679
  let text;
1362
1680
  try {
1363
1681
  text = await res.text();
1364
- } catch (e) {
1682
+ } catch (e2) {
1365
1683
  text = FALLBACK;
1366
1684
  }
1367
1685
  const obj = _nullishCoalesce(_core.tryParseJson.call(void 0, text), () => ( { message: text }));
@@ -1370,7 +1688,9 @@ ${this.details}`;
1370
1688
  obj.suggestion ? `Suggestion: ${String(obj.suggestion)}` : void 0,
1371
1689
  obj.docs ? `See also: ${String(obj.docs)}` : void 0
1372
1690
  ].filter(Boolean).join("\n") || void 0;
1373
- return new _LiveblocksError(message, res.status, details);
1691
+ const err = new _LiveblocksError(message, res.status, details);
1692
+ err.stack = origErrLocation.stack;
1693
+ return err;
1374
1694
  }
1375
1695
  };
1376
1696