@trestleinc/replicate 0.1.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +356 -420
  2. package/dist/client/collection.d.ts +78 -76
  3. package/dist/client/errors.d.ts +59 -0
  4. package/dist/client/index.d.ts +22 -18
  5. package/dist/client/logger.d.ts +0 -1
  6. package/dist/client/merge.d.ts +77 -0
  7. package/dist/client/persistence/adapters/index.d.ts +8 -0
  8. package/dist/client/persistence/adapters/opsqlite.d.ts +46 -0
  9. package/dist/client/persistence/adapters/sqljs.d.ts +83 -0
  10. package/dist/client/persistence/index.d.ts +49 -0
  11. package/dist/client/persistence/indexeddb.d.ts +17 -0
  12. package/dist/client/persistence/memory.d.ts +16 -0
  13. package/dist/client/persistence/sqlite-browser.d.ts +51 -0
  14. package/dist/client/persistence/sqlite-level.d.ts +63 -0
  15. package/dist/client/persistence/sqlite-rn.d.ts +36 -0
  16. package/dist/client/persistence/sqlite.d.ts +47 -0
  17. package/dist/client/persistence/types.d.ts +42 -0
  18. package/dist/client/prose.d.ts +56 -0
  19. package/dist/client/replicate.d.ts +40 -0
  20. package/dist/client/services/checkpoint.d.ts +18 -0
  21. package/dist/client/services/reconciliation.d.ts +24 -0
  22. package/dist/component/_generated/api.d.ts +35 -0
  23. package/dist/component/_generated/api.js +3 -3
  24. package/dist/component/_generated/component.d.ts +89 -0
  25. package/dist/component/_generated/component.js +0 -0
  26. package/dist/component/_generated/dataModel.d.ts +45 -0
  27. package/dist/component/_generated/dataModel.js +0 -0
  28. package/{src → dist}/component/_generated/server.d.ts +9 -38
  29. package/dist/component/convex.config.d.ts +2 -2
  30. package/dist/component/convex.config.js +2 -1
  31. package/dist/component/logger.d.ts +8 -0
  32. package/dist/component/logger.js +30 -0
  33. package/dist/component/public.d.ts +36 -61
  34. package/dist/component/public.js +232 -58
  35. package/dist/component/schema.d.ts +32 -8
  36. package/dist/component/schema.js +19 -6
  37. package/dist/index.js +1553 -308
  38. package/dist/server/builder.d.ts +94 -0
  39. package/dist/server/index.d.ts +14 -17
  40. package/dist/server/schema.d.ts +17 -63
  41. package/dist/server/storage.d.ts +80 -0
  42. package/dist/server.js +268 -83
  43. package/dist/shared/index.d.ts +5 -0
  44. package/dist/shared/index.js +2 -0
  45. package/dist/shared/types.d.ts +50 -0
  46. package/dist/shared/types.js +6 -0
  47. package/dist/shared.js +6 -0
  48. package/package.json +59 -49
  49. package/src/client/collection.ts +877 -450
  50. package/src/client/errors.ts +45 -0
  51. package/src/client/index.ts +52 -26
  52. package/src/client/logger.ts +2 -28
  53. package/src/client/merge.ts +374 -0
  54. package/src/client/persistence/adapters/index.ts +8 -0
  55. package/src/client/persistence/adapters/opsqlite.ts +54 -0
  56. package/src/client/persistence/adapters/sqljs.ts +128 -0
  57. package/src/client/persistence/index.ts +54 -0
  58. package/src/client/persistence/indexeddb.ts +110 -0
  59. package/src/client/persistence/memory.ts +61 -0
  60. package/src/client/persistence/sqlite-browser.ts +107 -0
  61. package/src/client/persistence/sqlite-level.ts +407 -0
  62. package/src/client/persistence/sqlite-rn.ts +44 -0
  63. package/src/client/persistence/sqlite.ts +161 -0
  64. package/src/client/persistence/types.ts +49 -0
  65. package/src/client/prose.ts +369 -0
  66. package/src/client/replicate.ts +80 -0
  67. package/src/client/services/checkpoint.ts +86 -0
  68. package/src/client/services/reconciliation.ts +108 -0
  69. package/src/component/_generated/api.ts +52 -0
  70. package/src/component/_generated/component.ts +103 -0
  71. package/src/component/_generated/{dataModel.d.ts → dataModel.ts} +1 -1
  72. package/src/component/_generated/server.ts +161 -0
  73. package/src/component/convex.config.ts +3 -1
  74. package/src/component/logger.ts +36 -0
  75. package/src/component/public.ts +364 -111
  76. package/src/component/schema.ts +18 -5
  77. package/src/env.d.ts +31 -0
  78. package/src/server/builder.ts +85 -0
  79. package/src/server/index.ts +9 -24
  80. package/src/server/schema.ts +20 -76
  81. package/src/server/storage.ts +313 -0
  82. package/src/shared/index.ts +5 -0
  83. package/src/shared/types.ts +52 -0
  84. package/LICENSE.package +0 -201
  85. package/dist/client/storage.d.ts +0 -143
  86. package/dist/server/replication.d.ts +0 -122
  87. package/dist/server/ssr.d.ts +0 -79
  88. package/dist/ssr.js +0 -19
  89. package/src/client/storage.ts +0 -206
  90. package/src/component/_generated/api.d.ts +0 -95
  91. package/src/component/_generated/api.js +0 -23
  92. package/src/component/_generated/server.js +0 -90
  93. package/src/server/replication.ts +0 -244
  94. package/src/server/ssr.ts +0 -106
@@ -0,0 +1,407 @@
1
+ /**
2
+ * SQLite-backed abstract-level implementation.
3
+ *
4
+ * Provides a LevelDB-compatible key-value store backed by SQLite,
5
+ * enabling y-leveldb to work with SQLite databases.
6
+ *
7
+ * Supports both browser (sql.js WASM) and React Native (op-sqlite).
8
+ */
9
+ import {
10
+ AbstractLevel,
11
+ AbstractIterator,
12
+ AbstractKeyIterator,
13
+ AbstractValueIterator,
14
+ } from 'abstract-level';
15
+
16
+ /**
17
+ * Interface for SQLite database operations.
18
+ * Abstracts over sql.js (browser) and op-sqlite (React Native).
19
+ */
20
+ export interface SqliteAdapter {
21
+ execute(sql: string, params?: unknown[]): Promise<{ rows: Record<string, unknown>[] }>;
22
+ close(): void;
23
+ }
24
+
25
+ interface SqliteLevelOptions {
26
+ /** Custom SQLite adapter (for testing or alternative backends) */
27
+ adapter?: SqliteAdapter;
28
+ /** Value encoding (default: 'utf8') */
29
+ valueEncoding?: string;
30
+ keyEncoding?: string;
31
+ }
32
+
33
+ /**
34
+ * SQLite-backed implementation of abstract-level.
35
+ *
36
+ * Uses a simple key-value table with lexicographic ordering:
37
+ * CREATE TABLE entries (key BLOB PRIMARY KEY, value BLOB)
38
+ */
39
+ export class SqliteLevel<K = string, V = string> extends AbstractLevel<K, V> {
40
+ private adapter: SqliteAdapter | null = null;
41
+ private adapterFactory: (() => Promise<SqliteAdapter>) | null = null;
42
+
43
+ constructor(_location: string, options?: SqliteLevelOptions) {
44
+ super(
45
+ {
46
+ encodings: { utf8: true, buffer: true, view: true },
47
+ seek: true,
48
+ permanence: true,
49
+ createIfMissing: true,
50
+ errorIfExists: false,
51
+ additionalMethods: {},
52
+ },
53
+ {
54
+ keyEncoding: options?.keyEncoding ?? 'utf8',
55
+ valueEncoding: options?.valueEncoding ?? 'utf8',
56
+ }
57
+ );
58
+
59
+ if (options?.adapter) {
60
+ this.adapter = options.adapter;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Set the adapter factory for deferred initialization.
66
+ * Call this before open() to configure the SQLite backend.
67
+ */
68
+ setAdapterFactory(factory: () => Promise<SqliteAdapter>): void {
69
+ this.adapterFactory = factory;
70
+ }
71
+
72
+ async _open(): Promise<void> {
73
+ if (!this.adapter) {
74
+ if (this.adapterFactory) {
75
+ this.adapter = await this.adapterFactory();
76
+ } else {
77
+ throw new Error('No SQLite adapter configured. Call setAdapterFactory() before open().');
78
+ }
79
+ }
80
+
81
+ // Create the entries table if it doesn't exist
82
+ await this.adapter.execute(`
83
+ CREATE TABLE IF NOT EXISTS entries (
84
+ key BLOB PRIMARY KEY,
85
+ value BLOB NOT NULL
86
+ )
87
+ `);
88
+
89
+ // Create index for range queries (lexicographic ordering)
90
+ await this.adapter.execute(`
91
+ CREATE INDEX IF NOT EXISTS entries_key_idx ON entries (key)
92
+ `);
93
+ }
94
+
95
+ async _close(): Promise<void> {
96
+ if (this.adapter) {
97
+ this.adapter.close();
98
+ this.adapter = null;
99
+ }
100
+ }
101
+
102
+ async _get(key: K): Promise<V | undefined> {
103
+ if (!this.adapter) throw new Error('Database not open');
104
+
105
+ const keyBytes = this.encodeKey(key);
106
+ const result = await this.adapter.execute('SELECT value FROM entries WHERE key = ?', [
107
+ keyBytes,
108
+ ]);
109
+
110
+ if (result.rows.length === 0) {
111
+ return undefined;
112
+ }
113
+
114
+ return this.decodeValue(result.rows[0].value as Uint8Array);
115
+ }
116
+
117
+ async _put(key: K, value: V): Promise<void> {
118
+ if (!this.adapter) throw new Error('Database not open');
119
+
120
+ const keyBytes = this.encodeKey(key);
121
+ const valueBytes = this.encodeValue(value);
122
+
123
+ await this.adapter.execute('INSERT OR REPLACE INTO entries (key, value) VALUES (?, ?)', [
124
+ keyBytes,
125
+ valueBytes,
126
+ ]);
127
+ }
128
+
129
+ async _del(key: K): Promise<void> {
130
+ if (!this.adapter) throw new Error('Database not open');
131
+
132
+ const keyBytes = this.encodeKey(key);
133
+ await this.adapter.execute('DELETE FROM entries WHERE key = ?', [keyBytes]);
134
+ }
135
+
136
+ async _batch(
137
+ operations: Array<{ type: 'put'; key: K; value: V } | { type: 'del'; key: K }>
138
+ ): Promise<void> {
139
+ if (!this.adapter) throw new Error('Database not open');
140
+
141
+ // Execute all operations in a transaction
142
+ await this.adapter.execute('BEGIN TRANSACTION');
143
+
144
+ try {
145
+ for (const op of operations) {
146
+ if (op.type === 'put') {
147
+ const keyBytes = this.encodeKey(op.key);
148
+ const valueBytes = this.encodeValue(op.value);
149
+ await this.adapter.execute('INSERT OR REPLACE INTO entries (key, value) VALUES (?, ?)', [
150
+ keyBytes,
151
+ valueBytes,
152
+ ]);
153
+ } else if (op.type === 'del') {
154
+ const keyBytes = this.encodeKey(op.key);
155
+ await this.adapter.execute('DELETE FROM entries WHERE key = ?', [keyBytes]);
156
+ }
157
+ }
158
+ await this.adapter.execute('COMMIT');
159
+ } catch (error) {
160
+ await this.adapter.execute('ROLLBACK');
161
+ throw error;
162
+ }
163
+ }
164
+
165
+ async _clear(): Promise<void> {
166
+ if (!this.adapter) throw new Error('Database not open');
167
+ await this.adapter.execute('DELETE FROM entries');
168
+ }
169
+
170
+ _iterator(options: Record<string, unknown>): AbstractIterator<typeof this, K, V> {
171
+ if (!this.adapter) throw new Error('Database not open');
172
+ return new SqliteIterator(this, this.adapter, options) as unknown as AbstractIterator<
173
+ typeof this,
174
+ K,
175
+ V
176
+ >;
177
+ }
178
+
179
+ _keys(options: Record<string, unknown>): AbstractKeyIterator<typeof this, K> {
180
+ if (!this.adapter) throw new Error('Database not open');
181
+ return new SqliteKeyIterator(this, this.adapter, options) as unknown as AbstractKeyIterator<
182
+ typeof this,
183
+ K
184
+ >;
185
+ }
186
+
187
+ _values(options: Record<string, unknown>): AbstractValueIterator<typeof this, K, V> {
188
+ if (!this.adapter) throw new Error('Database not open');
189
+ return new SqliteValueIterator(this, this.adapter, options) as unknown as AbstractValueIterator<
190
+ typeof this,
191
+ K,
192
+ V
193
+ >;
194
+ }
195
+
196
+ // Helper methods for encoding/decoding
197
+ private encodeKey(key: K): Uint8Array {
198
+ if (key instanceof Uint8Array) {
199
+ return key;
200
+ }
201
+ if (typeof key === 'string') {
202
+ return new TextEncoder().encode(key);
203
+ }
204
+ return new TextEncoder().encode(String(key));
205
+ }
206
+
207
+ private encodeValue(value: V): Uint8Array {
208
+ if (value instanceof Uint8Array) {
209
+ return value;
210
+ }
211
+ if (typeof value === 'string') {
212
+ return new TextEncoder().encode(value);
213
+ }
214
+ return new TextEncoder().encode(JSON.stringify(value));
215
+ }
216
+
217
+ private decodeValue(bytes: Uint8Array): V {
218
+ return new TextDecoder().decode(bytes) as V;
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Iterator for key-value pairs.
224
+ */
225
+ class SqliteIterator<K, V> extends AbstractIterator<SqliteLevel<K, V>, K, V> {
226
+ private adapter: SqliteAdapter;
227
+ private options: Record<string, unknown>;
228
+ private rows: Array<{ key: Uint8Array; value: Uint8Array }> | null = null;
229
+ private index = 0;
230
+
231
+ constructor(db: SqliteLevel<K, V>, adapter: SqliteAdapter, options: Record<string, unknown>) {
232
+ super(db, options);
233
+ this.adapter = adapter;
234
+ this.options = options;
235
+ }
236
+
237
+ async _next(): Promise<[K, V] | undefined> {
238
+ if (this.rows === null) {
239
+ await this.loadRows();
240
+ }
241
+
242
+ if (this.rows && this.index < this.rows.length) {
243
+ const row = this.rows[this.index++];
244
+ const key = new TextDecoder().decode(row.key) as K;
245
+ const value = new TextDecoder().decode(row.value) as V;
246
+ return [key, value];
247
+ }
248
+
249
+ return undefined;
250
+ }
251
+
252
+ async _nextv(size: number): Promise<Array<[K, V]>> {
253
+ if (this.rows === null) {
254
+ await this.loadRows();
255
+ }
256
+
257
+ const result: Array<[K, V]> = [];
258
+ while (this.rows && this.index < this.rows.length && result.length < size) {
259
+ const row = this.rows[this.index++];
260
+ const key = new TextDecoder().decode(row.key) as K;
261
+ const value = new TextDecoder().decode(row.value) as V;
262
+ result.push([key, value]);
263
+ }
264
+ return result;
265
+ }
266
+
267
+ private async loadRows(): Promise<void> {
268
+ const { reverse, limit, gt, gte, lt, lte } = this.options as {
269
+ reverse?: boolean;
270
+ limit?: number;
271
+ gt?: K;
272
+ gte?: K;
273
+ lt?: K;
274
+ lte?: K;
275
+ };
276
+
277
+ let sql = 'SELECT key, value FROM entries';
278
+ const params: unknown[] = [];
279
+ const conditions: string[] = [];
280
+
281
+ if (gt !== undefined) {
282
+ conditions.push('key > ?');
283
+ params.push(this.encodeKey(gt));
284
+ }
285
+ if (gte !== undefined) {
286
+ conditions.push('key >= ?');
287
+ params.push(this.encodeKey(gte));
288
+ }
289
+ if (lt !== undefined) {
290
+ conditions.push('key < ?');
291
+ params.push(this.encodeKey(lt));
292
+ }
293
+ if (lte !== undefined) {
294
+ conditions.push('key <= ?');
295
+ params.push(this.encodeKey(lte));
296
+ }
297
+
298
+ if (conditions.length > 0) {
299
+ sql += ` WHERE ${conditions.join(' AND ')}`;
300
+ }
301
+
302
+ sql += ` ORDER BY key ${reverse ? 'DESC' : 'ASC'}`;
303
+
304
+ if (limit !== undefined && limit >= 0) {
305
+ sql += ` LIMIT ${limit}`;
306
+ }
307
+
308
+ const result = await this.adapter.execute(sql, params);
309
+ this.rows = result.rows as Array<{ key: Uint8Array; value: Uint8Array }>;
310
+ }
311
+
312
+ private encodeKey(key: K): Uint8Array {
313
+ if (key instanceof Uint8Array) {
314
+ return key;
315
+ }
316
+ if (typeof key === 'string') {
317
+ return new TextEncoder().encode(key);
318
+ }
319
+ return new TextEncoder().encode(String(key));
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Iterator for keys only.
325
+ */
326
+ class SqliteKeyIterator<K, V> extends AbstractKeyIterator<SqliteLevel<K, V>, K> {
327
+ private adapter: SqliteAdapter;
328
+ private options: Record<string, unknown>;
329
+ private rows: Array<{ key: Uint8Array }> | null = null;
330
+ private index = 0;
331
+
332
+ constructor(db: SqliteLevel<K, V>, adapter: SqliteAdapter, options: Record<string, unknown>) {
333
+ super(db, options);
334
+ this.adapter = adapter;
335
+ this.options = options;
336
+ }
337
+
338
+ async _next(): Promise<K | undefined> {
339
+ if (this.rows === null) {
340
+ await this.loadRows();
341
+ }
342
+
343
+ if (this.rows && this.index < this.rows.length) {
344
+ const row = this.rows[this.index++];
345
+ return new TextDecoder().decode(row.key) as K;
346
+ }
347
+
348
+ return undefined;
349
+ }
350
+
351
+ private async loadRows(): Promise<void> {
352
+ const { reverse, limit } = this.options as { reverse?: boolean; limit?: number };
353
+
354
+ let sql = 'SELECT key FROM entries';
355
+ sql += ` ORDER BY key ${reverse ? 'DESC' : 'ASC'}`;
356
+
357
+ if (limit !== undefined && limit >= 0) {
358
+ sql += ` LIMIT ${limit}`;
359
+ }
360
+
361
+ const result = await this.adapter.execute(sql);
362
+ this.rows = result.rows as Array<{ key: Uint8Array }>;
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Iterator for values only.
368
+ */
369
+ class SqliteValueIterator<K, V> extends AbstractValueIterator<SqliteLevel<K, V>, K, V> {
370
+ private adapter: SqliteAdapter;
371
+ private options: Record<string, unknown>;
372
+ private rows: Array<{ value: Uint8Array }> | null = null;
373
+ private index = 0;
374
+
375
+ constructor(db: SqliteLevel<K, V>, adapter: SqliteAdapter, options: Record<string, unknown>) {
376
+ super(db, options);
377
+ this.adapter = adapter;
378
+ this.options = options;
379
+ }
380
+
381
+ async _next(): Promise<V | undefined> {
382
+ if (this.rows === null) {
383
+ await this.loadRows();
384
+ }
385
+
386
+ if (this.rows && this.index < this.rows.length) {
387
+ const row = this.rows[this.index++];
388
+ return new TextDecoder().decode(row.value) as V;
389
+ }
390
+
391
+ return undefined;
392
+ }
393
+
394
+ private async loadRows(): Promise<void> {
395
+ const { reverse, limit } = this.options as { reverse?: boolean; limit?: number };
396
+
397
+ let sql = 'SELECT value FROM entries';
398
+ sql += ` ORDER BY key ${reverse ? 'DESC' : 'ASC'}`;
399
+
400
+ if (limit !== undefined && limit >= 0) {
401
+ sql += ` LIMIT ${limit}`;
402
+ }
403
+
404
+ const result = await this.adapter.execute(sql);
405
+ this.rows = result.rows as Array<{ value: Uint8Array }>;
406
+ }
407
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * React Native SQLite persistence helper using op-sqlite.
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * import { createReactNativeSqlitePersistence } from '@trestleinc/replicate/client';
7
+ * import { open } from '@op-engineering/op-sqlite';
8
+ *
9
+ * const db = open({ name: 'myapp.db' });
10
+ * const persistence = await createReactNativeSqlitePersistence(db, 'myapp');
11
+ * ```
12
+ */
13
+ import { OPSqliteAdapter, type OPSQLiteDatabase } from './adapters/opsqlite.js';
14
+ import { sqlitePersistence } from './sqlite.js';
15
+ import type { Persistence } from './types.js';
16
+
17
+ /**
18
+ * Create React Native SQLite persistence using op-sqlite.
19
+ *
20
+ * @param db - The opened op-sqlite database instance
21
+ * @param dbName - Name for internal database identification
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * import { createReactNativeSqlitePersistence } from '@trestleinc/replicate/client';
26
+ * import { open } from '@op-engineering/op-sqlite';
27
+ *
28
+ * const db = open({ name: 'myapp.db' });
29
+ * const persistence = await createReactNativeSqlitePersistence(db, 'myapp');
30
+ *
31
+ * // Use in collection options
32
+ * convexCollectionOptions<Task>({
33
+ * // ...
34
+ * persistence,
35
+ * });
36
+ * ```
37
+ */
38
+ export async function createReactNativeSqlitePersistence(
39
+ db: OPSQLiteDatabase,
40
+ dbName: string
41
+ ): Promise<Persistence> {
42
+ const adapter = new OPSqliteAdapter(db);
43
+ return sqlitePersistence({ adapter, dbName });
44
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Universal SQLite persistence using a user-provided adapter.
3
+ *
4
+ * The consuming app is responsible for:
5
+ * 1. Installing the SQLite package (sql.js, op-sqlite, etc.)
6
+ * 2. Creating and initializing the database
7
+ * 3. Wrapping it with the appropriate adapter
8
+ * 4. Passing the adapter to sqlitePersistence()
9
+ *
10
+ * @example Browser (sql.js)
11
+ * ```typescript
12
+ * import initSqlJs from 'sql.js';
13
+ * import { sqlitePersistence, SqlJsAdapter } from '@trestleinc/replicate/client';
14
+ *
15
+ * const SQL = await initSqlJs({ locateFile: file => `/sql-wasm/${file}` });
16
+ * const db = new SQL.Database();
17
+ * const adapter = new SqlJsAdapter(db, {
18
+ * onPersist: async (data) => {
19
+ * // Persist to OPFS, localStorage, etc.
20
+ * }
21
+ * });
22
+ * const persistence = await sqlitePersistence({ adapter });
23
+ * ```
24
+ *
25
+ * @example React Native (op-sqlite)
26
+ * ```typescript
27
+ * import { open } from '@op-engineering/op-sqlite';
28
+ * import { sqlitePersistence, OPSqliteAdapter } from '@trestleinc/replicate/client';
29
+ *
30
+ * const db = open({ name: 'myapp.db' });
31
+ * const adapter = new OPSqliteAdapter(db);
32
+ * const persistence = await sqlitePersistence({ adapter });
33
+ * ```
34
+ */
35
+ import type * as Y from 'yjs';
36
+ import { LeveldbPersistence } from 'y-leveldb';
37
+ import { SqliteLevel, type SqliteAdapter } from './sqlite-level.js';
38
+ import type { Persistence, PersistenceProvider, KeyValueStore } from './types.js';
39
+
40
+ /**
41
+ * SQLite-backed key-value store using sqlite-level.
42
+ */
43
+ class SqliteKeyValueStore implements KeyValueStore {
44
+ private db: SqliteLevel<string, string>;
45
+ private prefix = 'kv:';
46
+
47
+ constructor(db: SqliteLevel<string, string>) {
48
+ this.db = db;
49
+ }
50
+
51
+ async get<T>(key: string): Promise<T | undefined> {
52
+ try {
53
+ const value = await this.db.get(this.prefix + key);
54
+ if (value === undefined) {
55
+ return undefined;
56
+ }
57
+ return JSON.parse(value) as T;
58
+ } catch {
59
+ return undefined;
60
+ }
61
+ }
62
+
63
+ async set<T>(key: string, value: T): Promise<void> {
64
+ await this.db.put(this.prefix + key, JSON.stringify(value));
65
+ }
66
+
67
+ async del(key: string): Promise<void> {
68
+ await this.db.del(this.prefix + key);
69
+ }
70
+ }
71
+
72
+ /**
73
+ * SQLite persistence provider using y-leveldb.
74
+ */
75
+ class SqlitePersistenceProvider implements PersistenceProvider {
76
+ private persistence: LeveldbPersistence;
77
+ readonly whenSynced: Promise<void>;
78
+
79
+ constructor(collection: string, _ydoc: Y.Doc, leveldb: LeveldbPersistence) {
80
+ this.persistence = leveldb;
81
+ // Load existing document state
82
+ this.whenSynced = this.persistence.getYDoc(collection).then((storedDoc: Y.Doc) => {
83
+ // Apply stored state to provided ydoc
84
+ const state = storedDoc.store;
85
+ if (state) {
86
+ // The stored doc and ydoc are merged via y-leveldb's internal mechanisms
87
+ }
88
+ });
89
+ }
90
+
91
+ destroy(): void {
92
+ this.persistence.destroy();
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Options for SQLite persistence.
98
+ */
99
+ export interface SqlitePersistenceOptions {
100
+ /**
101
+ * Pre-created SQLite adapter (required).
102
+ * Use SqlJsAdapter for browser or OPSqliteAdapter for React Native.
103
+ */
104
+ adapter: SqliteAdapter;
105
+
106
+ /**
107
+ * Database name for internal y-leveldb usage.
108
+ * @default 'replicate'
109
+ */
110
+ dbName?: string;
111
+ }
112
+
113
+ /**
114
+ * Create a universal SQLite persistence factory.
115
+ *
116
+ * Requires a pre-created SqliteAdapter - the replicate package does not
117
+ * import any SQLite packages directly, making it environment-agnostic.
118
+ *
119
+ * @param options - Configuration with required adapter
120
+ *
121
+ * @example Browser (sql.js)
122
+ * ```typescript
123
+ * import initSqlJs from 'sql.js';
124
+ * import { sqlitePersistence, SqlJsAdapter } from '@trestleinc/replicate/client';
125
+ *
126
+ * const SQL = await initSqlJs();
127
+ * const db = new SQL.Database();
128
+ * const adapter = new SqlJsAdapter(db);
129
+ * const persistence = await sqlitePersistence({ adapter });
130
+ * ```
131
+ *
132
+ * @example React Native (op-sqlite)
133
+ * ```typescript
134
+ * import { open } from '@op-engineering/op-sqlite';
135
+ * import { sqlitePersistence, OPSqliteAdapter } from '@trestleinc/replicate/client';
136
+ *
137
+ * const db = open({ name: 'myapp.db' });
138
+ * const adapter = new OPSqliteAdapter(db);
139
+ * const persistence = await sqlitePersistence({ adapter });
140
+ * ```
141
+ */
142
+ export async function sqlitePersistence(options: SqlitePersistenceOptions): Promise<Persistence> {
143
+ const { adapter, dbName = 'replicate' } = options;
144
+
145
+ // Create sqlite-level database with the provided adapter
146
+ const db = new SqliteLevel<string, string>(dbName);
147
+ db.setAdapterFactory(() => Promise.resolve(adapter));
148
+ await db.open();
149
+
150
+ // Create y-leveldb persistence (reuses the sqlite-level database)
151
+ const leveldb = new LeveldbPersistence(dbName, { level: db as any });
152
+
153
+ // Create key-value store
154
+ const kv = new SqliteKeyValueStore(db);
155
+
156
+ return {
157
+ createDocPersistence: (collection: string, ydoc: Y.Doc) =>
158
+ new SqlitePersistenceProvider(collection, ydoc, leveldb),
159
+ kv,
160
+ };
161
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Persistence layer types for swappable storage backends.
3
+ *
4
+ * Supports IndexedDB (browser), SQLite (React Native), and in-memory (testing).
5
+ */
6
+ import type * as Y from 'yjs';
7
+
8
+ /**
9
+ * Provider that persists Y.Doc state to storage.
10
+ *
11
+ * This wraps providers like y-indexeddb or y-op-sqlite, normalizing their APIs.
12
+ */
13
+ export interface PersistenceProvider {
14
+ /** Promise that resolves when initial sync from storage completes */
15
+ readonly whenSynced: Promise<void>;
16
+
17
+ /** Clean up resources (stop observing, close connections) */
18
+ destroy(): void;
19
+ }
20
+
21
+ /**
22
+ * Factory that creates persistence providers.
23
+ *
24
+ * Each persistence implementation (IndexedDB, SQLite, memory) exports a
25
+ * factory function that returns this interface.
26
+ */
27
+ export interface Persistence {
28
+ /** Create a Y.Doc persistence provider for a collection */
29
+ createDocPersistence(collection: string, ydoc: Y.Doc): PersistenceProvider;
30
+
31
+ /** Key-value store for metadata (checkpoints, clientID) */
32
+ readonly kv: KeyValueStore;
33
+ }
34
+
35
+ /**
36
+ * Simple key-value storage interface.
37
+ *
38
+ * Used for storing metadata like checkpoints and Yjs client IDs.
39
+ */
40
+ export interface KeyValueStore {
41
+ /** Get a value by key */
42
+ get<T>(key: string): Promise<T | undefined>;
43
+
44
+ /** Set a value by key */
45
+ set<T>(key: string, value: T): Promise<void>;
46
+
47
+ /** Delete a value by key */
48
+ del(key: string): Promise<void>;
49
+ }