@sqlrooms/room-store 0.29.0-rc.6 → 0.29.0-rc.7

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 CHANGED
@@ -16,6 +16,8 @@ npm install @sqlrooms/room-store
16
16
  - generic slice helper: `createSlice()`
17
17
  - React context/hooks: `RoomStateProvider`, `useBaseRoomStore`, `useRoomStoreApi`
18
18
  - persistence helpers: `persistSliceConfigs()`, `createPersistHelpers()`
19
+ - room-store persistence glue: `createRoomStorePersistence()`
20
+ - persistence controller: `createPersistenceController()`
19
21
 
20
22
  ## Quick start
21
23
 
@@ -101,6 +103,44 @@ export function incrementLater() {
101
103
  }
102
104
  ```
103
105
 
106
+ ## Persistence
107
+
108
+ For a Zustand room store with host-owned storage, prefer
109
+ `createRoomStorePersistence()`. It composes `createPersistHelpers()` with a
110
+ controller-backed `PersistStorage`, rehydrate saved-snapshot marking, optional
111
+ room-store subscription, autosave, and final flush helpers. This is the default
112
+ entry point for SQLRooms apps that persist room state to DuckDB, files, or another
113
+ project-owned store. See the
114
+ [Persistence developer guide](https://sqlrooms.org/persistence.html) for the
115
+ full integration model, data flow, and examples.
116
+
117
+ Use the lower-level `createPersistenceController()` only when you need the same
118
+ persistence policy outside a room store or Zustand persist. The controller is
119
+ storage-agnostic: hosts provide `load()` and `save()` adapter functions, while
120
+ SQLRooms handles hydration state, dirty tracking, scheduled saves, final flush,
121
+ in-flight save coalescing, and observable save status.
122
+
123
+ `createPersistHelpers()` still only handles schema-based partialization and
124
+ rehydrate merging. Let `createRoomStorePersistence()` combine those helpers with
125
+ save policy unless you have a custom integration that does not fit the room-store
126
+ helper.
127
+
128
+ ```ts
129
+ import {createRoomStorePersistence} from '@sqlrooms/room-store';
130
+
131
+ const persistence = createRoomStorePersistence({
132
+ partialize: (state) => ({room: state.room.config}),
133
+ autosaveDelayMs: 300,
134
+ load: async () => loadProjectSnapshot(),
135
+ save: async (snapshot, metadata) => {
136
+ await saveProjectSnapshot(snapshot, metadata?.reason);
137
+ },
138
+ });
139
+
140
+ await persistence.hydrate();
141
+ await persistence.flush('final-flush');
142
+ ```
143
+
104
144
  Inside components, `useRoomStoreApi()` gives you the raw store API:
105
145
 
106
146
  ```tsx
@@ -0,0 +1,88 @@
1
+ export type PersistenceSaveReason = 'autosave' | 'manual' | 'flush' | 'hydrate' | 'setItem' | (string & {});
2
+ export type PersistenceSaveMetadata = {
3
+ reason: PersistenceSaveReason;
4
+ };
5
+ export type PersistenceAdapter<TSnapshot> = {
6
+ load: () => Promise<TSnapshot | null>;
7
+ save: (snapshot: TSnapshot, metadata?: PersistenceSaveMetadata) => Promise<void>;
8
+ };
9
+ export type PersistenceControllerState = {
10
+ /** True while the adapter is loading a snapshot into memory. */
11
+ hydrating: boolean;
12
+ /**
13
+ * True when the controller has a snapshot that differs from the saved
14
+ * snapshot and should be persisted by autosave, `saveNow`, or `flush`.
15
+ */
16
+ dirty: boolean;
17
+ /** True while an adapter `save` call is in flight. */
18
+ saving: boolean;
19
+ /** Last adapter load/save error, or null after a successful save/snapshot mark. */
20
+ error: unknown;
21
+ /** Last reason passed to a save or dirty-marking operation. */
22
+ lastSaveReason: PersistenceSaveReason | null;
23
+ /** Unix timestamp, in milliseconds, of the last successful adapter save. */
24
+ lastSavedAt: number | null;
25
+ /**
26
+ * Monotonic counter for changes to the saved snapshot.
27
+ *
28
+ * This is the snapshot the controller currently considers clean: either the
29
+ * snapshot loaded during hydration, the snapshot passed to `markSnapshotSaved`,
30
+ * or the latest snapshot successfully written by the adapter.
31
+ * Consumers can watch this value to know that the clean saved state changed
32
+ * without reading or comparing the snapshot itself.
33
+ */
34
+ savedSnapshotVersion: number;
35
+ /**
36
+ * True when a save was requested while another save was already in flight.
37
+ * The controller will coalesce those requests and persist the latest snapshot.
38
+ */
39
+ pendingSave: boolean;
40
+ };
41
+ export type PersistenceControllerListener = (state: PersistenceControllerState) => void;
42
+ export type PersistenceController<TSnapshot> = {
43
+ /**
44
+ * Loads the adapter snapshot and treats it as the saved snapshot.
45
+ *
46
+ * Hydration does not mark the controller dirty. Hosts should merge the loaded
47
+ * snapshot into their runtime store, then call `markSnapshotSaved` if the merged
48
+ * runtime shape differs from the raw loaded snapshot.
49
+ */
50
+ hydrate: () => Promise<TSnapshot | null>;
51
+ /**
52
+ * Marks a snapshot as clean without writing it.
53
+ *
54
+ * Use this after hydration or other trusted reconciliation work once runtime
55
+ * state reflects durable storage and should not be interpreted as a user edit.
56
+ */
57
+ markSnapshotSaved: (snapshot: TSnapshot | null) => void;
58
+ /**
59
+ * Provides the latest snapshot to save and marks it dirty when it differs
60
+ * from the saved snapshot.
61
+ */
62
+ setSnapshot: (snapshot: TSnapshot, reason?: PersistenceSaveReason) => void;
63
+ /** Marks the current `getSnapshot` result dirty without providing it eagerly. */
64
+ markDirty: (reason?: PersistenceSaveReason) => void;
65
+ /** Saves the dirty snapshot immediately, bypassing autosave delay. */
66
+ saveNow: (reason?: PersistenceSaveReason) => Promise<void>;
67
+ /** Flushes pending dirty state before unload, close, or project switch. */
68
+ flush: (reason?: PersistenceSaveReason) => Promise<void>;
69
+ /**
70
+ * Runs work while dirty marking and snapshot updates are ignored.
71
+ *
72
+ * Use this around hydration or programmatic restore flows that should not be
73
+ * treated as user edits.
74
+ */
75
+ pause: <TResult>(fn: () => TResult | Promise<TResult>) => Promise<TResult>;
76
+ /** Returns a copy of the current observable controller state. */
77
+ getState: () => PersistenceControllerState;
78
+ /** Subscribes to observable controller state changes. */
79
+ subscribe: (listener: PersistenceControllerListener) => () => void;
80
+ };
81
+ export type CreatePersistenceControllerOptions<TSnapshot> = {
82
+ adapter: PersistenceAdapter<TSnapshot>;
83
+ getSnapshot?: () => TSnapshot | Promise<TSnapshot>;
84
+ autosaveDelayMs?: number | null;
85
+ now?: () => number;
86
+ };
87
+ export declare function createPersistenceController<TSnapshot>({ adapter, getSnapshot, autosaveDelayMs, now, }: CreatePersistenceControllerOptions<TSnapshot>): PersistenceController<TSnapshot>;
88
+ //# sourceMappingURL=PersistenceController.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PersistenceController.d.ts","sourceRoot":"","sources":["../src/PersistenceController.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,qBAAqB,GAC7B,UAAU,GACV,QAAQ,GACR,OAAO,GACP,SAAS,GACT,SAAS,GACT,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;AAElB,MAAM,MAAM,uBAAuB,GAAG;IACpC,MAAM,EAAE,qBAAqB,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,kBAAkB,CAAC,SAAS,IAAI;IAC1C,IAAI,EAAE,MAAM,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IACtC,IAAI,EAAE,CACJ,QAAQ,EAAE,SAAS,EACnB,QAAQ,CAAC,EAAE,uBAAuB,KAC/B,OAAO,CAAC,IAAI,CAAC,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG;IACvC,gEAAgE;IAChE,SAAS,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,KAAK,EAAE,OAAO,CAAC;IACf,sDAAsD;IACtD,MAAM,EAAE,OAAO,CAAC;IAChB,mFAAmF;IACnF,KAAK,EAAE,OAAO,CAAC;IACf,+DAA+D;IAC/D,cAAc,EAAE,qBAAqB,GAAG,IAAI,CAAC;IAC7C,4EAA4E;IAC5E,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B;;;;;;;;OAQG;IACH,oBAAoB,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,6BAA6B,GAAG,CAC1C,KAAK,EAAE,0BAA0B,KAC9B,IAAI,CAAC;AAEV,MAAM,MAAM,qBAAqB,CAAC,SAAS,IAAI;IAC7C;;;;;;OAMG;IACH,OAAO,EAAE,MAAM,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IACzC;;;;;OAKG;IACH,iBAAiB,EAAE,CAAC,QAAQ,EAAE,SAAS,GAAG,IAAI,KAAK,IAAI,CAAC;IACxD;;;OAGG;IACH,WAAW,EAAE,CAAC,QAAQ,EAAE,SAAS,EAAE,MAAM,CAAC,EAAE,qBAAqB,KAAK,IAAI,CAAC;IAC3E,iFAAiF;IACjF,SAAS,EAAE,CAAC,MAAM,CAAC,EAAE,qBAAqB,KAAK,IAAI,CAAC;IACpD,sEAAsE;IACtE,OAAO,EAAE,CAAC,MAAM,CAAC,EAAE,qBAAqB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,2EAA2E;IAC3E,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,qBAAqB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzD;;;;;OAKG;IACH,KAAK,EAAE,CAAC,OAAO,EAAE,EAAE,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3E,iEAAiE;IACjE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;IAC3C,yDAAyD;IACzD,SAAS,EAAE,CAAC,QAAQ,EAAE,6BAA6B,KAAK,MAAM,IAAI,CAAC;CACpE,CAAC;AAEF,MAAM,MAAM,kCAAkC,CAAC,SAAS,IAAI;IAC1D,OAAO,EAAE,kBAAkB,CAAC,SAAS,CAAC,CAAC;IACvC,WAAW,CAAC,EAAE,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;IACnD,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB,CAAC;AAYF,wBAAgB,2BAA2B,CAAC,SAAS,EAAE,EACrD,OAAO,EACP,WAAW,EACX,eAAsB,EACtB,GAAsB,GACvB,EAAE,kCAAkC,CAAC,SAAS,CAAC,GAAG,qBAAqB,CAAC,SAAS,CAAC,CAuPlF"}
@@ -0,0 +1,243 @@
1
+ const MissingSnapshotSourceError = new Error('Persistence controller cannot mark dirty without a pending snapshot or getSnapshot option.');
2
+ function cloneState(state) {
3
+ return { ...state };
4
+ }
5
+ export function createPersistenceController({ adapter, getSnapshot, autosaveDelayMs = null, now = () => Date.now(), }) {
6
+ // Snapshot last known to match durable storage. A new snapshot equal to this
7
+ // value is clean; a different snapshot is dirty and eligible for saving.
8
+ let lastSavedSnapshot = null;
9
+ let pendingSnapshot;
10
+ let inFlightSnapshot;
11
+ let pauseDepth = 0;
12
+ let timer = null;
13
+ let saveInFlight = null;
14
+ let pendingSaveReason = null;
15
+ const listeners = new Set();
16
+ const state = {
17
+ hydrating: false,
18
+ dirty: false,
19
+ saving: false,
20
+ error: null,
21
+ lastSaveReason: null,
22
+ lastSavedAt: null,
23
+ savedSnapshotVersion: 0,
24
+ pendingSave: false,
25
+ };
26
+ const notify = () => {
27
+ const snapshot = cloneState(state);
28
+ listeners.forEach((listener) => listener(snapshot));
29
+ };
30
+ const setState = (patch) => {
31
+ Object.assign(state, patch);
32
+ notify();
33
+ };
34
+ const clearTimer = () => {
35
+ if (timer) {
36
+ clearTimeout(timer);
37
+ timer = null;
38
+ }
39
+ };
40
+ const canPersistChange = () => !state.hydrating && pauseDepth === 0;
41
+ const hasSnapshotSource = () => pendingSnapshot !== undefined || getSnapshot !== undefined;
42
+ const scheduleAutosave = () => {
43
+ if (autosaveDelayMs === null || autosaveDelayMs === undefined)
44
+ return;
45
+ if (!state.dirty || !canPersistChange())
46
+ return;
47
+ clearTimer();
48
+ timer = setTimeout(() => {
49
+ timer = null;
50
+ void controller.saveNow('autosave').catch(() => {
51
+ // saveNow records the adapter error in controller state; suppress the
52
+ // timer task rejection so autosave failures do not become unhandled.
53
+ });
54
+ }, autosaveDelayMs);
55
+ };
56
+ const resolveSnapshot = async () => {
57
+ if (pendingSnapshot !== undefined) {
58
+ return pendingSnapshot;
59
+ }
60
+ if (getSnapshot) {
61
+ return getSnapshot();
62
+ }
63
+ return undefined;
64
+ };
65
+ const runSaveLoop = async (reason) => {
66
+ if (saveInFlight) {
67
+ if (pendingSnapshot !== undefined || state.pendingSave) {
68
+ pendingSaveReason = reason;
69
+ setState({ pendingSave: true });
70
+ }
71
+ return saveInFlight;
72
+ }
73
+ saveInFlight = (async () => {
74
+ let activeReason = reason;
75
+ setState({ saving: true, pendingSave: false });
76
+ try {
77
+ do {
78
+ activeReason = pendingSaveReason ?? activeReason;
79
+ pendingSaveReason = null;
80
+ setState({ pendingSave: false });
81
+ const snapshot = await resolveSnapshot();
82
+ if (snapshot === undefined || !state.dirty) {
83
+ if (state.dirty) {
84
+ setState({
85
+ dirty: false,
86
+ error: MissingSnapshotSourceError,
87
+ pendingSave: false,
88
+ });
89
+ }
90
+ continue;
91
+ }
92
+ pendingSnapshot = undefined;
93
+ inFlightSnapshot = snapshot;
94
+ try {
95
+ await adapter.save(snapshot, { reason: activeReason });
96
+ }
97
+ finally {
98
+ inFlightSnapshot = undefined;
99
+ }
100
+ lastSavedSnapshot = snapshot;
101
+ const hasNewSnapshot = pendingSnapshot !== undefined;
102
+ setState({
103
+ dirty: hasNewSnapshot || state.pendingSave,
104
+ error: null,
105
+ lastSaveReason: activeReason,
106
+ lastSavedAt: now(),
107
+ savedSnapshotVersion: state.savedSnapshotVersion + 1,
108
+ });
109
+ } while (state.pendingSave || pendingSnapshot !== undefined);
110
+ }
111
+ catch (error) {
112
+ setState({ error });
113
+ throw error;
114
+ }
115
+ finally {
116
+ setState({ saving: false });
117
+ saveInFlight = null;
118
+ pendingSaveReason = null;
119
+ if (state.dirty && state.pendingSave) {
120
+ scheduleAutosave();
121
+ }
122
+ }
123
+ })();
124
+ return saveInFlight;
125
+ };
126
+ const controller = {
127
+ hydrate: async () => {
128
+ clearTimer();
129
+ setState({ hydrating: true, error: null });
130
+ try {
131
+ const snapshot = await adapter.load();
132
+ lastSavedSnapshot = snapshot;
133
+ pendingSnapshot = undefined;
134
+ setState({
135
+ dirty: false,
136
+ hydrating: false,
137
+ savedSnapshotVersion: state.savedSnapshotVersion + 1,
138
+ });
139
+ return snapshot;
140
+ }
141
+ catch (error) {
142
+ setState({ error, hydrating: false });
143
+ throw error;
144
+ }
145
+ },
146
+ markSnapshotSaved: (snapshot) => {
147
+ clearTimer();
148
+ lastSavedSnapshot = snapshot;
149
+ pendingSnapshot = undefined;
150
+ setState({
151
+ dirty: false,
152
+ pendingSave: false,
153
+ error: null,
154
+ savedSnapshotVersion: state.savedSnapshotVersion + 1,
155
+ });
156
+ },
157
+ setSnapshot: (snapshot, reason = 'setItem') => {
158
+ if (!canPersistChange())
159
+ return;
160
+ const hasConflictingSaveInFlight = inFlightSnapshot !== undefined &&
161
+ inFlightSnapshot !== lastSavedSnapshot;
162
+ if (snapshot === lastSavedSnapshot && !hasConflictingSaveInFlight) {
163
+ controller.markSnapshotSaved(snapshot);
164
+ return;
165
+ }
166
+ pendingSnapshot = snapshot;
167
+ if (state.saving) {
168
+ pendingSaveReason = reason;
169
+ }
170
+ setState({
171
+ dirty: true,
172
+ error: null,
173
+ lastSaveReason: reason,
174
+ pendingSave: state.pendingSave || state.saving,
175
+ });
176
+ scheduleAutosave();
177
+ },
178
+ markDirty: (reason = 'manual') => {
179
+ if (!canPersistChange())
180
+ return;
181
+ if (!hasSnapshotSource()) {
182
+ setState({
183
+ dirty: false,
184
+ error: MissingSnapshotSourceError,
185
+ lastSaveReason: reason,
186
+ });
187
+ return;
188
+ }
189
+ if (state.saving) {
190
+ pendingSaveReason = reason;
191
+ }
192
+ setState({
193
+ dirty: true,
194
+ error: null,
195
+ lastSaveReason: reason,
196
+ pendingSave: state.pendingSave || state.saving,
197
+ });
198
+ scheduleAutosave();
199
+ },
200
+ saveNow: async (reason = 'manual') => {
201
+ clearTimer();
202
+ if (!canPersistChange() || !state.dirty)
203
+ return;
204
+ await runSaveLoop(reason);
205
+ },
206
+ flush: async (reason = 'flush') => {
207
+ clearTimer();
208
+ if (!state.dirty && !saveInFlight)
209
+ return;
210
+ if (state.hydrating || pauseDepth > 0)
211
+ return;
212
+ if (!state.dirty && saveInFlight) {
213
+ await saveInFlight;
214
+ return;
215
+ }
216
+ await runSaveLoop(reason);
217
+ },
218
+ pause: async (fn) => {
219
+ if (pauseDepth === 0) {
220
+ clearTimer();
221
+ }
222
+ pauseDepth += 1;
223
+ try {
224
+ return await fn();
225
+ }
226
+ finally {
227
+ pauseDepth -= 1;
228
+ if (pauseDepth === 0 && state.dirty) {
229
+ scheduleAutosave();
230
+ }
231
+ }
232
+ },
233
+ getState: () => cloneState(state),
234
+ subscribe: (listener) => {
235
+ listeners.add(listener);
236
+ return () => {
237
+ listeners.delete(listener);
238
+ };
239
+ },
240
+ };
241
+ return controller;
242
+ }
243
+ //# sourceMappingURL=PersistenceController.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PersistenceController.js","sourceRoot":"","sources":["../src/PersistenceController.ts"],"names":[],"mappings":"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","sourcesContent":["export type PersistenceSaveReason =\n | 'autosave'\n | 'manual'\n | 'flush'\n | 'hydrate'\n | 'setItem'\n | (string & {});\n\nexport type PersistenceSaveMetadata = {\n reason: PersistenceSaveReason;\n};\n\nexport type PersistenceAdapter<TSnapshot> = {\n load: () => Promise<TSnapshot | null>;\n save: (\n snapshot: TSnapshot,\n metadata?: PersistenceSaveMetadata,\n ) => Promise<void>;\n};\n\nexport type PersistenceControllerState = {\n /** True while the adapter is loading a snapshot into memory. */\n hydrating: boolean;\n /**\n * True when the controller has a snapshot that differs from the saved\n * snapshot and should be persisted by autosave, `saveNow`, or `flush`.\n */\n dirty: boolean;\n /** True while an adapter `save` call is in flight. */\n saving: boolean;\n /** Last adapter load/save error, or null after a successful save/snapshot mark. */\n error: unknown;\n /** Last reason passed to a save or dirty-marking operation. */\n lastSaveReason: PersistenceSaveReason | null;\n /** Unix timestamp, in milliseconds, of the last successful adapter save. */\n lastSavedAt: number | null;\n /**\n * Monotonic counter for changes to the saved snapshot.\n *\n * This is the snapshot the controller currently considers clean: either the\n * snapshot loaded during hydration, the snapshot passed to `markSnapshotSaved`,\n * or the latest snapshot successfully written by the adapter.\n * Consumers can watch this value to know that the clean saved state changed\n * without reading or comparing the snapshot itself.\n */\n savedSnapshotVersion: number;\n /**\n * True when a save was requested while another save was already in flight.\n * The controller will coalesce those requests and persist the latest snapshot.\n */\n pendingSave: boolean;\n};\n\nexport type PersistenceControllerListener = (\n state: PersistenceControllerState,\n) => void;\n\nexport type PersistenceController<TSnapshot> = {\n /**\n * Loads the adapter snapshot and treats it as the saved snapshot.\n *\n * Hydration does not mark the controller dirty. Hosts should merge the loaded\n * snapshot into their runtime store, then call `markSnapshotSaved` if the merged\n * runtime shape differs from the raw loaded snapshot.\n */\n hydrate: () => Promise<TSnapshot | null>;\n /**\n * Marks a snapshot as clean without writing it.\n *\n * Use this after hydration or other trusted reconciliation work once runtime\n * state reflects durable storage and should not be interpreted as a user edit.\n */\n markSnapshotSaved: (snapshot: TSnapshot | null) => void;\n /**\n * Provides the latest snapshot to save and marks it dirty when it differs\n * from the saved snapshot.\n */\n setSnapshot: (snapshot: TSnapshot, reason?: PersistenceSaveReason) => void;\n /** Marks the current `getSnapshot` result dirty without providing it eagerly. */\n markDirty: (reason?: PersistenceSaveReason) => void;\n /** Saves the dirty snapshot immediately, bypassing autosave delay. */\n saveNow: (reason?: PersistenceSaveReason) => Promise<void>;\n /** Flushes pending dirty state before unload, close, or project switch. */\n flush: (reason?: PersistenceSaveReason) => Promise<void>;\n /**\n * Runs work while dirty marking and snapshot updates are ignored.\n *\n * Use this around hydration or programmatic restore flows that should not be\n * treated as user edits.\n */\n pause: <TResult>(fn: () => TResult | Promise<TResult>) => Promise<TResult>;\n /** Returns a copy of the current observable controller state. */\n getState: () => PersistenceControllerState;\n /** Subscribes to observable controller state changes. */\n subscribe: (listener: PersistenceControllerListener) => () => void;\n};\n\nexport type CreatePersistenceControllerOptions<TSnapshot> = {\n adapter: PersistenceAdapter<TSnapshot>;\n getSnapshot?: () => TSnapshot | Promise<TSnapshot>;\n autosaveDelayMs?: number | null;\n now?: () => number;\n};\n\nconst MissingSnapshotSourceError = new Error(\n 'Persistence controller cannot mark dirty without a pending snapshot or getSnapshot option.',\n);\n\nfunction cloneState(\n state: PersistenceControllerState,\n): PersistenceControllerState {\n return {...state};\n}\n\nexport function createPersistenceController<TSnapshot>({\n adapter,\n getSnapshot,\n autosaveDelayMs = null,\n now = () => Date.now(),\n}: CreatePersistenceControllerOptions<TSnapshot>): PersistenceController<TSnapshot> {\n // Snapshot last known to match durable storage. A new snapshot equal to this\n // value is clean; a different snapshot is dirty and eligible for saving.\n let lastSavedSnapshot: TSnapshot | null = null;\n let pendingSnapshot: TSnapshot | undefined;\n let inFlightSnapshot: TSnapshot | undefined;\n let pauseDepth = 0;\n let timer: ReturnType<typeof setTimeout> | null = null;\n let saveInFlight: Promise<void> | null = null;\n let pendingSaveReason: PersistenceSaveReason | null = null;\n const listeners = new Set<PersistenceControllerListener>();\n const state: PersistenceControllerState = {\n hydrating: false,\n dirty: false,\n saving: false,\n error: null,\n lastSaveReason: null,\n lastSavedAt: null,\n savedSnapshotVersion: 0,\n pendingSave: false,\n };\n\n const notify = () => {\n const snapshot = cloneState(state);\n listeners.forEach((listener) => listener(snapshot));\n };\n\n const setState = (patch: Partial<PersistenceControllerState>) => {\n Object.assign(state, patch);\n notify();\n };\n\n const clearTimer = () => {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n };\n\n const canPersistChange = () => !state.hydrating && pauseDepth === 0;\n\n const hasSnapshotSource = () =>\n pendingSnapshot !== undefined || getSnapshot !== undefined;\n\n const scheduleAutosave = () => {\n if (autosaveDelayMs === null || autosaveDelayMs === undefined) return;\n if (!state.dirty || !canPersistChange()) return;\n clearTimer();\n timer = setTimeout(() => {\n timer = null;\n void controller.saveNow('autosave').catch(() => {\n // saveNow records the adapter error in controller state; suppress the\n // timer task rejection so autosave failures do not become unhandled.\n });\n }, autosaveDelayMs);\n };\n\n const resolveSnapshot = async (): Promise<TSnapshot | undefined> => {\n if (pendingSnapshot !== undefined) {\n return pendingSnapshot;\n }\n if (getSnapshot) {\n return getSnapshot();\n }\n return undefined;\n };\n\n const runSaveLoop = async (reason: PersistenceSaveReason) => {\n if (saveInFlight) {\n if (pendingSnapshot !== undefined || state.pendingSave) {\n pendingSaveReason = reason;\n setState({pendingSave: true});\n }\n return saveInFlight;\n }\n\n saveInFlight = (async () => {\n let activeReason = reason;\n setState({saving: true, pendingSave: false});\n try {\n do {\n activeReason = pendingSaveReason ?? activeReason;\n pendingSaveReason = null;\n setState({pendingSave: false});\n const snapshot = await resolveSnapshot();\n if (snapshot === undefined || !state.dirty) {\n if (state.dirty) {\n setState({\n dirty: false,\n error: MissingSnapshotSourceError,\n pendingSave: false,\n });\n }\n continue;\n }\n pendingSnapshot = undefined;\n inFlightSnapshot = snapshot;\n try {\n await adapter.save(snapshot, {reason: activeReason});\n } finally {\n inFlightSnapshot = undefined;\n }\n lastSavedSnapshot = snapshot;\n const hasNewSnapshot = pendingSnapshot !== undefined;\n setState({\n dirty: hasNewSnapshot || state.pendingSave,\n error: null,\n lastSaveReason: activeReason,\n lastSavedAt: now(),\n savedSnapshotVersion: state.savedSnapshotVersion + 1,\n });\n } while (state.pendingSave || pendingSnapshot !== undefined);\n } catch (error) {\n setState({error});\n throw error;\n } finally {\n setState({saving: false});\n saveInFlight = null;\n pendingSaveReason = null;\n if (state.dirty && state.pendingSave) {\n scheduleAutosave();\n }\n }\n })();\n\n return saveInFlight;\n };\n\n const controller: PersistenceController<TSnapshot> = {\n hydrate: async () => {\n clearTimer();\n setState({hydrating: true, error: null});\n try {\n const snapshot = await adapter.load();\n lastSavedSnapshot = snapshot;\n pendingSnapshot = undefined;\n setState({\n dirty: false,\n hydrating: false,\n savedSnapshotVersion: state.savedSnapshotVersion + 1,\n });\n return snapshot;\n } catch (error) {\n setState({error, hydrating: false});\n throw error;\n }\n },\n\n markSnapshotSaved: (snapshot) => {\n clearTimer();\n lastSavedSnapshot = snapshot;\n pendingSnapshot = undefined;\n setState({\n dirty: false,\n pendingSave: false,\n error: null,\n savedSnapshotVersion: state.savedSnapshotVersion + 1,\n });\n },\n\n setSnapshot: (snapshot, reason = 'setItem') => {\n if (!canPersistChange()) return;\n const hasConflictingSaveInFlight =\n inFlightSnapshot !== undefined &&\n inFlightSnapshot !== lastSavedSnapshot;\n if (snapshot === lastSavedSnapshot && !hasConflictingSaveInFlight) {\n controller.markSnapshotSaved(snapshot);\n return;\n }\n pendingSnapshot = snapshot;\n if (state.saving) {\n pendingSaveReason = reason;\n }\n setState({\n dirty: true,\n error: null,\n lastSaveReason: reason,\n pendingSave: state.pendingSave || state.saving,\n });\n scheduleAutosave();\n },\n\n markDirty: (reason = 'manual') => {\n if (!canPersistChange()) return;\n if (!hasSnapshotSource()) {\n setState({\n dirty: false,\n error: MissingSnapshotSourceError,\n lastSaveReason: reason,\n });\n return;\n }\n if (state.saving) {\n pendingSaveReason = reason;\n }\n setState({\n dirty: true,\n error: null,\n lastSaveReason: reason,\n pendingSave: state.pendingSave || state.saving,\n });\n scheduleAutosave();\n },\n\n saveNow: async (reason = 'manual') => {\n clearTimer();\n if (!canPersistChange() || !state.dirty) return;\n await runSaveLoop(reason);\n },\n\n flush: async (reason = 'flush') => {\n clearTimer();\n if (!state.dirty && !saveInFlight) return;\n if (state.hydrating || pauseDepth > 0) return;\n if (!state.dirty && saveInFlight) {\n await saveInFlight;\n return;\n }\n await runSaveLoop(reason);\n },\n\n pause: async (fn) => {\n if (pauseDepth === 0) {\n clearTimer();\n }\n pauseDepth += 1;\n try {\n return await fn();\n } finally {\n pauseDepth -= 1;\n if (pauseDepth === 0 && state.dirty) {\n scheduleAutosave();\n }\n }\n },\n\n getState: () => cloneState(state),\n\n subscribe: (listener) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n\n return controller;\n}\n"]}
@@ -1,12 +1,21 @@
1
- import { PersistOptions } from 'zustand/middleware';
2
1
  import z from 'zod';
2
+ import { PersistOptions } from 'zustand/middleware';
3
3
  import { StateCreator } from './BaseRoomStore';
4
+ type PersistedSliceConfigs<T extends Record<string, z.ZodType>> = {
5
+ [K in keyof T]: z.infer<T[K]>;
6
+ };
4
7
  /**
5
8
  * Creates partialize and merge functions for Zustand persist middleware.
6
9
  * Automatically handles extracting and merging slice configs.
7
10
  *
8
11
  * @param sliceConfigs - Map of slice names to their Zod config schemas
9
12
  * @returns Object with partialize and merge functions
13
+ * - `partialize`: serializes `state[slice].config` for each configured slice
14
+ * - `merge`: rehydrates each slice config from persisted storage
15
+ *
16
+ * `merge` supports schema-level customization via an internal symbol marker.
17
+ * When present on a schema, the marker function receives the current defaults and
18
+ * persisted value and can return custom parse input for `schema.parse(...)`.
10
19
  *
11
20
  * @example
12
21
  * ```ts
@@ -29,7 +38,7 @@ import { StateCreator } from './BaseRoomStore';
29
38
  * ```
30
39
  */
31
40
  export declare function createPersistHelpers<T extends Record<string, z.ZodType>>(sliceConfigs: T): {
32
- partialize: (state: any) => Record<string, any>;
41
+ partialize: (state: any) => PersistedSliceConfigs<T>;
33
42
  merge: (persistedState: any, currentState: any) => any;
34
43
  };
35
44
  /**
@@ -99,9 +108,10 @@ export declare function createPersistHelpers<T extends Record<string, z.ZodType>
99
108
  * );
100
109
  * ```
101
110
  */
102
- export declare function persistSliceConfigs<S>(options: {
103
- sliceConfigSchemas: Record<string, z.ZodType>;
104
- partialize?: (state: S) => Partial<S>;
111
+ export declare function persistSliceConfigs<S, TSliceConfigs extends Record<string, z.ZodType> = Record<string, z.ZodType>, PersistedState = PersistedSliceConfigs<TSliceConfigs>>(options: {
112
+ sliceConfigSchemas: TSliceConfigs;
113
+ partialize?: (state: S) => PersistedState;
105
114
  merge?: (persistedState: unknown, currentState: S) => S;
106
- } & Omit<PersistOptions<S>, 'partialize' | 'merge'>, stateCreator: StateCreator<S>): StateCreator<S>;
115
+ } & Omit<PersistOptions<S, PersistedState>, 'partialize' | 'merge'>, stateCreator: StateCreator<S>): StateCreator<S>;
116
+ export {};
107
117
  //# sourceMappingURL=createPersistHelpers.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"createPersistHelpers.d.ts","sourceRoot":"","sources":["../src/createPersistHelpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,cAAc,EAAC,MAAM,oBAAoB,CAAC;AAC3D,OAAO,CAAC,MAAM,KAAK,CAAC;AACpB,OAAO,EAAC,YAAY,EAAC,MAAM,iBAAiB,CAAC;AA6B7C;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,EACtE,YAAY,EAAE,CAAC;wBAGO,GAAG;4BAcC,GAAG,gBAAgB,GAAG;EAiBjD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EACnC,OAAO,EAAE;IACP,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC;IAC9C,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACtC,KAAK,CAAC,EAAE,CAAC,cAAc,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC,KAAK,CAAC,CAAC;CACzD,GAAG,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,EACnD,YAAY,EAAE,YAAY,CAAC,CAAC,CAAC,GAC5B,YAAY,CAAC,CAAC,CAAC,CAajB"}
1
+ {"version":3,"file":"createPersistHelpers.d.ts","sourceRoot":"","sources":["../src/createPersistHelpers.ts"],"names":[],"mappings":"AAAA,OAAO,CAAC,MAAM,KAAK,CAAC;AACpB,OAAO,EAAU,cAAc,EAAC,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAC,YAAY,EAAC,MAAM,iBAAiB,CAAC;AAE7C,KAAK,qBAAqB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI;KAC/D,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAC9B,CAAC;AA8DF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,EACtE,YAAY,EAAE,CAAC;wBAGO,GAAG,KAAG,qBAAqB,CAAC,CAAC,CAAC;4BAgB1B,GAAG,gBAAgB,GAAG;EAmCjD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;AACH,wBAAgB,mBAAmB,CACjC,CAAC,EACD,aAAa,SAAS,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,EAC3E,cAAc,GAAG,qBAAqB,CAAC,aAAa,CAAC,EAErD,OAAO,EAAE;IACP,kBAAkB,EAAE,aAAa,CAAC;IAClC,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,cAAc,CAAC;IAC1C,KAAK,CAAC,EAAE,CAAC,cAAc,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC,KAAK,CAAC,CAAC;CACzD,GAAG,IAAI,CAAC,cAAc,CAAC,CAAC,EAAE,cAAc,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,EACnE,YAAY,EAAE,YAAY,CAAC,CAAC,CAAC,GAC5B,YAAY,CAAC,CAAC,CAAC,CAajB"}
@@ -23,12 +23,34 @@ function createSafeStorage(base) {
23
23
  },
24
24
  };
25
25
  }
26
+ /**
27
+ * Internal symbol-based hook for schema-specific rehydrate merge input.
28
+ *
29
+ * If a Zod schema sets a function under this symbol, `createPersistHelpers().merge`
30
+ * will call it with `{defaults, persisted}` and parse the returned value instead of
31
+ * parsing `persisted` directly.
32
+ *
33
+ * This allows slices to opt into defaults-aware merging without hard-coding slice keys.
34
+ */
35
+ const PersistMergeInputSymbol = Symbol.for('sqlrooms.persist.mergeInput');
36
+ function getPersistMergeInputBuilder(schema) {
37
+ // Schemas can optionally expose a merge-input builder under this symbol.
38
+ // This lets slices define custom rehydrate behavior without key-based branching.
39
+ const marker = schema[PersistMergeInputSymbol];
40
+ return typeof marker === 'function' ? marker : undefined;
41
+ }
26
42
  /**
27
43
  * Creates partialize and merge functions for Zustand persist middleware.
28
44
  * Automatically handles extracting and merging slice configs.
29
45
  *
30
46
  * @param sliceConfigs - Map of slice names to their Zod config schemas
31
47
  * @returns Object with partialize and merge functions
48
+ * - `partialize`: serializes `state[slice].config` for each configured slice
49
+ * - `merge`: rehydrates each slice config from persisted storage
50
+ *
51
+ * `merge` supports schema-level customization via an internal symbol marker.
52
+ * When present on a schema, the marker function receives the current defaults and
53
+ * persisted value and can return custom parse input for `schema.parse(...)`.
32
54
  *
33
55
  * @example
34
56
  * ```ts
@@ -69,10 +91,25 @@ export function createPersistHelpers(sliceConfigs) {
69
91
  merge: (persistedState, currentState) => {
70
92
  const merged = { ...currentState };
71
93
  for (const [key, schema] of Object.entries(sliceConfigs)) {
94
+ const persistedConfig = persistedState?.[key];
95
+ if (persistedConfig === undefined || persistedConfig === null) {
96
+ continue;
97
+ }
72
98
  try {
99
+ // Default behavior parses persisted config as-is.
100
+ // If a schema declares a merge-input builder, we pass both persisted
101
+ // and current defaults so that schema can merge before validation.
102
+ const parseMergeInput = getPersistMergeInputBuilder(schema);
103
+ const mergeInput = parseMergeInput
104
+ ? parseMergeInput({
105
+ defaults: currentState[key]?.config,
106
+ persisted: persistedConfig,
107
+ })
108
+ : persistedConfig;
109
+ const config = schema.parse(mergeInput);
73
110
  merged[key] = {
74
111
  ...currentState[key],
75
- config: schema.parse(persistedState[key]),
112
+ config,
76
113
  };
77
114
  }
78
115
  catch (error) {
@@ -1 +1 @@
1
- {"version":3,"file":"createPersistHelpers.js","sourceRoot":"","sources":["../src/createPersistHelpers.ts"],"names":[],"mappings":"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","sourcesContent":["import {persist, PersistOptions} from 'zustand/middleware';\nimport z from 'zod';\nimport {StateCreator} from './BaseRoomStore';\n\n/**\n * Wraps a Zustand persist storage so that `setItem` silently drops writes\n * whose serialised payload exceeds the engine's string-length limit (or any\n * other serialisation error). This prevents `RangeError: Invalid string\n * length` from crashing the app when the state grows unexpectedly large.\n */\nfunction createSafeStorage<S>(\n base: PersistOptions<S>['storage'],\n): PersistOptions<S>['storage'] {\n if (!base) return base;\n const originalSetItem = base.setItem?.bind(base);\n if (!originalSetItem) return base;\n return {\n ...base,\n setItem: (...args: Parameters<typeof originalSetItem>) => {\n try {\n return originalSetItem(...args);\n } catch (error) {\n console.warn(\n 'Persist storage setItem failed (payload too large?):',\n error instanceof Error ? error.message : error,\n );\n }\n },\n };\n}\n\n/**\n * Creates partialize and merge functions for Zustand persist middleware.\n * Automatically handles extracting and merging slice configs.\n *\n * @param sliceConfigs - Map of slice names to their Zod config schemas\n * @returns Object with partialize and merge functions\n *\n * @example\n * ```ts\n * const {partialize, merge} = createPersistHelpers({\n * room: BaseRoomConfig,\n * layout: LayoutConfig,\n * sqlEditor: SqlEditorSliceConfig,\n * });\n *\n * export const {roomStore, useRoomStore} = createRoomStore<RoomState>(\n * persist(\n * (set, get, store) => ({...}),\n * {\n * name: 'my-app-state-storage',\n * partialize,\n * merge,\n * },\n * ) as StateCreator<RoomState>,\n * );\n * ```\n */\nexport function createPersistHelpers<T extends Record<string, z.ZodType>>(\n sliceConfigs: T,\n) {\n return {\n partialize: (state: any) => {\n const result: Record<string, any> = {};\n for (const [key, schema] of Object.entries(sliceConfigs)) {\n try {\n result[key] = schema.parse(state[key]?.config);\n } catch (error) {\n throw new Error(`Error parsing config key \"${key}\"`, {\n cause: error,\n });\n }\n }\n return result;\n },\n\n merge: (persistedState: any, currentState: any) => {\n const merged = {...currentState};\n for (const [key, schema] of Object.entries(sliceConfigs)) {\n try {\n merged[key] = {\n ...currentState[key],\n config: schema.parse(persistedState[key]),\n };\n } catch (error) {\n throw new Error(`Error parsing config key \"${key}\"`, {\n cause: error,\n });\n }\n }\n return merged;\n },\n };\n}\n\n/**\n * Wraps a state creator with Zustand's persist middleware and automatically\n * handles slice config serialization/deserialization using Zod schemas.\n *\n * This helper combines persist functionality with automatic `partialize` and `merge`\n * functions generated from your slice config schemas, eliminating manual type casting.\n *\n * @param options - Persist configuration object\n * @param options.name - Unique storage key (required)\n * @param options.sliceConfigSchemas - Map of slice names to Zod schemas for their configs\n * @param options.partialize - Optional custom partialize function (overrides auto-generated one)\n * @param options.merge - Optional custom merge function (overrides auto-generated one)\n * @param options.storage - Custom storage implementation (optional, defaults to localStorage)\n * @param options.version - Schema version for migrations (optional)\n * @param options.migrate - Migration function for version changes (optional)\n * @param options.skipHydration - Skip auto-hydration for SSR (optional)\n * @param stateCreator - Zustand state creator function\n * @returns Properly typed StateCreator with persist middleware applied\n *\n * @see {@link https://zustand.docs.pmnd.rs/middlewares/persist | Zustand persist middleware docs}\n *\n * @example\n * Basic usage:\n * ```ts\n * export const {roomStore, useRoomStore} = createRoomStore<RoomState>(\n * persistSliceConfigs(\n * {\n * name: 'my-app-state-storage',\n * sliceConfigSchemas: {\n * room: BaseRoomConfig,\n * layout: LayoutConfig,\n * sqlEditor: SqlEditorSliceConfig,\n * },\n * },\n * (set, get, store) => ({\n * ...createRoomSlice()(set, get, store),\n * ...createLayoutSlice({...})(set, get, store),\n * })\n * )\n * );\n * ```\n *\n * @example\n * With custom partialize/merge for additional state:\n * ```ts\n * export const {roomStore, useRoomStore} = createRoomStore<RoomState>(\n * persistSliceConfigs(\n * {\n * name: 'my-app-state-storage',\n * sliceConfigSchemas: {\n * room: BaseRoomConfig,\n * layout: LayoutConfig,\n * },\n * partialize: (state) => ({\n * apiKey: state.apiKey, // Persist additional field\n * ...createPersistHelpers({room: BaseRoomConfig, layout: LayoutConfig}).partialize(state),\n * }),\n * merge: (persisted, current) => ({\n * ...createPersistHelpers({room: BaseRoomConfig, layout: LayoutConfig}).merge(persisted, current),\n * apiKey: persisted.apiKey, // Restore additional field\n * }),\n * },\n * (set, get, store) => ({...})\n * )\n * );\n * ```\n */\nexport function persistSliceConfigs<S>(\n options: {\n sliceConfigSchemas: Record<string, z.ZodType>;\n partialize?: (state: S) => Partial<S>;\n merge?: (persistedState: unknown, currentState: S) => S;\n } & Omit<PersistOptions<S>, 'partialize' | 'merge'>,\n stateCreator: StateCreator<S>,\n): StateCreator<S> {\n const {sliceConfigSchemas, partialize, merge, storage, ...persistOptions} =\n options;\n const helpers = createPersistHelpers(sliceConfigSchemas);\n\n const safeStorage = createSafeStorage(storage);\n\n return persist<S>(stateCreator, {\n ...persistOptions,\n ...(safeStorage ? {storage: safeStorage} : {}),\n partialize: partialize || helpers.partialize,\n merge: merge || helpers.merge,\n } as PersistOptions<S>) as StateCreator<S>;\n}\n"]}
1
+ {"version":3,"file":"createPersistHelpers.js","sourceRoot":"","sources":["../src/createPersistHelpers.ts"],"names":[],"mappings":"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","sourcesContent":["import z from 'zod';\nimport {persist, PersistOptions} from 'zustand/middleware';\nimport {StateCreator} from './BaseRoomStore';\n\ntype PersistedSliceConfigs<T extends Record<string, z.ZodType>> = {\n [K in keyof T]: z.infer<T[K]>;\n};\n\n/**\n * Wraps a Zustand persist storage so that `setItem` silently drops writes\n * whose serialised payload exceeds the engine's string-length limit (or any\n * other serialisation error). This prevents `RangeError: Invalid string\n * length` from crashing the app when the state grows unexpectedly large.\n */\nfunction createSafeStorage<S, PersistedState>(\n base: PersistOptions<S, PersistedState>['storage'],\n): PersistOptions<S, PersistedState>['storage'] {\n if (!base) return base;\n const originalSetItem = base.setItem?.bind(base);\n if (!originalSetItem) return base;\n return {\n ...base,\n setItem: (...args: Parameters<typeof originalSetItem>) => {\n try {\n return originalSetItem(...args);\n } catch (error) {\n console.warn(\n 'Persist storage setItem failed (payload too large?):',\n error instanceof Error ? error.message : error,\n );\n }\n },\n };\n}\n\n/**\n * Internal symbol-based hook for schema-specific rehydrate merge input.\n *\n * If a Zod schema sets a function under this symbol, `createPersistHelpers().merge`\n * will call it with `{defaults, persisted}` and parse the returned value instead of\n * parsing `persisted` directly.\n *\n * This allows slices to opt into defaults-aware merging without hard-coding slice keys.\n */\nconst PersistMergeInputSymbol = Symbol.for('sqlrooms.persist.mergeInput');\n\n/**\n * Builds the value passed to `schema.parse(...)` during rehydrate merge.\n */\ntype PersistMergeInputBuilder = (params: {\n persisted: unknown;\n defaults: unknown;\n}) => unknown;\n\nfunction getPersistMergeInputBuilder(\n schema: z.ZodType,\n): PersistMergeInputBuilder | undefined {\n // Schemas can optionally expose a merge-input builder under this symbol.\n // This lets slices define custom rehydrate behavior without key-based branching.\n const marker = (\n schema as z.ZodType & {\n [PersistMergeInputSymbol]?: PersistMergeInputBuilder;\n }\n )[PersistMergeInputSymbol];\n\n return typeof marker === 'function' ? marker : undefined;\n}\n\n/**\n * Creates partialize and merge functions for Zustand persist middleware.\n * Automatically handles extracting and merging slice configs.\n *\n * @param sliceConfigs - Map of slice names to their Zod config schemas\n * @returns Object with partialize and merge functions\n * - `partialize`: serializes `state[slice].config` for each configured slice\n * - `merge`: rehydrates each slice config from persisted storage\n *\n * `merge` supports schema-level customization via an internal symbol marker.\n * When present on a schema, the marker function receives the current defaults and\n * persisted value and can return custom parse input for `schema.parse(...)`.\n *\n * @example\n * ```ts\n * const {partialize, merge} = createPersistHelpers({\n * room: BaseRoomConfig,\n * layout: LayoutConfig,\n * sqlEditor: SqlEditorSliceConfig,\n * });\n *\n * export const {roomStore, useRoomStore} = createRoomStore<RoomState>(\n * persist(\n * (set, get, store) => ({...}),\n * {\n * name: 'my-app-state-storage',\n * partialize,\n * merge,\n * },\n * ) as StateCreator<RoomState>,\n * );\n * ```\n */\nexport function createPersistHelpers<T extends Record<string, z.ZodType>>(\n sliceConfigs: T,\n) {\n return {\n partialize: (state: any): PersistedSliceConfigs<T> => {\n const result = {} as PersistedSliceConfigs<T>;\n for (const [key, schema] of Object.entries(sliceConfigs)) {\n try {\n (result as Record<string, unknown>)[key] = schema.parse(\n state[key]?.config,\n );\n } catch (error) {\n throw new Error(`Error parsing config key \"${key}\"`, {\n cause: error,\n });\n }\n }\n return result;\n },\n\n merge: (persistedState: any, currentState: any) => {\n const merged = {...currentState};\n for (const [key, schema] of Object.entries(sliceConfigs)) {\n const persistedConfig = persistedState?.[key];\n\n if (persistedConfig === undefined || persistedConfig === null) {\n continue;\n }\n\n try {\n // Default behavior parses persisted config as-is.\n // If a schema declares a merge-input builder, we pass both persisted\n // and current defaults so that schema can merge before validation.\n const parseMergeInput = getPersistMergeInputBuilder(schema);\n const mergeInput = parseMergeInput\n ? parseMergeInput({\n defaults: currentState[key]?.config,\n persisted: persistedConfig,\n })\n : persistedConfig;\n const config = schema.parse(mergeInput);\n\n merged[key] = {\n ...currentState[key],\n config,\n };\n } catch (error) {\n throw new Error(`Error parsing config key \"${key}\"`, {\n cause: error,\n });\n }\n }\n return merged;\n },\n };\n}\n\n/**\n * Wraps a state creator with Zustand's persist middleware and automatically\n * handles slice config serialization/deserialization using Zod schemas.\n *\n * This helper combines persist functionality with automatic `partialize` and `merge`\n * functions generated from your slice config schemas, eliminating manual type casting.\n *\n * @param options - Persist configuration object\n * @param options.name - Unique storage key (required)\n * @param options.sliceConfigSchemas - Map of slice names to Zod schemas for their configs\n * @param options.partialize - Optional custom partialize function (overrides auto-generated one)\n * @param options.merge - Optional custom merge function (overrides auto-generated one)\n * @param options.storage - Custom storage implementation (optional, defaults to localStorage)\n * @param options.version - Schema version for migrations (optional)\n * @param options.migrate - Migration function for version changes (optional)\n * @param options.skipHydration - Skip auto-hydration for SSR (optional)\n * @param stateCreator - Zustand state creator function\n * @returns Properly typed StateCreator with persist middleware applied\n *\n * @see {@link https://zustand.docs.pmnd.rs/middlewares/persist | Zustand persist middleware docs}\n *\n * @example\n * Basic usage:\n * ```ts\n * export const {roomStore, useRoomStore} = createRoomStore<RoomState>(\n * persistSliceConfigs(\n * {\n * name: 'my-app-state-storage',\n * sliceConfigSchemas: {\n * room: BaseRoomConfig,\n * layout: LayoutConfig,\n * sqlEditor: SqlEditorSliceConfig,\n * },\n * },\n * (set, get, store) => ({\n * ...createRoomSlice()(set, get, store),\n * ...createLayoutSlice({...})(set, get, store),\n * })\n * )\n * );\n * ```\n *\n * @example\n * With custom partialize/merge for additional state:\n * ```ts\n * export const {roomStore, useRoomStore} = createRoomStore<RoomState>(\n * persistSliceConfigs(\n * {\n * name: 'my-app-state-storage',\n * sliceConfigSchemas: {\n * room: BaseRoomConfig,\n * layout: LayoutConfig,\n * },\n * partialize: (state) => ({\n * apiKey: state.apiKey, // Persist additional field\n * ...createPersistHelpers({room: BaseRoomConfig, layout: LayoutConfig}).partialize(state),\n * }),\n * merge: (persisted, current) => ({\n * ...createPersistHelpers({room: BaseRoomConfig, layout: LayoutConfig}).merge(persisted, current),\n * apiKey: persisted.apiKey, // Restore additional field\n * }),\n * },\n * (set, get, store) => ({...})\n * )\n * );\n * ```\n */\nexport function persistSliceConfigs<\n S,\n TSliceConfigs extends Record<string, z.ZodType> = Record<string, z.ZodType>,\n PersistedState = PersistedSliceConfigs<TSliceConfigs>,\n>(\n options: {\n sliceConfigSchemas: TSliceConfigs;\n partialize?: (state: S) => PersistedState;\n merge?: (persistedState: unknown, currentState: S) => S;\n } & Omit<PersistOptions<S, PersistedState>, 'partialize' | 'merge'>,\n stateCreator: StateCreator<S>,\n): StateCreator<S> {\n const {sliceConfigSchemas, partialize, merge, storage, ...persistOptions} =\n options;\n const helpers = createPersistHelpers(sliceConfigSchemas);\n\n const safeStorage = createSafeStorage(storage);\n\n return persist<S, [], [], PersistedState>(stateCreator, {\n ...persistOptions,\n ...(safeStorage ? {storage: safeStorage} : {}),\n partialize: partialize || helpers.partialize,\n merge: merge || helpers.merge,\n } as PersistOptions<S, PersistedState>) as StateCreator<S>;\n}\n"]}
@@ -0,0 +1,198 @@
1
+ import type { StoreApi } from 'zustand';
2
+ import type { PersistStorage } from 'zustand/middleware';
3
+ import { type PersistenceController, type PersistenceSaveMetadata, type PersistenceSaveReason } from './PersistenceController';
4
+ /**
5
+ * Converts between a persisted room-state shape and the snapshot value handled
6
+ * by a persistence adapter.
7
+ *
8
+ * The default codec stores snapshots as JSON strings. Provide a custom codec
9
+ * when the backing store already accepts structured values, compressed payloads,
10
+ * encrypted payloads, or another non-string snapshot format.
11
+ *
12
+ * @typeParam TPersisted - The partial room-state shape persisted by Zustand.
13
+ * @typeParam TSnapshot - The durable snapshot shape accepted by the adapter.
14
+ */
15
+ export type RoomStorePersistenceSnapshotCodec<TPersisted, TSnapshot> = {
16
+ /** Serializes the already-partialized persisted state into a durable snapshot. */
17
+ serialize?: (persisted: TPersisted) => TSnapshot;
18
+ /** Deserializes a durable snapshot into the shape Zustand will merge. */
19
+ deserialize?: (snapshot: TSnapshot) => TPersisted;
20
+ };
21
+ export type RoomStorePersistenceSnapshotEquivalence<TSnapshot> = {
22
+ /**
23
+ * Returns true when two serialized snapshots should be treated as equivalent.
24
+ *
25
+ * Defaults to referential equality, which preserves existing behavior for
26
+ * primitive string snapshots.
27
+ */
28
+ compareSnapshots?: (next: TSnapshot, previous: TSnapshot) => boolean;
29
+ /**
30
+ * Extracts a stable revision from a snapshot for equivalence checks.
31
+ *
32
+ * Ignored when `compareSnapshots` is provided.
33
+ */
34
+ getSnapshotRevision?: (snapshot: TSnapshot) => unknown;
35
+ };
36
+ export type RoomStorePersistenceChangePredicate<TState> = (state: TState, previousState: TState) => boolean;
37
+ /**
38
+ * Options for creating controller-backed persistence for a room store.
39
+ *
40
+ * This helper composes Zustand persist storage, room-store partialization, and
41
+ * `createPersistenceController()` so apps can share dirty tracking, autosave,
42
+ * final flush, and hydration saved-snapshot behavior without repeating glue.
43
+ *
44
+ * @typeParam TState - The full room-store state.
45
+ * @typeParam TPersisted - The partial state shape written by Zustand persist.
46
+ * @typeParam TSnapshot - The durable snapshot shape handled by the adapter.
47
+ */
48
+ export type CreateRoomStorePersistenceOptions<TState, TPersisted = Partial<TState>, TSnapshot = string> = RoomStorePersistenceSnapshotCodec<TPersisted, TSnapshot> & RoomStorePersistenceSnapshotEquivalence<TSnapshot> & {
49
+ /**
50
+ * Selects the portion of full room-store state that should be persisted.
51
+ *
52
+ * When using Zustand's `persist` middleware, pass the same function to its
53
+ * `partialize` option so the storage receives the same `TPersisted` shape.
54
+ */
55
+ partialize: (state: TState) => TPersisted;
56
+ /** Loads the latest durable snapshot, or `null` when none exists. */
57
+ load: () => Promise<TSnapshot | null>;
58
+ /**
59
+ * Writes a durable snapshot.
60
+ *
61
+ * Metadata carries the save reason, such as `setItem`, `store-change`,
62
+ * `autosave`, or `final-flush`, for adapters that want observability.
63
+ */
64
+ save: (snapshot: TSnapshot, metadata?: PersistenceSaveMetadata) => Promise<void>;
65
+ /** Removes the durable snapshot when Zustand clears persistence. */
66
+ remove?: (name: string) => Promise<void>;
67
+ /**
68
+ * Optional Zustand store to observe directly.
69
+ *
70
+ * Provide this when persistence should track store changes independently of
71
+ * Zustand persist's `setItem` calls.
72
+ */
73
+ store?: StoreApi<TState>;
74
+ /**
75
+ * Returns true when a changed partialized snapshot should mark persistence
76
+ * dirty.
77
+ *
78
+ * Use this for host lifecycle guards, such as skipping restore-time updates
79
+ * or failed initialization states. Skipped snapshots are still recorded as
80
+ * observed so they are not saved later by an unrelated state change.
81
+ */
82
+ shouldPersistChange?: RoomStorePersistenceChangePredicate<TState>;
83
+ /**
84
+ * Applies a deserialized snapshot to runtime state during `hydrate()`.
85
+ *
86
+ * The helper runs this callback while persistence is paused, so restoring
87
+ * state does not mark the controller dirty.
88
+ */
89
+ applySnapshot?: (persisted: TPersisted, context: {
90
+ store?: StoreApi<TState>;
91
+ }) => void | Promise<void>;
92
+ /** Debounce delay for autosaves, or `null` to disable autosave. */
93
+ autosaveDelayMs?: number | null;
94
+ /** Version returned from `storage.getItem()` for Zustand persist migrations. */
95
+ version?: number;
96
+ /** Clock used by the underlying controller, mostly useful for tests. */
97
+ now?: () => number;
98
+ /**
99
+ * Initial state to snapshot when binding `store` immediately.
100
+ *
101
+ * Useful when the host creates persistence inside a Zustand state creator,
102
+ * before `store.getState()` returns the completed initial state.
103
+ */
104
+ initialState?: TState;
105
+ /**
106
+ * Whether an initially-bound store snapshot should be treated as already saved.
107
+ *
108
+ * Defaults to `true`, which avoids treating the current runtime state as a
109
+ * user edit immediately after binding.
110
+ */
111
+ markInitialSnapshotSaved?: boolean;
112
+ /** Save reason used when `bindStore()` detects a changed partialized snapshot. */
113
+ subscribeReason?: PersistenceSaveReason;
114
+ };
115
+ /**
116
+ * Controller-backed persistence utilities for a room store.
117
+ *
118
+ * `storage` is typed as `PersistStorage<TPersisted>` because Zustand persist
119
+ * calls custom storage with the already-partialized state shape, not the full
120
+ * runtime store state.
121
+ */
122
+ export type RoomStorePersistence<TState, TPersisted, TSnapshot> = {
123
+ /** Low-level persistence controller for save state, dirty tracking, and flushes. */
124
+ controller: PersistenceController<TSnapshot>;
125
+ /** Zustand persist storage backed by the controller and adapter callbacks. */
126
+ storage: PersistStorage<TPersisted>;
127
+ /** The partialization function passed into the helper. */
128
+ partialize: (state: TState) => TPersisted;
129
+ /**
130
+ * Hydrates through the controller, deserializes the snapshot, and optionally
131
+ * applies it to the bound store while persistence is paused.
132
+ */
133
+ hydrate: () => Promise<TPersisted | null>;
134
+ /** Flushes pending dirty state immediately. */
135
+ flush: (reason?: PersistenceSaveReason) => Promise<void>;
136
+ /** Marks the partialized snapshot for a full store state as saved. */
137
+ markStateSnapshotSaved: (state: TState) => void;
138
+ /**
139
+ * Convenience callback for Zustand persist's `onRehydrateStorage` option.
140
+ *
141
+ * Use this after Zustand has merged persisted state into the full runtime
142
+ * store, so the controller treats that merged runtime shape as clean.
143
+ */
144
+ onRehydrateStorage: () => (state?: TState, error?: unknown) => void;
145
+ /**
146
+ * Subscribes to a Zustand store and marks changed partialized snapshots dirty.
147
+ *
148
+ * Returns the unsubscribe function from `store.subscribe()`.
149
+ */
150
+ bindStore: (store: StoreApi<TState>, options?: {
151
+ initialState?: TState;
152
+ markInitialSnapshotSaved?: boolean;
153
+ shouldPersistChange?: RoomStorePersistenceChangePredicate<TState>;
154
+ } & RoomStorePersistenceSnapshotEquivalence<TSnapshot>) => () => void;
155
+ };
156
+ /**
157
+ * Creates persistence glue for a Zustand-backed room store.
158
+ *
159
+ * Use this helper when an app owns durable storage, such as a DuckDB project
160
+ * table or workspace file, but wants to reuse SQLRooms persistence semantics:
161
+ * explicit hydration, dirty tracking, autosave scheduling, final flushes, and
162
+ * saved-snapshot reconciliation after rehydrate.
163
+ *
164
+ * The helper can be used in two modes:
165
+ *
166
+ * - As custom Zustand persist `storage`, paired with the same `partialize`
167
+ * function passed to Zustand persist.
168
+ * - As a direct store subscription via `store` or `bindStore()` when the host
169
+ * wants to observe room-store changes outside Zustand persist.
170
+ *
171
+ * @example
172
+ * ```ts
173
+ * const persistence = createRoomStorePersistence<RoomState>({
174
+ * partialize: persistHelpers.partialize,
175
+ * autosaveDelayMs: 300,
176
+ * load: () => loadWorkspaceState(),
177
+ * save: (snapshot, metadata) =>
178
+ * saveWorkspaceState(snapshot, metadata?.reason),
179
+ * });
180
+ *
181
+ * const storeCreator = persistSliceConfigs(
182
+ * {
183
+ * name: 'workspace-state',
184
+ * sliceConfigSchemas,
185
+ * storage: persistence.storage,
186
+ * partialize: persistence.partialize,
187
+ * onRehydrateStorage: persistence.onRehydrateStorage,
188
+ * },
189
+ * createState,
190
+ * );
191
+ * ```
192
+ *
193
+ * @typeParam TState - The full room-store state.
194
+ * @typeParam TPersisted - The partial state shape written by Zustand persist.
195
+ * @typeParam TSnapshot - The durable snapshot shape handled by the adapter.
196
+ */
197
+ export declare function createRoomStorePersistence<TState, TPersisted = Partial<TState>, TSnapshot = string>({ partialize, load, save, remove, store, applySnapshot, autosaveDelayMs, version, now, serialize, deserialize, compareSnapshots, getSnapshotRevision, shouldPersistChange, initialState, markInitialSnapshotSaved, subscribeReason, }: CreateRoomStorePersistenceOptions<TState, TPersisted, TSnapshot>): RoomStorePersistence<TState, TPersisted, TSnapshot>;
198
+ //# sourceMappingURL=createRoomStorePersistence.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createRoomStorePersistence.d.ts","sourceRoot":"","sources":["../src/createRoomStorePersistence.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,157 @@
1
+ import { createPersistenceController, } from './PersistenceController';
2
+ function defaultSerialize(persisted) {
3
+ return JSON.stringify(persisted);
4
+ }
5
+ function defaultDeserialize(snapshot) {
6
+ if (typeof snapshot === 'string') {
7
+ return JSON.parse(snapshot);
8
+ }
9
+ return snapshot;
10
+ }
11
+ /**
12
+ * Creates persistence glue for a Zustand-backed room store.
13
+ *
14
+ * Use this helper when an app owns durable storage, such as a DuckDB project
15
+ * table or workspace file, but wants to reuse SQLRooms persistence semantics:
16
+ * explicit hydration, dirty tracking, autosave scheduling, final flushes, and
17
+ * saved-snapshot reconciliation after rehydrate.
18
+ *
19
+ * The helper can be used in two modes:
20
+ *
21
+ * - As custom Zustand persist `storage`, paired with the same `partialize`
22
+ * function passed to Zustand persist.
23
+ * - As a direct store subscription via `store` or `bindStore()` when the host
24
+ * wants to observe room-store changes outside Zustand persist.
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * const persistence = createRoomStorePersistence<RoomState>({
29
+ * partialize: persistHelpers.partialize,
30
+ * autosaveDelayMs: 300,
31
+ * load: () => loadWorkspaceState(),
32
+ * save: (snapshot, metadata) =>
33
+ * saveWorkspaceState(snapshot, metadata?.reason),
34
+ * });
35
+ *
36
+ * const storeCreator = persistSliceConfigs(
37
+ * {
38
+ * name: 'workspace-state',
39
+ * sliceConfigSchemas,
40
+ * storage: persistence.storage,
41
+ * partialize: persistence.partialize,
42
+ * onRehydrateStorage: persistence.onRehydrateStorage,
43
+ * },
44
+ * createState,
45
+ * );
46
+ * ```
47
+ *
48
+ * @typeParam TState - The full room-store state.
49
+ * @typeParam TPersisted - The partial state shape written by Zustand persist.
50
+ * @typeParam TSnapshot - The durable snapshot shape handled by the adapter.
51
+ */
52
+ export function createRoomStorePersistence({ partialize, load, save, remove, store, applySnapshot, autosaveDelayMs = null, version = 0, now, serialize = (defaultSerialize), deserialize = (defaultDeserialize), compareSnapshots, getSnapshotRevision, shouldPersistChange, initialState, markInitialSnapshotSaved = true, subscribeReason = 'store-change', }) {
53
+ const toSnapshot = (state) => serialize(partialize(state));
54
+ const areSnapshotsEquivalent = (next, previous, options = {}) => {
55
+ const compare = options.compareSnapshots ?? compareSnapshots;
56
+ if (compare) {
57
+ return compare(next, previous);
58
+ }
59
+ const getRevision = options.getSnapshotRevision ?? getSnapshotRevision;
60
+ if (getRevision) {
61
+ return getRevision(next) === getRevision(previous);
62
+ }
63
+ return next === previous;
64
+ };
65
+ const controller = createPersistenceController({
66
+ autosaveDelayMs,
67
+ now,
68
+ getSnapshot: store ? () => toSnapshot(store.getState()) : undefined,
69
+ adapter: {
70
+ load,
71
+ save,
72
+ },
73
+ });
74
+ const markStateSnapshotSaved = (state) => {
75
+ controller.markSnapshotSaved(toSnapshot(state));
76
+ };
77
+ const storage = {
78
+ getItem: async (name) => {
79
+ void name;
80
+ const snapshot = await controller.hydrate();
81
+ if (snapshot === null)
82
+ return null;
83
+ return {
84
+ state: deserialize(snapshot),
85
+ version,
86
+ };
87
+ },
88
+ setItem: async (name, value) => {
89
+ void name;
90
+ controller.setSnapshot(serialize(value.state), 'setItem');
91
+ },
92
+ removeItem: async (name) => {
93
+ if (!remove) {
94
+ throw new Error('Persistence storage cannot remove item without remove option.');
95
+ }
96
+ await controller.flush('remove');
97
+ await controller.pause(() => remove(name));
98
+ controller.markSnapshotSaved(null);
99
+ },
100
+ };
101
+ const bindStore = (storeToBind, options = {}) => {
102
+ let lastObservedSnapshot = toSnapshot(options.initialState ?? storeToBind.getState());
103
+ const shouldMarkInitial = options.markInitialSnapshotSaved ?? markInitialSnapshotSaved;
104
+ if (shouldMarkInitial) {
105
+ controller.markSnapshotSaved(lastObservedSnapshot);
106
+ }
107
+ else {
108
+ controller.setSnapshot(lastObservedSnapshot, subscribeReason);
109
+ }
110
+ return storeToBind.subscribe((state, previousState) => {
111
+ const snapshot = toSnapshot(state);
112
+ if (areSnapshotsEquivalent(snapshot, lastObservedSnapshot, options)) {
113
+ return;
114
+ }
115
+ lastObservedSnapshot = snapshot;
116
+ const shouldPersist = options.shouldPersistChange ?? shouldPersistChange;
117
+ if (shouldPersist && !shouldPersist(state, previousState)) {
118
+ return;
119
+ }
120
+ controller.setSnapshot(snapshot, subscribeReason);
121
+ });
122
+ };
123
+ if (store) {
124
+ bindStore(store, {
125
+ initialState,
126
+ markInitialSnapshotSaved,
127
+ shouldPersistChange,
128
+ });
129
+ }
130
+ return {
131
+ controller,
132
+ storage,
133
+ partialize,
134
+ hydrate: async () => {
135
+ const snapshot = await controller.hydrate();
136
+ if (snapshot === null)
137
+ return null;
138
+ const persisted = deserialize(snapshot);
139
+ if (applySnapshot) {
140
+ await controller.pause(() => applySnapshot(persisted, { store }));
141
+ if (store) {
142
+ markStateSnapshotSaved(store.getState());
143
+ }
144
+ }
145
+ return persisted;
146
+ },
147
+ flush: (reason = 'flush') => controller.flush(reason),
148
+ markStateSnapshotSaved,
149
+ onRehydrateStorage: () => (state, error) => {
150
+ if (error || !state)
151
+ return;
152
+ markStateSnapshotSaved(state);
153
+ },
154
+ bindStore,
155
+ };
156
+ }
157
+ //# sourceMappingURL=createRoomStorePersistence.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createRoomStorePersistence.js","sourceRoot":"","sources":["../src/createRoomStorePersistence.ts"],"names":[],"mappings":"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","sourcesContent":["import type {StoreApi} from 'zustand';\nimport type {PersistStorage, StorageValue} from 'zustand/middleware';\nimport {\n createPersistenceController,\n type PersistenceController,\n type PersistenceSaveMetadata,\n type PersistenceSaveReason,\n} from './PersistenceController';\n\n/**\n * Converts between a persisted room-state shape and the snapshot value handled\n * by a persistence adapter.\n *\n * The default codec stores snapshots as JSON strings. Provide a custom codec\n * when the backing store already accepts structured values, compressed payloads,\n * encrypted payloads, or another non-string snapshot format.\n *\n * @typeParam TPersisted - The partial room-state shape persisted by Zustand.\n * @typeParam TSnapshot - The durable snapshot shape accepted by the adapter.\n */\nexport type RoomStorePersistenceSnapshotCodec<TPersisted, TSnapshot> = {\n /** Serializes the already-partialized persisted state into a durable snapshot. */\n serialize?: (persisted: TPersisted) => TSnapshot;\n /** Deserializes a durable snapshot into the shape Zustand will merge. */\n deserialize?: (snapshot: TSnapshot) => TPersisted;\n};\n\nexport type RoomStorePersistenceSnapshotEquivalence<TSnapshot> = {\n /**\n * Returns true when two serialized snapshots should be treated as equivalent.\n *\n * Defaults to referential equality, which preserves existing behavior for\n * primitive string snapshots.\n */\n compareSnapshots?: (next: TSnapshot, previous: TSnapshot) => boolean;\n /**\n * Extracts a stable revision from a snapshot for equivalence checks.\n *\n * Ignored when `compareSnapshots` is provided.\n */\n getSnapshotRevision?: (snapshot: TSnapshot) => unknown;\n};\n\nexport type RoomStorePersistenceChangePredicate<TState> = (\n state: TState,\n previousState: TState,\n) => boolean;\n\n/**\n * Options for creating controller-backed persistence for a room store.\n *\n * This helper composes Zustand persist storage, room-store partialization, and\n * `createPersistenceController()` so apps can share dirty tracking, autosave,\n * final flush, and hydration saved-snapshot behavior without repeating glue.\n *\n * @typeParam TState - The full room-store state.\n * @typeParam TPersisted - The partial state shape written by Zustand persist.\n * @typeParam TSnapshot - The durable snapshot shape handled by the adapter.\n */\nexport type CreateRoomStorePersistenceOptions<\n TState,\n TPersisted = Partial<TState>,\n TSnapshot = string,\n> = RoomStorePersistenceSnapshotCodec<TPersisted, TSnapshot> &\n RoomStorePersistenceSnapshotEquivalence<TSnapshot> & {\n /**\n * Selects the portion of full room-store state that should be persisted.\n *\n * When using Zustand's `persist` middleware, pass the same function to its\n * `partialize` option so the storage receives the same `TPersisted` shape.\n */\n partialize: (state: TState) => TPersisted;\n /** Loads the latest durable snapshot, or `null` when none exists. */\n load: () => Promise<TSnapshot | null>;\n /**\n * Writes a durable snapshot.\n *\n * Metadata carries the save reason, such as `setItem`, `store-change`,\n * `autosave`, or `final-flush`, for adapters that want observability.\n */\n save: (\n snapshot: TSnapshot,\n metadata?: PersistenceSaveMetadata,\n ) => Promise<void>;\n /** Removes the durable snapshot when Zustand clears persistence. */\n remove?: (name: string) => Promise<void>;\n /**\n * Optional Zustand store to observe directly.\n *\n * Provide this when persistence should track store changes independently of\n * Zustand persist's `setItem` calls.\n */\n store?: StoreApi<TState>;\n /**\n * Returns true when a changed partialized snapshot should mark persistence\n * dirty.\n *\n * Use this for host lifecycle guards, such as skipping restore-time updates\n * or failed initialization states. Skipped snapshots are still recorded as\n * observed so they are not saved later by an unrelated state change.\n */\n shouldPersistChange?: RoomStorePersistenceChangePredicate<TState>;\n /**\n * Applies a deserialized snapshot to runtime state during `hydrate()`.\n *\n * The helper runs this callback while persistence is paused, so restoring\n * state does not mark the controller dirty.\n */\n applySnapshot?: (\n persisted: TPersisted,\n context: {store?: StoreApi<TState>},\n ) => void | Promise<void>;\n /** Debounce delay for autosaves, or `null` to disable autosave. */\n autosaveDelayMs?: number | null;\n /** Version returned from `storage.getItem()` for Zustand persist migrations. */\n version?: number;\n /** Clock used by the underlying controller, mostly useful for tests. */\n now?: () => number;\n /**\n * Initial state to snapshot when binding `store` immediately.\n *\n * Useful when the host creates persistence inside a Zustand state creator,\n * before `store.getState()` returns the completed initial state.\n */\n initialState?: TState;\n /**\n * Whether an initially-bound store snapshot should be treated as already saved.\n *\n * Defaults to `true`, which avoids treating the current runtime state as a\n * user edit immediately after binding.\n */\n markInitialSnapshotSaved?: boolean;\n /** Save reason used when `bindStore()` detects a changed partialized snapshot. */\n subscribeReason?: PersistenceSaveReason;\n };\n\n/**\n * Controller-backed persistence utilities for a room store.\n *\n * `storage` is typed as `PersistStorage<TPersisted>` because Zustand persist\n * calls custom storage with the already-partialized state shape, not the full\n * runtime store state.\n */\nexport type RoomStorePersistence<TState, TPersisted, TSnapshot> = {\n /** Low-level persistence controller for save state, dirty tracking, and flushes. */\n controller: PersistenceController<TSnapshot>;\n /** Zustand persist storage backed by the controller and adapter callbacks. */\n storage: PersistStorage<TPersisted>;\n /** The partialization function passed into the helper. */\n partialize: (state: TState) => TPersisted;\n /**\n * Hydrates through the controller, deserializes the snapshot, and optionally\n * applies it to the bound store while persistence is paused.\n */\n hydrate: () => Promise<TPersisted | null>;\n /** Flushes pending dirty state immediately. */\n flush: (reason?: PersistenceSaveReason) => Promise<void>;\n /** Marks the partialized snapshot for a full store state as saved. */\n markStateSnapshotSaved: (state: TState) => void;\n /**\n * Convenience callback for Zustand persist's `onRehydrateStorage` option.\n *\n * Use this after Zustand has merged persisted state into the full runtime\n * store, so the controller treats that merged runtime shape as clean.\n */\n onRehydrateStorage: () => (state?: TState, error?: unknown) => void;\n /**\n * Subscribes to a Zustand store and marks changed partialized snapshots dirty.\n *\n * Returns the unsubscribe function from `store.subscribe()`.\n */\n bindStore: (\n store: StoreApi<TState>,\n options?: {\n initialState?: TState;\n markInitialSnapshotSaved?: boolean;\n shouldPersistChange?: RoomStorePersistenceChangePredicate<TState>;\n } & RoomStorePersistenceSnapshotEquivalence<TSnapshot>,\n ) => () => void;\n};\n\nfunction defaultSerialize<TPersisted, TSnapshot>(\n persisted: TPersisted,\n): TSnapshot {\n return JSON.stringify(persisted) as TSnapshot;\n}\n\nfunction defaultDeserialize<TPersisted, TSnapshot>(\n snapshot: TSnapshot,\n): TPersisted {\n if (typeof snapshot === 'string') {\n return JSON.parse(snapshot) as TPersisted;\n }\n return snapshot as unknown as TPersisted;\n}\n\n/**\n * Creates persistence glue for a Zustand-backed room store.\n *\n * Use this helper when an app owns durable storage, such as a DuckDB project\n * table or workspace file, but wants to reuse SQLRooms persistence semantics:\n * explicit hydration, dirty tracking, autosave scheduling, final flushes, and\n * saved-snapshot reconciliation after rehydrate.\n *\n * The helper can be used in two modes:\n *\n * - As custom Zustand persist `storage`, paired with the same `partialize`\n * function passed to Zustand persist.\n * - As a direct store subscription via `store` or `bindStore()` when the host\n * wants to observe room-store changes outside Zustand persist.\n *\n * @example\n * ```ts\n * const persistence = createRoomStorePersistence<RoomState>({\n * partialize: persistHelpers.partialize,\n * autosaveDelayMs: 300,\n * load: () => loadWorkspaceState(),\n * save: (snapshot, metadata) =>\n * saveWorkspaceState(snapshot, metadata?.reason),\n * });\n *\n * const storeCreator = persistSliceConfigs(\n * {\n * name: 'workspace-state',\n * sliceConfigSchemas,\n * storage: persistence.storage,\n * partialize: persistence.partialize,\n * onRehydrateStorage: persistence.onRehydrateStorage,\n * },\n * createState,\n * );\n * ```\n *\n * @typeParam TState - The full room-store state.\n * @typeParam TPersisted - The partial state shape written by Zustand persist.\n * @typeParam TSnapshot - The durable snapshot shape handled by the adapter.\n */\nexport function createRoomStorePersistence<\n TState,\n TPersisted = Partial<TState>,\n TSnapshot = string,\n>({\n partialize,\n load,\n save,\n remove,\n store,\n applySnapshot,\n autosaveDelayMs = null,\n version = 0,\n now,\n serialize = defaultSerialize<TPersisted, TSnapshot>,\n deserialize = defaultDeserialize<TPersisted, TSnapshot>,\n compareSnapshots,\n getSnapshotRevision,\n shouldPersistChange,\n initialState,\n markInitialSnapshotSaved = true,\n subscribeReason = 'store-change',\n}: CreateRoomStorePersistenceOptions<\n TState,\n TPersisted,\n TSnapshot\n>): RoomStorePersistence<TState, TPersisted, TSnapshot> {\n const toSnapshot = (state: TState) => serialize(partialize(state));\n const areSnapshotsEquivalent = (\n next: TSnapshot,\n previous: TSnapshot,\n options: RoomStorePersistenceSnapshotEquivalence<TSnapshot> = {},\n ) => {\n const compare = options.compareSnapshots ?? compareSnapshots;\n if (compare) {\n return compare(next, previous);\n }\n const getRevision = options.getSnapshotRevision ?? getSnapshotRevision;\n if (getRevision) {\n return getRevision(next) === getRevision(previous);\n }\n return next === previous;\n };\n const controller = createPersistenceController<TSnapshot>({\n autosaveDelayMs,\n now,\n getSnapshot: store ? () => toSnapshot(store.getState()) : undefined,\n adapter: {\n load,\n save,\n },\n });\n\n const markStateSnapshotSaved = (state: TState) => {\n controller.markSnapshotSaved(toSnapshot(state));\n };\n\n const storage: PersistStorage<TPersisted> = {\n getItem: async (name: string): Promise<StorageValue<TPersisted> | null> => {\n void name;\n const snapshot = await controller.hydrate();\n if (snapshot === null) return null;\n return {\n state: deserialize(snapshot),\n version,\n };\n },\n setItem: async (\n name: string,\n value: StorageValue<TPersisted>,\n ): Promise<void> => {\n void name;\n controller.setSnapshot(serialize(value.state), 'setItem');\n },\n removeItem: async (name: string): Promise<void> => {\n if (!remove) {\n throw new Error(\n 'Persistence storage cannot remove item without remove option.',\n );\n }\n await controller.flush('remove');\n await controller.pause(() => remove(name));\n controller.markSnapshotSaved(null);\n },\n };\n\n const bindStore: RoomStorePersistence<\n TState,\n TPersisted,\n TSnapshot\n >['bindStore'] = (storeToBind, options = {}) => {\n let lastObservedSnapshot = toSnapshot(\n options.initialState ?? storeToBind.getState(),\n );\n const shouldMarkInitial =\n options.markInitialSnapshotSaved ?? markInitialSnapshotSaved;\n if (shouldMarkInitial) {\n controller.markSnapshotSaved(lastObservedSnapshot);\n } else {\n controller.setSnapshot(lastObservedSnapshot, subscribeReason);\n }\n\n return storeToBind.subscribe((state, previousState) => {\n const snapshot = toSnapshot(state);\n if (areSnapshotsEquivalent(snapshot, lastObservedSnapshot, options)) {\n return;\n }\n lastObservedSnapshot = snapshot;\n const shouldPersist = options.shouldPersistChange ?? shouldPersistChange;\n if (shouldPersist && !shouldPersist(state, previousState)) {\n return;\n }\n controller.setSnapshot(snapshot, subscribeReason);\n });\n };\n\n if (store) {\n bindStore(store, {\n initialState,\n markInitialSnapshotSaved,\n shouldPersistChange,\n });\n }\n\n return {\n controller,\n storage,\n partialize,\n hydrate: async () => {\n const snapshot = await controller.hydrate();\n if (snapshot === null) return null;\n const persisted = deserialize(snapshot);\n if (applySnapshot) {\n await controller.pause(() => applySnapshot(persisted, {store}));\n if (store) {\n markStateSnapshotSaved(store.getState());\n }\n }\n return persisted;\n },\n flush: (reason = 'flush') => controller.flush(reason),\n markStateSnapshotSaved,\n onRehydrateStorage:\n () =>\n (state?: TState, error?: unknown): void => {\n if (error || !state) return;\n markStateSnapshotSaved(state);\n },\n bindStore,\n };\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -8,6 +8,10 @@ export { createBaseRoomSlice, createBaseSlice, createRoomSlice, createRoomStore,
8
8
  export type { BaseRoomStore, BaseRoomStoreState, CreateBaseRoomSliceProps, SliceFunctions, UseRoomStore, } from './BaseRoomStore';
9
9
  export type { StateCreator, StoreApi } from 'zustand';
10
10
  export { createPersistHelpers, persistSliceConfigs, } from './createPersistHelpers';
11
+ export { createRoomStorePersistence } from './createRoomStorePersistence';
12
+ export type { CreateRoomStorePersistenceOptions, RoomStorePersistence, RoomStorePersistenceChangePredicate, RoomStorePersistenceSnapshotEquivalence, RoomStorePersistenceSnapshotCodec, } from './createRoomStorePersistence';
13
+ export { createPersistenceController } from './PersistenceController';
14
+ export type { CreatePersistenceControllerOptions, PersistenceAdapter, PersistenceController, PersistenceControllerListener, PersistenceControllerState, PersistenceSaveMetadata, PersistenceSaveReason, } from './PersistenceController';
11
15
  export { createCommandSlice, createRoomCommandExecutionContext, doesCommandRequireInput, exportCommandInputSchema, getCommandKeystrokes, getCommandInputComponent, getCommandShortcut, hasCommandSliceState, invokeCommandFromStore, listCommandsFromStore, registerCommandsForOwner, resolveCommandPolicyMetadata, unregisterCommandsForOwner, validateCommandInput, } from './CommandSlice';
12
16
  export type { CommandSliceState, CreateCommandSliceProps, RoomCommandDescriptor, RoomCommandExecuteOutput, RoomCommandKeystrokes, RoomCommandInvocation, RoomCommandInvokeFailureEvent, RoomCommandInvokeErrorEvent, RoomCommandInvocationOptions, RoomCommandInvokeStartEvent, RoomCommandInvokeSuccessEvent, RoomCommandMiddleware, RoomCommandMiddlewareNext, RegisteredRoomCommand, RoomCommand, RoomCommandInputComponent, RoomCommandInputComponentProps, RoomCommandListOptions, RoomCommandPolicyMetadata, RoomCommandPortableSchema, RoomCommandResult, RoomCommandRiskLevel, RoomCommandSurface, RoomCommandUiMetadata, RoomCommandExecutionContext, RoomCommandPredicate, } from './CommandSlice';
13
17
  export { createCommandCliAdapter, createCommandMcpAdapter, } from './CommandAdapters';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,GAChB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EAAC,sBAAsB,EAAC,MAAM,qBAAqB,CAAC;AAEhE,OAAO,EACL,mBAAmB,EACnB,eAAe,EACf,eAAe,EACf,eAAe,EACf,sBAAsB,EACtB,WAAW,EACX,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,iBAAiB,CAAC;AACzB,YAAY,EACV,aAAa,EACb,kBAAkB,EAClB,wBAAwB,EACxB,cAAc,EACd,YAAY,GACb,MAAM,iBAAiB,CAAC;AAEzB,YAAY,EAAC,YAAY,EAAE,QAAQ,EAAC,MAAM,SAAS,CAAC;AACpD,OAAO,EACL,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACL,kBAAkB,EAClB,iCAAiC,EACjC,uBAAuB,EACvB,wBAAwB,EACxB,oBAAoB,EACpB,wBAAwB,EACxB,kBAAkB,EAClB,oBAAoB,EACpB,sBAAsB,EACtB,qBAAqB,EACrB,wBAAwB,EACxB,4BAA4B,EAC5B,0BAA0B,EAC1B,oBAAoB,GACrB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EACV,iBAAiB,EACjB,uBAAuB,EACvB,qBAAqB,EACrB,wBAAwB,EACxB,qBAAqB,EACrB,qBAAqB,EACrB,6BAA6B,EAC7B,2BAA2B,EAC3B,4BAA4B,EAC5B,2BAA2B,EAC3B,6BAA6B,EAC7B,qBAAqB,EACrB,yBAAyB,EACzB,qBAAqB,EACrB,WAAW,EACX,yBAAyB,EACzB,8BAA8B,EAC9B,sBAAsB,EACtB,yBAAyB,EACzB,yBAAyB,EACzB,iBAAiB,EACjB,oBAAoB,EACpB,kBAAkB,EAClB,qBAAqB,EACrB,2BAA2B,EAC3B,oBAAoB,GACrB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,uBAAuB,EACvB,uBAAuB,GACxB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EACV,iBAAiB,EACjB,wBAAwB,EACxB,iBAAiB,EACjB,wBAAwB,EACxB,wBAAwB,GACzB,MAAM,mBAAmB,CAAC;AAI3B,OAAO,EACL,cAAc,EACd,cAAc,EACd,2BAA2B,EAC3B,UAAU,EACV,eAAe,EACf,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAChB,wBAAwB,EACxB,oBAAoB,EACpB,eAAe,EACf,QAAQ,EACR,eAAe,EACf,SAAS,EACT,sBAAsB,EACtB,kBAAkB,EAClB,kBAAkB,EAClB,uBAAuB,EACvB,mBAAmB,EACnB,aAAa,EACb,aAAa,EACb,eAAe,EACf,eAAe,EACf,cAAc,EACd,UAAU,EACV,YAAY,EACZ,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,uBAAuB,CAAC;AAE/B,YAAY,EAAC,eAAe,EAAC,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,GAChB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EAAC,sBAAsB,EAAC,MAAM,qBAAqB,CAAC;AAEhE,OAAO,EACL,mBAAmB,EACnB,eAAe,EACf,eAAe,EACf,eAAe,EACf,sBAAsB,EACtB,WAAW,EACX,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,iBAAiB,CAAC;AACzB,YAAY,EACV,aAAa,EACb,kBAAkB,EAClB,wBAAwB,EACxB,cAAc,EACd,YAAY,GACb,MAAM,iBAAiB,CAAC;AAEzB,YAAY,EAAC,YAAY,EAAE,QAAQ,EAAC,MAAM,SAAS,CAAC;AACpD,OAAO,EACL,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAC,0BAA0B,EAAC,MAAM,8BAA8B,CAAC;AACxE,YAAY,EACV,iCAAiC,EACjC,oBAAoB,EACpB,mCAAmC,EACnC,uCAAuC,EACvC,iCAAiC,GAClC,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAC,2BAA2B,EAAC,MAAM,yBAAyB,CAAC;AACpE,YAAY,EACV,kCAAkC,EAClC,kBAAkB,EAClB,qBAAqB,EACrB,6BAA6B,EAC7B,0BAA0B,EAC1B,uBAAuB,EACvB,qBAAqB,GACtB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EACL,kBAAkB,EAClB,iCAAiC,EACjC,uBAAuB,EACvB,wBAAwB,EACxB,oBAAoB,EACpB,wBAAwB,EACxB,kBAAkB,EAClB,oBAAoB,EACpB,sBAAsB,EACtB,qBAAqB,EACrB,wBAAwB,EACxB,4BAA4B,EAC5B,0BAA0B,EAC1B,oBAAoB,GACrB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EACV,iBAAiB,EACjB,uBAAuB,EACvB,qBAAqB,EACrB,wBAAwB,EACxB,qBAAqB,EACrB,qBAAqB,EACrB,6BAA6B,EAC7B,2BAA2B,EAC3B,4BAA4B,EAC5B,2BAA2B,EAC3B,6BAA6B,EAC7B,qBAAqB,EACrB,yBAAyB,EACzB,qBAAqB,EACrB,WAAW,EACX,yBAAyB,EACzB,8BAA8B,EAC9B,sBAAsB,EACtB,yBAAyB,EACzB,yBAAyB,EACzB,iBAAiB,EACjB,oBAAoB,EACpB,kBAAkB,EAClB,qBAAqB,EACrB,2BAA2B,EAC3B,oBAAoB,GACrB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,uBAAuB,EACvB,uBAAuB,GACxB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EACV,iBAAiB,EACjB,wBAAwB,EACxB,iBAAiB,EACjB,wBAAwB,EACxB,wBAAwB,GACzB,MAAM,mBAAmB,CAAC;AAI3B,OAAO,EACL,cAAc,EACd,cAAc,EACd,2BAA2B,EAC3B,UAAU,EACV,eAAe,EACf,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAChB,wBAAwB,EACxB,oBAAoB,EACpB,eAAe,EACf,QAAQ,EACR,eAAe,EACf,SAAS,EACT,sBAAsB,EACtB,kBAAkB,EAClB,kBAAkB,EAClB,uBAAuB,EACvB,mBAAmB,EACnB,aAAa,EACb,aAAa,EACb,eAAe,EACf,eAAe,EACf,cAAc,EACd,UAAU,EACV,YAAY,EACZ,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,uBAAuB,CAAC;AAE/B,YAAY,EAAC,eAAe,EAAC,MAAM,uBAAuB,CAAC"}
package/dist/index.js CHANGED
@@ -5,6 +5,8 @@
5
5
  export { RoomStateContext, RoomStateProvider, useBaseRoomStore, useRoomStoreApi, } from './RoomStateProvider';
6
6
  export { createBaseRoomSlice, createBaseSlice, createRoomSlice, createRoomStore, createRoomStoreCreator, createSlice, isRoomSliceWithDestroy, isRoomSliceWithInitialize, } from './BaseRoomStore';
7
7
  export { createPersistHelpers, persistSliceConfigs, } from './createPersistHelpers';
8
+ export { createRoomStorePersistence } from './createRoomStorePersistence';
9
+ export { createPersistenceController } from './PersistenceController';
8
10
  export { createCommandSlice, createRoomCommandExecutionContext, doesCommandRequireInput, exportCommandInputSchema, getCommandKeystrokes, getCommandInputComponent, getCommandShortcut, hasCommandSliceState, invokeCommandFromStore, listCommandsFromStore, registerCommandsForOwner, resolveCommandPolicyMetadata, unregisterCommandsForOwner, validateCommandInput, } from './CommandSlice';
9
11
  export { createCommandCliAdapter, createCommandMcpAdapter, } from './CommandAdapters';
10
12
  // Re-export from @sqlrooms/room-config
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,GAChB,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EACL,mBAAmB,EACnB,eAAe,EACf,eAAe,EACf,eAAe,EACf,sBAAsB,EACtB,WAAW,EACX,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,iBAAiB,CAAC;AAUzB,OAAO,EACL,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACL,kBAAkB,EAClB,iCAAiC,EACjC,uBAAuB,EACvB,wBAAwB,EACxB,oBAAoB,EACpB,wBAAwB,EACxB,kBAAkB,EAClB,oBAAoB,EACpB,sBAAsB,EACtB,qBAAqB,EACrB,wBAAwB,EACxB,4BAA4B,EAC5B,0BAA0B,EAC1B,oBAAoB,GACrB,MAAM,gBAAgB,CAAC;AA6BxB,OAAO,EACL,uBAAuB,EACvB,uBAAuB,GACxB,MAAM,mBAAmB,CAAC;AAS3B,uCAAuC;AACvC,2EAA2E;AAC3E,OAAO,EACL,cAAc,EACd,cAAc,EACd,2BAA2B,EAC3B,UAAU,EACV,eAAe,EACf,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAChB,wBAAwB,EACxB,oBAAoB,EACpB,eAAe,EACf,QAAQ,EACR,eAAe,EACf,SAAS,EACT,sBAAsB,EACtB,kBAAkB,EAClB,kBAAkB,EAClB,uBAAuB,EACvB,mBAAmB,EACnB,aAAa,EACb,aAAa,EACb,eAAe,EACf,eAAe,EACf,cAAc,EACd,UAAU,EACV,YAAY,EACZ,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,uBAAuB,CAAC","sourcesContent":["/**\n * {@include ../README.md}\n * @packageDocumentation\n */\n\nexport {\n RoomStateContext,\n RoomStateProvider,\n useBaseRoomStore,\n useRoomStoreApi,\n} from './RoomStateProvider';\nexport type {RoomStateProviderProps} from './RoomStateProvider';\n\nexport {\n createBaseRoomSlice,\n createBaseSlice,\n createRoomSlice,\n createRoomStore,\n createRoomStoreCreator,\n createSlice,\n isRoomSliceWithDestroy,\n isRoomSliceWithInitialize,\n} from './BaseRoomStore';\nexport type {\n BaseRoomStore,\n BaseRoomStoreState,\n CreateBaseRoomSliceProps,\n SliceFunctions,\n UseRoomStore,\n} from './BaseRoomStore';\n\nexport type {StateCreator, StoreApi} from 'zustand';\nexport {\n createPersistHelpers,\n persistSliceConfigs,\n} from './createPersistHelpers';\n\nexport {\n createCommandSlice,\n createRoomCommandExecutionContext,\n doesCommandRequireInput,\n exportCommandInputSchema,\n getCommandKeystrokes,\n getCommandInputComponent,\n getCommandShortcut,\n hasCommandSliceState,\n invokeCommandFromStore,\n listCommandsFromStore,\n registerCommandsForOwner,\n resolveCommandPolicyMetadata,\n unregisterCommandsForOwner,\n validateCommandInput,\n} from './CommandSlice';\nexport type {\n CommandSliceState,\n CreateCommandSliceProps,\n RoomCommandDescriptor,\n RoomCommandExecuteOutput,\n RoomCommandKeystrokes,\n RoomCommandInvocation,\n RoomCommandInvokeFailureEvent,\n RoomCommandInvokeErrorEvent,\n RoomCommandInvocationOptions,\n RoomCommandInvokeStartEvent,\n RoomCommandInvokeSuccessEvent,\n RoomCommandMiddleware,\n RoomCommandMiddlewareNext,\n RegisteredRoomCommand,\n RoomCommand,\n RoomCommandInputComponent,\n RoomCommandInputComponentProps,\n RoomCommandListOptions,\n RoomCommandPolicyMetadata,\n RoomCommandPortableSchema,\n RoomCommandResult,\n RoomCommandRiskLevel,\n RoomCommandSurface,\n RoomCommandUiMetadata,\n RoomCommandExecutionContext,\n RoomCommandPredicate,\n} from './CommandSlice';\nexport {\n createCommandCliAdapter,\n createCommandMcpAdapter,\n} from './CommandAdapters';\nexport type {\n CommandCliAdapter,\n CommandCliAdapterOptions,\n CommandMcpAdapter,\n CommandMcpAdapterOptions,\n CommandMcpToolDescriptor,\n} from './CommandAdapters';\n\n// Re-export from @sqlrooms/room-config\n// Values also export their corresponding types automatically (Zod pattern)\nexport {\n BaseDataSource,\n BaseRoomConfig,\n createDefaultBaseRoomConfig,\n DataSource,\n DataSourceTypes,\n DEFAULT_ROOM_TITLE,\n FileDataSource,\n isFileDataSource,\n isSpatialLoadFileOptions,\n isSqlQueryDataSource,\n isUrlDataSource,\n LoadFile,\n LoadFileOptions,\n MAIN_VIEW,\n SpatialLoadFileOptions,\n SpatialLoadOptions,\n SqlQueryDataSource,\n StandardLoadFileOptions,\n StandardLoadOptions,\n UrlDataSource,\n LayoutNodeKey,\n LayoutPanelNode,\n LayoutSplitNode,\n LayoutTabsNode,\n LayoutNode,\n LayoutConfig,\n isLayoutPanelNode,\n isLayoutSplitNode,\n isLayoutTabsNode,\n createDefaultLayout,\n} from '@sqlrooms/room-config';\n\nexport type {LayoutDirection} from '@sqlrooms/room-config';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,GAChB,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EACL,mBAAmB,EACnB,eAAe,EACf,eAAe,EACf,eAAe,EACf,sBAAsB,EACtB,WAAW,EACX,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,iBAAiB,CAAC;AAUzB,OAAO,EACL,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAC,0BAA0B,EAAC,MAAM,8BAA8B,CAAC;AAQxE,OAAO,EAAC,2BAA2B,EAAC,MAAM,yBAAyB,CAAC;AAWpE,OAAO,EACL,kBAAkB,EAClB,iCAAiC,EACjC,uBAAuB,EACvB,wBAAwB,EACxB,oBAAoB,EACpB,wBAAwB,EACxB,kBAAkB,EAClB,oBAAoB,EACpB,sBAAsB,EACtB,qBAAqB,EACrB,wBAAwB,EACxB,4BAA4B,EAC5B,0BAA0B,EAC1B,oBAAoB,GACrB,MAAM,gBAAgB,CAAC;AA6BxB,OAAO,EACL,uBAAuB,EACvB,uBAAuB,GACxB,MAAM,mBAAmB,CAAC;AAS3B,uCAAuC;AACvC,2EAA2E;AAC3E,OAAO,EACL,cAAc,EACd,cAAc,EACd,2BAA2B,EAC3B,UAAU,EACV,eAAe,EACf,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAChB,wBAAwB,EACxB,oBAAoB,EACpB,eAAe,EACf,QAAQ,EACR,eAAe,EACf,SAAS,EACT,sBAAsB,EACtB,kBAAkB,EAClB,kBAAkB,EAClB,uBAAuB,EACvB,mBAAmB,EACnB,aAAa,EACb,aAAa,EACb,eAAe,EACf,eAAe,EACf,cAAc,EACd,UAAU,EACV,YAAY,EACZ,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,uBAAuB,CAAC","sourcesContent":["/**\n * {@include ../README.md}\n * @packageDocumentation\n */\n\nexport {\n RoomStateContext,\n RoomStateProvider,\n useBaseRoomStore,\n useRoomStoreApi,\n} from './RoomStateProvider';\nexport type {RoomStateProviderProps} from './RoomStateProvider';\n\nexport {\n createBaseRoomSlice,\n createBaseSlice,\n createRoomSlice,\n createRoomStore,\n createRoomStoreCreator,\n createSlice,\n isRoomSliceWithDestroy,\n isRoomSliceWithInitialize,\n} from './BaseRoomStore';\nexport type {\n BaseRoomStore,\n BaseRoomStoreState,\n CreateBaseRoomSliceProps,\n SliceFunctions,\n UseRoomStore,\n} from './BaseRoomStore';\n\nexport type {StateCreator, StoreApi} from 'zustand';\nexport {\n createPersistHelpers,\n persistSliceConfigs,\n} from './createPersistHelpers';\nexport {createRoomStorePersistence} from './createRoomStorePersistence';\nexport type {\n CreateRoomStorePersistenceOptions,\n RoomStorePersistence,\n RoomStorePersistenceChangePredicate,\n RoomStorePersistenceSnapshotEquivalence,\n RoomStorePersistenceSnapshotCodec,\n} from './createRoomStorePersistence';\nexport {createPersistenceController} from './PersistenceController';\nexport type {\n CreatePersistenceControllerOptions,\n PersistenceAdapter,\n PersistenceController,\n PersistenceControllerListener,\n PersistenceControllerState,\n PersistenceSaveMetadata,\n PersistenceSaveReason,\n} from './PersistenceController';\n\nexport {\n createCommandSlice,\n createRoomCommandExecutionContext,\n doesCommandRequireInput,\n exportCommandInputSchema,\n getCommandKeystrokes,\n getCommandInputComponent,\n getCommandShortcut,\n hasCommandSliceState,\n invokeCommandFromStore,\n listCommandsFromStore,\n registerCommandsForOwner,\n resolveCommandPolicyMetadata,\n unregisterCommandsForOwner,\n validateCommandInput,\n} from './CommandSlice';\nexport type {\n CommandSliceState,\n CreateCommandSliceProps,\n RoomCommandDescriptor,\n RoomCommandExecuteOutput,\n RoomCommandKeystrokes,\n RoomCommandInvocation,\n RoomCommandInvokeFailureEvent,\n RoomCommandInvokeErrorEvent,\n RoomCommandInvocationOptions,\n RoomCommandInvokeStartEvent,\n RoomCommandInvokeSuccessEvent,\n RoomCommandMiddleware,\n RoomCommandMiddlewareNext,\n RegisteredRoomCommand,\n RoomCommand,\n RoomCommandInputComponent,\n RoomCommandInputComponentProps,\n RoomCommandListOptions,\n RoomCommandPolicyMetadata,\n RoomCommandPortableSchema,\n RoomCommandResult,\n RoomCommandRiskLevel,\n RoomCommandSurface,\n RoomCommandUiMetadata,\n RoomCommandExecutionContext,\n RoomCommandPredicate,\n} from './CommandSlice';\nexport {\n createCommandCliAdapter,\n createCommandMcpAdapter,\n} from './CommandAdapters';\nexport type {\n CommandCliAdapter,\n CommandCliAdapterOptions,\n CommandMcpAdapter,\n CommandMcpAdapterOptions,\n CommandMcpToolDescriptor,\n} from './CommandAdapters';\n\n// Re-export from @sqlrooms/room-config\n// Values also export their corresponding types automatically (Zod pattern)\nexport {\n BaseDataSource,\n BaseRoomConfig,\n createDefaultBaseRoomConfig,\n DataSource,\n DataSourceTypes,\n DEFAULT_ROOM_TITLE,\n FileDataSource,\n isFileDataSource,\n isSpatialLoadFileOptions,\n isSqlQueryDataSource,\n isUrlDataSource,\n LoadFile,\n LoadFileOptions,\n MAIN_VIEW,\n SpatialLoadFileOptions,\n SpatialLoadOptions,\n SqlQueryDataSource,\n StandardLoadFileOptions,\n StandardLoadOptions,\n UrlDataSource,\n LayoutNodeKey,\n LayoutPanelNode,\n LayoutSplitNode,\n LayoutTabsNode,\n LayoutNode,\n LayoutConfig,\n isLayoutPanelNode,\n isLayoutSplitNode,\n isLayoutTabsNode,\n createDefaultLayout,\n} from '@sqlrooms/room-config';\n\nexport type {LayoutDirection} from '@sqlrooms/room-config';\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sqlrooms/room-store",
3
- "version": "0.29.0-rc.6",
3
+ "version": "0.29.0-rc.7",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/sqlrooms/sqlrooms.git"
@@ -19,19 +19,23 @@
19
19
  "build": "tsc",
20
20
  "dev": "tsc -w",
21
21
  "lint": "eslint .",
22
- "test": "jest",
23
- "test:watch": "jest --watch",
22
+ "test": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest",
23
+ "test:watch": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest --watch",
24
24
  "typecheck": "tsc --noEmit",
25
25
  "typedoc": "typedoc"
26
26
  },
27
27
  "dependencies": {
28
- "@sqlrooms/room-config": "0.29.0-rc.6",
28
+ "@sqlrooms/room-config": "0.29.0-rc.7",
29
29
  "immer": "^11.0.1",
30
30
  "zod": "^4.1.8",
31
31
  "zustand": "^5.0.8"
32
32
  },
33
33
  "devDependencies": {
34
- "@sqlrooms/preset-jest": "0.29.0-rc.6",
34
+ "@jest/globals": "^30.2.0",
35
+ "@sqlrooms/ai-config": "0.29.0-rc.7",
36
+ "@sqlrooms/preset-jest": "0.29.0-rc.7",
37
+ "@types/jest": "^30.0.0",
38
+ "jest": "^30.1.3",
35
39
  "ts-jest": "^29.4.4"
36
40
  },
37
41
  "peerDependencies": {
@@ -40,5 +44,5 @@
40
44
  "publishConfig": {
41
45
  "access": "public"
42
46
  },
43
- "gitHead": "fbf5ee8897c5c06306b5433d26378cf658b5b298"
47
+ "gitHead": "db298360fd66dc7839d4a14c15b99ac4a4cbf8c5"
44
48
  }