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