@yydb/yydb 0.0.12 → 0.0.15

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.
Files changed (53) hide show
  1. package/README.md +86 -86
  2. package/dist/api/database.d.ts +40 -0
  3. package/dist/api/database.js +16 -0
  4. package/dist/cli/cli.js +28 -1
  5. package/dist/cli/info-cmd.js +30 -1
  6. package/dist/cli/verify-cmd.d.ts +11 -0
  7. package/dist/cli/verify-cmd.js +73 -0
  8. package/dist/database.d.ts +50 -0
  9. package/dist/database.js +181 -0
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.js +1 -1
  12. package/dist/micro.d.ts +63 -0
  13. package/dist/micro.js +160 -0
  14. package/dist/native/binding.d.ts +8 -0
  15. package/dist/native.d.ts +9 -0
  16. package/dist/native.js +32 -0
  17. package/dist/node/cli/cli.d.ts +1 -0
  18. package/dist/node/cli/cli.js +25 -0
  19. package/dist/node/cli/info-cmd.d.ts +3 -0
  20. package/dist/node/cli/info-cmd.js +10 -0
  21. package/dist/node/cli/init-cmd.d.ts +3 -0
  22. package/dist/node/cli/init-cmd.js +18 -0
  23. package/dist/node/cli/options.d.ts +4 -0
  24. package/dist/node/cli/options.js +14 -0
  25. package/dist/node/cli/serve-cmd.d.ts +3 -0
  26. package/dist/node/cli/serve-cmd.js +14 -0
  27. package/dist/node/cli/version-cmd.d.ts +3 -0
  28. package/dist/node/cli/version-cmd.js +8 -0
  29. package/dist/node/cli.d.ts +2 -0
  30. package/dist/node/cli.js +6 -0
  31. package/dist/node/database.d.ts +50 -0
  32. package/dist/node/database.js +173 -0
  33. package/dist/node/index.d.ts +7 -0
  34. package/dist/node/index.js +5 -0
  35. package/dist/node/native.d.ts +9 -0
  36. package/dist/node/native.js +32 -0
  37. package/dist/node/resolve-bin.d.ts +15 -0
  38. package/dist/node/resolve-bin.js +62 -0
  39. package/dist/resolve-bin.d.ts +15 -0
  40. package/dist/resolve-bin.js +62 -0
  41. package/dist/shared/errors.d.ts +62 -15
  42. package/dist/shared/errors.js +64 -41
  43. package/dist/shared/wasm-envelope.d.ts +41 -0
  44. package/dist/shared/wasm-envelope.js +56 -0
  45. package/dist/udf-types.d.ts +26 -0
  46. package/dist/udf-types.js +16 -0
  47. package/dist/wasm/binding.d.ts +1 -0
  48. package/dist/wasm/binding.js +1 -0
  49. package/dist/wasm/database.d.ts +54 -0
  50. package/dist/wasm/database.js +118 -0
  51. package/dist/wasm/index.d.ts +8 -0
  52. package/dist/wasm/index.js +5 -0
  53. package/package.json +40 -40
package/README.md CHANGED
@@ -1,86 +1,86 @@
1
- # `@yydb/yydb`
2
-
3
- Node embedded engine and CLI. `Database.open` starts a private loopback `yydb serve` process and talks over the YY
4
- wire protocol; platform `.node` bindings install through optional dependencies.
5
-
6
- Browsers are served by the pure TypeScript [`@yydb/light-db`](../light-db/readme.md) package. This package no longer
7
- ships a WebAssembly entry.
8
-
9
- For a wire client only (renderer or an app that already runs `yydb serve`), use
10
- [`@yydb/yydb-client`](https://www.npmjs.com/package/@yydb/yydb-client).
11
-
12
- [![npm](https://img.shields.io/npm/v/@yydb/yydb)](https://www.npmjs.com/package/@yydb/yydb)
13
- [![Node.js](https://img.shields.io/node/v/@yydb/yydb)](https://www.npmjs.com/package/@yydb/yydb)
14
-
15
- ## Exports
16
-
17
- | Import | Runtime | Role |
18
- |--------|---------|------|
19
- | `@yydb/yydb` | Node | `Database.open`, micro helpers, VOS re-exports |
20
- | `@yydb/yydb/cli` | Node | CLI (`version`, `init`, `info`, `serve`) |
21
-
22
- Node.js 18+.
23
-
24
- Schema truth lives in the `.yydb` file via `ensureSchema` and VOS documents.
25
-
26
- ## Example
27
-
28
- ```ts
29
- import { Database } from '@yydb/yydb';
30
-
31
- const db = await Database.open('./data/app.yydb');
32
-
33
- await db.ensureSchema(
34
- 1,
35
- `table Setting {
36
- @@id: uuid,
37
- key: utf8,
38
- value: utf8,
39
- }`,
40
- );
41
-
42
- await db.put('theme', 'dark');
43
- console.log(await db.get('theme'));
44
- await db.close();
45
- ```
46
-
47
- The file is created when missing. Schema version and VOS document persist with the data.
48
-
49
- ## API
50
-
51
- ```ts
52
- const db = await Database.open(path, options?);
53
-
54
- await db.ensureSchema(version, vosDocument);
55
- await db.getSchema();
56
- await db.put(key, stringOrBytes);
57
- await db.get(key);
58
- await db.info();
59
- await db.serverVersion();
60
- await db.registerMicro(microDefinition);
61
- await db.callScalar('double', 1, [21]);
62
- await db.close();
63
- ```
64
-
65
- `OpenOptions`: `cli` (explicit CLI script path), `port` (preferred loopback port), `serveArgs` (extra `yydb serve`
66
- arguments). `binary` is a deprecated alias for `cli`.
67
-
68
- The native binding exposes additive operations (`schemaVersion`, `schemaDocument`, `recordCount`, `journalMode`,
69
- `checkpoint`, `walFrameCount`, `get`, `put`); the CLI composes commands from them.
70
-
71
- ## Related packages
72
-
73
- | Need | Package |
74
- |------|---------|
75
- | Node embedded engine and CLI (this package) | **`@yydb/yydb`** |
76
- | Browser, pure TypeScript | **`@yydb/light-db`** |
77
- | Wire client only | [`@yydb/yydb-client`](https://www.npmjs.com/package/@yydb/yydb-client) |
78
- | In-process Rust embed | [`yydb`](https://crates.io/crates/yydb) |
79
- | Agent skills (wire protocol) | [`@yydb/yydb-skills`](https://www.npmjs.com/package/@yydb/yydb-skills) |
80
-
81
- Electron: main process can use `@yydb/yydb`; renderer code can use `@yydb/yydb-client` against the main-process
82
- endpoint.
83
-
84
- The managed engine listens on `127.0.0.1` with no authentication — keep the endpoint private.
85
-
86
- [YYDB overview](https://github.com/yy-database/yydb.rs) · [User guides](https://github.com/yy-database/yydb.rs/tree/dev/projects/packages/homepage/documentation/zh-hans)
1
+ # `@yydb/yydb`
2
+
3
+ Node embedded engine and CLI. `Database.open` starts a private loopback `yydb serve` process and talks over the YY
4
+ wire protocol; platform `.node` bindings install through optional dependencies.
5
+
6
+ Browsers are served by the pure TypeScript [`@yydb/light-db`](../light-db/readme.md) package. This package no longer
7
+ ships a WebAssembly entry.
8
+
9
+ For a wire client only (renderer or an app that already runs `yydb serve`), use
10
+ [`@yydb/yydb-client`](https://www.npmjs.com/package/@yydb/yydb-client).
11
+
12
+ [![npm](https://img.shields.io/npm/v/@yydb/yydb)](https://www.npmjs.com/package/@yydb/yydb)
13
+ [![Node.js](https://img.shields.io/node/v/@yydb/yydb)](https://www.npmjs.com/package/@yydb/yydb)
14
+
15
+ ## Exports
16
+
17
+ | Import | Runtime | Role |
18
+ |--------|---------|------|
19
+ | `@yydb/yydb` | Node | `Database.open`, micro helpers, VOS re-exports |
20
+ | `@yydb/yydb/cli` | Node | CLI (`version`, `init`, `info`, `serve`) |
21
+
22
+ Node.js 18+.
23
+
24
+ Schema truth lives in the `.yydb` file via `ensureSchema` and VOS documents.
25
+
26
+ ## Example
27
+
28
+ ```ts
29
+ import { Database } from '@yydb/yydb';
30
+
31
+ const db = await Database.open('./data/app.yydb');
32
+
33
+ await db.ensureSchema(
34
+ 1,
35
+ `table Setting {
36
+ @@id: uuid,
37
+ key: utf8,
38
+ value: utf8,
39
+ }`,
40
+ );
41
+
42
+ await db.put('theme', 'dark');
43
+ console.log(await db.get('theme'));
44
+ await db.close();
45
+ ```
46
+
47
+ The file is created when missing. Schema version and VOS document persist with the data.
48
+
49
+ ## API
50
+
51
+ ```ts
52
+ const db = await Database.open(path, options?);
53
+
54
+ await db.ensureSchema(version, vosDocument);
55
+ await db.getSchema();
56
+ await db.put(key, stringOrBytes);
57
+ await db.get(key);
58
+ await db.info();
59
+ await db.serverVersion();
60
+ await db.registerMicro(microDefinition);
61
+ await db.callScalar('double', 1, [21]);
62
+ await db.close();
63
+ ```
64
+
65
+ `OpenOptions`: `cli` (explicit CLI script path), `port` (preferred loopback port), `serveArgs` (extra `yydb serve`
66
+ arguments). `binary` is a deprecated alias for `cli`.
67
+
68
+ The native binding exposes additive operations (`schemaVersion`, `schemaDocument`, `recordCount`, `journalMode`,
69
+ `checkpoint`, `walFrameCount`, `get`, `put`); the CLI composes commands from them.
70
+
71
+ ## Related packages
72
+
73
+ | Need | Package |
74
+ |------|---------|
75
+ | Node embedded engine and CLI (this package) | **`@yydb/yydb`** |
76
+ | Browser, pure TypeScript | **`@yydb/light-db`** |
77
+ | Wire client only | [`@yydb/yydb-client`](https://www.npmjs.com/package/@yydb/yydb-client) |
78
+ | In-process Rust embed | [`yydb`](https://crates.io/crates/yydb) |
79
+ | Agent skills (wire protocol) | [`@yydb/yydb-skills`](https://www.npmjs.com/package/@yydb/yydb-skills) |
80
+
81
+ Electron: main process can use `@yydb/yydb`; renderer code can use `@yydb/yydb-client` against the main-process
82
+ endpoint.
83
+
84
+ The managed engine listens on `127.0.0.1` with no authentication — keep the endpoint private.
85
+
86
+ [YYDB overview](https://github.com/yy-database/yydb.rs) · [User guides](https://github.com/yy-database/yydb.rs/tree/dev/projects/packages/homepage/documentation/zh-hans)
@@ -43,6 +43,38 @@ export interface MigrateOptions {
43
43
  toField: string;
44
44
  }>;
45
45
  }
46
+ /** One consistency finding from {@link Database.doctor}. */
47
+ export interface DoctorIssue {
48
+ severity: 'warn' | 'error';
49
+ code: string;
50
+ message: string;
51
+ key_hint?: string | null;
52
+ }
53
+ /** Per-namespace usage snapshot from {@link Database.doctor}. */
54
+ export interface DoctorNamespaceStats {
55
+ prefix: string;
56
+ record_count: number;
57
+ bytes_used: number;
58
+ object_bytes: number;
59
+ quota_max_bytes?: number | null;
60
+ ttl_expired_pending: number;
61
+ }
62
+ /** Read-only consistency report. */
63
+ export interface DoctorReport {
64
+ file_path?: string | null;
65
+ last_commit_sequence: number;
66
+ last_checkpoint_sequence: number;
67
+ checkpoint_lag_records: number;
68
+ wal_bytes_uncheckpointed: number;
69
+ orphan_object_count: number;
70
+ namespaces: DoctorNamespaceStats[];
71
+ issues: DoctorIssue[];
72
+ }
73
+ /** Summary of a v0 `.yydx` compaction pass. */
74
+ export interface CompactReport {
75
+ checkpointed: boolean;
76
+ orphans_reclaimed: number;
77
+ }
46
78
  /**
47
79
  * Transaction-local handle passed to {@link Database.transaction} callbacks.
48
80
  * All CRUD methods forward to the underlying native handle while the
@@ -108,6 +140,14 @@ export declare class Database {
108
140
  compareExchange(key: string, expected: Uint8Array | string | null, replacement: Uint8Array | string | null): Promise<boolean>;
109
141
  /** Fold the WAL into the main file and truncate the sidecar. */
110
142
  checkpoint(): Promise<void>;
143
+ /** Checkpoint then copy a self-contained `.yydb` main file to `dest`. */
144
+ backup(dest: string): Promise<void>;
145
+ /** Copy main + WAL + SHM without checkpoint; opening `dest` replays the WAL tail. */
146
+ backupWithWal(dest: string): Promise<void>;
147
+ /** Run read-only consistency probes against the database. */
148
+ doctor(): Promise<DoctorReport>;
149
+ /** Fold WAL + reclaim orphan blobs (`.yydx` layouts only). */
150
+ compactYydx(): Promise<CompactReport>;
111
151
  /**
112
152
  * Run `fn` inside an explicit transaction. The transaction commits on
113
153
  * normal return and rolls back on throw. Nested calls are not supported.
@@ -150,6 +150,22 @@ export class Database {
150
150
  async checkpoint() {
151
151
  this.native.checkpoint();
152
152
  }
153
+ /** Checkpoint then copy a self-contained `.yydb` main file to `dest`. */
154
+ async backup(dest) {
155
+ this.native.backup(dest);
156
+ }
157
+ /** Copy main + WAL + SHM without checkpoint; opening `dest` replays the WAL tail. */
158
+ async backupWithWal(dest) {
159
+ this.native.backupWithWal(dest);
160
+ }
161
+ /** Run read-only consistency probes against the database. */
162
+ async doctor() {
163
+ return JSON.parse(this.native.doctor());
164
+ }
165
+ /** Fold WAL + reclaim orphan blobs (`.yydx` layouts only). */
166
+ async compactYydx() {
167
+ return JSON.parse(this.native.compactYydx());
168
+ }
153
169
  /**
154
170
  * Run `fn` inside an explicit transaction. The transaction commits on
155
171
  * normal return and rolls back on throw. Nested calls are not supported.
package/dist/cli/cli.js CHANGED
@@ -1,16 +1,22 @@
1
- import { dirname, join } from 'node:path';
1
+ import fs from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
2
3
  import { fileURLToPath } from 'node:url';
3
4
  import { createCli } from '@vmz/commander';
5
+ import { formatDiagnostic } from '@vmz/diagnostic';
6
+ import { YydbError } from '../shared/errors.js';
4
7
  import { registerInfoCommand } from './info-cmd.js';
5
8
  import { registerInitCommand } from './init-cmd.js';
6
9
  import { registerServeCommand } from './serve-cmd.js';
10
+ import { registerVerifyCommand } from './verify-cmd.js';
7
11
  import { registerVersionCommand } from './version-cmd.js';
12
+ const CLI_CATALOG = JSON.parse(fs.readFileSync(resolve(import.meta.dirname, '../../locales/en-US/cli.json'), 'utf8'));
8
13
  function buildYydbCli() {
9
14
  const localesRoot = join(dirname(fileURLToPath(import.meta.url)), '../../locales');
10
15
  const cli = createCli('yydb').locales(localesRoot).intro('cli.intro');
11
16
  registerVersionCommand(cli);
12
17
  registerInitCommand(cli);
13
18
  registerInfoCommand(cli);
19
+ registerVerifyCommand(cli);
14
20
  registerServeCommand(cli);
15
21
  return cli;
16
22
  }
@@ -19,6 +25,27 @@ export async function runCli(argv) {
19
25
  return await buildYydbCli().parse(argv);
20
26
  }
21
27
  catch (error) {
28
+ const parsed = error instanceof Error
29
+ ? YydbError.fromDiagnosticJson(error.message)
30
+ : null;
31
+ if (parsed) {
32
+ // Structured engine error from N-API / wire / WASM envelope.
33
+ const severity = parsed.retryable ? 'warning' : 'error';
34
+ const text = formatDiagnostic({
35
+ path: '',
36
+ severity,
37
+ code: parsed.code,
38
+ args: parsed.params,
39
+ message: error instanceof Error ? error.message : String(error),
40
+ }, {
41
+ locale: 'en-US',
42
+ catalog: CLI_CATALOG,
43
+ style: 'compact',
44
+ });
45
+ console.error(text);
46
+ return parsed.retryable ? 13 : 1;
47
+ }
48
+ // Plain text fallback (commander parse failures, unknown errors).
22
49
  console.error(`error: ${error instanceof Error ? error.message : String(error)}`);
23
50
  return 1;
24
51
  }
@@ -1,10 +1,17 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { formatDiagnostic } from '@vmz/diagnostic';
1
4
  import { loadNative } from '../native/binding.js';
2
5
  import { requiredPositional } from './options.js';
6
+ const CLI_CATALOG = JSON.parse(fs.readFileSync(path.resolve(import.meta.dirname, '../../locales/en-US/cli.json'), 'utf8'));
3
7
  export function registerInfoCommand(cli) {
4
- cli.command('info', 'cli.cmd.info').action((options) => cmdInfo(options));
8
+ cli.command('info', 'cli.cmd.info')
9
+ .option('--doctor', 'cli.opt.doctor')
10
+ .action((options) => cmdInfo(options));
5
11
  }
6
12
  export function cmdInfo(options) {
7
13
  const dbPath = requiredPositional(options, 'info');
14
+ const wantDoctor = Boolean(options.doctor);
8
15
  const database = loadNative().Database.open(dbPath);
9
16
  const lines = [`path=${database.path() ?? '<memory>'}`];
10
17
  const schemaVersion = database.schemaVersion();
@@ -25,6 +32,28 @@ export function cmdInfo(options) {
25
32
  lines.push(`shm=${shm}`);
26
33
  lines.push(`wal_frames=${database.walFrameCount()}`);
27
34
  console.log(lines.join('\n'));
35
+ if (wantDoctor) {
36
+ const report = JSON.parse(database.doctor());
37
+ if (report.issues.length === 0) {
38
+ console.log(`doctor=clean orphan_objects=${report.orphan_object_count}`);
39
+ }
40
+ else {
41
+ for (const issue of report.issues) {
42
+ const severity = issue.severity === 'error' ? 'error' : 'warning';
43
+ console.error(formatDiagnostic({
44
+ path: dbPath,
45
+ severity,
46
+ code: issue.code,
47
+ message: issue.message,
48
+ }, {
49
+ locale: 'en-US',
50
+ catalog: CLI_CATALOG,
51
+ style: 'compact',
52
+ }));
53
+ }
54
+ console.error(`doctor=issues(${report.issues.length}) orphans=${report.orphan_object_count}`);
55
+ }
56
+ }
28
57
  return 0;
29
58
  }
30
59
  function summarizeDocument(document) {
@@ -0,0 +1,11 @@
1
+ import type { Cli, ParsedOptions } from '@vmz/commander';
2
+ export declare function registerVerifyCommand(cli: Cli): void;
3
+ /**
4
+ * Run read-only consistency probes against a YYDB database file.
5
+ *
6
+ * Exit codes:
7
+ * 0 — clean (no issues)
8
+ * 1 — doctor found at least one error (database may be corrupt)
9
+ * 13 — only warnings present (readable but should be inspected)
10
+ */
11
+ export declare function cmdVerify(options: ParsedOptions): number;
@@ -0,0 +1,73 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { formatDiagnostic } from '@vmz/diagnostic';
4
+ import { loadNative } from '../native/binding.js';
5
+ import { requiredPositional } from './options.js';
6
+ /**
7
+ * Inline CLI catalog — commander's `.locales()` resolution is not exposed
8
+ * outside the CLI builder, so we ship a static copy here. Only one locale is
9
+ * loaded at runtime; future locale awareness should resolve the chosen locale
10
+ * from commander internals once they surface it.
11
+ */
12
+ const CLI_CATALOG = JSON.parse(fs.readFileSync(path.resolve(import.meta.dirname, '../../locales/en-US/cli.json'), 'utf8'));
13
+ const DIAGNOSTIC_STYLE = 'compact';
14
+ const DIAGNOSTIC_LOCALE = 'en-US';
15
+ export function registerVerifyCommand(cli) {
16
+ cli.command('verify', 'cli.cmd.verify').action((options) => cmdVerify(options));
17
+ }
18
+ /**
19
+ * Run read-only consistency probes against a YYDB database file.
20
+ *
21
+ * Exit codes:
22
+ * 0 — clean (no issues)
23
+ * 1 — doctor found at least one error (database may be corrupt)
24
+ * 13 — only warnings present (readable but should be inspected)
25
+ */
26
+ export function cmdVerify(options) {
27
+ const dbPath = requiredPositional(options, 'verify');
28
+ let report;
29
+ try {
30
+ const native = loadNative();
31
+ const db = native.Database.open(dbPath);
32
+ report = JSON.parse(db.doctor());
33
+ }
34
+ catch (err) {
35
+ const message = err instanceof Error ? err.message : String(err);
36
+ console.error(formatDiagnostic({
37
+ path: dbPath,
38
+ severity: 'error',
39
+ code: 'cli.err.open_failed',
40
+ args: { path: dbPath, reason: message },
41
+ }, {
42
+ locale: DIAGNOSTIC_LOCALE,
43
+ catalog: CLI_CATALOG,
44
+ style: DIAGNOSTIC_STYLE,
45
+ }));
46
+ return 1;
47
+ }
48
+ for (const issue of report.issues) {
49
+ const severity = issue.severity === 'error' ? 'error' : 'warning';
50
+ console.error(formatDiagnostic({
51
+ path: dbPath,
52
+ severity,
53
+ code: issue.code,
54
+ message: issue.message,
55
+ }, {
56
+ locale: DIAGNOSTIC_LOCALE,
57
+ catalog: CLI_CATALOG,
58
+ style: DIAGNOSTIC_STYLE,
59
+ }));
60
+ }
61
+ const errCount = report.issues.filter((i) => i.severity === 'error').length;
62
+ const warnCount = report.issues.length - errCount;
63
+ if (errCount > 0) {
64
+ console.log(`exit=1 errors=${errCount} warnings=${warnCount}`);
65
+ return 1;
66
+ }
67
+ if (warnCount > 0) {
68
+ console.log(`exit=13 errors=0 warnings=${warnCount}`);
69
+ return 13;
70
+ }
71
+ console.log(`exit=0 clean last_commit=${report.last_commit_sequence}`);
72
+ return 0;
73
+ }
@@ -0,0 +1,50 @@
1
+ import { defineMicro as buildMicro, type DefinedMicro, type MicroHandle } from './micro.js';
2
+ import type { UdfTypeDescriptor } from './udf-types.js';
3
+ export type { SchemaVersion } from '@yydb/yydb-client';
4
+ export interface OpenOptions {
5
+ /** Override CLI script path (tests / special installs). */
6
+ cli?: string;
7
+ /** @deprecated Use `cli`. */
8
+ binary?: string;
9
+ /** Prefer a specific loopback port; default = ephemeral free port. */
10
+ port?: number;
11
+ /** Extra args after `serve <path> --bind …`. */
12
+ serveArgs?: string[];
13
+ }
14
+ /**
15
+ * Node-facing YYDB handle. Looks like one library; owns a private loopback
16
+ * `yydb serve` child under the hood.
17
+ */
18
+ export declare class Database {
19
+ readonly path: string;
20
+ readonly endpoint: string;
21
+ private client;
22
+ private child;
23
+ private readonly microRegistry;
24
+ private constructor();
25
+ /**
26
+ * Open (create) a `.yydb` file. Spawns the engine on `127.0.0.1` automatically.
27
+ */
28
+ static open(dbPath: string, options?: OpenOptions): Promise<Database>;
29
+ serverVersion(): Promise<string>;
30
+ info(): Promise<string>;
31
+ getSchema(): Promise<import("@yydb/yydb-client").SchemaVersion | null>;
32
+ ensureSchema(version: number, document: string): Promise<void>;
33
+ get(key: string): Promise<Uint8Array | null>;
34
+ put(key: string, value: Uint8Array | string): Promise<void>;
35
+ /**
36
+ * Build a typed session-local micro definition. Does not register it with
37
+ * the engine until [`registerMicro`](./database.ts) is called.
38
+ */
39
+ defineMicro<TArgs extends readonly UdfTypeDescriptor[], TReturn extends UdfTypeDescriptor>(definition: Parameters<typeof buildMicro<TArgs, TReturn>>[0]): DefinedMicro<TArgs, TReturn>;
40
+ /**
41
+ * Install a [`defineMicro`](./database.ts) definition into the current
42
+ * session registry and sync metadata to the private engine. Host micros are
43
+ * session-local and never written to `.yydb`.
44
+ */
45
+ registerMicro<TArgs extends readonly UdfTypeDescriptor[], TReturn extends UdfTypeDescriptor>(definition: DefinedMicro<TArgs, TReturn>): Promise<MicroHandle>;
46
+ /** Invoke a registered scalar UDF on the private engine. */
47
+ callScalar(name: string, version: number, args: readonly (string | number | boolean | null)[]): Promise<string | number | boolean | null>;
48
+ /** Stop the private engine process. */
49
+ close(): Promise<void>;
50
+ }
@@ -0,0 +1,181 @@
1
+ import { spawn } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import net from 'node:net';
4
+ import path from 'node:path';
5
+ import { connect } from '@yydb/yydb-client/node';
6
+ import { defineMicro as buildMicro, MicroSessionRegistry } from './micro.js';
7
+ import { resolveYydbCli } from './resolve-bin.js';
8
+ function microFingerprint(name, version) {
9
+ const bytes = new Uint8Array(32);
10
+ const seed = `${name}@${version}`;
11
+ for (let index = 0; index < bytes.byteLength; index += 1) {
12
+ bytes[index] = seed.charCodeAt(index % seed.length) & 0xff;
13
+ }
14
+ return bytes;
15
+ }
16
+ async function freeLoopbackPort() {
17
+ return await new Promise((resolve, reject) => {
18
+ const server = net.createServer();
19
+ server.listen(0, '127.0.0.1', () => {
20
+ const address = server.address();
21
+ if (!address || typeof address === 'string') {
22
+ server.close();
23
+ reject(new Error('could not allocate loopback port'));
24
+ return;
25
+ }
26
+ const { port } = address;
27
+ server.close((error) => {
28
+ if (error)
29
+ reject(error);
30
+ else
31
+ resolve(port);
32
+ });
33
+ });
34
+ server.on('error', reject);
35
+ });
36
+ }
37
+ async function waitForTcp(host, port, timeoutMs) {
38
+ const deadline = Date.now() + timeoutMs;
39
+ while (Date.now() < deadline) {
40
+ try {
41
+ await new Promise((resolve, reject) => {
42
+ const socket = net.connect({ host, port }, () => {
43
+ socket.end();
44
+ resolve();
45
+ });
46
+ socket.on('error', reject);
47
+ });
48
+ return;
49
+ }
50
+ catch {
51
+ await new Promise((r) => setTimeout(r, 40));
52
+ }
53
+ }
54
+ throw new Error(`yydb engine did not accept connections on ${host}:${port}`);
55
+ }
56
+ /**
57
+ * Node-facing YYDB handle. Looks like one library; owns a private loopback
58
+ * `yydb serve` child under the hood.
59
+ */
60
+ export class Database {
61
+ path;
62
+ endpoint;
63
+ client;
64
+ child;
65
+ microRegistry;
66
+ constructor(dbPath, endpoint, client, child) {
67
+ this.path = dbPath;
68
+ this.endpoint = endpoint;
69
+ this.client = client;
70
+ this.child = child;
71
+ this.microRegistry = new MicroSessionRegistry();
72
+ this.client.setMicroHostHandler((payload) => this.microRegistry.invokeFromWire(payload));
73
+ }
74
+ /**
75
+ * Open (create) a `.yydb` file. Spawns the engine on `127.0.0.1` automatically.
76
+ */
77
+ static async open(dbPath, options = {}) {
78
+ const resolved = path.resolve(dbPath);
79
+ fs.mkdirSync(path.dirname(resolved), { recursive: true });
80
+ const cli = options.cli ?? options.binary ?? resolveYydbCli();
81
+ const port = options.port ?? (await freeLoopbackPort());
82
+ const bind = `127.0.0.1:${port}`;
83
+ const args = [cli, 'serve', resolved, '--bind', bind, ...(options.serveArgs ?? [])];
84
+ const child = spawn(process.execPath, args, {
85
+ stdio: 'pipe',
86
+ windowsHide: true,
87
+ });
88
+ let stderr = '';
89
+ child.stderr?.setEncoding('utf8');
90
+ child.stderr?.on('data', (chunk) => {
91
+ stderr += chunk;
92
+ });
93
+ const exitPromise = new Promise((_, reject) => {
94
+ child.once('exit', (code, signal) => {
95
+ reject(new Error(`yydb engine exited early (code=${code}, signal=${signal}): ${stderr.trim()}`));
96
+ });
97
+ child.once('error', reject);
98
+ });
99
+ try {
100
+ await Promise.race([waitForTcp('127.0.0.1', port, 8000), exitPromise]);
101
+ const client = await connect(bind);
102
+ child.removeAllListeners('exit');
103
+ child.removeAllListeners('error');
104
+ child.on('exit', () => {
105
+ /* closed via Database.close or crash */
106
+ });
107
+ return new Database(resolved, bind, client, child);
108
+ }
109
+ catch (error) {
110
+ child.kill('SIGTERM');
111
+ throw error;
112
+ }
113
+ }
114
+ async serverVersion() {
115
+ return this.client.serverVersion();
116
+ }
117
+ async info() {
118
+ return this.client.info();
119
+ }
120
+ async getSchema() {
121
+ return this.client.getSchema();
122
+ }
123
+ async ensureSchema(version, document) {
124
+ await this.client.ensureSchema(version, document);
125
+ }
126
+ async get(key) {
127
+ return this.client.get(key);
128
+ }
129
+ async put(key, value) {
130
+ await this.client.put(key, value);
131
+ }
132
+ /**
133
+ * Build a typed session-local micro definition. Does not register it with
134
+ * the engine until [`registerMicro`](./database.ts) is called.
135
+ */
136
+ defineMicro(definition) {
137
+ return buildMicro(definition);
138
+ }
139
+ /**
140
+ * Install a [`defineMicro`](./database.ts) definition into the current
141
+ * session registry and sync metadata to the private engine. Host micros are
142
+ * session-local and never written to `.yydb`.
143
+ */
144
+ async registerMicro(definition) {
145
+ const handle = this.microRegistry.register(definition);
146
+ await this.client.registerMicro({
147
+ hostId: handle.hostId,
148
+ handleVersion: handle.implementationVersion,
149
+ udfVersion: definition.version,
150
+ name: definition.name,
151
+ functionId: handle.functionId,
152
+ args: definition.args.map((arg) => arg.kind),
153
+ returns: definition.returns.kind,
154
+ fingerprint: microFingerprint(definition.name, definition.version),
155
+ });
156
+ return handle;
157
+ }
158
+ /** Invoke a registered scalar UDF on the private engine. */
159
+ async callScalar(name, version, args) {
160
+ return this.client.callScalar(name, version, args);
161
+ }
162
+ /** Stop the private engine process. */
163
+ async close() {
164
+ this.client.close();
165
+ const child = this.child;
166
+ this.child = null;
167
+ if (!child || child.exitCode !== null || child.killed) {
168
+ return;
169
+ }
170
+ await new Promise((resolve) => {
171
+ child.once('exit', () => resolve());
172
+ child.kill('SIGTERM');
173
+ setTimeout(() => {
174
+ if (child.exitCode === null && !child.killed) {
175
+ child.kill('SIGKILL');
176
+ }
177
+ resolve();
178
+ }, 2000);
179
+ });
180
+ }
181
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { Database, type OpenOptions, type SchemaVersion } from './api/database.js';
2
- export { YydbError, YydbErrorCodes, isRetryableYydbCode, isYydbError } from './shared/errors.js';
2
+ export { YydbError, YydbErrorCodes, YydbClientErrorCodes, isYydbError } from './shared/errors.js';
3
3
  export type { YydbErrorCode, YydbErrorOptions } from './shared/errors.js';
4
4
  export { defineMicro, type DefinedMicro, type MicroHandle, type TsMicroDefinitionInput, MicroSessionRegistry } from './shared/micro.js';
5
5
  export { type InferUdfArgs, type InferUdfReturn, type MicroEffect, type MicroInvocationMode, type UdfScalarDescriptor, type UdfScalarKind, } from './shared/udf-types.js';