@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
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ The supported set, and what deliberately does **not** get an adapter:
|
|
|
29
29
|
|
|
30
30
|
| Runtime | Adapter | Transport | Storage | Status |
|
|
31
31
|
| --- | --- | --- | --- | --- |
|
|
32
|
-
| **Bun / Node
|
|
32
|
+
| **Bun / Node 22.13+** | `@syncular/server-hono` | HTTP (`POST /sync`, segments, blobs) **+ WS realtime** (§8, host-driven upgrade) | `SqliteServerStorage` through `@syncular/server/sqlite`, Postgres, memory | **Supported now** — the reference deployment; runs the full conformance catalog on both bindings. |
|
|
33
33
|
| **Cloudflare Workers** | `@syncular/server-workers` | HTTP binding via Hono (Workers-native) **+ optional WS realtime** (§8) | `D1ServerStorage` behind one per-partition Durable Object queue; R2-as-S3 for segments/blobs | **Supported now** — D1 sync writes always traverse the DO; WebSocket upgrades remain optional. |
|
|
34
34
|
| Raw Deno / edge-misc | — | — | — | **Not adapted** (policy below). |
|
|
35
35
|
|
|
@@ -76,9 +76,9 @@ or the storage projection cannot migrate:
|
|
|
76
76
|
```ts
|
|
77
77
|
import {
|
|
78
78
|
ensureSyncServerReady,
|
|
79
|
-
SqliteServerStorage,
|
|
80
79
|
type SyncServerConfig,
|
|
81
80
|
} from '@syncular/server';
|
|
81
|
+
import { SqliteServerStorage } from '@syncular/server/sqlite';
|
|
82
82
|
|
|
83
83
|
const config: SyncServerConfig = {
|
|
84
84
|
schema,
|
|
@@ -91,6 +91,12 @@ await ensureSyncServerReady(config);
|
|
|
91
91
|
Bun.serve({ fetch: app.fetch });
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
+
`@syncular/server/sqlite` selects `bun:sqlite` on Bun and the built-in
|
|
95
|
+
`node:sqlite` module on Node. It covers server storage, segment storage, blob
|
|
96
|
+
storage, leases, and SQLite-image generation without an external SQLite
|
|
97
|
+
package. The runtime-specific database wrappers are `BunSqliteDatabase` and
|
|
98
|
+
`NodeSqliteDatabase` when a host needs direct access to the native handle.
|
|
99
|
+
|
|
94
100
|
The helper accepts the generated `ServerSchema`, compiles it, and calls the
|
|
95
101
|
storage backend's low-level `ensureSchema(CompiledSchema)`. A failure is a
|
|
96
102
|
`SyncServerReadinessError` with stable code `sync.schema_not_ready`, a `phase`
|
|
@@ -974,7 +980,8 @@ same table+scope during a storm, your TTL is shorter than the storm.
|
|
|
974
980
|
|
|
975
981
|
## Postgres storage (the production database path)
|
|
976
982
|
|
|
977
|
-
`SqliteServerStorage`
|
|
983
|
+
`SqliteServerStorage` from `@syncular/server/sqlite` uses `bun:sqlite` or
|
|
984
|
+
Node's built-in `node:sqlite`. For
|
|
978
985
|
production, `PostgresServerStorage` implements the same `ServerStorage`
|
|
979
986
|
contract against Postgres, with the inverted scope index carried through
|
|
980
987
|
as **covering indexes** so scope fanout is an index range scan, never a
|
package/dist/context.d.ts
CHANGED
|
@@ -148,7 +148,8 @@ export interface SyncServerConfig {
|
|
|
148
148
|
* §5.3 sqlite-image builder, injected so the pull path never
|
|
149
149
|
* statically imports `bun:sqlite`. Absent ⇒ the sqlite-image lane is off
|
|
150
150
|
* (bit-2 clients are served the rows lane) — the Workers/edge posture. A
|
|
151
|
-
* Bun
|
|
151
|
+
* Bun or Node host wires `buildSqliteImage` from
|
|
152
|
+
* `@syncular/server/sqlite`.
|
|
152
153
|
*/
|
|
153
154
|
readonly sqliteImageBuilder?: SqliteImageBuilder;
|
|
154
155
|
readonly realtime?: RealtimeNotifier;
|
package/dist/index.d.ts
CHANGED
|
@@ -41,9 +41,10 @@ export * from './seed.js';
|
|
|
41
41
|
export * from './segment-download.js';
|
|
42
42
|
export * from './segment-store.js';
|
|
43
43
|
export * from './signed-url.js';
|
|
44
|
-
export * from './sqlite-blob-store.js';
|
|
45
44
|
export * from './sqlite-dialect.js';
|
|
46
|
-
export * from './sqlite-
|
|
45
|
+
export * from './sqlite-driver.js';
|
|
46
|
+
export { IMAGE_METADATA_TABLE, IMAGE_VERSION_COLUMN, type SqliteImageBuilder, type SqliteImageInput, } from './sqlite-image.js';
|
|
47
|
+
export * from './sqlite-blob-store.js';
|
|
47
48
|
export * from './sqlite-lease-store.js';
|
|
48
49
|
export * from './sqlite-segment-store.js';
|
|
49
50
|
export * from './sqlite-storage.js';
|
package/dist/index.js
CHANGED
|
@@ -45,13 +45,10 @@ export * from './seed.js';
|
|
|
45
45
|
export * from './segment-download.js';
|
|
46
46
|
export * from './segment-store.js';
|
|
47
47
|
export * from './signed-url.js';
|
|
48
|
-
// Bun-specific storages (top-level `bun:sqlite`): re-exported for Bun/Node
|
|
49
|
-
// hosts. Workers/edge builds tree-shake them (and their `bun:sqlite` import)
|
|
50
|
-
// away — the runtime-neutral core closure is enforced by
|
|
51
|
-
// `test/runtime-neutrality.test.ts`.
|
|
52
|
-
export * from './sqlite-blob-store.js';
|
|
53
48
|
export * from './sqlite-dialect.js';
|
|
54
|
-
export * from './sqlite-
|
|
49
|
+
export * from './sqlite-driver.js';
|
|
50
|
+
export { IMAGE_METADATA_TABLE, IMAGE_VERSION_COLUMN, } from './sqlite-image.js';
|
|
51
|
+
export * from './sqlite-blob-store.js';
|
|
55
52
|
export * from './sqlite-lease-store.js';
|
|
56
53
|
export * from './sqlite-segment-store.js';
|
|
57
54
|
export * from './sqlite-storage.js';
|
package/dist/pull.js
CHANGED
|
@@ -19,7 +19,7 @@ async function resolveImageBuilder(ctx) {
|
|
|
19
19
|
if (cachedDefaultBuilder === undefined) {
|
|
20
20
|
const hasBun = globalThis.Bun !== undefined;
|
|
21
21
|
cachedDefaultBuilder = hasBun
|
|
22
|
-
? (await import('./sqlite-
|
|
22
|
+
? (await import('./sqlite-bun.js')).buildSqliteImage
|
|
23
23
|
: null;
|
|
24
24
|
}
|
|
25
25
|
return cachedDefaultBuilder ?? undefined;
|
|
@@ -1,16 +1,11 @@
|
|
|
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.js';
|
|
5
|
+
import { type SqliteDatabase } from './sqlite-driver.js';
|
|
11
6
|
export declare class SqliteBlobStore implements BlobStore {
|
|
12
|
-
readonly db:
|
|
13
|
-
constructor(db?:
|
|
7
|
+
readonly db: SqliteDatabase;
|
|
8
|
+
constructor(db?: SqliteDatabase | string);
|
|
14
9
|
put(partition: string, blobId: string, bytes: Uint8Array, nowMs: number, mediaType?: string): Promise<BlobRecord>;
|
|
15
10
|
has(partition: string, blobId: string): Promise<boolean>;
|
|
16
11
|
get(partition: string, blobId: string): Promise<{
|
|
@@ -1,16 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
* SQLite-backed blob store via `bun:sqlite` (dev/bench convenience,
|
|
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`).
|
|
8
|
-
*/
|
|
9
|
-
import { Database } from 'bun:sqlite';
|
|
1
|
+
import { SqliteAdapterRequiredError, } from './sqlite-driver.js';
|
|
10
2
|
export class SqliteBlobStore {
|
|
11
3
|
db;
|
|
12
4
|
constructor(db = ':memory:') {
|
|
13
|
-
|
|
5
|
+
if (typeof db === 'string') {
|
|
6
|
+
throw new SqliteAdapterRequiredError();
|
|
7
|
+
}
|
|
8
|
+
this.db = db;
|
|
14
9
|
this.db.exec(`
|
|
15
10
|
CREATE TABLE IF NOT EXISTS sync_blobs(
|
|
16
11
|
partition TEXT NOT NULL, blob_id TEXT NOT NULL,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import type { SqliteDatabase, SqliteRunResult, SqliteStatement, SqliteValue } from './sqlite-driver.js';
|
|
3
|
+
export declare class BunSqliteDatabase implements SqliteDatabase {
|
|
4
|
+
readonly native: Database;
|
|
5
|
+
constructor(path?: string);
|
|
6
|
+
exec(sql: string): void;
|
|
7
|
+
run(sql: string, bindings?: readonly SqliteValue[]): SqliteRunResult;
|
|
8
|
+
query<Row, Params extends readonly SqliteValue[]>(sql: string): SqliteStatement<Row, Params>;
|
|
9
|
+
serialize(): Uint8Array;
|
|
10
|
+
close(): void;
|
|
11
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
export class BunSqliteDatabase {
|
|
3
|
+
native;
|
|
4
|
+
constructor(path = ':memory:') {
|
|
5
|
+
this.native = new Database(path);
|
|
6
|
+
}
|
|
7
|
+
exec(sql) {
|
|
8
|
+
this.native.exec(sql);
|
|
9
|
+
}
|
|
10
|
+
run(sql, bindings = []) {
|
|
11
|
+
return this.native.run(sql, [...bindings]);
|
|
12
|
+
}
|
|
13
|
+
query(sql) {
|
|
14
|
+
const statement = this.native.query(sql);
|
|
15
|
+
return {
|
|
16
|
+
run: (...params) => statement.run(...params),
|
|
17
|
+
get: (...params) => statement.get(...params),
|
|
18
|
+
all: (...params) => statement.all(...params),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
serialize() {
|
|
22
|
+
return new Uint8Array(this.native.serialize());
|
|
23
|
+
}
|
|
24
|
+
close() {
|
|
25
|
+
this.native.close();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { SqliteBlobStore as SharedSqliteBlobStore } from './sqlite-blob-store.js';
|
|
2
|
+
import type { SqliteDatabase } from './sqlite-driver.js';
|
|
3
|
+
import { type SqliteImageBuilder } from './sqlite-image.js';
|
|
4
|
+
import { SqliteLeaseStore as SharedSqliteLeaseStore } from './sqlite-lease-store.js';
|
|
5
|
+
import { SqliteSegmentStore as SharedSqliteSegmentStore } from './sqlite-segment-store.js';
|
|
6
|
+
import { SqliteServerStorage as SharedSqliteServerStorage } from './sqlite-storage.js';
|
|
7
|
+
export declare class SqliteServerStorage extends SharedSqliteServerStorage {
|
|
8
|
+
constructor(value?: SqliteDatabase | string);
|
|
9
|
+
}
|
|
10
|
+
export declare class SqliteSegmentStore extends SharedSqliteSegmentStore {
|
|
11
|
+
constructor(value?: SqliteDatabase | string, options?: {
|
|
12
|
+
ttlMs?: number;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
export declare class SqliteBlobStore extends SharedSqliteBlobStore {
|
|
16
|
+
constructor(value?: SqliteDatabase | string);
|
|
17
|
+
}
|
|
18
|
+
export declare class SqliteLeaseStore extends SharedSqliteLeaseStore {
|
|
19
|
+
constructor(value?: SqliteDatabase | string, options?: {
|
|
20
|
+
readonly leaseId?: () => string;
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export declare const buildSqliteImage: SqliteImageBuilder;
|
|
24
|
+
export { BunSqliteDatabase } from './sqlite-bun-driver.js';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { SqliteBlobStore as SharedSqliteBlobStore } from './sqlite-blob-store.js';
|
|
2
|
+
import { BunSqliteDatabase } from './sqlite-bun-driver.js';
|
|
3
|
+
import { writeSqliteImage } from './sqlite-image.js';
|
|
4
|
+
import { SqliteLeaseStore as SharedSqliteLeaseStore } from './sqlite-lease-store.js';
|
|
5
|
+
import { SqliteSegmentStore as SharedSqliteSegmentStore } from './sqlite-segment-store.js';
|
|
6
|
+
import { SqliteServerStorage as SharedSqliteServerStorage } from './sqlite-storage.js';
|
|
7
|
+
function database(value) {
|
|
8
|
+
return typeof value === 'string' ? new BunSqliteDatabase(value) : value;
|
|
9
|
+
}
|
|
10
|
+
export class SqliteServerStorage extends SharedSqliteServerStorage {
|
|
11
|
+
constructor(value = ':memory:') {
|
|
12
|
+
super(database(value));
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export class SqliteSegmentStore extends SharedSqliteSegmentStore {
|
|
16
|
+
constructor(value = ':memory:', options) {
|
|
17
|
+
super(database(value), options);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export class SqliteBlobStore extends SharedSqliteBlobStore {
|
|
21
|
+
constructor(value = ':memory:') {
|
|
22
|
+
super(database(value));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export class SqliteLeaseStore extends SharedSqliteLeaseStore {
|
|
26
|
+
constructor(value = ':memory:', options) {
|
|
27
|
+
super(database(value), options);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export const buildSqliteImage = (input) => {
|
|
31
|
+
const db = new BunSqliteDatabase();
|
|
32
|
+
try {
|
|
33
|
+
writeSqliteImage(db, input);
|
|
34
|
+
return db.serialize();
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
db.close();
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
export { BunSqliteDatabase } from './sqlite-bun-driver.js';
|
package/dist/sqlite-dialect.d.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
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import type { ScopeMap } from '@syncular/core';
|
|
19
19
|
import type { StoredChange, StoredCommit, StoredPushResult, StoredRow } from './storage.js';
|
|
20
20
|
/**
|
|
21
|
-
* Schema DDL
|
|
21
|
+
* Schema DDL, one statement per `;`-delimited chunk. Native SQLite applies
|
|
22
22
|
* the whole string via `db.exec(SQLITE_DDL)`; D1 applies each statement
|
|
23
23
|
* separately (its `prepare`/`batch` API is one statement per call). Types
|
|
24
24
|
* are SQLite's: `INTEGER`/`TEXT`/`BLOB`. Scopes are stored as JSON `TEXT`
|
|
@@ -53,7 +53,7 @@ export interface SqliteChangeRecord {
|
|
|
53
53
|
scopes: string;
|
|
54
54
|
payload: Uint8Array | null;
|
|
55
55
|
}
|
|
56
|
-
/**
|
|
56
|
+
/** Native SQLite returns `Uint8Array`; D1 returns `ArrayBuffer` for BLOBs. */
|
|
57
57
|
export declare function asUint8Array(value: unknown): Uint8Array;
|
|
58
58
|
export declare function toStoredRow(record: SqliteRowRecord): StoredRow;
|
|
59
59
|
export declare function toStoredChange(record: SqliteChangeRecord): StoredChange;
|
|
@@ -61,7 +61,7 @@ export declare function toStoredChange(record: SqliteChangeRecord): StoredChange
|
|
|
61
61
|
* One result row of `commitWindowPageSql` (candidate LEFT JOIN commit meta
|
|
62
62
|
* LEFT JOIN changes): meta/change columns are NULL when the joined row
|
|
63
63
|
* vanished (see the builder's LEFT JOIN contract). `payload` is a BLOB —
|
|
64
|
-
*
|
|
64
|
+
* Native SQLite hands back `Uint8Array`, D1 `ArrayBuffer`; `toStoredChange`
|
|
65
65
|
* normalizes via `asUint8Array`.
|
|
66
66
|
*/
|
|
67
67
|
export interface SqliteCommitWindowRecord {
|
package/dist/sqlite-dialect.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { matchesEffective } from './scopes.js';
|
|
2
2
|
/**
|
|
3
|
-
* Schema DDL
|
|
3
|
+
* Schema DDL, one statement per `;`-delimited chunk. Native SQLite applies
|
|
4
4
|
* the whole string via `db.exec(SQLITE_DDL)`; D1 applies each statement
|
|
5
5
|
* separately (its `prepare`/`batch` API is one statement per call). Types
|
|
6
6
|
* are SQLite's: `INTEGER`/`TEXT`/`BLOB`. Scopes are stored as JSON `TEXT`
|
|
@@ -181,7 +181,7 @@ export function deserializePushResult(text) {
|
|
|
181
181
|
results,
|
|
182
182
|
};
|
|
183
183
|
}
|
|
184
|
-
/**
|
|
184
|
+
/** Native SQLite returns `Uint8Array`; D1 returns `ArrayBuffer` for BLOBs. */
|
|
185
185
|
export function asUint8Array(value) {
|
|
186
186
|
if (value instanceof Uint8Array)
|
|
187
187
|
return value;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Values accepted by the synchronous SQLite adapters. */
|
|
2
|
+
export type SqliteValue = string | number | bigint | boolean | Uint8Array | null;
|
|
3
|
+
/** Result of a SQLite statement that does not return rows. */
|
|
4
|
+
export interface SqliteRunResult {
|
|
5
|
+
readonly changes: number | bigint;
|
|
6
|
+
readonly lastInsertRowid: number | bigint;
|
|
7
|
+
}
|
|
8
|
+
/** Prepared synchronous SQLite statement used by the server stores. */
|
|
9
|
+
export interface SqliteStatement<Row, Params extends readonly SqliteValue[]> {
|
|
10
|
+
run(...params: Params): SqliteRunResult;
|
|
11
|
+
get(...params: Params): Row | null;
|
|
12
|
+
all(...params: Params): Row[];
|
|
13
|
+
}
|
|
14
|
+
/** Runtime-neutral database surface shared by the Bun and Node adapters. */
|
|
15
|
+
export interface SqliteDatabase {
|
|
16
|
+
exec(sql: string): void;
|
|
17
|
+
run(sql: string, bindings?: readonly SqliteValue[]): SqliteRunResult;
|
|
18
|
+
query<Row = Record<string, SqliteValue>, Params extends readonly SqliteValue[] = SqliteValue[]>(sql: string): SqliteStatement<Row, Params>;
|
|
19
|
+
close(): void;
|
|
20
|
+
}
|
|
21
|
+
/** Raised when a neutral-runtime import is used without a SQLite adapter. */
|
|
22
|
+
export declare class SqliteAdapterRequiredError extends Error {
|
|
23
|
+
readonly name = "SqliteAdapterRequiredError";
|
|
24
|
+
readonly code = "sync.sqlite_adapter_required";
|
|
25
|
+
constructor();
|
|
26
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Raised when a neutral-runtime import is used without a SQLite adapter. */
|
|
2
|
+
export class SqliteAdapterRequiredError extends Error {
|
|
3
|
+
name = 'SqliteAdapterRequiredError';
|
|
4
|
+
code = 'sync.sqlite_adapter_required';
|
|
5
|
+
constructor() {
|
|
6
|
+
super('SQLite paths require the @syncular/server/sqlite runtime adapter');
|
|
7
|
+
}
|
|
8
|
+
}
|
package/dist/sqlite-image.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CompiledTable } from './schema.js';
|
|
2
|
+
import type { SqliteDatabase } from './sqlite-driver.js';
|
|
2
3
|
import type { StoredRow } from './storage.js';
|
|
3
4
|
/** The §5.6 version column as it appears inside a sqlite image (§5.3). */
|
|
4
5
|
export declare const IMAGE_VERSION_COLUMN = "_syncular_version";
|
|
@@ -14,14 +15,11 @@ export interface SqliteImageInput {
|
|
|
14
15
|
/**
|
|
15
16
|
* The §5.3 image-builder capability, injected through
|
|
16
17
|
* `SyncServerConfig.sqliteImageBuilder`. Building an image needs
|
|
17
|
-
* a real SQLite engine
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* pull serves the rows lane (§5.3 clients advertise sqlite as an *accept*,
|
|
22
|
-
* never a requirement — the host chooses the served format from what it can
|
|
23
|
-
* produce; this is a support floor, not a fallback).
|
|
18
|
+
* a real SQLite engine, which is not available on every runtime. The core
|
|
19
|
+
* takes the builder as an optional capability rather than importing a driver
|
|
20
|
+
* on the pull path. A Bun or Node host passes `buildSqliteImage`; a Workers
|
|
21
|
+
* host omits it and serves the rows lane.
|
|
24
22
|
*/
|
|
25
23
|
export type SqliteImageBuilder = (input: SqliteImageInput) => Uint8Array;
|
|
26
|
-
/**
|
|
27
|
-
export declare
|
|
24
|
+
/** Populate a §5.3 image database for a whole-table snapshot. */
|
|
25
|
+
export declare function writeSqliteImage(db: SqliteDatabase, input: SqliteImageInput): void;
|
package/dist/sqlite-image.js
CHANGED
|
@@ -2,13 +2,12 @@
|
|
|
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 } from '@syncular/core';
|
|
13
12
|
/** The §5.6 version column as it appears inside a sqlite image (§5.3). */
|
|
14
13
|
export const IMAGE_VERSION_COLUMN = '_syncular_version';
|
|
@@ -45,39 +44,38 @@ function toSql(value) {
|
|
|
45
44
|
return value ? 1 : 0;
|
|
46
45
|
return value;
|
|
47
46
|
}
|
|
48
|
-
/**
|
|
49
|
-
export
|
|
47
|
+
/** Populate a §5.3 image database for a whole-table snapshot. */
|
|
48
|
+
export function writeSqliteImage(db, input) {
|
|
50
49
|
const { table, rows } = input;
|
|
51
50
|
const primaryKey = table.columns[table.primaryKeyIndex]?.name;
|
|
52
|
-
const
|
|
51
|
+
const columnDefs = table.columns.map((column) => {
|
|
52
|
+
const notNull = column.nullable ? '' : ' NOT NULL';
|
|
53
|
+
const pk = column.name === primaryKey ? ' PRIMARY KEY' : '';
|
|
54
|
+
return `${quoteIdent(column.name)} ${sqlType(column)}${notNull}${pk}`;
|
|
55
|
+
});
|
|
56
|
+
columnDefs.push(`${quoteIdent(IMAGE_VERSION_COLUMN)} INTEGER NOT NULL`);
|
|
57
|
+
db.exec(`CREATE TABLE ${quoteIdent(table.name)} (${columnDefs.join(', ')})`);
|
|
58
|
+
db.exec(`CREATE TABLE ${IMAGE_METADATA_TABLE} (
|
|
59
|
+
format INTEGER NOT NULL, "table" TEXT NOT NULL,
|
|
60
|
+
"schemaVersion" INTEGER NOT NULL, "asOfCommitSeq" INTEGER NOT NULL,
|
|
61
|
+
"scopeDigest" TEXT NOT NULL, "rowCount" INTEGER NOT NULL)`);
|
|
62
|
+
db.query(`INSERT INTO ${IMAGE_METADATA_TABLE} VALUES (1, ?, ?, ?, ?, ?)`).run(table.name, input.schemaVersion, input.asOfCommitSeq, input.scopeDigest, rows.length);
|
|
63
|
+
const names = [
|
|
64
|
+
...table.columns.map((column) => quoteIdent(column.name)),
|
|
65
|
+
quoteIdent(IMAGE_VERSION_COLUMN),
|
|
66
|
+
];
|
|
67
|
+
const insert = db.query(`INSERT INTO ${quoteIdent(table.name)} (${names.join(', ')})
|
|
68
|
+
VALUES (${names.map(() => '?').join(', ')})`);
|
|
69
|
+
db.exec('BEGIN');
|
|
53
70
|
try {
|
|
54
|
-
const columnDefs = table.columns.map((column) => {
|
|
55
|
-
const notNull = column.nullable ? '' : ' NOT NULL';
|
|
56
|
-
const pk = column.name === primaryKey ? ' PRIMARY KEY' : '';
|
|
57
|
-
return `${quoteIdent(column.name)} ${sqlType(column)}${notNull}${pk}`;
|
|
58
|
-
});
|
|
59
|
-
columnDefs.push(`${quoteIdent(IMAGE_VERSION_COLUMN)} INTEGER NOT NULL`);
|
|
60
|
-
db.exec(`CREATE TABLE ${quoteIdent(table.name)} (${columnDefs.join(', ')})`);
|
|
61
|
-
db.exec(`CREATE TABLE ${IMAGE_METADATA_TABLE} (
|
|
62
|
-
format INTEGER NOT NULL, "table" TEXT NOT NULL,
|
|
63
|
-
"schemaVersion" INTEGER NOT NULL, "asOfCommitSeq" INTEGER NOT NULL,
|
|
64
|
-
"scopeDigest" TEXT NOT NULL, "rowCount" INTEGER NOT NULL)`);
|
|
65
|
-
db.query(`INSERT INTO ${IMAGE_METADATA_TABLE} VALUES (1, ?, ?, ?, ?, ?)`).run(table.name, input.schemaVersion, input.asOfCommitSeq, input.scopeDigest, rows.length);
|
|
66
|
-
const names = [
|
|
67
|
-
...table.columns.map((column) => quoteIdent(column.name)),
|
|
68
|
-
quoteIdent(IMAGE_VERSION_COLUMN),
|
|
69
|
-
];
|
|
70
|
-
const insert = db.query(`INSERT INTO ${quoteIdent(table.name)} (${names.join(', ')})
|
|
71
|
-
VALUES (${names.map(() => '?').join(', ')})`);
|
|
72
|
-
db.exec('BEGIN');
|
|
73
71
|
for (const row of rows) {
|
|
74
72
|
const values = decodeRow(table.columns, row.payload);
|
|
75
73
|
insert.run(...values.map(toSql), row.serverVersion);
|
|
76
74
|
}
|
|
77
75
|
db.exec('COMMIT');
|
|
78
|
-
return new Uint8Array(db.serialize());
|
|
79
76
|
}
|
|
80
|
-
|
|
81
|
-
db.
|
|
77
|
+
catch (error) {
|
|
78
|
+
db.exec('ROLLBACK');
|
|
79
|
+
throw error;
|
|
82
80
|
}
|
|
83
|
-
}
|
|
81
|
+
}
|
|
@@ -1,18 +1,13 @@
|
|
|
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.js';
|
|
6
|
+
import { type SqliteDatabase } from './sqlite-driver.js';
|
|
12
7
|
export declare class SqliteLeaseStore implements LeaseStore {
|
|
13
8
|
#private;
|
|
14
|
-
readonly db:
|
|
15
|
-
constructor(db?:
|
|
9
|
+
readonly db: SqliteDatabase;
|
|
10
|
+
constructor(db?: SqliteDatabase | string, options?: {
|
|
16
11
|
readonly leaseId?: LeaseIdFactory;
|
|
17
12
|
});
|
|
18
13
|
get(partition: string, clientId: string): Promise<LeaseRecord | undefined>;
|
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* SQLite-backed auth-lease store via `bun:sqlite` (§7.3.1 reference 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`).
|
|
8
|
-
*/
|
|
9
|
-
import { Database } from 'bun:sqlite';
|
|
1
|
+
import { SqliteAdapterRequiredError, } from './sqlite-driver.js';
|
|
10
2
|
function defaultLeaseId() {
|
|
11
3
|
return `lease_${crypto.randomUUID()}`;
|
|
12
4
|
}
|
|
@@ -14,7 +6,10 @@ export class SqliteLeaseStore {
|
|
|
14
6
|
db;
|
|
15
7
|
#newId;
|
|
16
8
|
constructor(db = ':memory:', options) {
|
|
17
|
-
|
|
9
|
+
if (typeof db === 'string') {
|
|
10
|
+
throw new SqliteAdapterRequiredError();
|
|
11
|
+
}
|
|
12
|
+
this.db = db;
|
|
18
13
|
this.#newId = options?.leaseId ?? defaultLeaseId;
|
|
19
14
|
this.db.exec(`
|
|
20
15
|
CREATE TABLE IF NOT EXISTS sync_leases(
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import type { SqliteDatabase, SqliteRunResult, SqliteStatement, SqliteValue } from './sqlite-driver.js';
|
|
3
|
+
export declare class NodeSqliteDatabase implements SqliteDatabase {
|
|
4
|
+
readonly native: DatabaseSync;
|
|
5
|
+
constructor(path?: string);
|
|
6
|
+
exec(sql: string): void;
|
|
7
|
+
run(sql: string, bindings?: readonly SqliteValue[]): SqliteRunResult;
|
|
8
|
+
query<Row, Params extends readonly SqliteValue[]>(sql: string): SqliteStatement<Row, Params>;
|
|
9
|
+
close(): void;
|
|
10
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
function bind(value) {
|
|
3
|
+
return typeof value === 'boolean' ? (value ? 1 : 0) : value;
|
|
4
|
+
}
|
|
5
|
+
function row(value) {
|
|
6
|
+
return (value ?? null);
|
|
7
|
+
}
|
|
8
|
+
export class NodeSqliteDatabase {
|
|
9
|
+
native;
|
|
10
|
+
constructor(path = ':memory:') {
|
|
11
|
+
this.native = new DatabaseSync(path);
|
|
12
|
+
}
|
|
13
|
+
exec(sql) {
|
|
14
|
+
this.native.exec(sql);
|
|
15
|
+
}
|
|
16
|
+
run(sql, bindings = []) {
|
|
17
|
+
return this.native.prepare(sql).run(...bindings.map(bind));
|
|
18
|
+
}
|
|
19
|
+
query(sql) {
|
|
20
|
+
const statement = this.native.prepare(sql);
|
|
21
|
+
return {
|
|
22
|
+
run: (...params) => statement.run(...params.map(bind)),
|
|
23
|
+
get: (...params) => row(statement.get(...params.map(bind))),
|
|
24
|
+
all: (...params) => statement.all(...params.map(bind)),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
close() {
|
|
28
|
+
this.native.close();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { SqliteBlobStore as SharedSqliteBlobStore } from './sqlite-blob-store.js';
|
|
2
|
+
import type { SqliteDatabase } from './sqlite-driver.js';
|
|
3
|
+
import { type SqliteImageBuilder } from './sqlite-image.js';
|
|
4
|
+
import { SqliteLeaseStore as SharedSqliteLeaseStore } from './sqlite-lease-store.js';
|
|
5
|
+
import { SqliteSegmentStore as SharedSqliteSegmentStore } from './sqlite-segment-store.js';
|
|
6
|
+
import { SqliteServerStorage as SharedSqliteServerStorage } from './sqlite-storage.js';
|
|
7
|
+
export declare class SqliteServerStorage extends SharedSqliteServerStorage {
|
|
8
|
+
constructor(value?: SqliteDatabase | string);
|
|
9
|
+
}
|
|
10
|
+
export declare class SqliteSegmentStore extends SharedSqliteSegmentStore {
|
|
11
|
+
constructor(value?: SqliteDatabase | string, options?: {
|
|
12
|
+
ttlMs?: number;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
export declare class SqliteBlobStore extends SharedSqliteBlobStore {
|
|
16
|
+
constructor(value?: SqliteDatabase | string);
|
|
17
|
+
}
|
|
18
|
+
export declare class SqliteLeaseStore extends SharedSqliteLeaseStore {
|
|
19
|
+
constructor(value?: SqliteDatabase | string, options?: {
|
|
20
|
+
readonly leaseId?: () => string;
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export declare const buildSqliteImage: SqliteImageBuilder;
|
|
24
|
+
export { NodeSqliteDatabase } from './sqlite-node-driver.js';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { SqliteBlobStore as SharedSqliteBlobStore } from './sqlite-blob-store.js';
|
|
5
|
+
import { writeSqliteImage } from './sqlite-image.js';
|
|
6
|
+
import { SqliteLeaseStore as SharedSqliteLeaseStore } from './sqlite-lease-store.js';
|
|
7
|
+
import { NodeSqliteDatabase } from './sqlite-node-driver.js';
|
|
8
|
+
import { SqliteSegmentStore as SharedSqliteSegmentStore } from './sqlite-segment-store.js';
|
|
9
|
+
import { SqliteServerStorage as SharedSqliteServerStorage } from './sqlite-storage.js';
|
|
10
|
+
function database(value) {
|
|
11
|
+
return typeof value === 'string' ? new NodeSqliteDatabase(value) : value;
|
|
12
|
+
}
|
|
13
|
+
export class SqliteServerStorage extends SharedSqliteServerStorage {
|
|
14
|
+
constructor(value = ':memory:') {
|
|
15
|
+
super(database(value));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export class SqliteSegmentStore extends SharedSqliteSegmentStore {
|
|
19
|
+
constructor(value = ':memory:', options) {
|
|
20
|
+
super(database(value), options);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export class SqliteBlobStore extends SharedSqliteBlobStore {
|
|
24
|
+
constructor(value = ':memory:') {
|
|
25
|
+
super(database(value));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export class SqliteLeaseStore extends SharedSqliteLeaseStore {
|
|
29
|
+
constructor(value = ':memory:', options) {
|
|
30
|
+
super(database(value), options);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export const buildSqliteImage = (input) => {
|
|
34
|
+
const directory = mkdtempSync(join(tmpdir(), 'syncular-server-image-'));
|
|
35
|
+
const path = join(directory, 'segment.db');
|
|
36
|
+
const db = new NodeSqliteDatabase(path);
|
|
37
|
+
try {
|
|
38
|
+
try {
|
|
39
|
+
writeSqliteImage(db, input);
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
db.close();
|
|
43
|
+
}
|
|
44
|
+
return new Uint8Array(readFileSync(path));
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
rmSync(directory, { recursive: true, force: true });
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
export { NodeSqliteDatabase } from './sqlite-node-driver.js';
|