react-mirrorstate 0.3.0 → 0.4.0

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.
@@ -4,7 +4,6 @@ declare class WebSocketConnectionManager {
4
4
  private isConnecting;
5
5
  private listeners;
6
6
  private currentStates;
7
- private clientId;
8
7
  private lastSeq;
9
8
  private queuedUpdates;
10
9
  private getWebSocketConfig;
@@ -3,7 +3,6 @@ class WebSocketConnectionManager {
3
3
  isConnecting = false;
4
4
  listeners = new Map();
5
5
  currentStates = new Map();
6
- clientId = null;
7
6
  lastSeq = new Map();
8
7
  queuedUpdates = new Map();
9
8
  async getWebSocketConfig() {
@@ -43,38 +42,36 @@ class WebSocketConnectionManager {
43
42
  this.ws = new WebSocket(wsUrl);
44
43
  this.ws.onopen = () => {
45
44
  this.isConnecting = false;
45
+ // Flush any queued updates
46
+ this.queuedUpdates.forEach((state, name) => {
47
+ this.updateState(name, state);
48
+ });
49
+ this.queuedUpdates.clear();
46
50
  };
47
51
  this.ws.onclose = () => {
48
52
  this.cleanup();
49
53
  };
50
- this.ws.onerror = () => {
51
- console.error("WebSocket error");
54
+ this.ws.onerror = (e) => {
55
+ console.error("WebSocket error", e);
52
56
  this.cleanup();
53
57
  };
54
58
  this.ws.onmessage = (event) => {
59
+ let data;
55
60
  try {
56
- const data = JSON.parse(event.data);
57
- if (data.type === "connected") {
58
- this.clientId = data.clientId;
59
- // Flush any queued updates
60
- this.queuedUpdates.forEach((state, name) => {
61
- this.updateState(name, state);
62
- });
63
- this.queuedUpdates.clear();
64
- return;
65
- }
66
- if (data.type === "fileChange") {
67
- // Only apply updates with a higher sequence number
68
- const currentSeq = this.lastSeq.get(data.name) ?? -1;
69
- if (data.seq !== undefined && data.seq > currentSeq) {
70
- this.lastSeq.set(data.name, data.seq);
71
- this.currentStates.set(data.name, data.state);
72
- this.notifyListeners(data.name, data.state);
73
- }
74
- }
61
+ data = JSON.parse(event.data);
75
62
  }
76
63
  catch (error) {
77
64
  console.error("Error handling server message:", error);
65
+ return;
66
+ }
67
+ if (data.type === "fileChange") {
68
+ // Only apply updates with a higher sequence number
69
+ const currentSeq = this.lastSeq.get(data.name) ?? -1;
70
+ if (data.seq !== undefined && data.seq > currentSeq) {
71
+ this.lastSeq.set(data.name, data.seq);
72
+ this.currentStates.set(data.name, data.state);
73
+ this.notifyListeners(data.name, data.state);
74
+ }
78
75
  }
79
76
  };
80
77
  }
@@ -83,12 +80,7 @@ class WebSocketConnectionManager {
83
80
  this.listeners.set(name, new Set());
84
81
  }
85
82
  this.listeners.get(name).add(listener);
86
- // Connect if not already connected (dev mode)
87
83
  this.connect();
88
- // Don't immediately notify with currentStates here - it might be stale
89
- // and could revert optimistic updates. The component initializes from
90
- // INITIAL_STATES, and the server will send initialState messages when
91
- // the connection is established, which will update all subscribers.
92
84
  return () => {
93
85
  const nameListeners = this.listeners.get(name);
94
86
  if (nameListeners) {
@@ -101,7 +93,9 @@ class WebSocketConnectionManager {
101
93
  }
102
94
  pendingUpdates = new Map();
103
95
  updateState(name, state) {
104
- if (this.ws?.readyState !== WebSocket.OPEN || !this.clientId) {
96
+ // Immediately update currentStates so subsequent reads get the latest value
97
+ this.currentStates.set(name, state);
98
+ if (this.ws?.readyState !== WebSocket.OPEN) {
105
99
  this.queuedUpdates.set(name, state);
106
100
  return;
107
101
  }
@@ -110,17 +104,15 @@ class WebSocketConnectionManager {
110
104
  if (pendingUpdate) {
111
105
  clearTimeout(pendingUpdate);
112
106
  }
113
- // Debounce rapid updates
107
+ // Debounce rapid WebSocket sends
114
108
  const timeout = setTimeout(() => {
115
- if (!this.ws || !this.clientId) {
109
+ if (!this.ws) {
116
110
  return;
117
111
  }
118
112
  this.ws.send(JSON.stringify({
119
- clientId: this.clientId,
120
113
  name,
121
- state
114
+ state,
122
115
  }));
123
- this.currentStates.set(name, state);
124
116
  this.pendingUpdates.delete(name);
125
117
  }, 10);
126
118
  this.pendingUpdates.set(name, timeout);
package/dist/index.js CHANGED
@@ -2,15 +2,47 @@ import { useEffect, useState, useRef } from "react";
2
2
  import { produce } from "immer";
3
3
  import { connectionManager } from "./connection-manager";
4
4
  import { INITIAL_STATES } from "virtual:mirrorstate/initial-states";
5
+ // Batching state for each mirror state name
6
+ const batchQueues = new Map();
7
+ const batchPending = new Map();
8
+ const batchCallbacks = new Map();
9
+ function scheduleBatchFlush(name) {
10
+ if (batchPending.get(name)) {
11
+ return;
12
+ }
13
+ batchPending.set(name, true);
14
+ queueMicrotask(() => {
15
+ const queue = batchQueues.get(name);
16
+ const callbacks = batchCallbacks.get(name);
17
+ if (!queue || queue.length === 0) {
18
+ batchPending.set(name, false);
19
+ return;
20
+ }
21
+ // Apply all queued updates in sequence
22
+ // Get current state from connection manager, falling back to INITIAL_STATES
23
+ let currentState = connectionManager.getCurrentState(name) ??
24
+ INITIAL_STATES?.[name];
25
+ // Apply each update sequentially, handling both object mutations and primitive returns
26
+ let newState = currentState;
27
+ queue.forEach((updater) => {
28
+ newState = produce(newState, updater);
29
+ });
30
+ // Clear the queue
31
+ batchQueues.set(name, []);
32
+ batchPending.set(name, false);
33
+ // Update connection manager and notify all callbacks
34
+ connectionManager.updateState(name, newState);
35
+ callbacks?.forEach((callback) => callback(newState));
36
+ batchCallbacks.set(name, new Set());
37
+ });
38
+ }
5
39
  export function useMirrorState(name, initialValue) {
6
40
  // Capture initialValue once on first render to make it stable
7
- // This prevents re-renders when users pass inline objects/arrays
8
41
  const initialValueRef = useRef(initialValue);
9
42
  const [state, setState] = useState(() => INITIAL_STATES?.[name] ?? initialValueRef.current);
10
43
  const hasCreatedFile = useRef(false);
11
44
  useEffect(() => {
12
45
  // Subscribe to state changes for this name
13
- // Echo filtering happens in connection-manager using vector clocks
14
46
  const unsubscribe = connectionManager.subscribe(name, (newState) => {
15
47
  setState(newState);
16
48
  });
@@ -26,12 +58,17 @@ export function useMirrorState(name, initialValue) {
26
58
  };
27
59
  }, [name]);
28
60
  const updateMirrorState = (updater) => {
29
- const currentState = connectionManager.getCurrentState(name) ?? state;
30
- const newState = produce(currentState, updater);
31
- // Send update to connection manager (includes vector clock tracking)
32
- connectionManager.updateState(name, newState);
33
- // Optimistic local update for immediate UI feedback
34
- setState(newState);
61
+ // Initialize batch queue for this name if needed
62
+ if (!batchQueues.has(name)) {
63
+ batchQueues.set(name, []);
64
+ batchCallbacks.set(name, new Set());
65
+ }
66
+ // Add updater to batch queue
67
+ batchQueues.get(name).push(updater);
68
+ // Add setState to callbacks
69
+ batchCallbacks.get(name).add(setState);
70
+ // Schedule batch flush
71
+ scheduleBatchFlush(name);
35
72
  };
36
73
  return [state, updateMirrorState];
37
74
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-mirrorstate",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "React library for bidirectional state synchronization with MirrorState",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",