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.
@@ -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 and notify all callbacks
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(() => INITIAL_STATES?.[name] ?? initialValueRef.current);
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-mirrorstate",
3
- "version": "0.4.0",
3
+ "version": "0.6.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",