Skip to content

Commit de1d6d8

Browse files
authored
Connected view/src/StoredStateStore.tsx to patchConnection. (#77)
* feat(state): sync stored state with patch connection * feat: added defensive error handling * feat: fixed black screen issue * feat: cleaned the console logs and error handling
1 parent 4bf392c commit de1d6d8

File tree

2 files changed

+217
-3
lines changed

2 files changed

+217
-3
lines changed

view/src/StoredStateStore.test.tsx

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { vi } from "vitest";
2+
import React from "react";
3+
import { render, act } from "@testing-library/react";
4+
5+
// Declare mockPatch in outer scope so the mocked module can access the latest reference
6+
let mockPatch: any;
7+
8+
vi.mock("./common/patchConnection", () => ({
9+
getPatchConnection: () => mockPatch,
10+
}));
11+
12+
import { StoredStateStoreProvider, useStoredStateStore } from "./StoredStateStore";
13+
14+
// Helper component to drive updates
15+
const TestComponent: React.FC<{ onRender?: (api: any) => void }> = ({ onRender }) => {
16+
const store = useStoredStateStore();
17+
onRender?.(store);
18+
return null;
19+
};
20+
21+
describe("StoredStateStore patch integration", () => {
22+
let listeners: Record<string, Function[]>;
23+
24+
beforeEach(() => {
25+
listeners = {};
26+
mockPatch = {
27+
sendStoredStateValue: vi.fn(),
28+
requestStoredStateValue: vi.fn(),
29+
requestFullStoredState: vi.fn((cb) => {
30+
// simulate async callback with empty state
31+
setTimeout(() => cb({ values: {} }), 0);
32+
}),
33+
addStoredStateValueListener: vi.fn((fn) => {
34+
(listeners["state_key_value"] ||= []).push(fn);
35+
}),
36+
removeStoredStateValueListener: vi.fn((fn) => {
37+
listeners["state_key_value"] = (listeners["state_key_value"] || []).filter((l) => l !== fn);
38+
}),
39+
// utility to emit
40+
emit(key: string, value: any) {
41+
(listeners["state_key_value"] || []).forEach((l) => l({ key, value }));
42+
},
43+
};
44+
45+
// mock already registered above – just ensure module under test sees fresh mockPatch
46+
});
47+
48+
afterEach(() => {
49+
vi.resetModules();
50+
// Re-register mock after module reset (Vitest clears mocks); ensure StoredStateStore keeps working if re-imported.
51+
vi.doMock("./common/patchConnection", () => ({
52+
getPatchConnection: () => mockPatch,
53+
}));
54+
});
55+
56+
it("sends delta when updating local state", async () => {
57+
let api: any;
58+
render(
59+
<StoredStateStoreProvider>
60+
<TestComponent onRender={(a) => (api = a)} />
61+
</StoredStateStoreProvider>
62+
);
63+
64+
await act(async () => {
65+
api.updateStoredStateItem("selectedInstrument")("snare1");
66+
});
67+
68+
// should have sent stored state value for selectedInstrument
69+
expect(mockPatch.sendStoredStateValue).toHaveBeenCalledWith("selectedInstrument", "snare1");
70+
});
71+
72+
it("updates local state when patch emits change", async () => {
73+
let api: any;
74+
render(
75+
<StoredStateStoreProvider>
76+
<TestComponent onRender={(a) => (api = a)} />
77+
</StoredStateStoreProvider>
78+
);
79+
80+
await act(async () => {
81+
mockPatch.emit("selectedInstrument", "snare2");
82+
});
83+
84+
expect(api.storedState.selectedInstrument).toBe("snare2");
85+
});
86+
});

view/src/StoredStateStore.tsx

Lines changed: 131 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
* @todo: actually link to patchConnection
55
*/
66

7-
import React, { createContext, useState, useContext } from "react";
7+
import React, { createContext, useState, useContext, useEffect, useRef } from "react";
88
import { InstrumentKey, instrumentKeys } from "./params";
9-
9+
import { getPatchConnection } from "./common/patchConnection";
1010
export interface StoredState {
1111
selectedInstrument: InstrumentKey;
1212
}
@@ -37,9 +37,61 @@ export const StoredStateStoreProvider: React.FC<{
3737
children: React.ReactNode;
3838
}> = ({ children }) => {
3939
const [state, setState] = useState<StoredState>(initialState);
40+
// Keep a ref of last state so we can compare inside effects without stale closures
41+
const stateRef = useRef(state);
42+
useEffect(() => {
43+
stateRef.current = state;
44+
}, [state]);
45+
46+
// Push a delta to the patch connection for each updated key
47+
const sendStoredStateDelta = (delta: Partial<StoredState>) => {
48+
const patchConnection = getPatchConnection();
49+
if (!patchConnection) return;
50+
for (const k in delta) {
51+
const key = k as keyof StoredState;
52+
const value = delta[key];
53+
// Validate value before sending
54+
if (key === 'selectedInstrument' && value && !instrumentKeys.includes(value as InstrumentKey)) {
55+
console.warn(`Invalid selectedInstrument value: ${value}, skipping`);
56+
continue;
57+
}
58+
// Future-proof: allow null/undefined to clear
59+
patchConnection.sendStoredStateValue?.(key, value as any);
60+
}
61+
};
4062

4163
const setStoredState = (value: Partial<StoredState>) => {
42-
setState((prevState) => ({ ...prevState, ...value }));
64+
if (!value || Object.keys(value).length === 0) return;
65+
66+
// Validate incoming values
67+
const validatedValue: Partial<StoredState> = {};
68+
for (const k in value) {
69+
const key = k as keyof StoredState;
70+
const val = value[key];
71+
if (key === 'selectedInstrument') {
72+
if (val && instrumentKeys.includes(val as InstrumentKey)) {
73+
(validatedValue as any)[key] = val;
74+
} else {
75+
console.warn(`Invalid selectedInstrument: ${val}, keeping current value`);
76+
}
77+
} else {
78+
(validatedValue as any)[key] = val;
79+
}
80+
}
81+
82+
setState((prevState) => {
83+
const next = { ...prevState, ...validatedValue };
84+
// Send only changed keys
85+
const changed: Partial<StoredState> = {};
86+
for (const k in validatedValue) {
87+
const key = k as keyof StoredState;
88+
if (prevState[key] !== next[key]) (changed as any)[key] = next[key];
89+
}
90+
if (Object.keys(changed).length) {
91+
sendStoredStateDelta(changed);
92+
}
93+
return next;
94+
});
4395
};
4496

4597
const updateStoredStateItem: StoredStateUpdater = (key) => (value) =>
@@ -66,6 +118,82 @@ export const StoredStateStoreProvider: React.FC<{
66118
});
67119
};
68120

121+
// Initial sync + listener registration
122+
useEffect(() => {
123+
let patchConnection: any;
124+
try {
125+
patchConnection = getPatchConnection();
126+
} catch {
127+
return;
128+
}
129+
if (!patchConnection) return;
130+
131+
// Listener for individual key updates from patch
132+
const storedStateListener = ({ key, value }: { key: string; value: any }) => {
133+
if (!(key in stateRef.current)) return; // ignore keys we don't know yet
134+
const typedKey = key as keyof StoredState;
135+
136+
// Validate incoming value
137+
if (typedKey === 'selectedInstrument') {
138+
if (!value || !instrumentKeys.includes(value as InstrumentKey)) {
139+
return;
140+
}
141+
}
142+
143+
if (stateRef.current[typedKey] !== value) {
144+
setState((prev) => ({ ...prev, [typedKey]: value }));
145+
}
146+
};
147+
148+
try {
149+
patchConnection.addStoredStateValueListener?.(storedStateListener);
150+
} catch { }
151+
152+
// Request full state so we can merge existing values (if any) stored by host
153+
try {
154+
patchConnection.requestFullStoredState?.((full: any) => {
155+
try {
156+
const values = full?.values || {};
157+
const incoming: Partial<StoredState> = {};
158+
for (const k in values) {
159+
if (k in stateRef.current) {
160+
const key = k as keyof StoredState;
161+
const value = values[k];
162+
163+
// Validate values from patch
164+
if (key === 'selectedInstrument') {
165+
if (value && instrumentKeys.includes(value as InstrumentKey)) {
166+
(incoming as any)[key] = value;
167+
}
168+
} else {
169+
(incoming as any)[key] = value;
170+
}
171+
}
172+
}
173+
if (Object.keys(incoming).length) {
174+
setState((prev) => ({ ...prev, ...incoming }));
175+
} else {
176+
// If host has nothing, push our initial state so it becomes persisted
177+
sendStoredStateDelta(stateRef.current);
178+
}
179+
} catch { }
180+
});
181+
} catch { }
182+
183+
// Also request each individual key to trigger callbacks (mirrors ParamStore pattern)
184+
try {
185+
for (const key in stateRef.current) {
186+
patchConnection.requestStoredStateValue?.(key);
187+
}
188+
} catch { }
189+
190+
return () => {
191+
try {
192+
patchConnection.removeStoredStateValueListener?.(storedStateListener);
193+
} catch { }
194+
};
195+
}, []);
196+
69197
return (
70198
<StoredStateStoreContext.Provider
71199
value={{

0 commit comments

Comments
 (0)