react-mirrorstate 0.1.0-alpha.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.
@@ -0,0 +1,22 @@
1
+ type StateListener = (state: any) => void;
2
+ declare class WebSocketConnectionManager {
3
+ private ws;
4
+ private isConnecting;
5
+ private listeners;
6
+ private currentStates;
7
+ private initialized;
8
+ private getWebSocketConfig;
9
+ private buildWebSocketURL;
10
+ connect(): Promise<void>;
11
+ getInlinedInitialStates(): Promise<Record<string, any>>;
12
+ loadInitialStateFromInline(name: string): Promise<any>;
13
+ subscribe(name: string, listener: StateListener): () => void;
14
+ private lastSentState;
15
+ private pendingUpdates;
16
+ updateState(name: string, state: any): void;
17
+ isInitialized(name: string): boolean;
18
+ getCurrentState(name: string): any;
19
+ private notifyListeners;
20
+ }
21
+ export declare const connectionManager: WebSocketConnectionManager;
22
+ export {};
@@ -0,0 +1,168 @@
1
+ import debug from "debug";
2
+ const logger = debug("mirrorstate:ws-manager");
3
+ class WebSocketConnectionManager {
4
+ ws = null;
5
+ isConnecting = false;
6
+ listeners = new Map();
7
+ currentStates = new Map();
8
+ initialized = new Set();
9
+ getWebSocketConfig() {
10
+ try {
11
+ const config = require("virtual:mirrorstate/config");
12
+ return config;
13
+ }
14
+ catch {
15
+ return { WS_PATH: "/mirrorstate" };
16
+ }
17
+ }
18
+ buildWebSocketURL(path) {
19
+ if (typeof window === "undefined") {
20
+ return `ws://localhost:5173${path}`;
21
+ }
22
+ const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
23
+ const host = window.location.host;
24
+ return `${protocol}//${host}${path}`;
25
+ }
26
+ async connect() {
27
+ if (this.ws?.readyState === WebSocket.OPEN || this.isConnecting) {
28
+ return;
29
+ }
30
+ if (process.env.NODE_ENV === "production") {
31
+ // In production, don't create WebSocket but still allow loading initial state
32
+ return;
33
+ }
34
+ this.isConnecting = true;
35
+ const config = this.getWebSocketConfig();
36
+ const wsUrl = this.buildWebSocketURL(config.WS_PATH);
37
+ logger(`Connecting to ${wsUrl}`);
38
+ this.ws = new WebSocket(wsUrl);
39
+ this.ws.onopen = () => {
40
+ this.isConnecting = false;
41
+ logger("WebSocket connected");
42
+ };
43
+ this.ws.onclose = () => {
44
+ this.isConnecting = false;
45
+ this.ws = null;
46
+ logger("WebSocket closed");
47
+ };
48
+ this.ws.onerror = () => {
49
+ this.isConnecting = false;
50
+ this.ws = null;
51
+ console.error("WebSocket error");
52
+ };
53
+ this.ws.onmessage = (event) => {
54
+ try {
55
+ const data = JSON.parse(event.data);
56
+ if (data.type === "initialState") {
57
+ this.currentStates.set(data.name, data.state);
58
+ this.initialized.add(data.name);
59
+ this.notifyListeners(data.name, data.state);
60
+ logger(`Initial state loaded: ${data.name}`, data.state);
61
+ }
62
+ if (data.type === "fileChange") {
63
+ this.currentStates.set(data.name, data.state);
64
+ this.notifyListeners(data.name, data.state);
65
+ logger(`State updated: ${data.name}`, data.state);
66
+ }
67
+ }
68
+ catch (error) {
69
+ console.error("Error handling server message:", error);
70
+ }
71
+ };
72
+ }
73
+ async getInlinedInitialStates() {
74
+ try {
75
+ // Import the virtual module with inlined states
76
+ const module = await import("virtual:mirrorstate/initial-states");
77
+ return module.INITIAL_STATES || {};
78
+ }
79
+ catch (error) {
80
+ logger("Failed to load virtual module:", error);
81
+ return {};
82
+ }
83
+ }
84
+ async loadInitialStateFromInline(name) {
85
+ // Always try to load inlined states - both in dev and production
86
+ const inlinedStates = await this.getInlinedInitialStates();
87
+ const state = inlinedStates[name];
88
+ if (state !== undefined) {
89
+ this.currentStates.set(name, state);
90
+ this.initialized.add(name);
91
+ logger(`Loaded inlined initial state: ${name}`, state);
92
+ return state;
93
+ }
94
+ return undefined;
95
+ }
96
+ subscribe(name, listener) {
97
+ if (!this.listeners.has(name)) {
98
+ this.listeners.set(name, new Set());
99
+ }
100
+ this.listeners.get(name).add(listener);
101
+ // Connect if not already connected (dev mode)
102
+ this.connect();
103
+ // If we already have state for this name, notify immediately
104
+ if (this.currentStates.has(name)) {
105
+ listener(this.currentStates.get(name));
106
+ }
107
+ else {
108
+ // Try to load from inlined states (production) or wait for WebSocket (dev)
109
+ this.loadInitialStateFromInline(name).then((state) => {
110
+ if (state !== undefined) {
111
+ this.notifyListeners(name, state);
112
+ }
113
+ });
114
+ }
115
+ return () => {
116
+ const nameListeners = this.listeners.get(name);
117
+ if (nameListeners) {
118
+ nameListeners.delete(listener);
119
+ if (nameListeners.size === 0) {
120
+ this.listeners.delete(name);
121
+ }
122
+ }
123
+ };
124
+ }
125
+ lastSentState = new Map();
126
+ pendingUpdates = new Map();
127
+ updateState(name, state) {
128
+ if (this.ws?.readyState !== WebSocket.OPEN) {
129
+ return;
130
+ }
131
+ // Cancel any pending update for this state name
132
+ const pendingUpdate = this.pendingUpdates.get(name);
133
+ if (pendingUpdate) {
134
+ clearTimeout(pendingUpdate);
135
+ }
136
+ // Check if this is actually a different state
137
+ const lastState = this.lastSentState.get(name);
138
+ if (lastState === state) {
139
+ logger(`Skipping duplicate state update for ${name}`);
140
+ return;
141
+ }
142
+ // Debounce rapid updates
143
+ const timeout = setTimeout(() => {
144
+ if (!this.ws) {
145
+ return;
146
+ }
147
+ this.ws.send(JSON.stringify({ name, state }));
148
+ this.currentStates.set(name, state);
149
+ this.lastSentState.set(name, state);
150
+ this.pendingUpdates.delete(name);
151
+ logger(`Sent state update for ${name}`);
152
+ }, 10);
153
+ this.pendingUpdates.set(name, timeout);
154
+ }
155
+ isInitialized(name) {
156
+ return this.initialized.has(name);
157
+ }
158
+ getCurrentState(name) {
159
+ return this.currentStates.get(name);
160
+ }
161
+ notifyListeners(name, state) {
162
+ const nameListeners = this.listeners.get(name);
163
+ if (nameListeners) {
164
+ nameListeners.forEach((listener) => listener(state));
165
+ }
166
+ }
167
+ }
168
+ export const connectionManager = new WebSocketConnectionManager();
@@ -0,0 +1,3 @@
1
+ import { Draft } from "immer";
2
+ export declare function useMirrorState<T>(name: string, initialValue: T): readonly [T, (updater: (draft: Draft<T>) => void) => void];
3
+ export default useMirrorState;
package/dist/index.js ADDED
@@ -0,0 +1,42 @@
1
+ import { useEffect, useState } from "react";
2
+ import { produce } from "immer";
3
+ import { connectionManager } from "./connection-manager";
4
+ export function useMirrorState(name, initialValue) {
5
+ const [state, setState] = useState(initialValue);
6
+ const [isInitialized, setIsInitialized] = useState(false);
7
+ // The connection manager handles both WebSocket (dev) and inlined state (production)
8
+ useEffect(() => {
9
+ // Subscribe to state changes for this name
10
+ const unsubscribe = connectionManager.subscribe(name, (newState) => {
11
+ setState(newState);
12
+ setIsInitialized(true);
13
+ });
14
+ // Check if already initialized and has current state
15
+ if (connectionManager.isInitialized(name)) {
16
+ const currentState = connectionManager.getCurrentState(name);
17
+ if (currentState !== undefined) {
18
+ setState(currentState);
19
+ setIsInitialized(true);
20
+ }
21
+ }
22
+ // Set a timeout to mark as initialized even if no file exists
23
+ const initTimeout = setTimeout(() => {
24
+ if (!isInitialized && !connectionManager.isInitialized(name)) {
25
+ setIsInitialized(true);
26
+ }
27
+ }, 100);
28
+ return () => {
29
+ unsubscribe();
30
+ clearTimeout(initTimeout);
31
+ };
32
+ }, [name, isInitialized]);
33
+ const updateMirrorState = (updater) => {
34
+ setState((prevState) => {
35
+ const newState = produce(prevState, updater);
36
+ connectionManager.updateState(name, newState);
37
+ return newState;
38
+ });
39
+ };
40
+ return [state, updateMirrorState];
41
+ }
42
+ export default useMirrorState;
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "react-mirrorstate",
3
+ "version": "0.1.0-alpha.0",
4
+ "description": "React library for bidirectional state synchronization with MirrorState",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "dev": "tsc --watch"
10
+ },
11
+ "keywords": [
12
+ "react",
13
+ "state",
14
+ "sync",
15
+ "mirrorstate"
16
+ ],
17
+ "author": "",
18
+ "license": "MIT",
19
+ "dependencies": {
20
+ "debug": "^4.4.1",
21
+ "immer": "^10.0.3"
22
+ },
23
+ "devDependencies": {
24
+ "@types/debug": "^4.1.12",
25
+ "@types/react": "^18.2.43",
26
+ "react": "^18.2.0",
27
+ "typescript": "^5.3.3"
28
+ },
29
+ "peerDependencies": {
30
+ "react": "^18.0.0"
31
+ },
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "publishConfig": {
36
+ "access": "public"
37
+ }
38
+ }