@syncular/client 0.15.45 → 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 +35 -41
- package/dist/http.d.ts +5 -1
- package/dist/http.js +68 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/node-database.d.ts +6 -29
- package/dist/node-database.js +7 -69
- package/dist/query-guard.d.ts +2 -2
- package/dist/query-guard.js +2 -2
- package/dist/remote.d.ts +76 -0
- package/dist/remote.js +441 -0
- package/dist/sqlite-bun.d.ts +2 -0
- package/dist/sqlite-bun.js +4 -0
- package/dist/sqlite-node.d.ts +2 -0
- package/dist/sqlite-node.js +4 -0
- package/dist/transport.d.ts +11 -0
- package/package.json +12 -14
- package/src/http.ts +99 -7
- package/src/index.ts +2 -1
- package/src/node-database.ts +11 -108
- package/src/query-guard.ts +2 -2
- package/src/remote.ts +724 -0
- package/src/sqlite-bun.ts +6 -0
- package/src/sqlite-node.ts +6 -0
- package/src/transport.ts +19 -0
package/README.md
CHANGED
|
@@ -3,6 +3,18 @@
|
|
|
3
3
|
The TypeScript client protocol core (SPEC.md §§3–8, client side) plus its
|
|
4
4
|
browser platform bindings.
|
|
5
5
|
|
|
6
|
+
The normal `SyncClient` also runs in a CLI or background service.
|
|
7
|
+
Use `openSqliteDatabase(path)` from `@syncular/client/sqlite` for a persistent
|
|
8
|
+
local replica on Node or Bun. See the
|
|
9
|
+
[server-side sync client guide](https://syncular.dev/guide-server-clients/).
|
|
10
|
+
|
|
11
|
+
`SyncRemoteClient` is the database-less server client. It sends ordinary
|
|
12
|
+
push-only commits through `/sync` and can call registered typed queries,
|
|
13
|
+
server-authoritative commands, and live query watches through the remote
|
|
14
|
+
operation transport. See [remote server operations](https://syncular.dev/guide-remote-operations/).
|
|
15
|
+
Its schema and sync transport are optional for query-only or command-only
|
|
16
|
+
processes.
|
|
17
|
+
|
|
6
18
|
## Client-local FTS5 projections
|
|
7
19
|
|
|
8
20
|
Generated schemas may attach `ftsIndexes` to a synced table. The
|
|
@@ -459,10 +471,10 @@ directory or IndexedDB store. This is the pinned decision (SPEC §5.9.7 B1):
|
|
|
459
471
|
that pin them — a refcount adjust and a body insert/delete commit atomically,
|
|
460
472
|
so a crash never strands a body against a stale count.
|
|
461
473
|
- **Survives restarts for free.** The client DB already rides OPFS via the
|
|
462
|
-
sahpool VFS in the browser (and a plain file under `rusqlite
|
|
463
|
-
on native
|
|
464
|
-
eviction policy to keep coherent. Close the app, reopen
|
|
465
|
-
the cached body with no network.
|
|
474
|
+
sahpool VFS in the browser (and a plain file under `rusqlite`, `bun:sqlite`,
|
|
475
|
+
or `node:sqlite` on native runtimes), so there is no second persistence
|
|
476
|
+
surface and no second eviction policy to keep coherent. Close the app, reopen
|
|
477
|
+
it: `fetchBlob` serves the cached body with no network.
|
|
466
478
|
- **SQLite handles multi-MB images fine.** A page-cached `BLOB` read is a memory
|
|
467
479
|
copy, well within the image/document envelope this targets.
|
|
468
480
|
|
|
@@ -496,57 +508,38 @@ straight to a media element instead of pulling bytes through the cache — the
|
|
|
496
508
|
image-app default (refcounted `BLOB` cache) and the large-media path (presigned
|
|
497
509
|
URL, no byte cache) coexist per attachment.
|
|
498
510
|
|
|
499
|
-
## Node
|
|
511
|
+
## Node and Bun SQLite backend (`./sqlite`)
|
|
500
512
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
`openNodeDatabase`, a `ClientDatabase` over
|
|
504
|
-
[better-sqlite3](https://github.com/WiseLibs/better-sqlite3):
|
|
513
|
+
CLIs, background workers, Electron main processes, and services can use one
|
|
514
|
+
runtime-selected import:
|
|
505
515
|
|
|
506
516
|
```ts
|
|
507
|
-
import {
|
|
517
|
+
import { openSqliteDatabase } from '@syncular/client/sqlite';
|
|
508
518
|
import { SyncClient } from '@syncular/client';
|
|
509
519
|
|
|
510
|
-
const database =
|
|
520
|
+
const database = openSqliteDatabase('app.db'); // or ':memory:' (default)
|
|
511
521
|
const client = new SyncClient({ database, schema, /* … */ });
|
|
512
522
|
```
|
|
513
523
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
attach path is supported too, so a Node host can accept sqlite-image segments.
|
|
524
|
+
The export selects `bun:sqlite` on Bun and the built-in `node:sqlite` module on
|
|
525
|
+
Node 22.13 or newer. No SQLite package or native addon is required. Both
|
|
526
|
+
adapters support synchronous `exec`, `query`, nested transactions, boolean
|
|
527
|
+
bindings, `null`, `Uint8Array` BLOB values, and §5.3 SQLite-image attachment.
|
|
519
528
|
|
|
520
|
-
|
|
521
|
-
installs cleanly without it (browser-only apps never pay for a native build);
|
|
522
|
-
`openNodeDatabase()` loads it lazily on first call and throws a clear,
|
|
523
|
-
actionable error if the peer is missing. Add it in your app:
|
|
529
|
+
Runtime-specific imports remain available:
|
|
524
530
|
|
|
525
|
-
```
|
|
526
|
-
|
|
531
|
+
```ts
|
|
532
|
+
import { openBunDatabase } from '@syncular/client/bun';
|
|
533
|
+
import { openNodeDatabase } from '@syncular/client/node';
|
|
527
534
|
```
|
|
528
535
|
|
|
529
|
-
|
|
530
|
-
better-sqlite3 (`ERR_DLOPEN_FAILED`,
|
|
531
|
-
[oven-sh/bun#4290](https://github.com/oven-sh/bun/issues/4290)); calling
|
|
532
|
-
`openNodeDatabase()` under bun deliberately raises the same helpful error and
|
|
533
|
-
points you at `./bun` instead. So the bun test suite
|
|
534
|
-
(`test/node-database.test.ts`) proves what it can under bun — type/subpath
|
|
535
|
-
conformance, the missing-peer error, and that the shared behavioral contract
|
|
536
|
-
(`test/node-database/adapter-contract.ts`) passes on the reference bun:sqlite
|
|
537
|
-
backend — while the better-sqlite3 adapter's real behavior is proven under
|
|
538
|
-
**Node** against the actual native module by running that same contract:
|
|
536
|
+
The source and packed-package runtime contracts run under actual Node and Bun:
|
|
539
537
|
|
|
540
538
|
```sh
|
|
541
|
-
|
|
542
|
-
bun run verify:
|
|
539
|
+
bun run verify:runtimes
|
|
540
|
+
bun run build:packages && bun run verify:packages
|
|
543
541
|
```
|
|
544
542
|
|
|
545
|
-
That bundles the verifier with bun (transpile + resolve only — bun never
|
|
546
|
-
executes the native module) and runs the plain-JS bundle under Node, which
|
|
547
|
-
exercises `openNodeDatabase` against real better-sqlite3 and exits non-zero on
|
|
548
|
-
any divergence from the contract.
|
|
549
|
-
|
|
550
543
|
## RPC protocol (6 message types)
|
|
551
544
|
|
|
552
545
|
`init`, `call`, `ready`, `result`, `error`, `event` — every API method
|
|
@@ -562,8 +555,9 @@ they own their buffer.
|
|
|
562
555
|
| `.` | protocol core, transports, handle + RPC protocol (browser-safe, no SQLite) |
|
|
563
556
|
| `./worker` | `startSyncWorker` — worker-side bootstrap (pulls sqlite-wasm) |
|
|
564
557
|
| `./wasm` | sqlite-wasm bindings: `openPersistentWasmDatabase`, `openWasmDatabase` |
|
|
565
|
-
| `./
|
|
566
|
-
| `./
|
|
558
|
+
| `./sqlite` | Runtime-selected SQLite: `openSqliteDatabase` on Node or Bun |
|
|
559
|
+
| `./bun` | Explicit `bun:sqlite` binding: `openBunDatabase` |
|
|
560
|
+
| `./node` | Explicit built-in `node:sqlite` binding: `openNodeDatabase` |
|
|
567
561
|
|
|
568
562
|
Tests drive the real worker entry in a bun `Worker` with bun:sqlite
|
|
569
563
|
injected through the bootstrap's database-factory override
|
package/dist/http.d.ts
CHANGED
|
@@ -5,13 +5,17 @@
|
|
|
5
5
|
* (the loopback doctrine); the browser fixture exercises them in a real browser.
|
|
6
6
|
*/
|
|
7
7
|
import type { BlobTransport } from './blob.js';
|
|
8
|
-
import type { RealtimeConnector, SegmentDownloader, SyncTransport } from './transport.js';
|
|
8
|
+
import type { RealtimeConnector, RemoteOperationTransport, RemoteOperationRealtimeConnector, SegmentDownloader, SyncTransport } from './transport.js';
|
|
9
9
|
export interface HttpTransportOptions {
|
|
10
10
|
readonly headers?: Readonly<Record<string, string>>;
|
|
11
11
|
readonly fetch?: typeof fetch;
|
|
12
12
|
}
|
|
13
13
|
/** POST `<mount>/sync` with SSP2 bodies (§1.1). */
|
|
14
14
|
export declare function httpSyncTransport(syncUrl: string, options?: HttpTransportOptions): SyncTransport;
|
|
15
|
+
/** POST one registered authoritative operation to `<mount>/operations`. */
|
|
16
|
+
export declare function httpRemoteOperationTransport(operationsUrl: string, options?: HttpTransportOptions): RemoteOperationTransport;
|
|
17
|
+
/** WebSocket connector for registered query snapshots. */
|
|
18
|
+
export declare function webSocketRemoteOperationConnector(realtimeUrl: string): RemoteOperationRealtimeConnector;
|
|
15
19
|
/**
|
|
16
20
|
* §5.5 direct endpoint with the `X-Syncular-Scopes` re-authorization
|
|
17
21
|
* header, plus the §5.4 `fetchUrl` capability (advertises accept bit 3).
|
package/dist/http.js
CHANGED
|
@@ -35,6 +35,60 @@ export function httpSyncTransport(syncUrl, options) {
|
|
|
35
35
|
return new Uint8Array(await response.arrayBuffer());
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
|
+
/** POST one registered authoritative operation to `<mount>/operations`. */
|
|
39
|
+
export function httpRemoteOperationTransport(operationsUrl, options) {
|
|
40
|
+
const doFetch = options?.fetch ?? fetch;
|
|
41
|
+
return async (request) => {
|
|
42
|
+
const response = await doFetch(operationsUrl, {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: {
|
|
45
|
+
'Content-Type': 'application/vnd.syncular.operations.v1+json',
|
|
46
|
+
...options?.headers,
|
|
47
|
+
},
|
|
48
|
+
body: request.slice().buffer,
|
|
49
|
+
});
|
|
50
|
+
if (!response.ok)
|
|
51
|
+
await throwHttpError(response);
|
|
52
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** WebSocket connector for registered query snapshots. */
|
|
56
|
+
export function webSocketRemoteOperationConnector(realtimeUrl) {
|
|
57
|
+
return (handlers) => new Promise((resolve, reject) => {
|
|
58
|
+
const socket = new WebSocket(realtimeUrl);
|
|
59
|
+
let opened = false;
|
|
60
|
+
socket.binaryType = 'arraybuffer';
|
|
61
|
+
socket.onopen = () => {
|
|
62
|
+
opened = true;
|
|
63
|
+
resolve({
|
|
64
|
+
send: (bytes) => socket.send(bytes.slice().buffer),
|
|
65
|
+
close: () => socket.close(),
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
socket.onmessage = (event) => {
|
|
69
|
+
if (event.data instanceof ArrayBuffer) {
|
|
70
|
+
handlers.onMessage(new Uint8Array(event.data));
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
socket.onerror = () => {
|
|
74
|
+
if (!opened) {
|
|
75
|
+
reject(new ClientSyncError('sync.transport_failed', 'remote operation realtime socket failed to connect', true));
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
socket.close();
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
handlers.onClose?.();
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
socket.onclose = () => {
|
|
85
|
+
if (!opened) {
|
|
86
|
+
reject(new ClientSyncError('sync.transport_failed', 'remote operation realtime socket closed while connecting', true));
|
|
87
|
+
}
|
|
88
|
+
handlers.onClose?.();
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
}
|
|
38
92
|
/**
|
|
39
93
|
* §5.5 direct endpoint with the `X-Syncular-Scopes` re-authorization
|
|
40
94
|
* header, plus the §5.4 `fetchUrl` capability (advertises accept bit 3).
|
|
@@ -170,8 +224,10 @@ export function httpBlobTransport(blobsBaseUrl, options) {
|
|
|
170
224
|
export function webSocketRealtimeConnector(realtimeUrl) {
|
|
171
225
|
return (handlers) => new Promise((resolve, reject) => {
|
|
172
226
|
const socket = new WebSocket(realtimeUrl);
|
|
227
|
+
let opened = false;
|
|
173
228
|
socket.binaryType = 'arraybuffer';
|
|
174
229
|
socket.onopen = () => {
|
|
230
|
+
opened = true;
|
|
175
231
|
resolve({
|
|
176
232
|
send: (text) => socket.send(text),
|
|
177
233
|
sendBytes: (bytes) => {
|
|
@@ -187,9 +243,20 @@ export function webSocketRealtimeConnector(realtimeUrl) {
|
|
|
187
243
|
handlers.onBinary(new Uint8Array(event.data));
|
|
188
244
|
};
|
|
189
245
|
socket.onerror = () => {
|
|
190
|
-
|
|
246
|
+
if (!opened) {
|
|
247
|
+
reject(new ClientSyncError('sync.transport_failed', 'realtime socket failed to connect', true));
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
socket.close();
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
handlers.onClose?.();
|
|
254
|
+
}
|
|
191
255
|
};
|
|
192
256
|
socket.onclose = () => {
|
|
257
|
+
if (!opened) {
|
|
258
|
+
reject(new ClientSyncError('sync.transport_failed', 'realtime socket closed while connecting', true));
|
|
259
|
+
}
|
|
193
260
|
handlers.onClose?.();
|
|
194
261
|
};
|
|
195
262
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* SPEC.md is normative.
|
|
4
4
|
*
|
|
5
5
|
* Browser-safe root: database backends live behind subpath exports
|
|
6
|
-
* (`./
|
|
6
|
+
* (`./sqlite` for Node or Bun, `./wasm` for sqlite-wasm + OPFS); the
|
|
7
7
|
* worker-side bootstrap lives behind `./worker`. The main-thread handle
|
|
8
8
|
* (`worker-host`) and the RPC protocol types are root exports — they
|
|
9
9
|
* import no SQLite.
|
|
@@ -30,6 +30,7 @@ export * from './outbox.js';
|
|
|
30
30
|
export * from './outcomes.js';
|
|
31
31
|
export * from './query-guard.js';
|
|
32
32
|
export * from './reactive-store.js';
|
|
33
|
+
export * from './remote.js';
|
|
33
34
|
export * from './realtime-supervisor.js';
|
|
34
35
|
export * from './schema.js';
|
|
35
36
|
export * from './sql-tag.js';
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* SPEC.md is normative.
|
|
4
4
|
*
|
|
5
5
|
* Browser-safe root: database backends live behind subpath exports
|
|
6
|
-
* (`./
|
|
6
|
+
* (`./sqlite` for Node or Bun, `./wasm` for sqlite-wasm + OPFS); the
|
|
7
7
|
* worker-side bootstrap lives behind `./worker`. The main-thread handle
|
|
8
8
|
* (`worker-host`) and the RPC protocol types are root exports — they
|
|
9
9
|
* import no SQLite.
|
|
@@ -30,6 +30,7 @@ export * from './outbox.js';
|
|
|
30
30
|
export * from './outcomes.js';
|
|
31
31
|
export * from './query-guard.js';
|
|
32
32
|
export * from './reactive-store.js';
|
|
33
|
+
export * from './remote.js';
|
|
33
34
|
export * from './realtime-supervisor.js';
|
|
34
35
|
export * from './schema.js';
|
|
35
36
|
export * from './sql-tag.js';
|
package/dist/node-database.d.ts
CHANGED
|
@@ -1,41 +1,18 @@
|
|
|
1
|
-
import { type ClientDatabase, type SqlRow, type SqlValue } from './database.js';
|
|
2
1
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* package typechecks without the optional peer installed.
|
|
2
|
+
* `ClientDatabase` on Node's built-in `node:sqlite`. Semantics mirror the Bun
|
|
3
|
+
* adapter: synchronous queries, nested transactions, and SQLite image attach.
|
|
6
4
|
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
all(...params: NodeParam[]): unknown[];
|
|
10
|
-
}
|
|
11
|
-
interface BetterSqliteDatabase {
|
|
12
|
-
readonly inTransaction: boolean;
|
|
13
|
-
prepare(sql: string): BetterSqliteStatement;
|
|
14
|
-
exec(sql: string): unknown;
|
|
15
|
-
close(): void;
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* better-sqlite3 accepts string / number / bigint / null / Buffer|Uint8Array
|
|
19
|
-
* bind values, but NOT booleans (it throws "TypeError: can only bind …"). We
|
|
20
|
-
* coerce booleans to 0/1 exactly like the bun adapter so callers see one
|
|
21
|
-
* uniform bind contract across every backend.
|
|
22
|
-
*/
|
|
23
|
-
type NodeParam = string | number | bigint | Uint8Array | null;
|
|
5
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
6
|
+
import { type ClientDatabase, type SqlRow, type SqlValue } from './database.js';
|
|
24
7
|
export declare class NodeClientDatabase implements ClientDatabase {
|
|
25
8
|
#private;
|
|
26
|
-
readonly db:
|
|
9
|
+
readonly db: DatabaseSync;
|
|
27
10
|
constructor(path?: string);
|
|
28
11
|
exec(sql: string, params?: readonly SqlValue[]): void;
|
|
29
12
|
query(sql: string, params?: readonly SqlValue[]): SqlRow[];
|
|
30
13
|
transaction<T>(fn: () => T): T;
|
|
31
|
-
/**
|
|
32
|
-
* §5.3 image import: better-sqlite3 (like bun:sqlite) attaches files, not
|
|
33
|
-
* buffers, so the image lands in a private temp file for the duration of
|
|
34
|
-
* the ATTACH. Must be called outside any open transaction (SQLite cannot
|
|
35
|
-
* ATTACH inside one).
|
|
36
|
-
*/
|
|
14
|
+
/** §5.3 image import through a private file attached for one callback. */
|
|
37
15
|
withSqliteImage<T>(bytes: Uint8Array, alias: string, fn: () => T): T;
|
|
38
16
|
close(): void;
|
|
39
17
|
}
|
|
40
18
|
export declare function openNodeDatabase(path?: string): ClientDatabase;
|
|
41
|
-
export {};
|
package/dist/node-database.js
CHANGED
|
@@ -1,23 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `ClientDatabase` on
|
|
3
|
-
*
|
|
4
|
-
* exactly (synchronous exec/query/transaction with the shared savepoint
|
|
5
|
-
* helper, and the same §5.3 sqlite-image ATTACH path), so the core behaves
|
|
6
|
-
* identically whether it runs on bun:sqlite (tests), sqlite-wasm (browser)
|
|
7
|
-
* or better-sqlite3 (Node/Electron-main).
|
|
8
|
-
*
|
|
9
|
-
* better-sqlite3 is an OPTIONAL peer dependency, not a hard one: the package
|
|
10
|
-
* installs cleanly without it and this module errors helpfully only when a
|
|
11
|
-
* host actually calls `openNodeDatabase()` without having installed the peer.
|
|
12
|
-
* Not exported from the package root, so browser/bun entries never resolve
|
|
13
|
-
* the native module. Subpath export: `@syncular/client/node`.
|
|
14
|
-
*
|
|
15
|
-
* bun CANNOT dlopen better-sqlite3 (ERR_DLOPEN_FAILED, oven-sh/bun#4290), so
|
|
16
|
-
* this adapter is verified under real Node — see the README "Electron-main /
|
|
17
|
-
* plain-Node" section for the one-command recipe and `test/node-database`.
|
|
2
|
+
* `ClientDatabase` on Node's built-in `node:sqlite`. Semantics mirror the Bun
|
|
3
|
+
* adapter: synchronous queries, nested transactions, and SQLite image attach.
|
|
18
4
|
*/
|
|
5
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
19
6
|
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
20
|
-
import { createRequire } from 'node:module';
|
|
21
7
|
import { tmpdir } from 'node:os';
|
|
22
8
|
import { join } from 'node:path';
|
|
23
9
|
import { assertImageAlias, runTransaction, } from './database.js';
|
|
@@ -28,19 +14,12 @@ function coerceParams(params) {
|
|
|
28
14
|
return value;
|
|
29
15
|
});
|
|
30
16
|
}
|
|
31
|
-
/**
|
|
32
|
-
* better-sqlite3 returns BLOB columns as Node `Buffer`s. A Buffer IS a
|
|
33
|
-
* Uint8Array subclass, but it can be a view onto a shared pool buffer, so we
|
|
34
|
-
* normalize to a standalone Uint8Array — matching what bun:sqlite hands back
|
|
35
|
-
* and keeping the buffer-ownership assumptions elsewhere (worker transfer,
|
|
36
|
-
* structured clone) honest.
|
|
37
|
-
*/
|
|
38
17
|
function normalizeRow(row) {
|
|
39
18
|
const out = {};
|
|
40
19
|
for (const key in row) {
|
|
41
20
|
const value = row[key];
|
|
42
|
-
if (
|
|
43
|
-
out[key] = new Uint8Array(value);
|
|
21
|
+
if (value instanceof Uint8Array) {
|
|
22
|
+
out[key] = new Uint8Array(value);
|
|
44
23
|
}
|
|
45
24
|
else {
|
|
46
25
|
out[key] = value;
|
|
@@ -48,47 +27,11 @@ function normalizeRow(row) {
|
|
|
48
27
|
}
|
|
49
28
|
return out;
|
|
50
29
|
}
|
|
51
|
-
/**
|
|
52
|
-
* Load the optional peer AND open the database in one guarded step, so BOTH
|
|
53
|
-
* failure modes are turned into a clear, actionable error rather than a raw
|
|
54
|
-
* one:
|
|
55
|
-
*
|
|
56
|
-
* - `require('better-sqlite3')` throwing MODULE_NOT_FOUND — the peer is not
|
|
57
|
-
* installed (the common browser-only-host case), and
|
|
58
|
-
* - `new Database()` throwing ERR_DLOPEN_FAILED — the module resolves but the
|
|
59
|
-
* native addon cannot load, which is exactly what bun does for
|
|
60
|
-
* better-sqlite3 (oven-sh/bun#4290); the addon only dlopens at construction.
|
|
61
|
-
*/
|
|
62
|
-
function openBetterSqlite(path) {
|
|
63
|
-
const require = createRequire(import.meta.url);
|
|
64
|
-
try {
|
|
65
|
-
const mod = require('better-sqlite3');
|
|
66
|
-
const Database = mod.default ??
|
|
67
|
-
mod;
|
|
68
|
-
return new Database(path);
|
|
69
|
-
}
|
|
70
|
-
catch (error) {
|
|
71
|
-
const code = error?.code;
|
|
72
|
-
if (code === 'ERR_DLOPEN_FAILED') {
|
|
73
|
-
throw new Error("openNodeDatabase() requires the 'better-sqlite3' native module, but " +
|
|
74
|
-
'it failed to load. This most commonly means you are running under ' +
|
|
75
|
-
'bun, which cannot dlopen better-sqlite3 (oven-sh/bun#4290) — use ' +
|
|
76
|
-
"the bun:sqlite backend ('@syncular/client/bun') under bun, " +
|
|
77
|
-
"and reserve '@syncular/client/node' for Node/Electron-main. " +
|
|
78
|
-
`Underlying error: ${String(error)}`);
|
|
79
|
-
}
|
|
80
|
-
throw new Error('openNodeDatabase() requires the optional peer dependency ' +
|
|
81
|
-
"'better-sqlite3', which is not installed. Add it to your app " +
|
|
82
|
-
'(`npm install better-sqlite3` / `bun add better-sqlite3`) — it is ' +
|
|
83
|
-
'kept optional so @syncular/client installs without a native ' +
|
|
84
|
-
`build for browser-only hosts. Underlying error: ${String(error)}`);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
30
|
export class NodeClientDatabase {
|
|
88
31
|
db;
|
|
89
32
|
#tx = { depth: 0 };
|
|
90
33
|
constructor(path = ':memory:') {
|
|
91
|
-
this.db =
|
|
34
|
+
this.db = new DatabaseSync(path);
|
|
92
35
|
}
|
|
93
36
|
exec(sql, params = []) {
|
|
94
37
|
this.db.prepare(sql).run(...coerceParams(params));
|
|
@@ -100,12 +43,7 @@ export class NodeClientDatabase {
|
|
|
100
43
|
transaction(fn) {
|
|
101
44
|
return runTransaction(this.#tx, (sql) => this.db.exec(sql), fn);
|
|
102
45
|
}
|
|
103
|
-
/**
|
|
104
|
-
* §5.3 image import: better-sqlite3 (like bun:sqlite) attaches files, not
|
|
105
|
-
* buffers, so the image lands in a private temp file for the duration of
|
|
106
|
-
* the ATTACH. Must be called outside any open transaction (SQLite cannot
|
|
107
|
-
* ATTACH inside one).
|
|
108
|
-
*/
|
|
46
|
+
/** §5.3 image import through a private file attached for one callback. */
|
|
109
47
|
withSqliteImage(bytes, alias, fn) {
|
|
110
48
|
assertImageAlias(alias);
|
|
111
49
|
const dir = mkdtempSync(join(tmpdir(), 'syncular-image-'));
|
package/dist/query-guard.d.ts
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* bypasses the outbox (SPEC §7.1) and silently diverges from the
|
|
11
11
|
* server — writes MUST go through `client.mutate([...])`.
|
|
12
12
|
* 2. ONE STATEMENT. `sqlite-wasm`'s `exec` runs every statement in a
|
|
13
|
-
* multi-statement string (`SELECT 1; DROP TABLE t`), while
|
|
14
|
-
*
|
|
13
|
+
* multi-statement string (`SELECT 1; DROP TABLE t`), while the native
|
|
14
|
+
* SQLite adapters prepare only the first. We unify on the strict
|
|
15
15
|
* behaviour: exactly one statement per `query()`.
|
|
16
16
|
*
|
|
17
17
|
* The guard only fronts the PUBLIC `client.query()` — engine-internal reads
|
package/dist/query-guard.js
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* bypasses the outbox (SPEC §7.1) and silently diverges from the
|
|
11
11
|
* server — writes MUST go through `client.mutate([...])`.
|
|
12
12
|
* 2. ONE STATEMENT. `sqlite-wasm`'s `exec` runs every statement in a
|
|
13
|
-
* multi-statement string (`SELECT 1; DROP TABLE t`), while
|
|
14
|
-
*
|
|
13
|
+
* multi-statement string (`SELECT 1; DROP TABLE t`), while the native
|
|
14
|
+
* SQLite adapters prepare only the first. We unify on the strict
|
|
15
15
|
* behaviour: exactly one statement per `query()`.
|
|
16
16
|
*
|
|
17
17
|
* The guard only fronts the PUBLIC `client.query()` — engine-internal reads
|
package/dist/remote.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database-less SSP2 producer (§6.10). It prepares and sends ordinary commits
|
|
3
|
+
* through the existing push path without creating a local replica or outbox.
|
|
4
|
+
*/
|
|
5
|
+
import { type PushOperationResult, type PushResultFrame } from '@syncular/core';
|
|
6
|
+
import type { MutationInput } from './client.js';
|
|
7
|
+
import type { EncryptionConfig } from './encryption.js';
|
|
8
|
+
import { ClientSyncError } from './errors.js';
|
|
9
|
+
import { type ClientSchema } from './schema.js';
|
|
10
|
+
import type { SyncTransport } from './transport.js';
|
|
11
|
+
import type { RemoteOperationRealtimeConnector, RemoteOperationTransport } from './transport.js';
|
|
12
|
+
export interface SyncRemoteClientConfig {
|
|
13
|
+
/** Required only for ordinary row commits. */
|
|
14
|
+
readonly schema?: ClientSchema;
|
|
15
|
+
readonly clientId: string;
|
|
16
|
+
/** Required only for ordinary row commits. */
|
|
17
|
+
readonly transport?: SyncTransport;
|
|
18
|
+
readonly operations?: RemoteOperationTransport;
|
|
19
|
+
readonly operationRealtime?: RemoteOperationRealtimeConnector;
|
|
20
|
+
readonly encryption?: EncryptionConfig;
|
|
21
|
+
}
|
|
22
|
+
export interface RemoteCommitInput {
|
|
23
|
+
/** Stable caller-owned idempotency identity for this logical commit. */
|
|
24
|
+
readonly requestId: string;
|
|
25
|
+
readonly mutations: readonly MutationInput[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Prepared bytes are the retry unit. Persist them when a process must retry
|
|
29
|
+
* across restart, especially when encryption uses randomized nonces.
|
|
30
|
+
*/
|
|
31
|
+
export interface PreparedRemoteCommit {
|
|
32
|
+
readonly requestId: string;
|
|
33
|
+
readonly bytes: Uint8Array;
|
|
34
|
+
}
|
|
35
|
+
export interface RemoteCommitResult {
|
|
36
|
+
readonly requestId: string;
|
|
37
|
+
readonly status: PushResultFrame['status'];
|
|
38
|
+
readonly commitSeq?: number;
|
|
39
|
+
readonly results: readonly PushOperationResult[];
|
|
40
|
+
}
|
|
41
|
+
export interface RemoteQueryDescriptor<Row, Params = undefined> {
|
|
42
|
+
readonly id: string;
|
|
43
|
+
readonly mapRow: (row: Readonly<Record<string, unknown>>) => Row;
|
|
44
|
+
readonly __row?: Row;
|
|
45
|
+
readonly __params?: Params;
|
|
46
|
+
}
|
|
47
|
+
export interface RemoteQueryResult<Row> {
|
|
48
|
+
readonly rows: readonly Row[];
|
|
49
|
+
readonly maxCommitSeq: number;
|
|
50
|
+
}
|
|
51
|
+
export interface RemoteQueryWatchHandlers<Row> {
|
|
52
|
+
onSnapshot(result: RemoteQueryResult<Row>): void;
|
|
53
|
+
onError?(error: ClientSyncError): void;
|
|
54
|
+
}
|
|
55
|
+
export interface RemoteCommandDescriptor<Input = undefined> {
|
|
56
|
+
readonly id: string;
|
|
57
|
+
readonly __input?: Input;
|
|
58
|
+
}
|
|
59
|
+
export declare function remoteCommand<Input = undefined>(id: string): RemoteCommandDescriptor<Input>;
|
|
60
|
+
export interface RemoteCommandResult {
|
|
61
|
+
readonly requestId: string;
|
|
62
|
+
readonly status: 'applied' | 'cached' | 'rejected';
|
|
63
|
+
readonly commitSeq?: number;
|
|
64
|
+
readonly results: readonly unknown[];
|
|
65
|
+
}
|
|
66
|
+
export declare class SyncRemoteClient {
|
|
67
|
+
#private;
|
|
68
|
+
constructor(config: SyncRemoteClientConfig);
|
|
69
|
+
prepareCommit(input: RemoteCommitInput): Promise<PreparedRemoteCommit>;
|
|
70
|
+
sendCommit(prepared: PreparedRemoteCommit): Promise<RemoteCommitResult>;
|
|
71
|
+
commit(input: RemoteCommitInput): Promise<RemoteCommitResult>;
|
|
72
|
+
query<Row, Params = undefined>(descriptor: RemoteQueryDescriptor<Row, Params>, params?: Params): Promise<RemoteQueryResult<Row>>;
|
|
73
|
+
command<Input = undefined>(descriptor: RemoteCommandDescriptor<Input>, requestId: string, input?: Input): Promise<RemoteCommandResult>;
|
|
74
|
+
watch<Row, Params = undefined>(descriptor: RemoteQueryDescriptor<Row, Params>, params: Params, handlers: RemoteQueryWatchHandlers<Row>): Promise<() => void>;
|
|
75
|
+
close(): void;
|
|
76
|
+
}
|