@riffdb/client 0.0.0-stage → 0.2.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.
@@ -0,0 +1,65 @@
1
+ /** Exact private reimport-driver protocol generation. */
2
+ export declare const OPERATOR_DRIVER_PROTOCOL_VERSION: 1;
3
+ export interface OperatorIdentity {
4
+ readonly database: string;
5
+ readonly campaignId: string;
6
+ readonly portabilityManifestHash: string;
7
+ }
8
+ export interface ReimportOperation {
9
+ readonly campaignId: string;
10
+ readonly contractLineage: string;
11
+ readonly scope: string;
12
+ readonly portabilityManifestHash: string;
13
+ readonly exportManifestHash: string;
14
+ readonly exportReceiptHash: string;
15
+ readonly sourceDatabaseId: string;
16
+ readonly targetDatabaseId: string;
17
+ readonly sourceRows: bigint;
18
+ readonly sourcePages: bigint;
19
+ readonly nextPage: bigint;
20
+ readonly rowsApplied: bigint;
21
+ readonly phase: string;
22
+ readonly failure?: string;
23
+ readonly canonicalReimportReceiptJson?: string;
24
+ readonly reimportReceiptHash?: string;
25
+ }
26
+ export interface ReimportPage {
27
+ readonly exportOperationId: string;
28
+ readonly pageNumber: bigint;
29
+ readonly canonicalJsonLines: ReadonlyArray<string>;
30
+ readonly nextCursorBase64?: string;
31
+ readonly classComplete: boolean;
32
+ readonly operationComplete: boolean;
33
+ readonly pageHashHex: string;
34
+ readonly maximumAttempts?: number;
35
+ }
36
+ export interface OperatorErrorDetails {
37
+ readonly code: string;
38
+ readonly category: string;
39
+ readonly message: string;
40
+ readonly retryability: string;
41
+ readonly recoveryAction: string;
42
+ readonly outcomeUncertain: boolean;
43
+ }
44
+ /** Typed public-safe failure returned by the Rust operator host. */
45
+ export declare class OperatorError extends Error {
46
+ readonly details: OperatorErrorDetails;
47
+ readonly name = "OperatorError";
48
+ constructor(details: OperatorErrorDetails);
49
+ }
50
+ /**
51
+ * One serial connection to a Rust-configured reimport campaign.
52
+ *
53
+ * This surface has no application invocation, remote endpoint, TLS, bearer
54
+ * credential, manifest selection, or raw import method.
55
+ */
56
+ export declare class OperatorTransport {
57
+ #private;
58
+ private constructor();
59
+ static connect(socketPath: string, identity: OperatorIdentity): Promise<OperatorTransport>;
60
+ start(canonicalExportManifestJson: string, canonicalExportReceiptJson: string, maximumAttempts?: number): Promise<ReimportOperation>;
61
+ applyPage(page: ReimportPage): Promise<ReimportOperation>;
62
+ status(): Promise<ReimportOperation | undefined>;
63
+ cancel(maximumAttempts?: number): Promise<ReimportOperation | undefined>;
64
+ shutdown(): Promise<void>;
65
+ }
@@ -0,0 +1,313 @@
1
+ import { createConnection } from "node:net";
2
+ const MAX_OPERATOR_FRAME_BYTES = 4 * 1_024 * 1_024 + 256 * 1_024;
3
+ const MAX_PAGE_ITEMS = 500;
4
+ const HASH = /^[0-9a-f]{64}$/;
5
+ const UUID7 = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
6
+ /** Exact private reimport-driver protocol generation. */
7
+ export const OPERATOR_DRIVER_PROTOCOL_VERSION = 1;
8
+ /** Typed public-safe failure returned by the Rust operator host. */
9
+ export class OperatorError extends Error {
10
+ details;
11
+ name = "OperatorError";
12
+ constructor(details) {
13
+ super(`${details.code}: ${details.message}`);
14
+ this.details = details;
15
+ }
16
+ }
17
+ /**
18
+ * One serial connection to a Rust-configured reimport campaign.
19
+ *
20
+ * This surface has no application invocation, remote endpoint, TLS, bearer
21
+ * credential, manifest selection, or raw import method.
22
+ */
23
+ export class OperatorTransport {
24
+ #socket;
25
+ #identity;
26
+ #input = Buffer.alloc(0);
27
+ #pending;
28
+ #nextRequest = 1;
29
+ #closed = false;
30
+ constructor(socket, identity) {
31
+ this.#socket = socket;
32
+ this.#identity = identity;
33
+ socket.on("data", (chunk) => this.#receive(chunk));
34
+ socket.on("error", () => this.#fail(new Error("RiffDB operator session failed")));
35
+ socket.on("close", () => this.#fail(new Error("RiffDB operator session closed")));
36
+ }
37
+ static async connect(socketPath, identity) {
38
+ validateIdentity(socketPath, identity);
39
+ const socket = createConnection({ path: socketPath });
40
+ await new Promise((resolve, reject) => {
41
+ const onError = () => reject(new Error("RiffDB operator session failed"));
42
+ socket.once("connect", () => { socket.off("error", onError); resolve(); });
43
+ socket.once("error", onError);
44
+ });
45
+ const transport = new OperatorTransport(socket, identity);
46
+ try {
47
+ const requestId = transport.#requestId("handshake");
48
+ const response = await transport.#request({
49
+ type: "handshake",
50
+ request_id: requestId,
51
+ protocol_version: OPERATOR_DRIVER_PROTOCOL_VERSION,
52
+ database: identity.database,
53
+ campaign_id: identity.campaignId,
54
+ portability_manifest_hash: identity.portabilityManifestHash,
55
+ }, requestId);
56
+ if (!hasExactKeys(response, ["type", "request_id", "protocol_version", "driver_identity", "database", "campaign_id", "portability_manifest_hash"])
57
+ || response.type !== "handshake"
58
+ || response.protocol_version !== OPERATOR_DRIVER_PROTOCOL_VERSION
59
+ || typeof response.driver_identity !== "string" || response.driver_identity.length === 0
60
+ || response.database !== identity.database || response.campaign_id !== identity.campaignId
61
+ || response.portability_manifest_hash !== identity.portabilityManifestHash) {
62
+ throw new Error("RiffDB operator identity mismatch");
63
+ }
64
+ return transport;
65
+ }
66
+ catch (error) {
67
+ socket.destroy();
68
+ throw error;
69
+ }
70
+ }
71
+ async start(canonicalExportManifestJson, canonicalExportReceiptJson, maximumAttempts = 3) {
72
+ if (!canonicalDocument(canonicalExportManifestJson, 256 * 1_024)
73
+ || !canonicalDocument(canonicalExportReceiptJson, 256 * 1_024)) {
74
+ throw new Error("invalid RiffDB operator start request");
75
+ }
76
+ const requestId = this.#requestId("start");
77
+ const response = await this.#request({
78
+ type: "start", request_id: requestId,
79
+ canonical_export_manifest_json: canonicalExportManifestJson,
80
+ canonical_export_receipt_json: canonicalExportReceiptJson,
81
+ maximum_attempts: checkedAttempts(maximumAttempts),
82
+ }, requestId);
83
+ return requiredOperation(response);
84
+ }
85
+ async applyPage(page) {
86
+ validatePage(page);
87
+ const requestId = this.#requestId("page");
88
+ const response = await this.#request({
89
+ type: "apply_page", request_id: requestId,
90
+ export_operation_id: page.exportOperationId,
91
+ page_number: page.pageNumber,
92
+ canonical_json_lines: page.canonicalJsonLines,
93
+ next_cursor_base64: page.nextCursorBase64 ?? null,
94
+ class_complete: page.classComplete,
95
+ operation_complete: page.operationComplete,
96
+ page_hash_hex: page.pageHashHex,
97
+ maximum_attempts: checkedAttempts(page.maximumAttempts ?? 3),
98
+ }, requestId);
99
+ return requiredOperation(response);
100
+ }
101
+ async status() {
102
+ const requestId = this.#requestId("status");
103
+ return optionalOperation(await this.#request({ type: "status", request_id: requestId }, requestId));
104
+ }
105
+ async cancel(maximumAttempts = 3) {
106
+ const requestId = this.#requestId("cancel");
107
+ return optionalOperation(await this.#request({
108
+ type: "cancel", request_id: requestId, maximum_attempts: checkedAttempts(maximumAttempts),
109
+ }, requestId));
110
+ }
111
+ async shutdown() {
112
+ if (this.#closed)
113
+ return;
114
+ this.#closed = true;
115
+ this.#fail(new Error("RiffDB operator session closed"));
116
+ await new Promise((resolve) => {
117
+ if (this.#socket.destroyed) {
118
+ resolve();
119
+ return;
120
+ }
121
+ this.#socket.end(resolve);
122
+ });
123
+ }
124
+ async #request(request, requestId) {
125
+ if (this.#closed || this.#socket.destroyed)
126
+ throw new Error("RiffDB operator session closed");
127
+ if (this.#pending !== undefined)
128
+ throw new Error("RiffDB operator session is busy");
129
+ const frame = encodeFrame(request);
130
+ const response = new Promise((resolve, reject) => {
131
+ this.#pending = { id: requestId, resolve, reject };
132
+ this.#socket.write(frame, (error) => {
133
+ if (error === null || error === undefined)
134
+ return;
135
+ this.#pending = undefined;
136
+ reject(new Error("RiffDB operator session failed"));
137
+ });
138
+ });
139
+ const value = await response;
140
+ if (value.type === "error")
141
+ throw decodeError(value);
142
+ return value;
143
+ }
144
+ #receive(chunk) {
145
+ if (this.#closed)
146
+ return;
147
+ this.#input = Buffer.concat([this.#input, chunk]);
148
+ while (this.#input.length >= 4) {
149
+ const length = this.#input.readUInt32BE(0);
150
+ if (length < 2 || length > MAX_OPERATOR_FRAME_BYTES) {
151
+ this.#fail(new Error("RiffDB operator returned an invalid frame"));
152
+ this.#socket.destroy();
153
+ return;
154
+ }
155
+ if (this.#input.length < length + 4)
156
+ return;
157
+ const body = this.#input.subarray(4, length + 4);
158
+ this.#input = this.#input.subarray(length + 4);
159
+ let response;
160
+ try {
161
+ const decoded = JSON.parse(body.toString("utf8"));
162
+ response = exactObject(decoded);
163
+ }
164
+ catch {
165
+ this.#fail(new Error("RiffDB operator returned an invalid response"));
166
+ this.#socket.destroy();
167
+ return;
168
+ }
169
+ const pending = this.#pending;
170
+ if (pending === undefined || response.request_id !== pending.id) {
171
+ this.#fail(new Error("RiffDB operator returned an invalid request identity"));
172
+ this.#socket.destroy();
173
+ return;
174
+ }
175
+ this.#pending = undefined;
176
+ pending.resolve(response);
177
+ }
178
+ }
179
+ #fail(error) {
180
+ const pending = this.#pending;
181
+ this.#pending = undefined;
182
+ pending?.reject(error);
183
+ }
184
+ #requestId(kind) {
185
+ const value = `ts.operator.${kind}.${this.#nextRequest}`;
186
+ this.#nextRequest = this.#nextRequest === Number.MAX_SAFE_INTEGER ? 1 : this.#nextRequest + 1;
187
+ return value;
188
+ }
189
+ }
190
+ function encodeFrame(value) {
191
+ const body = Buffer.from(encodeCanonical(value), "utf8");
192
+ if (body.length < 2 || body.length > MAX_OPERATOR_FRAME_BYTES)
193
+ throw new Error("RiffDB operator request exceeds the frame bound");
194
+ const output = Buffer.allocUnsafe(body.length + 4);
195
+ output.writeUInt32BE(body.length, 0);
196
+ body.copy(output, 4);
197
+ return output;
198
+ }
199
+ function encodeCanonical(value) {
200
+ if (value === null)
201
+ return "null";
202
+ if (typeof value === "string" || typeof value === "boolean")
203
+ return JSON.stringify(value);
204
+ if (typeof value === "number") {
205
+ if (!Number.isSafeInteger(value) || value < 0)
206
+ throw new Error("invalid RiffDB operator integer");
207
+ return value.toString();
208
+ }
209
+ if (typeof value === "bigint") {
210
+ if (value < 0n || value > 18446744073709551615n)
211
+ throw new Error("invalid RiffDB operator integer");
212
+ return value.toString();
213
+ }
214
+ if (Array.isArray(value))
215
+ return `[${value.map(encodeCanonical).join(",")}]`;
216
+ if (typeof value === "object" && value !== null) {
217
+ return `{${Object.entries(value).map(([name, item]) => `${JSON.stringify(name)}:${encodeCanonical(item)}`).join(",")}}`;
218
+ }
219
+ throw new Error("invalid RiffDB operator request");
220
+ }
221
+ function validateIdentity(socketPath, value) {
222
+ if (!socketPath.startsWith("/") || socketPath.length > 4_096 || !short(value.database)
223
+ || !UUID7.test(value.campaignId) || !HASH.test(value.portabilityManifestHash)) {
224
+ throw new Error("invalid RiffDB operator configuration");
225
+ }
226
+ }
227
+ function validatePage(value) {
228
+ const bytes = value.canonicalJsonLines.reduce((total, item) => total + Buffer.byteLength(item) + 1, 0);
229
+ if (!UUID7.test(value.exportOperationId) || value.pageNumber < 1n
230
+ || value.canonicalJsonLines.length < 1 || value.canonicalJsonLines.length > MAX_PAGE_ITEMS
231
+ || bytes > 4 * 1_024 * 1_024 || value.canonicalJsonLines.some((line) => !canonicalDocument(line, 64 * 1_024))
232
+ || !HASH.test(value.pageHashHex) || value.operationComplete !== (value.nextCursorBase64 === undefined)
233
+ || (value.operationComplete && !value.classComplete)
234
+ || (value.nextCursorBase64 !== undefined && (value.nextCursorBase64.length < 1 || value.nextCursorBase64.length > 1_024))) {
235
+ throw new Error("invalid RiffDB operator page");
236
+ }
237
+ }
238
+ function requiredOperation(response) {
239
+ const operation = optionalOperation(response);
240
+ if (operation === undefined)
241
+ throw new Error("RiffDB operator campaign was not found");
242
+ return operation;
243
+ }
244
+ function optionalOperation(response) {
245
+ if (response.type === "not_found" && hasExactKeys(response, ["type", "request_id"]))
246
+ return undefined;
247
+ if (response.type !== "operation" || !hasExactKeys(response, ["type", "request_id", "operation"])) {
248
+ throw new Error("RiffDB operator returned an invalid response");
249
+ }
250
+ const value = exactObject(response.operation);
251
+ if (!hasExactKeys(value, [
252
+ "campaign_id", "contract_lineage", "scope", "portability_manifest_hash",
253
+ "export_manifest_hash", "export_receipt_hash", "source_database_id", "target_database_id",
254
+ "source_rows", "source_pages", "next_page", "rows_applied", "phase", "failure",
255
+ "canonical_reimport_receipt_json", "reimport_receipt_hash",
256
+ ]))
257
+ throw new Error("RiffDB operator returned invalid progress");
258
+ const failure = optionalShort(value.failure);
259
+ const receipt = optionalCanonicalReceipt(value.canonical_reimport_receipt_json);
260
+ const receiptHash = optionalHash(value.reimport_receipt_hash);
261
+ return {
262
+ campaignId: uuid7(value.campaign_id), contractLineage: requiredShort(value.contract_lineage),
263
+ scope: requiredShort(value.scope), portabilityManifestHash: requiredHash(value.portability_manifest_hash),
264
+ exportManifestHash: requiredHash(value.export_manifest_hash), exportReceiptHash: requiredHash(value.export_receipt_hash),
265
+ sourceDatabaseId: uuid7(value.source_database_id), targetDatabaseId: uuid7(value.target_database_id),
266
+ sourceRows: unsigned(value.source_rows), sourcePages: unsigned(value.source_pages), nextPage: unsigned(value.next_page),
267
+ rowsApplied: unsigned(value.rows_applied), phase: requiredShort(value.phase),
268
+ ...(failure === undefined ? {} : { failure }),
269
+ ...(receipt === undefined ? {} : { canonicalReimportReceiptJson: receipt }),
270
+ ...(receiptHash === undefined ? {} : { reimportReceiptHash: receiptHash }),
271
+ };
272
+ }
273
+ function decodeError(value) {
274
+ if (!hasExactKeys(value, ["type", "request_id", "code", "category", "message", "retryability", "recovery_action", "outcome_uncertain"])
275
+ || value.type !== "error" || typeof value.outcome_uncertain !== "boolean") {
276
+ throw new Error("RiffDB operator returned an invalid error");
277
+ }
278
+ return new OperatorError({
279
+ code: requiredShort(value.code), category: requiredShort(value.category), message: requiredShort(value.message),
280
+ retryability: requiredShort(value.retryability), recoveryAction: requiredShort(value.recovery_action),
281
+ outcomeUncertain: value.outcome_uncertain,
282
+ });
283
+ }
284
+ function exactObject(value) {
285
+ if (typeof value !== "object" || value === null || Array.isArray(value))
286
+ throw new Error("invalid RiffDB operator object");
287
+ return value;
288
+ }
289
+ function hasExactKeys(value, keys) {
290
+ const actual = Object.keys(value);
291
+ return actual.length === keys.length && keys.every((key) => Object.hasOwn(value, key));
292
+ }
293
+ function short(value) { return value.length > 0 && value.length <= 4_096 && !/[\n\r\0]/u.test(value); }
294
+ function requiredShort(value) { if (typeof value !== "string" || !short(value))
295
+ throw new Error("invalid RiffDB operator text"); return value; }
296
+ function optionalShort(value) { return value === null || value === undefined ? undefined : requiredShort(value); }
297
+ function requiredHash(value) { if (typeof value !== "string" || !HASH.test(value))
298
+ throw new Error("invalid RiffDB operator hash"); return value; }
299
+ function optionalHash(value) { return value === null || value === undefined ? undefined : requiredHash(value); }
300
+ function uuid7(value) { if (typeof value !== "string" || !UUID7.test(value))
301
+ throw new Error("invalid RiffDB operator identity"); return value; }
302
+ function unsigned(value) { if (typeof value !== "string" || !/^(?:0|[1-9][0-9]*)$/u.test(value))
303
+ throw new Error("invalid RiffDB operator count"); return BigInt(value); }
304
+ function checkedAttempts(value) { if (!Number.isInteger(value) || value < 1 || value > 10)
305
+ throw new Error("invalid RiffDB operator attempt bound"); return value; }
306
+ function canonicalDocument(value, maximum) { return Buffer.byteLength(value) <= maximum && value.startsWith("{") && value.endsWith("}") && !/[\n\r]/u.test(value); }
307
+ function optionalCanonicalReceipt(value) {
308
+ if (value === null || value === undefined)
309
+ return undefined;
310
+ if (typeof value !== "string" || !value.endsWith("\n") || !canonicalDocument(value.slice(0, -1), 256 * 1_024 - 1))
311
+ throw new Error("invalid RiffDB operator receipt");
312
+ return value;
313
+ }
package/package.json CHANGED
@@ -1,6 +1 @@
1
- {
2
- "name": "@riffdb/client",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
1
+ {"description":"First-party application runtime for the Rust-owned RiffDB driver host","engines":{"node":">=22"},"exports":{".":"./dist/index.js","./operator":"./dist/operator.js"},"files":["dist","README.md","conformance-manifest-v1.json"],"license":"MIT OR Apache-2.0","name":"@riffdb/client","repository":{"type":"git","url":"git+https://github.com/riffdb/riffdb.git"},"type":"module","types":"./dist/index.d.ts","version":"0.2.0"}