react-mirrorstate 0.4.0 → 0.6.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 +4 -0
- package/dist/connection-manager.js +57 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +13 -9
- package/package.json +1 -1
|
@@ -6,6 +6,9 @@ declare class WebSocketConnectionManager {
|
|
|
6
6
|
private currentStates;
|
|
7
7
|
private lastSeq;
|
|
8
8
|
private queuedUpdates;
|
|
9
|
+
private getStorageObject;
|
|
10
|
+
private saveToLocalStorage;
|
|
11
|
+
loadFromLocalStorage(name: string): any | undefined;
|
|
9
12
|
private getWebSocketConfig;
|
|
10
13
|
private buildWebSocketURL;
|
|
11
14
|
private cleanup;
|
|
@@ -14,6 +17,7 @@ declare class WebSocketConnectionManager {
|
|
|
14
17
|
private pendingUpdates;
|
|
15
18
|
updateState(name: string, state: any): void;
|
|
16
19
|
getCurrentState(name: string): any;
|
|
20
|
+
getStatesHash(): string;
|
|
17
21
|
private notifyListeners;
|
|
18
22
|
}
|
|
19
23
|
export declare const connectionManager: WebSocketConnectionManager;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { STATES_HASH, PROJECT_NAME } from "virtual:mirrorstate/initial-states";
|
|
2
|
+
const LOCALSTORAGE_KEY = `mirrorstate:${PROJECT_NAME}`;
|
|
1
3
|
class WebSocketConnectionManager {
|
|
2
4
|
ws = null;
|
|
3
5
|
isConnecting = false;
|
|
@@ -5,6 +7,51 @@ class WebSocketConnectionManager {
|
|
|
5
7
|
currentStates = new Map();
|
|
6
8
|
lastSeq = new Map();
|
|
7
9
|
queuedUpdates = new Map();
|
|
10
|
+
getStorageObject() {
|
|
11
|
+
if (typeof window === "undefined" || !window.localStorage) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const stored = window.localStorage.getItem(LOCALSTORAGE_KEY);
|
|
16
|
+
if (stored !== null) {
|
|
17
|
+
const obj = JSON.parse(stored);
|
|
18
|
+
// Check hash - if mismatch, discard and return null
|
|
19
|
+
if (obj.__hash__ !== STATES_HASH) {
|
|
20
|
+
window.localStorage.removeItem(LOCALSTORAGE_KEY);
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
return obj;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Ignore parse errors
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
saveToLocalStorage(name, state) {
|
|
32
|
+
if (typeof window === "undefined" || !window.localStorage) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
// Load existing object or create new one
|
|
37
|
+
let obj = this.getStorageObject();
|
|
38
|
+
if (!obj) {
|
|
39
|
+
obj = { __hash__: STATES_HASH };
|
|
40
|
+
}
|
|
41
|
+
obj[name] = state;
|
|
42
|
+
window.localStorage.setItem(LOCALSTORAGE_KEY, JSON.stringify(obj));
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Ignore localStorage errors (quota exceeded, etc.)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
loadFromLocalStorage(name) {
|
|
49
|
+
const obj = this.getStorageObject();
|
|
50
|
+
if (obj && name in obj) {
|
|
51
|
+
return obj[name];
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
8
55
|
async getWebSocketConfig() {
|
|
9
56
|
try {
|
|
10
57
|
const config = await import("virtual:mirrorstate/config");
|
|
@@ -95,6 +142,13 @@ class WebSocketConnectionManager {
|
|
|
95
142
|
updateState(name, state) {
|
|
96
143
|
// Immediately update currentStates so subsequent reads get the latest value
|
|
97
144
|
this.currentStates.set(name, state);
|
|
145
|
+
// Notify all local subscribers immediately (for same-page component sync)
|
|
146
|
+
this.notifyListeners(name, state);
|
|
147
|
+
// In production, persist to localStorage instead of WebSocket
|
|
148
|
+
if (process.env.NODE_ENV === "production") {
|
|
149
|
+
this.saveToLocalStorage(name, state);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
98
152
|
if (this.ws?.readyState !== WebSocket.OPEN) {
|
|
99
153
|
this.queuedUpdates.set(name, state);
|
|
100
154
|
return;
|
|
@@ -120,6 +174,9 @@ class WebSocketConnectionManager {
|
|
|
120
174
|
getCurrentState(name) {
|
|
121
175
|
return this.currentStates.get(name);
|
|
122
176
|
}
|
|
177
|
+
getStatesHash() {
|
|
178
|
+
return STATES_HASH;
|
|
179
|
+
}
|
|
123
180
|
notifyListeners(name, state) {
|
|
124
181
|
const nameListeners = this.listeners.get(name);
|
|
125
182
|
if (nameListeners) {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { Draft } from "immer";
|
|
1
|
+
import { produce, type Draft } from "immer";
|
|
2
|
+
export { type Draft, type Immutable } from "immer";
|
|
3
|
+
export { produce };
|
|
2
4
|
export declare function useMirrorState<T>(name: string): [T | undefined, (updater: (draft: Draft<T>) => void) => void];
|
|
3
5
|
export declare function useMirrorState<T>(name: string, initialValue: T): [T, (updater: (draft: Draft<T>) => void) => void];
|
|
4
6
|
export default useMirrorState;
|
package/dist/index.js
CHANGED
|
@@ -2,10 +2,10 @@ 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
|
+
export { produce };
|
|
5
6
|
// Batching state for each mirror state name
|
|
6
7
|
const batchQueues = new Map();
|
|
7
8
|
const batchPending = new Map();
|
|
8
|
-
const batchCallbacks = new Map();
|
|
9
9
|
function scheduleBatchFlush(name) {
|
|
10
10
|
if (batchPending.get(name)) {
|
|
11
11
|
return;
|
|
@@ -13,7 +13,6 @@ function scheduleBatchFlush(name) {
|
|
|
13
13
|
batchPending.set(name, true);
|
|
14
14
|
queueMicrotask(() => {
|
|
15
15
|
const queue = batchQueues.get(name);
|
|
16
|
-
const callbacks = batchCallbacks.get(name);
|
|
17
16
|
if (!queue || queue.length === 0) {
|
|
18
17
|
batchPending.set(name, false);
|
|
19
18
|
return;
|
|
@@ -30,16 +29,24 @@ function scheduleBatchFlush(name) {
|
|
|
30
29
|
// Clear the queue
|
|
31
30
|
batchQueues.set(name, []);
|
|
32
31
|
batchPending.set(name, false);
|
|
33
|
-
// Update connection manager
|
|
32
|
+
// Update connection manager (which notifies all subscribers)
|
|
34
33
|
connectionManager.updateState(name, newState);
|
|
35
|
-
callbacks?.forEach((callback) => callback(newState));
|
|
36
|
-
batchCallbacks.set(name, new Set());
|
|
37
34
|
});
|
|
38
35
|
}
|
|
39
36
|
export function useMirrorState(name, initialValue) {
|
|
40
37
|
// Capture initialValue once on first render to make it stable
|
|
41
38
|
const initialValueRef = useRef(initialValue);
|
|
42
|
-
const [state, setState] = useState(() =>
|
|
39
|
+
const [state, setState] = useState(() => {
|
|
40
|
+
// In production, check localStorage first for persisted state
|
|
41
|
+
if (process.env.NODE_ENV === "production") {
|
|
42
|
+
const storedState = connectionManager.loadFromLocalStorage(name);
|
|
43
|
+
if (storedState !== undefined) {
|
|
44
|
+
return storedState;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Fall back to build-time initial states or user-provided initialValue
|
|
48
|
+
return INITIAL_STATES?.[name] ?? initialValueRef.current;
|
|
49
|
+
});
|
|
43
50
|
const hasCreatedFile = useRef(false);
|
|
44
51
|
useEffect(() => {
|
|
45
52
|
// Subscribe to state changes for this name
|
|
@@ -61,12 +68,9 @@ export function useMirrorState(name, initialValue) {
|
|
|
61
68
|
// Initialize batch queue for this name if needed
|
|
62
69
|
if (!batchQueues.has(name)) {
|
|
63
70
|
batchQueues.set(name, []);
|
|
64
|
-
batchCallbacks.set(name, new Set());
|
|
65
71
|
}
|
|
66
72
|
// Add updater to batch queue
|
|
67
73
|
batchQueues.get(name).push(updater);
|
|
68
|
-
// Add setState to callbacks
|
|
69
|
-
batchCallbacks.get(name).add(setState);
|
|
70
74
|
// Schedule batch flush
|
|
71
75
|
scheduleBatchFlush(name);
|
|
72
76
|
};
|