@weasel-js/labkit 1.1.0 → 1.2.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/README.md +27 -0
- package/dist/_dts/{DrawCommand-BkZztJsW.d.ts → DrawCommand-C_XboUpz.d.ts} +48 -6
- package/dist/_dts/{index-iAP6XbH3.d.ts → index-JFAYj5Tv.d.ts} +12 -0
- package/dist/_dts/types-DJ79Tg5J.d.ts +56 -0
- package/dist/_dts/{useTrialState-DYe2vUwN.d.ts → useTrialState-BMNIx3Cy.d.ts} +5 -9
- package/dist/canvas/index.d.ts +2 -1
- package/dist/{chunk-C6GJKPUI.js → chunk-73KA7WBO.js} +69 -63
- package/dist/chunk-73KA7WBO.js.map +1 -0
- package/dist/{chunk-DJLDIRFN.js → chunk-BOHF3PQO.js} +4375 -3720
- package/dist/chunk-BOHF3PQO.js.map +1 -0
- package/dist/{chunk-NRKWVTVT.js → chunk-G5TJVQQT.js} +3 -3
- package/dist/{chunk-NRKWVTVT.js.map → chunk-G5TJVQQT.js.map} +1 -1
- package/dist/chunk-LN6JDUGB.js +106 -0
- package/dist/chunk-LN6JDUGB.js.map +1 -0
- package/dist/chunk-THBG7FQZ.js +167 -0
- package/dist/chunk-THBG7FQZ.js.map +1 -0
- package/dist/{chunk-3TYUJR7Z.js → chunk-VUU5UXHE.js} +6 -3
- package/dist/chunk-VUU5UXHE.js.map +1 -0
- package/dist/dragdrop/index.d.ts +2 -1
- package/dist/index.d.ts +71 -11
- package/dist/index.js +165 -22
- package/dist/index.js.map +1 -1
- package/dist/job/index.d.ts +13 -0
- package/dist/job/index.js +3 -0
- package/dist/job/index.js.map +1 -0
- package/dist/layers/index.d.ts +3 -2
- package/dist/passthrough/weasel-canvas.d.ts +1 -1
- package/dist/passthrough/weasel-canvas.js +1 -1
- package/dist/passthrough/weasel-ui.d.ts +20 -4
- package/dist/passthrough/weasel-ui.js +2 -2
- package/dist/state/index.d.ts +2 -2
- package/dist/state/index.js +2 -2
- package/dist/styles.css +10 -0
- package/dist/surface/index.d.ts +77 -0
- package/dist/surface/index.js +3 -0
- package/dist/surface/index.js.map +1 -0
- package/dist/ui/layers/index.js +3 -3
- package/dist/undo/index.d.ts +2 -1
- package/package.json +9 -1
- package/src/canvas/AGENTS.md +8 -0
- package/src/canvas/useOrbit.test.ts +71 -0
- package/src/canvas/useOrbit.ts +149 -0
- package/src/index.test.ts +18 -0
- package/src/index.ts +13 -0
- package/src/instrument/SineWave.smoke.test.tsx +2 -1
- package/src/instrument/types.ts +13 -1
- package/src/job/index.ts +3 -0
- package/src/job/types.ts +47 -0
- package/src/job/useJob.test.tsx +210 -0
- package/src/job/useJob.ts +134 -0
- package/src/lab/Lab.stories.tsx +1 -2
- package/src/lab/Workspace.surface.test.tsx +49 -0
- package/src/lab/Workspace.tsx +14 -1
- package/src/state/store.ts +8 -2
- package/src/state/types.ts +4 -2
- package/src/state/view.test.ts +127 -0
- package/src/state/view.ts +18 -0
- package/src/surface/AGENTS.md +64 -0
- package/src/surface/SurfaceContext.ts +5 -0
- package/src/surface/composeRects.test.ts +50 -0
- package/src/surface/composeRects.ts +19 -0
- package/src/surface/deviceRect.test.ts +40 -0
- package/src/surface/deviceRect.ts +19 -0
- package/src/surface/index.ts +7 -0
- package/src/surface/rect.ts +16 -0
- package/src/surface/useSurfaceTile.test.tsx +67 -0
- package/src/surface/useSurfaceTile.ts +32 -0
- package/src/surface/useTiledSurface.test.tsx +231 -0
- package/src/surface/useTiledSurface.ts +157 -0
- package/src/trial/DefaultStatusBar.tsx +2 -2
- package/src/trial/Trial.job.test.tsx +75 -0
- package/src/trial/Trial.less +12 -0
- package/src/trial/Trial.stories.tsx +8 -1
- package/src/trial/Trial.tsx +32 -6
- package/src/trial/TrialChrome.tsx +33 -6
- package/src/trial/slotTypes.ts +2 -1
- package/dist/chunk-3TYUJR7Z.js.map +0 -1
- package/dist/chunk-C6GJKPUI.js.map +0 -1
- package/dist/chunk-DJLDIRFN.js.map +0 -1
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import { composeRects, rectsEqual } from './composeRects';
|
|
3
|
+
import type { Box, Rect } from './rect';
|
|
4
|
+
|
|
5
|
+
/** What a surface owner is handed once per animation frame. `rects` carries every
|
|
6
|
+
* tile, not only the dirty ones: a scissored draw has to know where it is drawing
|
|
7
|
+
* relative to a surface that may have resized under it. */
|
|
8
|
+
export interface SurfaceFrame {
|
|
9
|
+
dirty: ReadonlySet<string>;
|
|
10
|
+
rects: ReadonlyMap<string, Rect>;
|
|
11
|
+
dpr: number;
|
|
12
|
+
size: { width: number; height: number };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The invalidators and the two ref callbacks that publish geometry. */
|
|
16
|
+
export interface SurfaceHandle {
|
|
17
|
+
/** Mark one tile for redraw. */
|
|
18
|
+
invalidate: (id: string) => void;
|
|
19
|
+
/** Mark every tile — what a resize or a tile-set change means. */
|
|
20
|
+
invalidateAll: () => void;
|
|
21
|
+
/** Re-measure before the next frame. The escape hatch for a host that knows it
|
|
22
|
+
* moved something a ResizeObserver cannot see. */
|
|
23
|
+
invalidateRects: () => void;
|
|
24
|
+
registerTile: (id: string, el: HTMLElement | null) => void;
|
|
25
|
+
containerRef: (el: HTMLElement | null) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface UseTiledSurfaceOptions {
|
|
29
|
+
onFrame: (frame: SurfaceFrame) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function useTiledSurface({ onFrame }: UseTiledSurfaceOptions): SurfaceHandle {
|
|
33
|
+
const container = useRef<HTMLElement | null>(null);
|
|
34
|
+
const tiles = useRef(new Map<string, HTMLElement>());
|
|
35
|
+
const rects = useRef(new Map<string, Rect>());
|
|
36
|
+
const dirty = useRef(new Set<string>());
|
|
37
|
+
const needsMeasure = useRef(true);
|
|
38
|
+
const raf = useRef(0);
|
|
39
|
+
const observer = useRef<ResizeObserver | null>(null);
|
|
40
|
+
const lastDpr = useRef(0);
|
|
41
|
+
|
|
42
|
+
// Held in a ref so a caller passing an inline closure does not re-create every
|
|
43
|
+
// callback below on each render.
|
|
44
|
+
const onFrameRef = useRef(onFrame);
|
|
45
|
+
onFrameRef.current = onFrame;
|
|
46
|
+
|
|
47
|
+
const measure = useCallback((): boolean => {
|
|
48
|
+
const el = container.current;
|
|
49
|
+
if (!el) return false;
|
|
50
|
+
const boxes = new Map<string, Box>();
|
|
51
|
+
for (const [id, tile] of tiles.current) boxes.set(id, tile.getBoundingClientRect());
|
|
52
|
+
const next = composeRects(el.getBoundingClientRect(), boxes);
|
|
53
|
+
let changed = next.size !== rects.current.size;
|
|
54
|
+
for (const [id, rect] of next) {
|
|
55
|
+
if (!rectsEqual(rects.current.get(id), rect)) changed = true;
|
|
56
|
+
}
|
|
57
|
+
rects.current = next;
|
|
58
|
+
return changed;
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
const schedule = useCallback(() => {
|
|
62
|
+
if (raf.current) return;
|
|
63
|
+
raf.current = requestAnimationFrame(() => {
|
|
64
|
+
raf.current = 0;
|
|
65
|
+
const el = container.current;
|
|
66
|
+
if (!el) {
|
|
67
|
+
dirty.current.clear();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (needsMeasure.current) {
|
|
71
|
+
needsMeasure.current = false;
|
|
72
|
+
if (measure()) for (const id of rects.current.keys()) dirty.current.add(id);
|
|
73
|
+
}
|
|
74
|
+
const dpr = globalThis.devicePixelRatio ?? 1;
|
|
75
|
+
if (dpr !== lastDpr.current) {
|
|
76
|
+
lastDpr.current = dpr;
|
|
77
|
+
for (const id of rects.current.keys()) dirty.current.add(id);
|
|
78
|
+
}
|
|
79
|
+
if (dirty.current.size === 0) return;
|
|
80
|
+
const box = el.getBoundingClientRect();
|
|
81
|
+
onFrameRef.current({
|
|
82
|
+
dirty: new Set(dirty.current),
|
|
83
|
+
rects: new Map(rects.current),
|
|
84
|
+
dpr,
|
|
85
|
+
size: { width: box.width, height: box.height },
|
|
86
|
+
});
|
|
87
|
+
dirty.current.clear();
|
|
88
|
+
});
|
|
89
|
+
}, [measure]);
|
|
90
|
+
|
|
91
|
+
const invalidate = useCallback(
|
|
92
|
+
(id: string) => {
|
|
93
|
+
dirty.current.add(id);
|
|
94
|
+
schedule();
|
|
95
|
+
},
|
|
96
|
+
[schedule],
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const invalidateAll = useCallback(() => {
|
|
100
|
+
for (const id of tiles.current.keys()) dirty.current.add(id);
|
|
101
|
+
schedule();
|
|
102
|
+
}, [schedule]);
|
|
103
|
+
|
|
104
|
+
const invalidateRects = useCallback(() => {
|
|
105
|
+
needsMeasure.current = true;
|
|
106
|
+
schedule();
|
|
107
|
+
}, [schedule]);
|
|
108
|
+
|
|
109
|
+
const registerTile = useCallback(
|
|
110
|
+
(id: string, el: HTMLElement | null) => {
|
|
111
|
+
const known = tiles.current.get(id);
|
|
112
|
+
if (known === el) return;
|
|
113
|
+
if (known) observer.current?.unobserve(known);
|
|
114
|
+
if (el) {
|
|
115
|
+
tiles.current.set(id, el);
|
|
116
|
+
observer.current?.observe(el);
|
|
117
|
+
} else {
|
|
118
|
+
tiles.current.delete(id);
|
|
119
|
+
rects.current.delete(id);
|
|
120
|
+
dirty.current.delete(id);
|
|
121
|
+
}
|
|
122
|
+
needsMeasure.current = true;
|
|
123
|
+
schedule();
|
|
124
|
+
},
|
|
125
|
+
[schedule],
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const containerRef = useCallback(
|
|
129
|
+
(el: HTMLElement | null) => {
|
|
130
|
+
if (container.current === el) return;
|
|
131
|
+
if (container.current) observer.current?.unobserve(container.current);
|
|
132
|
+
container.current = el;
|
|
133
|
+
if (el) observer.current?.observe(el);
|
|
134
|
+
needsMeasure.current = true;
|
|
135
|
+
schedule();
|
|
136
|
+
},
|
|
137
|
+
[schedule],
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
const ro = new ResizeObserver(() => {
|
|
142
|
+
needsMeasure.current = true;
|
|
143
|
+
schedule();
|
|
144
|
+
});
|
|
145
|
+
observer.current = ro;
|
|
146
|
+
if (container.current) ro.observe(container.current);
|
|
147
|
+
for (const el of tiles.current.values()) ro.observe(el);
|
|
148
|
+
return () => {
|
|
149
|
+
ro.disconnect();
|
|
150
|
+
observer.current = null;
|
|
151
|
+
if (raf.current) cancelAnimationFrame(raf.current);
|
|
152
|
+
raf.current = 0;
|
|
153
|
+
};
|
|
154
|
+
}, [schedule]);
|
|
155
|
+
|
|
156
|
+
return { invalidate, invalidateAll, invalidateRects, registerTile, containerRef };
|
|
157
|
+
}
|
|
@@ -8,12 +8,12 @@ export interface DefaultStatusBarProps {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
/** The status bar a trial renders when no `statusBar` slot is supplied:
|
|
11
|
-
* the instrument's name and the
|
|
11
|
+
* the instrument's name, and the zoom when the trial's view has one. */
|
|
12
12
|
export function DefaultStatusBar({ ctx }: DefaultStatusBarProps) {
|
|
13
13
|
return (
|
|
14
14
|
<StatusBar>
|
|
15
15
|
<StatusBar.Section>{ctx.instrumentName}</StatusBar.Section>
|
|
16
|
-
<StatusBar.Section>{formatZoom(ctx.zoom)}</StatusBar.Section>
|
|
16
|
+
{ctx.zoom === null ? null : <StatusBar.Section>{formatZoom(ctx.zoom)}</StatusBar.Section>}
|
|
17
17
|
</StatusBar>
|
|
18
18
|
);
|
|
19
19
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { render, waitFor } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { defineInstrument } from '../instrument/defineInstrument';
|
|
5
|
+
import type { JobEvent } from '../job/types';
|
|
6
|
+
import { Lab } from '../lab/Lab';
|
|
7
|
+
|
|
8
|
+
interface S {
|
|
9
|
+
items: number[];
|
|
10
|
+
}
|
|
11
|
+
interface C {
|
|
12
|
+
n: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function makeInstrument(name: string, n: number, delay: number) {
|
|
16
|
+
return defineInstrument<S, C>({
|
|
17
|
+
name,
|
|
18
|
+
defaultConfig: () => ({ n }),
|
|
19
|
+
initialState: () => ({ items: [] }),
|
|
20
|
+
render: () => null,
|
|
21
|
+
job: {
|
|
22
|
+
auto: true,
|
|
23
|
+
run: async function* ({ config, signal }): AsyncGenerator<JobEvent<never>> {
|
|
24
|
+
yield { kind: 'total', total: config.n };
|
|
25
|
+
for (let i = 0; i < config.n; i++) {
|
|
26
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
27
|
+
if (signal.aborted) return;
|
|
28
|
+
yield { kind: 'item', item: i as never };
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
onItem: (item, state) => ({ items: [...state.items, item as number] }),
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('a trial whose instrument declares a job', () => {
|
|
37
|
+
it('shows progress in the trial chrome', async () => {
|
|
38
|
+
const fast = makeInstrument('fast', 3, 1);
|
|
39
|
+
render(<Lab instruments={[fast]} defaultInstrument="fast" storage={null} />);
|
|
40
|
+
await waitFor(() => {
|
|
41
|
+
expect(document.querySelector('.lk-trial__job')).toBeInTheDocument();
|
|
42
|
+
});
|
|
43
|
+
await waitFor(() => {
|
|
44
|
+
expect(document.querySelector('.lk-trial__job-count')?.textContent).toMatch(/3\s*\/\s*3/);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('offers a cancel control while running, and drops it once cancelled', async () => {
|
|
49
|
+
const slow = makeInstrument('slow', 40, 20);
|
|
50
|
+
render(<Lab instruments={[slow]} defaultInstrument="slow" storage={null} />);
|
|
51
|
+
const cancel = await waitFor(() => {
|
|
52
|
+
const el = document.querySelector('.lk-trial__job-cancel');
|
|
53
|
+
expect(el).toBeInTheDocument();
|
|
54
|
+
return el as HTMLElement;
|
|
55
|
+
});
|
|
56
|
+
await userEvent.click(cancel);
|
|
57
|
+
await waitFor(() => {
|
|
58
|
+
expect(document.querySelector('.lk-trial__job-cancel')).not.toBeInTheDocument();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('renders no job chrome for an instrument that declares none', async () => {
|
|
63
|
+
const plain = defineInstrument<S, C>({
|
|
64
|
+
name: 'plain',
|
|
65
|
+
defaultConfig: () => ({ n: 0 }),
|
|
66
|
+
initialState: () => ({ items: [] }),
|
|
67
|
+
render: () => null,
|
|
68
|
+
});
|
|
69
|
+
render(<Lab instruments={[plain]} defaultInstrument="plain" storage={null} />);
|
|
70
|
+
await waitFor(() => {
|
|
71
|
+
expect(document.querySelector('.lk-trial')).toBeInTheDocument();
|
|
72
|
+
});
|
|
73
|
+
expect(document.querySelector('.lk-trial__job')).not.toBeInTheDocument();
|
|
74
|
+
});
|
|
75
|
+
});
|
package/src/trial/Trial.less
CHANGED
|
@@ -56,3 +56,15 @@
|
|
|
56
56
|
color: var(--wzl-fg-muted);
|
|
57
57
|
font-size: var(--wzl-font-size-sm);
|
|
58
58
|
}
|
|
59
|
+
|
|
60
|
+
.lk-trial__job {
|
|
61
|
+
display: flex;
|
|
62
|
+
gap: 8px;
|
|
63
|
+
align-items: center;
|
|
64
|
+
font-size: 0.75rem;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.lk-trial__job-failures,
|
|
68
|
+
.lk-trial__job-error {
|
|
69
|
+
color: var(--wsl-color-danger, #c0392b);
|
|
70
|
+
}
|
|
@@ -5,6 +5,7 @@ import { noneAdapter } from '../state/adapters';
|
|
|
5
5
|
import { LabStoreContext } from '../state/context';
|
|
6
6
|
import { createLabStore } from '../state/store';
|
|
7
7
|
import type { TrialRecord } from '../state/types';
|
|
8
|
+
import { DEFAULT_VIEW } from '../state/view';
|
|
8
9
|
import { TrialChrome } from './TrialChrome';
|
|
9
10
|
|
|
10
11
|
const noop = () => {};
|
|
@@ -52,7 +53,13 @@ function Harness() {
|
|
|
52
53
|
config: {},
|
|
53
54
|
setState: () => {},
|
|
54
55
|
setConfig: () => {},
|
|
55
|
-
trial: {
|
|
56
|
+
trial: {
|
|
57
|
+
id: 'ws-demo',
|
|
58
|
+
view: DEFAULT_VIEW,
|
|
59
|
+
setView: () => {},
|
|
60
|
+
zoom: 1,
|
|
61
|
+
setZoom: () => {},
|
|
62
|
+
},
|
|
56
63
|
emit: () => {},
|
|
57
64
|
})}
|
|
58
65
|
</TrialChrome>
|
package/src/trial/Trial.tsx
CHANGED
|
@@ -9,13 +9,14 @@ import type {
|
|
|
9
9
|
LayerDescriptor,
|
|
10
10
|
PaletteItem,
|
|
11
11
|
RenderContext,
|
|
12
|
-
ViewTransform,
|
|
13
12
|
} from '../instrument/types';
|
|
13
|
+
import { useJob } from '../job/useJob';
|
|
14
14
|
import { useLabContext } from '../lab/LabContext';
|
|
15
15
|
import { LayerList } from '../layers/LayerList';
|
|
16
16
|
import { LabStoreContext } from '../state/context';
|
|
17
17
|
import type { LabStore } from '../state/store';
|
|
18
18
|
import type { TrialRecord } from '../state/types';
|
|
19
|
+
import { as2DView, DEFAULT_VIEW } from '../state/view';
|
|
19
20
|
import { createEventBus, type EventBus } from '../undo/eventBus';
|
|
20
21
|
import { pushSnapshot, redo as undoRedo, undo as undoUndo } from '../undo/undoStack';
|
|
21
22
|
import type { UndoBindings } from './TrialChrome';
|
|
@@ -85,7 +86,21 @@ function TrialRuntime({ record, instrument, store, isLast }: TrialRuntimeProps)
|
|
|
85
86
|
updateTrialUndoStack(record.id, (prev) => pushSnapshot(prev, snap, maxDepth));
|
|
86
87
|
};
|
|
87
88
|
|
|
88
|
-
const setView = (v:
|
|
89
|
+
const setView = (v: unknown): void => updateTrialView(record.id, v);
|
|
90
|
+
|
|
91
|
+
// `CanvasStack` and the zoom controls are 2D; a trial holding another view shape
|
|
92
|
+
// gets the default here and simply never renders them.
|
|
93
|
+
const view2d = as2DView(record.view);
|
|
94
|
+
|
|
95
|
+
const jobCap = instrument.job;
|
|
96
|
+
// Hooks cannot be conditional, so a trial without the capability still calls
|
|
97
|
+
// this — with a runner that yields nothing and is never started.
|
|
98
|
+
const job = useJob({
|
|
99
|
+
capability: jobCap ?? { run: async function* () {}, onItem: (_i, st) => st },
|
|
100
|
+
config: record.config,
|
|
101
|
+
state: record.state,
|
|
102
|
+
setState: (next) => updateTrialState(record.id, next as never),
|
|
103
|
+
});
|
|
89
104
|
|
|
90
105
|
const renderCtx: RenderContext<unknown, unknown> = {
|
|
91
106
|
state: record.state,
|
|
@@ -105,13 +120,19 @@ function TrialRuntime({ record, instrument, store, isLast }: TrialRuntimeProps)
|
|
|
105
120
|
},
|
|
106
121
|
trial: {
|
|
107
122
|
id: record.id,
|
|
108
|
-
|
|
109
|
-
|
|
123
|
+
view: record.view,
|
|
124
|
+
setView,
|
|
125
|
+
zoom: view2d?.zoom ?? 1,
|
|
126
|
+
setZoom: (z) => {
|
|
127
|
+
if (!view2d) return;
|
|
128
|
+
updateTrialView(record.id, { ...view2d, zoom: z });
|
|
129
|
+
},
|
|
110
130
|
},
|
|
111
131
|
emit: (event) => {
|
|
112
132
|
snapshotIfNeeded(event);
|
|
113
133
|
bus.emit(event);
|
|
114
134
|
},
|
|
135
|
+
job: jobCap ? job : undefined,
|
|
115
136
|
};
|
|
116
137
|
|
|
117
138
|
const undoBindings: UndoBindings | undefined = undoCap
|
|
@@ -160,7 +181,7 @@ function TrialRuntime({ record, instrument, store, isLast }: TrialRuntimeProps)
|
|
|
160
181
|
const dragDropResult = useDragDrop({
|
|
161
182
|
capability: instrument.dragDrop ?? { palette: [], onDrop: (_p, _i, s) => s },
|
|
162
183
|
canvasContainerRef,
|
|
163
|
-
view:
|
|
184
|
+
view: view2d ?? DEFAULT_VIEW,
|
|
164
185
|
state: record.state,
|
|
165
186
|
config: record.config,
|
|
166
187
|
setState: (next) => {
|
|
@@ -212,7 +233,11 @@ function TrialRuntime({ record, instrument, store, isLast }: TrialRuntimeProps)
|
|
|
212
233
|
className="lk-trial__canvas-host"
|
|
213
234
|
style={{ width: '100%', height: '100%', position: 'relative' }}
|
|
214
235
|
>
|
|
215
|
-
<CanvasStack
|
|
236
|
+
<CanvasStack
|
|
237
|
+
layers={layersWithFeedback}
|
|
238
|
+
view={view2d ?? DEFAULT_VIEW}
|
|
239
|
+
onViewChange={setView}
|
|
240
|
+
>
|
|
216
241
|
{instrument.render(renderCtx)}
|
|
217
242
|
</CanvasStack>
|
|
218
243
|
<DragOverlay drag={dragDropResult.drag} />
|
|
@@ -245,6 +270,7 @@ function TrialRuntime({ record, instrument, store, isLast }: TrialRuntimeProps)
|
|
|
245
270
|
|
|
246
271
|
return (
|
|
247
272
|
<TrialChrome
|
|
273
|
+
job={jobCap ? job : undefined}
|
|
248
274
|
trialId={record.id}
|
|
249
275
|
record={record}
|
|
250
276
|
instrument={instrument}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { type KeyboardEvent, type ReactNode, useContext, useMemo } from 'react';
|
|
2
2
|
import { useStore } from 'zustand/react';
|
|
3
3
|
import type { Instrument } from '../instrument/types';
|
|
4
|
+
import type { JobHandle } from '../job/types';
|
|
4
5
|
import { useLabContext } from '../lab/LabContext';
|
|
5
6
|
import { LabStoreContext } from '../state/context';
|
|
6
7
|
import type { TrialRecord } from '../state/types';
|
|
8
|
+
import { as2DView } from '../state/view';
|
|
7
9
|
import { DefaultSidebar } from './DefaultSidebar';
|
|
8
10
|
import { DefaultStatusBar } from './DefaultStatusBar';
|
|
9
11
|
import { DefaultToolbar } from './DefaultToolbar';
|
|
@@ -33,6 +35,8 @@ export interface TrialChromeProps {
|
|
|
33
35
|
sidebar?: SidebarSlot;
|
|
34
36
|
statusBar?: StatusBarSlot;
|
|
35
37
|
undoBindings?: UndoBindings;
|
|
38
|
+
/** Supplied when the instrument declares a `job`. */
|
|
39
|
+
job?: JobHandle;
|
|
36
40
|
sidebarExtras?: ReactNode;
|
|
37
41
|
children: ReactNode;
|
|
38
42
|
}
|
|
@@ -49,6 +53,7 @@ export function TrialChrome({
|
|
|
49
53
|
sidebar,
|
|
50
54
|
statusBar,
|
|
51
55
|
undoBindings,
|
|
56
|
+
job,
|
|
52
57
|
sidebarExtras,
|
|
53
58
|
children,
|
|
54
59
|
}: TrialChromeProps) {
|
|
@@ -59,9 +64,14 @@ export function TrialChrome({
|
|
|
59
64
|
const updateTrialConfig = useStore(storeCtx.store, (s) => s.updateTrialConfig);
|
|
60
65
|
const updateTrialState = useStore(storeCtx.store, (s) => s.updateTrialState);
|
|
61
66
|
|
|
67
|
+
// The trial view is opaque to labkit, so the zoom chrome asks for the 2D shape
|
|
68
|
+
// and goes inert when the trial holds something else — an orbit, say.
|
|
69
|
+
const view2d = as2DView(record.view);
|
|
70
|
+
|
|
62
71
|
const toolbarCtx = useMemo<TrialToolbarContext>(() => {
|
|
63
72
|
const setZoom = (z: number): void => {
|
|
64
|
-
|
|
73
|
+
if (!view2d) return;
|
|
74
|
+
updateTrialView(trialId, { ...view2d, zoom: z });
|
|
65
75
|
};
|
|
66
76
|
return {
|
|
67
77
|
trialId,
|
|
@@ -71,10 +81,10 @@ export function TrialChrome({
|
|
|
71
81
|
canRedo: undoBindings?.canRedo ?? false,
|
|
72
82
|
undo: undoBindings?.undo ?? (() => {}),
|
|
73
83
|
redo: undoBindings?.redo ?? (() => {}),
|
|
74
|
-
zoom:
|
|
84
|
+
zoom: view2d?.zoom ?? 1,
|
|
75
85
|
setZoom,
|
|
76
|
-
zoomIn: () => setZoom(
|
|
77
|
-
zoomOut: () => setZoom(
|
|
86
|
+
zoomIn: () => setZoom((view2d?.zoom ?? 1) * 1.25),
|
|
87
|
+
zoomOut: () => setZoom((view2d?.zoom ?? 1) * 0.8),
|
|
78
88
|
resetZoom: () => setZoom(1),
|
|
79
89
|
hasCanvas: instrument.canvas != null,
|
|
80
90
|
savedSnapshots: lab.savedSnapshots.filter((s) => s.trialId === trialId),
|
|
@@ -85,7 +95,7 @@ export function TrialChrome({
|
|
|
85
95
|
close: () => lab.closeTrial(trialId),
|
|
86
96
|
isLastTrial,
|
|
87
97
|
};
|
|
88
|
-
}, [trialId, record, instrument, lab, isLastTrial, updateTrialView, undoBindings]);
|
|
98
|
+
}, [trialId, record, instrument, lab, isLastTrial, updateTrialView, undoBindings, view2d]);
|
|
89
99
|
|
|
90
100
|
const sidebarCtx = useMemo<TrialSidebarContext>(
|
|
91
101
|
() => ({
|
|
@@ -114,7 +124,7 @@ export function TrialChrome({
|
|
|
114
124
|
const statusCtx: TrialStatusBarContext = {
|
|
115
125
|
trialId,
|
|
116
126
|
instrumentName: record.instrumentName,
|
|
117
|
-
zoom:
|
|
127
|
+
zoom: view2d ? view2d.zoom : null,
|
|
118
128
|
};
|
|
119
129
|
|
|
120
130
|
const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>): void => {
|
|
@@ -147,6 +157,23 @@ export function TrialChrome({
|
|
|
147
157
|
<div className="lk-trial__content">{children}</div>
|
|
148
158
|
</div>
|
|
149
159
|
<div className="lk-trial__status">
|
|
160
|
+
{job ? (
|
|
161
|
+
<div className="lk-trial__job">
|
|
162
|
+
<span className="lk-trial__job-count">
|
|
163
|
+
{job.done}
|
|
164
|
+
{job.total === null ? '' : ` / ${job.total}`}
|
|
165
|
+
</span>
|
|
166
|
+
{job.failures.length > 0 ? (
|
|
167
|
+
<span className="lk-trial__job-failures">{job.failures.length} failed</span>
|
|
168
|
+
) : null}
|
|
169
|
+
{job.error ? <span className="lk-trial__job-error">{job.error}</span> : null}
|
|
170
|
+
{job.status === 'running' ? (
|
|
171
|
+
<button type="button" className="lk-trial__job-cancel" onClick={job.cancel}>
|
|
172
|
+
Cancel
|
|
173
|
+
</button>
|
|
174
|
+
) : null}
|
|
175
|
+
</div>
|
|
176
|
+
) : null}
|
|
150
177
|
{statusBar ? statusBar(statusCtx) : <DefaultStatusBar ctx={statusCtx} />}
|
|
151
178
|
</div>
|
|
152
179
|
</section>
|
package/src/trial/slotTypes.ts
CHANGED
|
@@ -41,7 +41,8 @@ export interface TrialSidebarContext {
|
|
|
41
41
|
export interface TrialStatusBarContext {
|
|
42
42
|
trialId: string;
|
|
43
43
|
instrumentName: string;
|
|
44
|
-
|
|
44
|
+
/** Null when the trial holds a view that is not the 2D one. */
|
|
45
|
+
zoom: number | null;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
/** Replaces a trial's toolbar. Receives everything the default one uses,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/state/helpers.ts","../src/state/adapters.ts","../src/state/context.tsx","../src/state/document.ts","../src/state/store.ts","../src/state/useTrialState.ts"],"names":["useContext","useStore"],"mappings":"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","file":"chunk-3TYUJR7Z.js","sourcesContent":["import type { InstrumentSerializers, SerializedTrial, TrialRecord, UndoStack } from './types';\n\n/** The storage key a lab writes one of its buckets under. Namespaced by\n * `storageKey` so two labs sharing an origin do not collide. */\nexport function labStorageKey(\n storageKey: string,\n bucket: 'workspaces' | 'saves' | 'theme' | 'layout',\n): string {\n return `lk:${storageKey}:${bucket}`;\n}\n\n/** Encode a string for the URL fragment. */\nexport function encodeUrlHash(value: string): string {\n return btoa(encodeURIComponent(value));\n}\n\n/** Decode a URL fragment written by `encodeUrlHash`, or `null` if it is\n * malformed. */\nexport function decodeUrlHash(hash: string): string | null {\n if (!hash) return null;\n try {\n return decodeURIComponent(atob(hash));\n } catch {\n return null;\n }\n}\n\n/** A fresh, empty undo history. */\nexport function emptyUndoStack(): UndoStack {\n return { past: [], future: [] };\n}\n\n/** Serialize trials for storage, running each instrument's own serializer\n * over its state. Undo history is deliberately dropped — it does not survive\n * a reload. */\nexport function serializeTrials(\n trials: TrialRecord[],\n serializers: InstrumentSerializers,\n): SerializedTrial[] {\n return trials.map(({ undoStack: _undo, ...w }) => {\n const s = serializers[w.instrumentName];\n return { ...w, state: s?.serialize ? s.serialize(w.state) : w.state };\n });\n}\n\n/** Rebuild trials from storage, running each instrument's deserializer\n * over its state and starting each with an empty undo history. Returns an\n * empty list rather than throwing on malformed input. */\nexport function deserializeTrials(\n records: SerializedTrial[],\n deserializers: InstrumentSerializers,\n): TrialRecord[] {\n if (!Array.isArray(records)) {\n console.warn('[labkit] deserializeTrials: not an array, returning empty list');\n return [];\n }\n return records.map((r) => {\n const d = deserializers[r.instrumentName];\n return {\n ...r,\n state: d?.deserialize ? d.deserialize(r.state) : r.state,\n undoStack: emptyUndoStack(),\n };\n });\n}\n","import { decodeUrlHash, encodeUrlHash } from './helpers';\nimport type { StorageAdapter } from './types';\n\n/** Persist to `localStorage` — state survives a reload and a new tab. Falls\n * back to no-ops when storage is unavailable. */\nexport const localStorageAdapter: StorageAdapter = {\n read: (key) => {\n try {\n return localStorage.getItem(key);\n } catch {\n return null;\n }\n },\n write: (key, value) => {\n try {\n localStorage.setItem(key, value);\n } catch (e) {\n console.warn('[labkit] localStorage write failed:', e);\n }\n },\n delete: (key) => {\n try {\n localStorage.removeItem(key);\n } catch {\n /* ignore */\n }\n },\n};\n\n/** Persist to `sessionStorage` — state survives a reload but not a new tab. */\nexport const sessionStorageAdapter: StorageAdapter = {\n read: (key) => {\n try {\n return sessionStorage.getItem(key);\n } catch {\n return null;\n }\n },\n write: (key, value) => {\n try {\n sessionStorage.setItem(key, value);\n } catch (e) {\n console.warn('[labkit] sessionStorage write failed:', e);\n }\n },\n delete: (key) => {\n try {\n sessionStorage.removeItem(key);\n } catch {\n /* ignore */\n }\n },\n};\n\nconst URL_HASH_GUARD = typeof window !== 'undefined';\n\nfunction readHashMap(): Record<string, string> {\n if (!URL_HASH_GUARD) return {};\n const raw = decodeUrlHash(window.location.hash.replace(/^#/, ''));\n if (!raw) return {};\n try {\n return JSON.parse(raw) as Record<string, string>;\n } catch {\n return {};\n }\n}\n\nfunction writeHashMap(map: Record<string, string>): void {\n if (!URL_HASH_GUARD) return;\n const encoded = encodeUrlHash(JSON.stringify(map));\n window.history.replaceState(null, '', `#${encoded}`);\n}\n\n/** Persist into the URL fragment, so the page's link carries its state and\n * can be shared or bookmarked. */\nexport const urlHashAdapter: StorageAdapter = {\n read: (key) => readHashMap()[key] ?? null,\n write: (key, value) => {\n const map = readHashMap();\n map[key] = value;\n writeHashMap(map);\n },\n delete: (key) => {\n const map = readHashMap();\n delete map[key];\n writeHashMap(map);\n },\n};\n\n/** An in-memory store, discarded on reload. For tests, and for labs that\n * should start fresh every time. */\nexport function createMemoryAdapter(): StorageAdapter {\n const store = new Map<string, string>();\n return {\n read: (key) => store.get(key) ?? null,\n write: (key, value) => {\n store.set(key, value);\n },\n delete: (key) => {\n store.delete(key);\n },\n };\n}\n\n/** Persists nothing and reads back nothing. */\nexport const noneAdapter: StorageAdapter = {\n read: () => null,\n write: () => {},\n delete: () => {},\n};\n","import { createContext, type ReactElement, type ReactNode, useContext } from 'react';\nimport { useStore } from 'zustand/react';\nimport type { LabStore } from './store';\nimport type { LabStoreState } from './types';\n\ntype LabStoreCtx = { store: LabStore } | null;\n\n/** Context carrying the lab store. Prefer `useLabStore`; this is exported for\n * code that needs to read the context without subscribing. */\nexport const LabStoreContext = createContext<LabStoreCtx>(null);\n\n/** Provides a lab store to its subtree. */\nexport function LabStoreProvider({\n store,\n children,\n}: {\n store: LabStore;\n children: ReactNode;\n}): ReactElement {\n return <LabStoreContext.Provider value={{ store }}>{children}</LabStoreContext.Provider>;\n}\n\n/** Subscribe to the whole lab store. Throws outside a `<LabStoreProvider>`. */\nexport function useLabStore(): LabStoreState & ReturnType<LabStore['getState']> {\n const ctx = useContext(LabStoreContext);\n if (!ctx) throw new Error('[labkit] useLabStore must be used inside <LabStoreProvider>');\n return useStore(ctx.store);\n}\n\n/** Context carrying which trial the subtree belongs to. */\nexport const TrialIdContext = createContext<string | null>(null);\n\n/** Names the trial its subtree belongs to, so an instrument's hooks can\n * find their own record in the store without being passed an id. */\nexport function TrialIdProvider({\n trialId,\n children,\n}: {\n trialId: string;\n children: ReactNode;\n}): ReactElement {\n return <TrialIdContext.Provider value={trialId}>{children}</TrialIdContext.Provider>;\n}\n\n/** The id of the trial this component is inside. Throws outside a\n * `<TrialIdProvider>`. */\nexport function useTrialId(): string {\n const id = useContext(TrialIdContext);\n if (!id) throw new Error('[labkit] useTrialId must be used inside <TrialIdProvider>');\n return id;\n}\n","import { labStorageKey } from './helpers';\nimport type {\n LabDocument,\n LabMode,\n Migration,\n SavedSnapshot,\n SerializedTrial,\n StorageAdapter,\n} from './types';\n\n/** Bumped whenever the persisted shape changes; every bump needs a migration. */\nexport const CURRENT_DOCUMENT_VERSION = 2;\n\n/** The one key a lab persists under. The `:doc` suffix keeps it out of the\n * legacy bucket namespace, where a lab named `a:saves` would otherwise write\n * its document over lab `a`'s saves bucket. */\nexport function labDocumentKey(storageKey: string): string {\n return `lk:${storageKey}:doc`;\n}\n\n/** Where a document that failed to migrate is set aside, so a bad migration\n * loses state loudly rather than silently. */\nexport function quarantineKey(storageKey: string): string {\n return `lk:${storageKey}:quarantine`;\n}\n\n/** A fresh document at the current version. */\nexport function emptyDocument(mode: LabMode): LabDocument {\n return {\n version: CURRENT_DOCUMENT_VERSION,\n trials: [],\n saves: [],\n layout: {},\n mode,\n };\n}\n\n/** What a migration run produced: the migrated document, or why it refused. */\nexport type MigrationOutcome =\n | { ok: true; doc: Record<string, unknown>; migrated: boolean }\n | { ok: false; reason: 'future' | 'failed'; error?: unknown };\n\n/** Walk a document forward to `target`, one migration at a time. A version\n * above `target` is refused rather than parsed under a shape it may not\n * have. */\nexport function runMigrations(\n raw: Record<string, unknown>,\n migrations: Migration[],\n target: number,\n): MigrationOutcome {\n const from = typeof raw.version === 'number' ? raw.version : 0;\n if (from > target) return { ok: false, reason: 'future' };\n if (from === target) return { ok: true, doc: raw, migrated: false };\n\n let doc = raw;\n try {\n for (let v = from; v < target; v++) {\n const migration = migrations[v];\n if (!migration) throw new Error(`[labkit] no migration from version ${v}`);\n doc = migration(doc);\n }\n } catch (error) {\n return { ok: false, reason: 'failed', error };\n }\n return { ok: true, doc, migrated: true };\n}\n\nconst LEGACY_BUCKETS = ['workspaces', 'saves', 'layout', 'theme'] as const;\n\nfunction parseOr<T>(raw: string | null, fallback: T, bucket: string): T {\n if (raw === null) return fallback;\n try {\n return JSON.parse(raw) as T;\n } catch {\n console.warn(`[labkit] unparseable legacy bucket \"${bucket}\", dropping it`);\n return fallback;\n }\n}\n\n/** Assemble a version-0 document out of the four pre-document keys, or null\n * if none of them is present. `fallbackMode` is used when the theme key is\n * absent or holds something unrecognized; `interstellar` passes through\n * untouched for `migrateV0toV1` to coerce. */\nexport function readLegacyDocument(\n storage: StorageAdapter,\n storageKey: string,\n fallbackMode: LabMode,\n): Record<string, unknown> | null {\n const present = LEGACY_BUCKETS.some((b) => storage.read(labStorageKey(storageKey, b)) !== null);\n if (!present) return null;\n\n const rawMode = storage.read(labStorageKey(storageKey, 'theme'));\n const mode =\n rawMode === 'light' || rawMode === 'dark' || rawMode === 'auto' || rawMode === 'interstellar'\n ? rawMode\n : fallbackMode;\n\n return {\n version: 0,\n workspaces: parseOr(storage.read(labStorageKey(storageKey, 'workspaces')), [], 'workspaces'),\n saves: parseOr(storage.read(labStorageKey(storageKey, 'saves')), [], 'saves'),\n layout: parseOr(storage.read(labStorageKey(storageKey, 'layout')), {}, 'layout'),\n mode,\n };\n}\n\n/** Delete the four pre-document keys, but only once `expectedDocument` is\n * confirmed to be exactly what is stored under the document key — a\n * read-back, not a trust of the write that produced it. On any mismatch\n * (write failed, landed partially, or never happened) it deletes nothing,\n * warns, and returns `false` so the legacy buckets stay as a recoverable\n * copy. Deletion itself is read back too — `StorageAdapter.delete` is\n * optional, so an adapter without it leaves the keys in place — and `true`\n * is returned only once all four are actually gone. */\nexport function deleteLegacyKeys(\n storage: StorageAdapter,\n storageKey: string,\n expectedDocument: string,\n): boolean {\n const actual = storage.read(labDocumentKey(storageKey));\n if (actual !== expectedDocument) {\n console.warn(\n `[labkit] keeping legacy keys for \"${storageKey}\": could not confirm the migrated document was written`,\n );\n return false;\n }\n let allGone = true;\n for (const bucket of LEGACY_BUCKETS) {\n const key = labStorageKey(storageKey, bucket);\n storage.delete?.(key);\n if (storage.read(key) !== null) allGone = false;\n }\n return allGone;\n}\n\n/** Set `raw` aside under the quarantine key, reading it back to confirm it\n * landed. Returns `false` when it did not — a full disk is exactly when a\n * document goes unreadable — so the caller can leave the original alone\n * rather than overwrite the only copy of it. */\nexport function quarantineDocument(\n storage: StorageAdapter,\n storageKey: string,\n raw: string,\n): boolean {\n storage.write(quarantineKey(storageKey), raw);\n return storage.read(quarantineKey(storageKey)) === raw;\n}\n\n/** Fill in whatever a document is missing or holds the wrong shape of, so a\n * hydrated store never sees `undefined` where a section should be. Applied to\n * every document, migrated or already current. */\nexport function normalizeDocument(\n doc: Record<string, unknown>,\n fallbackMode: LabMode,\n): LabDocument {\n const mode =\n doc.mode === 'light' || doc.mode === 'dark' || doc.mode === 'auto' ? doc.mode : fallbackMode;\n return {\n version: CURRENT_DOCUMENT_VERSION,\n trials: Array.isArray(doc.trials) ? (doc.trials as SerializedTrial[]) : [],\n saves: Array.isArray(doc.saves) ? (doc.saves as SavedSnapshot[]) : [],\n layout:\n doc.layout && typeof doc.layout === 'object' ? (doc.layout as Record<string, unknown>) : {},\n mode,\n };\n}\n\n/** Version 0 (four loose keys) to version 1 (one document). Normalizes the\n * mode, including `interstellar` — the dark mode's name back when it was a\n * theme. Coerces against version 1's own field names rather than reusing\n * `normalizeDocument`, which produces the current shape and would drop\n * `workspaces` from a document it labels version 1. */\nexport function migrateV0toV1(doc: Record<string, unknown>): Record<string, unknown> {\n const raw = doc.mode === 'interstellar' ? 'dark' : doc.mode;\n const mode = raw === 'light' || raw === 'dark' || raw === 'auto' ? raw : 'auto';\n return {\n version: 1,\n workspaces: Array.isArray(doc.workspaces) ? doc.workspaces : [],\n saves: Array.isArray(doc.saves) ? doc.saves : [],\n layout: doc.layout && typeof doc.layout === 'object' ? doc.layout : {},\n mode,\n };\n}\n\n/** Version 1 to version 2: the vocabulary refresh renamed a tile from\n * workspace to trial. Only the records field moves; a record's own fields are\n * unchanged. */\nexport function migrateV1toV2(doc: Record<string, unknown>): Record<string, unknown> {\n const { workspaces, ...rest } = doc;\n return { ...rest, trials: Array.isArray(workspaces) ? workspaces : [], version: 2 };\n}\n\n/** Index `i` migrates a version-`i` document to version `i + 1`. */\nexport const MIGRATIONS: Migration[] = [migrateV0toV1, migrateV1toV2];\n","import { createStore, type StoreApi } from 'zustand/vanilla';\nimport {\n CURRENT_DOCUMENT_VERSION,\n deleteLegacyKeys,\n emptyDocument,\n labDocumentKey,\n MIGRATIONS,\n normalizeDocument,\n quarantineDocument,\n readLegacyDocument,\n runMigrations,\n} from './document';\nimport { deserializeTrials, emptyUndoStack, serializeTrials } from './helpers';\nimport type {\n CreateLabStoreOptions,\n InstrumentSerializers,\n LabDocument,\n LabMode,\n LabStoreState,\n SavedSnapshot,\n TrialRecord,\n} from './types';\n\n/** Every mutation a lab store supports: managing trials, saving and\n * restoring snapshots, and setting the color mode. */\nexport interface LabStoreActions {\n addTrial: (record: Omit<TrialRecord, 'undoStack'>) => void;\n removeTrial: (id: string) => void;\n updateTrialState: <TS>(id: string, next: TS | ((prev: TS) => TS)) => void;\n updateTrialConfig: <TC>(id: string, key: keyof TC, value: TC[keyof TC]) => void;\n updateTrialView: (id: string, view: TrialRecord['view']) => void;\n updateTrialUndoStack: (\n id: string,\n next: TrialRecord['undoStack'] | ((prev: TrialRecord['undoStack']) => TrialRecord['undoStack']),\n ) => void;\n setTrialInstrument: (id: string, instrumentName: string) => void;\n saveSnapshot: (trialId: string, name: string) => void;\n loadSnapshot: (snapshotId: string, trialId: string) => void;\n deleteSnapshot: (snapshotId: string) => void;\n listSnapshots: (trialId?: string) => SavedSnapshot[];\n setMode: (mode: LabMode) => void;\n setLayout: (layout: Record<string, unknown>) => void;\n}\n\n/** A lab's store: its state and actions, plus the hook instruments use to\n * register how their state is serialized. */\nexport type LabStore = StoreApi<LabStoreState & LabStoreActions> & {\n registerSerializers: (s: InstrumentSerializers) => void;\n};\n\n/** Build a lab store, hydrating from storage if anything was saved under the\n * same key. Writes back are debounced. */\nexport function createLabStore(options: CreateLabStoreOptions): LabStore {\n let serializers: InstrumentSerializers = {};\n let flushTimer: ReturnType<typeof setTimeout> | null = null;\n\n const hydration = hydrateDocument(options);\n const hydrated = hydration.document;\n const persistDisabled = hydration.persistDisabled;\n // Cleared once the legacy keys are actually gone; see the flush.\n let foldedFromLegacy = hydration.foldedFromLegacy;\n\n const hydratedTrials = deserializeTrials(hydrated.trials, serializers);\n const hydratedSnapshots = hydrated.saves;\n const hydratedLayout = hydrated.layout;\n const hydratedMode = hydrated.mode;\n\n const store = createStore<LabStoreState & LabStoreActions>()((set, get) => ({\n trials: hydratedTrials,\n savedSnapshots: hydratedSnapshots,\n mode: hydratedMode,\n layout: hydratedLayout,\n\n addTrial: (record) => {\n set((s) => ({\n trials: [...s.trials, { ...record, undoStack: emptyUndoStack() }],\n }));\n scheduleFlush();\n },\n\n removeTrial: (id) => {\n set((s) => ({ trials: s.trials.filter((w) => w.id !== id) }));\n scheduleFlush();\n },\n\n updateTrialState: (id, next) => {\n set((s) => ({\n trials: s.trials.map((w) => {\n if (w.id !== id) return w;\n const nextState =\n typeof next === 'function' ? (next as (prev: unknown) => unknown)(w.state) : next;\n return { ...w, state: nextState };\n }),\n }));\n scheduleFlush();\n },\n\n updateTrialConfig: (id, key, value) => {\n set((s) => ({\n trials: s.trials.map((w) => {\n if (w.id !== id) return w;\n return {\n ...w,\n config: { ...(w.config as Record<string, unknown>), [key as string]: value },\n };\n }),\n }));\n scheduleFlush();\n },\n\n updateTrialView: (id, view) => {\n set((s) => ({\n trials: s.trials.map((w) => (w.id === id ? { ...w, view } : w)),\n }));\n scheduleFlush();\n },\n\n updateTrialUndoStack: (id, next) => {\n set((s) => ({\n trials: s.trials.map((w) => {\n if (w.id !== id) return w;\n const undoStack =\n typeof next === 'function'\n ? (next as (prev: TrialRecord['undoStack']) => TrialRecord['undoStack'])(w.undoStack)\n : next;\n return { ...w, undoStack };\n }),\n }));\n },\n\n setTrialInstrument: (id, instrumentName) => {\n set((s) => ({\n trials: s.trials.map((w) => (w.id === id ? { ...w, instrumentName } : w)),\n }));\n scheduleFlush();\n },\n\n saveSnapshot: (trialId, name) => {\n const trial = get().trials.find((w) => w.id === trialId);\n if (!trial) return;\n const reg = serializers[trial.instrumentName];\n const serializedState = reg?.serialize\n ? reg.serialize(trial.state)\n : structuredClone(trial.state);\n const clonedConfig = structuredClone(trial.config);\n const lastAt = get().savedSnapshots.reduce((m, sn) => (sn.savedAt > m ? sn.savedAt : m), 0);\n const savedAt = Math.max(Date.now(), lastAt + 1);\n const snapshot: SavedSnapshot = {\n id: crypto.randomUUID(),\n name,\n trialId,\n instrumentName: trial.instrumentName,\n config: clonedConfig,\n state: serializedState,\n savedAt,\n };\n set((s) => ({ savedSnapshots: [...s.savedSnapshots, snapshot] }));\n scheduleFlush();\n },\n\n loadSnapshot: (snapshotId, trialId) => {\n const snapshot = get().savedSnapshots.find((sn) => sn.id === snapshotId);\n if (!snapshot) return;\n const trial = get().trials.find((w) => w.id === trialId);\n if (!trial) return;\n if (snapshot.instrumentName !== trial.instrumentName) {\n console.warn(\n `[labkit] loadSnapshot: instrument mismatch (snapshot=${snapshot.instrumentName}, trial=${trial.instrumentName}); refusing to load`,\n );\n return;\n }\n const reg = serializers[snapshot.instrumentName];\n const restoredState = reg?.deserialize ? reg.deserialize(snapshot.state) : snapshot.state;\n set((s) => ({\n trials: s.trials.map((w) =>\n w.id === trialId ? { ...w, state: restoredState, config: snapshot.config } : w,\n ),\n }));\n scheduleFlush();\n },\n\n deleteSnapshot: (snapshotId) => {\n set((s) => ({\n savedSnapshots: s.savedSnapshots.filter((sn) => sn.id !== snapshotId),\n }));\n scheduleFlush();\n },\n\n listSnapshots: (trialId) => {\n const all = get().savedSnapshots;\n const filtered = trialId ? all.filter((sn) => sn.trialId === trialId) : all;\n return [...filtered].sort((a, b) => b.savedAt - a.savedAt);\n },\n\n setMode: (mode) => {\n set({ mode });\n scheduleFlush();\n },\n\n setLayout: (layout) => {\n set({ layout });\n scheduleFlush();\n },\n }));\n\n function scheduleFlush(): void {\n if (persistDisabled) return;\n if (flushTimer) clearTimeout(flushTimer);\n flushTimer = setTimeout(() => {\n const s = store.getState();\n const document: LabDocument = {\n version: CURRENT_DOCUMENT_VERSION,\n trials: serializeTrials(s.trials, serializers),\n saves: s.savedSnapshots,\n layout: s.layout,\n mode: s.mode,\n };\n const serialized = JSON.stringify(document);\n options.storage.write(labDocumentKey(options.storageKey), serialized);\n if (foldedFromLegacy && deleteLegacyKeys(options.storage, options.storageKey, serialized)) {\n foldedFromLegacy = false;\n }\n flushTimer = null;\n }, 300);\n }\n\n // A lab opened and closed without a single mutation still completes its\n // fold; the flush is what removes the legacy keys.\n if (foldedFromLegacy) scheduleFlush();\n\n return Object.assign(store, {\n registerSerializers(s: InstrumentSerializers) {\n serializers = s;\n },\n });\n}\n\ninterface HydrateResult {\n document: LabDocument;\n /** True when flushing would destroy the only copy of something: a document\n * newer than this code understands, or an unusable one whose quarantine\n * copy did not land. */\n persistDisabled: boolean;\n foldedFromLegacy: boolean;\n}\n\n/** Copy an unusable document aside, and report whether the store may go on\n * persisting. A failed quarantine write means the copy in storage is the only\n * one there is, so the store must not overwrite it. */\nfunction setAside(\n options: CreateLabStoreOptions,\n raw: string,\n why: string,\n error?: unknown,\n): boolean {\n const quarantined = quarantineDocument(options.storage, options.storageKey, raw);\n const message = quarantined\n ? `[labkit] ${why}; quarantined it and starting empty`\n : `[labkit] ${why} and could not be quarantined; leaving it in place and not persisting`;\n if (error === undefined) console.warn(message);\n else console.warn(message, error);\n return quarantined;\n}\n\nfunction hydrateDocument(options: CreateLabStoreOptions): HydrateResult {\n const fallback = emptyDocument(options.initialMode ?? 'auto');\n const raw = options.storage.read(labDocumentKey(options.storageKey));\n\n let parsed: Record<string, unknown> | null = null;\n if (raw !== null) {\n try {\n parsed = JSON.parse(raw) as Record<string, unknown>;\n } catch {\n const persistDisabled = !setAside(options, raw, 'lab document is unparseable');\n return { document: fallback, persistDisabled, foldedFromLegacy: false };\n }\n } else {\n parsed = readLegacyDocument(options.storage, options.storageKey, options.initialMode ?? 'auto');\n }\n\n if (parsed === null) {\n return { document: fallback, persistDisabled: false, foldedFromLegacy: false };\n }\n\n const foldedFromLegacy = raw === null;\n const outcome = runMigrations(parsed, MIGRATIONS, CURRENT_DOCUMENT_VERSION);\n\n if (!outcome.ok) {\n if (outcome.reason === 'future') {\n console.warn(\n '[labkit] lab document is from a newer version of labkit; starting empty and leaving it alone',\n );\n return { document: fallback, persistDisabled: true, foldedFromLegacy: false };\n }\n const stored = JSON.stringify(parsed);\n const persistDisabled = !setAside(\n options,\n stored,\n 'lab document failed to migrate',\n outcome.error,\n );\n return { document: fallback, persistDisabled, foldedFromLegacy: false };\n }\n\n return {\n document: normalizeDocument(outcome.doc, options.initialMode ?? 'auto'),\n persistDisabled: false,\n foldedFromLegacy,\n };\n}\n","import { useContext } from 'react';\nimport { useStore } from 'zustand/react';\nimport { LabStoreContext, TrialIdContext } from './context';\nimport type { TrialStateHandle } from './types';\n\n/** An instrument's own state and config, plus setters. Reads the surrounding\n * trial id, so an instrument never has to know which trial it is\n * running in. Throws outside a lab store and trial. */\nexport function useTrialState<TS = unknown, TC = unknown>(): TrialStateHandle<TS, TC> {\n const ctx = useContext(LabStoreContext);\n if (!ctx) throw new Error('[labkit] useTrialState must be used inside <LabStoreProvider>');\n\n const trialId = useContext(TrialIdContext);\n if (!trialId) throw new Error('[labkit] useTrialState must be used inside <TrialIdProvider>');\n\n const record = useStore(ctx.store, (s) => s.trials.find((w) => w.id === trialId));\n\n if (!record) throw new Error(`[labkit] No trial found with id \"${trialId}\"`);\n\n const updateTrialState = useStore(ctx.store, (s) => s.updateTrialState);\n const updateTrialConfig = useStore(ctx.store, (s) => s.updateTrialConfig);\n\n return {\n state: record.state as TS,\n config: record.config as TC,\n setState: (next) => updateTrialState(trialId, next as Parameters<typeof updateTrialState>[1]),\n setConfig: (key, value) => updateTrialConfig(trialId, key as never, value as never),\n };\n}\n"]}
|