@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.
- package/README.md +446 -260
- 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 -1620
- 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 -161
- package/src/client/services/checkpoint.ts +0 -86
- package/src/client/services/reconciliation.ts +0 -108
|
@@ -1,128 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,107 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,407 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
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
|
-
}
|