@weasel-js/labkit 1.4.4 → 1.5.1
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 +54 -6
- package/dist/_dts/{CanvasStackContext-kTEZvEgE.d.ts → CanvasStackContext-CJNMS6mz.d.ts} +1 -1
- package/dist/_dts/{PrefsForm.d-DDHFANkc.d.ts → PrefsForm.d-DPy9I8It.d.ts} +4 -0
- package/dist/_dts/Select.d-Gwr8fLpK.d.ts +453 -0
- package/dist/_dts/{frac-z7ker2Vx.d.ts → frac-B4okC2lX.d.ts} +291 -31
- package/dist/_dts/{index-Blv9uQzu.d.ts → index-BNdz-tNd.d.ts} +105 -17
- package/dist/_dts/index-CSJoj4LC.d.ts +248 -0
- package/dist/_dts/{types-B9_zrHmb.d.ts → types-DzoNY1pO.d.ts} +91 -4
- package/dist/_dts/usePanZoom-Pzf9f2jh.d.ts +160 -0
- package/dist/_dts/useTrialState-uLlmaccn.d.ts +212 -0
- package/dist/canvas/index.d.ts +18 -129
- package/dist/canvas/index.js +4 -2
- package/dist/chrome/index.d.ts +5 -93
- package/dist/chrome/index.js +10 -6
- package/dist/chunk-7PFH3CNM.js +150 -0
- package/dist/chunk-7PFH3CNM.js.map +1 -0
- package/dist/{chunk-I6JCVE24.js → chunk-CEIUBPPW.js} +143 -17
- package/dist/chunk-CEIUBPPW.js.map +1 -0
- package/dist/chunk-EH7UEQV7.js +590 -0
- package/dist/chunk-EH7UEQV7.js.map +1 -0
- package/dist/{chunk-MOM3GOVY.js → chunk-EHGVROVV.js} +125 -27
- package/dist/chunk-EHGVROVV.js.map +1 -0
- package/dist/{chunk-TJ7QY3OC.js → chunk-ERFMC6WU.js} +160 -78
- package/dist/chunk-ERFMC6WU.js.map +1 -0
- package/dist/{chunk-CTRKTLYZ.js → chunk-H6ZAOWNE.js} +11 -11
- package/dist/chunk-H6ZAOWNE.js.map +1 -0
- package/dist/{chunk-64ZCN3DA.js → chunk-IHUVQ3TX.js} +250 -160
- package/dist/chunk-IHUVQ3TX.js.map +1 -0
- package/dist/{chunk-FQMJUVHJ.js → chunk-IJJUK4FU.js} +53 -46
- package/dist/chunk-IJJUK4FU.js.map +1 -0
- package/dist/chunk-IXTQI7JX.js +737 -0
- package/dist/chunk-IXTQI7JX.js.map +1 -0
- package/dist/{chunk-BKFVHKJH.js → chunk-MDVTK5U3.js} +4 -4
- package/dist/{chunk-BKFVHKJH.js.map → chunk-MDVTK5U3.js.map} +1 -1
- package/dist/chunk-PHK4W562.js +72 -0
- package/dist/chunk-PHK4W562.js.map +1 -0
- package/dist/chunk-PJ7WEQBJ.js +18 -0
- package/dist/chunk-PJ7WEQBJ.js.map +1 -0
- package/dist/chunk-RWNDDKOH.js +682 -0
- package/dist/chunk-RWNDDKOH.js.map +1 -0
- package/dist/chunk-T3EC2DXD.js +12628 -0
- package/dist/chunk-T3EC2DXD.js.map +1 -0
- package/dist/{chunk-SMHP6XZ4.js → chunk-UN4U4P63.js} +10 -8
- package/dist/chunk-UN4U4P63.js.map +1 -0
- package/dist/chunk-WIG6XHJ7.js +35 -0
- package/dist/chunk-WIG6XHJ7.js.map +1 -0
- package/dist/chunk-Z76KEZAX.js +15 -0
- package/dist/chunk-Z76KEZAX.js.map +1 -0
- package/dist/chunk-ZDHY47EV.js +363 -0
- package/dist/chunk-ZDHY47EV.js.map +1 -0
- package/dist/config/index.d.ts +276 -0
- package/dist/config/index.js +8 -0
- package/dist/config/index.js.map +1 -0
- package/dist/controls/index.d.ts +3 -4
- package/dist/controls/index.js +4 -3
- package/dist/dragdrop/index.d.ts +3 -5
- package/dist/index.d.ts +266 -413
- package/dist/index.js +1775 -2251
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +4 -6
- package/dist/layers/index.js +2 -2
- package/dist/loupe/index.d.ts +69 -20
- package/dist/loupe/index.js +2 -2
- package/dist/passthrough/weasel-ui.d.ts +175 -309
- package/dist/passthrough/weasel-ui.js +1 -1
- package/dist/primitives/index.d.ts +55 -6
- package/dist/primitives/index.js +6 -4
- package/dist/state/index.d.ts +13 -8
- package/dist/state/index.js +18 -13
- package/dist/state/index.js.map +1 -1
- package/dist/styles.css +223 -76
- package/dist/surface/index.d.ts +44 -7
- package/dist/surface/index.js +2 -1
- package/dist/ui/layers/index.js +1 -1
- package/dist/undo/index.d.ts +3 -5
- package/package.json +15 -8
- package/src/annotations/AnnotationOverlay.tsx +56 -5
- package/src/annotations/Annotations.less +4 -0
- package/src/annotations/Annotations.overlay.test.tsx +105 -6
- package/src/annotations/drawOne.test.ts +1 -1
- package/src/annotations/drawOne.ts +2 -2
- package/src/annotations/paint.test.ts +2 -2
- package/src/annotations/paint.ts +1 -1
- package/src/annotations/preload.ts +6 -0
- package/src/annotations/store.test.ts +18 -0
- package/src/annotations/store.ts +5 -4
- package/src/annotations/svgNodes.test.ts +2 -2
- package/src/annotations/svgNodes.ts +1 -0
- package/src/annotations/toolMap.test.ts +9 -5
- package/src/annotations/toolMap.ts +6 -3
- package/src/annotations/types.ts +6 -3
- package/src/canvas/CameraWheelContext.ts +12 -0
- package/src/canvas/CanvasStack.stories.tsx +1 -1
- package/src/canvas/CanvasStack.test.tsx +25 -0
- package/src/canvas/CanvasStack.tsx +31 -4
- package/src/canvas/Stage.less +18 -0
- package/src/canvas/Stage.test.tsx +186 -0
- package/src/canvas/Stage.tsx +134 -0
- package/src/canvas/camera.test.ts +17 -2
- package/src/canvas/camera.ts +9 -9
- package/src/canvas/index.ts +4 -0
- package/src/canvas/usePanZoom.test.ts +28 -0
- package/src/canvas/usePanZoom.ts +21 -9
- package/src/chrome/ChromeRegions.stories.tsx +1 -4
- package/src/chrome/LabChrome.tsx +130 -0
- package/src/chrome/builtins.test.ts +1 -0
- package/src/chrome/builtins.tsx +21 -31
- package/src/chrome/index.ts +16 -0
- package/src/chrome/labRegions.test.tsx +208 -0
- package/src/chrome/labTypes.ts +50 -0
- package/src/chrome/merge.ts +7 -9
- package/src/chrome/regions/PaletteRegion.test.tsx +6 -4
- package/src/chrome/regions/PaletteRegion.tsx +16 -9
- package/src/chrome/regions/SidebarRegion.stories.tsx +44 -0
- package/src/chrome/regions/SidebarRegion.tsx +25 -13
- package/src/chrome/regions/StatusRegion.tsx +15 -8
- package/src/chrome/regions/ToolbarRegion.tsx +34 -16
- package/src/chrome/types.ts +50 -20
- package/src/config/auto.test.ts +12 -0
- package/src/config/auto.ts +15 -0
- package/src/config/autoConfig.test.ts +107 -0
- package/src/config/autoConfig.ts +153 -0
- package/src/config/builder.test.ts +42 -0
- package/src/config/builder.ts +68 -17
- package/src/config/declarationEmit.fixture.ts +11 -0
- package/src/config/declarationEmit.test.ts +51 -0
- package/src/config/entry.test.ts +46 -0
- package/src/config/index.ts +16 -1
- package/src/config/nodeClasses.test.ts +18 -0
- package/src/config/path.ts +7 -2
- package/src/config/resolve.test.ts +25 -0
- package/src/config/resolve.ts +17 -1
- package/src/config/types.ts +34 -2
- package/src/config/useResolvedConfig.ts +26 -0
- package/src/controls/AutoControls.stories.tsx +88 -0
- package/src/controls/ControlPanel.stories.tsx +42 -1
- package/src/controls/ControlPanel.test.tsx +530 -4
- package/src/controls/ControlPanel.tsx +562 -82
- package/src/fake-indexeddb-auto.d.ts +5 -0
- package/src/index.test.ts +7 -0
- package/src/index.ts +66 -5
- package/src/instrument/serializers.test.ts +22 -0
- package/src/instrument/serializers.ts +40 -1
- package/src/instrument/types.ts +25 -1
- package/src/lab/Lab.chrome.test.tsx +16 -0
- package/src/lab/Lab.less +4 -13
- package/src/lab/Lab.persist.test.tsx +147 -0
- package/src/lab/Lab.stories.tsx +1 -3
- package/src/lab/Lab.surface.test.tsx +71 -3
- package/src/lab/Lab.test.tsx +168 -16
- package/src/lab/Lab.tsx +434 -129
- package/src/lab/LabContext.ts +8 -0
- package/src/lab/LabFit.stories.less +18 -0
- package/src/lab/LabFit.stories.tsx +219 -0
- package/src/lab/LabFullChrome.stories.tsx +1 -4
- package/src/lab/LabHeader.test.tsx +40 -9
- package/src/lab/LabHeader.tsx +7 -14
- package/src/lab/LabPalette.tsx +13 -26
- package/src/lab/LabRoot.test.tsx +56 -0
- package/src/lab/LabRoot.tsx +47 -0
- package/src/lab/LabShell.less +73 -15
- package/src/lab/LabShell.stories.tsx +50 -1
- package/src/lab/LabShell.tsx +18 -27
- package/src/lab/LabSwitcher.less +18 -11
- package/src/lab/LabSwitcher.test.tsx +14 -9
- package/src/lab/LabSwitcher.tsx +8 -4
- package/src/lab/Workspace.less +1 -1
- package/src/lab/Workspace.stories.tsx +1 -1
- package/src/lab/Workspace.surface.test.tsx +1 -0
- package/src/lab/fitCheck.test.ts +57 -0
- package/src/lab/fitCheck.ts +125 -0
- package/src/lab/index.ts +2 -0
- package/src/layers/LayerList.stories.tsx +1 -1
- package/src/loupe/AGENTS.md +17 -5
- package/src/loupe/Loupe.less +1 -1
- package/src/loupe/LoupeGestures.tsx +67 -0
- package/src/loupe/TrialLoupe.tsx +43 -28
- package/src/loupe/canvasLens.ts +1 -1
- package/src/loupe/index.ts +4 -0
- package/src/loupe/loupeActions.ts +98 -0
- package/src/loupe/types.ts +1 -1
- package/src/loupe/useLoupe.test.tsx +60 -3
- package/src/loupe/useLoupe.ts +29 -49
- package/src/passthrough/weasel-ui.ts +9 -0
- package/src/primitives/FloatingPanel.stories.tsx +14 -2
- package/src/primitives/FloatingPanel.test.tsx +47 -16
- package/src/primitives/FloatingPanel.tsx +11 -25
- package/src/primitives/FpsMeter.stories.tsx +1 -1
- package/src/primitives/JobProgress.stories.tsx +1 -1
- package/src/primitives/Legend.stories.tsx +1 -1
- package/src/primitives/ScaleIndicator.stories.tsx +1 -1
- package/src/primitives/Sidebar.less +7 -0
- package/src/primitives/Sidebar.stories.tsx +1 -1
- package/src/primitives/Split.less +14 -0
- package/src/primitives/Split.test.tsx +133 -0
- package/src/primitives/Split.tsx +175 -0
- package/src/primitives/StatusBar.stories.tsx +1 -1
- package/src/primitives/Toolbar.stories.tsx +1 -1
- package/src/primitives/ZoomControl.tsx +5 -2
- package/src/primitives/index.ts +2 -0
- package/src/specimen/Specimen.less +95 -0
- package/src/specimen/Specimen.stories.tsx +13 -0
- package/src/specimen/Specimen.test.tsx +13 -0
- package/src/specimen/Specimen.tsx +888 -0
- package/src/state/Persistence.stories.tsx +75 -0
- package/src/state/Persistence.tsx +33 -0
- package/src/state/SingletonExperiment.test.tsx +47 -53
- package/src/state/SingletonExperiment.tsx +24 -13
- package/src/state/adapterContract.ts +116 -0
- package/src/state/adapters.test.ts +113 -61
- package/src/state/adapters.ts +328 -69
- package/src/state/document.test.ts +71 -96
- package/src/state/document.ts +66 -52
- package/src/state/helpers.test.ts +1 -1
- package/src/state/index.ts +8 -0
- package/src/state/labRecords.test.ts +105 -0
- package/src/state/labRecords.ts +160 -0
- package/src/state/openLabStore.test.ts +658 -0
- package/src/state/openLabStore.ts +403 -0
- package/src/state/records.test.ts +234 -0
- package/src/state/records.ts +219 -0
- package/src/state/store.test.ts +169 -635
- package/src/state/store.ts +146 -163
- package/src/state/toolSlot.test.ts +1 -1
- package/src/state/types.ts +65 -17
- package/src/state/useOpenOnce.ts +68 -0
- package/src/state/usePersistedState.test.tsx +125 -0
- package/src/state/usePersistedState.ts +66 -0
- package/src/state/useTrialState.test.tsx +87 -3
- package/src/state/useTrialState.ts +9 -1
- package/src/state/view.test.ts +49 -15
- package/src/state/view.ts +18 -1
- package/src/styles.less +3 -0
- package/src/surface/AGENTS.md +39 -7
- package/src/surface/SurfaceContext.ts +26 -3
- package/src/surface/index.ts +2 -0
- package/src/surface/useSurfaceTile.test.tsx +1 -0
- package/src/surface/useSurfaceTile.ts +6 -5
- package/src/surface/useTiledSurface.test.tsx +231 -1
- package/src/surface/useTiledSurface.ts +76 -15
- package/src/theme/Interstellar.stories.tsx +9 -9
- package/src/theme/interstellar.test.ts +17 -6
- package/src/theme/interstellar.theme.json +180 -0
- package/src/theme/interstellar.ts +8 -5
- package/src/tools/labTool.ts +11 -0
- package/src/trial/Trial.annotations.persist.test.tsx +100 -58
- package/src/trial/Trial.annotations.test.tsx +59 -7
- package/src/trial/Trial.canvas.test.tsx +32 -0
- package/src/trial/Trial.config.test.tsx +75 -2
- package/src/trial/Trial.job.test.tsx +3 -3
- package/src/trial/Trial.less +5 -26
- package/src/trial/Trial.stories.tsx +5 -3
- package/src/trial/Trial.test.tsx +31 -4
- package/src/trial/Trial.trialId.test.tsx +23 -1
- package/src/trial/Trial.tsx +139 -52
- package/src/trial/TrialBody.tsx +17 -141
- package/src/trial/TrialChrome.tsx +24 -7
- package/src/trial/trialOps.test.ts +101 -1
- package/src/trial/trialOps.ts +67 -7
- package/dist/_dts/types-BP2OCcpg.d.ts +0 -155
- package/dist/_dts/types-x92Kfeme.d.ts +0 -62
- package/dist/_dts/useTrialState-D6Vb3T-g.d.ts +0 -99
- package/dist/chunk-2P6PP5N4.js +0 -553
- package/dist/chunk-2P6PP5N4.js.map +0 -1
- package/dist/chunk-64ZCN3DA.js.map +0 -1
- package/dist/chunk-CTRKTLYZ.js.map +0 -1
- package/dist/chunk-E44SU6XS.js +0 -50
- package/dist/chunk-E44SU6XS.js.map +0 -1
- package/dist/chunk-FQMJUVHJ.js.map +0 -1
- package/dist/chunk-I6JCVE24.js.map +0 -1
- package/dist/chunk-MOM3GOVY.js.map +0 -1
- package/dist/chunk-SMHP6XZ4.js.map +0 -1
- package/dist/chunk-TJ7QY3OC.js.map +0 -1
- package/dist/chunk-U3IYHIAE.js +0 -343
- package/dist/chunk-U3IYHIAE.js.map +0 -1
- package/dist/chunk-W3ECWC2K.js +0 -7335
- package/dist/chunk-W3ECWC2K.js.map +0 -1
- package/src/theme/interstellar.tokens.json +0 -61
package/src/state/store.ts
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
import { createStore, type StoreApi } from 'zustand/vanilla';
|
|
2
|
-
import {
|
|
2
|
+
import { applyConfigWrite } from '../config/autoConfig';
|
|
3
|
+
import { fillConfigDefaults } from '../config/path';
|
|
3
4
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
runMigrations,
|
|
13
|
-
} from './document';
|
|
14
|
-
import { deserializeTrials, emptyUndoStack, newId, serializeTrials } from './helpers';
|
|
5
|
+
configDefaultsOf,
|
|
6
|
+
configMigrationsOf,
|
|
7
|
+
configSchemasOf,
|
|
8
|
+
serializersOf,
|
|
9
|
+
} from '../instrument/serializers';
|
|
10
|
+
import type { InstrumentList } from '../instrument/types';
|
|
11
|
+
import { emptyDocument } from './document';
|
|
12
|
+
import { deserializeTrials, emptyUndoStack, newId } from './helpers';
|
|
15
13
|
import type {
|
|
16
14
|
CreateLabStoreOptions,
|
|
17
|
-
|
|
15
|
+
InstrumentHooks,
|
|
16
|
+
InstrumentSerializers,
|
|
18
17
|
LabMode,
|
|
19
18
|
LabStoreState,
|
|
20
19
|
SavedSnapshot,
|
|
20
|
+
SerializedTrial,
|
|
21
21
|
TrialRecord,
|
|
22
22
|
} from './types';
|
|
23
23
|
import {
|
|
@@ -59,54 +59,45 @@ export interface LabStoreActions {
|
|
|
59
59
|
undockPanel: (trialId: string, sectionId: string, as?: UndockedPanel['as']) => void;
|
|
60
60
|
/** Put one section back, or — with no `sectionId` — every panel the trial owns. */
|
|
61
61
|
dockPanel: (trialId: string, sectionId?: string) => void;
|
|
62
|
+
/** Replace the lab's instruments. Trials and saves of an instrument that is
|
|
63
|
+
* new or a different object are brought up to date against it in the same
|
|
64
|
+
* write; every other trial keeps its object. */
|
|
65
|
+
setInstruments: (instruments: InstrumentList) => void;
|
|
66
|
+
/** The per-instrument hooks in force now. Read them at use; never keep them. */
|
|
67
|
+
instrumentHooks: () => InstrumentHooks;
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
/** A lab's store: its state and its actions. How each instrument's state is
|
|
65
|
-
* serialized comes in through `CreateLabStoreOptions.
|
|
71
|
+
* serialized comes in through `CreateLabStoreOptions.instruments`. */
|
|
66
72
|
export type LabStore = StoreApi<LabStoreState & LabStoreActions>;
|
|
67
73
|
|
|
68
|
-
/** Build a lab store
|
|
69
|
-
*
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
let
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
const filled = (instrumentName: string, config: unknown): unknown => {
|
|
82
|
-
const defaults = configDefaults[instrumentName];
|
|
83
|
-
return defaults ? fillConfigDefaults(config, defaults()) : config;
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
const hydratedTrials = deserializeTrials(
|
|
87
|
-
hydrated.trials.map((t) => ({ ...t, config: filled(t.instrumentName, t.config) })),
|
|
88
|
-
serializers,
|
|
89
|
-
);
|
|
90
|
-
const hydratedSnapshots = hydrated.saves.map((sn) => ({
|
|
91
|
-
...sn,
|
|
92
|
-
config: filled(sn.instrumentName, sn.config),
|
|
93
|
-
}));
|
|
94
|
-
const hydratedLayout = hydrated.layout;
|
|
95
|
-
const hydratedMode = hydrated.mode;
|
|
74
|
+
/** Build a lab store holding `initial`, or an empty lab. It knows nothing
|
|
75
|
+
* about storage; `openLabStore` reads a stored lab and binds one of these to
|
|
76
|
+
* it. */
|
|
77
|
+
export function createLabStore(options: CreateLabStoreOptions = {}): LabStore {
|
|
78
|
+
let hooks: InstrumentHooks = options.instruments
|
|
79
|
+
? hooksOf(options.instruments)
|
|
80
|
+
: {
|
|
81
|
+
serializers: options.serializers ?? {},
|
|
82
|
+
configDefaults: options.configDefaults ?? {},
|
|
83
|
+
configMigrations: options.configMigrations ?? {},
|
|
84
|
+
configSchemas: options.configSchemas ?? {},
|
|
85
|
+
};
|
|
86
|
+
const initial = options.initial ?? emptyDocument(options.initialMode ?? 'auto');
|
|
96
87
|
|
|
97
88
|
const store = createStore<LabStoreState & LabStoreActions>()((set, get) => ({
|
|
98
|
-
trials:
|
|
99
|
-
savedSnapshots:
|
|
100
|
-
mode:
|
|
101
|
-
layout:
|
|
102
|
-
undockedPanels:
|
|
89
|
+
trials: hydrateTrials(initial.trials, hooks.serializers, hooks),
|
|
90
|
+
savedSnapshots: hydrateSnapshots(initial.saves, hooks),
|
|
91
|
+
mode: initial.mode,
|
|
92
|
+
layout: initial.layout,
|
|
93
|
+
undockedPanels: initial.undockedPanels,
|
|
103
94
|
activeToolId: null,
|
|
95
|
+
instruments: options.instruments ?? null,
|
|
104
96
|
|
|
105
97
|
addTrial: (record) => {
|
|
106
98
|
set((s) => ({
|
|
107
99
|
trials: [...s.trials, { ...record, undoStack: emptyUndoStack() }],
|
|
108
100
|
}));
|
|
109
|
-
scheduleFlush();
|
|
110
101
|
},
|
|
111
102
|
|
|
112
103
|
removeTrial: (id) => {
|
|
@@ -114,7 +105,6 @@ export function createLabStore(options: CreateLabStoreOptions): LabStore {
|
|
|
114
105
|
trials: s.trials.filter((w) => w.id !== id),
|
|
115
106
|
undockedPanels: dockPanelIn(s.undockedPanels, id),
|
|
116
107
|
}));
|
|
117
|
-
scheduleFlush();
|
|
118
108
|
},
|
|
119
109
|
|
|
120
110
|
updateTrialState: (id, next) => {
|
|
@@ -130,23 +120,29 @@ export function createLabStore(options: CreateLabStoreOptions): LabStore {
|
|
|
130
120
|
return { ...w, state: nextState };
|
|
131
121
|
}),
|
|
132
122
|
}));
|
|
133
|
-
scheduleFlush();
|
|
134
123
|
},
|
|
135
124
|
|
|
136
125
|
updateTrialConfig: (id, path, value) => {
|
|
137
126
|
set((s) => ({
|
|
138
|
-
trials: s.trials.map((w) =>
|
|
139
|
-
w.id
|
|
140
|
-
|
|
127
|
+
trials: s.trials.map((w) => {
|
|
128
|
+
if (w.id !== id) return w;
|
|
129
|
+
const was = w.auto ?? [];
|
|
130
|
+
// The sentinel is never stored: it becomes membership in `auto`, and
|
|
131
|
+
// the pinned value at the path is left exactly where it is.
|
|
132
|
+
const next = applyConfigWrite(w.config, was, path, value);
|
|
133
|
+
if (next.config === w.config && next.autoPaths === was) return w;
|
|
134
|
+
const { auto: _was, ...kept } = w;
|
|
135
|
+
const record: TrialRecord = { ...kept, config: next.config };
|
|
136
|
+
if (next.autoPaths.length > 0) record.auto = next.autoPaths;
|
|
137
|
+
return record;
|
|
138
|
+
}),
|
|
141
139
|
}));
|
|
142
|
-
scheduleFlush();
|
|
143
140
|
},
|
|
144
141
|
|
|
145
142
|
updateTrialView: (id, view) => {
|
|
146
143
|
set((s) => ({
|
|
147
144
|
trials: s.trials.map((w) => (w.id === id && !Object.is(view, w.view) ? { ...w, view } : w)),
|
|
148
145
|
}));
|
|
149
|
-
scheduleFlush();
|
|
150
146
|
},
|
|
151
147
|
|
|
152
148
|
updateTrialSidebarWidth: (id, width) => {
|
|
@@ -155,7 +151,6 @@ export function createLabStore(options: CreateLabStoreOptions): LabStore {
|
|
|
155
151
|
w.id === id && w.sidebarWidth !== width ? { ...w, sidebarWidth: width } : w,
|
|
156
152
|
),
|
|
157
153
|
}));
|
|
158
|
-
scheduleFlush();
|
|
159
154
|
},
|
|
160
155
|
|
|
161
156
|
setTrialTitle: (id, title) => {
|
|
@@ -167,7 +162,6 @@ export function createLabStore(options: CreateLabStoreOptions): LabStore {
|
|
|
167
162
|
return { ...w, title: next };
|
|
168
163
|
}),
|
|
169
164
|
}));
|
|
170
|
-
scheduleFlush();
|
|
171
165
|
},
|
|
172
166
|
|
|
173
167
|
setTrialSectionCollapsed: (id, key, collapsed) => {
|
|
@@ -178,7 +172,6 @@ export function createLabStore(options: CreateLabStoreOptions): LabStore {
|
|
|
178
172
|
return { ...w, collapsedSections: { ...w.collapsedSections, [key]: collapsed } };
|
|
179
173
|
}),
|
|
180
174
|
}));
|
|
181
|
-
scheduleFlush();
|
|
182
175
|
},
|
|
183
176
|
|
|
184
177
|
updateTrialAnnotations: (id, doc) => {
|
|
@@ -187,7 +180,6 @@ export function createLabStore(options: CreateLabStoreOptions): LabStore {
|
|
|
187
180
|
w.id === id && !Object.is(doc, w.annotations) ? { ...w, annotations: doc } : w,
|
|
188
181
|
),
|
|
189
182
|
}));
|
|
190
|
-
scheduleFlush();
|
|
191
183
|
},
|
|
192
184
|
|
|
193
185
|
updateTrialUndoStack: (id, next) => {
|
|
@@ -207,13 +199,12 @@ export function createLabStore(options: CreateLabStoreOptions): LabStore {
|
|
|
207
199
|
set((s) => ({
|
|
208
200
|
trials: s.trials.map((w) => (w.id === id ? { ...w, instrumentName } : w)),
|
|
209
201
|
}));
|
|
210
|
-
scheduleFlush();
|
|
211
202
|
},
|
|
212
203
|
|
|
213
204
|
saveSnapshot: (trialId, name) => {
|
|
214
205
|
const trial = get().trials.find((w) => w.id === trialId);
|
|
215
206
|
if (!trial) return;
|
|
216
|
-
const reg = serializers[trial.instrumentName];
|
|
207
|
+
const reg = hooks.serializers[trial.instrumentName];
|
|
217
208
|
const serializedState = reg?.serialize
|
|
218
209
|
? reg.serialize(trial.state)
|
|
219
210
|
: structuredClone(trial.state);
|
|
@@ -227,10 +218,10 @@ export function createLabStore(options: CreateLabStoreOptions): LabStore {
|
|
|
227
218
|
instrumentName: trial.instrumentName,
|
|
228
219
|
config: clonedConfig,
|
|
229
220
|
state: serializedState,
|
|
221
|
+
auto: [...(trial.auto ?? [])],
|
|
230
222
|
savedAt,
|
|
231
223
|
};
|
|
232
224
|
set((s) => ({ savedSnapshots: [...s.savedSnapshots, snapshot] }));
|
|
233
|
-
scheduleFlush();
|
|
234
225
|
},
|
|
235
226
|
|
|
236
227
|
loadSnapshot: (snapshotId, trialId) => {
|
|
@@ -244,23 +235,26 @@ export function createLabStore(options: CreateLabStoreOptions): LabStore {
|
|
|
244
235
|
);
|
|
245
236
|
return;
|
|
246
237
|
}
|
|
247
|
-
const reg = serializers[snapshot.instrumentName];
|
|
238
|
+
const reg = hooks.serializers[snapshot.instrumentName];
|
|
248
239
|
const restoredState = reg?.deserialize
|
|
249
240
|
? reg.deserialize(snapshot.state, snapshot.config)
|
|
250
241
|
: snapshot.state;
|
|
251
242
|
set((s) => ({
|
|
252
|
-
trials: s.trials.map((w) =>
|
|
253
|
-
w.id
|
|
254
|
-
|
|
243
|
+
trials: s.trials.map((w) => {
|
|
244
|
+
if (w.id !== trialId) return w;
|
|
245
|
+
const loaded: TrialRecord = { ...w, state: restoredState, config: snapshot.config };
|
|
246
|
+
if (snapshot.auto === undefined) return loaded;
|
|
247
|
+
delete loaded.auto;
|
|
248
|
+
if (snapshot.auto.length > 0) loaded.auto = [...snapshot.auto];
|
|
249
|
+
return loaded;
|
|
250
|
+
}),
|
|
255
251
|
}));
|
|
256
|
-
scheduleFlush();
|
|
257
252
|
},
|
|
258
253
|
|
|
259
254
|
deleteSnapshot: (snapshotId) => {
|
|
260
255
|
set((s) => ({
|
|
261
256
|
savedSnapshots: s.savedSnapshots.filter((sn) => sn.id !== snapshotId),
|
|
262
257
|
}));
|
|
263
|
-
scheduleFlush();
|
|
264
258
|
},
|
|
265
259
|
|
|
266
260
|
listSnapshots: (trialId) => {
|
|
@@ -271,7 +265,6 @@ export function createLabStore(options: CreateLabStoreOptions): LabStore {
|
|
|
271
265
|
|
|
272
266
|
setMode: (mode) => {
|
|
273
267
|
set({ mode });
|
|
274
|
-
scheduleFlush();
|
|
275
268
|
},
|
|
276
269
|
|
|
277
270
|
setLabTool: (id) => set({ activeToolId: id }),
|
|
@@ -283,119 +276,109 @@ export function createLabStore(options: CreateLabStoreOptions): LabStore {
|
|
|
283
276
|
|
|
284
277
|
undockPanel: (trialId, sectionId, as) => {
|
|
285
278
|
set((s) => ({ undockedPanels: undockPanelIn(s.undockedPanels, trialId, sectionId, as) }));
|
|
286
|
-
scheduleFlush();
|
|
287
279
|
},
|
|
288
280
|
|
|
289
281
|
dockPanel: (trialId, sectionId) => {
|
|
290
282
|
set((s) => ({ undockedPanels: dockPanelIn(s.undockedPanels, trialId, sectionId) }));
|
|
291
|
-
scheduleFlush();
|
|
292
283
|
},
|
|
293
284
|
|
|
294
285
|
setLayout: (layout) => {
|
|
295
286
|
set({ layout });
|
|
296
|
-
scheduleFlush();
|
|
297
287
|
},
|
|
298
|
-
}));
|
|
299
288
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
};
|
|
313
|
-
const serialized = JSON.stringify(document);
|
|
314
|
-
options.storage.write(labDocumentKey(options.storageKey), serialized);
|
|
315
|
-
if (foldedFromLegacy && deleteLegacyKeys(options.storage, options.storageKey, serialized)) {
|
|
316
|
-
foldedFromLegacy = false;
|
|
289
|
+
setInstruments: (instruments) => {
|
|
290
|
+
const before = get().instruments;
|
|
291
|
+
if (before === instruments) return;
|
|
292
|
+
const next = mergedHooks(hooks, instruments);
|
|
293
|
+
const previous = new Map((before ?? []).map((i) => [i.name, i]));
|
|
294
|
+
const changed = new Set(
|
|
295
|
+
instruments.filter((i) => previous.get(i.name) !== i).map((i) => i.name),
|
|
296
|
+
);
|
|
297
|
+
if (changed.size === 0) {
|
|
298
|
+
hooks = next;
|
|
299
|
+
set({ instruments });
|
|
300
|
+
return;
|
|
317
301
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
302
|
+
const s = get();
|
|
303
|
+
const trials = s.trials.map((w) =>
|
|
304
|
+
changed.has(w.instrumentName)
|
|
305
|
+
? { ...w, config: hydratedConfig(next, w.instrumentName, w.config) }
|
|
306
|
+
: w,
|
|
307
|
+
);
|
|
308
|
+
const savedSnapshots = s.savedSnapshots.map((sn) =>
|
|
309
|
+
changed.has(sn.instrumentName)
|
|
310
|
+
? { ...sn, config: hydratedConfig(next, sn.instrumentName, sn.config) }
|
|
311
|
+
: sn,
|
|
312
|
+
);
|
|
313
|
+
hooks = next;
|
|
314
|
+
set({ instruments, trials, savedSnapshots });
|
|
315
|
+
},
|
|
321
316
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if (foldedFromLegacy) scheduleFlush();
|
|
317
|
+
instrumentHooks: () => hooks,
|
|
318
|
+
}));
|
|
325
319
|
|
|
326
320
|
return store;
|
|
327
321
|
}
|
|
328
322
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
323
|
+
function hooksOf(instruments: InstrumentList): InstrumentHooks {
|
|
324
|
+
return {
|
|
325
|
+
serializers: serializersOf(instruments),
|
|
326
|
+
configDefaults: configDefaultsOf(instruments),
|
|
327
|
+
configSchemas: configSchemasOf(instruments),
|
|
328
|
+
configMigrations: configMigrationsOf(instruments),
|
|
329
|
+
};
|
|
336
330
|
}
|
|
337
331
|
|
|
338
|
-
/**
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
function
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
:
|
|
351
|
-
|
|
352
|
-
else console.warn(message, error);
|
|
353
|
-
return quarantined;
|
|
332
|
+
/** Hooks for `instruments`, keeping those of any instrument no longer listed:
|
|
333
|
+
* its trials stay open, and their state still has to round-trip. A listed
|
|
334
|
+
* name takes its hooks wholly from the new list. */
|
|
335
|
+
function mergedHooks(old: InstrumentHooks, instruments: InstrumentList): InstrumentHooks {
|
|
336
|
+
const listed = new Set(instruments.map((i) => i.name));
|
|
337
|
+
const fresh = hooksOf(instruments);
|
|
338
|
+
const unlisted = <T>(entries: Record<string, T>): Record<string, T> =>
|
|
339
|
+
Object.fromEntries(Object.entries(entries).filter(([name]) => !listed.has(name)));
|
|
340
|
+
return {
|
|
341
|
+
serializers: { ...unlisted(old.serializers), ...fresh.serializers },
|
|
342
|
+
configDefaults: { ...unlisted(old.configDefaults), ...fresh.configDefaults },
|
|
343
|
+
configSchemas: { ...unlisted(old.configSchemas), ...fresh.configSchemas },
|
|
344
|
+
configMigrations: { ...unlisted(old.configMigrations), ...fresh.configMigrations },
|
|
345
|
+
};
|
|
354
346
|
}
|
|
355
347
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
parsed = readLegacyDocument(options.storage, options.storageKey, options.initialMode ?? 'auto');
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
if (parsed === null) {
|
|
373
|
-
return { document: fallback, persistDisabled: false, foldedFromLegacy: false };
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
const foldedFromLegacy = raw === null;
|
|
377
|
-
const outcome = runMigrations(parsed, MIGRATIONS, CURRENT_DOCUMENT_VERSION);
|
|
378
|
-
|
|
379
|
-
if (!outcome.ok) {
|
|
380
|
-
if (outcome.reason === 'future') {
|
|
381
|
-
console.warn(
|
|
382
|
-
'[labkit] lab document is from a newer version of labkit; starting empty and leaving it alone',
|
|
383
|
-
);
|
|
384
|
-
return { document: fallback, persistDisabled: true, foldedFromLegacy: false };
|
|
385
|
-
}
|
|
386
|
-
const stored = JSON.stringify(parsed);
|
|
387
|
-
const persistDisabled = !setAside(
|
|
388
|
-
options,
|
|
389
|
-
stored,
|
|
390
|
-
'lab document failed to migrate',
|
|
391
|
-
outcome.error,
|
|
392
|
-
);
|
|
393
|
-
return { document: fallback, persistDisabled, foldedFromLegacy: false };
|
|
394
|
-
}
|
|
348
|
+
/** How a stored config is brought up to date on the way in. */
|
|
349
|
+
export type ConfigHydration = Pick<CreateLabStoreOptions, 'configDefaults' | 'configMigrations'>;
|
|
350
|
+
|
|
351
|
+
function hydratedConfig(
|
|
352
|
+
hydration: ConfigHydration,
|
|
353
|
+
instrumentName: string,
|
|
354
|
+
config: unknown,
|
|
355
|
+
): unknown {
|
|
356
|
+
const migrate = hydration.configMigrations?.[instrumentName];
|
|
357
|
+
const moved = migrate ? migrate(config) : config;
|
|
358
|
+
const defaults = hydration.configDefaults?.[instrumentName];
|
|
359
|
+
return defaults ? fillConfigDefaults(moved, defaults()) : moved;
|
|
360
|
+
}
|
|
395
361
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
362
|
+
/** Rebuild stored trials: migrate each config, fill its gaps from its
|
|
363
|
+
* instrument's defaults, then run the deserializer against the result. */
|
|
364
|
+
export function hydrateTrials(
|
|
365
|
+
trials: SerializedTrial[],
|
|
366
|
+
serializers: InstrumentSerializers,
|
|
367
|
+
hydration: ConfigHydration = {},
|
|
368
|
+
): TrialRecord[] {
|
|
369
|
+
return deserializeTrials(
|
|
370
|
+
trials.map((t) => ({ ...t, config: hydratedConfig(hydration, t.instrumentName, t.config) })),
|
|
371
|
+
serializers,
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** Bring each stored snapshot's config up to date the way `hydrateTrials` does a trial's. */
|
|
376
|
+
export function hydrateSnapshots(
|
|
377
|
+
saves: SavedSnapshot[],
|
|
378
|
+
hydration: ConfigHydration = {},
|
|
379
|
+
): SavedSnapshot[] {
|
|
380
|
+
return saves.map((sn) => ({
|
|
381
|
+
...sn,
|
|
382
|
+
config: hydratedConfig(hydration, sn.instrumentName, sn.config),
|
|
383
|
+
}));
|
|
401
384
|
}
|
|
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest';
|
|
|
2
2
|
import { createLabStore } from './store';
|
|
3
3
|
|
|
4
4
|
function store() {
|
|
5
|
-
return createLabStore(
|
|
5
|
+
return createLabStore();
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
const record = { id: 't1', instrumentName: 'X', config: {}, state: {}, view: {} };
|
package/src/state/types.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Auto } from '../config/auto';
|
|
2
|
+
import type { ConfigPath, ResolvedConfig, ValueAtPath } from '../config/types';
|
|
3
|
+
import type { InstrumentList } from '../instrument/types';
|
|
2
4
|
import type { UndockedPanels } from './undock';
|
|
3
5
|
/** A trial's undo history, as snapshots of its state either side of the
|
|
4
6
|
* present. */
|
|
@@ -30,6 +32,15 @@ export interface TrialRecord<TS = unknown, TC = unknown, TV = unknown> {
|
|
|
30
32
|
* defaults. Kept so Reset restores the trial's own subject rather than the
|
|
31
33
|
* bare defaults. */
|
|
32
34
|
configSeed?: Partial<TC>;
|
|
35
|
+
/** Dotted paths the trial's seed config opened as auto. Kept beside
|
|
36
|
+
* `configSeed`, which cannot carry them: the sentinel is stripped on the way
|
|
37
|
+
* in, so Reset would otherwise have no way back to the state the trial
|
|
38
|
+
* opened in. Absent means none. */
|
|
39
|
+
autoSeed?: readonly string[];
|
|
40
|
+
/** Dotted paths this trial has unpinned. The value at such a path stays in
|
|
41
|
+
* `config` — it is what the field pins back to — but the instrument reads
|
|
42
|
+
* the resolver's value, or `undefined`, instead. Absent means none. */
|
|
43
|
+
auto?: readonly string[];
|
|
33
44
|
/** This trial's own tool slot. Undefined means it reads the lab's. */
|
|
34
45
|
activeToolId?: string | null;
|
|
35
46
|
/** The extent the trial's sidebar was last dragged to, in pixels. Undefined
|
|
@@ -59,6 +70,10 @@ export interface SavedSnapshot {
|
|
|
59
70
|
instrumentName: string;
|
|
60
71
|
config: unknown;
|
|
61
72
|
state: unknown;
|
|
73
|
+
/** The trial's `auto` paths when saved; empty means every field was pinned.
|
|
74
|
+
* Absent on a snapshot saved before snapshots carried them, which loads
|
|
75
|
+
* over the trial's current set rather than guessing one. */
|
|
76
|
+
auto?: readonly string[];
|
|
62
77
|
savedAt: number;
|
|
63
78
|
}
|
|
64
79
|
|
|
@@ -79,15 +94,29 @@ export interface LabStoreState {
|
|
|
79
94
|
/** Sidebar sections torn out of their trial. A panel here is not rendered in
|
|
80
95
|
* its trial's sidebar; the workspace renders it instead. */
|
|
81
96
|
undockedPanels: UndockedPanels;
|
|
97
|
+
/** The instruments this store serializes, migrates and fills configs for.
|
|
98
|
+
* Null for a store built without them, whose trials look their instrument
|
|
99
|
+
* up on the lab instead. Not persisted. */
|
|
100
|
+
instruments: InstrumentList | null;
|
|
82
101
|
}
|
|
83
102
|
|
|
84
|
-
/**
|
|
85
|
-
*
|
|
86
|
-
|
|
103
|
+
/** A change someone else made to one record: its new value, or `undefined`
|
|
104
|
+
* when it was deleted. */
|
|
105
|
+
export type StorageChange = [key: string, value: unknown];
|
|
106
|
+
|
|
107
|
+
/** Where a lab persists itself: asynchronous keyed storage of
|
|
108
|
+
* structured-clone values, so IndexedDB, the URL, memory or a server can all
|
|
109
|
+
* back one. */
|
|
87
110
|
export interface StorageAdapter {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
111
|
+
/** `undefined` when the key is absent. */
|
|
112
|
+
get(key: string): Promise<unknown>;
|
|
113
|
+
list(prefix: string): Promise<[string, unknown][]>;
|
|
114
|
+
/** Rejects when the value did not land. */
|
|
115
|
+
set(key: string, value: unknown): Promise<void>;
|
|
116
|
+
delete(key: string): Promise<void>;
|
|
117
|
+
/** Reports writes under `prefix` made by anyone but this adapter — another
|
|
118
|
+
* tab, another instance, a server. Omit when the substrate cannot tell. */
|
|
119
|
+
subscribe?(prefix: string, on: (changes: StorageChange[]) => void): () => void;
|
|
91
120
|
}
|
|
92
121
|
|
|
93
122
|
/** What `useTrialState` hands an instrument: its state and config, with
|
|
@@ -96,28 +125,47 @@ export interface TrialStateHandle<TS, TC> {
|
|
|
96
125
|
state: TS;
|
|
97
126
|
setState: (next: TS | ((prev: TS) => TS)) => void;
|
|
98
127
|
config: TC;
|
|
128
|
+
/** The config as stored, before any auto path is resolved. What the control
|
|
129
|
+
* panel renders, so a ghosted control sits at the value it pins back to. */
|
|
130
|
+
raw: TC;
|
|
131
|
+
/** Dotted paths this trial has unpinned. */
|
|
132
|
+
auto: ReadonlySet<string>;
|
|
99
133
|
/** Write one config value, by dotted path — `'grid.size'` for a leaf under
|
|
100
134
|
* an `f.group`, `'cellSize'` for one at the root. */
|
|
101
|
-
setConfig: <P extends ConfigPath<TC> & string>(path: P, value: ValueAtPath<TC, P>) => void;
|
|
135
|
+
setConfig: <P extends ConfigPath<TC> & string>(path: P, value: ValueAtPath<TC, P> | Auto) => void;
|
|
102
136
|
}
|
|
103
137
|
|
|
104
|
-
/** Options for `createLabStore
|
|
105
|
-
* two labs on one origin do not collide. */
|
|
138
|
+
/** Options for `createLabStore`, which knows nothing about storage. */
|
|
106
139
|
export interface CreateLabStoreOptions {
|
|
107
|
-
|
|
108
|
-
|
|
140
|
+
/** A document already read and migrated — what `openLabStore` hands over. */
|
|
141
|
+
initial?: LabDocument;
|
|
109
142
|
initialMode?: LabMode;
|
|
110
143
|
/** Each instrument's default config, keyed by instrument name, used to fill
|
|
111
144
|
* the gaps in a stored one. A config saved before its schema grew a branch
|
|
112
145
|
* arrives holding that branch's defaults rather than `undefined`, and keeps
|
|
113
|
-
* whatever keys the schema has since stopped naming.
|
|
114
|
-
* off its `instruments`. */
|
|
146
|
+
* whatever keys the schema has since stopped naming. */
|
|
115
147
|
configDefaults?: Record<string, () => unknown>;
|
|
148
|
+
/** Each instrument's `migrateConfig`, keyed by instrument name, run on a
|
|
149
|
+
* stored config before its defaults fill it. */
|
|
150
|
+
configMigrations?: Record<string, (stored: unknown) => unknown>;
|
|
151
|
+
/** Each instrument's resolved config schema, keyed by instrument name. What
|
|
152
|
+
* a trial's auto paths are read against, away from the lab that would
|
|
153
|
+
* otherwise supply the rules. */
|
|
154
|
+
configSchemas?: Record<string, ResolvedConfig>;
|
|
116
155
|
/** How each instrument's state survives a reload. Hydration is the first
|
|
117
|
-
* thing `createLabStore` does, so these have to arrive with the store
|
|
118
|
-
* anything registered afterwards is already too late to read the document
|
|
119
|
-
* it was built from. `<Lab>` collects them off its `instruments`. */
|
|
156
|
+
* thing `createLabStore` does, so these have to arrive with the store. */
|
|
120
157
|
serializers?: InstrumentSerializers;
|
|
158
|
+
/** The instruments to serialize, migrate and fill configs for. Given, it
|
|
159
|
+
* supplies all three and the three fields above are ignored. */
|
|
160
|
+
instruments?: InstrumentList;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** What a store does per instrument on the way in and out. */
|
|
164
|
+
export interface InstrumentHooks {
|
|
165
|
+
serializers: InstrumentSerializers;
|
|
166
|
+
configDefaults: Record<string, () => unknown>;
|
|
167
|
+
configMigrations: Record<string, (stored: unknown) => unknown>;
|
|
168
|
+
configSchemas: Record<string, ResolvedConfig>;
|
|
121
169
|
}
|
|
122
170
|
|
|
123
171
|
/** Per-instrument serialize/deserialize hooks, keyed by instrument name. An
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
interface Closable {
|
|
4
|
+
close(): Promise<void> | void;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface Entry<T> {
|
|
8
|
+
value: T | null;
|
|
9
|
+
pending: Promise<T> | null;
|
|
10
|
+
mounts: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Open something once for a component's lifetime: at once when `open` returns
|
|
15
|
+
* a value, after it settles when it returns a promise. Closed on unmount — but
|
|
16
|
+
* the close waits a microtask, and the remount React StrictMode performs right
|
|
17
|
+
* after its rehearsal unmount cancels it, so StrictMode opens it once.
|
|
18
|
+
*/
|
|
19
|
+
export function useOpenOnce<T extends Closable>(open: () => T | Promise<T>): T | null {
|
|
20
|
+
const entry = useRef<Entry<T> | null>(null);
|
|
21
|
+
if (entry.current === null) {
|
|
22
|
+
const result = open();
|
|
23
|
+
entry.current =
|
|
24
|
+
result instanceof Promise
|
|
25
|
+
? { value: null, pending: result, mounts: 0 }
|
|
26
|
+
: { value: result, pending: null, mounts: 0 };
|
|
27
|
+
}
|
|
28
|
+
const [value, setValue] = useState<T | null>(entry.current.value);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
const e = entry.current as Entry<T>;
|
|
32
|
+
e.mounts += 1;
|
|
33
|
+
let live = true;
|
|
34
|
+
void e.pending?.then((opened) => {
|
|
35
|
+
e.value = opened;
|
|
36
|
+
if (live) setValue(opened);
|
|
37
|
+
});
|
|
38
|
+
return () => {
|
|
39
|
+
live = false;
|
|
40
|
+
e.mounts -= 1;
|
|
41
|
+
queueMicrotask(() => {
|
|
42
|
+
if (e.mounts > 0) return;
|
|
43
|
+
if (e.value) void e.value.close();
|
|
44
|
+
else
|
|
45
|
+
void e.pending?.then((opened) => {
|
|
46
|
+
if (e.mounts === 0) void opened.close();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
}, []);
|
|
51
|
+
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Warn once, in development, when a prop read only at mount changes after it. */
|
|
56
|
+
export function useWarnIgnoredChange(component: string, props: Record<string, unknown>): void {
|
|
57
|
+
const first = useRef(props);
|
|
58
|
+
const warned = useRef(false);
|
|
59
|
+
if (process.env.NODE_ENV === 'production' || warned.current) return;
|
|
60
|
+
for (const key of Object.keys(props)) {
|
|
61
|
+
if (Object.is(first.current[key], props[key])) continue;
|
|
62
|
+
warned.current = true;
|
|
63
|
+
console.warn(
|
|
64
|
+
`[labkit] ${component} reads \`${key}\` once, at mount; a later change is ignored`,
|
|
65
|
+
);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}
|