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.
- package/dist/connection-manager.d.ts +3 -1
- package/dist/connection-manager.js +30 -19
- package/dist/index.js +14 -9
- package/package.json +1 -1
|
@@ -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 === "
|
|
55
|
-
this.
|
|
56
|
-
|
|
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
|
-
|
|
60
|
-
this.
|
|
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
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
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({
|
|
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
|
-
|
|
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
|
-
|
|
19
|
+
initialValueRef.current !== undefined &&
|
|
16
20
|
!hasCreatedFile.current) {
|
|
17
21
|
hasCreatedFile.current = true;
|
|
18
|
-
connectionManager.updateState(name,
|
|
22
|
+
connectionManager.updateState(name, initialValueRef.current);
|
|
19
23
|
}
|
|
20
24
|
return () => {
|
|
21
25
|
unsubscribe();
|
|
22
26
|
};
|
|
23
|
-
}, [name
|
|
27
|
+
}, [name]);
|
|
24
28
|
const updateMirrorState = (updater) => {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
}
|