canvu-react 0.3.11 → 0.3.13

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/realtime.js CHANGED
@@ -2072,6 +2072,10 @@ function useRealtimeSession(options) {
2072
2072
  }, []);
2073
2073
  const applyDocument = useCallback(
2074
2074
  (snapshot, options2) => {
2075
+ const currentSnapshot = latestDocumentRef.current;
2076
+ if (currentSnapshot && currentSnapshot.revision === snapshot.revision && currentSnapshot.updatedByClientId === snapshot.updatedByClientId && sameSerializedItems(currentSnapshot.items, snapshot.items)) {
2077
+ return;
2078
+ }
2075
2079
  currentRevisionRef.current = snapshot.revision;
2076
2080
  latestDocumentRef.current = snapshot;
2077
2081
  setDocument(snapshot);
@@ -2198,7 +2202,7 @@ function useRealtimeSession(options) {
2198
2202
  items: preparedItems.items,
2199
2203
  serialized: preparedItems.serialized
2200
2204
  };
2201
- const didSend = sendRaw({
2205
+ const didSend = sendRawRef.current({
2202
2206
  type: "document:update",
2203
2207
  roomId,
2204
2208
  clientId: clientIdRef.current,
@@ -2210,7 +2214,7 @@ function useRealtimeSession(options) {
2210
2214
  outboundInFlightRef.current = null;
2211
2215
  setHasPendingDocumentSync(true);
2212
2216
  }
2213
- }, [clearDocumentFlushSchedule, roomId, sendRaw]);
2217
+ }, [clearDocumentFlushSchedule, roomId]);
2214
2218
  const scheduleDocumentFlush = useCallback(() => {
2215
2219
  clearDocumentFlushSchedule();
2216
2220
  documentFlushTimerRef.current = window.setTimeout(() => {
@@ -2233,9 +2237,9 @@ function useRealtimeSession(options) {
2233
2237
  queuedItemsRef.current = items;
2234
2238
  setHasPendingDocumentSync(true);
2235
2239
  if (conflictRef.current) return;
2236
- scheduleDocumentFlush();
2240
+ scheduleDocumentFlushRef.current();
2237
2241
  },
2238
- [roomId, scheduleDocumentFlush, scheduleDraftPersistence, setLocalDraft]
2242
+ [roomId, scheduleDraftPersistence, setLocalDraft]
2239
2243
  );
2240
2244
  const applyDraftSnapshot = useCallback(
2241
2245
  (draft, options2) => {
@@ -2253,7 +2257,7 @@ function useRealtimeSession(options) {
2253
2257
  return false;
2254
2258
  }
2255
2259
  if (sameSerializedItems(localDraft.items, serverDocument.items)) {
2256
- clearLocalDraft();
2260
+ clearLocalDraftRef.current();
2257
2261
  setHasPendingDocumentSync(false);
2258
2262
  setConflictState(null);
2259
2263
  applyDocument(serverDocument, options2);
@@ -2280,13 +2284,7 @@ function useRealtimeSession(options) {
2280
2284
  });
2281
2285
  return true;
2282
2286
  },
2283
- [
2284
- applyDocument,
2285
- applyDraftSnapshot,
2286
- clearLocalDraft,
2287
- scheduleDocumentFlush,
2288
- setConflictState
2289
- ]
2287
+ [applyDocument, applyDraftSnapshot, scheduleDocumentFlush, setConflictState]
2290
2288
  );
2291
2289
  const sendPresenceUpdate = useCallback(() => {
2292
2290
  sendRaw({
@@ -2373,6 +2371,26 @@ function useRealtimeSession(options) {
2373
2371
  setLocalDraft
2374
2372
  ]
2375
2373
  );
2374
+ const setConflictStateRef = useRef(setConflictState);
2375
+ setConflictStateRef.current = setConflictState;
2376
+ const updateConnectionRef = useRef(updateConnection);
2377
+ updateConnectionRef.current = updateConnection;
2378
+ const applyDocumentRef = useRef(applyDocument);
2379
+ applyDocumentRef.current = applyDocument;
2380
+ const clearLocalDraftRef = useRef(clearLocalDraft);
2381
+ clearLocalDraftRef.current = clearLocalDraft;
2382
+ const collapsePeersToSelfRef = useRef(collapsePeersToSelf);
2383
+ collapsePeersToSelfRef.current = collapsePeersToSelf;
2384
+ const applyPeersRef = useRef(applyPeers);
2385
+ applyPeersRef.current = applyPeers;
2386
+ const resolveAuthoritativeDocumentRef = useRef(resolveAuthoritativeDocument);
2387
+ resolveAuthoritativeDocumentRef.current = resolveAuthoritativeDocument;
2388
+ const scheduleDocumentFlushRef = useRef(scheduleDocumentFlush);
2389
+ scheduleDocumentFlushRef.current = scheduleDocumentFlush;
2390
+ const scheduleReconnectRef = useRef(scheduleReconnect);
2391
+ scheduleReconnectRef.current = scheduleReconnect;
2392
+ const sendRawRef = useRef(sendRaw);
2393
+ sendRawRef.current = sendRaw;
2376
2394
  useEffect(() => {
2377
2395
  if (!roomId) {
2378
2396
  clearDocumentFlushSchedule();
@@ -2426,8 +2444,8 @@ function useRealtimeSession(options) {
2426
2444
  queuedItemsRef.current = localDraftRef.current?.items ?? null;
2427
2445
  outboundInFlightRef.current = null;
2428
2446
  setHasPendingDocumentSync(localDraftRef.current != null);
2429
- collapsePeersToSelf("offline");
2430
- updateConnection((prev) => ({
2447
+ collapsePeersToSelfRef.current("offline");
2448
+ updateConnectionRef.current((prev) => ({
2431
2449
  ...prev,
2432
2450
  state: "offline",
2433
2451
  connected: false,
@@ -2439,8 +2457,8 @@ function useRealtimeSession(options) {
2439
2457
  manualDisconnectRef.current = false;
2440
2458
  const socketUrl = normalizeSocketUrl(url);
2441
2459
  if (!socketUrl) {
2442
- collapsePeersToSelf("offline");
2443
- updateConnection((prev) => ({
2460
+ collapsePeersToSelfRef.current("offline");
2461
+ updateConnectionRef.current((prev) => ({
2444
2462
  ...prev,
2445
2463
  state: "offline",
2446
2464
  connected: false,
@@ -2450,7 +2468,7 @@ function useRealtimeSession(options) {
2450
2468
  return;
2451
2469
  }
2452
2470
  if (!isValidSocketUrl(socketUrl)) {
2453
- updateConnection((prev) => ({
2471
+ updateConnectionRef.current((prev) => ({
2454
2472
  ...prev,
2455
2473
  state: "error",
2456
2474
  connected: false,
@@ -2461,7 +2479,7 @@ function useRealtimeSession(options) {
2461
2479
  return;
2462
2480
  }
2463
2481
  let disposed = false;
2464
- updateConnection((prev) => ({
2482
+ updateConnectionRef.current((prev) => ({
2465
2483
  ...prev,
2466
2484
  state: retryCountRef.current > 0 ? "reconnecting" : "connecting",
2467
2485
  connected: false,
@@ -2479,7 +2497,7 @@ function useRealtimeSession(options) {
2479
2497
  socket.addEventListener("open", () => {
2480
2498
  if (disposed) return;
2481
2499
  clearConnectTimeout();
2482
- sendRaw({
2500
+ sendRawRef.current({
2483
2501
  type: "session:join",
2484
2502
  roomId,
2485
2503
  peer: {
@@ -2492,7 +2510,7 @@ function useRealtimeSession(options) {
2492
2510
  });
2493
2511
  clearHeartbeatTimer();
2494
2512
  heartbeatTimerRef.current = window.setInterval(() => {
2495
- sendRaw({
2513
+ sendRawRef.current({
2496
2514
  type: "session:ping",
2497
2515
  roomId,
2498
2516
  clientId: clientIdRef.current,
@@ -2512,13 +2530,13 @@ function useRealtimeSession(options) {
2512
2530
  }
2513
2531
  const parsed = parseRealtimeServerMessage(payload);
2514
2532
  if (!parsed) return;
2515
- updateConnection((prev) => ({
2533
+ updateConnectionRef.current((prev) => ({
2516
2534
  ...prev,
2517
2535
  lastMessageAt: nowMs()
2518
2536
  }));
2519
2537
  if (parsed.type === "session:welcome") {
2520
2538
  retryCountRef.current = 0;
2521
- updateConnection((prev) => ({
2539
+ updateConnectionRef.current((prev) => ({
2522
2540
  ...prev,
2523
2541
  state: "connected",
2524
2542
  connected: true,
@@ -2527,8 +2545,8 @@ function useRealtimeSession(options) {
2527
2545
  lastConnectedAt: nowMs(),
2528
2546
  lastError: null
2529
2547
  }));
2530
- applyPeers(parsed.peers);
2531
- const handledByDraft = resolveAuthoritativeDocument(
2548
+ applyPeersRef.current(parsed.peers);
2549
+ const handledByDraft = resolveAuthoritativeDocumentRef.current(
2532
2550
  sanitizeRealtimeSnapshot(parsed.document),
2533
2551
  {
2534
2552
  suppressSubscriberNotify: localDraftRef.current != null && sameSerializedItems(
@@ -2542,11 +2560,11 @@ function useRealtimeSession(options) {
2542
2560
  outboundInFlightRef.current = null;
2543
2561
  setHasPendingDocumentSync(false);
2544
2562
  }
2545
- scheduleDocumentFlush();
2563
+ scheduleDocumentFlushRef.current();
2546
2564
  return;
2547
2565
  }
2548
2566
  if (parsed.type === "presence:sync") {
2549
- applyPeers(parsed.peers);
2567
+ applyPeersRef.current(parsed.peers);
2550
2568
  return;
2551
2569
  }
2552
2570
  if (parsed.type === "session:peer-joined") {
@@ -2568,14 +2586,14 @@ function useRealtimeSession(options) {
2568
2586
  return;
2569
2587
  }
2570
2588
  if (parsed.type === "session:pong") {
2571
- updateConnection((prev) => ({
2589
+ updateConnectionRef.current((prev) => ({
2572
2590
  ...prev,
2573
2591
  lastPongAt: parsed.serverTime
2574
2592
  }));
2575
2593
  return;
2576
2594
  }
2577
2595
  if (parsed.type === "session:error") {
2578
- updateConnection((prev) => ({
2596
+ updateConnectionRef.current((prev) => ({
2579
2597
  ...prev,
2580
2598
  state: prev.connected ? prev.state : "error",
2581
2599
  lastError: parsed.message
@@ -2591,42 +2609,46 @@ function useRealtimeSession(options) {
2591
2609
  outboundInFlightRef.current = null;
2592
2610
  queuedItemsRef.current = localDraftRef.current?.items ?? null;
2593
2611
  setHasPendingDocumentSync(queuedItemsRef.current != null);
2594
- resolveAuthoritativeDocument(sanitizeRealtimeSnapshot(parsed.document));
2612
+ resolveAuthoritativeDocumentRef.current(
2613
+ sanitizeRealtimeSnapshot(parsed.document)
2614
+ );
2595
2615
  return;
2596
2616
  }
2597
2617
  const shouldSuppress = inFlight != null && sameSerializedItems(inFlight.items, parsed.document.items);
2598
2618
  outboundInFlightRef.current = null;
2599
- applyDocument(sanitizeRealtimeSnapshot(parsed.document), {
2619
+ applyDocumentRef.current(sanitizeRealtimeSnapshot(parsed.document), {
2600
2620
  suppressSubscriberNotify: shouldSuppress
2601
2621
  });
2602
2622
  if (queuedItemsRef.current) {
2603
2623
  if (sameSerializedItems(queuedItemsRef.current, parsed.document.items)) {
2604
2624
  queuedItemsRef.current = null;
2605
2625
  } else {
2606
- scheduleDocumentFlush();
2626
+ scheduleDocumentFlushRef.current();
2607
2627
  }
2608
2628
  }
2609
2629
  if (!queuedItemsRef.current) {
2610
- clearLocalDraft();
2630
+ clearLocalDraftRef.current();
2611
2631
  }
2612
2632
  setHasPendingDocumentSync(queuedItemsRef.current != null);
2613
- setConflictState(null);
2633
+ setConflictStateRef.current(null);
2614
2634
  return;
2615
2635
  }
2616
2636
  if (parsed.type === "document:resync-required") {
2617
2637
  outboundInFlightRef.current = null;
2618
2638
  queuedItemsRef.current = localDraftRef.current?.items ?? null;
2619
2639
  setHasPendingDocumentSync(queuedItemsRef.current != null);
2620
- updateConnection((prev) => ({
2640
+ updateConnectionRef.current((prev) => ({
2621
2641
  ...prev,
2622
2642
  lastError: parsed.reason
2623
2643
  }));
2624
- resolveAuthoritativeDocument(sanitizeRealtimeSnapshot(parsed.document));
2644
+ resolveAuthoritativeDocumentRef.current(
2645
+ sanitizeRealtimeSnapshot(parsed.document)
2646
+ );
2625
2647
  }
2626
2648
  });
2627
2649
  socket.addEventListener("error", () => {
2628
2650
  if (disposed) return;
2629
- updateConnection((prev) => ({
2651
+ updateConnectionRef.current((prev) => ({
2630
2652
  ...prev,
2631
2653
  state: prev.connected ? prev.state : "error",
2632
2654
  lastError: "Falha de conex\xE3o websocket."
@@ -2637,17 +2659,17 @@ function useRealtimeSession(options) {
2637
2659
  clearHeartbeatTimer();
2638
2660
  clearConnectTimeout();
2639
2661
  wsRef.current = null;
2640
- collapsePeersToSelf(
2662
+ collapsePeersToSelfRef.current(
2641
2663
  manualDisconnectRef.current || !enabled ? "offline" : "reconnecting"
2642
2664
  );
2643
- updateConnection((prev) => ({
2665
+ updateConnectionRef.current((prev) => ({
2644
2666
  ...prev,
2645
2667
  connected: false,
2646
2668
  clientId: prev.clientId,
2647
2669
  state: manualDisconnectRef.current || !enabled ? "offline" : prev.state
2648
2670
  }));
2649
2671
  if (!manualDisconnectRef.current && enabled) {
2650
- scheduleReconnect();
2672
+ scheduleReconnectRef.current();
2651
2673
  }
2652
2674
  });
2653
2675
  return () => {
@@ -2659,14 +2681,10 @@ function useRealtimeSession(options) {
2659
2681
  socket.close();
2660
2682
  };
2661
2683
  }, [
2662
- applyDocument,
2663
- applyPeers,
2664
2684
  clearConnectTimeout,
2685
+ clearDocumentFlushSchedule,
2665
2686
  clearHeartbeatTimer,
2666
- clearLocalDraft,
2667
2687
  clearReconnectTimer,
2668
- collapsePeersToSelf,
2669
- clearDocumentFlushSchedule,
2670
2688
  connectSequence,
2671
2689
  connectTimeoutMs,
2672
2690
  enabled,
@@ -2675,13 +2693,7 @@ function useRealtimeSession(options) {
2675
2693
  peer.displayName,
2676
2694
  peer.id,
2677
2695
  peer.image,
2678
- resolveAuthoritativeDocument,
2679
2696
  roomId,
2680
- scheduleDocumentFlush,
2681
- scheduleReconnect,
2682
- sendRaw,
2683
- setConflictState,
2684
- updateConnection,
2685
2697
  url
2686
2698
  ]);
2687
2699
  useEffect(() => {
@@ -2987,7 +2999,12 @@ function useRealtimeCanvasDocument(options) {
2987
2999
  } = options;
2988
3000
  const [loading, setLoading] = useState(false);
2989
3001
  const lastAppliedRevisionRef = useRef(null);
3002
+ const inFlightRevisionRef = useRef(null);
2990
3003
  const realtimeEnabled = enabled && session != null;
3004
+ const documentRevision = session?.document?.revision ?? null;
3005
+ const documentItems = session?.document?.items;
3006
+ const documentUpdatedByClientId = session?.document?.updatedByClientId ?? null;
3007
+ const connectionClientId = session?.connection.clientId ?? null;
2991
3008
  const applyIncomingItems = useCallback(
2992
3009
  async (nextItems) => {
2993
3010
  const normalizedItems = normalizeItems ? normalizeItems(nextItems) : [...nextItems];
@@ -3010,22 +3027,38 @@ function useRealtimeCanvasDocument(options) {
3010
3027
  );
3011
3028
  useEffect(() => {
3012
3029
  if (!realtimeEnabled || !onItemsChange || !session?.document) return;
3013
- if (session.document.updatedByClientId === session.connection.clientId) return;
3014
- if (lastAppliedRevisionRef.current === session.document.revision) return;
3030
+ if (documentUpdatedByClientId === connectionClientId) return;
3031
+ if (documentRevision == null) return;
3032
+ if (lastAppliedRevisionRef.current === documentRevision) return;
3033
+ if (inFlightRevisionRef.current === documentRevision) return;
3034
+ inFlightRevisionRef.current = documentRevision;
3015
3035
  let cancelled = false;
3016
3036
  setLoading(true);
3017
- void applyIncomingItems(session.document.items).then((resolvedItems) => {
3037
+ void applyIncomingItems(documentItems ?? []).then((resolvedItems) => {
3018
3038
  if (cancelled) return;
3019
- lastAppliedRevisionRef.current = session.document?.revision ?? null;
3039
+ if (inFlightRevisionRef.current !== documentRevision) return;
3040
+ lastAppliedRevisionRef.current = documentRevision;
3020
3041
  onItemsChange(resolvedItems);
3021
3042
  }).finally(() => {
3043
+ if (inFlightRevisionRef.current === documentRevision) {
3044
+ inFlightRevisionRef.current = null;
3045
+ }
3022
3046
  if (cancelled) return;
3023
3047
  setLoading(false);
3024
3048
  });
3025
3049
  return () => {
3026
3050
  cancelled = true;
3027
3051
  };
3028
- }, [applyIncomingItems, realtimeEnabled, onItemsChange, session]);
3052
+ }, [
3053
+ applyIncomingItems,
3054
+ connectionClientId,
3055
+ documentItems,
3056
+ documentRevision,
3057
+ documentUpdatedByClientId,
3058
+ onItemsChange,
3059
+ realtimeEnabled,
3060
+ session?.document
3061
+ ]);
3029
3062
  return useMemo(
3030
3063
  () => ({
3031
3064
  items,