@trestleinc/replicate 1.1.0 → 1.1.2-preview.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 (91) hide show
  1. package/README.md +446 -260
  2. package/dist/client/index.d.ts +311 -19
  3. package/dist/client/index.js +4027 -0
  4. package/dist/component/_generated/api.d.ts +13 -17
  5. package/dist/component/_generated/api.js +24 -4
  6. package/dist/component/_generated/component.d.ts +79 -77
  7. package/dist/component/_generated/component.js +1 -0
  8. package/dist/component/_generated/dataModel.d.ts +12 -15
  9. package/dist/component/_generated/dataModel.js +1 -0
  10. package/dist/component/_generated/server.d.ts +19 -22
  11. package/dist/component/_generated/server.js +65 -1
  12. package/dist/component/_virtual/rolldown_runtime.js +18 -0
  13. package/dist/component/convex.config.d.ts +6 -2
  14. package/dist/component/convex.config.js +7 -3
  15. package/dist/component/logger.d.ts +10 -6
  16. package/dist/component/logger.js +25 -28
  17. package/dist/component/public.d.ts +70 -61
  18. package/dist/component/public.js +311 -295
  19. package/dist/component/schema.d.ts +53 -45
  20. package/dist/component/schema.js +26 -32
  21. package/dist/component/shared/types.d.ts +9 -0
  22. package/dist/component/shared/types.js +15 -0
  23. package/dist/server/index.d.ts +134 -13
  24. package/dist/server/index.js +368 -0
  25. package/dist/shared/index.d.ts +27 -3
  26. package/dist/shared/index.js +1 -2
  27. package/package.json +34 -29
  28. package/src/client/collection.ts +339 -306
  29. package/src/client/errors.ts +9 -9
  30. package/src/client/index.ts +13 -32
  31. package/src/client/logger.ts +2 -2
  32. package/src/client/merge.ts +37 -34
  33. package/src/client/persistence/custom.ts +84 -0
  34. package/src/client/persistence/index.ts +9 -46
  35. package/src/client/persistence/indexeddb.ts +111 -84
  36. package/src/client/persistence/memory.ts +3 -3
  37. package/src/client/persistence/sqlite/browser.ts +168 -0
  38. package/src/client/persistence/sqlite/native.ts +29 -0
  39. package/src/client/persistence/sqlite/schema.ts +124 -0
  40. package/src/client/persistence/types.ts +32 -28
  41. package/src/client/prose-schema.ts +55 -0
  42. package/src/client/prose.ts +28 -25
  43. package/src/client/replicate.ts +5 -5
  44. package/src/client/services/cursor.ts +109 -0
  45. package/src/component/_generated/component.ts +31 -29
  46. package/src/component/convex.config.ts +2 -2
  47. package/src/component/logger.ts +7 -7
  48. package/src/component/public.ts +225 -237
  49. package/src/component/schema.ts +18 -15
  50. package/src/server/builder.ts +20 -7
  51. package/src/server/index.ts +3 -5
  52. package/src/server/schema.ts +5 -5
  53. package/src/server/storage.ts +113 -59
  54. package/src/shared/index.ts +5 -5
  55. package/src/shared/types.ts +51 -14
  56. package/dist/client/collection.d.ts +0 -96
  57. package/dist/client/errors.d.ts +0 -59
  58. package/dist/client/logger.d.ts +0 -2
  59. package/dist/client/merge.d.ts +0 -77
  60. package/dist/client/persistence/adapters/index.d.ts +0 -8
  61. package/dist/client/persistence/adapters/opsqlite.d.ts +0 -46
  62. package/dist/client/persistence/adapters/sqljs.d.ts +0 -83
  63. package/dist/client/persistence/index.d.ts +0 -49
  64. package/dist/client/persistence/indexeddb.d.ts +0 -17
  65. package/dist/client/persistence/memory.d.ts +0 -16
  66. package/dist/client/persistence/sqlite-browser.d.ts +0 -51
  67. package/dist/client/persistence/sqlite-level.d.ts +0 -63
  68. package/dist/client/persistence/sqlite-rn.d.ts +0 -36
  69. package/dist/client/persistence/sqlite.d.ts +0 -47
  70. package/dist/client/persistence/types.d.ts +0 -42
  71. package/dist/client/prose.d.ts +0 -56
  72. package/dist/client/replicate.d.ts +0 -40
  73. package/dist/client/services/checkpoint.d.ts +0 -18
  74. package/dist/client/services/reconciliation.d.ts +0 -24
  75. package/dist/index.js +0 -1620
  76. package/dist/server/builder.d.ts +0 -94
  77. package/dist/server/schema.d.ts +0 -27
  78. package/dist/server/storage.d.ts +0 -80
  79. package/dist/server.js +0 -281
  80. package/dist/shared/types.d.ts +0 -50
  81. package/dist/shared/types.js +0 -6
  82. package/dist/shared.js +0 -6
  83. package/src/client/persistence/adapters/index.ts +0 -8
  84. package/src/client/persistence/adapters/opsqlite.ts +0 -54
  85. package/src/client/persistence/adapters/sqljs.ts +0 -128
  86. package/src/client/persistence/sqlite-browser.ts +0 -107
  87. package/src/client/persistence/sqlite-level.ts +0 -407
  88. package/src/client/persistence/sqlite-rn.ts +0 -44
  89. package/src/client/persistence/sqlite.ts +0 -161
  90. package/src/client/services/checkpoint.ts +0 -86
  91. package/src/client/services/reconciliation.ts +0 -108
@@ -1,110 +1,137 @@
1
- /**
2
- * IndexedDB persistence implementation for browser environments.
3
- *
4
- * Uses y-indexeddb for Y.Doc persistence and browser-level for key-value storage.
5
- * browser-level is an abstract-level database backed by IndexedDB.
6
- */
7
- import type * as Y from 'yjs';
8
- import { IndexeddbPersistence } from 'y-indexeddb';
9
- import { BrowserLevel } from 'browser-level';
10
- import type { Persistence, PersistenceProvider, KeyValueStore } from './types.js';
11
-
12
- /**
13
- * browser-level backed key-value store.
14
- *
15
- * Uses the Level ecosystem for consistent API across browser and React Native.
16
- */
17
- class BrowserLevelKeyValueStore implements KeyValueStore {
18
- private db: BrowserLevel<string, string>;
19
-
20
- constructor(dbName: string) {
21
- this.db = new BrowserLevel(dbName);
22
- }
1
+ import * as Y from "yjs";
2
+ import type { Persistence, PersistenceProvider, KeyValueStore } from "./types.js";
3
+
4
+ const UPDATES_STORE = "updates";
5
+ const SNAPSHOTS_STORE = "snapshots";
6
+ const KV_STORE = "kv";
23
7
 
24
- async get<T>(key: string): Promise<T | undefined> {
25
- try {
26
- const value = await this.db.get(key);
27
- if (value === undefined) {
28
- return undefined;
8
+ function openDatabase(dbName: string): Promise<IDBDatabase> {
9
+ return new Promise((resolve, reject) => {
10
+ const request = indexedDB.open(`replicate-${dbName}`, 1);
11
+ request.onerror = () => reject(request.error ?? new Error("IndexedDB open failed"));
12
+ request.onsuccess = () => resolve(request.result);
13
+ request.onupgradeneeded = () => {
14
+ const db = request.result;
15
+ if (!db.objectStoreNames.contains(UPDATES_STORE)) {
16
+ db.createObjectStore(UPDATES_STORE, { autoIncrement: true });
29
17
  }
30
- return JSON.parse(value) as T;
31
- } catch (err: any) {
32
- // Level throws LEVEL_NOT_FOUND error for missing keys
33
- if (err.code === 'LEVEL_NOT_FOUND') {
34
- return undefined;
18
+ if (!db.objectStoreNames.contains(SNAPSHOTS_STORE)) {
19
+ db.createObjectStore(SNAPSHOTS_STORE);
35
20
  }
36
- throw err;
37
- }
38
- }
21
+ if (!db.objectStoreNames.contains(KV_STORE)) {
22
+ db.createObjectStore(KV_STORE);
23
+ }
24
+ };
25
+ });
26
+ }
27
+
28
+ class IDBKeyValueStore implements KeyValueStore {
29
+ constructor(private db: IDBDatabase) {}
39
30
 
40
- async set<T>(key: string, value: T): Promise<void> {
41
- await this.db.put(key, JSON.stringify(value));
31
+ get<T>(key: string): Promise<T | undefined> {
32
+ return new Promise((resolve, reject) => {
33
+ const tx = this.db.transaction(KV_STORE, "readonly");
34
+ const store = tx.objectStore(KV_STORE);
35
+ const request = store.get(key);
36
+ request.onerror = () => reject(request.error ?? new Error("IndexedDB get failed"));
37
+ request.onsuccess = () => resolve(request.result as T | undefined);
38
+ });
42
39
  }
43
40
 
44
- async del(key: string): Promise<void> {
45
- try {
46
- await this.db.del(key);
47
- } catch (err: any) {
48
- // Ignore not found errors on delete
49
- if (err.code !== 'LEVEL_NOT_FOUND') {
50
- throw err;
51
- }
52
- }
41
+ set<T>(key: string, value: T): Promise<void> {
42
+ return new Promise((resolve, reject) => {
43
+ const tx = this.db.transaction(KV_STORE, "readwrite");
44
+ const store = tx.objectStore(KV_STORE);
45
+ const request = store.put(value, key);
46
+ request.onerror = () => reject(request.error ?? new Error("IndexedDB set failed"));
47
+ request.onsuccess = () => resolve();
48
+ });
53
49
  }
54
50
 
55
- async close(): Promise<void> {
56
- await this.db.close();
51
+ del(key: string): Promise<void> {
52
+ return new Promise((resolve, reject) => {
53
+ const tx = this.db.transaction(KV_STORE, "readwrite");
54
+ const store = tx.objectStore(KV_STORE);
55
+ const request = store.delete(key);
56
+ request.onerror = () => reject(request.error ?? new Error("IndexedDB delete failed"));
57
+ request.onsuccess = () => resolve();
58
+ });
57
59
  }
58
60
  }
59
61
 
60
- /**
61
- * IndexedDB persistence provider wrapping y-indexeddb.
62
- */
63
- class IndexedDBPersistenceProvider implements PersistenceProvider {
64
- private persistence: IndexeddbPersistence;
62
+ class IDBPersistenceProvider implements PersistenceProvider {
63
+ private updateHandler: (update: Uint8Array, origin: unknown) => void;
65
64
  readonly whenSynced: Promise<void>;
66
65
 
67
- constructor(collection: string, ydoc: Y.Doc) {
68
- this.persistence = new IndexeddbPersistence(collection, ydoc);
69
-
70
- // Handle race: check synced state before attaching listener
71
- // If database is empty or fast, synced event may fire before we attach
72
- this.whenSynced = new Promise((resolve) => {
73
- if (this.persistence.synced) {
74
- // Already synced - resolve immediately
75
- resolve();
76
- } else {
77
- // Not yet synced - wait for event (use once to prevent listener accumulation)
78
- this.persistence.once('synced', () => resolve());
66
+ constructor(
67
+ private db: IDBDatabase,
68
+ private collection: string,
69
+ private ydoc: Y.Doc,
70
+ ) {
71
+ this.whenSynced = this.loadState();
72
+
73
+ this.updateHandler = (update: Uint8Array, origin: unknown) => {
74
+ if (origin !== "idb") {
75
+ void this.saveUpdate(update);
79
76
  }
77
+ };
78
+ this.ydoc.on("update", this.updateHandler);
79
+ }
80
+
81
+ private loadState(): Promise<void> {
82
+ return new Promise((resolve, reject) => {
83
+ const tx = this.db.transaction([SNAPSHOTS_STORE, UPDATES_STORE], "readonly");
84
+
85
+ const snapshotStore = tx.objectStore(SNAPSHOTS_STORE);
86
+ const snapshotRequest = snapshotStore.get(this.collection);
87
+
88
+ snapshotRequest.onsuccess = () => {
89
+ if (snapshotRequest.result) {
90
+ Y.applyUpdate(this.ydoc, snapshotRequest.result, "idb");
91
+ }
92
+
93
+ const updatesStore = tx.objectStore(UPDATES_STORE);
94
+ const updatesRequest = updatesStore.getAll();
95
+
96
+ updatesRequest.onsuccess = () => {
97
+ const updates = updatesRequest.result as Uint8Array[];
98
+ for (const update of updates) {
99
+ Y.applyUpdate(this.ydoc, update, "idb");
100
+ }
101
+ resolve();
102
+ };
103
+
104
+ updatesRequest.onerror = () =>
105
+ reject(updatesRequest.error ?? new Error("IndexedDB updates load failed"));
106
+ };
107
+
108
+ snapshotRequest.onerror = () =>
109
+ reject(snapshotRequest.error ?? new Error("IndexedDB snapshot load failed"));
110
+ });
111
+ }
112
+
113
+ private saveUpdate(update: Uint8Array): Promise<void> {
114
+ return new Promise((resolve, reject) => {
115
+ const tx = this.db.transaction(UPDATES_STORE, "readwrite");
116
+ const store = tx.objectStore(UPDATES_STORE);
117
+ const request = store.add(update);
118
+ request.onerror = () => reject(request.error ?? new Error("IndexedDB save update failed"));
119
+ request.onsuccess = () => resolve();
80
120
  });
81
121
  }
82
122
 
83
123
  destroy(): void {
84
- this.persistence.destroy();
124
+ this.ydoc.off("update", this.updateHandler);
85
125
  }
86
126
  }
87
127
 
88
- /**
89
- * Create an IndexedDB persistence factory.
90
- *
91
- * Uses y-indexeddb for Y.Doc persistence and browser-level for metadata storage.
92
- *
93
- * @param dbName - Name for the LevelDB database (default: 'replicate-kv')
94
- *
95
- * @example
96
- * ```typescript
97
- * convexCollectionOptions<Task>({
98
- * // ... other options
99
- * persistence: indexeddbPersistence(),
100
- * });
101
- * ```
102
- */
103
- export function indexeddbPersistence(dbName = 'replicate-kv'): Persistence {
104
- const kv = new BrowserLevelKeyValueStore(dbName);
128
+ export async function createIndexedDBPersistence(dbName: string): Promise<Persistence> {
129
+ const db = await openDatabase(dbName);
130
+ const kv = new IDBKeyValueStore(db);
131
+
105
132
  return {
106
133
  createDocPersistence: (collection: string, ydoc: Y.Doc) =>
107
- new IndexedDBPersistenceProvider(collection, ydoc),
134
+ new IDBPersistenceProvider(db, collection, ydoc),
108
135
  kv,
109
136
  };
110
137
  }
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * State is not persisted across sessions - useful for tests and development.
5
5
  */
6
- import type * as Y from 'yjs';
7
- import type { Persistence, PersistenceProvider, KeyValueStore } from './types.js';
6
+ import type * as Y from "yjs";
7
+ import type { Persistence, PersistenceProvider, KeyValueStore } from "./types.js";
8
8
 
9
9
  /**
10
10
  * In-memory key-value store.
@@ -55,7 +55,7 @@ class MemoryPersistenceProvider implements PersistenceProvider {
55
55
  export function memoryPersistence(): Persistence {
56
56
  const kv = new MemoryKeyValueStore();
57
57
  return {
58
- createDocPersistence: (_collection: string, _ydoc: Y.Doc) => new MemoryPersistenceProvider(),
58
+ createDocPersistence: (_: string, __: Y.Doc) => new MemoryPersistenceProvider(),
59
59
  kv,
60
60
  };
61
61
  }
@@ -0,0 +1,168 @@
1
+ import { initSchema, createPersistenceFromExecutor, type Executor } from "./schema.js";
2
+ import type { Persistence } from "../types.js";
3
+
4
+ interface SqlJsDatabase {
5
+ run(sql: string, params?: unknown): unknown;
6
+ prepare(sql: string): {
7
+ bind(params?: unknown): void;
8
+ step(): boolean;
9
+ getAsObject(): Record<string, unknown>;
10
+ free(): void;
11
+ };
12
+ export(): Uint8Array;
13
+ close(): void;
14
+ }
15
+
16
+ interface SqlJsStatic {
17
+ Database: new (data?: ArrayLike<number> | Buffer | null) => SqlJsDatabase;
18
+ }
19
+
20
+ function hasOPFS(): boolean {
21
+ return typeof navigator !== "undefined"
22
+ && "storage" in navigator
23
+ && "getDirectory" in navigator.storage;
24
+ }
25
+
26
+ async function loadFromOPFS(dbName: string): Promise<Uint8Array | null> {
27
+ try {
28
+ const root = await navigator.storage.getDirectory();
29
+ const handle = await root.getFileHandle(`${dbName}.sqlite`);
30
+ const file = await handle.getFile();
31
+ return new Uint8Array(await file.arrayBuffer());
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ async function saveToOPFS(dbName: string, data: Uint8Array): Promise<void> {
39
+ const root = await navigator.storage.getDirectory();
40
+ const handle = await root.getFileHandle(`${dbName}.sqlite`, { create: true });
41
+ const writable = await handle.createWritable();
42
+ await writable.write(new Uint8Array(data));
43
+ await writable.close();
44
+ }
45
+
46
+ const IDB_STORE = "sqlite-db";
47
+
48
+ function openIDB(dbName: string): Promise<IDBDatabase> {
49
+ return new Promise((resolve, reject) => {
50
+ const request = indexedDB.open(`replicate-sqlite-${dbName}`, 1);
51
+ request.onerror = () => reject(request.error ?? new Error("IndexedDB open failed"));
52
+ request.onsuccess = () => resolve(request.result);
53
+ request.onupgradeneeded = () => {
54
+ request.result.createObjectStore(IDB_STORE);
55
+ };
56
+ });
57
+ }
58
+
59
+ async function loadFromIDB(dbName: string): Promise<Uint8Array | null> {
60
+ try {
61
+ const db = await openIDB(dbName);
62
+ return new Promise((resolve) => {
63
+ const tx = db.transaction(IDB_STORE, "readonly");
64
+ const request = tx.objectStore(IDB_STORE).get("data");
65
+ request.onsuccess = () => {
66
+ db.close();
67
+ resolve(request.result ?? null);
68
+ };
69
+ request.onerror = () => {
70
+ db.close();
71
+ resolve(null);
72
+ };
73
+ });
74
+ }
75
+ catch {
76
+ return null;
77
+ }
78
+ }
79
+
80
+ async function saveToIDB(dbName: string, data: Uint8Array): Promise<void> {
81
+ const db = await openIDB(dbName);
82
+ return new Promise((resolve, reject) => {
83
+ const tx = db.transaction(IDB_STORE, "readwrite");
84
+ const request = tx.objectStore(IDB_STORE).put(data, "data");
85
+ request.onsuccess = () => {
86
+ db.close();
87
+ resolve();
88
+ };
89
+ request.onerror = () => {
90
+ db.close();
91
+ reject(request.error ?? new Error("IndexedDB put failed"));
92
+ };
93
+ });
94
+ }
95
+
96
+ interface StorageBackend {
97
+ load(): Promise<Uint8Array | null>;
98
+ save(data: Uint8Array): Promise<void>;
99
+ }
100
+
101
+ function createStorageBackend(dbName: string): StorageBackend {
102
+ if (hasOPFS()) {
103
+ return {
104
+ load: () => loadFromOPFS(dbName),
105
+ save: data => saveToOPFS(dbName, data),
106
+ };
107
+ }
108
+ return {
109
+ load: () => loadFromIDB(dbName),
110
+ save: data => saveToIDB(dbName, data),
111
+ };
112
+ }
113
+
114
+ class SqlJsExecutor implements Executor {
115
+ constructor(
116
+ private db: SqlJsDatabase,
117
+ private storage: StorageBackend,
118
+ ) {}
119
+
120
+ async execute(sql: string, params?: unknown[]): Promise<{ rows: Record<string, unknown>[] }> {
121
+ const rows: Record<string, unknown>[] = [];
122
+ const trimmed = sql.trim().toUpperCase();
123
+
124
+ const isWrite = trimmed.startsWith("CREATE")
125
+ || trimmed.startsWith("INSERT")
126
+ || trimmed.startsWith("UPDATE")
127
+ || trimmed.startsWith("DELETE")
128
+ || trimmed.startsWith("BEGIN")
129
+ || trimmed.startsWith("COMMIT")
130
+ || trimmed.startsWith("ROLLBACK");
131
+
132
+ if (isWrite) {
133
+ this.db.run(sql, params);
134
+ await this.storage.save(this.db.export());
135
+ return { rows };
136
+ }
137
+
138
+ const stmt = this.db.prepare(sql);
139
+ if (params?.length) {
140
+ stmt.bind(params);
141
+ }
142
+
143
+ while (stmt.step()) {
144
+ rows.push(stmt.getAsObject());
145
+ }
146
+ stmt.free();
147
+
148
+ return { rows };
149
+ }
150
+
151
+ close(): void {
152
+ this.db.close();
153
+ }
154
+ }
155
+
156
+ export async function createBrowserSqlitePersistence(
157
+ SQL: SqlJsStatic,
158
+ dbName: string,
159
+ ): Promise<Persistence> {
160
+ const storage = createStorageBackend(dbName);
161
+ const existingData = await storage.load();
162
+ const db = existingData ? new SQL.Database(existingData) : new SQL.Database();
163
+ const executor = new SqlJsExecutor(db, storage);
164
+
165
+ await initSchema(executor);
166
+
167
+ return createPersistenceFromExecutor(executor);
168
+ }
@@ -0,0 +1,29 @@
1
+ import { initSchema, createPersistenceFromExecutor, type Executor } from "./schema.js";
2
+ import type { Persistence } from "../types.js";
3
+
4
+ interface OPSQLiteDatabase {
5
+ execute(sql: string, params?: unknown[]): Promise<{ rows: Record<string, unknown>[] }>;
6
+ close(): void;
7
+ }
8
+
9
+ class OPSqliteExecutor implements Executor {
10
+ constructor(private db: OPSQLiteDatabase) {}
11
+
12
+ async execute(sql: string, params?: unknown[]): Promise<{ rows: Record<string, unknown>[] }> {
13
+ const result = await this.db.execute(sql, params);
14
+ return { rows: result.rows || [] };
15
+ }
16
+
17
+ close(): void {
18
+ this.db.close();
19
+ }
20
+ }
21
+
22
+ export async function createNativeSqlitePersistence(
23
+ db: OPSQLiteDatabase,
24
+ _dbName: string,
25
+ ): Promise<Persistence> {
26
+ const executor = new OPSqliteExecutor(db);
27
+ await initSchema(executor);
28
+ return createPersistenceFromExecutor(executor);
29
+ }
@@ -0,0 +1,124 @@
1
+ import * as Y from "yjs";
2
+ import type { Persistence, PersistenceProvider, KeyValueStore } from "../types.js";
3
+
4
+ export interface Executor {
5
+ execute(sql: string, params?: unknown[]): Promise<{ rows: Record<string, unknown>[] }>;
6
+ close(): void;
7
+ }
8
+
9
+ export async function initSchema(executor: Executor): Promise<void> {
10
+ await executor.execute(`
11
+ CREATE TABLE IF NOT EXISTS snapshots (
12
+ collection TEXT PRIMARY KEY,
13
+ data BLOB NOT NULL,
14
+ state_vector BLOB,
15
+ seq INTEGER DEFAULT 0
16
+ )
17
+ `);
18
+
19
+ await executor.execute(`
20
+ CREATE TABLE IF NOT EXISTS updates (
21
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
22
+ collection TEXT NOT NULL,
23
+ data BLOB NOT NULL
24
+ )
25
+ `);
26
+
27
+ await executor.execute(`
28
+ CREATE INDEX IF NOT EXISTS updates_collection_idx ON updates (collection)
29
+ `);
30
+
31
+ await executor.execute(`
32
+ CREATE TABLE IF NOT EXISTS kv (
33
+ key TEXT PRIMARY KEY,
34
+ value TEXT NOT NULL
35
+ )
36
+ `);
37
+ }
38
+
39
+ class SqliteKeyValueStore implements KeyValueStore {
40
+ constructor(private executor: Executor) {}
41
+
42
+ async get<T>(key: string): Promise<T | undefined> {
43
+ const result = await this.executor.execute(
44
+ "SELECT value FROM kv WHERE key = ?",
45
+ [key],
46
+ );
47
+ if (result.rows.length === 0) return undefined;
48
+ return JSON.parse(result.rows[0].value as string) as T;
49
+ }
50
+
51
+ async set<T>(key: string, value: T): Promise<void> {
52
+ await this.executor.execute(
53
+ "INSERT OR REPLACE INTO kv (key, value) VALUES (?, ?)",
54
+ [key, JSON.stringify(value)],
55
+ );
56
+ }
57
+
58
+ async del(key: string): Promise<void> {
59
+ await this.executor.execute("DELETE FROM kv WHERE key = ?", [key]);
60
+ }
61
+ }
62
+
63
+ class SqlitePersistenceProvider implements PersistenceProvider {
64
+ private updateHandler: (update: Uint8Array, origin: unknown) => void;
65
+ readonly whenSynced: Promise<void>;
66
+
67
+ constructor(
68
+ private executor: Executor,
69
+ private collection: string,
70
+ private ydoc: Y.Doc,
71
+ ) {
72
+ this.whenSynced = this.loadState();
73
+
74
+ this.updateHandler = (update: Uint8Array, origin: unknown) => {
75
+ if (origin !== "sqlite") {
76
+ void this.saveUpdate(update);
77
+ }
78
+ };
79
+ this.ydoc.on("update", this.updateHandler);
80
+ }
81
+
82
+ private async loadState(): Promise<void> {
83
+ const snapshotResult = await this.executor.execute(
84
+ "SELECT data FROM snapshots WHERE collection = ?",
85
+ [this.collection],
86
+ );
87
+
88
+ if (snapshotResult.rows.length > 0) {
89
+ const raw = snapshotResult.rows[0].data;
90
+ const snapshotData = raw instanceof Uint8Array ? raw : new Uint8Array(raw as ArrayBuffer);
91
+ Y.applyUpdate(this.ydoc, snapshotData, "sqlite");
92
+ }
93
+
94
+ const updatesResult = await this.executor.execute(
95
+ "SELECT data FROM updates WHERE collection = ? ORDER BY id ASC",
96
+ [this.collection],
97
+ );
98
+
99
+ for (const row of updatesResult.rows) {
100
+ const raw = row.data;
101
+ const updateData = raw instanceof Uint8Array ? raw : new Uint8Array(raw as ArrayBuffer);
102
+ Y.applyUpdate(this.ydoc, updateData, "sqlite");
103
+ }
104
+ }
105
+
106
+ private async saveUpdate(update: Uint8Array): Promise<void> {
107
+ await this.executor.execute(
108
+ "INSERT INTO updates (collection, data) VALUES (?, ?)",
109
+ [this.collection, update],
110
+ );
111
+ }
112
+
113
+ destroy(): void {
114
+ this.ydoc.off("update", this.updateHandler);
115
+ }
116
+ }
117
+
118
+ export function createPersistenceFromExecutor(executor: Executor): Persistence {
119
+ return {
120
+ createDocPersistence: (collection: string, ydoc: Y.Doc) =>
121
+ new SqlitePersistenceProvider(executor, collection, ydoc),
122
+ kv: new SqliteKeyValueStore(executor),
123
+ };
124
+ }
@@ -1,49 +1,53 @@
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';
1
+ import type * as Y from "yjs";
7
2
 
8
3
  /**
9
- * Provider that persists Y.Doc state to storage.
4
+ * Low-level storage adapter for custom backends (Chrome extension, localStorage, cloud).
5
+ * For SQLite, use `persistence.sqlite()` directly.
10
6
  *
11
- * This wraps providers like y-indexeddb or y-op-sqlite, normalizing their APIs.
7
+ * @example
8
+ * ```typescript
9
+ * class ChromeStorageAdapter implements StorageAdapter {
10
+ * async get(key: string) {
11
+ * const result = await chrome.storage.local.get(key);
12
+ * return result[key] ? new Uint8Array(result[key]) : undefined;
13
+ * }
14
+ * async set(key: string, value: Uint8Array) {
15
+ * await chrome.storage.local.set({ [key]: Array.from(value) });
16
+ * }
17
+ * async delete(key: string) {
18
+ * await chrome.storage.local.remove(key);
19
+ * }
20
+ * async keys(prefix: string) {
21
+ * const all = await chrome.storage.local.get(null);
22
+ * return Object.keys(all).filter(k => k.startsWith(prefix));
23
+ * }
24
+ * }
25
+ * ```
12
26
  */
27
+ export interface StorageAdapter {
28
+ get(key: string): Promise<Uint8Array | undefined>;
29
+ set(key: string, value: Uint8Array): Promise<void>;
30
+ delete(key: string): Promise<void>;
31
+ keys(prefix: string): Promise<string[]>;
32
+ close?(): void;
33
+ }
34
+
13
35
  export interface PersistenceProvider {
14
- /** Promise that resolves when initial sync from storage completes */
15
36
  readonly whenSynced: Promise<void>;
16
-
17
- /** Clean up resources (stop observing, close connections) */
18
37
  destroy(): void;
19
38
  }
20
39
 
21
40
  /**
22
- * Factory that creates persistence providers.
23
- *
24
- * Each persistence implementation (IndexedDB, SQLite, memory) exports a
25
- * factory function that returns this interface.
41
+ * High-level persistence interface for collections.
42
+ * Create via `persistence.sqlite()`, `persistence.memory()`, or `persistence.custom()`.
26
43
  */
27
44
  export interface Persistence {
28
- /** Create a Y.Doc persistence provider for a collection */
29
45
  createDocPersistence(collection: string, ydoc: Y.Doc): PersistenceProvider;
30
-
31
- /** Key-value store for metadata (checkpoints, clientID) */
32
46
  readonly kv: KeyValueStore;
33
47
  }
34
48
 
35
- /**
36
- * Simple key-value storage interface.
37
- *
38
- * Used for storing metadata like checkpoints and Yjs client IDs.
39
- */
40
49
  export interface KeyValueStore {
41
- /** Get a value by key */
42
50
  get<T>(key: string): Promise<T | undefined>;
43
-
44
- /** Set a value by key */
45
51
  set<T>(key: string, value: T): Promise<void>;
46
-
47
- /** Delete a value by key */
48
52
  del(key: string): Promise<void>;
49
53
  }