@yydb/yydb 0.0.11 → 0.0.12

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.
@@ -1,5 +1,6 @@
1
1
  import { type EmittableItem } from '@voml/vos';
2
2
  import { defineMicro as buildMicro, type DefinedMicro, type MicroHandle } from '../shared/micro.js';
3
+ import { loadNative } from '../native/binding.js';
3
4
  import type { UdfScalarDescriptor } from '../shared/udf-types.js';
4
5
  /** Published schema metadata. */
5
6
  export interface SchemaVersion {
@@ -11,6 +12,54 @@ export interface OpenOptions {
11
12
  /** Create parent directories when missing (default: true). */
12
13
  mkdir?: boolean;
13
14
  }
15
+ /** One row from a {@link Database.scan}. */
16
+ export interface ScanRow {
17
+ key: string;
18
+ value: Uint8Array;
19
+ }
20
+ /** Options for {@link Database.scan}. */
21
+ export interface ScanOptions {
22
+ /** Include this prefix in every returned key; default `""` returns everything. */
23
+ prefix?: string;
24
+ /** Exclusive continuation cursor from a previous scan's last row; default `null` starts fresh. */
25
+ after?: string | null;
26
+ /** Max rows returned per call; default `1000`. */
27
+ limit?: number;
28
+ }
29
+ /** One change in a {@link Database.batch}. Use `null` to delete. */
30
+ export interface BatchChange {
31
+ key: string;
32
+ value: Uint8Array | string | null;
33
+ }
34
+ /** Options for {@link Database.migrateSchema}. */
35
+ export interface MigrateOptions {
36
+ /** Rename entries — `fromType`/`toType` updates a type name; `type`/`fromField`/`toField` updates one field. */
37
+ renames?: ReadonlyArray<{
38
+ fromType: string;
39
+ toType: string;
40
+ } | {
41
+ type: string;
42
+ fromField: string;
43
+ toField: string;
44
+ }>;
45
+ }
46
+ /**
47
+ * Transaction-local handle passed to {@link Database.transaction} callbacks.
48
+ * All CRUD methods forward to the underlying native handle while the
49
+ * transaction is in flight. Commit/rollback are owned by {@link Database}.
50
+ */
51
+ export declare class TransactionHandle {
52
+ private readonly native;
53
+ constructor(native: ReturnType<ReturnType<typeof loadNative>['Database']['open']>);
54
+ get(key: string): Promise<Uint8Array | null>;
55
+ put(key: string, value: Uint8Array | string): Promise<void>;
56
+ delete(key: string): Promise<void>;
57
+ scan(options?: ScanOptions): Promise<ScanRow[]>;
58
+ batch(changes: readonly BatchChange[]): Promise<void>;
59
+ compareExchange(key: string, expected: Uint8Array | string | null, replacement: Uint8Array | string | null): Promise<boolean>;
60
+ query<T = Record<string, unknown>>(source: string): Promise<T[]>;
61
+ execute(source: string): Promise<void>;
62
+ }
14
63
  /**
15
64
  * Node-facing YYDB local database handle.
16
65
  *
@@ -39,26 +88,52 @@ export declare class Database {
39
88
  * Accepts either a raw `.vos` source string, one `@voml/vos` descriptor
40
89
  * (`v.table()`, `v.class()`) or a list of descriptors. When descriptors
41
90
  * are supplied, they are rendered to source via `emitSource` first.
42
- *
43
- * ```ts
44
- * // Raw source (unchanged behaviour):
45
- * await db.ensureSchema('namespace demo { table User { @@id: uuid } }');
46
- *
47
- * // Zod-like builder — the Vos schema-ecosystem sweet spot:
48
- * const User = v.table('User', { id: v.uuid().primary(), name: v.utf8() });
49
- * await db.ensureSchema(User);
50
- *
51
- * // Multiple declarations in one shot:
52
- * await db.ensureSchema(TableA, TableB, ClassC);
53
- * ```
54
91
  */
55
92
  ensureSchema(...input: [string] | [string, ...EmittableItem[]] | EmittableItem[]): Promise<void>;
56
93
  /** Read one record, or `null` when the key is absent. */
57
94
  get(key: string): Promise<Uint8Array | null>;
58
95
  /** Insert or replace one record. */
59
96
  put(key: string, value: Uint8Array | string): Promise<void>;
97
+ /** Delete one record by key. Silently succeeds when the key is absent. */
98
+ delete(key: string): Promise<void>;
99
+ /** List key/value pairs in lexical order. `prefix` defaults to `""` (all keys). */
100
+ scan(options?: ScanOptions): Promise<ScanRow[]>;
101
+ /** Atomically write or delete many keys in one call. */
102
+ batch(changes: readonly BatchChange[]): Promise<void>;
103
+ /**
104
+ * Compare-and-swap: replace or delete `key` only when its current value
105
+ * exactly equals `expected`. Both `expected` and `replacement` may be
106
+ * `null` (meaning absent / delete). Returns `true` when the swap applied.
107
+ */
108
+ compareExchange(key: string, expected: Uint8Array | string | null, replacement: Uint8Array | string | null): Promise<boolean>;
60
109
  /** Fold the WAL into the main file and truncate the sidecar. */
61
110
  checkpoint(): Promise<void>;
111
+ /**
112
+ * Run `fn` inside an explicit transaction. The transaction commits on
113
+ * normal return and rolls back on throw. Nested calls are not supported.
114
+ *
115
+ * ```ts
116
+ * await db.transaction(async (tx) => {
117
+ * await tx.put('a', '1');
118
+ * await tx.put('b', '2');
119
+ * });
120
+ * ```
121
+ */
122
+ transaction<T>(fn: (tx: TransactionHandle) => Promise<T>): Promise<T>;
123
+ /** Execute a VOS read pipeline and return typed rows. */
124
+ query<T = Record<string, unknown>>(source: string): Promise<T[]>;
125
+ /** Execute a VOS write program. */
126
+ execute(source: string): Promise<void>;
127
+ /**
128
+ * Publish a new schema version, migrating existing records when their
129
+ * shape no longer matches.
130
+ *
131
+ * Accepts raw `.vos` source or `@voml/vos` descriptors (same as
132
+ * {@link Database.ensureSchema}). Use `renames` to tell the engine which
133
+ * old type/field names map to the new ones — records that would otherwise
134
+ * look structurally unrelated get preserved.
135
+ */
136
+ migrateSchema(documentOrSource: string | EmittableItem, ...rest: (EmittableItem | MigrateOptions)[]): Promise<void>;
62
137
  /** Build a typed session-local micro definition (without registering). */
63
138
  defineMicro<TArgs extends readonly UdfScalarDescriptor[], TReturn extends UdfScalarDescriptor>(definition: Parameters<typeof buildMicro<TArgs, TReturn>>[0]): DefinedMicro<TArgs, TReturn>;
64
139
  /**
@@ -3,6 +3,44 @@ import path from 'node:path';
3
3
  import { emitSource, } from '@voml/vos';
4
4
  import { defineMicro as buildMicro, microFingerprint, udfWireKind, MicroSessionRegistry, } from '../shared/micro.js';
5
5
  import { loadNative } from '../native/binding.js';
6
+ import { YydbError } from '../shared/errors.js';
7
+ /**
8
+ * Transaction-local handle passed to {@link Database.transaction} callbacks.
9
+ * All CRUD methods forward to the underlying native handle while the
10
+ * transaction is in flight. Commit/rollback are owned by {@link Database}.
11
+ */
12
+ export class TransactionHandle {
13
+ native;
14
+ constructor(native) {
15
+ this.native = native;
16
+ }
17
+ async get(key) { return this.native.get(key); }
18
+ async put(key, value) {
19
+ const bytes = typeof value === 'string' ? new TextEncoder().encode(value) : value;
20
+ this.native.put(key, bytes);
21
+ }
22
+ async delete(key) { this.native.delete(key); }
23
+ async scan(options = {}) {
24
+ const rowsJson = this.native.scanPrefix(options.prefix ?? '', options.after ?? null, options.limit ?? 1000);
25
+ const rows = JSON.parse(rowsJson);
26
+ return rows.map((r) => ({ key: r.key, value: new Uint8Array(r.value) }));
27
+ }
28
+ async batch(changes) {
29
+ const body = changes.map((c) => ({
30
+ key: c.key,
31
+ value: c.value === null ? null : typeof c.value === 'string' ? Array.from(new TextEncoder().encode(c.value)) : Array.from(c.value),
32
+ }));
33
+ this.native.writeBatch(JSON.stringify(body));
34
+ }
35
+ async compareExchange(key, expected, replacement) {
36
+ const toBytes = (v) => v === null ? null : typeof v === 'string' ? new TextEncoder().encode(v) : v;
37
+ return this.native.compareExchange(key, toBytes(expected), toBytes(replacement));
38
+ }
39
+ async query(source) {
40
+ return JSON.parse(this.native.queryVos(source));
41
+ }
42
+ async execute(source) { this.native.executeVosWrite(source); }
43
+ }
6
44
  /**
7
45
  * Node-facing YYDB local database handle.
8
46
  *
@@ -57,27 +95,13 @@ export class Database {
57
95
  * Accepts either a raw `.vos` source string, one `@voml/vos` descriptor
58
96
  * (`v.table()`, `v.class()`) or a list of descriptors. When descriptors
59
97
  * are supplied, they are rendered to source via `emitSource` first.
60
- *
61
- * ```ts
62
- * // Raw source (unchanged behaviour):
63
- * await db.ensureSchema('namespace demo { table User { @@id: uuid } }');
64
- *
65
- * // Zod-like builder — the Vos schema-ecosystem sweet spot:
66
- * const User = v.table('User', { id: v.uuid().primary(), name: v.utf8() });
67
- * await db.ensureSchema(User);
68
- *
69
- * // Multiple declarations in one shot:
70
- * await db.ensureSchema(TableA, TableB, ClassC);
71
- * ```
72
98
  */
73
99
  async ensureSchema(...input) {
74
100
  let document;
75
101
  if (typeof input[0] === 'string') {
76
- // Raw source string, optionally followed by descriptors we just ignore.
77
102
  document = input[0];
78
103
  }
79
104
  else {
80
- // One or more Vos descriptor items → render to source.
81
105
  document = emitSource(input);
82
106
  }
83
107
  this.native.ensureSchema(document);
@@ -91,10 +115,112 @@ export class Database {
91
115
  const bytes = typeof value === 'string' ? new TextEncoder().encode(value) : value;
92
116
  this.native.put(key, bytes);
93
117
  }
118
+ /** Delete one record by key. Silently succeeds when the key is absent. */
119
+ async delete(key) {
120
+ this.native.delete(key);
121
+ }
122
+ /** List key/value pairs in lexical order. `prefix` defaults to `""` (all keys). */
123
+ async scan(options = {}) {
124
+ const rowsJson = this.native.scanPrefix(options.prefix ?? '', options.after ?? null, options.limit ?? 1000);
125
+ const rows = JSON.parse(rowsJson);
126
+ return rows.map((r) => ({ key: r.key, value: new Uint8Array(r.value) }));
127
+ }
128
+ /** Atomically write or delete many keys in one call. */
129
+ async batch(changes) {
130
+ const body = changes.map((c) => ({
131
+ key: c.key,
132
+ value: c.value === null
133
+ ? null
134
+ : typeof c.value === 'string'
135
+ ? Array.from(new TextEncoder().encode(c.value))
136
+ : Array.from(c.value),
137
+ }));
138
+ this.native.writeBatch(JSON.stringify(body));
139
+ }
140
+ /**
141
+ * Compare-and-swap: replace or delete `key` only when its current value
142
+ * exactly equals `expected`. Both `expected` and `replacement` may be
143
+ * `null` (meaning absent / delete). Returns `true` when the swap applied.
144
+ */
145
+ async compareExchange(key, expected, replacement) {
146
+ const toBytes = (v) => v === null ? null : typeof v === 'string' ? new TextEncoder().encode(v) : v;
147
+ return this.native.compareExchange(key, toBytes(expected), toBytes(replacement));
148
+ }
94
149
  /** Fold the WAL into the main file and truncate the sidecar. */
95
150
  async checkpoint() {
96
151
  this.native.checkpoint();
97
152
  }
153
+ /**
154
+ * Run `fn` inside an explicit transaction. The transaction commits on
155
+ * normal return and rolls back on throw. Nested calls are not supported.
156
+ *
157
+ * ```ts
158
+ * await db.transaction(async (tx) => {
159
+ * await tx.put('a', '1');
160
+ * await tx.put('b', '2');
161
+ * });
162
+ * ```
163
+ */
164
+ async transaction(fn) {
165
+ this.native.begin();
166
+ const tx = new TransactionHandle(this.native);
167
+ try {
168
+ const result = await fn(tx);
169
+ this.native.commit();
170
+ return result;
171
+ }
172
+ catch (err) {
173
+ try {
174
+ this.native.rollback();
175
+ }
176
+ catch {
177
+ // rollback already failed, don't mask the original error
178
+ }
179
+ throw YydbError.fromDiagnosticJson(err instanceof Error ? err.message : String(err)) ?? err;
180
+ }
181
+ }
182
+ /** Execute a VOS read pipeline and return typed rows. */
183
+ async query(source) {
184
+ const rowsJson = this.native.queryVos(source);
185
+ return JSON.parse(rowsJson);
186
+ }
187
+ /** Execute a VOS write program. */
188
+ async execute(source) {
189
+ this.native.executeVosWrite(source);
190
+ }
191
+ /**
192
+ * Publish a new schema version, migrating existing records when their
193
+ * shape no longer matches.
194
+ *
195
+ * Accepts raw `.vos` source or `@voml/vos` descriptors (same as
196
+ * {@link Database.ensureSchema}). Use `renames` to tell the engine which
197
+ * old type/field names map to the new ones — records that would otherwise
198
+ * look structurally unrelated get preserved.
199
+ */
200
+ async migrateSchema(documentOrSource, ...rest) {
201
+ let document;
202
+ let options = {};
203
+ const all = [documentOrSource, ...rest];
204
+ const last = all[all.length - 1];
205
+ if (last &&
206
+ typeof last === 'object' &&
207
+ !('table' in last) &&
208
+ !('class' in last) &&
209
+ !('emit' in last) &&
210
+ ('renames' in last || Object.keys(last).length === 0)) {
211
+ options = all.pop() ?? {};
212
+ }
213
+ if (typeof all[0] === 'string') {
214
+ document = all[0];
215
+ }
216
+ else {
217
+ document = emitSource(all);
218
+ }
219
+ const renamesJson = options.renames?.length
220
+ ? JSON.stringify(options.renames)
221
+ : null;
222
+ this.native.migrateSchema(document, renamesJson);
223
+ }
98
224
  /** Build a typed session-local micro definition (without registering). */
99
225
  defineMicro(definition) {
100
226
  return buildMicro(definition);
@@ -18,6 +18,26 @@ export interface NativeDatabase {
18
18
  walFrameCount(): number;
19
19
  get(key: string): Uint8Array | null;
20
20
  put(key: string, value: Uint8Array): void;
21
+ /** Delete one record by key. */
22
+ delete(key: string): void;
23
+ /** Scan keys with a prefix; returns JSON `[{ key, value }, ...]`. */
24
+ scanPrefix(prefix: string, after: string | null, limit: number): string;
25
+ /** Atomically write or delete many keys; body is JSON `[{ key, value|null }, ...]`. */
26
+ writeBatch(changesJson: string): void;
27
+ /** Compare-and-swap; returns true on success. */
28
+ compareExchange(key: string, expected: Uint8Array | null, replacement: Uint8Array | null): boolean;
29
+ /** Begin an explicit transaction. */
30
+ begin(): void;
31
+ /** Commit the current transaction. */
32
+ commit(): void;
33
+ /** Roll back the current transaction. */
34
+ rollback(): void;
35
+ /** Execute a VOS read pipeline; returns JSON rows. */
36
+ queryVos(source: string): string;
37
+ /** Execute a VOS write program. */
38
+ executeVosWrite(source: string): void;
39
+ /** Publish a new schema version, optionally with rename map. */
40
+ migrateSchema(document: string, renamesJson?: string | null): void;
21
41
  registerMicro(name: string, version: number, args: string[], returns: string, hostId: number, functionId: string, handleVersion: number, fingerprint: Uint8Array): void;
22
42
  callScalar(name: string, version: number, argsJson: string): string;
23
43
  }
@@ -40,6 +40,8 @@ export declare function isRetryableYydbCode(code: string): boolean;
40
40
  export interface YydbErrorOptions {
41
41
  /** Operation that failed (`schemaGet`, `kvPut`, `query`, …). */
42
42
  operation?: string;
43
+ /** Structured parameters for catalog templating (when present). */
44
+ params?: Readonly<Record<string, string>>;
43
45
  /** Underlying failure when this error wraps one. */
44
46
  cause?: unknown;
45
47
  }
@@ -51,7 +53,33 @@ export declare class YydbError extends Error {
51
53
  readonly operation: string;
52
54
  /** Whether the same call may be retried. */
53
55
  readonly retryable: boolean;
56
+ /** Structured parameters for catalog templating (when present). */
57
+ readonly params?: Readonly<Record<string, string>>;
54
58
  constructor(code: YydbErrorCode, message: string, options?: YydbErrorOptions);
59
+ /**
60
+ * Parse the diagnostic JSON that N-API and WASM surfaces embed in their
61
+ * `Error.message`. Every boundary surface (N-API reason field, WASM JSON
62
+ * envelope, wire protocol) carries the same `{ code, operation, retryable,
63
+ * message, params? }` contract produced by `yydb::Error::to_json`.
64
+ *
65
+ * Returns the parsed {@link YydbError} if `value` is a diagnostic object,
66
+ * or `null` when the input is plain text. Hosts typically use this as a
67
+ * fallback in catch blocks:
68
+ *
69
+ * ```ts
70
+ * try {
71
+ * await db.put('key', value);
72
+ * } catch (err) {
73
+ * const parsed = YydbError.fromDiagnosticJson(
74
+ * err instanceof Error ? err.message : String(err),
75
+ * );
76
+ * if (parsed) {
77
+ * // branch on parsed.code / retryable / operation
78
+ * }
79
+ * }
80
+ * ```
81
+ */
82
+ static fromDiagnosticJson(value: unknown): YydbError | null;
55
83
  }
56
84
  /** Narrowing helper so callers can branch without `instanceof` imports. */
57
85
  export declare function isYydbError(value: unknown): value is YydbError;
@@ -54,12 +54,75 @@ export class YydbError extends Error {
54
54
  operation;
55
55
  /** Whether the same call may be retried. */
56
56
  retryable;
57
+ /** Structured parameters for catalog templating (when present). */
58
+ params;
57
59
  constructor(code, message, options = {}) {
58
60
  super(message, { cause: options.cause });
59
61
  this.name = 'YydbError';
60
62
  this.code = code;
61
63
  this.operation = options.operation ?? 'unknown';
62
64
  this.retryable = isRetryableYydbCode(code);
65
+ this.params = options.params;
66
+ }
67
+ /**
68
+ * Parse the diagnostic JSON that N-API and WASM surfaces embed in their
69
+ * `Error.message`. Every boundary surface (N-API reason field, WASM JSON
70
+ * envelope, wire protocol) carries the same `{ code, operation, retryable,
71
+ * message, params? }` contract produced by `yydb::Error::to_json`.
72
+ *
73
+ * Returns the parsed {@link YydbError} if `value` is a diagnostic object,
74
+ * or `null` when the input is plain text. Hosts typically use this as a
75
+ * fallback in catch blocks:
76
+ *
77
+ * ```ts
78
+ * try {
79
+ * await db.put('key', value);
80
+ * } catch (err) {
81
+ * const parsed = YydbError.fromDiagnosticJson(
82
+ * err instanceof Error ? err.message : String(err),
83
+ * );
84
+ * if (parsed) {
85
+ * // branch on parsed.code / retryable / operation
86
+ * }
87
+ * }
88
+ * ```
89
+ */
90
+ static fromDiagnosticJson(value) {
91
+ let obj;
92
+ if (typeof value === 'string') {
93
+ try {
94
+ const parsed = JSON.parse(value);
95
+ if (!parsed || typeof parsed !== 'object')
96
+ return null;
97
+ obj = parsed;
98
+ }
99
+ catch {
100
+ return null;
101
+ }
102
+ }
103
+ else if (value && typeof value === 'object') {
104
+ obj = value;
105
+ }
106
+ else {
107
+ return null;
108
+ }
109
+ const code = typeof obj.code === 'string' ? obj.code : null;
110
+ const message = typeof obj.message === 'string' ? obj.message : '';
111
+ if (!code)
112
+ return null;
113
+ const operation = typeof obj.operation === 'string' ? obj.operation : undefined;
114
+ const retryable = typeof obj.retryable === 'boolean' ? obj.retryable : undefined;
115
+ const params = obj.params && typeof obj.params === 'object'
116
+ ? obj.params
117
+ : undefined;
118
+ const err = new YydbError(code, message, {
119
+ operation,
120
+ params,
121
+ });
122
+ if (typeof retryable === 'boolean') {
123
+ err.retryable = retryable;
124
+ }
125
+ return err;
63
126
  }
64
127
  }
65
128
  /** Narrowing helper so callers can branch without `instanceof` imports. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yydb/yydb",
3
- "version": "0.0.11",
3
+ "version": "0.0.12",
4
4
  "description": "YYDB Node embedded engine and CLI",
5
5
  "license": "MPL-2.0",
6
6
  "type": "module",
@@ -59,9 +59,9 @@
59
59
  "node": ">=18"
60
60
  },
61
61
  "optionalDependencies": {
62
- "@yydb/yydb-darwin-arm64": "0.0.11",
63
- "@yydb/yydb-darwin-x64": "0.0.11",
64
- "@yydb/yydb-linux-x64": "0.0.11",
65
- "@yydb/yydb-win32-x64": "0.0.11"
62
+ "@yydb/yydb-darwin-arm64": "0.0.12",
63
+ "@yydb/yydb-darwin-x64": "0.0.12",
64
+ "@yydb/yydb-linux-x64": "0.0.12",
65
+ "@yydb/yydb-win32-x64": "0.0.12"
66
66
  }
67
67
  }