@zvndev/powdb-client 0.7.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +49 -0
- package/dist/index.d.ts +77 -3
- package/dist/index.js +188 -3
- package/dist/protocol.d.ts +68 -0
- package/dist/protocol.js +287 -0
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.0 - 2026-07-02
|
|
4
|
+
|
|
5
|
+
- Released in lockstep with PowDB workspace v0.8.0 (Embedded Sync Milestone 0).
|
|
6
|
+
- Added **experimental** low-level authenticated embedded-sync protocol helpers:
|
|
7
|
+
`client.syncStatus`, `client.syncPull`, and `client.syncAck`.
|
|
8
|
+
- Added TypeScript wire support for the private sync status/pull/ack frames
|
|
9
|
+
(`0x20`-`0x25`), retained-unit payloads, sync repair actions, and the
|
|
10
|
+
`sync` observability event.
|
|
11
|
+
- These sync helpers are experimental and beta-gated: pin matching client/server
|
|
12
|
+
versions. Plain `query(powql)` / `querySql(sql)` frames are unchanged.
|
|
13
|
+
|
|
3
14
|
## 0.5.1 - 2026-06-17
|
|
4
15
|
|
|
5
16
|
- Kept the npm client release in lockstep with PowDB workspace v0.5.1.
|
|
@@ -12,6 +23,7 @@ All notable changes to `@zvndev/powdb-client`.
|
|
|
12
23
|
|
|
13
24
|
| Client version | Compatible PowDB server | Notes |
|
|
14
25
|
|---|---|---|
|
|
26
|
+
| 0.8.x | Matching sync-enabled 0.8.x server | Adds experimental private authenticated embedded-sync helpers (`syncStatus`, `syncPull`, `syncAck`) over wire frames `0x20`-`0x25`. Pin matching client/server versions for these helpers until the stable `@zvndev/powdb-sync` package boundary exists; plain `query(powql)` and `querySql(sql)` frames remain unchanged. |
|
|
15
27
|
| 0.5.x | 0.4.7+ | Adds `client.query(powql, params)` for `$N` parameter binding. The `QueryWithParams` (`0x04`) wire message is only understood by server ≥0.4.7; parameterized queries against an older server error out. Plain `query(powql)` calls remain compatible with 0.3.x–0.4.x. |
|
|
16
28
|
| 0.4.x | 0.3.x – 0.4.x | Wire protocol v1 plus the optional Connect `username` field. **Multi-user mode requires client ≥0.4.0 AND server ≥0.4.6** (the server enforces roles as of 0.4.6; 0.4.5 accepted the username but did not enforce `readonly`). When `user` is omitted the Connect frame is byte-identical to the 0.3.x shape, so legacy shared-password and no-auth servers work unchanged. |
|
|
17
29
|
| 0.3.x | 0.3.x – 0.4.x | Wire protocol v1. The client warns only on a major-version mismatch, so any `0.x` server connects. Minor server bumps may add new opcodes; the client tolerates unknown response codes by surfacing `PowDBError`. Pin both ends. **Caveat:** the 0.3.x client has no `user` option, so it cannot authenticate to a 0.4.5+ server running in **multi-user mode** (the server requires a username once any named user is defined). Shared-password mode works fine. |
|
package/README.md
CHANGED
|
@@ -81,6 +81,54 @@ await client.query("insert User { name := $1, age := $2 }", ["Dana", null]);
|
|
|
81
81
|
|
|
82
82
|
`QueryParam` is `string | number | bigint | boolean | null`. The params form sends the `QueryWithParams` (0x04) wire message and **requires powdb-server >= 0.4.7**. The plain `query(q)` and `query(q, { signal })` forms are unchanged.
|
|
83
83
|
|
|
84
|
+
## Experimental embedded sync protocol helpers
|
|
85
|
+
|
|
86
|
+
The client exposes experimental low-level helpers for the private authenticated
|
|
87
|
+
embedded-sync frames used by the in-progress replica product:
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
client.on("sync", ({ operation, durationMs, ok, stale, repairAction }) => {
|
|
91
|
+
metrics.histogram("powdb_sync_ms", durationMs, {
|
|
92
|
+
operation,
|
|
93
|
+
ok: String(ok),
|
|
94
|
+
repairAction: repairAction ?? "error",
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const status = await client.syncStatus("replica-a");
|
|
99
|
+
|
|
100
|
+
if (status.repairAction === "pull" && status.lastAppliedLsn !== null) {
|
|
101
|
+
const pull = await client.syncPull({
|
|
102
|
+
replicaId: "replica-a",
|
|
103
|
+
sinceLsn: status.lastAppliedLsn,
|
|
104
|
+
maxUnits: 4096,
|
|
105
|
+
maxBytes: 16n * 1024n * 1024n,
|
|
106
|
+
databaseId: "0123456789abcdeffedcba9876543210",
|
|
107
|
+
primaryGeneration: 1n,
|
|
108
|
+
walFormatVersion: 1,
|
|
109
|
+
catalogVersion: 1,
|
|
110
|
+
segmentFormatVersion: 1,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Apply `pull.units` with the embedded replica layer, then acknowledge.
|
|
114
|
+
const lastUnit = pull.units.at(-1);
|
|
115
|
+
if (lastUnit) {
|
|
116
|
+
await client.syncAck({
|
|
117
|
+
replicaId: "replica-a",
|
|
118
|
+
appliedLsn: lastUnit.lsn,
|
|
119
|
+
remoteLsn: pull.status.remoteLsn,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
These methods are intentionally not a complete or stable
|
|
126
|
+
`@zvndev/powdb-sync` package. They expose the authenticated server
|
|
127
|
+
pull/status/ack protocol so the embedded replica layer can be built and tested
|
|
128
|
+
without hand-rolled frame code. Pin matching client/server versions while using
|
|
129
|
+
them; the stable product boundary will be the sync package. Servers must have
|
|
130
|
+
sync enabled and reject unauthenticated or `readonly` users.
|
|
131
|
+
|
|
84
132
|
## Authentication
|
|
85
133
|
|
|
86
134
|
For servers using the legacy shared password (`POWDB_PASSWORD`), pass
|
|
@@ -254,6 +302,7 @@ Events:
|
|
|
254
302
|
| Event | Payload | Fires when |
|
|
255
303
|
|---|---|---|
|
|
256
304
|
| `query` | `{ query, durationMs, ok, kind?, error? }` | After every query completes (success or failure) |
|
|
305
|
+
| `sync` | `{ operation, replicaId, durationMs, ok, status?, stale?, repairAction?, units?, advanced?, error? }` | After every sync status/pull/ack request completes |
|
|
257
306
|
| `close` | `{ error: Error \| null }` | Exactly once per socket, on normal or error close |
|
|
258
307
|
|
|
259
308
|
## Cancellation
|
package/dist/index.d.ts
CHANGED
|
@@ -16,9 +16,10 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import * as tls from "node:tls";
|
|
18
18
|
import { EventEmitter } from "node:events";
|
|
19
|
+
import { type SyncRepairAction, type WireRetainedUnit, type WireSyncStatus } from "./protocol.js";
|
|
19
20
|
import { type TypedRow, type TypedSchema } from "./typed.js";
|
|
20
21
|
/** Client library version. Compared to the server's reported version. */
|
|
21
|
-
export declare const CLIENT_VERSION = "0.
|
|
22
|
+
export declare const CLIENT_VERSION = "0.8.0";
|
|
22
23
|
export type QueryResult = {
|
|
23
24
|
kind: "rows";
|
|
24
25
|
columns: string[];
|
|
@@ -42,6 +43,41 @@ export type QueryResult = {
|
|
|
42
43
|
* binds as an int; `null` binds PowQL `null`.
|
|
43
44
|
*/
|
|
44
45
|
export type QueryParam = string | number | bigint | boolean | null;
|
|
46
|
+
/** Unsigned 64-bit sync protocol value. Numbers must be safe non-negative integers. */
|
|
47
|
+
export type SyncU64 = bigint | number;
|
|
48
|
+
/**
|
|
49
|
+
* Database identity carried by the sync protocol. Strings are the 32-hex-char
|
|
50
|
+
* form stored in `.powdb-sync/identity.json`; byte arrays must be exactly 16B.
|
|
51
|
+
*/
|
|
52
|
+
export type SyncDatabaseId = string | Uint8Array;
|
|
53
|
+
export interface SyncPullRequest {
|
|
54
|
+
replicaId: string;
|
|
55
|
+
sinceLsn: SyncU64;
|
|
56
|
+
maxUnits: number;
|
|
57
|
+
maxBytes: SyncU64;
|
|
58
|
+
databaseId: SyncDatabaseId;
|
|
59
|
+
primaryGeneration: SyncU64;
|
|
60
|
+
walFormatVersion: number;
|
|
61
|
+
catalogVersion: number;
|
|
62
|
+
segmentFormatVersion: number;
|
|
63
|
+
}
|
|
64
|
+
export interface SyncAckRequest {
|
|
65
|
+
replicaId: string;
|
|
66
|
+
appliedLsn: SyncU64;
|
|
67
|
+
remoteLsn: SyncU64;
|
|
68
|
+
}
|
|
69
|
+
export interface SyncPullResult {
|
|
70
|
+
status: WireSyncStatus;
|
|
71
|
+
units: WireRetainedUnit[];
|
|
72
|
+
hasMore: boolean;
|
|
73
|
+
}
|
|
74
|
+
export interface SyncAckResult {
|
|
75
|
+
previousAppliedLsn: bigint;
|
|
76
|
+
appliedLsn: bigint;
|
|
77
|
+
remoteLsn: bigint;
|
|
78
|
+
advanced: boolean;
|
|
79
|
+
status: WireSyncStatus;
|
|
80
|
+
}
|
|
45
81
|
export interface ClientOptions {
|
|
46
82
|
/** TCP host. Required unless `path` (a Unix domain socket) is given. */
|
|
47
83
|
host?: string;
|
|
@@ -93,6 +129,20 @@ export interface ClientEvents {
|
|
|
93
129
|
error?: Error;
|
|
94
130
|
}
|
|
95
131
|
];
|
|
132
|
+
sync: [
|
|
133
|
+
{
|
|
134
|
+
operation: "status" | "pull" | "ack";
|
|
135
|
+
replicaId: string;
|
|
136
|
+
durationMs: number;
|
|
137
|
+
ok: boolean;
|
|
138
|
+
status?: WireSyncStatus;
|
|
139
|
+
stale?: boolean;
|
|
140
|
+
repairAction?: SyncRepairAction;
|
|
141
|
+
units?: number;
|
|
142
|
+
advanced?: boolean;
|
|
143
|
+
error?: Error;
|
|
144
|
+
}
|
|
145
|
+
];
|
|
96
146
|
close: [{
|
|
97
147
|
error: Error | null;
|
|
98
148
|
}];
|
|
@@ -130,6 +180,30 @@ export declare class Client extends EventEmitter<ClientEvents> {
|
|
|
130
180
|
querySql(query: string, opts?: {
|
|
131
181
|
signal?: AbortSignal;
|
|
132
182
|
}): Promise<QueryResult>;
|
|
183
|
+
/**
|
|
184
|
+
* Fetch primary-side sync status for one embedded replica cursor.
|
|
185
|
+
*
|
|
186
|
+
* This speaks the private authenticated sync frame added for the embedded
|
|
187
|
+
* replica product. Servers without sync support return a protocol/query
|
|
188
|
+
* error; unauthenticated or readonly users are rejected server-side.
|
|
189
|
+
*/
|
|
190
|
+
syncStatus(replicaId: string, opts?: {
|
|
191
|
+
signal?: AbortSignal;
|
|
192
|
+
}): Promise<WireSyncStatus>;
|
|
193
|
+
/**
|
|
194
|
+
* Pull a bounded retained-unit chunk after this replica's primary-side cursor.
|
|
195
|
+
*
|
|
196
|
+
* The caller supplies the database identity and format versions from the
|
|
197
|
+
* sync bootstrap metadata. The server rejects mismatches and non-applyable
|
|
198
|
+
* transaction cuts instead of returning ambiguous history.
|
|
199
|
+
*/
|
|
200
|
+
syncPull(request: SyncPullRequest, opts?: {
|
|
201
|
+
signal?: AbortSignal;
|
|
202
|
+
}): Promise<SyncPullResult>;
|
|
203
|
+
/** Acknowledge that the replica applied retained history through `appliedLsn`. */
|
|
204
|
+
syncAck(request: SyncAckRequest, opts?: {
|
|
205
|
+
signal?: AbortSignal;
|
|
206
|
+
}): Promise<SyncAckResult>;
|
|
133
207
|
/**
|
|
134
208
|
* Like {@link query}, but coerces string result columns to typed JS values
|
|
135
209
|
* using the caller-supplied schema. See `./typed.ts` for the coercion
|
|
@@ -175,8 +249,8 @@ export declare class Client extends EventEmitter<ClientEvents> {
|
|
|
175
249
|
private onClose;
|
|
176
250
|
}
|
|
177
251
|
export { encode, tryDecode } from "./protocol.js";
|
|
178
|
-
export type { Message, WireParam } from "./protocol.js";
|
|
179
|
-
export { MAX_PAYLOAD_SIZE, MAX_ROWS, MAX_COLUMNS, MAX_PARAMS, } from "./protocol.js";
|
|
252
|
+
export type { Message, SyncRepairAction, WireParam, WireRetainedUnit, WireSyncStatus, } from "./protocol.js";
|
|
253
|
+
export { MAX_PAYLOAD_SIZE, MAX_ROWS, MAX_COLUMNS, MAX_PARAMS, MAX_SYNC_UNITS, MAX_SYNC_PULL_UNITS, MAX_SYNC_PULL_BYTES, } from "./protocol.js";
|
|
180
254
|
export { escapeLiteral, escapeIdent, ident, powql, PowqlIdent, } from "./escape.js";
|
|
181
255
|
export { Pool } from "./pool.js";
|
|
182
256
|
export type { PoolOptions } from "./pool.js";
|
package/dist/index.js
CHANGED
|
@@ -17,11 +17,11 @@
|
|
|
17
17
|
import * as net from "node:net";
|
|
18
18
|
import * as tls from "node:tls";
|
|
19
19
|
import { EventEmitter } from "node:events";
|
|
20
|
-
import { encode, tryDecode, } from "./protocol.js";
|
|
20
|
+
import { encode, tryDecode, MAX_SYNC_PULL_BYTES, MAX_SYNC_PULL_UNITS, } from "./protocol.js";
|
|
21
21
|
import { PowDBError } from "./errors.js";
|
|
22
22
|
import { coerceRows, } from "./typed.js";
|
|
23
23
|
/** Client library version. Compared to the server's reported version. */
|
|
24
|
-
export const CLIENT_VERSION = "0.
|
|
24
|
+
export const CLIENT_VERSION = "0.8.0";
|
|
25
25
|
function socketChunkToBuffer(chunk) {
|
|
26
26
|
return typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
27
27
|
}
|
|
@@ -44,6 +44,52 @@ function toWireParam(p) {
|
|
|
44
44
|
throw new PowDBError(`unsupported query parameter type: ${typeof p}`, "protocol_error");
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
+
function toU64(value, label) {
|
|
48
|
+
if (typeof value === "bigint") {
|
|
49
|
+
if (value < 0n || value > 0xffffffffffffffffn) {
|
|
50
|
+
throw new PowDBError(`${label} must fit in u64`, "protocol_error");
|
|
51
|
+
}
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
55
|
+
throw new PowDBError(`${label} must be a safe non-negative integer or bigint`, "protocol_error");
|
|
56
|
+
}
|
|
57
|
+
return BigInt(value);
|
|
58
|
+
}
|
|
59
|
+
function toU16(value, label) {
|
|
60
|
+
if (!Number.isInteger(value) || value < 0 || value > 0xffff) {
|
|
61
|
+
throw new PowDBError(`${label} must fit in u16`, "protocol_error");
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
function toSyncMaxUnits(value) {
|
|
66
|
+
if (!Number.isInteger(value) ||
|
|
67
|
+
value < 1 ||
|
|
68
|
+
value > MAX_SYNC_PULL_UNITS) {
|
|
69
|
+
throw new PowDBError(`maxUnits must be between 1 and ${MAX_SYNC_PULL_UNITS}`, "protocol_error");
|
|
70
|
+
}
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
function toSyncMaxBytes(value) {
|
|
74
|
+
const bytes = toU64(value, "maxBytes");
|
|
75
|
+
if (bytes < 1n || bytes > BigInt(MAX_SYNC_PULL_BYTES)) {
|
|
76
|
+
throw new PowDBError(`maxBytes must be between 1 and ${MAX_SYNC_PULL_BYTES}`, "protocol_error");
|
|
77
|
+
}
|
|
78
|
+
return bytes;
|
|
79
|
+
}
|
|
80
|
+
function toDatabaseId(value) {
|
|
81
|
+
if (typeof value === "string") {
|
|
82
|
+
if (!/^[0-9a-fA-F]{32}$/.test(value)) {
|
|
83
|
+
throw new PowDBError("databaseId string must be exactly 32 hex characters", "protocol_error");
|
|
84
|
+
}
|
|
85
|
+
return Buffer.from(value, "hex");
|
|
86
|
+
}
|
|
87
|
+
const bytes = Buffer.from(value);
|
|
88
|
+
if (bytes.length !== 16) {
|
|
89
|
+
throw new PowDBError(`databaseId must be exactly 16 bytes, got ${bytes.length}`, "protocol_error");
|
|
90
|
+
}
|
|
91
|
+
return bytes;
|
|
92
|
+
}
|
|
47
93
|
/** Module-level set of host:port pairs we've already warned about. */
|
|
48
94
|
const versionWarnings = new Set();
|
|
49
95
|
/** Extract the major component of a dotted version string, e.g. "0.2.0" → "0". */
|
|
@@ -267,6 +313,145 @@ export class Client extends EventEmitter {
|
|
|
267
313
|
throw err;
|
|
268
314
|
}
|
|
269
315
|
}
|
|
316
|
+
/**
|
|
317
|
+
* Fetch primary-side sync status for one embedded replica cursor.
|
|
318
|
+
*
|
|
319
|
+
* This speaks the private authenticated sync frame added for the embedded
|
|
320
|
+
* replica product. Servers without sync support return a protocol/query
|
|
321
|
+
* error; unauthenticated or readonly users are rejected server-side.
|
|
322
|
+
*/
|
|
323
|
+
async syncStatus(replicaId, opts) {
|
|
324
|
+
const start = Date.now();
|
|
325
|
+
try {
|
|
326
|
+
const reply = await this.send({ type: "SyncStatus", replicaId }, opts);
|
|
327
|
+
if (reply.type === "Error") {
|
|
328
|
+
throw new PowDBError(`sync status failed: ${reply.message}`, "query_failed");
|
|
329
|
+
}
|
|
330
|
+
if (reply.type !== "SyncStatusResult") {
|
|
331
|
+
throw new PowDBError(`unexpected reply: ${reply.type}`, "protocol_error");
|
|
332
|
+
}
|
|
333
|
+
this.emit("sync", {
|
|
334
|
+
operation: "status",
|
|
335
|
+
replicaId,
|
|
336
|
+
durationMs: Date.now() - start,
|
|
337
|
+
ok: true,
|
|
338
|
+
status: reply.status,
|
|
339
|
+
stale: reply.status.stale,
|
|
340
|
+
repairAction: reply.status.repairAction,
|
|
341
|
+
});
|
|
342
|
+
return reply.status;
|
|
343
|
+
}
|
|
344
|
+
catch (err) {
|
|
345
|
+
this.emit("sync", {
|
|
346
|
+
operation: "status",
|
|
347
|
+
replicaId,
|
|
348
|
+
durationMs: Date.now() - start,
|
|
349
|
+
ok: false,
|
|
350
|
+
error: err,
|
|
351
|
+
});
|
|
352
|
+
throw err;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Pull a bounded retained-unit chunk after this replica's primary-side cursor.
|
|
357
|
+
*
|
|
358
|
+
* The caller supplies the database identity and format versions from the
|
|
359
|
+
* sync bootstrap metadata. The server rejects mismatches and non-applyable
|
|
360
|
+
* transaction cuts instead of returning ambiguous history.
|
|
361
|
+
*/
|
|
362
|
+
async syncPull(request, opts) {
|
|
363
|
+
const start = Date.now();
|
|
364
|
+
try {
|
|
365
|
+
const reply = await this.send({
|
|
366
|
+
type: "SyncPull",
|
|
367
|
+
replicaId: request.replicaId,
|
|
368
|
+
sinceLsn: toU64(request.sinceLsn, "sinceLsn"),
|
|
369
|
+
maxUnits: toSyncMaxUnits(request.maxUnits),
|
|
370
|
+
maxBytes: toSyncMaxBytes(request.maxBytes),
|
|
371
|
+
databaseId: toDatabaseId(request.databaseId),
|
|
372
|
+
primaryGeneration: toU64(request.primaryGeneration, "primaryGeneration"),
|
|
373
|
+
walFormatVersion: toU16(request.walFormatVersion, "walFormatVersion"),
|
|
374
|
+
catalogVersion: toU16(request.catalogVersion, "catalogVersion"),
|
|
375
|
+
segmentFormatVersion: toU16(request.segmentFormatVersion, "segmentFormatVersion"),
|
|
376
|
+
}, opts);
|
|
377
|
+
if (reply.type === "Error") {
|
|
378
|
+
throw new PowDBError(`sync pull failed: ${reply.message}`, "query_failed");
|
|
379
|
+
}
|
|
380
|
+
if (reply.type !== "SyncPullResult") {
|
|
381
|
+
throw new PowDBError(`unexpected reply: ${reply.type}`, "protocol_error");
|
|
382
|
+
}
|
|
383
|
+
this.emit("sync", {
|
|
384
|
+
operation: "pull",
|
|
385
|
+
replicaId: request.replicaId,
|
|
386
|
+
durationMs: Date.now() - start,
|
|
387
|
+
ok: true,
|
|
388
|
+
status: reply.status,
|
|
389
|
+
stale: reply.status.stale,
|
|
390
|
+
repairAction: reply.status.repairAction,
|
|
391
|
+
units: reply.units.length,
|
|
392
|
+
});
|
|
393
|
+
return {
|
|
394
|
+
status: reply.status,
|
|
395
|
+
units: reply.units,
|
|
396
|
+
hasMore: reply.hasMore,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
catch (err) {
|
|
400
|
+
this.emit("sync", {
|
|
401
|
+
operation: "pull",
|
|
402
|
+
replicaId: request.replicaId,
|
|
403
|
+
durationMs: Date.now() - start,
|
|
404
|
+
ok: false,
|
|
405
|
+
error: err,
|
|
406
|
+
});
|
|
407
|
+
throw err;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
/** Acknowledge that the replica applied retained history through `appliedLsn`. */
|
|
411
|
+
async syncAck(request, opts) {
|
|
412
|
+
const start = Date.now();
|
|
413
|
+
try {
|
|
414
|
+
const reply = await this.send({
|
|
415
|
+
type: "SyncAck",
|
|
416
|
+
replicaId: request.replicaId,
|
|
417
|
+
appliedLsn: toU64(request.appliedLsn, "appliedLsn"),
|
|
418
|
+
remoteLsn: toU64(request.remoteLsn, "remoteLsn"),
|
|
419
|
+
}, opts);
|
|
420
|
+
if (reply.type === "Error") {
|
|
421
|
+
throw new PowDBError(`sync ack failed: ${reply.message}`, "query_failed");
|
|
422
|
+
}
|
|
423
|
+
if (reply.type !== "SyncAckResult") {
|
|
424
|
+
throw new PowDBError(`unexpected reply: ${reply.type}`, "protocol_error");
|
|
425
|
+
}
|
|
426
|
+
this.emit("sync", {
|
|
427
|
+
operation: "ack",
|
|
428
|
+
replicaId: request.replicaId,
|
|
429
|
+
durationMs: Date.now() - start,
|
|
430
|
+
ok: true,
|
|
431
|
+
status: reply.status,
|
|
432
|
+
stale: reply.status.stale,
|
|
433
|
+
repairAction: reply.status.repairAction,
|
|
434
|
+
advanced: reply.advanced,
|
|
435
|
+
});
|
|
436
|
+
return {
|
|
437
|
+
previousAppliedLsn: reply.previousAppliedLsn,
|
|
438
|
+
appliedLsn: reply.appliedLsn,
|
|
439
|
+
remoteLsn: reply.remoteLsn,
|
|
440
|
+
advanced: reply.advanced,
|
|
441
|
+
status: reply.status,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
catch (err) {
|
|
445
|
+
this.emit("sync", {
|
|
446
|
+
operation: "ack",
|
|
447
|
+
replicaId: request.replicaId,
|
|
448
|
+
durationMs: Date.now() - start,
|
|
449
|
+
ok: false,
|
|
450
|
+
error: err,
|
|
451
|
+
});
|
|
452
|
+
throw err;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
270
455
|
/**
|
|
271
456
|
* Like {@link query}, but coerces string result columns to typed JS values
|
|
272
457
|
* using the caller-supplied schema. See `./typed.ts` for the coercion
|
|
@@ -605,7 +790,7 @@ function openSocket(target, timeoutMs, tlsOpt) {
|
|
|
605
790
|
});
|
|
606
791
|
}
|
|
607
792
|
export { encode, tryDecode } from "./protocol.js";
|
|
608
|
-
export { MAX_PAYLOAD_SIZE, MAX_ROWS, MAX_COLUMNS, MAX_PARAMS, } from "./protocol.js";
|
|
793
|
+
export { MAX_PAYLOAD_SIZE, MAX_ROWS, MAX_COLUMNS, MAX_PARAMS, MAX_SYNC_UNITS, MAX_SYNC_PULL_UNITS, MAX_SYNC_PULL_BYTES, } from "./protocol.js";
|
|
609
794
|
export { escapeLiteral, escapeIdent, ident, powql, PowqlIdent, } from "./escape.js";
|
|
610
795
|
export { Pool } from "./pool.js";
|
|
611
796
|
export { PowDBError, isPowDBError } from "./errors.js";
|
package/dist/protocol.d.ts
CHANGED
|
@@ -17,6 +17,13 @@ export declare const MSG_QUERY = 3;
|
|
|
17
17
|
export declare const MSG_QUERY_PARAMS = 4;
|
|
18
18
|
/** SQL query frame. Plain Query remains PowQL for backward compatibility. */
|
|
19
19
|
export declare const MSG_QUERY_SQL = 5;
|
|
20
|
+
/** Private embedded-sync control frames. Requires authenticated server support. */
|
|
21
|
+
export declare const MSG_SYNC_STATUS = 32;
|
|
22
|
+
export declare const MSG_SYNC_PULL = 33;
|
|
23
|
+
export declare const MSG_SYNC_ACK = 34;
|
|
24
|
+
export declare const MSG_SYNC_STATUS_RESULT = 35;
|
|
25
|
+
export declare const MSG_SYNC_PULL_RESULT = 36;
|
|
26
|
+
export declare const MSG_SYNC_ACK_RESULT = 37;
|
|
20
27
|
export declare const MSG_RESULT_ROWS = 7;
|
|
21
28
|
export declare const MSG_RESULT_SCALAR = 8;
|
|
22
29
|
export declare const MSG_RESULT_OK = 9;
|
|
@@ -33,6 +40,12 @@ export declare const MAX_ROWS = 10000000;
|
|
|
33
40
|
export declare const MAX_COLUMNS = 4096;
|
|
34
41
|
/** Maximum number of bound parameters in a QueryWithParams message. */
|
|
35
42
|
export declare const MAX_PARAMS = 4096;
|
|
43
|
+
/** Maximum retained units accepted in one sync pull result. */
|
|
44
|
+
export declare const MAX_SYNC_UNITS = 4096;
|
|
45
|
+
/** Maximum retained units accepted by the server for one sync pull request. */
|
|
46
|
+
export declare const MAX_SYNC_PULL_UNITS = 4096;
|
|
47
|
+
/** Maximum retained-unit payload budget accepted by the server for one sync pull. */
|
|
48
|
+
export declare const MAX_SYNC_PULL_BYTES: number;
|
|
36
49
|
/**
|
|
37
50
|
* A positional parameter value for {@link MSG_QUERY_PARAMS}. Wire encoding
|
|
38
51
|
* per param: a 1-byte tag followed by the body —
|
|
@@ -57,6 +70,27 @@ export type WireParam = {
|
|
|
57
70
|
tag: "str";
|
|
58
71
|
value: string;
|
|
59
72
|
};
|
|
73
|
+
export type SyncRepairAction = "none" | "pull" | "awaitArchive" | "rebootstrap";
|
|
74
|
+
export interface WireSyncStatus {
|
|
75
|
+
replicaId: string;
|
|
76
|
+
active: boolean;
|
|
77
|
+
lastAppliedLsn: bigint | null;
|
|
78
|
+
remoteLsn: bigint;
|
|
79
|
+
servableLsn: bigint | null;
|
|
80
|
+
unarchivedLsn: bigint | null;
|
|
81
|
+
lagLsn: bigint | null;
|
|
82
|
+
lagBytes: bigint | null;
|
|
83
|
+
lagMs: bigint | null;
|
|
84
|
+
stale: boolean;
|
|
85
|
+
repairAction: SyncRepairAction;
|
|
86
|
+
lastSyncError: string | null;
|
|
87
|
+
}
|
|
88
|
+
export interface WireRetainedUnit {
|
|
89
|
+
txId: bigint;
|
|
90
|
+
recordType: number;
|
|
91
|
+
lsn: bigint;
|
|
92
|
+
data: Buffer;
|
|
93
|
+
}
|
|
60
94
|
export type Message = {
|
|
61
95
|
type: "Connect";
|
|
62
96
|
dbName: string;
|
|
@@ -82,6 +116,40 @@ export type Message = {
|
|
|
82
116
|
type: "QueryWithParams";
|
|
83
117
|
query: string;
|
|
84
118
|
params: WireParam[];
|
|
119
|
+
} | {
|
|
120
|
+
type: "SyncStatus";
|
|
121
|
+
replicaId: string;
|
|
122
|
+
} | {
|
|
123
|
+
type: "SyncPull";
|
|
124
|
+
replicaId: string;
|
|
125
|
+
sinceLsn: bigint;
|
|
126
|
+
maxUnits: number;
|
|
127
|
+
maxBytes: bigint;
|
|
128
|
+
databaseId: Buffer;
|
|
129
|
+
primaryGeneration: bigint;
|
|
130
|
+
walFormatVersion: number;
|
|
131
|
+
catalogVersion: number;
|
|
132
|
+
segmentFormatVersion: number;
|
|
133
|
+
} | {
|
|
134
|
+
type: "SyncAck";
|
|
135
|
+
replicaId: string;
|
|
136
|
+
appliedLsn: bigint;
|
|
137
|
+
remoteLsn: bigint;
|
|
138
|
+
} | {
|
|
139
|
+
type: "SyncStatusResult";
|
|
140
|
+
status: WireSyncStatus;
|
|
141
|
+
} | {
|
|
142
|
+
type: "SyncPullResult";
|
|
143
|
+
status: WireSyncStatus;
|
|
144
|
+
units: WireRetainedUnit[];
|
|
145
|
+
hasMore: boolean;
|
|
146
|
+
} | {
|
|
147
|
+
type: "SyncAckResult";
|
|
148
|
+
previousAppliedLsn: bigint;
|
|
149
|
+
appliedLsn: bigint;
|
|
150
|
+
remoteLsn: bigint;
|
|
151
|
+
advanced: boolean;
|
|
152
|
+
status: WireSyncStatus;
|
|
85
153
|
} | {
|
|
86
154
|
type: "ResultRows";
|
|
87
155
|
columns: string[];
|
package/dist/protocol.js
CHANGED
|
@@ -17,6 +17,13 @@ export const MSG_QUERY = 0x03;
|
|
|
17
17
|
export const MSG_QUERY_PARAMS = 0x04;
|
|
18
18
|
/** SQL query frame. Plain Query remains PowQL for backward compatibility. */
|
|
19
19
|
export const MSG_QUERY_SQL = 0x05;
|
|
20
|
+
/** Private embedded-sync control frames. Requires authenticated server support. */
|
|
21
|
+
export const MSG_SYNC_STATUS = 0x20;
|
|
22
|
+
export const MSG_SYNC_PULL = 0x21;
|
|
23
|
+
export const MSG_SYNC_ACK = 0x22;
|
|
24
|
+
export const MSG_SYNC_STATUS_RESULT = 0x23;
|
|
25
|
+
export const MSG_SYNC_PULL_RESULT = 0x24;
|
|
26
|
+
export const MSG_SYNC_ACK_RESULT = 0x25;
|
|
20
27
|
export const MSG_RESULT_ROWS = 0x07;
|
|
21
28
|
export const MSG_RESULT_SCALAR = 0x08;
|
|
22
29
|
export const MSG_RESULT_OK = 0x09;
|
|
@@ -34,6 +41,12 @@ export const MAX_ROWS = 10_000_000;
|
|
|
34
41
|
export const MAX_COLUMNS = 4096;
|
|
35
42
|
/** Maximum number of bound parameters in a QueryWithParams message. */
|
|
36
43
|
export const MAX_PARAMS = 4096;
|
|
44
|
+
/** Maximum retained units accepted in one sync pull result. */
|
|
45
|
+
export const MAX_SYNC_UNITS = 4096;
|
|
46
|
+
/** Maximum retained units accepted by the server for one sync pull request. */
|
|
47
|
+
export const MAX_SYNC_PULL_UNITS = 4096;
|
|
48
|
+
/** Maximum retained-unit payload budget accepted by the server for one sync pull. */
|
|
49
|
+
export const MAX_SYNC_PULL_BYTES = 16 * 1024 * 1024;
|
|
37
50
|
// ───── Encoding ────────────────────────────────────────────────────────────
|
|
38
51
|
export function encode(msg) {
|
|
39
52
|
let msgType;
|
|
@@ -102,6 +115,63 @@ export function encode(msg) {
|
|
|
102
115
|
msgType = MSG_QUERY_PARAMS;
|
|
103
116
|
break;
|
|
104
117
|
}
|
|
118
|
+
case "SyncStatus":
|
|
119
|
+
payload = encodeString(msg.replicaId);
|
|
120
|
+
msgType = MSG_SYNC_STATUS;
|
|
121
|
+
break;
|
|
122
|
+
case "SyncPull": {
|
|
123
|
+
if (msg.databaseId.length !== 16) {
|
|
124
|
+
throw new Error(`sync databaseId must be exactly 16 bytes, got ${msg.databaseId.length}`);
|
|
125
|
+
}
|
|
126
|
+
payload = Buffer.concat([
|
|
127
|
+
encodeString(msg.replicaId),
|
|
128
|
+
u64LE(msg.sinceLsn),
|
|
129
|
+
u32LE(msg.maxUnits),
|
|
130
|
+
u64LE(msg.maxBytes),
|
|
131
|
+
msg.databaseId,
|
|
132
|
+
u64LE(msg.primaryGeneration),
|
|
133
|
+
u16LE(msg.walFormatVersion),
|
|
134
|
+
u16LE(msg.catalogVersion),
|
|
135
|
+
u16LE(msg.segmentFormatVersion),
|
|
136
|
+
]);
|
|
137
|
+
msgType = MSG_SYNC_PULL;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
case "SyncAck":
|
|
141
|
+
payload = Buffer.concat([
|
|
142
|
+
encodeString(msg.replicaId),
|
|
143
|
+
u64LE(msg.appliedLsn),
|
|
144
|
+
u64LE(msg.remoteLsn),
|
|
145
|
+
]);
|
|
146
|
+
msgType = MSG_SYNC_ACK;
|
|
147
|
+
break;
|
|
148
|
+
case "SyncStatusResult":
|
|
149
|
+
payload = encodeSyncStatus(msg.status);
|
|
150
|
+
msgType = MSG_SYNC_STATUS_RESULT;
|
|
151
|
+
break;
|
|
152
|
+
case "SyncPullResult": {
|
|
153
|
+
const parts = [
|
|
154
|
+
encodeSyncStatus(msg.status),
|
|
155
|
+
u32LE(msg.units.length),
|
|
156
|
+
];
|
|
157
|
+
for (const unit of msg.units) {
|
|
158
|
+
parts.push(encodeRetainedUnit(unit));
|
|
159
|
+
}
|
|
160
|
+
parts.push(Buffer.from([msg.hasMore ? 1 : 0]));
|
|
161
|
+
payload = Buffer.concat(parts);
|
|
162
|
+
msgType = MSG_SYNC_PULL_RESULT;
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
case "SyncAckResult":
|
|
166
|
+
payload = Buffer.concat([
|
|
167
|
+
u64LE(msg.previousAppliedLsn),
|
|
168
|
+
u64LE(msg.appliedLsn),
|
|
169
|
+
u64LE(msg.remoteLsn),
|
|
170
|
+
Buffer.from([msg.advanced ? 1 : 0]),
|
|
171
|
+
encodeSyncStatus(msg.status),
|
|
172
|
+
]);
|
|
173
|
+
msgType = MSG_SYNC_ACK_RESULT;
|
|
174
|
+
break;
|
|
105
175
|
case "ResultRows": {
|
|
106
176
|
const parts = [];
|
|
107
177
|
const colCount = Buffer.alloc(2);
|
|
@@ -235,6 +305,70 @@ function decodePayload(msgType, payload) {
|
|
|
235
305
|
}
|
|
236
306
|
return { type: "QueryWithParams", query, params };
|
|
237
307
|
}
|
|
308
|
+
case MSG_SYNC_STATUS:
|
|
309
|
+
return { type: "SyncStatus", replicaId: decodeString(payload, cursor) };
|
|
310
|
+
case MSG_SYNC_PULL: {
|
|
311
|
+
const replicaId = decodeString(payload, cursor);
|
|
312
|
+
const sinceLsn = readU64(payload, cursor, "sync pull since LSN");
|
|
313
|
+
const maxUnits = readU32(payload, cursor, "sync pull max units");
|
|
314
|
+
const maxBytes = readU64(payload, cursor, "sync pull max bytes");
|
|
315
|
+
const databaseId = readFixedBytes(payload, cursor, 16, "sync database id");
|
|
316
|
+
const primaryGeneration = readU64(payload, cursor, "sync primary generation");
|
|
317
|
+
const walFormatVersion = readU16(payload, cursor, "sync WAL format version");
|
|
318
|
+
const catalogVersion = readU16(payload, cursor, "sync catalog version");
|
|
319
|
+
const segmentFormatVersion = readU16(payload, cursor, "sync segment format version");
|
|
320
|
+
return {
|
|
321
|
+
type: "SyncPull",
|
|
322
|
+
replicaId,
|
|
323
|
+
sinceLsn,
|
|
324
|
+
maxUnits,
|
|
325
|
+
maxBytes,
|
|
326
|
+
databaseId,
|
|
327
|
+
primaryGeneration,
|
|
328
|
+
walFormatVersion,
|
|
329
|
+
catalogVersion,
|
|
330
|
+
segmentFormatVersion,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
case MSG_SYNC_ACK: {
|
|
334
|
+
const replicaId = decodeString(payload, cursor);
|
|
335
|
+
const appliedLsn = readU64(payload, cursor, "sync ack applied LSN");
|
|
336
|
+
const remoteLsn = readU64(payload, cursor, "sync ack remote LSN");
|
|
337
|
+
return { type: "SyncAck", replicaId, appliedLsn, remoteLsn };
|
|
338
|
+
}
|
|
339
|
+
case MSG_SYNC_STATUS_RESULT:
|
|
340
|
+
return {
|
|
341
|
+
type: "SyncStatusResult",
|
|
342
|
+
status: decodeSyncStatus(payload, cursor),
|
|
343
|
+
};
|
|
344
|
+
case MSG_SYNC_PULL_RESULT: {
|
|
345
|
+
const status = decodeSyncStatus(payload, cursor);
|
|
346
|
+
const count = readU32(payload, cursor, "sync retained unit count");
|
|
347
|
+
if (count > MAX_SYNC_UNITS) {
|
|
348
|
+
throw new Error(`too many retained units: ${count} (max ${MAX_SYNC_UNITS})`);
|
|
349
|
+
}
|
|
350
|
+
const units = [];
|
|
351
|
+
for (let i = 0; i < count; i++) {
|
|
352
|
+
units.push(decodeRetainedUnit(payload, cursor));
|
|
353
|
+
}
|
|
354
|
+
const hasMore = readBool(payload, cursor, "sync has_more");
|
|
355
|
+
return { type: "SyncPullResult", status, units, hasMore };
|
|
356
|
+
}
|
|
357
|
+
case MSG_SYNC_ACK_RESULT: {
|
|
358
|
+
const previousAppliedLsn = readU64(payload, cursor, "previous applied LSN");
|
|
359
|
+
const appliedLsn = readU64(payload, cursor, "applied LSN");
|
|
360
|
+
const remoteLsn = readU64(payload, cursor, "remote LSN");
|
|
361
|
+
const advanced = readBool(payload, cursor, "sync ack advanced");
|
|
362
|
+
const status = decodeSyncStatus(payload, cursor);
|
|
363
|
+
return {
|
|
364
|
+
type: "SyncAckResult",
|
|
365
|
+
previousAppliedLsn,
|
|
366
|
+
appliedLsn,
|
|
367
|
+
remoteLsn,
|
|
368
|
+
advanced,
|
|
369
|
+
status,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
238
372
|
case MSG_RESULT_ROWS: {
|
|
239
373
|
const colCount = readU16(payload, cursor, "column count");
|
|
240
374
|
if (colCount > MAX_COLUMNS) {
|
|
@@ -248,6 +382,14 @@ function decodePayload(msgType, payload) {
|
|
|
248
382
|
if (rowCount > MAX_ROWS) {
|
|
249
383
|
throw new Error(`too many rows: ${rowCount} (max ${MAX_ROWS})`);
|
|
250
384
|
}
|
|
385
|
+
if (colCount === 0 && rowCount > 0) {
|
|
386
|
+
throw new Error("nonzero row count with zero columns");
|
|
387
|
+
}
|
|
388
|
+
const remaining = BigInt(payload.length - cursor.pos);
|
|
389
|
+
const minRowBytes = BigInt(rowCount) * BigInt(colCount) * 4n;
|
|
390
|
+
if (remaining < minRowBytes) {
|
|
391
|
+
throw new Error(`row data too short for declared shape: ${rowCount} rows x ${colCount} columns`);
|
|
392
|
+
}
|
|
251
393
|
const rows = [];
|
|
252
394
|
for (let r = 0; r < rowCount; r++) {
|
|
253
395
|
const row = [];
|
|
@@ -278,6 +420,112 @@ function decodePayload(msgType, payload) {
|
|
|
278
420
|
throw new Error(`unknown message type: 0x${msgType.toString(16)}`);
|
|
279
421
|
}
|
|
280
422
|
}
|
|
423
|
+
function encodeSyncStatus(status) {
|
|
424
|
+
return Buffer.concat([
|
|
425
|
+
encodeString(status.replicaId),
|
|
426
|
+
Buffer.from([status.active ? 1 : 0]),
|
|
427
|
+
encodeOptionalU64(status.lastAppliedLsn),
|
|
428
|
+
u64LE(status.remoteLsn),
|
|
429
|
+
encodeOptionalU64(status.servableLsn),
|
|
430
|
+
encodeOptionalU64(status.unarchivedLsn),
|
|
431
|
+
encodeOptionalU64(status.lagLsn),
|
|
432
|
+
encodeOptionalU64(status.lagBytes),
|
|
433
|
+
encodeOptionalU64(status.lagMs),
|
|
434
|
+
Buffer.from([
|
|
435
|
+
status.stale ? 1 : 0,
|
|
436
|
+
encodeRepairAction(status.repairAction),
|
|
437
|
+
]),
|
|
438
|
+
encodeOptionalString(status.lastSyncError),
|
|
439
|
+
]);
|
|
440
|
+
}
|
|
441
|
+
function decodeSyncStatus(buf, cursor) {
|
|
442
|
+
const replicaId = decodeString(buf, cursor);
|
|
443
|
+
const active = readBool(buf, cursor, "sync status active");
|
|
444
|
+
const lastAppliedLsn = readOptionalU64(buf, cursor, "sync status last applied LSN");
|
|
445
|
+
const remoteLsn = readU64(buf, cursor, "sync status remote LSN");
|
|
446
|
+
const servableLsn = readOptionalU64(buf, cursor, "sync status servable LSN");
|
|
447
|
+
const unarchivedLsn = readOptionalU64(buf, cursor, "sync status unarchived LSN");
|
|
448
|
+
const lagLsn = readOptionalU64(buf, cursor, "sync status lag LSN");
|
|
449
|
+
const lagBytes = readOptionalU64(buf, cursor, "sync status lag bytes");
|
|
450
|
+
const lagMs = readOptionalU64(buf, cursor, "sync status lag milliseconds");
|
|
451
|
+
const stale = readBool(buf, cursor, "sync status stale");
|
|
452
|
+
const repairAction = decodeRepairAction(readU8(buf, cursor, "sync repair action"));
|
|
453
|
+
const lastSyncError = readOptionalString(buf, cursor, "sync status last error");
|
|
454
|
+
return {
|
|
455
|
+
replicaId,
|
|
456
|
+
active,
|
|
457
|
+
lastAppliedLsn,
|
|
458
|
+
remoteLsn,
|
|
459
|
+
servableLsn,
|
|
460
|
+
unarchivedLsn,
|
|
461
|
+
lagLsn,
|
|
462
|
+
lagBytes,
|
|
463
|
+
lagMs,
|
|
464
|
+
stale,
|
|
465
|
+
repairAction,
|
|
466
|
+
lastSyncError,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
function encodeRetainedUnit(unit) {
|
|
470
|
+
return Buffer.concat([
|
|
471
|
+
u64LE(unit.txId),
|
|
472
|
+
Buffer.from([u8(unit.recordType, "sync retained unit record type")]),
|
|
473
|
+
u64LE(unit.lsn),
|
|
474
|
+
encodeBytes(unit.data),
|
|
475
|
+
]);
|
|
476
|
+
}
|
|
477
|
+
function decodeRetainedUnit(buf, cursor) {
|
|
478
|
+
const txId = readU64(buf, cursor, "sync retained unit tx id");
|
|
479
|
+
const recordType = readU8(buf, cursor, "sync retained unit record type");
|
|
480
|
+
const lsn = readU64(buf, cursor, "sync retained unit LSN");
|
|
481
|
+
const data = readBytes(buf, cursor, "sync retained unit payload");
|
|
482
|
+
return { txId, recordType, lsn, data };
|
|
483
|
+
}
|
|
484
|
+
function encodeRepairAction(action) {
|
|
485
|
+
switch (action) {
|
|
486
|
+
case "none":
|
|
487
|
+
return 0;
|
|
488
|
+
case "pull":
|
|
489
|
+
return 1;
|
|
490
|
+
case "awaitArchive":
|
|
491
|
+
return 2;
|
|
492
|
+
case "rebootstrap":
|
|
493
|
+
return 3;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
function decodeRepairAction(raw) {
|
|
497
|
+
switch (raw) {
|
|
498
|
+
case 0:
|
|
499
|
+
return "none";
|
|
500
|
+
case 1:
|
|
501
|
+
return "pull";
|
|
502
|
+
case 2:
|
|
503
|
+
return "awaitArchive";
|
|
504
|
+
case 3:
|
|
505
|
+
return "rebootstrap";
|
|
506
|
+
default:
|
|
507
|
+
throw new Error(`unknown sync repair action: ${raw}`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
function encodeOptionalU64(value) {
|
|
511
|
+
return value === null
|
|
512
|
+
? Buffer.from([0])
|
|
513
|
+
: Buffer.concat([Buffer.from([1]), u64LE(value)]);
|
|
514
|
+
}
|
|
515
|
+
function readOptionalU64(buf, cursor, label) {
|
|
516
|
+
return readBool(buf, cursor, label) ? readU64(buf, cursor, label) : null;
|
|
517
|
+
}
|
|
518
|
+
function encodeOptionalString(value) {
|
|
519
|
+
return value === null
|
|
520
|
+
? Buffer.from([0])
|
|
521
|
+
: Buffer.concat([Buffer.from([1]), encodeString(value)]);
|
|
522
|
+
}
|
|
523
|
+
function readOptionalString(buf, cursor, label) {
|
|
524
|
+
return readBool(buf, cursor, label) ? decodeString(buf, cursor) : null;
|
|
525
|
+
}
|
|
526
|
+
function encodeBytes(bytes) {
|
|
527
|
+
return Buffer.concat([u32LE(bytes.length), bytes]);
|
|
528
|
+
}
|
|
281
529
|
// ───── String helpers ──────────────────────────────────────────────────────
|
|
282
530
|
function encodeString(s) {
|
|
283
531
|
const bytes = Buffer.from(s, "utf8");
|
|
@@ -306,6 +554,14 @@ function readU8(buf, cursor, label) {
|
|
|
306
554
|
cursor.pos += 1;
|
|
307
555
|
return value;
|
|
308
556
|
}
|
|
557
|
+
function readBool(buf, cursor, label) {
|
|
558
|
+
const raw = readU8(buf, cursor, label);
|
|
559
|
+
if (raw === 0)
|
|
560
|
+
return false;
|
|
561
|
+
if (raw === 1)
|
|
562
|
+
return true;
|
|
563
|
+
throw new Error(`invalid boolean for ${label}: ${raw}`);
|
|
564
|
+
}
|
|
309
565
|
function readI64(buf, cursor, label) {
|
|
310
566
|
if (cursor.pos + 8 > buf.length) {
|
|
311
567
|
throw new Error(`truncated ${label}`);
|
|
@@ -346,8 +602,39 @@ function readU64(buf, cursor, label) {
|
|
|
346
602
|
cursor.pos += 8;
|
|
347
603
|
return value;
|
|
348
604
|
}
|
|
605
|
+
function readFixedBytes(buf, cursor, len, label) {
|
|
606
|
+
if (cursor.pos + len > buf.length) {
|
|
607
|
+
throw new Error(`truncated ${label}`);
|
|
608
|
+
}
|
|
609
|
+
const value = Buffer.from(buf.subarray(cursor.pos, cursor.pos + len));
|
|
610
|
+
cursor.pos += len;
|
|
611
|
+
return value;
|
|
612
|
+
}
|
|
613
|
+
function readBytes(buf, cursor, label) {
|
|
614
|
+
const len = readU32(buf, cursor, label);
|
|
615
|
+
return readFixedBytes(buf, cursor, len, label);
|
|
616
|
+
}
|
|
617
|
+
function u16LE(n) {
|
|
618
|
+
const b = Buffer.alloc(2);
|
|
619
|
+
b.writeUInt16LE(n, 0);
|
|
620
|
+
return b;
|
|
621
|
+
}
|
|
622
|
+
function u8(n, label) {
|
|
623
|
+
if (!Number.isInteger(n) || n < 0 || n > 0xff) {
|
|
624
|
+
throw new Error(`${label} must fit in u8`);
|
|
625
|
+
}
|
|
626
|
+
return n;
|
|
627
|
+
}
|
|
349
628
|
function u32LE(n) {
|
|
350
629
|
const b = Buffer.alloc(4);
|
|
351
630
|
b.writeUInt32LE(n, 0);
|
|
352
631
|
return b;
|
|
353
632
|
}
|
|
633
|
+
function u64LE(n) {
|
|
634
|
+
if (n < 0n || n > 0xffffffffffffffffn) {
|
|
635
|
+
throw new Error(`u64 value out of range: ${n}`);
|
|
636
|
+
}
|
|
637
|
+
const b = Buffer.alloc(8);
|
|
638
|
+
b.writeBigUInt64LE(n, 0);
|
|
639
|
+
return b;
|
|
640
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zvndev/powdb-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "TypeScript client for PowDB (PowQL wire protocol)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.29.3",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"test": "tsx test/run-with-server.ts test/client.test.ts",
|
|
52
52
|
"test:escape": "tsx test/escape.test.ts",
|
|
53
53
|
"test:protocol": "tsx test/protocol.test.ts",
|
|
54
|
+
"test:sync-live": "tsx test/sync-live.test.ts",
|
|
54
55
|
"test:auth": "tsx test/auth.test.ts",
|
|
55
56
|
"test:pool": "tsx test/run-with-server.ts test/pool.test.ts",
|
|
56
57
|
"test:typed": "tsx test/typed.test.ts",
|