@weasel-js/labkit 1.0.4 → 1.1.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 +6 -6
- package/dist/_dts/{index-CFlDPeZh.d.ts → index-iAP6XbH3.d.ts} +17 -7
- package/dist/_dts/{useExperimentState-D7EQnnwJ.d.ts → useTrialState-DYe2vUwN.d.ts} +60 -36
- package/dist/canvas/index.d.ts +1 -1
- package/dist/chunk-3TYUJR7Z.js +488 -0
- package/dist/chunk-3TYUJR7Z.js.map +1 -0
- package/dist/{chunk-BAPZPDDA.js → chunk-C6GJKPUI.js} +365 -340
- package/dist/chunk-C6GJKPUI.js.map +1 -0
- package/dist/{chunk-DWV7SFKR.js → chunk-DJLDIRFN.js} +28 -12
- package/dist/chunk-DJLDIRFN.js.map +1 -0
- package/dist/{chunk-DEWXYFEU.js → chunk-NRKWVTVT.js} +3 -3
- package/dist/{chunk-DEWXYFEU.js.map → chunk-NRKWVTVT.js.map} +1 -1
- package/dist/dragdrop/index.d.ts +1 -1
- package/dist/index.d.ts +149 -149
- package/dist/index.js +164 -159
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +2 -2
- package/dist/passthrough/weasel-canvas.js +1 -1
- package/dist/passthrough/weasel-ui.d.ts +9 -2
- package/dist/passthrough/weasel-ui.js +2 -2
- package/dist/state/index.d.ts +12 -12
- package/dist/state/index.js +5 -5
- package/dist/state/index.js.map +1 -1
- package/dist/styles.css +21 -13
- package/dist/ui/layers/index.js +3 -3
- package/dist/undo/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/canvas/AGENTS.md +6 -2
- package/src/dragdrop/dragDrop.test.tsx +4 -7
- package/src/index.test.ts +52 -0
- package/src/index.ts +11 -7
- package/src/instrument/SineWave.smoke.test.tsx +1 -1
- package/src/instrument/capabilityDetector.ts +1 -1
- package/src/instrument/types.ts +18 -6
- package/src/lab/Lab.less +16 -0
- package/src/lab/Lab.stories.tsx +5 -5
- package/src/lab/Lab.test.tsx +25 -25
- package/src/lab/Lab.tsx +50 -50
- package/src/lab/LabContext.ts +12 -12
- package/src/lab/LabShell.tsx +1 -1
- package/src/lab/{WorkspaceGrid.less → Workspace.less} +2 -2
- package/src/lab/{WorkspaceGrid.stories.tsx → Workspace.stories.tsx} +13 -13
- package/src/lab/{WorkspaceGrid.test.tsx → Workspace.test.tsx} +24 -24
- package/src/lab/{WorkspaceGrid.tsx → Workspace.tsx} +18 -18
- package/src/lab/index.ts +2 -2
- package/src/layers/AGENTS.md +3 -3
- package/src/primitives/Toolbar.stories.tsx +2 -2
- package/src/state/SingletonExperiment.test.tsx +8 -8
- package/src/state/SingletonExperiment.tsx +9 -9
- package/src/state/context.tsx +12 -12
- package/src/state/document.test.ts +369 -0
- package/src/state/document.ts +194 -0
- package/src/state/helpers.test.ts +74 -46
- package/src/state/helpers.ts +21 -29
- package/src/state/index.ts +16 -8
- package/src/state/store.test.ts +440 -41
- package/src/state/store.ts +159 -102
- package/src/state/types.ts +29 -12
- package/src/state/{useExperimentState.test.tsx → useTrialState.test.tsx} +18 -18
- package/src/state/useTrialState.ts +29 -0
- package/src/styles.less +2 -2
- package/src/theme/base.less +1 -1
- package/src/{workspace → trial}/DefaultSidebar.tsx +3 -3
- package/src/{workspace → trial}/DefaultStatusBar.tsx +5 -4
- package/src/{workspace → trial}/DefaultToolbar.tsx +9 -8
- package/src/trial/Trial.canvas.test.tsx +80 -0
- package/src/{workspace/Workspace.less → trial/Trial.less} +1 -1
- package/src/{workspace/Workspace.stories.tsx → trial/Trial.stories.tsx} +13 -18
- package/src/{workspace/Workspace.test.tsx → trial/Trial.test.tsx} +26 -26
- package/src/{workspace/Workspace.tsx → trial/Trial.tsx} +48 -43
- package/src/{workspace/WorkspaceChrome.tsx → trial/TrialChrome.tsx} +42 -42
- package/src/{workspace → trial}/index.ts +13 -13
- package/src/{workspace → trial}/slotTypes.ts +16 -16
- package/src/{workspace/workspaceOps.test.ts → trial/trialOps.test.ts} +38 -44
- package/src/trial/trialOps.ts +99 -0
- package/src/ui/format.test.ts +33 -0
- package/src/ui/format.ts +11 -0
- package/src/ui/properties/CurveField.tsx +6 -6
- package/src/ui/properties/SpeechBalloonPanels.stories.tsx +3 -3
- package/dist/chunk-BAPZPDDA.js.map +0 -1
- package/dist/chunk-DWV7SFKR.js.map +0 -1
- package/dist/chunk-KSTEW2AF.js +0 -367
- package/dist/chunk-KSTEW2AF.js.map +0 -1
- package/src/state/useExperimentState.ts +0 -31
- package/src/workspace/workspaceOps.ts +0 -102
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { labStorageKey } from './helpers';
|
|
2
|
+
import type {
|
|
3
|
+
LabDocument,
|
|
4
|
+
LabMode,
|
|
5
|
+
Migration,
|
|
6
|
+
SavedSnapshot,
|
|
7
|
+
SerializedTrial,
|
|
8
|
+
StorageAdapter,
|
|
9
|
+
} from './types';
|
|
10
|
+
|
|
11
|
+
/** Bumped whenever the persisted shape changes; every bump needs a migration. */
|
|
12
|
+
export const CURRENT_DOCUMENT_VERSION = 2;
|
|
13
|
+
|
|
14
|
+
/** The one key a lab persists under. The `:doc` suffix keeps it out of the
|
|
15
|
+
* legacy bucket namespace, where a lab named `a:saves` would otherwise write
|
|
16
|
+
* its document over lab `a`'s saves bucket. */
|
|
17
|
+
export function labDocumentKey(storageKey: string): string {
|
|
18
|
+
return `lk:${storageKey}:doc`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Where a document that failed to migrate is set aside, so a bad migration
|
|
22
|
+
* loses state loudly rather than silently. */
|
|
23
|
+
export function quarantineKey(storageKey: string): string {
|
|
24
|
+
return `lk:${storageKey}:quarantine`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A fresh document at the current version. */
|
|
28
|
+
export function emptyDocument(mode: LabMode): LabDocument {
|
|
29
|
+
return {
|
|
30
|
+
version: CURRENT_DOCUMENT_VERSION,
|
|
31
|
+
trials: [],
|
|
32
|
+
saves: [],
|
|
33
|
+
layout: {},
|
|
34
|
+
mode,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** What a migration run produced: the migrated document, or why it refused. */
|
|
39
|
+
export type MigrationOutcome =
|
|
40
|
+
| { ok: true; doc: Record<string, unknown>; migrated: boolean }
|
|
41
|
+
| { ok: false; reason: 'future' | 'failed'; error?: unknown };
|
|
42
|
+
|
|
43
|
+
/** Walk a document forward to `target`, one migration at a time. A version
|
|
44
|
+
* above `target` is refused rather than parsed under a shape it may not
|
|
45
|
+
* have. */
|
|
46
|
+
export function runMigrations(
|
|
47
|
+
raw: Record<string, unknown>,
|
|
48
|
+
migrations: Migration[],
|
|
49
|
+
target: number,
|
|
50
|
+
): MigrationOutcome {
|
|
51
|
+
const from = typeof raw.version === 'number' ? raw.version : 0;
|
|
52
|
+
if (from > target) return { ok: false, reason: 'future' };
|
|
53
|
+
if (from === target) return { ok: true, doc: raw, migrated: false };
|
|
54
|
+
|
|
55
|
+
let doc = raw;
|
|
56
|
+
try {
|
|
57
|
+
for (let v = from; v < target; v++) {
|
|
58
|
+
const migration = migrations[v];
|
|
59
|
+
if (!migration) throw new Error(`[labkit] no migration from version ${v}`);
|
|
60
|
+
doc = migration(doc);
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return { ok: false, reason: 'failed', error };
|
|
64
|
+
}
|
|
65
|
+
return { ok: true, doc, migrated: true };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const LEGACY_BUCKETS = ['workspaces', 'saves', 'layout', 'theme'] as const;
|
|
69
|
+
|
|
70
|
+
function parseOr<T>(raw: string | null, fallback: T, bucket: string): T {
|
|
71
|
+
if (raw === null) return fallback;
|
|
72
|
+
try {
|
|
73
|
+
return JSON.parse(raw) as T;
|
|
74
|
+
} catch {
|
|
75
|
+
console.warn(`[labkit] unparseable legacy bucket "${bucket}", dropping it`);
|
|
76
|
+
return fallback;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Assemble a version-0 document out of the four pre-document keys, or null
|
|
81
|
+
* if none of them is present. `fallbackMode` is used when the theme key is
|
|
82
|
+
* absent or holds something unrecognized; `interstellar` passes through
|
|
83
|
+
* untouched for `migrateV0toV1` to coerce. */
|
|
84
|
+
export function readLegacyDocument(
|
|
85
|
+
storage: StorageAdapter,
|
|
86
|
+
storageKey: string,
|
|
87
|
+
fallbackMode: LabMode,
|
|
88
|
+
): Record<string, unknown> | null {
|
|
89
|
+
const present = LEGACY_BUCKETS.some((b) => storage.read(labStorageKey(storageKey, b)) !== null);
|
|
90
|
+
if (!present) return null;
|
|
91
|
+
|
|
92
|
+
const rawMode = storage.read(labStorageKey(storageKey, 'theme'));
|
|
93
|
+
const mode =
|
|
94
|
+
rawMode === 'light' || rawMode === 'dark' || rawMode === 'auto' || rawMode === 'interstellar'
|
|
95
|
+
? rawMode
|
|
96
|
+
: fallbackMode;
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
version: 0,
|
|
100
|
+
workspaces: parseOr(storage.read(labStorageKey(storageKey, 'workspaces')), [], 'workspaces'),
|
|
101
|
+
saves: parseOr(storage.read(labStorageKey(storageKey, 'saves')), [], 'saves'),
|
|
102
|
+
layout: parseOr(storage.read(labStorageKey(storageKey, 'layout')), {}, 'layout'),
|
|
103
|
+
mode,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Delete the four pre-document keys, but only once `expectedDocument` is
|
|
108
|
+
* confirmed to be exactly what is stored under the document key — a
|
|
109
|
+
* read-back, not a trust of the write that produced it. On any mismatch
|
|
110
|
+
* (write failed, landed partially, or never happened) it deletes nothing,
|
|
111
|
+
* warns, and returns `false` so the legacy buckets stay as a recoverable
|
|
112
|
+
* copy. Deletion itself is read back too — `StorageAdapter.delete` is
|
|
113
|
+
* optional, so an adapter without it leaves the keys in place — and `true`
|
|
114
|
+
* is returned only once all four are actually gone. */
|
|
115
|
+
export function deleteLegacyKeys(
|
|
116
|
+
storage: StorageAdapter,
|
|
117
|
+
storageKey: string,
|
|
118
|
+
expectedDocument: string,
|
|
119
|
+
): boolean {
|
|
120
|
+
const actual = storage.read(labDocumentKey(storageKey));
|
|
121
|
+
if (actual !== expectedDocument) {
|
|
122
|
+
console.warn(
|
|
123
|
+
`[labkit] keeping legacy keys for "${storageKey}": could not confirm the migrated document was written`,
|
|
124
|
+
);
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
let allGone = true;
|
|
128
|
+
for (const bucket of LEGACY_BUCKETS) {
|
|
129
|
+
const key = labStorageKey(storageKey, bucket);
|
|
130
|
+
storage.delete?.(key);
|
|
131
|
+
if (storage.read(key) !== null) allGone = false;
|
|
132
|
+
}
|
|
133
|
+
return allGone;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Set `raw` aside under the quarantine key, reading it back to confirm it
|
|
137
|
+
* landed. Returns `false` when it did not — a full disk is exactly when a
|
|
138
|
+
* document goes unreadable — so the caller can leave the original alone
|
|
139
|
+
* rather than overwrite the only copy of it. */
|
|
140
|
+
export function quarantineDocument(
|
|
141
|
+
storage: StorageAdapter,
|
|
142
|
+
storageKey: string,
|
|
143
|
+
raw: string,
|
|
144
|
+
): boolean {
|
|
145
|
+
storage.write(quarantineKey(storageKey), raw);
|
|
146
|
+
return storage.read(quarantineKey(storageKey)) === raw;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Fill in whatever a document is missing or holds the wrong shape of, so a
|
|
150
|
+
* hydrated store never sees `undefined` where a section should be. Applied to
|
|
151
|
+
* every document, migrated or already current. */
|
|
152
|
+
export function normalizeDocument(
|
|
153
|
+
doc: Record<string, unknown>,
|
|
154
|
+
fallbackMode: LabMode,
|
|
155
|
+
): LabDocument {
|
|
156
|
+
const mode =
|
|
157
|
+
doc.mode === 'light' || doc.mode === 'dark' || doc.mode === 'auto' ? doc.mode : fallbackMode;
|
|
158
|
+
return {
|
|
159
|
+
version: CURRENT_DOCUMENT_VERSION,
|
|
160
|
+
trials: Array.isArray(doc.trials) ? (doc.trials as SerializedTrial[]) : [],
|
|
161
|
+
saves: Array.isArray(doc.saves) ? (doc.saves as SavedSnapshot[]) : [],
|
|
162
|
+
layout:
|
|
163
|
+
doc.layout && typeof doc.layout === 'object' ? (doc.layout as Record<string, unknown>) : {},
|
|
164
|
+
mode,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Version 0 (four loose keys) to version 1 (one document). Normalizes the
|
|
169
|
+
* mode, including `interstellar` — the dark mode's name back when it was a
|
|
170
|
+
* theme. Coerces against version 1's own field names rather than reusing
|
|
171
|
+
* `normalizeDocument`, which produces the current shape and would drop
|
|
172
|
+
* `workspaces` from a document it labels version 1. */
|
|
173
|
+
export function migrateV0toV1(doc: Record<string, unknown>): Record<string, unknown> {
|
|
174
|
+
const raw = doc.mode === 'interstellar' ? 'dark' : doc.mode;
|
|
175
|
+
const mode = raw === 'light' || raw === 'dark' || raw === 'auto' ? raw : 'auto';
|
|
176
|
+
return {
|
|
177
|
+
version: 1,
|
|
178
|
+
workspaces: Array.isArray(doc.workspaces) ? doc.workspaces : [],
|
|
179
|
+
saves: Array.isArray(doc.saves) ? doc.saves : [],
|
|
180
|
+
layout: doc.layout && typeof doc.layout === 'object' ? doc.layout : {},
|
|
181
|
+
mode,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Version 1 to version 2: the vocabulary refresh renamed a tile from
|
|
186
|
+
* workspace to trial. Only the records field moves; a record's own fields are
|
|
187
|
+
* unchanged. */
|
|
188
|
+
export function migrateV1toV2(doc: Record<string, unknown>): Record<string, unknown> {
|
|
189
|
+
const { workspaces, ...rest } = doc;
|
|
190
|
+
return { ...rest, trials: Array.isArray(workspaces) ? workspaces : [], version: 2 };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Index `i` migrates a version-`i` document to version `i + 1`. */
|
|
194
|
+
export const MIGRATIONS: Migration[] = [migrateV0toV1, migrateV1toV2];
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import {
|
|
3
3
|
decodeUrlHash,
|
|
4
|
-
|
|
4
|
+
deserializeTrials,
|
|
5
5
|
emptyUndoStack,
|
|
6
6
|
encodeUrlHash,
|
|
7
7
|
labStorageKey,
|
|
8
|
-
|
|
8
|
+
serializeTrials,
|
|
9
9
|
} from './helpers';
|
|
10
|
-
import type { WorkspaceRecord } from './types';
|
|
11
10
|
|
|
12
11
|
describe('labStorageKey', () => {
|
|
13
12
|
it('produces namespaced keys', () => {
|
|
@@ -19,7 +18,7 @@ describe('labStorageKey', () => {
|
|
|
19
18
|
|
|
20
19
|
describe('encodeUrlHash / decodeUrlHash', () => {
|
|
21
20
|
it('round-trips a string', () => {
|
|
22
|
-
const original = JSON.stringify({
|
|
21
|
+
const original = JSON.stringify({ trials: '[]', saves: '[]' });
|
|
23
22
|
expect(decodeUrlHash(encodeUrlHash(original))).toBe(original);
|
|
24
23
|
});
|
|
25
24
|
|
|
@@ -35,55 +34,84 @@ describe('emptyUndoStack', () => {
|
|
|
35
34
|
});
|
|
36
35
|
});
|
|
37
36
|
|
|
38
|
-
describe('
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
37
|
+
describe('serializeTrials', () => {
|
|
38
|
+
it('returns records with the undo stack dropped', () => {
|
|
39
|
+
const records = serializeTrials(
|
|
40
|
+
[
|
|
41
|
+
{
|
|
42
|
+
id: 'w1',
|
|
43
|
+
instrumentName: 'Test',
|
|
44
|
+
config: { a: 1 },
|
|
45
|
+
state: { b: 2 },
|
|
46
|
+
view: { zoom: 1, pan: { x: 0, y: 0 } },
|
|
47
|
+
undoStack: { past: [{ b: 1 }], future: [] },
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
{},
|
|
51
|
+
);
|
|
52
|
+
expect(records).toEqual([
|
|
53
|
+
{
|
|
54
|
+
id: 'w1',
|
|
55
|
+
instrumentName: 'Test',
|
|
56
|
+
config: { a: 1 },
|
|
57
|
+
state: { b: 2 },
|
|
58
|
+
view: { zoom: 1, pan: { x: 0, y: 0 } },
|
|
59
|
+
},
|
|
60
|
+
]);
|
|
59
61
|
});
|
|
60
62
|
|
|
61
|
-
it('
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
it('runs the instrument serializer over the state', () => {
|
|
64
|
+
const records = serializeTrials(
|
|
65
|
+
[
|
|
66
|
+
{
|
|
67
|
+
id: 'w1',
|
|
68
|
+
instrumentName: 'Test',
|
|
69
|
+
config: {},
|
|
70
|
+
state: { n: 2 },
|
|
71
|
+
view: { zoom: 1, pan: { x: 0, y: 0 } },
|
|
72
|
+
undoStack: { past: [], future: [] },
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
{ Test: { serialize: (s) => ({ doubled: (s as { n: number }).n * 2 }) } },
|
|
76
|
+
);
|
|
77
|
+
expect(records[0].state).toEqual({ doubled: 4 });
|
|
65
78
|
});
|
|
79
|
+
});
|
|
66
80
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
81
|
+
describe('deserializeTrials', () => {
|
|
82
|
+
it('rebuilds records with an empty undo stack', () => {
|
|
83
|
+
const out = deserializeTrials(
|
|
84
|
+
[
|
|
85
|
+
{
|
|
86
|
+
id: 'w1',
|
|
87
|
+
instrumentName: 'Test',
|
|
88
|
+
config: {},
|
|
89
|
+
state: { n: 1 },
|
|
90
|
+
view: { zoom: 1, pan: { x: 0, y: 0 } },
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
{},
|
|
94
|
+
);
|
|
95
|
+
expect(out[0].undoStack).toEqual({ past: [], future: [] });
|
|
77
96
|
});
|
|
78
97
|
|
|
79
|
-
it('
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
98
|
+
it('runs the instrument deserializer over the state', () => {
|
|
99
|
+
const out = deserializeTrials(
|
|
100
|
+
[
|
|
101
|
+
{
|
|
102
|
+
id: 'w1',
|
|
103
|
+
instrumentName: 'Test',
|
|
104
|
+
config: {},
|
|
105
|
+
state: { doubled: 4 },
|
|
106
|
+
view: { zoom: 1, pan: { x: 0, y: 0 } },
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
{ Test: { deserialize: (d) => ({ n: (d as { doubled: number }).doubled / 2 }) } },
|
|
110
|
+
);
|
|
111
|
+
expect(out[0].state).toEqual({ n: 2 });
|
|
83
112
|
});
|
|
84
113
|
|
|
85
|
-
it('returns empty
|
|
86
|
-
|
|
87
|
-
expect(result).toEqual([]);
|
|
114
|
+
it('returns an empty list when given something that is not an array', () => {
|
|
115
|
+
expect(deserializeTrials(undefined as never, {})).toEqual([]);
|
|
88
116
|
});
|
|
89
117
|
});
|
package/src/state/helpers.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { InstrumentSerializers,
|
|
1
|
+
import type { InstrumentSerializers, SerializedTrial, TrialRecord, UndoStack } from './types';
|
|
2
2
|
|
|
3
3
|
/** The storage key a lab writes one of its buckets under. Namespaced by
|
|
4
4
|
* `storageKey` so two labs sharing an origin do not collide. */
|
|
@@ -30,44 +30,36 @@ export function emptyUndoStack(): UndoStack {
|
|
|
30
30
|
return { past: [], future: [] };
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
/** Serialize workspaces for storage, running each instrument's own serializer
|
|
33
|
+
/** Serialize trials for storage, running each instrument's own serializer
|
|
36
34
|
* over its state. Undo history is deliberately dropped — it does not survive
|
|
37
35
|
* a reload. */
|
|
38
|
-
export function
|
|
39
|
-
|
|
36
|
+
export function serializeTrials(
|
|
37
|
+
trials: TrialRecord[],
|
|
40
38
|
serializers: InstrumentSerializers,
|
|
41
|
-
):
|
|
42
|
-
|
|
39
|
+
): SerializedTrial[] {
|
|
40
|
+
return trials.map(({ undoStack: _undo, ...w }) => {
|
|
43
41
|
const s = serializers[w.instrumentName];
|
|
44
|
-
return {
|
|
45
|
-
...w,
|
|
46
|
-
state: s?.serialize ? s.serialize(w.state) : w.state,
|
|
47
|
-
};
|
|
42
|
+
return { ...w, state: s?.serialize ? s.serialize(w.state) : w.state };
|
|
48
43
|
});
|
|
49
|
-
return JSON.stringify(records);
|
|
50
44
|
}
|
|
51
45
|
|
|
52
|
-
/** Rebuild
|
|
46
|
+
/** Rebuild trials from storage, running each instrument's deserializer
|
|
53
47
|
* over its state and starting each with an empty undo history. Returns an
|
|
54
48
|
* empty list rather than throwing on malformed input. */
|
|
55
|
-
export function
|
|
56
|
-
|
|
49
|
+
export function deserializeTrials(
|
|
50
|
+
records: SerializedTrial[],
|
|
57
51
|
deserializers: InstrumentSerializers,
|
|
58
|
-
):
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return records.map((r) => {
|
|
62
|
-
const d = deserializers[r.instrumentName];
|
|
63
|
-
return {
|
|
64
|
-
...r,
|
|
65
|
-
state: d?.deserialize ? d.deserialize(r.state) : r.state,
|
|
66
|
-
undoStack: emptyUndoStack(),
|
|
67
|
-
};
|
|
68
|
-
});
|
|
69
|
-
} catch {
|
|
70
|
-
console.warn('[labkit] deserializeWorkspaces: failed to parse, returning empty list');
|
|
52
|
+
): TrialRecord[] {
|
|
53
|
+
if (!Array.isArray(records)) {
|
|
54
|
+
console.warn('[labkit] deserializeTrials: not an array, returning empty list');
|
|
71
55
|
return [];
|
|
72
56
|
}
|
|
57
|
+
return records.map((r) => {
|
|
58
|
+
const d = deserializers[r.instrumentName];
|
|
59
|
+
return {
|
|
60
|
+
...r,
|
|
61
|
+
state: d?.deserialize ? d.deserialize(r.state) : r.state,
|
|
62
|
+
undoStack: emptyUndoStack(),
|
|
63
|
+
};
|
|
64
|
+
});
|
|
73
65
|
}
|
package/src/state/index.ts
CHANGED
|
@@ -8,18 +8,23 @@ export {
|
|
|
8
8
|
export {
|
|
9
9
|
LabStoreContext,
|
|
10
10
|
LabStoreProvider,
|
|
11
|
+
TrialIdContext,
|
|
12
|
+
TrialIdProvider,
|
|
11
13
|
useLabStore,
|
|
12
|
-
|
|
13
|
-
WorkspaceIdContext,
|
|
14
|
-
WorkspaceIdProvider,
|
|
14
|
+
useTrialId,
|
|
15
15
|
} from './context';
|
|
16
|
+
export {
|
|
17
|
+
CURRENT_DOCUMENT_VERSION,
|
|
18
|
+
labDocumentKey,
|
|
19
|
+
quarantineKey,
|
|
20
|
+
} from './document';
|
|
16
21
|
export {
|
|
17
22
|
decodeUrlHash,
|
|
18
|
-
|
|
23
|
+
deserializeTrials,
|
|
19
24
|
emptyUndoStack,
|
|
20
25
|
encodeUrlHash,
|
|
21
26
|
labStorageKey,
|
|
22
|
-
|
|
27
|
+
serializeTrials,
|
|
23
28
|
} from './helpers';
|
|
24
29
|
export {
|
|
25
30
|
SingletonExperimentProvider,
|
|
@@ -29,12 +34,15 @@ export type { LabStore, LabStoreActions } from './store';
|
|
|
29
34
|
export { createLabStore } from './store';
|
|
30
35
|
export type {
|
|
31
36
|
CreateLabStoreOptions,
|
|
32
|
-
ExperimentStateHandle,
|
|
33
37
|
InstrumentSerializers,
|
|
38
|
+
LabDocument,
|
|
34
39
|
LabStoreState,
|
|
40
|
+
Migration,
|
|
35
41
|
SavedSnapshot,
|
|
42
|
+
SerializedTrial,
|
|
36
43
|
StorageAdapter,
|
|
44
|
+
TrialRecord,
|
|
45
|
+
TrialStateHandle,
|
|
37
46
|
UndoStack,
|
|
38
|
-
WorkspaceRecord,
|
|
39
47
|
} from './types';
|
|
40
|
-
export {
|
|
48
|
+
export { useTrialState } from './useTrialState';
|