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.
- package/dist/connection-manager.d.ts +0 -1
- package/dist/connection-manager.js +25 -33
- package/dist/index.js +45 -8
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
107
|
+
// Debounce rapid WebSocket sends
|
|
114
108
|
const timeout = setTimeout(() => {
|
|
115
|
-
if (!this.ws
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
}
|