@liveblocks/react 1.3.4 → 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.4";
8
+ var PKG_VERSION = "1.3.5";
9
9
  var PKG_FORMAT = "esm";
10
10
 
11
11
  // src/ClientSideSuspense.tsx
@@ -31,7 +31,9 @@ import * as React2 from "react";
31
31
  import { useSyncExternalStoreWithSelector } from "use-sync-external-store/shim/with-selector.js";
32
32
 
33
33
  // src/comments/CommentsRoom.ts
34
- import { makePoller } from "@liveblocks/core";
34
+ import {
35
+ makeEventSource
36
+ } from "@liveblocks/core";
35
37
  import { nanoid } from "nanoid";
36
38
  import { useEffect as useEffect2 } from "react";
37
39
  import { useSyncExternalStore } from "use-sync-external-store/shim/index.js";
@@ -78,158 +80,166 @@ var DeleteCommentError = class extends Error {
78
80
  }
79
81
  };
80
82
 
81
- // src/comments/lib/store.ts
82
- import { makeEventSource } from "@liveblocks/core";
83
- function createStore(initialState) {
84
- 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;
85
98
  const eventSource = makeEventSource();
86
99
  return {
87
- get() {
88
- return state;
100
+ get cache() {
101
+ return cache;
89
102
  },
90
- set(newState) {
91
- state = newState;
92
- eventSource.notify(state);
103
+ set cache(value) {
104
+ cache = value;
105
+ eventSource.notify(cache);
93
106
  },
94
- subscribe(callback) {
95
- return eventSource.subscribe(callback);
107
+ get request() {
108
+ return request;
109
+ },
110
+ set request(value) {
111
+ request = value;
96
112
  },
97
- subscribeOnce(callback) {
98
- return eventSource.subscribeOnce(callback);
113
+ get mutation() {
114
+ return mutation;
99
115
  },
100
- subscribersCount() {
101
- return eventSource.count();
116
+ set mutation(value) {
117
+ mutation = value;
102
118
  },
103
- destroy() {
104
- return eventSource.clear();
119
+ subscribe(callback) {
120
+ return eventSource.subscribe(callback);
105
121
  }
106
122
  };
107
123
  }
108
-
109
- // src/comments/CommentsRoom.ts
110
- var POLLING_INTERVAL_REALTIME = 3e4;
111
- var POLLING_INTERVAL = 5e3;
112
- var THREAD_ID_PREFIX = "th";
113
- var COMMENT_ID_PREFIX = "cm";
114
- function createOptimisticId(prefix) {
115
- return `${prefix}_${nanoid()}`;
116
- }
117
124
  function createCommentsRoom(room, errorEventSource) {
118
- const store = createStore({
119
- isLoading: true
120
- });
121
- let fetchThreadsPromise = null;
122
- let numberOfMutations = 0;
123
- function endMutation() {
124
- numberOfMutations--;
125
- if (numberOfMutations === 0) {
126
- void revalidateThreads();
127
- }
128
- }
129
- function startMutation() {
130
- pollingHub.threads.stop();
131
- numberOfMutations++;
132
- }
133
- const pollingHub = {
134
- // TODO: If there's an error, it will currently infinitely retry at the current polling rate → add retry logic
135
- threads: makePoller(revalidateThreads)
136
- };
137
- let unsubscribeRealtimeEvents;
138
- let unsubscribeRealtimeConnection;
139
- let realtimeClientConnected = false;
140
- function getPollingInterval() {
141
- return realtimeClientConnected ? POLLING_INTERVAL_REALTIME : POLLING_INTERVAL;
142
- }
143
- function ensureThreadsAreLoadedForMutations() {
144
- const state = store.get();
145
- if (state.isLoading || state.error) {
146
- throw new Error(
147
- "Cannot update threads or comments before they are loaded"
148
- );
149
- }
150
- return state.threads;
151
- }
152
- async function revalidateThreads() {
153
- pollingHub.threads.pause();
154
- if (numberOfMutations === 0) {
155
- if (fetchThreadsPromise === null) {
156
- 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;
157
145
  }
158
- setThreads(await fetchThreadsPromise);
159
- fetchThreadsPromise = null;
146
+ } catch (err) {
147
+ manager.cache = currentCache;
148
+ throw err;
160
149
  }
161
- pollingHub.threads.resume();
162
- }
163
- function subscribe() {
164
- if (!unsubscribeRealtimeEvents) {
165
- unsubscribeRealtimeEvents = room.events.comments.subscribe(() => {
166
- pollingHub.threads.restart(getPollingInterval());
167
- void revalidateThreads();
168
- });
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;
169
167
  }
170
- if (!unsubscribeRealtimeConnection) {
171
- unsubscribeRealtimeConnection = room.events.status.subscribe((status) => {
172
- const nextRealtimeClientConnected = status === "connected";
173
- if (nextRealtimeClientConnected !== realtimeClientConnected) {
174
- realtimeClientConnected = nextRealtimeClientConnected;
175
- pollingHub.threads.restart(getPollingInterval());
176
- }
177
- });
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);
178
175
  }
179
- pollingHub.threads.start(getPollingInterval());
180
- return () => {
181
- 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)
182
188
  return;
189
+ const newData = await activeRequest.fetcher;
190
+ startAt = activeRequest.timestamp;
191
+ if (shouldStartRequest) {
192
+ setTimeout(deleteActiveRequest, DEDUPING_INTERVAL);
183
193
  }
184
- pollingHub.threads.stop();
185
- unsubscribeRealtimeEvents?.();
186
- unsubscribeRealtimeEvents = void 0;
187
- unsubscribeRealtimeConnection?.();
188
- unsubscribeRealtimeConnection = void 0;
189
- };
190
- }
191
- function setThreads(newThreads) {
192
- store.set({
193
- threads: newThreads,
194
- isLoading: false
195
- });
196
- }
197
- function useThreadsInternal() {
198
- useEffect2(subscribe, []);
199
- return useSyncExternalStore(
200
- store.subscribe,
201
- store.get,
202
- store.get
203
- );
204
- }
205
- function useThreads() {
206
- useEffect2(() => {
207
- void revalidateThreads();
208
- }, []);
209
- return useThreadsInternal();
210
- }
211
- function useThreadsSuspense() {
212
- const result = useThreadsInternal();
213
- if (result.isLoading) {
214
- throw revalidateThreads();
215
- }
216
- if (result.error) {
217
- 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
+ };
218
212
  }
219
- return result.threads;
220
213
  }
221
- function getCurrentUserId() {
222
- const self = room.getSelf();
223
- if (self === null || self.id === void 0) {
224
- return "anonymous";
225
- } else {
226
- return self.id;
227
- }
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
+ });
228
238
  }
229
239
  function createThread(options) {
230
240
  const body = options.body;
231
241
  const metadata = "metadata" in options ? options.metadata : {};
232
- const threads = ensureThreadsAreLoadedForMutations();
242
+ const threads = getThreads();
233
243
  const threadId = createOptimisticId(THREAD_ID_PREFIX);
234
244
  const commentId = createOptimisticId(COMMENT_ID_PREFIX);
235
245
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -249,9 +259,9 @@ function createCommentsRoom(room, errorEventSource) {
249
259
  }
250
260
  ]
251
261
  };
252
- setThreads([...threads, newThread]);
253
- startMutation();
254
- room.createThread({ threadId, commentId, body, metadata }).catch(
262
+ mutate(room.createThread({ threadId, commentId, body, metadata }), {
263
+ optimisticData: [...threads, newThread]
264
+ }).catch(
255
265
  (er) => errorEventSource.notify(
256
266
  new CreateThreadError(er, {
257
267
  roomId: room.id,
@@ -261,40 +271,14 @@ function createCommentsRoom(room, errorEventSource) {
261
271
  metadata
262
272
  })
263
273
  )
264
- ).finally(endMutation);
265
- return newThread;
266
- }
267
- function editThreadMetadata(options) {
268
- const threadId = options.threadId;
269
- const metadata = "metadata" in options ? options.metadata : {};
270
- const threads = ensureThreadsAreLoadedForMutations();
271
- setThreads(
272
- threads.map(
273
- (thread) => thread.id === threadId ? {
274
- ...thread,
275
- metadata: {
276
- ...thread.metadata,
277
- ...metadata
278
- }
279
- } : thread
280
- )
281
274
  );
282
- startMutation();
283
- room.editThreadMetadata({ metadata, threadId }).catch(
284
- (er) => errorEventSource.notify(
285
- new EditThreadMetadataError(er, {
286
- roomId: room.id,
287
- threadId,
288
- metadata
289
- })
290
- )
291
- ).finally(endMutation);
275
+ return newThread;
292
276
  }
293
277
  function createComment({
294
278
  threadId,
295
279
  body
296
280
  }) {
297
- const threads = ensureThreadsAreLoadedForMutations();
281
+ const threads = getThreads();
298
282
  const commentId = createOptimisticId(COMMENT_ID_PREFIX);
299
283
  const now = (/* @__PURE__ */ new Date()).toISOString();
300
284
  const comment = {
@@ -306,16 +290,15 @@ function createCommentsRoom(room, errorEventSource) {
306
290
  userId: getCurrentUserId(),
307
291
  body
308
292
  };
309
- setThreads(
310
- threads.map(
311
- (thread) => thread.id === threadId ? {
312
- ...thread,
313
- comments: [...thread.comments, comment]
314
- } : thread
315
- )
293
+ const optimisticData = threads.map(
294
+ (thread) => thread.id === threadId ? {
295
+ ...thread,
296
+ comments: [...thread.comments, comment]
297
+ } : thread
316
298
  );
317
- startMutation();
318
- room.createComment({ threadId, commentId, body }).catch(
299
+ mutate(room.createComment({ threadId, commentId, body }), {
300
+ optimisticData
301
+ }).catch(
319
302
  (er) => errorEventSource.notify(
320
303
  new CreateCommentError(er, {
321
304
  roomId: room.id,
@@ -324,28 +307,27 @@ function createCommentsRoom(room, errorEventSource) {
324
307
  body
325
308
  })
326
309
  )
327
- ).finally(endMutation);
310
+ );
328
311
  return comment;
329
312
  }
330
313
  function editComment({ threadId, commentId, body }) {
331
- const threads = ensureThreadsAreLoadedForMutations();
314
+ const threads = getThreads();
332
315
  const now = (/* @__PURE__ */ new Date()).toISOString();
333
- setThreads(
334
- threads.map(
335
- (thread) => thread.id === threadId ? {
336
- ...thread,
337
- comments: thread.comments.map(
338
- (comment) => comment.id === commentId ? {
339
- ...comment,
340
- editedAt: now,
341
- body
342
- } : comment
343
- )
344
- } : thread
345
- )
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
346
327
  );
347
- startMutation();
348
- room.editComment({ threadId, commentId, body }).catch(
328
+ mutate(room.editComment({ threadId, commentId, body }), {
329
+ optimisticData
330
+ }).catch(
349
331
  (er) => errorEventSource.notify(
350
332
  new EditCommentError(er, {
351
333
  roomId: room.id,
@@ -354,10 +336,10 @@ function createCommentsRoom(room, errorEventSource) {
354
336
  body
355
337
  })
356
338
  )
357
- ).finally(endMutation);
339
+ );
358
340
  }
359
341
  function deleteComment({ threadId, commentId }) {
360
- const threads = ensureThreadsAreLoadedForMutations();
342
+ const threads = getThreads();
361
343
  const now = (/* @__PURE__ */ new Date()).toISOString();
362
344
  const newThreads = [];
363
345
  for (const thread of threads) {
@@ -379,9 +361,9 @@ function createCommentsRoom(room, errorEventSource) {
379
361
  newThreads.push(thread);
380
362
  }
381
363
  }
382
- setThreads(newThreads);
383
- startMutation();
384
- room.deleteComment({ threadId, commentId }).catch(
364
+ mutate(room.deleteComment({ threadId, commentId }), {
365
+ optimisticData: newThreads
366
+ }).catch(
385
367
  (er) => errorEventSource.notify(
386
368
  new DeleteCommentError(er, {
387
369
  roomId: room.id,
@@ -389,13 +371,103 @@ function createCommentsRoom(room, errorEventSource) {
389
371
  commentId
390
372
  })
391
373
  )
392
- ).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;
393
465
  }
394
466
  return {
395
467
  useThreads,
396
468
  useThreadsSuspense,
397
- createThread,
398
469
  editThreadMetadata,
470
+ createThread,
399
471
  createComment,
400
472
  editComment,
401
473
  deleteComment
@@ -642,9 +714,9 @@ function createRoomContext(client, options) {
642
714
  );
643
715
  setRoom(room2);
644
716
  return () => {
645
- const commentsRoom = commentsRooms.get(roomId);
717
+ const commentsRoom = commentsRooms.get(room2);
646
718
  if (commentsRoom) {
647
- commentsRooms.delete(roomId);
719
+ commentsRooms.delete(room2);
648
720
  }
649
721
  client.leave(roomId);
650
722
  };
@@ -1000,10 +1072,10 @@ function createRoomContext(client, options) {
1000
1072
  const commentsErrorEventSource = makeEventSource2();
1001
1073
  const commentsRooms = /* @__PURE__ */ new Map();
1002
1074
  function getCommentsRoom(room) {
1003
- let commentsRoom = commentsRooms.get(room.id);
1075
+ let commentsRoom = commentsRooms.get(room);
1004
1076
  if (commentsRoom === void 0) {
1005
1077
  commentsRoom = createCommentsRoom(room, commentsErrorEventSource);
1006
- commentsRooms.set(room.id, commentsRoom);
1078
+ commentsRooms.set(room, commentsRoom);
1007
1079
  }
1008
1080
  return commentsRoom;
1009
1081
  }