@weasel-js/labkit 1.4.3 → 1.5.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 +13 -2
- package/dist/_dts/{CanvasStackContext-Dde8_1my.d.ts → CanvasStackContext-CqkxkFJW.d.ts} +1 -1
- package/dist/_dts/ToggleBar.d-DbHoWKIA.d.ts +116 -0
- package/dist/_dts/{frac-B5mB8LsJ.d.ts → frac-8R6UmlvN.d.ts} +256 -33
- package/dist/_dts/index-B8uu2ba7.d.ts +237 -0
- package/dist/_dts/{index-DelFfeV0.d.ts → index-retnAut7.d.ts} +45 -12
- package/dist/_dts/{types-B9_zrHmb.d.ts → types-Ca2LCnPq.d.ts} +65 -2
- package/dist/_dts/usePanZoom-nS798rOF.d.ts +160 -0
- package/dist/_dts/useTrialState-jcx_TdU6.d.ts +212 -0
- package/dist/canvas/index.d.ts +17 -128
- package/dist/canvas/index.js +4 -2
- package/dist/chrome/index.d.ts +4 -92
- package/dist/chrome/index.js +9 -5
- package/dist/chunk-67SJLMC7.js +704 -0
- package/dist/chunk-67SJLMC7.js.map +1 -0
- package/dist/chunk-6ZDGOZQV.js +310 -0
- package/dist/chunk-6ZDGOZQV.js.map +1 -0
- package/dist/{chunk-QAGAHJKZ.js → chunk-EKIICY6X.js} +111 -25
- package/dist/chunk-EKIICY6X.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-2RQPGOQF.js → chunk-KJALCDWE.js} +9 -7
- package/dist/chunk-KJALCDWE.js.map +1 -0
- package/dist/{chunk-ISCOFAUO.js → chunk-MUKOW3TC.js} +3 -3
- package/dist/{chunk-ISCOFAUO.js.map → chunk-MUKOW3TC.js.map} +1 -1
- package/dist/chunk-NDRYLVOW.js +15 -0
- package/dist/chunk-NDRYLVOW.js.map +1 -0
- package/dist/{chunk-C42T6DJT.js → chunk-NS64DXMN.js} +226 -159
- package/dist/chunk-NS64DXMN.js.map +1 -0
- package/dist/{chunk-I6JCVE24.js → chunk-PGETSEDK.js} +135 -13
- package/dist/chunk-PGETSEDK.js.map +1 -0
- package/dist/{chunk-FQMJUVHJ.js → chunk-QLSV2N3G.js} +32 -37
- package/dist/chunk-QLSV2N3G.js.map +1 -0
- package/dist/chunk-RWNDDKOH.js +682 -0
- package/dist/chunk-RWNDDKOH.js.map +1 -0
- package/dist/chunk-S2SHZRD7.js +7681 -0
- package/dist/chunk-S2SHZRD7.js.map +1 -0
- package/dist/{chunk-ASWKLRKJ.js → chunk-TQBBYTAF.js} +19 -82
- package/dist/chunk-TQBBYTAF.js.map +1 -0
- package/dist/chunk-UU3NJO6N.js +72 -0
- package/dist/chunk-UU3NJO6N.js.map +1 -0
- package/dist/chunk-WIG6XHJ7.js +35 -0
- package/dist/chunk-WIG6XHJ7.js.map +1 -0
- package/dist/{chunk-TJ7QY3OC.js → chunk-ZPH5WTWF.js} +21 -15
- package/dist/chunk-ZPH5WTWF.js.map +1 -0
- package/dist/config/index.d.ts +232 -0
- package/dist/config/index.js +6 -0
- package/dist/config/index.js.map +1 -0
- package/dist/controls/index.d.ts +2 -3
- package/dist/controls/index.js +3 -2
- package/dist/dragdrop/index.d.ts +2 -4
- package/dist/index.d.ts +123 -348
- package/dist/index.js +794 -837
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +3 -5
- package/dist/layers/index.js +2 -2
- package/dist/loupe/index.d.ts +6 -8
- package/dist/loupe/index.js +2 -2
- package/dist/passthrough/weasel-ui.d.ts +169 -117
- package/dist/passthrough/weasel-ui.js +1 -1
- package/dist/primitives/index.d.ts +53 -6
- package/dist/primitives/index.js +6 -4
- package/dist/state/index.d.ts +12 -7
- package/dist/state/index.js +16 -13
- package/dist/state/index.js.map +1 -1
- package/dist/styles.css +163 -62
- 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 +2 -4
- package/package.json +15 -8
- package/src/annotations/AnnotationOverlay.tsx +53 -5
- package/src/annotations/Annotations.overlay.test.tsx +85 -6
- package/src/annotations/drawOne.test.ts +1 -1
- package/src/annotations/drawOne.ts +2 -2
- package/src/annotations/frac.test.ts +4 -4
- package/src/annotations/frac.ts +3 -2
- package/src/annotations/index.ts +1 -1
- 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 +7 -6
- package/src/annotations/svgNodes.test.ts +2 -2
- package/src/annotations/svgNodes.ts +1 -2
- package/src/annotations/toolMap.ts +3 -3
- package/src/annotations/types.ts +6 -3
- package/src/canvas/CameraWheelContext.ts +12 -0
- 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.ts +10 -5
- package/src/chrome/ChromeRegions.stories.tsx +0 -3
- package/src/chrome/LabChrome.tsx +114 -0
- package/src/chrome/builtins.tsx +20 -31
- package/src/chrome/index.ts +15 -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 +21 -9
- package/src/chrome/regions/SidebarRegion.tsx +25 -13
- package/src/chrome/regions/StatusRegion.tsx +15 -8
- package/src/chrome/regions/ToolbarRegion.tsx +30 -16
- package/src/chrome/types.ts +47 -20
- package/src/config/builder.test.ts +4 -0
- package/src/config/builder.ts +16 -10
- 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 +13 -1
- package/src/config/nodeClasses.test.ts +18 -0
- package/src/config/types.ts +3 -1
- package/src/controls/ControlPanel.test.tsx +16 -0
- package/src/controls/ControlPanel.tsx +5 -2
- package/src/fake-indexeddb-auto.d.ts +5 -0
- package/src/index.test.ts +7 -0
- package/src/index.ts +39 -6
- package/src/instrument/serializers.test.ts +22 -0
- package/src/instrument/serializers.ts +11 -0
- package/src/instrument/types.ts +25 -1
- package/src/lab/Lab.less +4 -13
- package/src/lab/Lab.persist.test.tsx +147 -0
- package/src/lab/Lab.stories.tsx +0 -2
- package/src/lab/Lab.surface.test.tsx +66 -3
- package/src/lab/Lab.test.tsx +91 -17
- package/src/lab/Lab.tsx +337 -124
- package/src/lab/LabFit.stories.less +18 -0
- package/src/lab/LabFit.stories.tsx +219 -0
- package/src/lab/LabFullChrome.stories.tsx +0 -3
- package/src/lab/LabHeader.test.tsx +40 -9
- package/src/lab/LabHeader.tsx +7 -13
- package/src/lab/LabPalette.tsx +13 -26
- package/src/lab/LabShell.less +49 -12
- package/src/lab/LabShell.tsx +14 -2
- package/src/lab/LabSwitcher.less +92 -0
- package/src/lab/LabSwitcher.test.tsx +96 -0
- package/src/lab/LabSwitcher.tsx +112 -0
- package/src/lab/Workspace.surface.test.tsx +2 -1
- package/src/lab/fitCheck.test.ts +57 -0
- package/src/lab/fitCheck.ts +125 -0
- package/src/lab/index.ts +2 -0
- package/src/loupe/AGENTS.md +1 -1
- package/src/loupe/Loupe.less +1 -1
- package/src/loupe/canvasLens.ts +1 -1
- package/src/loupe/types.ts +1 -1
- package/src/loupe/useLoupe.test.tsx +1 -1
- package/src/loupe/useLoupe.ts +1 -1
- package/src/passthrough/weasel-ui.ts +9 -0
- package/src/primitives/FloatingPanel.stories.tsx +13 -1
- package/src/primitives/FloatingPanel.test.tsx +47 -16
- package/src/primitives/FloatingPanel.tsx +11 -25
- package/src/primitives/Split.less +14 -0
- package/src/primitives/Split.test.tsx +108 -0
- package/src/primitives/Split.tsx +159 -0
- package/src/primitives/ZoomControl.tsx +5 -2
- package/src/primitives/index.ts +2 -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 +20 -0
- package/src/state/helpers.ts +16 -0
- 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 +104 -635
- package/src/state/store.ts +117 -158
- package/src/state/toolSlot.test.ts +1 -1
- package/src/state/types.ts +39 -15
- 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 +2 -3
- 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 +2 -1
- package/src/surface/useSurfaceTile.ts +6 -5
- package/src/surface/useTiledSurface.test.tsx +175 -0
- package/src/surface/useTiledSurface.ts +54 -5
- package/src/theme/Interstellar.stories.tsx +7 -7
- package/src/theme/base.less +4 -4
- package/src/theme/interstellar.test.ts +1 -1
- package/src/theme/interstellar.tokens.json +2 -2
- package/src/tools/labTool.ts +11 -0
- package/src/trial/Trial.annotations.persist.test.tsx +100 -58
- package/src/trial/Trial.annotations.test.tsx +47 -4
- package/src/trial/Trial.job.test.tsx +3 -3
- package/src/trial/Trial.less +1 -26
- package/src/trial/Trial.stories.tsx +1 -2
- package/src/trial/Trial.test.tsx +28 -4
- package/src/trial/Trial.trialId.test.tsx +23 -1
- package/src/trial/Trial.tsx +118 -38
- package/src/trial/TrialBody.tsx +17 -141
- package/src/trial/TrialChrome.tsx +7 -4
- package/src/trial/trialOps.ts +7 -4
- 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-2RQPGOQF.js.map +0 -1
- package/dist/chunk-5TROQVQ2.js +0 -547
- package/dist/chunk-5TROQVQ2.js.map +0 -1
- package/dist/chunk-ASWKLRKJ.js.map +0 -1
- package/dist/chunk-C42T6DJT.js.map +0 -1
- package/dist/chunk-CTRKTLYZ.js.map +0 -1
- package/dist/chunk-FQMJUVHJ.js.map +0 -1
- package/dist/chunk-I6JCVE24.js.map +0 -1
- package/dist/chunk-MHAGC6VD.js +0 -7320
- package/dist/chunk-MHAGC6VD.js.map +0 -1
- package/dist/chunk-QAGAHJKZ.js.map +0 -1
- package/dist/chunk-TJ7QY3OC.js.map +0 -1
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
import { newId, emptyUndoStack, deserializeTrials, openRecords, labPrefix, documentOfRecords, recordsOfDocument, trialRecord, trialValuesPrefix, saveRecord, LAYOUT_RECORD, UNDOCK_RECORD, META_RECORD, parseRecordName, compareTrialOrder, serializeTrials, labStorageKey } from './chunk-RWNDDKOH.js';
|
|
2
|
+
import { withValueAtPath, fillConfigDefaults } from './chunk-E44SU6XS.js';
|
|
3
|
+
import { LabStoreContext, TrialIdContext } from './chunk-2YILQ7Y5.js';
|
|
4
|
+
import { createStore } from 'zustand/vanilla';
|
|
5
|
+
import { useContext } from 'react';
|
|
6
|
+
import { useStore } from 'zustand/react';
|
|
7
|
+
|
|
8
|
+
// src/state/document.ts
|
|
9
|
+
var CURRENT_DOCUMENT_VERSION = 4;
|
|
10
|
+
function labDocumentKey(storageKey) {
|
|
11
|
+
return `lk:${storageKey}:doc`;
|
|
12
|
+
}
|
|
13
|
+
function quarantineKey(storageKey) {
|
|
14
|
+
return `lk:${storageKey}:quarantine`;
|
|
15
|
+
}
|
|
16
|
+
function emptyDocument(mode) {
|
|
17
|
+
return {
|
|
18
|
+
version: CURRENT_DOCUMENT_VERSION,
|
|
19
|
+
undockedPanels: {},
|
|
20
|
+
trials: [],
|
|
21
|
+
saves: [],
|
|
22
|
+
layout: {},
|
|
23
|
+
mode
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function runMigrations(raw, migrations, target) {
|
|
27
|
+
const from = typeof raw.version === "number" ? raw.version : 0;
|
|
28
|
+
if (from > target) return { ok: false, reason: "future" };
|
|
29
|
+
if (from === target) return { ok: true, doc: raw, migrated: false };
|
|
30
|
+
let doc = raw;
|
|
31
|
+
try {
|
|
32
|
+
for (let v = from; v < target; v++) {
|
|
33
|
+
const migration = migrations[v];
|
|
34
|
+
if (!migration) throw new Error(`[labkit] no migration from version ${v}`);
|
|
35
|
+
doc = migration(doc);
|
|
36
|
+
}
|
|
37
|
+
} catch (error) {
|
|
38
|
+
return { ok: false, reason: "failed", error };
|
|
39
|
+
}
|
|
40
|
+
return { ok: true, doc, migrated: true };
|
|
41
|
+
}
|
|
42
|
+
var LEGACY_BUCKETS = ["workspaces", "saves", "layout", "theme"];
|
|
43
|
+
function legacyKeys(storageKey) {
|
|
44
|
+
return LEGACY_BUCKETS.map((bucket) => labStorageKey(storageKey, bucket));
|
|
45
|
+
}
|
|
46
|
+
function parseStored(value) {
|
|
47
|
+
if (typeof value !== "string") return value;
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(value);
|
|
50
|
+
} catch {
|
|
51
|
+
return void 0;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function bucketOr(value, fallback, bucket, shape) {
|
|
55
|
+
if (value === void 0) return fallback;
|
|
56
|
+
const parsed = parseStored(value);
|
|
57
|
+
const fits = shape === "array" ? Array.isArray(parsed) : !!parsed && typeof parsed === "object";
|
|
58
|
+
if (fits) return parsed;
|
|
59
|
+
console.warn(`[labkit] unparseable legacy bucket "${bucket}", dropping it`);
|
|
60
|
+
return fallback;
|
|
61
|
+
}
|
|
62
|
+
async function readLegacyDocument(read, storageKey, fallbackMode) {
|
|
63
|
+
const [workspaces, saves, layout, theme] = await Promise.all(
|
|
64
|
+
LEGACY_BUCKETS.map((bucket) => read(labStorageKey(storageKey, bucket)))
|
|
65
|
+
);
|
|
66
|
+
if ([workspaces, saves, layout, theme].every((v) => v === void 0)) return null;
|
|
67
|
+
const mode = theme === "light" || theme === "dark" || theme === "auto" || theme === "interstellar" ? theme : fallbackMode;
|
|
68
|
+
return {
|
|
69
|
+
version: 0,
|
|
70
|
+
workspaces: bucketOr(workspaces, [], "workspaces", "array"),
|
|
71
|
+
saves: bucketOr(saves, [], "saves", "array"),
|
|
72
|
+
layout: bucketOr(layout, {}, "layout", "object"),
|
|
73
|
+
mode
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function sameStored(a, b) {
|
|
77
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
78
|
+
}
|
|
79
|
+
async function deleteConfirmed(storage, keys) {
|
|
80
|
+
try {
|
|
81
|
+
await Promise.all(keys.map((key) => storage.delete(key)));
|
|
82
|
+
const left = await Promise.all(keys.map((key) => storage.get(key)));
|
|
83
|
+
return left.every((v) => v === void 0);
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function quarantineDocument(storage, storageKey, value) {
|
|
89
|
+
try {
|
|
90
|
+
await storage.set(quarantineKey(storageKey), value);
|
|
91
|
+
return sameStored(await storage.get(quarantineKey(storageKey)), value);
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function normalizeDocument(doc, fallbackMode) {
|
|
97
|
+
const mode = doc.mode === "light" || doc.mode === "dark" || doc.mode === "auto" ? doc.mode : fallbackMode;
|
|
98
|
+
return {
|
|
99
|
+
version: CURRENT_DOCUMENT_VERSION,
|
|
100
|
+
trials: Array.isArray(doc.trials) ? doc.trials : [],
|
|
101
|
+
saves: Array.isArray(doc.saves) ? doc.saves : [],
|
|
102
|
+
layout: doc.layout && typeof doc.layout === "object" ? doc.layout : {},
|
|
103
|
+
undockedPanels: doc.undockedPanels && typeof doc.undockedPanels === "object" ? doc.undockedPanels : {},
|
|
104
|
+
mode
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function migrateV0toV1(doc) {
|
|
108
|
+
const raw = doc.mode === "interstellar" ? "dark" : doc.mode;
|
|
109
|
+
const mode = raw === "light" || raw === "dark" || raw === "auto" ? raw : "auto";
|
|
110
|
+
return {
|
|
111
|
+
version: 1,
|
|
112
|
+
workspaces: Array.isArray(doc.workspaces) ? doc.workspaces : [],
|
|
113
|
+
saves: Array.isArray(doc.saves) ? doc.saves : [],
|
|
114
|
+
layout: doc.layout && typeof doc.layout === "object" ? doc.layout : {},
|
|
115
|
+
mode
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function migrateV1toV2(doc) {
|
|
119
|
+
const { workspaces, ...rest } = doc;
|
|
120
|
+
return { ...rest, trials: Array.isArray(workspaces) ? workspaces : [], version: 2 };
|
|
121
|
+
}
|
|
122
|
+
function migrateV2toV3(doc) {
|
|
123
|
+
return { ...doc, undockedPanels: {}, version: 3 };
|
|
124
|
+
}
|
|
125
|
+
function migrateV3toV4(doc) {
|
|
126
|
+
return { ...doc, version: 4 };
|
|
127
|
+
}
|
|
128
|
+
var MIGRATIONS = [migrateV0toV1, migrateV1toV2, migrateV2toV3, migrateV3toV4];
|
|
129
|
+
|
|
130
|
+
// src/state/undock.ts
|
|
131
|
+
function panelKey(trialId, sectionId) {
|
|
132
|
+
return `${trialId}::${sectionId}`;
|
|
133
|
+
}
|
|
134
|
+
function undockPanel(panels, trialId, sectionId, as = "tile") {
|
|
135
|
+
return { ...panels, [panelKey(trialId, sectionId)]: { trialId, sectionId, as } };
|
|
136
|
+
}
|
|
137
|
+
function dockPanel(panels, trialId, sectionId) {
|
|
138
|
+
const out = {};
|
|
139
|
+
for (const [key, panel] of Object.entries(panels)) {
|
|
140
|
+
if (panel.trialId === trialId && (sectionId === void 0 || panel.sectionId === sectionId)) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
out[key] = panel;
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/instrument/serializers.ts
|
|
149
|
+
function configDefaultsOf(instruments) {
|
|
150
|
+
const out = {};
|
|
151
|
+
for (const instrument of instruments) out[instrument.name] = () => instrument.defaultConfig();
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
function configMigrationsOf(instruments) {
|
|
155
|
+
const out = {};
|
|
156
|
+
for (const { name, migrateConfig } of instruments) {
|
|
157
|
+
if (migrateConfig) out[name] = (stored) => migrateConfig(stored);
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
function serializersOf(instruments) {
|
|
162
|
+
const out = {};
|
|
163
|
+
for (const instrument of instruments) {
|
|
164
|
+
const { name, serialize, deserialize } = instrument;
|
|
165
|
+
if (!serialize && !deserialize) continue;
|
|
166
|
+
out[name] = {
|
|
167
|
+
...serialize ? { serialize: (state) => serialize(state) } : {},
|
|
168
|
+
...deserialize ? { deserialize: (data, config) => deserialize(data, config) } : {}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// src/state/store.ts
|
|
175
|
+
function createLabStore(options = {}) {
|
|
176
|
+
let hooks = options.instruments ? hooksOf(options.instruments) : {
|
|
177
|
+
serializers: options.serializers ?? {},
|
|
178
|
+
configDefaults: options.configDefaults ?? {},
|
|
179
|
+
configMigrations: options.configMigrations ?? {}
|
|
180
|
+
};
|
|
181
|
+
const initial = options.initial ?? emptyDocument(options.initialMode ?? "auto");
|
|
182
|
+
const store = createStore()((set, get) => ({
|
|
183
|
+
trials: hydrateTrials(initial.trials, hooks.serializers, hooks),
|
|
184
|
+
savedSnapshots: hydrateSnapshots(initial.saves, hooks),
|
|
185
|
+
mode: initial.mode,
|
|
186
|
+
layout: initial.layout,
|
|
187
|
+
undockedPanels: initial.undockedPanels,
|
|
188
|
+
activeToolId: null,
|
|
189
|
+
instruments: options.instruments ?? null,
|
|
190
|
+
addTrial: (record) => {
|
|
191
|
+
set((s) => ({
|
|
192
|
+
trials: [...s.trials, { ...record, undoStack: emptyUndoStack() }]
|
|
193
|
+
}));
|
|
194
|
+
},
|
|
195
|
+
removeTrial: (id) => {
|
|
196
|
+
set((s) => ({
|
|
197
|
+
trials: s.trials.filter((w) => w.id !== id),
|
|
198
|
+
undockedPanels: dockPanel(s.undockedPanels, id)
|
|
199
|
+
}));
|
|
200
|
+
},
|
|
201
|
+
updateTrialState: (id, next) => {
|
|
202
|
+
set((s) => ({
|
|
203
|
+
trials: s.trials.map((w) => {
|
|
204
|
+
if (w.id !== id) return w;
|
|
205
|
+
const nextState = typeof next === "function" ? next(w.state) : next;
|
|
206
|
+
if (Object.is(nextState, w.state)) return w;
|
|
207
|
+
return { ...w, state: nextState };
|
|
208
|
+
})
|
|
209
|
+
}));
|
|
210
|
+
},
|
|
211
|
+
updateTrialConfig: (id, path, value) => {
|
|
212
|
+
set((s) => ({
|
|
213
|
+
trials: s.trials.map(
|
|
214
|
+
(w) => w.id === id ? { ...w, config: withValueAtPath(w.config, path, value) } : w
|
|
215
|
+
)
|
|
216
|
+
}));
|
|
217
|
+
},
|
|
218
|
+
updateTrialView: (id, view) => {
|
|
219
|
+
set((s) => ({
|
|
220
|
+
trials: s.trials.map((w) => w.id === id && !Object.is(view, w.view) ? { ...w, view } : w)
|
|
221
|
+
}));
|
|
222
|
+
},
|
|
223
|
+
updateTrialSidebarWidth: (id, width) => {
|
|
224
|
+
set((s) => ({
|
|
225
|
+
trials: s.trials.map(
|
|
226
|
+
(w) => w.id === id && w.sidebarWidth !== width ? { ...w, sidebarWidth: width } : w
|
|
227
|
+
)
|
|
228
|
+
}));
|
|
229
|
+
},
|
|
230
|
+
setTrialTitle: (id, title) => {
|
|
231
|
+
set((s) => ({
|
|
232
|
+
trials: s.trials.map((w) => {
|
|
233
|
+
if (w.id !== id) return w;
|
|
234
|
+
const next = title === null ? void 0 : title;
|
|
235
|
+
if ((w.title ?? void 0) === next) return w;
|
|
236
|
+
return { ...w, title: next };
|
|
237
|
+
})
|
|
238
|
+
}));
|
|
239
|
+
},
|
|
240
|
+
setTrialSectionCollapsed: (id, key, collapsed) => {
|
|
241
|
+
set((s) => ({
|
|
242
|
+
trials: s.trials.map((w) => {
|
|
243
|
+
if (w.id !== id) return w;
|
|
244
|
+
if (w.collapsedSections?.[key] === collapsed) return w;
|
|
245
|
+
return { ...w, collapsedSections: { ...w.collapsedSections, [key]: collapsed } };
|
|
246
|
+
})
|
|
247
|
+
}));
|
|
248
|
+
},
|
|
249
|
+
updateTrialAnnotations: (id, doc) => {
|
|
250
|
+
set((s) => ({
|
|
251
|
+
trials: s.trials.map(
|
|
252
|
+
(w) => w.id === id && !Object.is(doc, w.annotations) ? { ...w, annotations: doc } : w
|
|
253
|
+
)
|
|
254
|
+
}));
|
|
255
|
+
},
|
|
256
|
+
updateTrialUndoStack: (id, next) => {
|
|
257
|
+
set((s) => ({
|
|
258
|
+
trials: s.trials.map((w) => {
|
|
259
|
+
if (w.id !== id) return w;
|
|
260
|
+
const undoStack = typeof next === "function" ? next(w.undoStack) : next;
|
|
261
|
+
return { ...w, undoStack };
|
|
262
|
+
})
|
|
263
|
+
}));
|
|
264
|
+
},
|
|
265
|
+
setTrialInstrument: (id, instrumentName) => {
|
|
266
|
+
set((s) => ({
|
|
267
|
+
trials: s.trials.map((w) => w.id === id ? { ...w, instrumentName } : w)
|
|
268
|
+
}));
|
|
269
|
+
},
|
|
270
|
+
saveSnapshot: (trialId, name) => {
|
|
271
|
+
const trial = get().trials.find((w) => w.id === trialId);
|
|
272
|
+
if (!trial) return;
|
|
273
|
+
const reg = hooks.serializers[trial.instrumentName];
|
|
274
|
+
const serializedState = reg?.serialize ? reg.serialize(trial.state) : structuredClone(trial.state);
|
|
275
|
+
const clonedConfig = structuredClone(trial.config);
|
|
276
|
+
const lastAt = get().savedSnapshots.reduce((m, sn) => sn.savedAt > m ? sn.savedAt : m, 0);
|
|
277
|
+
const savedAt = Math.max(Date.now(), lastAt + 1);
|
|
278
|
+
const snapshot = {
|
|
279
|
+
id: newId(),
|
|
280
|
+
name,
|
|
281
|
+
trialId,
|
|
282
|
+
instrumentName: trial.instrumentName,
|
|
283
|
+
config: clonedConfig,
|
|
284
|
+
state: serializedState,
|
|
285
|
+
savedAt
|
|
286
|
+
};
|
|
287
|
+
set((s) => ({ savedSnapshots: [...s.savedSnapshots, snapshot] }));
|
|
288
|
+
},
|
|
289
|
+
loadSnapshot: (snapshotId, trialId) => {
|
|
290
|
+
const snapshot = get().savedSnapshots.find((sn) => sn.id === snapshotId);
|
|
291
|
+
if (!snapshot) return;
|
|
292
|
+
const trial = get().trials.find((w) => w.id === trialId);
|
|
293
|
+
if (!trial) return;
|
|
294
|
+
if (snapshot.instrumentName !== trial.instrumentName) {
|
|
295
|
+
console.warn(
|
|
296
|
+
`[labkit] loadSnapshot: instrument mismatch (snapshot=${snapshot.instrumentName}, trial=${trial.instrumentName}); refusing to load`
|
|
297
|
+
);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const reg = hooks.serializers[snapshot.instrumentName];
|
|
301
|
+
const restoredState = reg?.deserialize ? reg.deserialize(snapshot.state, snapshot.config) : snapshot.state;
|
|
302
|
+
set((s) => ({
|
|
303
|
+
trials: s.trials.map(
|
|
304
|
+
(w) => w.id === trialId ? { ...w, state: restoredState, config: snapshot.config } : w
|
|
305
|
+
)
|
|
306
|
+
}));
|
|
307
|
+
},
|
|
308
|
+
deleteSnapshot: (snapshotId) => {
|
|
309
|
+
set((s) => ({
|
|
310
|
+
savedSnapshots: s.savedSnapshots.filter((sn) => sn.id !== snapshotId)
|
|
311
|
+
}));
|
|
312
|
+
},
|
|
313
|
+
listSnapshots: (trialId) => {
|
|
314
|
+
const all = get().savedSnapshots;
|
|
315
|
+
const filtered = trialId ? all.filter((sn) => sn.trialId === trialId) : all;
|
|
316
|
+
return [...filtered].sort((a, b) => b.savedAt - a.savedAt);
|
|
317
|
+
},
|
|
318
|
+
setMode: (mode) => {
|
|
319
|
+
set({ mode });
|
|
320
|
+
},
|
|
321
|
+
setLabTool: (id) => set({ activeToolId: id }),
|
|
322
|
+
setTrialTool: (trialId, id) => set((s) => ({
|
|
323
|
+
trials: s.trials.map((t) => t.id === trialId ? { ...t, activeToolId: id } : t)
|
|
324
|
+
})),
|
|
325
|
+
undockPanel: (trialId, sectionId, as) => {
|
|
326
|
+
set((s) => ({ undockedPanels: undockPanel(s.undockedPanels, trialId, sectionId, as) }));
|
|
327
|
+
},
|
|
328
|
+
dockPanel: (trialId, sectionId) => {
|
|
329
|
+
set((s) => ({ undockedPanels: dockPanel(s.undockedPanels, trialId, sectionId) }));
|
|
330
|
+
},
|
|
331
|
+
setLayout: (layout) => {
|
|
332
|
+
set({ layout });
|
|
333
|
+
},
|
|
334
|
+
setInstruments: (instruments) => {
|
|
335
|
+
const before = get().instruments;
|
|
336
|
+
if (before === instruments) return;
|
|
337
|
+
const next = mergedHooks(hooks, instruments);
|
|
338
|
+
const previous = new Map((before ?? []).map((i) => [i.name, i]));
|
|
339
|
+
const changed = new Set(
|
|
340
|
+
instruments.filter((i) => previous.get(i.name) !== i).map((i) => i.name)
|
|
341
|
+
);
|
|
342
|
+
if (changed.size === 0) {
|
|
343
|
+
hooks = next;
|
|
344
|
+
set({ instruments });
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const s = get();
|
|
348
|
+
const trials = s.trials.map(
|
|
349
|
+
(w) => changed.has(w.instrumentName) ? { ...w, config: hydratedConfig(next, w.instrumentName, w.config) } : w
|
|
350
|
+
);
|
|
351
|
+
const savedSnapshots = s.savedSnapshots.map(
|
|
352
|
+
(sn) => changed.has(sn.instrumentName) ? { ...sn, config: hydratedConfig(next, sn.instrumentName, sn.config) } : sn
|
|
353
|
+
);
|
|
354
|
+
hooks = next;
|
|
355
|
+
set({ instruments, trials, savedSnapshots });
|
|
356
|
+
},
|
|
357
|
+
instrumentHooks: () => hooks
|
|
358
|
+
}));
|
|
359
|
+
return store;
|
|
360
|
+
}
|
|
361
|
+
function hooksOf(instruments) {
|
|
362
|
+
return {
|
|
363
|
+
serializers: serializersOf(instruments),
|
|
364
|
+
configDefaults: configDefaultsOf(instruments),
|
|
365
|
+
configMigrations: configMigrationsOf(instruments)
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
function mergedHooks(old, instruments) {
|
|
369
|
+
const listed = new Set(instruments.map((i) => i.name));
|
|
370
|
+
const fresh = hooksOf(instruments);
|
|
371
|
+
const unlisted = (entries) => Object.fromEntries(Object.entries(entries).filter(([name]) => !listed.has(name)));
|
|
372
|
+
return {
|
|
373
|
+
serializers: { ...unlisted(old.serializers), ...fresh.serializers },
|
|
374
|
+
configDefaults: { ...unlisted(old.configDefaults), ...fresh.configDefaults },
|
|
375
|
+
configMigrations: { ...unlisted(old.configMigrations), ...fresh.configMigrations }
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
function hydratedConfig(hydration, instrumentName, config) {
|
|
379
|
+
const migrate = hydration.configMigrations?.[instrumentName];
|
|
380
|
+
const moved = migrate ? migrate(config) : config;
|
|
381
|
+
const defaults = hydration.configDefaults?.[instrumentName];
|
|
382
|
+
return defaults ? fillConfigDefaults(moved, defaults()) : moved;
|
|
383
|
+
}
|
|
384
|
+
function hydrateTrials(trials, serializers, hydration = {}) {
|
|
385
|
+
return deserializeTrials(
|
|
386
|
+
trials.map((t) => ({ ...t, config: hydratedConfig(hydration, t.instrumentName, t.config) })),
|
|
387
|
+
serializers
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
function hydrateSnapshots(saves, hydration = {}) {
|
|
391
|
+
return saves.map((sn) => ({
|
|
392
|
+
...sn,
|
|
393
|
+
config: hydratedConfig(hydration, sn.instrumentName, sn.config)
|
|
394
|
+
}));
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// src/state/openLabStore.ts
|
|
398
|
+
async function openLabStore(options) {
|
|
399
|
+
const records = await openRecords({
|
|
400
|
+
storage: options.storage,
|
|
401
|
+
prefix: labPrefix(options.storageKey)
|
|
402
|
+
});
|
|
403
|
+
const { doc, orders } = await readLab(options, records);
|
|
404
|
+
const store = createLabStore({ ...options, initial: doc });
|
|
405
|
+
const unbind = bindLabStore(store, records, { orders });
|
|
406
|
+
return {
|
|
407
|
+
store,
|
|
408
|
+
records,
|
|
409
|
+
close: async () => {
|
|
410
|
+
unbind();
|
|
411
|
+
await records.close();
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
function emptyLab(mode) {
|
|
416
|
+
return { doc: emptyDocument(mode), orders: /* @__PURE__ */ new Map() };
|
|
417
|
+
}
|
|
418
|
+
function ordersOf(doc) {
|
|
419
|
+
return new Map(doc.trials.map((t, i) => [t.id, i]));
|
|
420
|
+
}
|
|
421
|
+
async function readLab(options, records) {
|
|
422
|
+
const mode = options.initialMode ?? "auto";
|
|
423
|
+
if (!records.writable) return emptyLab(mode);
|
|
424
|
+
const joined = documentOfRecords(records.entries());
|
|
425
|
+
if (!joined) return foldOldStorage(options, records);
|
|
426
|
+
const outcome = runMigrations(joined.doc, MIGRATIONS, CURRENT_DOCUMENT_VERSION);
|
|
427
|
+
if (outcome.ok) {
|
|
428
|
+
return { doc: normalizeDocument(outcome.doc, mode), orders: joined.orders };
|
|
429
|
+
}
|
|
430
|
+
const why = outcome.reason === "future" ? "are from a newer version of labkit" : "failed to migrate";
|
|
431
|
+
console.warn(`[labkit] lab records ${why}; starting empty and leaving them alone`, outcome.error);
|
|
432
|
+
records.stopWriting();
|
|
433
|
+
return emptyLab(mode);
|
|
434
|
+
}
|
|
435
|
+
async function foldOldStorage(options, records) {
|
|
436
|
+
const { storage, storageKey } = options;
|
|
437
|
+
const mode = options.initialMode ?? "auto";
|
|
438
|
+
let raw;
|
|
439
|
+
let oldKeys;
|
|
440
|
+
try {
|
|
441
|
+
const stored = await storage.get(labDocumentKey(storageKey));
|
|
442
|
+
if (stored !== void 0) {
|
|
443
|
+
raw = stored;
|
|
444
|
+
oldKeys = [labDocumentKey(storageKey)];
|
|
445
|
+
} else {
|
|
446
|
+
raw = await readLegacyDocument((key) => storage.get(key), storageKey, mode);
|
|
447
|
+
oldKeys = legacyKeys(storageKey);
|
|
448
|
+
if (raw === null) return emptyLab(mode);
|
|
449
|
+
}
|
|
450
|
+
} catch (error) {
|
|
451
|
+
console.warn(
|
|
452
|
+
`[labkit] could not read the old storage for "${storageKey}"; not persisting`,
|
|
453
|
+
error
|
|
454
|
+
);
|
|
455
|
+
records.stopWriting();
|
|
456
|
+
return emptyLab(mode);
|
|
457
|
+
}
|
|
458
|
+
const setAside = async (value, why, error) => {
|
|
459
|
+
if (await quarantineDocument(storage, storageKey, value)) {
|
|
460
|
+
console.warn(`[labkit] ${why}; quarantined it and starting empty`, error);
|
|
461
|
+
await deleteConfirmed(storage, oldKeys);
|
|
462
|
+
records.set(META_RECORD, { version: CURRENT_DOCUMENT_VERSION, mode });
|
|
463
|
+
} else {
|
|
464
|
+
console.warn(
|
|
465
|
+
`[labkit] ${why} and could not be quarantined; leaving it in place and not persisting`,
|
|
466
|
+
error
|
|
467
|
+
);
|
|
468
|
+
records.stopWriting();
|
|
469
|
+
}
|
|
470
|
+
return emptyLab(mode);
|
|
471
|
+
};
|
|
472
|
+
const parsed = parseStored(raw);
|
|
473
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
474
|
+
return setAside(raw, "lab document is unparseable");
|
|
475
|
+
}
|
|
476
|
+
const outcome = runMigrations(
|
|
477
|
+
parsed,
|
|
478
|
+
MIGRATIONS,
|
|
479
|
+
CURRENT_DOCUMENT_VERSION
|
|
480
|
+
);
|
|
481
|
+
if (!outcome.ok) {
|
|
482
|
+
if (outcome.reason === "future") {
|
|
483
|
+
console.warn(
|
|
484
|
+
"[labkit] lab document is from a newer version of labkit; starting empty and leaving it alone"
|
|
485
|
+
);
|
|
486
|
+
records.stopWriting();
|
|
487
|
+
return emptyLab(mode);
|
|
488
|
+
}
|
|
489
|
+
return setAside(parsed, "lab document failed to migrate", outcome.error);
|
|
490
|
+
}
|
|
491
|
+
const doc = normalizeDocument(outcome.doc, mode);
|
|
492
|
+
const entries = recordsOfDocument(doc);
|
|
493
|
+
records.seed(entries);
|
|
494
|
+
if (await writeConfirmed(storage, records.prefix, entries)) {
|
|
495
|
+
await deleteConfirmed(storage, oldKeys);
|
|
496
|
+
} else {
|
|
497
|
+
console.warn(
|
|
498
|
+
`[labkit] keeping the old storage for "${storageKey}": could not confirm its records were written`
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
return { doc, orders: ordersOf(doc) };
|
|
502
|
+
}
|
|
503
|
+
async function writeConfirmed(storage, prefix, entries) {
|
|
504
|
+
try {
|
|
505
|
+
for (const [name, value] of entries) await storage.set(prefix + name, value);
|
|
506
|
+
const back = new Map(await storage.list(prefix));
|
|
507
|
+
return entries.every(([name, value]) => sameStored(back.get(prefix + name), value));
|
|
508
|
+
} catch {
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
function persistedFieldsDiffer(a, b) {
|
|
513
|
+
if (a === b) return false;
|
|
514
|
+
const left = a;
|
|
515
|
+
const right = b;
|
|
516
|
+
for (const key of /* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)])) {
|
|
517
|
+
if (key !== "undoStack" && !Object.is(left[key], right[key])) return true;
|
|
518
|
+
}
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
function bindLabStore(store, records, options) {
|
|
522
|
+
const orderOf = new Map(options.orders);
|
|
523
|
+
let applyingRemote = false;
|
|
524
|
+
const nextOrder = () => {
|
|
525
|
+
let max = -1;
|
|
526
|
+
for (const order of orderOf.values()) max = Math.max(max, order);
|
|
527
|
+
return max + 1;
|
|
528
|
+
};
|
|
529
|
+
const writeTrial = (trial) => {
|
|
530
|
+
const [serialized] = serializeTrials([trial], store.getState().instrumentHooks().serializers);
|
|
531
|
+
records.set(trialRecord(trial.id), { ...serialized, order: orderOf.get(trial.id) });
|
|
532
|
+
};
|
|
533
|
+
const inStoredOrder = (trials) => {
|
|
534
|
+
for (let i = 1; i < trials.length; i++) {
|
|
535
|
+
const a = trials[i - 1];
|
|
536
|
+
const b = trials[i];
|
|
537
|
+
const at = { trial: a, order: orderOf.get(a.id) ?? 0 };
|
|
538
|
+
const bt = { trial: b, order: orderOf.get(b.id) ?? 0 };
|
|
539
|
+
if (compareTrialOrder(at, bt) >= 0) return false;
|
|
540
|
+
}
|
|
541
|
+
return true;
|
|
542
|
+
};
|
|
543
|
+
const offStore = store.subscribe((state, prev) => {
|
|
544
|
+
if (applyingRemote) return;
|
|
545
|
+
let wrote = false;
|
|
546
|
+
if (state.trials !== prev.trials) {
|
|
547
|
+
const before = new Map(prev.trials.map((t) => [t.id, t]));
|
|
548
|
+
const now = new Set(state.trials.map((t) => t.id));
|
|
549
|
+
for (const t of prev.trials) {
|
|
550
|
+
if (now.has(t.id)) continue;
|
|
551
|
+
records.delete(trialRecord(t.id));
|
|
552
|
+
for (const [name] of records.entries(trialValuesPrefix(t.id))) records.delete(name);
|
|
553
|
+
orderOf.delete(t.id);
|
|
554
|
+
wrote = true;
|
|
555
|
+
}
|
|
556
|
+
const added = state.trials.filter((t) => !before.has(t.id));
|
|
557
|
+
for (const t of added) orderOf.set(t.id, nextOrder());
|
|
558
|
+
if (inStoredOrder(state.trials)) {
|
|
559
|
+
for (const t of state.trials) {
|
|
560
|
+
const old = before.get(t.id);
|
|
561
|
+
if (old && !persistedFieldsDiffer(old, t)) continue;
|
|
562
|
+
writeTrial(t);
|
|
563
|
+
wrote = true;
|
|
564
|
+
}
|
|
565
|
+
} else {
|
|
566
|
+
for (const [i, t] of state.trials.entries()) orderOf.set(t.id, i);
|
|
567
|
+
for (const t of state.trials) writeTrial(t);
|
|
568
|
+
wrote = true;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
if (state.savedSnapshots !== prev.savedSnapshots) {
|
|
572
|
+
const before = new Map(prev.savedSnapshots.map((s) => [s.id, s]));
|
|
573
|
+
const now = new Set(state.savedSnapshots.map((s) => s.id));
|
|
574
|
+
for (const s of state.savedSnapshots) {
|
|
575
|
+
if (before.get(s.id) === s) continue;
|
|
576
|
+
records.set(saveRecord(s.id), s);
|
|
577
|
+
wrote = true;
|
|
578
|
+
}
|
|
579
|
+
for (const s of prev.savedSnapshots) {
|
|
580
|
+
if (now.has(s.id)) continue;
|
|
581
|
+
records.delete(saveRecord(s.id));
|
|
582
|
+
wrote = true;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
if (state.layout !== prev.layout) {
|
|
586
|
+
records.set(LAYOUT_RECORD, state.layout);
|
|
587
|
+
wrote = true;
|
|
588
|
+
}
|
|
589
|
+
if (state.undockedPanels !== prev.undockedPanels) {
|
|
590
|
+
records.set(UNDOCK_RECORD, state.undockedPanels);
|
|
591
|
+
wrote = true;
|
|
592
|
+
}
|
|
593
|
+
if (state.mode !== prev.mode || wrote && !records.has(META_RECORD)) {
|
|
594
|
+
records.set(META_RECORD, {
|
|
595
|
+
version: CURRENT_DOCUMENT_VERSION,
|
|
596
|
+
mode: state.mode
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
const offRecords = records.subscribe((changes) => {
|
|
601
|
+
const remote = changes.filter((c) => c.origin === "remote");
|
|
602
|
+
if (remote.length === 0) return;
|
|
603
|
+
const current = store.getState();
|
|
604
|
+
let trials = current.trials;
|
|
605
|
+
let saves = current.savedSnapshots;
|
|
606
|
+
const patch = {};
|
|
607
|
+
for (const { name, value } of remote) {
|
|
608
|
+
const parsed = parseRecordName(name);
|
|
609
|
+
if (!parsed) continue;
|
|
610
|
+
switch (parsed.kind) {
|
|
611
|
+
case "trial": {
|
|
612
|
+
const id = parsed.id;
|
|
613
|
+
if (value === void 0 || !value || typeof value !== "object") {
|
|
614
|
+
trials = trials.filter((t) => t.id !== id);
|
|
615
|
+
orderOf.delete(id);
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
618
|
+
const { order, ...rest } = value;
|
|
619
|
+
orderOf.set(
|
|
620
|
+
id,
|
|
621
|
+
typeof order === "number" && Number.isFinite(order) ? order : nextOrder()
|
|
622
|
+
);
|
|
623
|
+
const hooks = store.getState().instrumentHooks();
|
|
624
|
+
const [replacement] = hydrateTrials([{ ...rest, id }], hooks.serializers, hooks);
|
|
625
|
+
if (!replacement) break;
|
|
626
|
+
trials = trials.some((t) => t.id === id) ? trials.map((t) => t.id === id ? replacement : t) : [...trials, replacement];
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
case "save": {
|
|
630
|
+
const id = parsed.id;
|
|
631
|
+
if (value === void 0 || !value || typeof value !== "object") {
|
|
632
|
+
saves = saves.filter((s) => s.id !== id);
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
const [snapshot] = hydrateSnapshots(
|
|
636
|
+
[{ ...value, id }],
|
|
637
|
+
store.getState().instrumentHooks()
|
|
638
|
+
);
|
|
639
|
+
if (!snapshot) break;
|
|
640
|
+
saves = saves.some((s) => s.id === id) ? saves.map((s) => s.id === id ? snapshot : s) : [...saves, snapshot];
|
|
641
|
+
break;
|
|
642
|
+
}
|
|
643
|
+
case "layout":
|
|
644
|
+
patch.layout = value ?? {};
|
|
645
|
+
break;
|
|
646
|
+
case "undock":
|
|
647
|
+
patch.undockedPanels = value ?? {};
|
|
648
|
+
break;
|
|
649
|
+
case "meta": {
|
|
650
|
+
const meta = value;
|
|
651
|
+
if (typeof meta?.version === "number" && meta.version > CURRENT_DOCUMENT_VERSION) {
|
|
652
|
+
console.warn(
|
|
653
|
+
"[labkit] another tab wrote this lab with a newer labkit; no longer persisting"
|
|
654
|
+
);
|
|
655
|
+
if ("stopWriting" in records) records.stopWriting();
|
|
656
|
+
break;
|
|
657
|
+
}
|
|
658
|
+
if (meta?.mode === "light" || meta?.mode === "dark" || meta?.mode === "auto") {
|
|
659
|
+
patch.mode = meta.mode;
|
|
660
|
+
}
|
|
661
|
+
break;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (trials !== current.trials) {
|
|
666
|
+
trials = [...trials].sort(
|
|
667
|
+
(a, b) => compareTrialOrder(
|
|
668
|
+
{ trial: a, order: orderOf.get(a.id) ?? 0 },
|
|
669
|
+
{ trial: b, order: orderOf.get(b.id) ?? 0 }
|
|
670
|
+
)
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
applyingRemote = true;
|
|
674
|
+
try {
|
|
675
|
+
store.setState({ ...patch, trials, savedSnapshots: saves });
|
|
676
|
+
} finally {
|
|
677
|
+
applyingRemote = false;
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
return () => {
|
|
681
|
+
offStore();
|
|
682
|
+
offRecords();
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
function useTrialState() {
|
|
686
|
+
const ctx = useContext(LabStoreContext);
|
|
687
|
+
if (!ctx) throw new Error("[labkit] useTrialState must be used inside <LabStoreProvider>");
|
|
688
|
+
const trialId = useContext(TrialIdContext);
|
|
689
|
+
if (!trialId) throw new Error("[labkit] useTrialState must be used inside <TrialIdProvider>");
|
|
690
|
+
const record = useStore(ctx.store, (s) => s.trials.find((w) => w.id === trialId));
|
|
691
|
+
if (!record) throw new Error(`[labkit] No trial found with id "${trialId}"`);
|
|
692
|
+
const updateTrialState = useStore(ctx.store, (s) => s.updateTrialState);
|
|
693
|
+
const updateTrialConfig = useStore(ctx.store, (s) => s.updateTrialConfig);
|
|
694
|
+
return {
|
|
695
|
+
state: record.state,
|
|
696
|
+
config: record.config,
|
|
697
|
+
setState: (next) => updateTrialState(trialId, next),
|
|
698
|
+
setConfig: (path, value) => updateTrialConfig(trialId, path, value)
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
export { CURRENT_DOCUMENT_VERSION, createLabStore, dockPanel, labDocumentKey, openLabStore, panelKey, quarantineKey, undockPanel, useTrialState };
|
|
703
|
+
//# sourceMappingURL=chunk-67SJLMC7.js.map
|
|
704
|
+
//# sourceMappingURL=chunk-67SJLMC7.js.map
|