@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.
Files changed (85) hide show
  1. package/README.md +6 -6
  2. package/dist/_dts/{index-CFlDPeZh.d.ts → index-iAP6XbH3.d.ts} +17 -7
  3. package/dist/_dts/{useExperimentState-D7EQnnwJ.d.ts → useTrialState-DYe2vUwN.d.ts} +60 -36
  4. package/dist/canvas/index.d.ts +1 -1
  5. package/dist/chunk-3TYUJR7Z.js +488 -0
  6. package/dist/chunk-3TYUJR7Z.js.map +1 -0
  7. package/dist/{chunk-BAPZPDDA.js → chunk-C6GJKPUI.js} +365 -340
  8. package/dist/chunk-C6GJKPUI.js.map +1 -0
  9. package/dist/{chunk-DWV7SFKR.js → chunk-DJLDIRFN.js} +28 -12
  10. package/dist/chunk-DJLDIRFN.js.map +1 -0
  11. package/dist/{chunk-DEWXYFEU.js → chunk-NRKWVTVT.js} +3 -3
  12. package/dist/{chunk-DEWXYFEU.js.map → chunk-NRKWVTVT.js.map} +1 -1
  13. package/dist/dragdrop/index.d.ts +1 -1
  14. package/dist/index.d.ts +149 -149
  15. package/dist/index.js +164 -159
  16. package/dist/index.js.map +1 -1
  17. package/dist/layers/index.d.ts +2 -2
  18. package/dist/passthrough/weasel-canvas.js +1 -1
  19. package/dist/passthrough/weasel-ui.d.ts +9 -2
  20. package/dist/passthrough/weasel-ui.js +2 -2
  21. package/dist/state/index.d.ts +12 -12
  22. package/dist/state/index.js +5 -5
  23. package/dist/state/index.js.map +1 -1
  24. package/dist/styles.css +21 -13
  25. package/dist/ui/layers/index.js +3 -3
  26. package/dist/undo/index.d.ts +1 -1
  27. package/package.json +1 -1
  28. package/src/canvas/AGENTS.md +6 -2
  29. package/src/dragdrop/dragDrop.test.tsx +4 -7
  30. package/src/index.test.ts +52 -0
  31. package/src/index.ts +11 -7
  32. package/src/instrument/SineWave.smoke.test.tsx +1 -1
  33. package/src/instrument/capabilityDetector.ts +1 -1
  34. package/src/instrument/types.ts +18 -6
  35. package/src/lab/Lab.less +16 -0
  36. package/src/lab/Lab.stories.tsx +5 -5
  37. package/src/lab/Lab.test.tsx +25 -25
  38. package/src/lab/Lab.tsx +50 -50
  39. package/src/lab/LabContext.ts +12 -12
  40. package/src/lab/LabShell.tsx +1 -1
  41. package/src/lab/{WorkspaceGrid.less → Workspace.less} +2 -2
  42. package/src/lab/{WorkspaceGrid.stories.tsx → Workspace.stories.tsx} +13 -13
  43. package/src/lab/{WorkspaceGrid.test.tsx → Workspace.test.tsx} +24 -24
  44. package/src/lab/{WorkspaceGrid.tsx → Workspace.tsx} +18 -18
  45. package/src/lab/index.ts +2 -2
  46. package/src/layers/AGENTS.md +3 -3
  47. package/src/primitives/Toolbar.stories.tsx +2 -2
  48. package/src/state/SingletonExperiment.test.tsx +8 -8
  49. package/src/state/SingletonExperiment.tsx +9 -9
  50. package/src/state/context.tsx +12 -12
  51. package/src/state/document.test.ts +369 -0
  52. package/src/state/document.ts +194 -0
  53. package/src/state/helpers.test.ts +74 -46
  54. package/src/state/helpers.ts +21 -29
  55. package/src/state/index.ts +16 -8
  56. package/src/state/store.test.ts +440 -41
  57. package/src/state/store.ts +159 -102
  58. package/src/state/types.ts +29 -12
  59. package/src/state/{useExperimentState.test.tsx → useTrialState.test.tsx} +18 -18
  60. package/src/state/useTrialState.ts +29 -0
  61. package/src/styles.less +2 -2
  62. package/src/theme/base.less +1 -1
  63. package/src/{workspace → trial}/DefaultSidebar.tsx +3 -3
  64. package/src/{workspace → trial}/DefaultStatusBar.tsx +5 -4
  65. package/src/{workspace → trial}/DefaultToolbar.tsx +9 -8
  66. package/src/trial/Trial.canvas.test.tsx +80 -0
  67. package/src/{workspace/Workspace.less → trial/Trial.less} +1 -1
  68. package/src/{workspace/Workspace.stories.tsx → trial/Trial.stories.tsx} +13 -18
  69. package/src/{workspace/Workspace.test.tsx → trial/Trial.test.tsx} +26 -26
  70. package/src/{workspace/Workspace.tsx → trial/Trial.tsx} +48 -43
  71. package/src/{workspace/WorkspaceChrome.tsx → trial/TrialChrome.tsx} +42 -42
  72. package/src/{workspace → trial}/index.ts +13 -13
  73. package/src/{workspace → trial}/slotTypes.ts +16 -16
  74. package/src/{workspace/workspaceOps.test.ts → trial/trialOps.test.ts} +38 -44
  75. package/src/trial/trialOps.ts +99 -0
  76. package/src/ui/format.test.ts +33 -0
  77. package/src/ui/format.ts +11 -0
  78. package/src/ui/properties/CurveField.tsx +6 -6
  79. package/src/ui/properties/SpeechBalloonPanels.stories.tsx +3 -3
  80. package/dist/chunk-BAPZPDDA.js.map +0 -1
  81. package/dist/chunk-DWV7SFKR.js.map +0 -1
  82. package/dist/chunk-KSTEW2AF.js +0 -367
  83. package/dist/chunk-KSTEW2AF.js.map +0 -1
  84. package/src/state/useExperimentState.ts +0 -31
  85. 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
- deserializeWorkspaces,
4
+ deserializeTrials,
5
5
  emptyUndoStack,
6
6
  encodeUrlHash,
7
7
  labStorageKey,
8
- serializeWorkspaces,
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({ workspaces: '[]', saves: '[]' });
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('serializeWorkspaces / deserializeWorkspaces', () => {
39
- const ws: WorkspaceRecord = {
40
- id: 'w1',
41
- instrumentName: 'Test',
42
- config: { x: 1 },
43
- state: { items: [] },
44
- view: { zoom: 1, pan: { x: 0, y: 0 } },
45
- undoStack: { past: [1, 2], future: [] },
46
- };
47
-
48
- it('round-trips workspace records', () => {
49
- const serialized = serializeWorkspaces([ws], {});
50
- const [result] = deserializeWorkspaces(serialized, {});
51
- expect(result?.id).toBe('w1');
52
- expect(result?.state).toEqual({ items: [] });
53
- });
54
-
55
- it('strips undoStack on serialization', () => {
56
- const serialized = serializeWorkspaces([ws], {});
57
- const parsed = JSON.parse(serialized) as unknown[];
58
- expect((parsed[0] as Record<string, unknown>).undoStack).toBeUndefined();
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('restores emptyUndoStack after deserialization', () => {
62
- const serialized = serializeWorkspaces([ws], {});
63
- const [result] = deserializeWorkspaces(serialized, {});
64
- expect(result?.undoStack).toEqual({ past: [], future: [] });
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
- it('uses instrument.serialize / deserialize when provided', () => {
68
- const serializers = {
69
- Test: {
70
- serialize: (s: unknown) => ({ compressed: true, data: s }),
71
- deserialize: (d: unknown) => (d as { data: unknown }).data,
72
- },
73
- };
74
- const serialized = serializeWorkspaces([ws], serializers);
75
- const [result] = deserializeWorkspaces(serialized, serializers);
76
- expect(result?.state).toEqual({ items: [] });
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('falls back to identity when instrumentName not in registry', () => {
80
- const serialized = serializeWorkspaces([ws], {});
81
- const [result] = deserializeWorkspaces(serialized, {});
82
- expect(result?.state).toEqual({ items: [] });
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 array for malformed JSON', () => {
86
- const result = deserializeWorkspaces('NOT JSON', {});
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
  });
@@ -1,4 +1,4 @@
1
- import type { InstrumentSerializers, UndoStack, WorkspaceRecord } from './types';
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
- type SerializedRecord = Omit<WorkspaceRecord, 'undoStack'>;
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 serializeWorkspaces(
39
- workspaces: WorkspaceRecord[],
36
+ export function serializeTrials(
37
+ trials: TrialRecord[],
40
38
  serializers: InstrumentSerializers,
41
- ): string {
42
- const records: SerializedRecord[] = workspaces.map(({ undoStack: _undo, ...w }) => {
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 workspaces from storage, running each instrument's deserializer
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 deserializeWorkspaces(
56
- raw: string,
49
+ export function deserializeTrials(
50
+ records: SerializedTrial[],
57
51
  deserializers: InstrumentSerializers,
58
- ): WorkspaceRecord[] {
59
- try {
60
- const records = JSON.parse(raw) as SerializedRecord[];
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
  }
@@ -8,18 +8,23 @@ export {
8
8
  export {
9
9
  LabStoreContext,
10
10
  LabStoreProvider,
11
+ TrialIdContext,
12
+ TrialIdProvider,
11
13
  useLabStore,
12
- useWorkspaceId,
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
- deserializeWorkspaces,
23
+ deserializeTrials,
19
24
  emptyUndoStack,
20
25
  encodeUrlHash,
21
26
  labStorageKey,
22
- serializeWorkspaces,
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 { useExperimentState } from './useExperimentState';
48
+ export { useTrialState } from './useTrialState';