@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/src/node-database.ts
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, type SQLInputValue } 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 {
|
|
@@ -28,54 +14,19 @@ import {
|
|
|
28
14
|
type SqlValue,
|
|
29
15
|
} from './database';
|
|
30
16
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
* type it locally (rather than importing `better-sqlite3`'s types) so the
|
|
34
|
-
* package typechecks without the optional peer installed.
|
|
35
|
-
*/
|
|
36
|
-
interface BetterSqliteStatement {
|
|
37
|
-
run(...params: NodeParam[]): unknown;
|
|
38
|
-
all(...params: NodeParam[]): unknown[];
|
|
39
|
-
}
|
|
40
|
-
interface BetterSqliteDatabase {
|
|
41
|
-
readonly inTransaction: boolean;
|
|
42
|
-
prepare(sql: string): BetterSqliteStatement;
|
|
43
|
-
exec(sql: string): unknown;
|
|
44
|
-
close(): void;
|
|
45
|
-
}
|
|
46
|
-
type BetterSqliteConstructor = new (
|
|
47
|
-
path: string,
|
|
48
|
-
options?: { readonly?: boolean; fileMustExist?: boolean },
|
|
49
|
-
) => BetterSqliteDatabase;
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* better-sqlite3 accepts string / number / bigint / null / Buffer|Uint8Array
|
|
53
|
-
* bind values, but NOT booleans (it throws "TypeError: can only bind …"). We
|
|
54
|
-
* coerce booleans to 0/1 exactly like the bun adapter so callers see one
|
|
55
|
-
* uniform bind contract across every backend.
|
|
56
|
-
*/
|
|
57
|
-
type NodeParam = string | number | bigint | Uint8Array | null;
|
|
58
|
-
|
|
59
|
-
function coerceParams(params: readonly SqlValue[]): NodeParam[] {
|
|
60
|
-
return params.map((value): NodeParam => {
|
|
17
|
+
function coerceParams(params: readonly SqlValue[]): SQLInputValue[] {
|
|
18
|
+
return params.map((value): SQLInputValue => {
|
|
61
19
|
if (typeof value === 'boolean') return value ? 1 : 0;
|
|
62
20
|
return value;
|
|
63
21
|
});
|
|
64
22
|
}
|
|
65
23
|
|
|
66
|
-
/**
|
|
67
|
-
* better-sqlite3 returns BLOB columns as Node `Buffer`s. A Buffer IS a
|
|
68
|
-
* Uint8Array subclass, but it can be a view onto a shared pool buffer, so we
|
|
69
|
-
* normalize to a standalone Uint8Array — matching what bun:sqlite hands back
|
|
70
|
-
* and keeping the buffer-ownership assumptions elsewhere (worker transfer,
|
|
71
|
-
* structured clone) honest.
|
|
72
|
-
*/
|
|
73
24
|
function normalizeRow(row: Record<string, unknown>): SqlRow {
|
|
74
25
|
const out: SqlRow = {};
|
|
75
26
|
for (const key in row) {
|
|
76
27
|
const value = row[key];
|
|
77
|
-
if (
|
|
78
|
-
out[key] = new Uint8Array(value);
|
|
28
|
+
if (value instanceof Uint8Array) {
|
|
29
|
+
out[key] = new Uint8Array(value);
|
|
79
30
|
} else {
|
|
80
31
|
out[key] = value as SqlValue;
|
|
81
32
|
}
|
|
@@ -83,55 +34,12 @@ function normalizeRow(row: Record<string, unknown>): SqlRow {
|
|
|
83
34
|
return out;
|
|
84
35
|
}
|
|
85
36
|
|
|
86
|
-
/**
|
|
87
|
-
* Load the optional peer AND open the database in one guarded step, so BOTH
|
|
88
|
-
* failure modes are turned into a clear, actionable error rather than a raw
|
|
89
|
-
* one:
|
|
90
|
-
*
|
|
91
|
-
* - `require('better-sqlite3')` throwing MODULE_NOT_FOUND — the peer is not
|
|
92
|
-
* installed (the common browser-only-host case), and
|
|
93
|
-
* - `new Database()` throwing ERR_DLOPEN_FAILED — the module resolves but the
|
|
94
|
-
* native addon cannot load, which is exactly what bun does for
|
|
95
|
-
* better-sqlite3 (oven-sh/bun#4290); the addon only dlopens at construction.
|
|
96
|
-
*/
|
|
97
|
-
function openBetterSqlite(path: string): BetterSqliteDatabase {
|
|
98
|
-
const require = createRequire(import.meta.url);
|
|
99
|
-
try {
|
|
100
|
-
const mod = require('better-sqlite3') as
|
|
101
|
-
| BetterSqliteConstructor
|
|
102
|
-
| { default: BetterSqliteConstructor };
|
|
103
|
-
const Database =
|
|
104
|
-
(mod as { default?: BetterSqliteConstructor }).default ??
|
|
105
|
-
(mod as BetterSqliteConstructor);
|
|
106
|
-
return new Database(path);
|
|
107
|
-
} catch (error) {
|
|
108
|
-
const code = (error as { code?: string })?.code;
|
|
109
|
-
if (code === 'ERR_DLOPEN_FAILED') {
|
|
110
|
-
throw new Error(
|
|
111
|
-
"openNodeDatabase() requires the 'better-sqlite3' native module, but " +
|
|
112
|
-
'it failed to load. This most commonly means you are running under ' +
|
|
113
|
-
'bun, which cannot dlopen better-sqlite3 (oven-sh/bun#4290) — use ' +
|
|
114
|
-
"the bun:sqlite backend ('@syncular/client/bun') under bun, " +
|
|
115
|
-
"and reserve '@syncular/client/node' for Node/Electron-main. " +
|
|
116
|
-
`Underlying error: ${String(error)}`,
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
throw new Error(
|
|
120
|
-
'openNodeDatabase() requires the optional peer dependency ' +
|
|
121
|
-
"'better-sqlite3', which is not installed. Add it to your app " +
|
|
122
|
-
'(`npm install better-sqlite3` / `bun add better-sqlite3`) — it is ' +
|
|
123
|
-
'kept optional so @syncular/client installs without a native ' +
|
|
124
|
-
`build for browser-only hosts. Underlying error: ${String(error)}`,
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
37
|
export class NodeClientDatabase implements ClientDatabase {
|
|
130
|
-
readonly db:
|
|
38
|
+
readonly db: DatabaseSync;
|
|
131
39
|
#tx = { depth: 0 };
|
|
132
40
|
|
|
133
41
|
constructor(path = ':memory:') {
|
|
134
|
-
this.db =
|
|
42
|
+
this.db = new DatabaseSync(path);
|
|
135
43
|
}
|
|
136
44
|
|
|
137
45
|
exec(sql: string, params: readonly SqlValue[] = []): void {
|
|
@@ -140,19 +48,14 @@ export class NodeClientDatabase implements ClientDatabase {
|
|
|
140
48
|
|
|
141
49
|
query(sql: string, params: readonly SqlValue[] = []): SqlRow[] {
|
|
142
50
|
const rows = this.db.prepare(sql).all(...coerceParams(params));
|
|
143
|
-
return
|
|
51
|
+
return rows.map(normalizeRow);
|
|
144
52
|
}
|
|
145
53
|
|
|
146
54
|
transaction<T>(fn: () => T): T {
|
|
147
55
|
return runTransaction(this.#tx, (sql) => this.db.exec(sql), fn);
|
|
148
56
|
}
|
|
149
57
|
|
|
150
|
-
/**
|
|
151
|
-
* §5.3 image import: better-sqlite3 (like bun:sqlite) attaches files, not
|
|
152
|
-
* buffers, so the image lands in a private temp file for the duration of
|
|
153
|
-
* the ATTACH. Must be called outside any open transaction (SQLite cannot
|
|
154
|
-
* ATTACH inside one).
|
|
155
|
-
*/
|
|
58
|
+
/** §5.3 image import through a private file attached for one callback. */
|
|
156
59
|
withSqliteImage<T>(bytes: Uint8Array, alias: string, fn: () => T): T {
|
|
157
60
|
assertImageAlias(alias);
|
|
158
61
|
const dir = mkdtempSync(join(tmpdir(), 'syncular-image-'));
|
package/src/query-guard.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
|