@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.
- package/README.md +356 -420
- package/dist/client/collection.d.ts +78 -76
- package/dist/client/errors.d.ts +59 -0
- package/dist/client/index.d.ts +22 -18
- package/dist/client/logger.d.ts +0 -1
- package/dist/client/merge.d.ts +77 -0
- package/dist/client/persistence/adapters/index.d.ts +8 -0
- package/dist/client/persistence/adapters/opsqlite.d.ts +46 -0
- package/dist/client/persistence/adapters/sqljs.d.ts +83 -0
- package/dist/client/persistence/index.d.ts +49 -0
- package/dist/client/persistence/indexeddb.d.ts +17 -0
- package/dist/client/persistence/memory.d.ts +16 -0
- package/dist/client/persistence/sqlite-browser.d.ts +51 -0
- package/dist/client/persistence/sqlite-level.d.ts +63 -0
- package/dist/client/persistence/sqlite-rn.d.ts +36 -0
- package/dist/client/persistence/sqlite.d.ts +47 -0
- package/dist/client/persistence/types.d.ts +42 -0
- package/dist/client/prose.d.ts +56 -0
- package/dist/client/replicate.d.ts +40 -0
- package/dist/client/services/checkpoint.d.ts +18 -0
- package/dist/client/services/reconciliation.d.ts +24 -0
- package/dist/component/_generated/api.d.ts +35 -0
- package/dist/component/_generated/api.js +3 -3
- package/dist/component/_generated/component.d.ts +89 -0
- package/dist/component/_generated/component.js +0 -0
- package/dist/component/_generated/dataModel.d.ts +45 -0
- package/dist/component/_generated/dataModel.js +0 -0
- package/{src → dist}/component/_generated/server.d.ts +9 -38
- package/dist/component/convex.config.d.ts +2 -2
- package/dist/component/convex.config.js +2 -1
- package/dist/component/logger.d.ts +8 -0
- package/dist/component/logger.js +30 -0
- package/dist/component/public.d.ts +36 -61
- package/dist/component/public.js +232 -58
- package/dist/component/schema.d.ts +32 -8
- package/dist/component/schema.js +19 -6
- package/dist/index.js +1553 -308
- package/dist/server/builder.d.ts +94 -0
- package/dist/server/index.d.ts +14 -17
- package/dist/server/schema.d.ts +17 -63
- package/dist/server/storage.d.ts +80 -0
- package/dist/server.js +268 -83
- package/dist/shared/index.d.ts +5 -0
- package/dist/shared/index.js +2 -0
- package/dist/shared/types.d.ts +50 -0
- package/dist/shared/types.js +6 -0
- package/dist/shared.js +6 -0
- package/package.json +59 -49
- package/src/client/collection.ts +877 -450
- package/src/client/errors.ts +45 -0
- package/src/client/index.ts +52 -26
- package/src/client/logger.ts +2 -28
- package/src/client/merge.ts +374 -0
- package/src/client/persistence/adapters/index.ts +8 -0
- package/src/client/persistence/adapters/opsqlite.ts +54 -0
- package/src/client/persistence/adapters/sqljs.ts +128 -0
- package/src/client/persistence/index.ts +54 -0
- package/src/client/persistence/indexeddb.ts +110 -0
- package/src/client/persistence/memory.ts +61 -0
- package/src/client/persistence/sqlite-browser.ts +107 -0
- package/src/client/persistence/sqlite-level.ts +407 -0
- package/src/client/persistence/sqlite-rn.ts +44 -0
- package/src/client/persistence/sqlite.ts +161 -0
- package/src/client/persistence/types.ts +49 -0
- package/src/client/prose.ts +369 -0
- package/src/client/replicate.ts +80 -0
- package/src/client/services/checkpoint.ts +86 -0
- package/src/client/services/reconciliation.ts +108 -0
- package/src/component/_generated/api.ts +52 -0
- package/src/component/_generated/component.ts +103 -0
- package/src/component/_generated/{dataModel.d.ts → dataModel.ts} +1 -1
- package/src/component/_generated/server.ts +161 -0
- package/src/component/convex.config.ts +3 -1
- package/src/component/logger.ts +36 -0
- package/src/component/public.ts +364 -111
- package/src/component/schema.ts +18 -5
- package/src/env.d.ts +31 -0
- package/src/server/builder.ts +85 -0
- package/src/server/index.ts +9 -24
- package/src/server/schema.ts +20 -76
- package/src/server/storage.ts +313 -0
- package/src/shared/index.ts +5 -0
- package/src/shared/types.ts +52 -0
- package/LICENSE.package +0 -201
- package/dist/client/storage.d.ts +0 -143
- package/dist/server/replication.d.ts +0 -122
- package/dist/server/ssr.d.ts +0 -79
- package/dist/ssr.js +0 -19
- package/src/client/storage.ts +0 -206
- package/src/component/_generated/api.d.ts +0 -95
- package/src/component/_generated/api.js +0 -23
- package/src/component/_generated/server.js +0 -90
- package/src/server/replication.ts +0 -244
- package/src/server/ssr.ts +0 -106
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sql.js adapter wrapper for browser SQLite.
|
|
3
|
+
*
|
|
4
|
+
* The consuming app imports sql.js and creates the database,
|
|
5
|
+
* then passes it to this wrapper.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import initSqlJs from 'sql.js';
|
|
10
|
+
* import { SqlJsAdapter } from '@trestleinc/replicate/client';
|
|
11
|
+
*
|
|
12
|
+
* const SQL = await initSqlJs({ locateFile: f => `/wasm/${f}` });
|
|
13
|
+
* const db = new SQL.Database();
|
|
14
|
+
* const adapter = new SqlJsAdapter(db, {
|
|
15
|
+
* onPersist: async (data) => {
|
|
16
|
+
* // Persist to OPFS, localStorage, etc.
|
|
17
|
+
* }
|
|
18
|
+
* });
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
import type { SqliteAdapter } from '../sqlite-level.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Interface for sql.js Database.
|
|
25
|
+
* Consumer must install sql.js and pass a Database instance.
|
|
26
|
+
*/
|
|
27
|
+
export interface SqlJsDatabase {
|
|
28
|
+
run(sql: string, params?: unknown[]): void;
|
|
29
|
+
prepare(sql: string): {
|
|
30
|
+
bind(params?: unknown[]): void;
|
|
31
|
+
step(): boolean;
|
|
32
|
+
getAsObject(): Record<string, unknown>;
|
|
33
|
+
free(): void;
|
|
34
|
+
};
|
|
35
|
+
export(): Uint8Array;
|
|
36
|
+
close(): void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Options for the SqlJsAdapter.
|
|
41
|
+
*/
|
|
42
|
+
export interface SqlJsAdapterOptions {
|
|
43
|
+
/**
|
|
44
|
+
* Callback to persist database after write operations.
|
|
45
|
+
* Called with the exported database bytes.
|
|
46
|
+
*
|
|
47
|
+
* @example OPFS persistence
|
|
48
|
+
* ```typescript
|
|
49
|
+
* onPersist: async (data) => {
|
|
50
|
+
* const root = await navigator.storage.getDirectory();
|
|
51
|
+
* const handle = await root.getFileHandle('myapp.sqlite', { create: true });
|
|
52
|
+
* const writable = await handle.createWritable();
|
|
53
|
+
* await writable.write(data.buffer);
|
|
54
|
+
* await writable.close();
|
|
55
|
+
* }
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
onPersist?: (data: Uint8Array) => Promise<void>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Wraps a sql.js Database as a SqliteAdapter.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```typescript
|
|
66
|
+
* import initSqlJs from 'sql.js';
|
|
67
|
+
* import { SqlJsAdapter } from '@trestleinc/replicate/client';
|
|
68
|
+
*
|
|
69
|
+
* const SQL = await initSqlJs();
|
|
70
|
+
* const db = new SQL.Database();
|
|
71
|
+
* const adapter = new SqlJsAdapter(db);
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export class SqlJsAdapter implements SqliteAdapter {
|
|
75
|
+
private db: SqlJsDatabase;
|
|
76
|
+
private onPersist?: (data: Uint8Array) => Promise<void>;
|
|
77
|
+
|
|
78
|
+
constructor(db: SqlJsDatabase, options: SqlJsAdapterOptions = {}) {
|
|
79
|
+
this.db = db;
|
|
80
|
+
this.onPersist = options.onPersist;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async execute(sql: string, params?: unknown[]): Promise<{ rows: Record<string, unknown>[] }> {
|
|
84
|
+
const rows: Record<string, unknown>[] = [];
|
|
85
|
+
|
|
86
|
+
// Handle statements that don't return data
|
|
87
|
+
if (
|
|
88
|
+
sql.trim().toUpperCase().startsWith('CREATE') ||
|
|
89
|
+
sql.trim().toUpperCase().startsWith('INSERT') ||
|
|
90
|
+
sql.trim().toUpperCase().startsWith('UPDATE') ||
|
|
91
|
+
sql.trim().toUpperCase().startsWith('DELETE') ||
|
|
92
|
+
sql.trim().toUpperCase().startsWith('BEGIN') ||
|
|
93
|
+
sql.trim().toUpperCase().startsWith('COMMIT') ||
|
|
94
|
+
sql.trim().toUpperCase().startsWith('ROLLBACK')
|
|
95
|
+
) {
|
|
96
|
+
this.db.run(sql, params);
|
|
97
|
+
await this.persist();
|
|
98
|
+
return { rows };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Handle SELECT statements
|
|
102
|
+
const stmt = this.db.prepare(sql);
|
|
103
|
+
if (params && params.length > 0) {
|
|
104
|
+
stmt.bind(params);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
while (stmt.step()) {
|
|
108
|
+
rows.push(stmt.getAsObject());
|
|
109
|
+
}
|
|
110
|
+
stmt.free();
|
|
111
|
+
|
|
112
|
+
return { rows };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
close(): void {
|
|
116
|
+
this.db.close();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Persist database using the onPersist callback if provided.
|
|
121
|
+
*/
|
|
122
|
+
private async persist(): Promise<void> {
|
|
123
|
+
if (this.onPersist) {
|
|
124
|
+
const data = this.db.export();
|
|
125
|
+
await this.onPersist(new Uint8Array(data));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistence layer exports.
|
|
3
|
+
*
|
|
4
|
+
* Provides swappable storage backends for Y.Doc and key-value data.
|
|
5
|
+
*/
|
|
6
|
+
export type { Persistence, PersistenceProvider, KeyValueStore } from './types.js';
|
|
7
|
+
export type { SqlitePersistenceOptions } from './sqlite.js';
|
|
8
|
+
export type { SqlJsStatic } from './sqlite-browser.js';
|
|
9
|
+
export type { SqliteAdapter } from './sqlite-level.js';
|
|
10
|
+
|
|
11
|
+
// Internal imports for the persistence object
|
|
12
|
+
import { indexeddbPersistence } from './indexeddb.js';
|
|
13
|
+
import { memoryPersistence } from './memory.js';
|
|
14
|
+
import { sqlitePersistence } from './sqlite.js';
|
|
15
|
+
import { createBrowserSqlitePersistence } from './sqlite-browser.js';
|
|
16
|
+
import { createReactNativeSqlitePersistence } from './sqlite-rn.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Persistence API - nested object pattern for ergonomic access.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* import { persistence } from '@trestleinc/replicate/client';
|
|
24
|
+
*
|
|
25
|
+
* // Browser SQLite (recommended for web)
|
|
26
|
+
* const p = await persistence.sqlite.browser(SQL, 'myapp');
|
|
27
|
+
*
|
|
28
|
+
* // React Native SQLite
|
|
29
|
+
* const p = await persistence.sqlite.native(db, 'myapp');
|
|
30
|
+
*
|
|
31
|
+
* // IndexedDB fallback
|
|
32
|
+
* const p = persistence.indexeddb('myapp');
|
|
33
|
+
*
|
|
34
|
+
* // In-memory (testing)
|
|
35
|
+
* const p = persistence.memory();
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export const persistence = {
|
|
39
|
+
/** IndexedDB-backed persistence (browser) */
|
|
40
|
+
indexeddb: indexeddbPersistence,
|
|
41
|
+
|
|
42
|
+
/** In-memory persistence (testing/ephemeral) */
|
|
43
|
+
memory: memoryPersistence,
|
|
44
|
+
|
|
45
|
+
/** SQLite persistence variants */
|
|
46
|
+
sqlite: {
|
|
47
|
+
/** Browser SQLite with OPFS (sql.js) */
|
|
48
|
+
browser: createBrowserSqlitePersistence,
|
|
49
|
+
/** React Native SQLite (op-sqlite) */
|
|
50
|
+
native: createReactNativeSqlitePersistence,
|
|
51
|
+
/** Custom SQLite adapter */
|
|
52
|
+
create: sqlitePersistence,
|
|
53
|
+
},
|
|
54
|
+
} as const;
|
|
@@ -0,0 +1,110 @@
|
|
|
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
|
+
}
|
|
23
|
+
|
|
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;
|
|
29
|
+
}
|
|
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;
|
|
35
|
+
}
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async set<T>(key: string, value: T): Promise<void> {
|
|
41
|
+
await this.db.put(key, JSON.stringify(value));
|
|
42
|
+
}
|
|
43
|
+
|
|
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
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async close(): Promise<void> {
|
|
56
|
+
await this.db.close();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* IndexedDB persistence provider wrapping y-indexeddb.
|
|
62
|
+
*/
|
|
63
|
+
class IndexedDBPersistenceProvider implements PersistenceProvider {
|
|
64
|
+
private persistence: IndexeddbPersistence;
|
|
65
|
+
readonly whenSynced: Promise<void>;
|
|
66
|
+
|
|
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());
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
destroy(): void {
|
|
84
|
+
this.persistence.destroy();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
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);
|
|
105
|
+
return {
|
|
106
|
+
createDocPersistence: (collection: string, ydoc: Y.Doc) =>
|
|
107
|
+
new IndexedDBPersistenceProvider(collection, ydoc),
|
|
108
|
+
kv,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory persistence implementation for testing.
|
|
3
|
+
*
|
|
4
|
+
* State is not persisted across sessions - useful for tests and development.
|
|
5
|
+
*/
|
|
6
|
+
import type * as Y from 'yjs';
|
|
7
|
+
import type { Persistence, PersistenceProvider, KeyValueStore } from './types.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* In-memory key-value store.
|
|
11
|
+
*/
|
|
12
|
+
class MemoryKeyValueStore implements KeyValueStore {
|
|
13
|
+
private store = new Map<string, unknown>();
|
|
14
|
+
|
|
15
|
+
async get<T>(key: string): Promise<T | undefined> {
|
|
16
|
+
return this.store.get(key) as T | undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async set<T>(key: string, value: T): Promise<void> {
|
|
20
|
+
this.store.set(key, value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async del(key: string): Promise<void> {
|
|
24
|
+
this.store.delete(key);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* No-op persistence provider for in-memory usage.
|
|
30
|
+
*
|
|
31
|
+
* The Y.Doc is kept in memory without persistence.
|
|
32
|
+
*/
|
|
33
|
+
class MemoryPersistenceProvider implements PersistenceProvider {
|
|
34
|
+
readonly whenSynced = Promise.resolve();
|
|
35
|
+
|
|
36
|
+
destroy(): void {
|
|
37
|
+
// No resources to clean up
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Create an in-memory persistence factory.
|
|
43
|
+
*
|
|
44
|
+
* Useful for testing where you don't want IndexedDB side effects.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```typescript
|
|
48
|
+
* // In tests
|
|
49
|
+
* convexCollectionOptions<Task>({
|
|
50
|
+
* // ... other options
|
|
51
|
+
* persistence: memoryPersistence(),
|
|
52
|
+
* });
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export function memoryPersistence(): Persistence {
|
|
56
|
+
const kv = new MemoryKeyValueStore();
|
|
57
|
+
return {
|
|
58
|
+
createDocPersistence: (_collection: string, _ydoc: Y.Doc) => new MemoryPersistenceProvider(),
|
|
59
|
+
kv,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser SQLite persistence helper using sql.js and OPFS.
|
|
3
|
+
*
|
|
4
|
+
* Handles all the boilerplate for browser SQLite:
|
|
5
|
+
* - Loading existing database from OPFS
|
|
6
|
+
* - Persisting to OPFS on every write
|
|
7
|
+
* - Creating the SqlJsAdapter
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { createBrowserSqlitePersistence } from '@trestleinc/replicate/client';
|
|
12
|
+
* import initSqlJs from 'sql.js';
|
|
13
|
+
*
|
|
14
|
+
* const SQL = await initSqlJs({ locateFile: f => `https://sql.js.org/dist/${f}` });
|
|
15
|
+
* const persistence = await createBrowserSqlitePersistence(SQL, 'myapp');
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
import { SqlJsAdapter, type SqlJsDatabase } from './adapters/sqljs.js';
|
|
19
|
+
import { sqlitePersistence } from './sqlite.js';
|
|
20
|
+
import type { Persistence } from './types.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Interface for the sql.js module (the result of initSqlJs).
|
|
24
|
+
*/
|
|
25
|
+
export interface SqlJsStatic {
|
|
26
|
+
Database: new (data?: ArrayLike<number>) => SqlJsDatabase;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Load existing database from OPFS if available.
|
|
31
|
+
*/
|
|
32
|
+
async function loadFromOPFS(dbName: string): Promise<Uint8Array | null> {
|
|
33
|
+
try {
|
|
34
|
+
const root = await navigator.storage.getDirectory();
|
|
35
|
+
const handle = await root.getFileHandle(`${dbName}.sqlite`);
|
|
36
|
+
const file = await handle.getFile();
|
|
37
|
+
const buffer = await file.arrayBuffer();
|
|
38
|
+
return new Uint8Array(buffer);
|
|
39
|
+
} catch {
|
|
40
|
+
// File doesn't exist yet
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Save database to OPFS for durable storage.
|
|
47
|
+
*/
|
|
48
|
+
function createOPFSSaver(dbName: string): (data: Uint8Array) => Promise<void> {
|
|
49
|
+
return async (data: Uint8Array): Promise<void> => {
|
|
50
|
+
try {
|
|
51
|
+
const root = await navigator.storage.getDirectory();
|
|
52
|
+
const handle = await root.getFileHandle(`${dbName}.sqlite`, { create: true });
|
|
53
|
+
const writable = await handle.createWritable();
|
|
54
|
+
// Copy to a new ArrayBuffer to satisfy TypeScript's strict ArrayBuffer type
|
|
55
|
+
const buffer = new ArrayBuffer(data.length);
|
|
56
|
+
new Uint8Array(buffer).set(data);
|
|
57
|
+
await writable.write(buffer);
|
|
58
|
+
await writable.close();
|
|
59
|
+
} catch {
|
|
60
|
+
// Silently fail - OPFS may not be available
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Create browser SQLite persistence with OPFS storage.
|
|
67
|
+
*
|
|
68
|
+
* This helper handles all the OPFS boilerplate:
|
|
69
|
+
* - Loads existing database from OPFS on init
|
|
70
|
+
* - Persists to OPFS after every write operation
|
|
71
|
+
*
|
|
72
|
+
* @param SQL - The initialized sql.js module (from `await initSqlJs()`)
|
|
73
|
+
* @param dbName - Name for the database (used for OPFS filename: `{dbName}.sqlite`)
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```typescript
|
|
77
|
+
* import { createBrowserSqlitePersistence } from '@trestleinc/replicate/client';
|
|
78
|
+
* import initSqlJs from 'sql.js';
|
|
79
|
+
*
|
|
80
|
+
* const SQL = await initSqlJs({ locateFile: f => `https://sql.js.org/dist/${f}` });
|
|
81
|
+
* const persistence = await createBrowserSqlitePersistence(SQL, 'intervals');
|
|
82
|
+
*
|
|
83
|
+
* // Use in collection options
|
|
84
|
+
* convexCollectionOptions<Task>({
|
|
85
|
+
* // ...
|
|
86
|
+
* persistence,
|
|
87
|
+
* });
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
export async function createBrowserSqlitePersistence(
|
|
91
|
+
SQL: SqlJsStatic,
|
|
92
|
+
dbName: string
|
|
93
|
+
): Promise<Persistence> {
|
|
94
|
+
// Load existing database from OPFS if available
|
|
95
|
+
const existingData = await loadFromOPFS(dbName);
|
|
96
|
+
|
|
97
|
+
// Create database (with existing data if found)
|
|
98
|
+
const db = existingData ? new SQL.Database(existingData) : new SQL.Database();
|
|
99
|
+
|
|
100
|
+
// Create adapter with OPFS persistence
|
|
101
|
+
const adapter = new SqlJsAdapter(db, {
|
|
102
|
+
onPersist: createOPFSSaver(dbName),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Create and return persistence
|
|
106
|
+
return sqlitePersistence({ adapter, dbName });
|
|
107
|
+
}
|