@trestleinc/replicate 1.1.1 → 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.
- package/README.md +395 -146
- package/dist/client/index.d.ts +311 -19
- package/dist/client/index.js +4027 -0
- package/dist/component/_generated/api.d.ts +13 -17
- package/dist/component/_generated/api.js +24 -4
- package/dist/component/_generated/component.d.ts +79 -77
- package/dist/component/_generated/component.js +1 -0
- package/dist/component/_generated/dataModel.d.ts +12 -15
- package/dist/component/_generated/dataModel.js +1 -0
- package/dist/component/_generated/server.d.ts +19 -22
- package/dist/component/_generated/server.js +65 -1
- package/dist/component/_virtual/rolldown_runtime.js +18 -0
- package/dist/component/convex.config.d.ts +6 -2
- package/dist/component/convex.config.js +7 -3
- package/dist/component/logger.d.ts +10 -6
- package/dist/component/logger.js +25 -28
- package/dist/component/public.d.ts +70 -61
- package/dist/component/public.js +311 -295
- package/dist/component/schema.d.ts +53 -45
- package/dist/component/schema.js +26 -32
- package/dist/component/shared/types.d.ts +9 -0
- package/dist/component/shared/types.js +15 -0
- package/dist/server/index.d.ts +134 -13
- package/dist/server/index.js +368 -0
- package/dist/shared/index.d.ts +27 -3
- package/dist/shared/index.js +1 -2
- package/package.json +34 -29
- package/src/client/collection.ts +339 -306
- package/src/client/errors.ts +9 -9
- package/src/client/index.ts +13 -32
- package/src/client/logger.ts +2 -2
- package/src/client/merge.ts +37 -34
- package/src/client/persistence/custom.ts +84 -0
- package/src/client/persistence/index.ts +9 -46
- package/src/client/persistence/indexeddb.ts +111 -84
- package/src/client/persistence/memory.ts +3 -3
- package/src/client/persistence/sqlite/browser.ts +168 -0
- package/src/client/persistence/sqlite/native.ts +29 -0
- package/src/client/persistence/sqlite/schema.ts +124 -0
- package/src/client/persistence/types.ts +32 -28
- package/src/client/prose-schema.ts +55 -0
- package/src/client/prose.ts +28 -25
- package/src/client/replicate.ts +5 -5
- package/src/client/services/cursor.ts +109 -0
- package/src/component/_generated/component.ts +31 -29
- package/src/component/convex.config.ts +2 -2
- package/src/component/logger.ts +7 -7
- package/src/component/public.ts +225 -237
- package/src/component/schema.ts +18 -15
- package/src/server/builder.ts +20 -7
- package/src/server/index.ts +3 -5
- package/src/server/schema.ts +5 -5
- package/src/server/storage.ts +113 -59
- package/src/shared/index.ts +5 -5
- package/src/shared/types.ts +51 -14
- package/dist/client/collection.d.ts +0 -96
- package/dist/client/errors.d.ts +0 -59
- package/dist/client/logger.d.ts +0 -2
- package/dist/client/merge.d.ts +0 -77
- package/dist/client/persistence/adapters/index.d.ts +0 -8
- package/dist/client/persistence/adapters/opsqlite.d.ts +0 -46
- package/dist/client/persistence/adapters/sqljs.d.ts +0 -83
- package/dist/client/persistence/index.d.ts +0 -49
- package/dist/client/persistence/indexeddb.d.ts +0 -17
- package/dist/client/persistence/memory.d.ts +0 -16
- package/dist/client/persistence/sqlite-browser.d.ts +0 -51
- package/dist/client/persistence/sqlite-level.d.ts +0 -63
- package/dist/client/persistence/sqlite-rn.d.ts +0 -36
- package/dist/client/persistence/sqlite.d.ts +0 -47
- package/dist/client/persistence/types.d.ts +0 -42
- package/dist/client/prose.d.ts +0 -56
- package/dist/client/replicate.d.ts +0 -40
- package/dist/client/services/checkpoint.d.ts +0 -18
- package/dist/client/services/reconciliation.d.ts +0 -24
- package/dist/index.js +0 -1618
- package/dist/server/builder.d.ts +0 -94
- package/dist/server/schema.d.ts +0 -27
- package/dist/server/storage.d.ts +0 -80
- package/dist/server.js +0 -281
- package/dist/shared/types.d.ts +0 -50
- package/dist/shared/types.js +0 -6
- package/dist/shared.js +0 -6
- package/src/client/persistence/adapters/index.ts +0 -8
- package/src/client/persistence/adapters/opsqlite.ts +0 -54
- package/src/client/persistence/adapters/sqljs.ts +0 -128
- package/src/client/persistence/sqlite-browser.ts +0 -107
- package/src/client/persistence/sqlite-level.ts +0 -407
- package/src/client/persistence/sqlite-rn.ts +0 -44
- package/src/client/persistence/sqlite.ts +0 -160
- package/src/client/services/checkpoint.ts +0 -86
- package/src/client/services/reconciliation.ts +0 -108
|
@@ -1,110 +1,137 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
this.whenSynced =
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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.
|
|
124
|
+
this.ydoc.off("update", this.updateHandler);
|
|
85
125
|
}
|
|
86
126
|
}
|
|
87
127
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
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
|
|
7
|
-
import type { Persistence, PersistenceProvider, KeyValueStore } from
|
|
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: (
|
|
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
|
-
*
|
|
4
|
+
* Low-level storage adapter for custom backends (Chrome extension, localStorage, cloud).
|
|
5
|
+
* For SQLite, use `persistence.sqlite()` directly.
|
|
10
6
|
*
|
|
11
|
-
*
|
|
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
|
-
*
|
|
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
|
}
|