@syncular/server 0.15.46 → 0.15.47
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 +10 -3
- package/dist/context.d.ts +2 -1
- package/dist/index-bun.d.ts +2 -0
- package/dist/index-bun.js +2 -0
- package/dist/index-node.d.ts +2 -0
- package/dist/index-node.js +2 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -6
- package/dist/pull.js +1 -1
- package/dist/sqlite-blob-store.d.ts +4 -9
- package/dist/sqlite-blob-store.js +5 -10
- package/dist/sqlite-bun-driver.d.ts +11 -0
- package/dist/sqlite-bun-driver.js +27 -0
- package/dist/sqlite-bun.d.ts +24 -0
- package/dist/sqlite-bun.js +40 -0
- package/dist/sqlite-dialect.d.ts +7 -7
- package/dist/sqlite-dialect.js +2 -2
- package/dist/sqlite-driver.d.ts +26 -0
- package/dist/sqlite-driver.js +8 -0
- package/dist/sqlite-image.d.ts +7 -9
- package/dist/sqlite-image.js +26 -28
- package/dist/sqlite-lease-store.d.ts +4 -9
- package/dist/sqlite-lease-store.js +5 -10
- package/dist/sqlite-node-driver.d.ts +10 -0
- package/dist/sqlite-node-driver.js +30 -0
- package/dist/sqlite-node.d.ts +24 -0
- package/dist/sqlite-node.js +50 -0
- package/dist/sqlite-segment-store.d.ts +4 -10
- package/dist/sqlite-segment-store.js +6 -9
- package/dist/sqlite-storage.d.ts +3 -11
- package/dist/sqlite-storage.js +8 -5
- package/dist/storage-errors.js +4 -1
- package/package.json +18 -3
- package/src/context.ts +2 -1
- package/src/index-bun.ts +9 -0
- package/src/index-node.ts +9 -0
- package/src/index.ts +8 -6
- package/src/pull.ts +1 -1
- package/src/sqlite-blob-store.ts +11 -10
- package/src/sqlite-bun-driver.ts +42 -0
- package/src/sqlite-bun.ts +53 -0
- package/src/sqlite-dialect.ts +7 -7
- package/src/sqlite-driver.ts +44 -0
- package/src/sqlite-image.ts +44 -49
- package/src/sqlite-lease-store.ts +11 -10
- package/src/sqlite-node-driver.ts +46 -0
- package/src/sqlite-node.ts +62 -0
- package/src/sqlite-segment-store.ts +11 -11
- package/src/sqlite-storage.ts +13 -7
- package/src/storage-errors.ts +4 -1
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SQLite-backed segment store
|
|
3
|
-
* dependency-free). Bun-specific by design: it imports `bun:sqlite` at the
|
|
4
|
-
* top level, so it lives in its own module — importing it opts into the Bun
|
|
5
|
-
* runtime. The runtime-neutral `SegmentStore` interface, `MemorySegmentStore`,
|
|
6
|
-
* and `segmentIdFor` stay in `segment-store.ts` so the Workers/edge core can
|
|
7
|
-
* import them without pulling in `bun:sqlite` (runtime neutrality is enforced
|
|
8
|
-
* by `test/runtime-neutrality.test.ts`).
|
|
2
|
+
* SQLite-backed segment store over the shared synchronous driver.
|
|
9
3
|
*/
|
|
10
|
-
import { Database } from 'bun:sqlite';
|
|
11
4
|
import { type SegmentFindKey, type SegmentMetadata, type SegmentRecord, type SegmentStore, type SegmentStoreStats } from './segment-store.js';
|
|
5
|
+
import { type SqliteDatabase } from './sqlite-driver.js';
|
|
12
6
|
export declare class SqliteSegmentStore implements SegmentStore {
|
|
13
7
|
#private;
|
|
14
|
-
readonly db:
|
|
15
|
-
constructor(db?:
|
|
8
|
+
readonly db: SqliteDatabase;
|
|
9
|
+
constructor(db?: SqliteDatabase | string, options?: {
|
|
16
10
|
ttlMs?: number;
|
|
17
11
|
});
|
|
18
12
|
put(metadata: SegmentMetadata, bytes: Uint8Array, nowMs: number): Promise<SegmentRecord>;
|
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SQLite-backed segment store
|
|
3
|
-
* dependency-free). Bun-specific by design: it imports `bun:sqlite` at the
|
|
4
|
-
* top level, so it lives in its own module — importing it opts into the Bun
|
|
5
|
-
* runtime. The runtime-neutral `SegmentStore` interface, `MemorySegmentStore`,
|
|
6
|
-
* and `segmentIdFor` stay in `segment-store.ts` so the Workers/edge core can
|
|
7
|
-
* import them without pulling in `bun:sqlite` (runtime neutrality is enforced
|
|
8
|
-
* by `test/runtime-neutrality.test.ts`).
|
|
2
|
+
* SQLite-backed segment store over the shared synchronous driver.
|
|
9
3
|
*/
|
|
10
|
-
import { Database } from 'bun:sqlite';
|
|
11
4
|
import { DEFAULT_SEGMENT_TTL_MS, segmentIdFor, } from './segment-store.js';
|
|
5
|
+
import { SqliteAdapterRequiredError, } from './sqlite-driver.js';
|
|
12
6
|
export class SqliteSegmentStore {
|
|
13
7
|
db;
|
|
14
8
|
#ttlMs;
|
|
15
9
|
constructor(db = ':memory:', options) {
|
|
16
|
-
|
|
10
|
+
if (typeof db === 'string') {
|
|
11
|
+
throw new SqliteAdapterRequiredError();
|
|
12
|
+
}
|
|
13
|
+
this.db = db;
|
|
17
14
|
this.#ttlMs = options?.ttlMs ?? DEFAULT_SEGMENT_TTL_MS;
|
|
18
15
|
this.db.exec(`
|
|
19
16
|
CREATE TABLE IF NOT EXISTS sync_segments(
|
package/dist/sqlite-storage.d.ts
CHANGED
|
@@ -1,18 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SQLite storage via `bun:sqlite` (dev-speed, dependency-free).
|
|
3
|
-
*
|
|
4
|
-
* Scope fanout is index-first: both the commit log and the
|
|
5
|
-
* current-row table carry a (table, variable, value) inverted index; reads
|
|
6
|
-
* select candidates from the index and verify the full multi-variable
|
|
7
|
-
* match against the stored scope map — never a log scan.
|
|
8
|
-
*/
|
|
9
|
-
import { Database } from 'bun:sqlite';
|
|
10
1
|
import type { CompiledSchema, CompiledTable } from './schema.js';
|
|
2
|
+
import { type SqliteDatabase } from './sqlite-driver.js';
|
|
11
3
|
import type { AuthoritativeQueryRequest, AuthoritativeQueryResult, ClientCursorInfo, ClientRecord, CommitMetadata, CommitMetadataQuery, CommitWindowQuery, IndexRowScanQuery, PrunedReactionCounts, ReactionClaimQuery, ReactionFailureUpdate, ReactionListQuery, ReactionPruneQuery, RowScanQuery, ScopeActivityQuery, ScopeCommitActivity, ServerStorage, StorageTransaction, StoredCommit, StoredPushResult, StoredReaction, StoredRow } from './storage.js';
|
|
12
4
|
export declare class SqliteServerStorage implements ServerStorage {
|
|
13
5
|
#private;
|
|
14
|
-
readonly db:
|
|
15
|
-
constructor(db?:
|
|
6
|
+
readonly db: SqliteDatabase;
|
|
7
|
+
constructor(db?: SqliteDatabase | string);
|
|
16
8
|
/** Resolve a table's compiled schema; row operations require `ensureSchema`. */
|
|
17
9
|
table(name: string): CompiledTable;
|
|
18
10
|
ensureSchema(schema: CompiledSchema): Promise<void>;
|
package/dist/sqlite-storage.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SQLite storage
|
|
2
|
+
* SQLite server storage over the shared synchronous driver.
|
|
3
3
|
*
|
|
4
4
|
* Scope fanout is index-first: both the commit log and the
|
|
5
5
|
* current-row table carry a (table, variable, value) inverted index; reads
|
|
6
6
|
* select candidates from the index and verify the full multi-variable
|
|
7
7
|
* match against the stored scope map — never a log scan.
|
|
8
8
|
*/
|
|
9
|
-
import { Database } from 'bun:sqlite';
|
|
10
9
|
import { bindAuthoritativePartition, prepareAuthoritativeQuery, } from './authoritative-query.js';
|
|
11
10
|
import { syncError } from './errors.js';
|
|
12
11
|
import { commitWindowPageSql, deleteRowSql, dropTableDdl, indexRowPageStatement, layoutsOf, migratePayload, parseLayouts, retiredTableNames, rewritePlan, rewriteRowSql, rewriteValues, SCHEMA_META_DDL_SQLITE, scanRowPageSql, schemaDdl, selectRowScopesSql, selectRowSql, selectRowsForRewriteSql, upsertSql, upsertValues, } from './relational-rows.js';
|
|
13
12
|
import { matchesEffective } from './scopes.js';
|
|
14
13
|
import { collectCommitWindowPage, deserializePushResult, SQLITE_DDL, serializePushResult, toStoredRow, } from './sqlite-dialect.js';
|
|
14
|
+
import { SqliteAdapterRequiredError, } from './sqlite-driver.js';
|
|
15
15
|
import { isSqliteConstraintError, StorageConstraintError, } from './storage-errors.js';
|
|
16
16
|
import { assertScopeIndexedScan, resolveIndexRowScan } from './storage-query.js';
|
|
17
17
|
function toStoredReaction(record) {
|
|
@@ -62,7 +62,7 @@ class SqliteTransaction {
|
|
|
62
62
|
}
|
|
63
63
|
getPushResult(clientId, clientCommitId) {
|
|
64
64
|
this.#assertOpen();
|
|
65
|
-
// One shared
|
|
65
|
+
// One shared SQLite connection: this read runs inside this
|
|
66
66
|
// transaction's BEGIN IMMEDIATE.
|
|
67
67
|
return this.#storage.getPushResult(this.#partition, clientId, clientCommitId);
|
|
68
68
|
}
|
|
@@ -194,7 +194,7 @@ class SqliteTransaction {
|
|
|
194
194
|
}
|
|
195
195
|
export class SqliteServerStorage {
|
|
196
196
|
db;
|
|
197
|
-
/** One
|
|
197
|
+
/** One SQLite connection can own only one transaction at a time. */
|
|
198
198
|
#transactionTail = Promise.resolve();
|
|
199
199
|
/** Set by `ensureSchema`: app-table lookup for the relational row store. */
|
|
200
200
|
#tables;
|
|
@@ -214,7 +214,10 @@ export class SqliteServerStorage {
|
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
216
|
constructor(db = ':memory:') {
|
|
217
|
-
|
|
217
|
+
if (typeof db === 'string') {
|
|
218
|
+
throw new SqliteAdapterRequiredError();
|
|
219
|
+
}
|
|
220
|
+
this.db = db;
|
|
218
221
|
this.db.exec(SQLITE_DDL);
|
|
219
222
|
}
|
|
220
223
|
/** Resolve a table's compiled schema; row operations require `ensureSchema`. */
|
package/dist/storage-errors.js
CHANGED
|
@@ -44,7 +44,10 @@ export function isSqliteConstraintError(error) {
|
|
|
44
44
|
return true;
|
|
45
45
|
}
|
|
46
46
|
const errno = candidate?.errno;
|
|
47
|
-
|
|
47
|
+
if (typeof errno === 'number' && (errno & 0xff) === 19)
|
|
48
|
+
return true;
|
|
49
|
+
const errcode = candidate?.errcode;
|
|
50
|
+
return typeof errcode === 'number' && (errcode & 0xff) === 19;
|
|
48
51
|
}
|
|
49
52
|
/** PostgreSQL SQLSTATE class 23: integrity constraint violation. */
|
|
50
53
|
export function isPostgresConstraintError(error) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/server",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.47",
|
|
4
4
|
"description": "Syncular server: handleSyncRequest + storage/auth interfaces for the sync protocol",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Benjamin Kniffler",
|
|
@@ -27,13 +27,25 @@
|
|
|
27
27
|
},
|
|
28
28
|
"exports": {
|
|
29
29
|
".": {
|
|
30
|
-
"bun": "./src/index.ts",
|
|
30
|
+
"bun": "./src/index-bun.ts",
|
|
31
|
+
"node": {
|
|
32
|
+
"types": "./dist/index-node.d.ts",
|
|
33
|
+
"default": "./dist/index-node.js"
|
|
34
|
+
},
|
|
31
35
|
"browser": "./dist/index.js",
|
|
32
36
|
"import": {
|
|
33
37
|
"types": "./dist/index.d.ts",
|
|
34
38
|
"default": "./dist/index.js"
|
|
35
39
|
}
|
|
36
40
|
},
|
|
41
|
+
"./sqlite": {
|
|
42
|
+
"bun": "./src/sqlite-bun.ts",
|
|
43
|
+
"node": {
|
|
44
|
+
"types": "./dist/sqlite-node.d.ts",
|
|
45
|
+
"default": "./dist/sqlite-node.js"
|
|
46
|
+
},
|
|
47
|
+
"types": "./dist/sqlite-node.d.ts"
|
|
48
|
+
},
|
|
37
49
|
"./pglite": {
|
|
38
50
|
"bun": "./src/pg-executor-pglite.ts",
|
|
39
51
|
"browser": "./dist/pg-executor-pglite.js",
|
|
@@ -52,8 +64,11 @@
|
|
|
52
64
|
"!dist/**/*.test.js",
|
|
53
65
|
"!dist/**/*.test.d.ts"
|
|
54
66
|
],
|
|
67
|
+
"scripts": {
|
|
68
|
+
"verify:node": "cd ../.. && bun build ./packages/server/test/sqlite-runtime/verify-node.mjs --target=node --conditions=bun --outfile=./packages/server/.verify-node.built.mjs && node ./packages/server/.verify-node.built.mjs"
|
|
69
|
+
},
|
|
55
70
|
"dependencies": {
|
|
56
|
-
"@syncular/core": "0.15.
|
|
71
|
+
"@syncular/core": "0.15.47"
|
|
57
72
|
},
|
|
58
73
|
"devDependencies": {
|
|
59
74
|
"@electric-sql/pglite": "^0.5.4"
|
package/src/context.ts
CHANGED
|
@@ -167,7 +167,8 @@ export interface SyncServerConfig {
|
|
|
167
167
|
* §5.3 sqlite-image builder, injected so the pull path never
|
|
168
168
|
* statically imports `bun:sqlite`. Absent ⇒ the sqlite-image lane is off
|
|
169
169
|
* (bit-2 clients are served the rows lane) — the Workers/edge posture. A
|
|
170
|
-
* Bun
|
|
170
|
+
* Bun or Node host wires `buildSqliteImage` from
|
|
171
|
+
* `@syncular/server/sqlite`.
|
|
171
172
|
*/
|
|
172
173
|
readonly sqliteImageBuilder?: SqliteImageBuilder;
|
|
173
174
|
readonly realtime?: RealtimeNotifier;
|
package/src/index-bun.ts
ADDED
package/src/index.ts
CHANGED
|
@@ -73,13 +73,15 @@ export * from './seed';
|
|
|
73
73
|
export * from './segment-download';
|
|
74
74
|
export * from './segment-store';
|
|
75
75
|
export * from './signed-url';
|
|
76
|
-
// Bun-specific storages (top-level `bun:sqlite`): re-exported for Bun/Node
|
|
77
|
-
// hosts. Workers/edge builds tree-shake them (and their `bun:sqlite` import)
|
|
78
|
-
// away — the runtime-neutral core closure is enforced by
|
|
79
|
-
// `test/runtime-neutrality.test.ts`.
|
|
80
|
-
export * from './sqlite-blob-store';
|
|
81
76
|
export * from './sqlite-dialect';
|
|
82
|
-
export * from './sqlite-
|
|
77
|
+
export * from './sqlite-driver';
|
|
78
|
+
export {
|
|
79
|
+
IMAGE_METADATA_TABLE,
|
|
80
|
+
IMAGE_VERSION_COLUMN,
|
|
81
|
+
type SqliteImageBuilder,
|
|
82
|
+
type SqliteImageInput,
|
|
83
|
+
} from './sqlite-image';
|
|
84
|
+
export * from './sqlite-blob-store';
|
|
83
85
|
export * from './sqlite-lease-store';
|
|
84
86
|
export * from './sqlite-segment-store';
|
|
85
87
|
export * from './sqlite-storage';
|
package/src/pull.ts
CHANGED
|
@@ -36,7 +36,7 @@ async function resolveImageBuilder(
|
|
|
36
36
|
if (cachedDefaultBuilder === undefined) {
|
|
37
37
|
const hasBun = (globalThis as { Bun?: unknown }).Bun !== undefined;
|
|
38
38
|
cachedDefaultBuilder = hasBun
|
|
39
|
-
? (await import('./sqlite-
|
|
39
|
+
? (await import('./sqlite-bun')).buildSqliteImage
|
|
40
40
|
: null;
|
|
41
41
|
}
|
|
42
42
|
return cachedDefaultBuilder ?? undefined;
|
package/src/sqlite-blob-store.ts
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SQLite-backed blob store
|
|
3
|
-
* dependency-free). Bun-specific by design (top-level `bun:sqlite` import),
|
|
4
|
-
* so it lives in its own module — the runtime-neutral `BlobStore` interface,
|
|
5
|
-
* `MemoryBlobStore`, `blobIdFor`, and `isBlobId` stay in `blob-store.ts` for
|
|
6
|
-
* the Workers/edge core (runtime neutrality is enforced by
|
|
7
|
-
* `test/runtime-neutrality.test.ts`).
|
|
2
|
+
* SQLite-backed blob store over the shared synchronous driver.
|
|
8
3
|
*/
|
|
9
|
-
import { Database } from 'bun:sqlite';
|
|
10
4
|
import type { BlobRecord, BlobStore, BlobStoreStats } from './blob-store';
|
|
5
|
+
import {
|
|
6
|
+
SqliteAdapterRequiredError,
|
|
7
|
+
type SqliteDatabase,
|
|
8
|
+
} from './sqlite-driver';
|
|
11
9
|
|
|
12
10
|
export class SqliteBlobStore implements BlobStore {
|
|
13
|
-
readonly db:
|
|
11
|
+
readonly db: SqliteDatabase;
|
|
14
12
|
|
|
15
|
-
constructor(db:
|
|
16
|
-
|
|
13
|
+
constructor(db: SqliteDatabase | string = ':memory:') {
|
|
14
|
+
if (typeof db === 'string') {
|
|
15
|
+
throw new SqliteAdapterRequiredError();
|
|
16
|
+
}
|
|
17
|
+
this.db = db;
|
|
17
18
|
this.db.exec(`
|
|
18
19
|
CREATE TABLE IF NOT EXISTS sync_blobs(
|
|
19
20
|
partition TEXT NOT NULL, blob_id TEXT NOT NULL,
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Database, type SQLQueryBindings } from 'bun:sqlite';
|
|
2
|
+
import type {
|
|
3
|
+
SqliteDatabase,
|
|
4
|
+
SqliteRunResult,
|
|
5
|
+
SqliteStatement,
|
|
6
|
+
SqliteValue,
|
|
7
|
+
} from './sqlite-driver';
|
|
8
|
+
|
|
9
|
+
export class BunSqliteDatabase implements SqliteDatabase {
|
|
10
|
+
readonly native: Database;
|
|
11
|
+
|
|
12
|
+
constructor(path = ':memory:') {
|
|
13
|
+
this.native = new Database(path);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
exec(sql: string): void {
|
|
17
|
+
this.native.exec(sql);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
run(sql: string, bindings: readonly SqliteValue[] = []): SqliteRunResult {
|
|
21
|
+
return this.native.run(sql, [...bindings]);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
query<Row, Params extends readonly SqliteValue[]>(
|
|
25
|
+
sql: string,
|
|
26
|
+
): SqliteStatement<Row, Params> {
|
|
27
|
+
const statement = this.native.query<Row, SQLQueryBindings[]>(sql);
|
|
28
|
+
return {
|
|
29
|
+
run: (...params) => statement.run(...params),
|
|
30
|
+
get: (...params) => statement.get(...params),
|
|
31
|
+
all: (...params) => statement.all(...params),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
serialize(): Uint8Array {
|
|
36
|
+
return new Uint8Array(this.native.serialize());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
close(): void {
|
|
40
|
+
this.native.close();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { SqliteBlobStore as SharedSqliteBlobStore } from './sqlite-blob-store';
|
|
2
|
+
import { BunSqliteDatabase } from './sqlite-bun-driver';
|
|
3
|
+
import type { SqliteDatabase } from './sqlite-driver';
|
|
4
|
+
import { type SqliteImageBuilder, writeSqliteImage } from './sqlite-image';
|
|
5
|
+
import { SqliteLeaseStore as SharedSqliteLeaseStore } from './sqlite-lease-store';
|
|
6
|
+
import { SqliteSegmentStore as SharedSqliteSegmentStore } from './sqlite-segment-store';
|
|
7
|
+
import { SqliteServerStorage as SharedSqliteServerStorage } from './sqlite-storage';
|
|
8
|
+
|
|
9
|
+
function database(value: SqliteDatabase | string): SqliteDatabase {
|
|
10
|
+
return typeof value === 'string' ? new BunSqliteDatabase(value) : value;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class SqliteServerStorage extends SharedSqliteServerStorage {
|
|
14
|
+
constructor(value: SqliteDatabase | string = ':memory:') {
|
|
15
|
+
super(database(value));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class SqliteSegmentStore extends SharedSqliteSegmentStore {
|
|
20
|
+
constructor(
|
|
21
|
+
value: SqliteDatabase | string = ':memory:',
|
|
22
|
+
options?: { ttlMs?: number },
|
|
23
|
+
) {
|
|
24
|
+
super(database(value), options);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class SqliteBlobStore extends SharedSqliteBlobStore {
|
|
29
|
+
constructor(value: SqliteDatabase | string = ':memory:') {
|
|
30
|
+
super(database(value));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class SqliteLeaseStore extends SharedSqliteLeaseStore {
|
|
35
|
+
constructor(
|
|
36
|
+
value: SqliteDatabase | string = ':memory:',
|
|
37
|
+
options?: { readonly leaseId?: () => string },
|
|
38
|
+
) {
|
|
39
|
+
super(database(value), options);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const buildSqliteImage: SqliteImageBuilder = (input) => {
|
|
44
|
+
const db = new BunSqliteDatabase();
|
|
45
|
+
try {
|
|
46
|
+
writeSqliteImage(db, input);
|
|
47
|
+
return db.serialize();
|
|
48
|
+
} finally {
|
|
49
|
+
db.close();
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export { BunSqliteDatabase } from './sqlite-bun-driver';
|
package/src/sqlite-dialect.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared SQLite dialect for
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Shared SQLite dialect for synchronous `SqliteServerStorage` on Bun or Node
|
|
3
|
+
* and asynchronous `D1ServerStorage` on Cloudflare Workers. D1 is SQLite:
|
|
4
|
+
* same DDL, same statement grammar,
|
|
5
5
|
* same `?` positional placeholders, same `INSERT ... ON CONFLICT` / `INSERT
|
|
6
6
|
* OR IGNORE` upsert idioms — so the schema and the value (de)serialization are
|
|
7
7
|
* genuinely common ground and live here.
|
|
8
8
|
*
|
|
9
|
-
* What is
|
|
9
|
+
* What is not shared: statement execution. The server SQLite driver is sync
|
|
10
10
|
* (`db.query(sql).get(...)`) and D1 is async (`await
|
|
11
11
|
* db.prepare(sql).bind(...).all()`); a shared execution layer would have to
|
|
12
12
|
* pick one calling convention and adapt the other, which is uglier than two
|
|
@@ -25,7 +25,7 @@ import type {
|
|
|
25
25
|
} from './storage';
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
* Schema DDL
|
|
28
|
+
* Schema DDL, one statement per `;`-delimited chunk. Native SQLite applies
|
|
29
29
|
* the whole string via `db.exec(SQLITE_DDL)`; D1 applies each statement
|
|
30
30
|
* separately (its `prepare`/`batch` API is one statement per call). Types
|
|
31
31
|
* are SQLite's: `INTEGER`/`TEXT`/`BLOB`. Scopes are stored as JSON `TEXT`
|
|
@@ -246,7 +246,7 @@ export interface SqliteChangeRecord {
|
|
|
246
246
|
payload: Uint8Array | null;
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
-
/**
|
|
249
|
+
/** Native SQLite returns `Uint8Array`; D1 returns `ArrayBuffer` for BLOBs. */
|
|
250
250
|
export function asUint8Array(value: unknown): Uint8Array {
|
|
251
251
|
if (value instanceof Uint8Array) return value;
|
|
252
252
|
if (value instanceof ArrayBuffer) return new Uint8Array(value);
|
|
@@ -280,7 +280,7 @@ export function toStoredChange(record: SqliteChangeRecord): StoredChange {
|
|
|
280
280
|
* One result row of `commitWindowPageSql` (candidate LEFT JOIN commit meta
|
|
281
281
|
* LEFT JOIN changes): meta/change columns are NULL when the joined row
|
|
282
282
|
* vanished (see the builder's LEFT JOIN contract). `payload` is a BLOB —
|
|
283
|
-
*
|
|
283
|
+
* Native SQLite hands back `Uint8Array`, D1 `ArrayBuffer`; `toStoredChange`
|
|
284
284
|
* normalizes via `asUint8Array`.
|
|
285
285
|
*/
|
|
286
286
|
export interface SqliteCommitWindowRecord {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Values accepted by the synchronous SQLite adapters. */
|
|
2
|
+
export type SqliteValue =
|
|
3
|
+
| string
|
|
4
|
+
| number
|
|
5
|
+
| bigint
|
|
6
|
+
| boolean
|
|
7
|
+
| Uint8Array
|
|
8
|
+
| null;
|
|
9
|
+
|
|
10
|
+
/** Result of a SQLite statement that does not return rows. */
|
|
11
|
+
export interface SqliteRunResult {
|
|
12
|
+
readonly changes: number | bigint;
|
|
13
|
+
readonly lastInsertRowid: number | bigint;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Prepared synchronous SQLite statement used by the server stores. */
|
|
17
|
+
export interface SqliteStatement<Row, Params extends readonly SqliteValue[]> {
|
|
18
|
+
run(...params: Params): SqliteRunResult;
|
|
19
|
+
get(...params: Params): Row | null;
|
|
20
|
+
all(...params: Params): Row[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Runtime-neutral database surface shared by the Bun and Node adapters. */
|
|
24
|
+
export interface SqliteDatabase {
|
|
25
|
+
exec(sql: string): void;
|
|
26
|
+
run(sql: string, bindings?: readonly SqliteValue[]): SqliteRunResult;
|
|
27
|
+
query<
|
|
28
|
+
Row = Record<string, SqliteValue>,
|
|
29
|
+
Params extends readonly SqliteValue[] = SqliteValue[],
|
|
30
|
+
>(
|
|
31
|
+
sql: string,
|
|
32
|
+
): SqliteStatement<Row, Params>;
|
|
33
|
+
close(): void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Raised when a neutral-runtime import is used without a SQLite adapter. */
|
|
37
|
+
export class SqliteAdapterRequiredError extends Error {
|
|
38
|
+
override readonly name = 'SqliteAdapterRequiredError';
|
|
39
|
+
readonly code = 'sync.sqlite_adapter_required';
|
|
40
|
+
|
|
41
|
+
constructor() {
|
|
42
|
+
super('SQLite paths require the @syncular/server/sqlite runtime adapter');
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/sqlite-image.ts
CHANGED
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
* SQLite-image segment generation (SPEC.md §5.3): a complete SQLite
|
|
3
3
|
* database file carrying one table's whole effective-scope snapshot at
|
|
4
4
|
* the bootstrap pin, plus the single-row `_syncular_segment` metadata
|
|
5
|
-
* table.
|
|
5
|
+
* table. Runtime entries provide the concrete SQLite database.
|
|
6
6
|
*
|
|
7
7
|
* Images are NOT byte-deterministic (§5.3): the content address pins the
|
|
8
8
|
* served bytes, and cross-client dedup comes from the segment store's
|
|
9
9
|
* metadata lookup (`SegmentStore.find`), not from hash convergence.
|
|
10
10
|
*/
|
|
11
|
-
import { Database } from 'bun:sqlite';
|
|
12
11
|
import { decodeRow, type RowColumn, type RowValue } from '@syncular/core';
|
|
13
12
|
import type { CompiledTable } from './schema';
|
|
13
|
+
import type { SqliteDatabase } from './sqlite-driver';
|
|
14
14
|
import type { StoredRow } from './storage';
|
|
15
15
|
|
|
16
16
|
/** The §5.6 version column as it appears inside a sqlite image (§5.3). */
|
|
@@ -63,62 +63,57 @@ export interface SqliteImageInput {
|
|
|
63
63
|
/**
|
|
64
64
|
* The §5.3 image-builder capability, injected through
|
|
65
65
|
* `SyncServerConfig.sqliteImageBuilder`. Building an image needs
|
|
66
|
-
* a real SQLite engine
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* pull serves the rows lane (§5.3 clients advertise sqlite as an *accept*,
|
|
71
|
-
* never a requirement — the host chooses the served format from what it can
|
|
72
|
-
* produce; this is a support floor, not a fallback).
|
|
66
|
+
* a real SQLite engine, which is not available on every runtime. The core
|
|
67
|
+
* takes the builder as an optional capability rather than importing a driver
|
|
68
|
+
* on the pull path. A Bun or Node host passes `buildSqliteImage`; a Workers
|
|
69
|
+
* host omits it and serves the rows lane.
|
|
73
70
|
*/
|
|
74
71
|
export type SqliteImageBuilder = (input: SqliteImageInput) => Uint8Array;
|
|
75
72
|
|
|
76
|
-
/**
|
|
77
|
-
export
|
|
73
|
+
/** Populate a §5.3 image database for a whole-table snapshot. */
|
|
74
|
+
export function writeSqliteImage(
|
|
75
|
+
db: SqliteDatabase,
|
|
76
|
+
input: SqliteImageInput,
|
|
77
|
+
): void {
|
|
78
78
|
const { table, rows } = input;
|
|
79
79
|
const primaryKey = table.columns[table.primaryKeyIndex]?.name;
|
|
80
|
-
const
|
|
80
|
+
const columnDefs = table.columns.map((column) => {
|
|
81
|
+
const notNull = column.nullable ? '' : ' NOT NULL';
|
|
82
|
+
const pk = column.name === primaryKey ? ' PRIMARY KEY' : '';
|
|
83
|
+
return `${quoteIdent(column.name)} ${sqlType(column)}${notNull}${pk}`;
|
|
84
|
+
});
|
|
85
|
+
columnDefs.push(`${quoteIdent(IMAGE_VERSION_COLUMN)} INTEGER NOT NULL`);
|
|
86
|
+
db.exec(`CREATE TABLE ${quoteIdent(table.name)} (${columnDefs.join(', ')})`);
|
|
87
|
+
db.exec(
|
|
88
|
+
`CREATE TABLE ${IMAGE_METADATA_TABLE} (
|
|
89
|
+
format INTEGER NOT NULL, "table" TEXT NOT NULL,
|
|
90
|
+
"schemaVersion" INTEGER NOT NULL, "asOfCommitSeq" INTEGER NOT NULL,
|
|
91
|
+
"scopeDigest" TEXT NOT NULL, "rowCount" INTEGER NOT NULL)`,
|
|
92
|
+
);
|
|
93
|
+
db.query(`INSERT INTO ${IMAGE_METADATA_TABLE} VALUES (1, ?, ?, ?, ?, ?)`).run(
|
|
94
|
+
table.name,
|
|
95
|
+
input.schemaVersion,
|
|
96
|
+
input.asOfCommitSeq,
|
|
97
|
+
input.scopeDigest,
|
|
98
|
+
rows.length,
|
|
99
|
+
);
|
|
100
|
+
const names = [
|
|
101
|
+
...table.columns.map((column) => quoteIdent(column.name)),
|
|
102
|
+
quoteIdent(IMAGE_VERSION_COLUMN),
|
|
103
|
+
];
|
|
104
|
+
const insert = db.query(
|
|
105
|
+
`INSERT INTO ${quoteIdent(table.name)} (${names.join(', ')})
|
|
106
|
+
VALUES (${names.map(() => '?').join(', ')})`,
|
|
107
|
+
);
|
|
108
|
+
db.exec('BEGIN');
|
|
81
109
|
try {
|
|
82
|
-
const columnDefs = table.columns.map((column) => {
|
|
83
|
-
const notNull = column.nullable ? '' : ' NOT NULL';
|
|
84
|
-
const pk = column.name === primaryKey ? ' PRIMARY KEY' : '';
|
|
85
|
-
return `${quoteIdent(column.name)} ${sqlType(column)}${notNull}${pk}`;
|
|
86
|
-
});
|
|
87
|
-
columnDefs.push(`${quoteIdent(IMAGE_VERSION_COLUMN)} INTEGER NOT NULL`);
|
|
88
|
-
db.exec(
|
|
89
|
-
`CREATE TABLE ${quoteIdent(table.name)} (${columnDefs.join(', ')})`,
|
|
90
|
-
);
|
|
91
|
-
db.exec(
|
|
92
|
-
`CREATE TABLE ${IMAGE_METADATA_TABLE} (
|
|
93
|
-
format INTEGER NOT NULL, "table" TEXT NOT NULL,
|
|
94
|
-
"schemaVersion" INTEGER NOT NULL, "asOfCommitSeq" INTEGER NOT NULL,
|
|
95
|
-
"scopeDigest" TEXT NOT NULL, "rowCount" INTEGER NOT NULL)`,
|
|
96
|
-
);
|
|
97
|
-
db.query(
|
|
98
|
-
`INSERT INTO ${IMAGE_METADATA_TABLE} VALUES (1, ?, ?, ?, ?, ?)`,
|
|
99
|
-
).run(
|
|
100
|
-
table.name,
|
|
101
|
-
input.schemaVersion,
|
|
102
|
-
input.asOfCommitSeq,
|
|
103
|
-
input.scopeDigest,
|
|
104
|
-
rows.length,
|
|
105
|
-
);
|
|
106
|
-
const names = [
|
|
107
|
-
...table.columns.map((column) => quoteIdent(column.name)),
|
|
108
|
-
quoteIdent(IMAGE_VERSION_COLUMN),
|
|
109
|
-
];
|
|
110
|
-
const insert = db.query(
|
|
111
|
-
`INSERT INTO ${quoteIdent(table.name)} (${names.join(', ')})
|
|
112
|
-
VALUES (${names.map(() => '?').join(', ')})`,
|
|
113
|
-
);
|
|
114
|
-
db.exec('BEGIN');
|
|
115
110
|
for (const row of rows) {
|
|
116
111
|
const values = decodeRow(table.columns, row.payload);
|
|
117
112
|
insert.run(...values.map(toSql), row.serverVersion);
|
|
118
113
|
}
|
|
119
114
|
db.exec('COMMIT');
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
115
|
+
} catch (error) {
|
|
116
|
+
db.exec('ROLLBACK');
|
|
117
|
+
throw error;
|
|
123
118
|
}
|
|
124
|
-
}
|
|
119
|
+
}
|
|
@@ -1,28 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SQLite-backed auth-lease store
|
|
3
|
-
* dependency-free). Bun-specific by design (top-level `bun:sqlite` import),
|
|
4
|
-
* so it lives in its own module — the runtime-neutral `LeaseStore` interface,
|
|
5
|
-
* `LeaseRecord`, and `MemoryLeaseStore` stay in `lease-store.ts` for the
|
|
6
|
-
* Workers/edge core (runtime neutrality is enforced by
|
|
7
|
-
* `test/runtime-neutrality.test.ts`).
|
|
2
|
+
* SQLite-backed auth-lease store over the shared synchronous driver.
|
|
8
3
|
*/
|
|
9
|
-
import { Database } from 'bun:sqlite';
|
|
10
4
|
import type { ScopeMap } from '@syncular/core';
|
|
11
5
|
import type { LeaseIdFactory, LeaseRecord, LeaseStore } from './lease-store';
|
|
6
|
+
import {
|
|
7
|
+
SqliteAdapterRequiredError,
|
|
8
|
+
type SqliteDatabase,
|
|
9
|
+
} from './sqlite-driver';
|
|
12
10
|
|
|
13
11
|
function defaultLeaseId(): string {
|
|
14
12
|
return `lease_${crypto.randomUUID()}`;
|
|
15
13
|
}
|
|
16
14
|
|
|
17
15
|
export class SqliteLeaseStore implements LeaseStore {
|
|
18
|
-
readonly db:
|
|
16
|
+
readonly db: SqliteDatabase;
|
|
19
17
|
readonly #newId: LeaseIdFactory;
|
|
20
18
|
|
|
21
19
|
constructor(
|
|
22
|
-
db:
|
|
20
|
+
db: SqliteDatabase | string = ':memory:',
|
|
23
21
|
options?: { readonly leaseId?: LeaseIdFactory },
|
|
24
22
|
) {
|
|
25
|
-
|
|
23
|
+
if (typeof db === 'string') {
|
|
24
|
+
throw new SqliteAdapterRequiredError();
|
|
25
|
+
}
|
|
26
|
+
this.db = db;
|
|
26
27
|
this.#newId = options?.leaseId ?? defaultLeaseId;
|
|
27
28
|
this.db.exec(`
|
|
28
29
|
CREATE TABLE IF NOT EXISTS sync_leases(
|