react-mirrorstate 0.2.3 → 0.3.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,12 +4,14 @@ declare class WebSocketConnectionManager {
4
4
  private isConnecting;
5
5
  private listeners;
6
6
  private currentStates;
7
+ private clientId;
8
+ private lastSeq;
9
+ private queuedUpdates;
7
10
  private getWebSocketConfig;
8
11
  private buildWebSocketURL;
9
12
  private cleanup;
10
13
  connect(): Promise<void>;
11
14
  subscribe(name: string, listener: StateListener): () => void;
12
- private lastSentState;
13
15
  private pendingUpdates;
14
16
  updateState(name: string, state: any): void;
15
17
  getCurrentState(name: string): any;
@@ -3,6 +3,9 @@ class WebSocketConnectionManager {
3
3
  isConnecting = false;
4
4
  listeners = new Map();
5
5
  currentStates = new Map();
6
+ clientId = null;
7
+ lastSeq = new Map();
8
+ queuedUpdates = new Map();
6
9
  async getWebSocketConfig() {
7
10
  try {
8
11
  const config = await import("virtual:mirrorstate/config");
@@ -51,13 +54,23 @@ class WebSocketConnectionManager {
51
54
  this.ws.onmessage = (event) => {
52
55
  try {
53
56
  const data = JSON.parse(event.data);
54
- if (data.type === "initialState") {
55
- this.currentStates.set(data.name, data.state);
56
- this.notifyListeners(data.name, data.state);
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;
57
65
  }
58
66
  if (data.type === "fileChange") {
59
- this.currentStates.set(data.name, data.state);
60
- this.notifyListeners(data.name, data.state);
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
+ }
61
74
  }
62
75
  }
63
76
  catch (error) {
@@ -72,10 +85,10 @@ class WebSocketConnectionManager {
72
85
  this.listeners.get(name).add(listener);
73
86
  // Connect if not already connected (dev mode)
74
87
  this.connect();
75
- // If we already have state for this name, notify immediately
76
- if (this.currentStates.has(name)) {
77
- listener(this.currentStates.get(name));
78
- }
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.
79
92
  return () => {
80
93
  const nameListeners = this.listeners.get(name);
81
94
  if (nameListeners) {
@@ -86,10 +99,10 @@ class WebSocketConnectionManager {
86
99
  }
87
100
  };
88
101
  }
89
- lastSentState = new Map();
90
102
  pendingUpdates = new Map();
91
103
  updateState(name, state) {
92
- if (this.ws?.readyState !== WebSocket.OPEN) {
104
+ if (this.ws?.readyState !== WebSocket.OPEN || !this.clientId) {
105
+ this.queuedUpdates.set(name, state);
93
106
  return;
94
107
  }
95
108
  // Cancel any pending update for this state name
@@ -97,19 +110,17 @@ class WebSocketConnectionManager {
97
110
  if (pendingUpdate) {
98
111
  clearTimeout(pendingUpdate);
99
112
  }
100
- // Check if this is actually a different state
101
- const lastState = this.lastSentState.get(name);
102
- if (lastState === state) {
103
- return;
104
- }
105
113
  // Debounce rapid updates
106
114
  const timeout = setTimeout(() => {
107
- if (!this.ws) {
115
+ if (!this.ws || !this.clientId) {
108
116
  return;
109
117
  }
110
- this.ws.send(JSON.stringify({ name, state }));
118
+ this.ws.send(JSON.stringify({
119
+ clientId: this.clientId,
120
+ name,
121
+ state
122
+ }));
111
123
  this.currentStates.set(name, state);
112
- this.lastSentState.set(name, state);
113
124
  this.pendingUpdates.delete(name);
114
125
  }, 10);
115
126
  this.pendingUpdates.set(name, timeout);
package/dist/index.js CHANGED
@@ -3,30 +3,35 @@ import { produce } from "immer";
3
3
  import { connectionManager } from "./connection-manager";
4
4
  import { INITIAL_STATES } from "virtual:mirrorstate/initial-states";
5
5
  export function useMirrorState(name, initialValue) {
6
- const [state, setState] = useState(() => INITIAL_STATES?.[name] ?? initialValue);
6
+ // Capture initialValue once on first render to make it stable
7
+ // This prevents re-renders when users pass inline objects/arrays
8
+ const initialValueRef = useRef(initialValue);
9
+ const [state, setState] = useState(() => INITIAL_STATES?.[name] ?? initialValueRef.current);
7
10
  const hasCreatedFile = useRef(false);
8
11
  useEffect(() => {
9
12
  // Subscribe to state changes for this name
13
+ // Echo filtering happens in connection-manager using vector clocks
10
14
  const unsubscribe = connectionManager.subscribe(name, (newState) => {
11
15
  setState(newState);
12
16
  });
13
17
  // If file doesn't exist and initialValue was provided, create it
14
18
  if (INITIAL_STATES?.[name] === undefined &&
15
- initialValue !== undefined &&
19
+ initialValueRef.current !== undefined &&
16
20
  !hasCreatedFile.current) {
17
21
  hasCreatedFile.current = true;
18
- connectionManager.updateState(name, initialValue);
22
+ connectionManager.updateState(name, initialValueRef.current);
19
23
  }
20
24
  return () => {
21
25
  unsubscribe();
22
26
  };
23
- }, [name, initialValue]);
27
+ }, [name]);
24
28
  const updateMirrorState = (updater) => {
25
- setState((prevState) => {
26
- const newState = produce(prevState, updater);
27
- connectionManager.updateState(name, newState);
28
- return newState;
29
- });
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);
30
35
  };
31
36
  return [state, updateMirrorState];
32
37
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-mirrorstate",
3
- "version": "0.2.3",
3
+ "version": "0.3.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",