@liveblocks/react 1.3.3 → 1.3.5

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.mjs CHANGED
@@ -5,7 +5,7 @@ import { detectDupes } from "@liveblocks/core";
5
5
 
6
6
  // src/version.ts
7
7
  var PKG_NAME = "@liveblocks/react";
8
- var PKG_VERSION = "1.3.3";
8
+ var PKG_VERSION = "1.3.5";
9
9
  var PKG_FORMAT = "esm";
10
10
 
11
11
  // src/ClientSideSuspense.tsx
@@ -24,13 +24,16 @@ import {
24
24
  createAsyncCache,
25
25
  deprecateIf,
26
26
  errorIf,
27
+ isLiveNode,
27
28
  makeEventSource as makeEventSource2
28
29
  } from "@liveblocks/core";
29
30
  import * as React2 from "react";
30
31
  import { useSyncExternalStoreWithSelector } from "use-sync-external-store/shim/with-selector.js";
31
32
 
32
33
  // src/comments/CommentsRoom.ts
33
- import { makePoller } from "@liveblocks/core";
34
+ import {
35
+ makeEventSource
36
+ } from "@liveblocks/core";
34
37
  import { nanoid } from "nanoid";
35
38
  import { useEffect as useEffect2 } from "react";
36
39
  import { useSyncExternalStore } from "use-sync-external-store/shim/index.js";
@@ -77,158 +80,166 @@ var DeleteCommentError = class extends Error {
77
80
  }
78
81
  };
79
82
 
80
- // src/comments/lib/store.ts
81
- import { makeEventSource } from "@liveblocks/core";
82
- function createStore(initialState) {
83
- let state = initialState;
83
+ // src/comments/CommentsRoom.ts
84
+ var POLLING_INTERVAL_REALTIME = 3e4;
85
+ var POLLING_INTERVAL = 5e3;
86
+ var MAX_ERROR_RETRY_COUNT = 5;
87
+ var ERROR_RETRY_INTERVAL = 5e3;
88
+ var THREAD_ID_PREFIX = "th";
89
+ var COMMENT_ID_PREFIX = "cm";
90
+ var DEDUPING_INTERVAL = 1e3;
91
+ function createOptimisticId(prefix) {
92
+ return `${prefix}_${nanoid()}`;
93
+ }
94
+ function createThreadsManager() {
95
+ let cache;
96
+ let request;
97
+ let mutation;
84
98
  const eventSource = makeEventSource();
85
99
  return {
86
- get() {
87
- return state;
100
+ get cache() {
101
+ return cache;
88
102
  },
89
- set(newState) {
90
- state = newState;
91
- eventSource.notify(state);
103
+ set cache(value) {
104
+ cache = value;
105
+ eventSource.notify(cache);
92
106
  },
93
- subscribe(callback) {
94
- return eventSource.subscribe(callback);
107
+ get request() {
108
+ return request;
109
+ },
110
+ set request(value) {
111
+ request = value;
95
112
  },
96
- subscribeOnce(callback) {
97
- return eventSource.subscribeOnce(callback);
113
+ get mutation() {
114
+ return mutation;
98
115
  },
99
- subscribersCount() {
100
- return eventSource.count();
116
+ set mutation(value) {
117
+ mutation = value;
101
118
  },
102
- destroy() {
103
- return eventSource.clear();
119
+ subscribe(callback) {
120
+ return eventSource.subscribe(callback);
104
121
  }
105
122
  };
106
123
  }
107
-
108
- // src/comments/CommentsRoom.ts
109
- var POLLING_INTERVAL_REALTIME = 3e4;
110
- var POLLING_INTERVAL = 5e3;
111
- var THREAD_ID_PREFIX = "th";
112
- var COMMENT_ID_PREFIX = "cm";
113
- function createOptimisticId(prefix) {
114
- return `${prefix}_${nanoid()}`;
115
- }
116
124
  function createCommentsRoom(room, errorEventSource) {
117
- const store = createStore({
118
- isLoading: true
119
- });
120
- let fetchThreadsPromise = null;
121
- let numberOfMutations = 0;
122
- function endMutation() {
123
- numberOfMutations--;
124
- if (numberOfMutations === 0) {
125
- void revalidateThreads();
126
- }
127
- }
128
- function startMutation() {
129
- pollingHub.threads.stop();
130
- numberOfMutations++;
131
- }
132
- const pollingHub = {
133
- // TODO: If there's an error, it will currently infinitely retry at the current polling rate → add retry logic
134
- threads: makePoller(revalidateThreads)
135
- };
136
- let unsubscribeRealtimeEvents;
137
- let unsubscribeRealtimeConnection;
138
- let realtimeClientConnected = false;
139
- function getPollingInterval() {
140
- return realtimeClientConnected ? POLLING_INTERVAL_REALTIME : POLLING_INTERVAL;
141
- }
142
- function ensureThreadsAreLoadedForMutations() {
143
- const state = store.get();
144
- if (state.isLoading || state.error) {
145
- throw new Error(
146
- "Cannot update threads or comments before they are loaded"
147
- );
148
- }
149
- return state.threads;
150
- }
151
- async function revalidateThreads() {
152
- pollingHub.threads.pause();
153
- if (numberOfMutations === 0) {
154
- if (fetchThreadsPromise === null) {
155
- fetchThreadsPromise = room.getThreads();
125
+ const manager = createThreadsManager();
126
+ let timestamp = 0;
127
+ let commentsEventRefCount = 0;
128
+ let commentsEventDisposer;
129
+ async function mutate(data, options) {
130
+ const beforeMutationTimestamp = ++timestamp;
131
+ manager.mutation = {
132
+ startTime: beforeMutationTimestamp,
133
+ endTime: 0
134
+ };
135
+ const currentCache = manager.cache;
136
+ manager.cache = {
137
+ isLoading: false,
138
+ threads: options.optimisticData
139
+ };
140
+ try {
141
+ await data;
142
+ const activeMutation = manager.mutation;
143
+ if (activeMutation && beforeMutationTimestamp !== activeMutation.startTime) {
144
+ return;
156
145
  }
157
- setThreads(await fetchThreadsPromise);
158
- fetchThreadsPromise = null;
146
+ } catch (err) {
147
+ manager.cache = currentCache;
148
+ throw err;
159
149
  }
160
- pollingHub.threads.resume();
161
- }
162
- function subscribe() {
163
- if (!unsubscribeRealtimeEvents) {
164
- unsubscribeRealtimeEvents = room.events.comments.subscribe(() => {
165
- pollingHub.threads.restart(getPollingInterval());
166
- void revalidateThreads();
167
- });
150
+ manager.mutation = {
151
+ startTime: beforeMutationTimestamp,
152
+ endTime: ++timestamp
153
+ };
154
+ manager.request = void 0;
155
+ void revalidateCache(false);
156
+ }
157
+ async function revalidateCache(shouldDedupe, retryCount = 0) {
158
+ let startAt;
159
+ const shouldStartRequest = !manager.request || !shouldDedupe;
160
+ function deleteActiveRequest() {
161
+ const activeRequest = manager.request;
162
+ if (!activeRequest)
163
+ return;
164
+ if (activeRequest.timestamp !== startAt)
165
+ return;
166
+ manager.request = void 0;
168
167
  }
169
- if (!unsubscribeRealtimeConnection) {
170
- unsubscribeRealtimeConnection = room.events.status.subscribe((status) => {
171
- const nextRealtimeClientConnected = status === "connected";
172
- if (nextRealtimeClientConnected !== realtimeClientConnected) {
173
- realtimeClientConnected = nextRealtimeClientConnected;
174
- pollingHub.threads.restart(getPollingInterval());
175
- }
176
- });
168
+ function handleError() {
169
+ const timeout = ~~((Math.random() + 0.5) * (1 << (retryCount < 8 ? retryCount : 8))) * ERROR_RETRY_INTERVAL;
170
+ if (retryCount > MAX_ERROR_RETRY_COUNT)
171
+ return;
172
+ setTimeout(() => {
173
+ void revalidateCache(true, retryCount + 1);
174
+ }, timeout);
177
175
  }
178
- pollingHub.threads.start(getPollingInterval());
179
- return () => {
180
- if (store.subscribersCount() > 1) {
176
+ try {
177
+ if (shouldStartRequest) {
178
+ const currentCache = manager.cache;
179
+ if (!currentCache)
180
+ manager.cache = { isLoading: true };
181
+ manager.request = {
182
+ fetcher: room.getThreads(),
183
+ timestamp: ++timestamp
184
+ };
185
+ }
186
+ const activeRequest = manager.request;
187
+ if (!activeRequest)
181
188
  return;
189
+ const newData = await activeRequest.fetcher;
190
+ startAt = activeRequest.timestamp;
191
+ if (shouldStartRequest) {
192
+ setTimeout(deleteActiveRequest, DEDUPING_INTERVAL);
182
193
  }
183
- pollingHub.threads.stop();
184
- unsubscribeRealtimeEvents?.();
185
- unsubscribeRealtimeEvents = void 0;
186
- unsubscribeRealtimeConnection?.();
187
- unsubscribeRealtimeConnection = void 0;
188
- };
189
- }
190
- function setThreads(newThreads) {
191
- store.set({
192
- threads: newThreads,
193
- isLoading: false
194
- });
195
- }
196
- function useThreadsInternal() {
197
- useEffect2(subscribe, []);
198
- return useSyncExternalStore(
199
- store.subscribe,
200
- store.get,
201
- store.get
202
- );
203
- }
204
- function useThreads() {
205
- useEffect2(() => {
206
- void revalidateThreads();
207
- }, []);
208
- return useThreadsInternal();
209
- }
210
- function useThreadsSuspense() {
211
- const result = useThreadsInternal();
212
- if (result.isLoading) {
213
- throw revalidateThreads();
214
- }
215
- if (result.error) {
216
- throw result.error;
194
+ if (!manager.request || manager.request.timestamp !== startAt)
195
+ return;
196
+ const activeMutation = manager.mutation;
197
+ if (activeMutation && (activeMutation.startTime > startAt || activeMutation.endTime > startAt || activeMutation.endTime === 0)) {
198
+ return;
199
+ }
200
+ manager.cache = {
201
+ isLoading: false,
202
+ threads: newData
203
+ };
204
+ } catch (err) {
205
+ if (shouldStartRequest)
206
+ handleError();
207
+ deleteActiveRequest();
208
+ manager.cache = {
209
+ isLoading: false,
210
+ error: err
211
+ };
217
212
  }
218
- return result.threads;
219
213
  }
220
- function getCurrentUserId() {
221
- const self = room.getSelf();
222
- if (self === null || self.id === void 0) {
223
- return "anonymous";
224
- } else {
225
- return self.id;
226
- }
214
+ function editThreadMetadata(options) {
215
+ const threadId = options.threadId;
216
+ const metadata = "metadata" in options ? options.metadata : {};
217
+ const threads = getThreads();
218
+ const optimisticData = threads.map(
219
+ (thread) => thread.id === threadId ? {
220
+ ...thread,
221
+ metadata: {
222
+ ...thread.metadata,
223
+ ...metadata
224
+ }
225
+ } : thread
226
+ );
227
+ mutate(room.editThreadMetadata({ metadata, threadId }), {
228
+ optimisticData
229
+ }).catch((err) => {
230
+ errorEventSource.notify(
231
+ new EditThreadMetadataError(err, {
232
+ roomId: room.id,
233
+ threadId,
234
+ metadata
235
+ })
236
+ );
237
+ });
227
238
  }
228
239
  function createThread(options) {
229
240
  const body = options.body;
230
241
  const metadata = "metadata" in options ? options.metadata : {};
231
- const threads = ensureThreadsAreLoadedForMutations();
242
+ const threads = getThreads();
232
243
  const threadId = createOptimisticId(THREAD_ID_PREFIX);
233
244
  const commentId = createOptimisticId(COMMENT_ID_PREFIX);
234
245
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -248,9 +259,9 @@ function createCommentsRoom(room, errorEventSource) {
248
259
  }
249
260
  ]
250
261
  };
251
- setThreads([...threads, newThread]);
252
- startMutation();
253
- room.createThread({ threadId, commentId, body, metadata }).catch(
262
+ mutate(room.createThread({ threadId, commentId, body, metadata }), {
263
+ optimisticData: [...threads, newThread]
264
+ }).catch(
254
265
  (er) => errorEventSource.notify(
255
266
  new CreateThreadError(er, {
256
267
  roomId: room.id,
@@ -260,40 +271,14 @@ function createCommentsRoom(room, errorEventSource) {
260
271
  metadata
261
272
  })
262
273
  )
263
- ).finally(endMutation);
264
- return newThread;
265
- }
266
- function editThreadMetadata(options) {
267
- const threadId = options.threadId;
268
- const metadata = "metadata" in options ? options.metadata : {};
269
- const threads = ensureThreadsAreLoadedForMutations();
270
- setThreads(
271
- threads.map(
272
- (thread) => thread.id === threadId ? {
273
- ...thread,
274
- metadata: {
275
- ...thread.metadata,
276
- ...metadata
277
- }
278
- } : thread
279
- )
280
274
  );
281
- startMutation();
282
- room.editThreadMetadata({ metadata, threadId }).catch(
283
- (er) => errorEventSource.notify(
284
- new EditThreadMetadataError(er, {
285
- roomId: room.id,
286
- threadId,
287
- metadata
288
- })
289
- )
290
- ).finally(endMutation);
275
+ return newThread;
291
276
  }
292
277
  function createComment({
293
278
  threadId,
294
279
  body
295
280
  }) {
296
- const threads = ensureThreadsAreLoadedForMutations();
281
+ const threads = getThreads();
297
282
  const commentId = createOptimisticId(COMMENT_ID_PREFIX);
298
283
  const now = (/* @__PURE__ */ new Date()).toISOString();
299
284
  const comment = {
@@ -305,16 +290,15 @@ function createCommentsRoom(room, errorEventSource) {
305
290
  userId: getCurrentUserId(),
306
291
  body
307
292
  };
308
- setThreads(
309
- threads.map(
310
- (thread) => thread.id === threadId ? {
311
- ...thread,
312
- comments: [...thread.comments, comment]
313
- } : thread
314
- )
293
+ const optimisticData = threads.map(
294
+ (thread) => thread.id === threadId ? {
295
+ ...thread,
296
+ comments: [...thread.comments, comment]
297
+ } : thread
315
298
  );
316
- startMutation();
317
- room.createComment({ threadId, commentId, body }).catch(
299
+ mutate(room.createComment({ threadId, commentId, body }), {
300
+ optimisticData
301
+ }).catch(
318
302
  (er) => errorEventSource.notify(
319
303
  new CreateCommentError(er, {
320
304
  roomId: room.id,
@@ -323,28 +307,27 @@ function createCommentsRoom(room, errorEventSource) {
323
307
  body
324
308
  })
325
309
  )
326
- ).finally(endMutation);
310
+ );
327
311
  return comment;
328
312
  }
329
313
  function editComment({ threadId, commentId, body }) {
330
- const threads = ensureThreadsAreLoadedForMutations();
314
+ const threads = getThreads();
331
315
  const now = (/* @__PURE__ */ new Date()).toISOString();
332
- setThreads(
333
- threads.map(
334
- (thread) => thread.id === threadId ? {
335
- ...thread,
336
- comments: thread.comments.map(
337
- (comment) => comment.id === commentId ? {
338
- ...comment,
339
- editedAt: now,
340
- body
341
- } : comment
342
- )
343
- } : thread
344
- )
316
+ const optimisticData = threads.map(
317
+ (thread) => thread.id === threadId ? {
318
+ ...thread,
319
+ comments: thread.comments.map(
320
+ (comment) => comment.id === commentId ? {
321
+ ...comment,
322
+ editedAt: now,
323
+ body
324
+ } : comment
325
+ )
326
+ } : thread
345
327
  );
346
- startMutation();
347
- room.editComment({ threadId, commentId, body }).catch(
328
+ mutate(room.editComment({ threadId, commentId, body }), {
329
+ optimisticData
330
+ }).catch(
348
331
  (er) => errorEventSource.notify(
349
332
  new EditCommentError(er, {
350
333
  roomId: room.id,
@@ -353,10 +336,10 @@ function createCommentsRoom(room, errorEventSource) {
353
336
  body
354
337
  })
355
338
  )
356
- ).finally(endMutation);
339
+ );
357
340
  }
358
341
  function deleteComment({ threadId, commentId }) {
359
- const threads = ensureThreadsAreLoadedForMutations();
342
+ const threads = getThreads();
360
343
  const now = (/* @__PURE__ */ new Date()).toISOString();
361
344
  const newThreads = [];
362
345
  for (const thread of threads) {
@@ -378,9 +361,9 @@ function createCommentsRoom(room, errorEventSource) {
378
361
  newThreads.push(thread);
379
362
  }
380
363
  }
381
- setThreads(newThreads);
382
- startMutation();
383
- room.deleteComment({ threadId, commentId }).catch(
364
+ mutate(room.deleteComment({ threadId, commentId }), {
365
+ optimisticData: newThreads
366
+ }).catch(
384
367
  (er) => errorEventSource.notify(
385
368
  new DeleteCommentError(er, {
386
369
  roomId: room.id,
@@ -388,13 +371,103 @@ function createCommentsRoom(room, errorEventSource) {
388
371
  commentId
389
372
  })
390
373
  )
391
- ).finally(endMutation);
374
+ );
375
+ }
376
+ function getCurrentUserId() {
377
+ const self = room.getSelf();
378
+ if (self === null || self.id === void 0) {
379
+ return "anonymous";
380
+ } else {
381
+ return self.id;
382
+ }
383
+ }
384
+ function getThreads() {
385
+ const threads = manager.cache;
386
+ if (!threads || threads.isLoading || threads.error) {
387
+ throw new Error(
388
+ "Cannot update threads or comments before they are loaded."
389
+ );
390
+ }
391
+ return threads.threads;
392
+ }
393
+ function _subscribe() {
394
+ if (commentsEventRefCount === 0) {
395
+ commentsEventDisposer = room.events.comments.subscribe(() => {
396
+ void revalidateCache(true);
397
+ });
398
+ }
399
+ commentsEventRefCount = commentsEventRefCount + 1;
400
+ return () => {
401
+ commentsEventRefCount = commentsEventRefCount - 1;
402
+ if (commentsEventRefCount > 0)
403
+ return;
404
+ commentsEventDisposer?.();
405
+ commentsEventDisposer = void 0;
406
+ };
407
+ }
408
+ function usePolling() {
409
+ const status = useSyncExternalStore(
410
+ room.events.status.subscribe,
411
+ room.getStatus,
412
+ room.getStatus
413
+ );
414
+ useEffect2(
415
+ () => {
416
+ const interval = status === "connected" ? POLLING_INTERVAL_REALTIME : POLLING_INTERVAL;
417
+ let revalidationTimerId;
418
+ function scheduleRevalidation() {
419
+ revalidationTimerId = window.setTimeout(
420
+ executeRevalidation,
421
+ interval
422
+ );
423
+ }
424
+ function executeRevalidation() {
425
+ void revalidateCache(true).then(scheduleRevalidation);
426
+ }
427
+ scheduleRevalidation();
428
+ return () => {
429
+ window.clearTimeout(revalidationTimerId);
430
+ };
431
+ },
432
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- ESLint recommends against adding `revalidateCache` as a dependency, but not doing so causes the code inside `useEffect` to reference an outdated version of `revalidateCache`
433
+ [status, revalidateCache]
434
+ );
435
+ }
436
+ function useThreadsInternal() {
437
+ useEffect2(_subscribe, [_subscribe]);
438
+ usePolling();
439
+ const cache = useSyncExternalStore(
440
+ manager.subscribe,
441
+ () => manager.cache,
442
+ () => manager.cache
443
+ );
444
+ return cache ?? { isLoading: true };
445
+ }
446
+ function useThreads() {
447
+ useEffect2(
448
+ () => {
449
+ void revalidateCache(true);
450
+ },
451
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- ESLint recommends against adding `revalidateCache` as a dependency, but not doing so causes the code inside `useEffect` to reference an outdated version of `revalidateCache`
452
+ [revalidateCache]
453
+ );
454
+ return useThreadsInternal();
455
+ }
456
+ function useThreadsSuspense() {
457
+ const cache = useThreadsInternal();
458
+ if (cache.isLoading) {
459
+ throw revalidateCache(true);
460
+ }
461
+ if (cache.error) {
462
+ throw cache.error;
463
+ }
464
+ return cache.threads;
392
465
  }
393
466
  return {
394
467
  useThreads,
395
468
  useThreadsSuspense,
396
- createThread,
397
469
  editThreadMetadata,
470
+ createThread,
398
471
  createComment,
399
472
  editComment,
400
473
  deleteComment
@@ -641,9 +714,9 @@ function createRoomContext(client, options) {
641
714
  );
642
715
  setRoom(room2);
643
716
  return () => {
644
- const commentsRoom = commentsRooms.get(roomId);
717
+ const commentsRoom = commentsRooms.get(room2);
645
718
  if (commentsRoom) {
646
- commentsRooms.delete(roomId);
719
+ commentsRooms.delete(room2);
647
720
  }
648
721
  client.leave(roomId);
649
722
  };
@@ -847,46 +920,39 @@ function createRoomContext(client, options) {
847
920
  }
848
921
  function useLegacyKey(key) {
849
922
  const room = useRoom();
850
- const root = useMutableStorageRoot();
923
+ const rootOrNull = useMutableStorageRoot();
851
924
  const rerender = useRerender();
852
925
  React2.useEffect(() => {
853
- if (root === null) {
926
+ if (rootOrNull === null) {
854
927
  return;
855
928
  }
856
- let liveValue = root.get(key);
929
+ const root = rootOrNull;
930
+ let unsubCurr;
931
+ let curr = root.get(key);
932
+ function subscribeToCurr() {
933
+ unsubCurr = isLiveNode(curr) ? room.subscribe(curr, rerender) : void 0;
934
+ }
857
935
  function onRootChange() {
858
- const newCrdt = root.get(key);
859
- if (newCrdt !== liveValue) {
860
- unsubscribeCrdt();
861
- liveValue = newCrdt;
862
- unsubscribeCrdt = room.subscribe(
863
- liveValue,
864
- // TODO: This is hiding a bug! If `liveValue` happens to be the string `"event"` this actually subscribes an event handler!
865
- rerender
866
- );
936
+ const newValue = root.get(key);
937
+ if (newValue !== curr) {
938
+ unsubCurr?.();
939
+ curr = newValue;
940
+ subscribeToCurr();
867
941
  rerender();
868
942
  }
869
943
  }
870
- let unsubscribeCrdt = room.subscribe(
871
- liveValue,
872
- // TODO: This is hiding a bug! If `liveValue` happens to be the string `"event"` this actually subscribes an event handler!
873
- rerender
874
- );
875
- const unsubscribeRoot = room.subscribe(
876
- root,
877
- // TODO: This is hiding a bug! If `liveValue` happens to be the string `"event"` this actually subscribes an event handler!
878
- onRootChange
879
- );
944
+ subscribeToCurr();
880
945
  rerender();
946
+ const unsubscribeRoot = room.subscribe(root, onRootChange);
881
947
  return () => {
882
948
  unsubscribeRoot();
883
- unsubscribeCrdt();
949
+ unsubCurr?.();
884
950
  };
885
- }, [root, room, key, rerender]);
886
- if (root === null) {
951
+ }, [rootOrNull, room, key, rerender]);
952
+ if (rootOrNull === null) {
887
953
  return null;
888
954
  } else {
889
- return root.get(key);
955
+ return rootOrNull.get(key);
890
956
  }
891
957
  }
892
958
  function useStorage(selector, isEqual) {
@@ -1006,10 +1072,10 @@ function createRoomContext(client, options) {
1006
1072
  const commentsErrorEventSource = makeEventSource2();
1007
1073
  const commentsRooms = /* @__PURE__ */ new Map();
1008
1074
  function getCommentsRoom(room) {
1009
- let commentsRoom = commentsRooms.get(room.id);
1075
+ let commentsRoom = commentsRooms.get(room);
1010
1076
  if (commentsRoom === void 0) {
1011
1077
  commentsRoom = createCommentsRoom(room, commentsErrorEventSource);
1012
- commentsRooms.set(room.id, commentsRoom);
1078
+ commentsRooms.set(room, commentsRoom);
1013
1079
  }
1014
1080
  return commentsRoom;
1015
1081
  }