@sqlrooms/crdt 0.27.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.md ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright 2025 SQLRooms Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # @sqlrooms/crdt
2
+
3
+ CRDT utilities for SQLRooms built on top of Loro Mirror. The package exposes `createCrdtSlice`, a Zustand slice helper that mirrors selected parts of your store into a Loro CRDT document, plus a small set of persistence and sync helpers.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @sqlrooms/crdt loro-crdt loro-mirror
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```ts
14
+ import {schema} from 'loro-mirror';
15
+ import {createRoomStore, persistSliceConfigs} from '@sqlrooms/room-shell';
16
+ import {BaseRoomConfig, LayoutConfig} from '@sqlrooms/room-config';
17
+ import {
18
+ createCrdtSlice,
19
+ createLocalStorageDocStorage,
20
+ createWebSocketSyncConnector,
21
+ } from '@sqlrooms/crdt';
22
+
23
+ const mirrorSchema = schema({
24
+ room: schema.LoroMap({config: schema.Ignore()}), // mirror only the config portions
25
+ layout: schema.LoroMap({config: schema.Ignore()}),
26
+ });
27
+
28
+ const {roomStore, useRoomStore} = createRoomStore(
29
+ persistSliceConfigs(
30
+ {
31
+ name: 'sqlrooms-sync-demo',
32
+ sliceConfigSchemas: {room: BaseRoomConfig, layout: LayoutConfig},
33
+ },
34
+ (set, get, store) => ({
35
+ ...createCrdtSlice({
36
+ schema: mirrorSchema,
37
+ bindings: [
38
+ {key: 'room', select: (s) => s.room?.config, apply: (value) => set({room: {...get().room, config: value}})},
39
+ {key: 'layout', select: (s) => s.layout?.config, apply: (value) => set({layout: {...get().layout, config: value}})},
40
+ ],
41
+ storage: createLocalStorageDocStorage('sqlrooms-sync-demo'),
42
+ sync: createWebSocketSyncConnector({
43
+ url: 'ws://localhost:4800',
44
+ roomId: 'demo-room',
45
+ // If your server sends a snapshot on join (like sqlrooms-duckdb-server),
46
+ // prefer updates-only to avoid re-sending full snapshots on reconnects.
47
+ sendSnapshotOnConnect: false,
48
+ // Still seed an empty server once after join (important if you load initial
49
+ // state from local persistence without generating CRDT ops).
50
+ sendSnapshotIfServerEmpty: true,
51
+ }),
52
+ })(set, get, store),
53
+ // add your other slices here
54
+ }),
55
+ ),
56
+ );
57
+
58
+ // Call roomStore.getState().crdt.initialize() after creating the store to start sync
59
+ ```
60
+
61
+ ### Concepts
62
+
63
+ - **Bindings**: map each mirrored CRDT field to a piece of Zustand state. Provide `select` to choose the outbound data and optional `apply` to merge inbound CRDT updates. If `apply` is omitted, the binding writes to a top-level key.
64
+ - **Storage**: implement `CrdtDocStorage` to persist snapshots (localStorage helper included).
65
+ - **Sync**: plug a `CrdtSyncConnector` (websocket helper included) that forwards local updates and applies remote updates via `doc.import`.
66
+
67
+ See `examples/sync` for an end-to-end demonstration with the Python sync server.
68
+
69
+ ### Testing & debugging
70
+
71
+ - Mirror emits `tags` metadata; we tag store-origin writes as `from-store` to avoid loops. Log `mirror.subscribe` in your app if you need deeper inspection.
72
+ - Use the `storage` hook to inject an in-memory or temp store in tests; pass a fake `sync` connector that captures `subscribeLocalUpdates` traffic for assertions.
73
+ - The websocket connector exposes `onStatus` for basic connection logging; attach console logs or telemetry there during development.
74
+
@@ -0,0 +1,88 @@
1
+ import { LoroDoc } from 'loro-crdt';
2
+ import { InferInputType, SchemaType } from 'loro-mirror';
3
+ import { StateCreator } from 'zustand';
4
+ import { InferredState, MirrorSchema, StoreGet, StoreSet, StripCidDeep } from './type-helpers';
5
+ export type CrdtMirrorValueSelector<S, TSchema extends SchemaType> = (state: S) => StripCidDeep<InferredState<TSchema>>;
6
+ export type CrdtMirrorValueApplier<S, TSchema extends SchemaType> = (value: InferredState<TSchema>, set: StoreSet<S>, get: StoreGet<S>) => void;
7
+ export type CrdtDocStorage = {
8
+ load: () => Promise<Uint8Array | undefined>;
9
+ save: (data: Uint8Array) => Promise<void>;
10
+ };
11
+ export type CrdtConnectionStatus = 'idle' | 'connecting' | 'open' | 'closed' | 'error';
12
+ export type CrdtSyncConnector = {
13
+ connect: (doc: LoroDoc) => Promise<void>;
14
+ disconnect?: () => Promise<void>;
15
+ /**
16
+ * Optional hook for connectors to report connection status into the CRDT slice.
17
+ *
18
+ * `createCrdtSlice` will wire this automatically to `crdt.setConnectionStatus`,
19
+ * so consumer apps don't need to pass `onStatus` callbacks manually.
20
+ */
21
+ setStatusListener?: (listener: (status: CrdtConnectionStatus) => void) => void;
22
+ };
23
+ export type CrdtMirror<S, TSchema extends SchemaType = SchemaType> = {
24
+ /**
25
+ * Schema for the value stored under this mirror's key in the Loro doc.
26
+ *
27
+ * Example: if the mirror key is `"canvas"`, this schema describes the value at
28
+ * `doc.root.canvas` (not an extra `{canvas: ...}` wrapper).
29
+ */
30
+ schema: MirrorSchema<TSchema>;
31
+ /**
32
+ * Select the value to write under this mirror key.
33
+ *
34
+ * If omitted, the store field at the same key name will be mirrored.
35
+ */
36
+ select?: CrdtMirrorValueSelector<S, TSchema>;
37
+ /**
38
+ * Apply an incoming CRDT value under this mirror key back into the store.
39
+ *
40
+ * If omitted, the store field at the same key name will be replaced.
41
+ */
42
+ apply?: CrdtMirrorValueApplier<S, TSchema>;
43
+ /**
44
+ * Initial value written under this mirror key when creating the Mirror.
45
+ */
46
+ initialState?: Partial<InferInputType<TSchema>>;
47
+ mirrorOptions?: Record<string, unknown>;
48
+ };
49
+ export type CreateCrdtSliceOptions<S, TSchema extends SchemaType> = {
50
+ /**
51
+ * CRDT mirrors keyed by their root key in the Loro document.
52
+ *
53
+ * Each entry becomes one `loro-mirror` `Mirror` instance on a shared `LoroDoc`.
54
+ */
55
+ mirrors: Record<string, CrdtMirror<S, any>>;
56
+ doc?: LoroDoc;
57
+ createDoc?: () => LoroDoc;
58
+ storage?: CrdtDocStorage;
59
+ sync?: CrdtSyncConnector;
60
+ mirrorOptions?: Record<string, unknown>;
61
+ onError?: (error: unknown) => void;
62
+ };
63
+ export type CrdtSliceState = {
64
+ crdt: {
65
+ status: 'idle' | 'ready' | 'error';
66
+ error?: string;
67
+ /**
68
+ * Optional sync connection status for UIs.
69
+ *
70
+ * This is intentionally generic and primarily used by websocket-based sync
71
+ * connectors via `onStatus`.
72
+ */
73
+ connectionStatus: CrdtConnectionStatus;
74
+ /**
75
+ * Update `connectionStatus` (typically wired to a sync connector `onStatus` callback).
76
+ */
77
+ setConnectionStatus: (status: CrdtConnectionStatus) => void;
78
+ initialize: () => Promise<void>;
79
+ destroy: () => Promise<void>;
80
+ };
81
+ };
82
+ /**
83
+ * Create a CRDT-backed slice that mirrors selected store fields into a Loro doc.
84
+ *
85
+ * The returned state creator is intended to be composed into a larger Zustand store.
86
+ */
87
+ export declare function createCrdtSlice<S extends Record<string, any>, TSchema extends SchemaType = SchemaType>(options: CreateCrdtSliceOptions<S, TSchema>): StateCreator<CrdtSliceState>;
88
+ //# sourceMappingURL=createCrdtSlice.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createCrdtSlice.d.ts","sourceRoot":"","sources":["../src/createCrdtSlice.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,222 @@
1
+ import { setAutoFreeze } from 'immer';
2
+ import { LoroDoc } from 'loro-crdt';
3
+ import { Mirror, schema } from 'loro-mirror';
4
+ import { createSlice, } from './type-helpers';
5
+ // Mirror can’t stamp $cid on frozen objects, so disable auto-freeze.
6
+ setAutoFreeze(false);
7
+ /**
8
+ * Create a CRDT-backed slice that mirrors selected store fields into a Loro doc.
9
+ *
10
+ * The returned state creator is intended to be composed into a larger Zustand store.
11
+ */
12
+ export function createCrdtSlice(options) {
13
+ return createSlice((set, get, store) => {
14
+ let doc;
15
+ let mirrors = [];
16
+ let mirrorKeys = [];
17
+ let unsubStore;
18
+ let unsubMirrors = [];
19
+ let unsubDocLocal;
20
+ let initializing;
21
+ let suppressStoreToMirror = false;
22
+ const lastOutboundByKey = new Map();
23
+ const persistDoc = async () => {
24
+ if (!options.storage || !doc)
25
+ return;
26
+ try {
27
+ const snapshot = doc.export({ mode: 'snapshot' });
28
+ await options.storage.save(snapshot);
29
+ }
30
+ catch (error) {
31
+ options.onError?.(error);
32
+ }
33
+ };
34
+ const pushFromStore = (state) => {
35
+ if (mirrors.length === 0 || suppressStoreToMirror)
36
+ return;
37
+ for (let i = 0; i < mirrors.length; i += 1) {
38
+ const mirror = mirrors[i];
39
+ const key = mirrorKeys[i];
40
+ if (!mirror || !key)
41
+ continue;
42
+ const cfg = options.mirrors[key];
43
+ if (!cfg)
44
+ continue;
45
+ const value = cfg.select ? cfg.select(state) : state[key];
46
+ const lastOutbound = lastOutboundByKey.get(key);
47
+ if (lastOutbound === value)
48
+ continue;
49
+ lastOutboundByKey.set(key, value);
50
+ mirror.setState((draft) => {
51
+ draft[key] = value;
52
+ }, { tags: ['from-store'] });
53
+ }
54
+ void persistDoc();
55
+ };
56
+ const applyMirrorToStore = (key, cfg, mirrorState, tags) => {
57
+ if (tags?.includes('from-store'))
58
+ return;
59
+ suppressStoreToMirror = true;
60
+ try {
61
+ const value = mirrorState?.[key];
62
+ if (cfg.apply) {
63
+ cfg.apply(value, set, get);
64
+ }
65
+ else {
66
+ set({ [key]: value });
67
+ }
68
+ }
69
+ finally {
70
+ suppressStoreToMirror = false;
71
+ }
72
+ void persistDoc();
73
+ };
74
+ const initialize = async () => {
75
+ // Idempotency: room stores (and React StrictMode in dev) can call initialize()
76
+ // more than once. A second initialization without a prior destroy() would leak
77
+ // subscriptions and can desync the sync connector from the active doc.
78
+ if (initializing)
79
+ return initializing;
80
+ if (get().crdt?.status === 'ready' && doc && mirrors.length > 0)
81
+ return;
82
+ initializing = (async () => {
83
+ try {
84
+ mirrorKeys = Object.keys(options.mirrors);
85
+ if (mirrorKeys.length === 0) {
86
+ throw new Error('[crdt] `mirrors` must have at least one entry.');
87
+ }
88
+ doc = options.doc ?? options.createDoc?.() ?? new LoroDoc();
89
+ if (!doc)
90
+ throw new Error('[crdt] Failed to create Loro doc');
91
+ const activeDoc = doc;
92
+ if (options.storage) {
93
+ const snapshot = await options.storage.load();
94
+ if (snapshot) {
95
+ activeDoc.import(snapshot);
96
+ }
97
+ }
98
+ mirrors = mirrorKeys.map((key) => {
99
+ const cfg = options.mirrors[key];
100
+ if (!cfg)
101
+ throw new Error(`[crdt] Missing mirror config for "${key}".`);
102
+ // Wrap per-mirror schema/value under its root key.
103
+ const rootSchema = schema({ [key]: cfg.schema });
104
+ const initialState = cfg.initialState
105
+ ? { [key]: cfg.initialState }
106
+ : undefined;
107
+ return new Mirror({
108
+ doc: activeDoc,
109
+ schema: rootSchema,
110
+ initialState,
111
+ ...(cfg.mirrorOptions ?? options.mirrorOptions ?? {}),
112
+ });
113
+ });
114
+ // Debug local doc updates to verify mirror.setState is producing CRDT ops.
115
+ unsubDocLocal = doc.subscribeLocalUpdates?.((update) => { });
116
+ // Subscribe mirror->store first so snapshot/imported state wins.
117
+ unsubMirrors = mirrors.map((m, idx) => {
118
+ const key = mirrorKeys[idx];
119
+ if (!key)
120
+ return () => { };
121
+ const cfg = options.mirrors[key];
122
+ if (!cfg)
123
+ return () => { };
124
+ return m.subscribe((state, meta) => applyMirrorToStore(key, cfg, state, meta?.tags));
125
+ });
126
+ // Wait a tick so mirrors can emit any imported state, then align store once.
127
+ await Promise.resolve();
128
+ for (let i = 0; i < mirrors.length; i += 1) {
129
+ const m = mirrors[i];
130
+ const key = mirrorKeys[i];
131
+ if (!m || !key)
132
+ continue;
133
+ const cfg = options.mirrors[key];
134
+ if (!cfg)
135
+ continue;
136
+ applyMirrorToStore(key, cfg, m.getState(), []);
137
+ }
138
+ // Now subscribe store->mirror for local changes.
139
+ unsubStore = store.subscribe((state) => {
140
+ pushFromStore(state);
141
+ });
142
+ if (options.sync) {
143
+ options.sync.setStatusListener?.((status) => {
144
+ get().crdt.setConnectionStatus(status);
145
+ });
146
+ await options.sync.connect(doc);
147
+ }
148
+ const prevCrdt = get().crdt;
149
+ set({
150
+ crdt: {
151
+ ...(prevCrdt ?? {}),
152
+ status: 'ready',
153
+ error: undefined,
154
+ initialize: prevCrdt?.initialize ?? initialize,
155
+ destroy: prevCrdt?.destroy ?? destroy,
156
+ },
157
+ });
158
+ }
159
+ catch (error) {
160
+ options.onError?.(error);
161
+ const prevCrdt = get().crdt;
162
+ set({
163
+ crdt: {
164
+ ...(prevCrdt ?? {}),
165
+ status: 'error',
166
+ error: error?.message ?? 'Failed to initialize CRDT',
167
+ initialize: prevCrdt?.initialize ?? initialize,
168
+ destroy: prevCrdt?.destroy ?? destroy,
169
+ },
170
+ });
171
+ }
172
+ finally {
173
+ initializing = undefined;
174
+ }
175
+ })();
176
+ return initializing;
177
+ };
178
+ const destroy = async () => {
179
+ unsubStore?.();
180
+ for (const unsub of unsubMirrors)
181
+ unsub?.();
182
+ unsubDocLocal?.();
183
+ if (options.sync?.disconnect) {
184
+ await options.sync.disconnect();
185
+ }
186
+ doc = undefined;
187
+ mirrors = [];
188
+ mirrorKeys = [];
189
+ unsubMirrors = [];
190
+ initializing = undefined;
191
+ const prevCrdt = get().crdt;
192
+ set({
193
+ crdt: {
194
+ ...(prevCrdt ?? {}),
195
+ status: 'idle',
196
+ error: undefined,
197
+ connectionStatus: 'idle',
198
+ initialize: prevCrdt?.initialize ?? initialize,
199
+ destroy: prevCrdt?.destroy ?? destroy,
200
+ },
201
+ });
202
+ };
203
+ return {
204
+ crdt: {
205
+ status: 'idle',
206
+ connectionStatus: 'idle',
207
+ setConnectionStatus: (status) => {
208
+ const prev = get().crdt;
209
+ set({
210
+ crdt: {
211
+ ...(prev ?? {}),
212
+ connectionStatus: status,
213
+ },
214
+ });
215
+ },
216
+ initialize,
217
+ destroy,
218
+ },
219
+ };
220
+ });
221
+ }
222
+ //# sourceMappingURL=createCrdtSlice.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createCrdtSlice.js","sourceRoot":"","sources":["../src/createCrdtSlice.ts"],"names":[],"mappings":"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","sourcesContent":["import {setAutoFreeze} from 'immer';\nimport {LoroDoc} from 'loro-crdt';\nimport {InferInputType, Mirror, SchemaType, schema} from 'loro-mirror';\nimport {StateCreator} from 'zustand';\nimport {\n InferredState,\n MirrorSchema,\n StoreGet,\n StoreSet,\n StripCidDeep,\n createSlice,\n} from './type-helpers';\n\n// Mirror can’t stamp $cid on frozen objects, so disable auto-freeze.\nsetAutoFreeze(false);\n\nexport type CrdtMirrorValueSelector<S, TSchema extends SchemaType> = (\n state: S,\n) => StripCidDeep<InferredState<TSchema>>;\n\nexport type CrdtMirrorValueApplier<S, TSchema extends SchemaType> = (\n value: InferredState<TSchema>,\n set: StoreSet<S>,\n get: StoreGet<S>,\n) => void;\n\nexport type CrdtDocStorage = {\n load: () => Promise<Uint8Array | undefined>;\n save: (data: Uint8Array) => Promise<void>;\n};\n\nexport type CrdtConnectionStatus =\n | 'idle'\n | 'connecting'\n | 'open'\n | 'closed'\n | 'error';\n\nexport type CrdtSyncConnector = {\n connect: (doc: LoroDoc) => Promise<void>;\n disconnect?: () => Promise<void>;\n /**\n * Optional hook for connectors to report connection status into the CRDT slice.\n *\n * `createCrdtSlice` will wire this automatically to `crdt.setConnectionStatus`,\n * so consumer apps don't need to pass `onStatus` callbacks manually.\n */\n setStatusListener?: (\n listener: (status: CrdtConnectionStatus) => void,\n ) => void;\n};\n\nexport type CrdtMirror<S, TSchema extends SchemaType = SchemaType> = {\n /**\n * Schema for the value stored under this mirror's key in the Loro doc.\n *\n * Example: if the mirror key is `\"canvas\"`, this schema describes the value at\n * `doc.root.canvas` (not an extra `{canvas: ...}` wrapper).\n */\n schema: MirrorSchema<TSchema>;\n /**\n * Select the value to write under this mirror key.\n *\n * If omitted, the store field at the same key name will be mirrored.\n */\n select?: CrdtMirrorValueSelector<S, TSchema>;\n /**\n * Apply an incoming CRDT value under this mirror key back into the store.\n *\n * If omitted, the store field at the same key name will be replaced.\n */\n apply?: CrdtMirrorValueApplier<S, TSchema>;\n /**\n * Initial value written under this mirror key when creating the Mirror.\n */\n initialState?: Partial<InferInputType<TSchema>>;\n mirrorOptions?: Record<string, unknown>;\n};\n\nexport type CreateCrdtSliceOptions<S, TSchema extends SchemaType> = {\n /**\n * CRDT mirrors keyed by their root key in the Loro document.\n *\n * Each entry becomes one `loro-mirror` `Mirror` instance on a shared `LoroDoc`.\n */\n mirrors: Record<string, CrdtMirror<S, any>>;\n doc?: LoroDoc;\n createDoc?: () => LoroDoc;\n storage?: CrdtDocStorage;\n sync?: CrdtSyncConnector;\n mirrorOptions?: Record<string, unknown>;\n onError?: (error: unknown) => void;\n};\n\nexport type CrdtSliceState = {\n crdt: {\n status: 'idle' | 'ready' | 'error';\n error?: string;\n /**\n * Optional sync connection status for UIs.\n *\n * This is intentionally generic and primarily used by websocket-based sync\n * connectors via `onStatus`.\n */\n connectionStatus: CrdtConnectionStatus;\n /**\n * Update `connectionStatus` (typically wired to a sync connector `onStatus` callback).\n */\n setConnectionStatus: (status: CrdtConnectionStatus) => void;\n initialize: () => Promise<void>;\n destroy: () => Promise<void>;\n };\n};\n\n/**\n * Create a CRDT-backed slice that mirrors selected store fields into a Loro doc.\n *\n * The returned state creator is intended to be composed into a larger Zustand store.\n */\nexport function createCrdtSlice<\n S extends Record<string, any>,\n TSchema extends SchemaType = SchemaType,\n>(options: CreateCrdtSliceOptions<S, TSchema>): StateCreator<CrdtSliceState> {\n return createSlice<CrdtSliceState, S & CrdtSliceState>((set, get, store) => {\n let doc: LoroDoc | undefined;\n let mirrors: Mirror<any>[] = [];\n let mirrorKeys: string[] = [];\n let unsubStore: (() => void) | undefined;\n let unsubMirrors: Array<() => void> = [];\n let unsubDocLocal: (() => void) | undefined;\n let initializing: Promise<void> | undefined;\n let suppressStoreToMirror = false;\n const lastOutboundByKey = new Map<string, unknown>();\n\n const persistDoc = async () => {\n if (!options.storage || !doc) return;\n try {\n const snapshot = doc.export({mode: 'snapshot'});\n await options.storage.save(snapshot);\n } catch (error) {\n options.onError?.(error);\n }\n };\n\n const pushFromStore = (state: S) => {\n if (mirrors.length === 0 || suppressStoreToMirror) return;\n for (let i = 0; i < mirrors.length; i += 1) {\n const mirror = mirrors[i];\n const key = mirrorKeys[i];\n if (!mirror || !key) continue;\n const cfg = options.mirrors[key];\n if (!cfg) continue;\n\n const value = cfg.select ? cfg.select(state) : (state as any)[key];\n\n const lastOutbound = lastOutboundByKey.get(key);\n if (lastOutbound === value) continue;\n\n lastOutboundByKey.set(key, value);\n\n mirror.setState(\n (draft: any) => {\n draft[key] = value;\n },\n {tags: ['from-store']},\n );\n }\n void persistDoc();\n };\n\n const applyMirrorToStore = (\n key: string,\n cfg: CrdtMirror<S, any>,\n mirrorState: any,\n tags?: string[],\n ) => {\n if (tags?.includes('from-store')) return;\n suppressStoreToMirror = true;\n try {\n const value = mirrorState?.[key];\n if (cfg.apply) {\n cfg.apply(value, set, get);\n } else {\n set({[key]: value} as any);\n }\n } finally {\n suppressStoreToMirror = false;\n }\n void persistDoc();\n };\n\n const initialize = async () => {\n // Idempotency: room stores (and React StrictMode in dev) can call initialize()\n // more than once. A second initialization without a prior destroy() would leak\n // subscriptions and can desync the sync connector from the active doc.\n if (initializing) return initializing;\n if (get().crdt?.status === 'ready' && doc && mirrors.length > 0) return;\n\n initializing = (async () => {\n try {\n mirrorKeys = Object.keys(options.mirrors);\n if (mirrorKeys.length === 0) {\n throw new Error('[crdt] `mirrors` must have at least one entry.');\n }\n\n doc = options.doc ?? options.createDoc?.() ?? new LoroDoc();\n if (!doc) throw new Error('[crdt] Failed to create Loro doc');\n const activeDoc = doc;\n\n if (options.storage) {\n const snapshot = await options.storage.load();\n if (snapshot) {\n activeDoc.import(snapshot);\n }\n }\n\n mirrors = mirrorKeys.map((key) => {\n const cfg = options.mirrors[key];\n if (!cfg)\n throw new Error(`[crdt] Missing mirror config for \"${key}\".`);\n\n // Wrap per-mirror schema/value under its root key.\n const rootSchema = schema({[key]: cfg.schema as any} as any);\n const initialState = cfg.initialState\n ? ({[key]: cfg.initialState} as any)\n : undefined;\n\n return new Mirror<any>({\n doc: activeDoc,\n schema: rootSchema as any,\n initialState,\n ...(cfg.mirrorOptions ?? options.mirrorOptions ?? {}),\n });\n });\n // Debug local doc updates to verify mirror.setState is producing CRDT ops.\n unsubDocLocal = doc.subscribeLocalUpdates?.(\n (update: Uint8Array) => {},\n );\n\n // Subscribe mirror->store first so snapshot/imported state wins.\n unsubMirrors = mirrors.map((m, idx) => {\n const key = mirrorKeys[idx];\n if (!key) return () => {};\n const cfg = options.mirrors[key];\n if (!cfg) return () => {};\n return m.subscribe((state: any, meta: any) =>\n applyMirrorToStore(key, cfg, state, meta?.tags),\n );\n });\n\n // Wait a tick so mirrors can emit any imported state, then align store once.\n await Promise.resolve();\n for (let i = 0; i < mirrors.length; i += 1) {\n const m = mirrors[i];\n const key = mirrorKeys[i];\n if (!m || !key) continue;\n const cfg = options.mirrors[key];\n if (!cfg) continue;\n applyMirrorToStore(key, cfg, m.getState() as any, []);\n }\n // Now subscribe store->mirror for local changes.\n unsubStore = store.subscribe((state) => {\n pushFromStore(state);\n });\n\n if (options.sync) {\n options.sync.setStatusListener?.((status) => {\n get().crdt.setConnectionStatus(status);\n });\n await options.sync.connect(doc);\n }\n\n const prevCrdt = get().crdt;\n set({\n crdt: {\n ...(prevCrdt ?? {}),\n status: 'ready',\n error: undefined,\n initialize: prevCrdt?.initialize ?? initialize,\n destroy: prevCrdt?.destroy ?? destroy,\n },\n } as Partial<S & CrdtSliceState>);\n } catch (error: any) {\n options.onError?.(error);\n const prevCrdt = get().crdt;\n set({\n crdt: {\n ...(prevCrdt ?? {}),\n status: 'error',\n error: error?.message ?? 'Failed to initialize CRDT',\n initialize: prevCrdt?.initialize ?? initialize,\n destroy: prevCrdt?.destroy ?? destroy,\n },\n } as Partial<S & CrdtSliceState>);\n } finally {\n initializing = undefined;\n }\n })();\n\n return initializing;\n };\n\n const destroy = async () => {\n unsubStore?.();\n for (const unsub of unsubMirrors) unsub?.();\n unsubDocLocal?.();\n if (options.sync?.disconnect) {\n await options.sync.disconnect();\n }\n doc = undefined;\n mirrors = [];\n mirrorKeys = [];\n unsubMirrors = [];\n initializing = undefined;\n const prevCrdt = get().crdt;\n set({\n crdt: {\n ...(prevCrdt ?? {}),\n status: 'idle',\n error: undefined,\n connectionStatus: 'idle',\n initialize: prevCrdt?.initialize ?? initialize,\n destroy: prevCrdt?.destroy ?? destroy,\n },\n } as Partial<S & CrdtSliceState>);\n };\n\n return {\n crdt: {\n status: 'idle',\n connectionStatus: 'idle',\n setConnectionStatus: (status) => {\n const prev = get().crdt;\n set({\n crdt: {\n ...(prev ?? {}),\n connectionStatus: status,\n },\n } as Partial<S & CrdtSliceState>);\n },\n initialize,\n destroy,\n },\n };\n });\n}\n"]}
@@ -0,0 +1,7 @@
1
+ export type { CrdtDocStorage, CrdtMirror, CrdtSliceState, CrdtSyncConnector, CreateCrdtSliceOptions, } from './createCrdtSlice';
2
+ export type { MirrorSchema } from './type-helpers';
3
+ export { createCrdtSlice } from './createCrdtSlice';
4
+ export { createLocalStorageDocStorage } from './storages/localStorageStorage';
5
+ export { createIndexedDbDocStorage } from './storages/indexedDbStorage';
6
+ export { createWebSocketSyncConnector } from './sync/webSocketSyncConnector';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,cAAc,EACd,UAAU,EACV,cAAc,EACd,iBAAiB,EACjB,sBAAsB,GACvB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAC,eAAe,EAAC,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAC,4BAA4B,EAAC,MAAM,gCAAgC,CAAC;AAC5E,OAAO,EAAC,yBAAyB,EAAC,MAAM,6BAA6B,CAAC;AACtE,OAAO,EAAC,4BAA4B,EAAC,MAAM,+BAA+B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { createCrdtSlice } from './createCrdtSlice';
2
+ export { createLocalStorageDocStorage } from './storages/localStorageStorage';
3
+ export { createIndexedDbDocStorage } from './storages/indexedDbStorage';
4
+ export { createWebSocketSyncConnector } from './sync/webSocketSyncConnector';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,OAAO,EAAC,eAAe,EAAC,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAC,4BAA4B,EAAC,MAAM,gCAAgC,CAAC;AAC5E,OAAO,EAAC,yBAAyB,EAAC,MAAM,6BAA6B,CAAC;AACtE,OAAO,EAAC,4BAA4B,EAAC,MAAM,+BAA+B,CAAC","sourcesContent":["export type {\n CrdtDocStorage,\n CrdtMirror,\n CrdtSliceState,\n CrdtSyncConnector,\n CreateCrdtSliceOptions,\n} from './createCrdtSlice';\nexport type {MirrorSchema} from './type-helpers';\nexport {createCrdtSlice} from './createCrdtSlice';\nexport {createLocalStorageDocStorage} from './storages/localStorageStorage';\nexport {createIndexedDbDocStorage} from './storages/indexedDbStorage';\nexport {createWebSocketSyncConnector} from './sync/webSocketSyncConnector';\n"]}
@@ -0,0 +1,26 @@
1
+ import { CrdtDocStorage } from '../createCrdtSlice';
2
+ type IndexedDbDocStorageOptions = {
3
+ /**
4
+ * IndexedDB database name.
5
+ * @defaultValue "sqlrooms-crdt"
6
+ */
7
+ dbName?: string;
8
+ /**
9
+ * IndexedDB object store name.
10
+ * @defaultValue "docs"
11
+ */
12
+ storeName?: string;
13
+ /**
14
+ * IndexedDB key to store the snapshot under.
15
+ */
16
+ key: string;
17
+ };
18
+ /**
19
+ * Creates an IndexedDB-backed CRDT doc storage.
20
+ *
21
+ * Prefer IndexedDB over LocalStorage for larger snapshots and better durability.
22
+ * In non-browser environments (SSR/tests without DOM), this storage becomes a no-op.
23
+ */
24
+ export declare function createIndexedDbDocStorage(options: IndexedDbDocStorageOptions): CrdtDocStorage;
25
+ export {};
26
+ //# sourceMappingURL=indexedDbStorage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"indexedDbStorage.d.ts","sourceRoot":"","sources":["../../src/storages/indexedDbStorage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,cAAc,EAAC,MAAM,oBAAoB,CAAC;AAElD,KAAK,0BAA0B,GAAG;IAChC;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;OAEG;IACH,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAiCF;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,0BAA0B,GAClC,cAAc,CA+DhB"}
@@ -0,0 +1,87 @@
1
+ function openDb(dbName, storeName) {
2
+ return new Promise((resolve, reject) => {
3
+ const request = indexedDB.open(dbName, 1);
4
+ request.onupgradeneeded = () => {
5
+ const db = request.result;
6
+ if (!db.objectStoreNames.contains(storeName)) {
7
+ db.createObjectStore(storeName);
8
+ }
9
+ };
10
+ request.onsuccess = () => resolve(request.result);
11
+ request.onerror = () => reject(request.error ?? new Error('Failed to open IndexedDB'));
12
+ });
13
+ }
14
+ function runTx(db, storeName, mode, fn) {
15
+ return new Promise((resolve, reject) => {
16
+ const tx = db.transaction(storeName, mode);
17
+ const store = tx.objectStore(storeName);
18
+ const req = fn(store);
19
+ req.onsuccess = () => resolve(req.result);
20
+ req.onerror = () => reject(req.error ?? new Error('IndexedDB request failed'));
21
+ });
22
+ }
23
+ /**
24
+ * Creates an IndexedDB-backed CRDT doc storage.
25
+ *
26
+ * Prefer IndexedDB over LocalStorage for larger snapshots and better durability.
27
+ * In non-browser environments (SSR/tests without DOM), this storage becomes a no-op.
28
+ */
29
+ export function createIndexedDbDocStorage(options) {
30
+ const dbName = options.dbName ?? 'sqlrooms-crdt';
31
+ const storeName = options.storeName ?? 'docs';
32
+ const key = options.key;
33
+ let dbPromise;
34
+ const getDb = async () => {
35
+ if (typeof window === 'undefined' || typeof indexedDB === 'undefined') {
36
+ return undefined;
37
+ }
38
+ dbPromise ??= openDb(dbName, storeName);
39
+ return dbPromise;
40
+ };
41
+ return {
42
+ async load() {
43
+ const db = await getDb();
44
+ if (!db)
45
+ return undefined;
46
+ try {
47
+ const result = await runTx(db, storeName, 'readonly', (s) => s.get(key));
48
+ if (!result)
49
+ return undefined;
50
+ if (result instanceof ArrayBuffer)
51
+ return new Uint8Array(result);
52
+ if (ArrayBuffer.isView(result)) {
53
+ const view = result;
54
+ return new Uint8Array(view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength));
55
+ }
56
+ // Back-compat if someone stored a base64 string.
57
+ if (typeof result === 'string') {
58
+ const binary = atob(result);
59
+ const bytes = new Uint8Array(binary.length);
60
+ for (let i = 0; i < binary.length; i += 1) {
61
+ bytes[i] = binary.charCodeAt(i);
62
+ }
63
+ return bytes;
64
+ }
65
+ return undefined;
66
+ }
67
+ catch (error) {
68
+ console.warn('Failed to load CRDT snapshot from IndexedDB', error);
69
+ return undefined;
70
+ }
71
+ },
72
+ async save(data) {
73
+ const db = await getDb();
74
+ if (!db)
75
+ return;
76
+ try {
77
+ // Store as ArrayBuffer for broad structured-clone support.
78
+ const buf = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
79
+ await runTx(db, storeName, 'readwrite', (s) => s.put(buf, key));
80
+ }
81
+ catch (error) {
82
+ console.warn('Failed to persist CRDT snapshot to IndexedDB', error);
83
+ }
84
+ },
85
+ };
86
+ }
87
+ //# sourceMappingURL=indexedDbStorage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"indexedDbStorage.js","sourceRoot":"","sources":["../../src/storages/indexedDbStorage.ts"],"names":[],"mappings":"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","sourcesContent":["import {CrdtDocStorage} from '../createCrdtSlice';\n\ntype IndexedDbDocStorageOptions = {\n /**\n * IndexedDB database name.\n * @defaultValue \"sqlrooms-crdt\"\n */\n dbName?: string;\n /**\n * IndexedDB object store name.\n * @defaultValue \"docs\"\n */\n storeName?: string;\n /**\n * IndexedDB key to store the snapshot under.\n */\n key: string;\n};\n\nfunction openDb(dbName: string, storeName: string): Promise<IDBDatabase> {\n return new Promise((resolve, reject) => {\n const request = indexedDB.open(dbName, 1);\n request.onupgradeneeded = () => {\n const db = request.result;\n if (!db.objectStoreNames.contains(storeName)) {\n db.createObjectStore(storeName);\n }\n };\n request.onsuccess = () => resolve(request.result);\n request.onerror = () =>\n reject(request.error ?? new Error('Failed to open IndexedDB'));\n });\n}\n\nfunction runTx<T>(\n db: IDBDatabase,\n storeName: string,\n mode: IDBTransactionMode,\n fn: (store: IDBObjectStore) => IDBRequest<T>,\n): Promise<T> {\n return new Promise((resolve, reject) => {\n const tx = db.transaction(storeName, mode);\n const store = tx.objectStore(storeName);\n const req = fn(store);\n req.onsuccess = () => resolve(req.result);\n req.onerror = () =>\n reject(req.error ?? new Error('IndexedDB request failed'));\n });\n}\n\n/**\n * Creates an IndexedDB-backed CRDT doc storage.\n *\n * Prefer IndexedDB over LocalStorage for larger snapshots and better durability.\n * In non-browser environments (SSR/tests without DOM), this storage becomes a no-op.\n */\nexport function createIndexedDbDocStorage(\n options: IndexedDbDocStorageOptions,\n): CrdtDocStorage {\n const dbName = options.dbName ?? 'sqlrooms-crdt';\n const storeName = options.storeName ?? 'docs';\n const key = options.key;\n\n let dbPromise: Promise<IDBDatabase> | undefined;\n const getDb = async () => {\n if (typeof window === 'undefined' || typeof indexedDB === 'undefined') {\n return undefined;\n }\n dbPromise ??= openDb(dbName, storeName);\n return dbPromise;\n };\n\n return {\n async load() {\n const db = await getDb();\n if (!db) return undefined;\n try {\n const result = await runTx<unknown>(db, storeName, 'readonly', (s) =>\n s.get(key),\n );\n if (!result) return undefined;\n if (result instanceof ArrayBuffer) return new Uint8Array(result);\n if (ArrayBuffer.isView(result)) {\n const view = result as ArrayBufferView;\n return new Uint8Array(\n view.buffer.slice(\n view.byteOffset,\n view.byteOffset + view.byteLength,\n ),\n );\n }\n // Back-compat if someone stored a base64 string.\n if (typeof result === 'string') {\n const binary = atob(result);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i += 1) {\n bytes[i] = binary.charCodeAt(i);\n }\n return bytes;\n }\n return undefined;\n } catch (error) {\n console.warn('Failed to load CRDT snapshot from IndexedDB', error);\n return undefined;\n }\n },\n async save(data: Uint8Array) {\n const db = await getDb();\n if (!db) return;\n try {\n // Store as ArrayBuffer for broad structured-clone support.\n const buf = data.buffer.slice(\n data.byteOffset,\n data.byteOffset + data.byteLength,\n );\n await runTx(db, storeName, 'readwrite', (s) => s.put(buf, key));\n } catch (error) {\n console.warn('Failed to persist CRDT snapshot to IndexedDB', error);\n }\n },\n };\n}\n"]}
@@ -0,0 +1,7 @@
1
+ import { CrdtDocStorage } from '../createCrdtSlice';
2
+ type LocalStorageDocStorageOptions = {
3
+ key: string;
4
+ };
5
+ export declare function createLocalStorageDocStorage({ key, }: LocalStorageDocStorageOptions): CrdtDocStorage;
6
+ export {};
7
+ //# sourceMappingURL=localStorageStorage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"localStorageStorage.d.ts","sourceRoot":"","sources":["../../src/storages/localStorageStorage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,cAAc,EAAC,MAAM,oBAAoB,CAAC;AAmBlD,KAAK,6BAA6B,GAAG;IACnC,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,wBAAgB,4BAA4B,CAAC,EAC3C,GAAG,GACJ,EAAE,6BAA6B,GAAG,cAAc,CAsBhD"}