react-mirrorstate 0.2.1 → 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,11 +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;
12
+ private cleanup;
9
13
  connect(): Promise<void>;
10
14
  subscribe(name: string, listener: StateListener): () => void;
11
- private lastSentState;
12
15
  private pendingUpdates;
13
16
  updateState(name: string, state: any): void;
14
17
  getCurrentState(name: string): any;
@@ -1,10 +1,11 @@
1
- import debug from "debug";
2
- const logger = debug("mirrorstate:ws-manager");
3
1
  class WebSocketConnectionManager {
4
2
  ws = null;
5
3
  isConnecting = false;
6
4
  listeners = new Map();
7
5
  currentStates = new Map();
6
+ clientId = null;
7
+ lastSeq = new Map();
8
+ queuedUpdates = new Map();
8
9
  async getWebSocketConfig() {
9
10
  try {
10
11
  const config = await import("virtual:mirrorstate/config");
@@ -22,6 +23,12 @@ class WebSocketConnectionManager {
22
23
  const host = window.location.host;
23
24
  return `${protocol}//${host}${path}`;
24
25
  }
26
+ cleanup() {
27
+ this.isConnecting = false;
28
+ this.ws = null;
29
+ this.pendingUpdates.forEach((timeout) => clearTimeout(timeout));
30
+ this.pendingUpdates.clear();
31
+ }
25
32
  async connect() {
26
33
  if (this.ws?.readyState === WebSocket.OPEN || this.isConnecting) {
27
34
  return;
@@ -33,34 +40,37 @@ class WebSocketConnectionManager {
33
40
  this.isConnecting = true;
34
41
  const config = await this.getWebSocketConfig();
35
42
  const wsUrl = this.buildWebSocketURL(config.WS_PATH);
36
- logger(`Connecting to ${wsUrl}`);
37
43
  this.ws = new WebSocket(wsUrl);
38
44
  this.ws.onopen = () => {
39
45
  this.isConnecting = false;
40
- logger("WebSocket connected");
41
46
  };
42
47
  this.ws.onclose = () => {
43
- this.isConnecting = false;
44
- this.ws = null;
45
- logger("WebSocket closed");
48
+ this.cleanup();
46
49
  };
47
50
  this.ws.onerror = () => {
48
- this.isConnecting = false;
49
- this.ws = null;
50
51
  console.error("WebSocket error");
52
+ this.cleanup();
51
53
  };
52
54
  this.ws.onmessage = (event) => {
53
55
  try {
54
56
  const data = JSON.parse(event.data);
55
- if (data.type === "initialState") {
56
- this.currentStates.set(data.name, data.state);
57
- this.notifyListeners(data.name, data.state);
58
- logger(`Initial state loaded: ${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;
59
65
  }
60
66
  if (data.type === "fileChange") {
61
- this.currentStates.set(data.name, data.state);
62
- this.notifyListeners(data.name, data.state);
63
- logger(`State updated: ${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
+ }
64
74
  }
65
75
  }
66
76
  catch (error) {
@@ -75,10 +85,10 @@ class WebSocketConnectionManager {
75
85
  this.listeners.get(name).add(listener);
76
86
  // Connect if not already connected (dev mode)
77
87
  this.connect();
78
- // If we already have state for this name, notify immediately
79
- if (this.currentStates.has(name)) {
80
- listener(this.currentStates.get(name));
81
- }
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.
82
92
  return () => {
83
93
  const nameListeners = this.listeners.get(name);
84
94
  if (nameListeners) {
@@ -89,10 +99,10 @@ class WebSocketConnectionManager {
89
99
  }
90
100
  };
91
101
  }
92
- lastSentState = new Map();
93
102
  pendingUpdates = new Map();
94
103
  updateState(name, state) {
95
- if (this.ws?.readyState !== WebSocket.OPEN) {
104
+ if (this.ws?.readyState !== WebSocket.OPEN || !this.clientId) {
105
+ this.queuedUpdates.set(name, state);
96
106
  return;
97
107
  }
98
108
  // Cancel any pending update for this state name
@@ -100,22 +110,18 @@ class WebSocketConnectionManager {
100
110
  if (pendingUpdate) {
101
111
  clearTimeout(pendingUpdate);
102
112
  }
103
- // Check if this is actually a different state
104
- const lastState = this.lastSentState.get(name);
105
- if (lastState === state) {
106
- logger(`Skipping duplicate state update for ${name}`);
107
- return;
108
- }
109
113
  // Debounce rapid updates
110
114
  const timeout = setTimeout(() => {
111
- if (!this.ws) {
115
+ if (!this.ws || !this.clientId) {
112
116
  return;
113
117
  }
114
- this.ws.send(JSON.stringify({ name, state }));
118
+ this.ws.send(JSON.stringify({
119
+ clientId: this.clientId,
120
+ name,
121
+ state
122
+ }));
115
123
  this.currentStates.set(name, state);
116
- this.lastSentState.set(name, state);
117
124
  this.pendingUpdates.delete(name);
118
- logger(`Sent state update for ${name}`);
119
125
  }, 10);
120
126
  this.pendingUpdates.set(name, timeout);
121
127
  }
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.1",
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",
@@ -22,11 +22,9 @@
22
22
  "directory": "packages/react-mirrorstate"
23
23
  },
24
24
  "dependencies": {
25
- "debug": "^4.4.3",
26
25
  "immer": "^10.1.3"
27
26
  },
28
27
  "devDependencies": {
29
- "@types/debug": "^4.1.12",
30
28
  "@types/react": "^19.2.2",
31
29
  "react": "^19.2.0",
32
30
  "typescript": "^5.9.3"