albedo-node 0.7.2 โ†’ 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,7 +9,7 @@ This package wraps the core database engine with a concise TypeScript API, ships
9
9
  - ๐Ÿงฑ Access to the full Albedo bucket API from Node.js or Bun
10
10
  - ๐Ÿ“ฆ Precompiled native modules for Linux (glibc & musl), macOS, and arm64/x64
11
11
  - ๐Ÿ” Generator-based iterators for queries, transforms, and live subscriptions
12
- - ๐Ÿ”„ Built-in replication callback and apply mechanisms
12
+ - ๐Ÿ”„ WAL-based replication helpers with explicit cursors and batches
13
13
  - ๐Ÿงช TypeScript typings and Bun-based test suite
14
14
 
15
15
  ## Installation
@@ -131,25 +131,36 @@ Notes:
131
131
  ### Replication
132
132
 
133
133
  ```ts
134
- const primary = Bucket.open("./primary.bucket");
135
- const replica = Bucket.open("./replica.bucket");
136
-
137
- const batches: Uint8Array[] = [];
138
- primary.setReplicationCallback((data) => {
139
- batches.push(data);
134
+ const primary = Bucket.open("./primary.bucket", {
135
+ wal: true,
136
+ write_durability: "all",
140
137
  });
138
+ const replica = Bucket.open("./replica.bucket", {
139
+ wal: true,
140
+ write_durability: "all",
141
+ });
142
+
143
+ const cursor = primary.replicationCursor();
141
144
 
142
145
  primary.insert({ name: "Replica", version: 1 });
143
- primary.close();
144
146
 
145
- // Apply the first batch to the replica bucket
146
- if (batches.length > 0) {
147
- replica.applyReplicationBatch(batches[0]);
147
+ const batch = primary.readReplicationBatch(cursor);
148
+ if (batch) {
149
+ const nextCursor = replica.applyReplicationBatch(batch);
150
+ console.log(nextCursor.next_frame_index);
148
151
  }
149
152
 
153
+ primary.close();
150
154
  replica.close();
151
155
  ```
152
156
 
157
+ Notes:
158
+
159
+ - Replication requires WAL mode to be enabled on both buckets.
160
+ - `replicationCursor()` snapshots the current position in the WAL stream.
161
+ - `readReplicationBatch(cursor, maxBytes?)` returns `null` when there is nothing new to send.
162
+ - `applyReplicationBatch(batch)` applies the batch and returns the replica's new cursor.
163
+
153
164
  ## Query objects and BSON payloads
154
165
 
155
166
  Most bucket operations accept either:
@@ -181,22 +192,37 @@ Serialized documents that contain `_id` fields with a BSON ObjectId will be revi
181
192
  ## API reference
182
193
 
183
194
  - `default` export โ€” raw Albedo native module
184
- - `ObjectId`, `serialize`, `deserialize`, `open`, `close`, `insert`, `delete`, `list`, `transform`, `setReplicationCallback`, etc.
195
+ - `ObjectId`, `serialize`, `deserialize`, `open`, `open_with_options`, `close`, `insert`, `checkpoint`, `delete`, `list`, `transform`, `replicationCursor`, `readReplicationBatch`, `applyReplicationBatch`, etc.
185
196
  - `BSON.serialize(value)` / `BSON.deserialize(bytes)` โ€” helper wrappers
186
197
  - `Bucket`
187
- - `static open(path: string): Bucket`
198
+ - `static open(path: string, options?: OpenBucketOptions): Bucket`
188
199
  - `insert(doc: object | Uint8Array)`
200
+ - `checkpoint()`
189
201
  - `delete(query?: object | Query)`
190
202
  - `list<T>(query?: object | Query): Generator<T>`
191
203
  - `subscribe<T>(query?: object | Query, options?: { pollingTimeout?: number; batchSize?: number }): AsyncGenerator<SubscriptionEvent<T>>`
192
204
  - `transformIterator<T>(query?: object | Query): Generator<T, undefined, object | null>`
205
+ - `transform<T>(query, fn)` / `update<T>(query, fn)`
206
+ - `all<T>(query?)` / `one<T>(query?)`
207
+ - `beginTransaction()` / `tx(fn)`
193
208
  - `ensureIndex(name: string, options: { unique: boolean; sparse: boolean; reverse: boolean })`
194
209
  - `dropIndex(name: string)`
195
210
  - `indexes: Record<string, { name: string; unique: boolean; sparse: boolean; reverse: boolean }>`
196
- - `setReplicationCallback(cb: (data: Uint8Array) => void)`
197
- - `applyReplicationBatch(batch: Uint8Array)`
211
+ - `replicationCursor(): { generation: bigint; next_frame_index: bigint }`
212
+ - `readReplicationBatch(cursor, maxBytes?): Uint8Array | null`
213
+ - `applyReplicationBatch(batch: Uint8Array): { generation: bigint; next_frame_index: bigint }`
198
214
  - accepts raw BSON `Uint8Array` payloads anywhere a query or document object is expected
199
215
 
216
+ - `OpenBucketOptions`
217
+ - `buildIdIndex?: boolean`
218
+ - `mode?: string`
219
+ - `auto_vaccuum?: boolean`
220
+ - `page_cache_capacity?: number`
221
+ - `wal?: boolean`
222
+ - `write_durability?: "all" | "manual" | { periodic: number }`
223
+ - `read_durability?: "shared" | "process"`
224
+ - `wal_auto_checkpoint?: (defaults to 1000 pages)`
225
+
200
226
  - `SubscriptionEvent<T>`
201
227
  - `seqno: bigint`
202
228
  - `op: "insert" | "update" | "delete"`
package/dist/index.d.ts CHANGED
@@ -1,8 +1,13 @@
1
1
  type ByteBuffer = Uint8Array;
2
2
  type BucketHandle = object;
3
+ type TransactionHandle = object;
3
4
  type ListIteratorHandle = object;
4
5
  type TransformIteratorHandle = object;
5
6
  type SubscriptionHandle = object;
7
+ export interface ReplicationCursor {
8
+ generation: bigint;
9
+ next_frame_index: bigint;
10
+ }
6
11
  interface IndexOptions {
7
12
  unique: boolean;
8
13
  sparse: boolean;
@@ -69,19 +74,28 @@ interface AlbedoModule {
69
74
  listClose(cursor: ListIteratorHandle): void;
70
75
  listData(cursor: ListIteratorHandle): unknown | null;
71
76
  insert(bucket: BucketHandle, doc: ByteBuffer | object): void;
77
+ checkpoint(bucket: BucketHandle): void;
78
+ transactionBegin(bucket: BucketHandle): TransactionHandle;
79
+ transactionInsert(tx: TransactionHandle, doc: ByteBuffer | object): void;
72
80
  ensureIndex(bucket: BucketHandle, name: string, options: IndexOptions): void;
73
81
  listIndexes(bucket: BucketHandle): Record<string, IndexInfo>;
74
82
  dropIndex(bucket: BucketHandle, name: string): void;
75
83
  delete(bucket: BucketHandle, query: object): void;
84
+ transactionDelete(tx: TransactionHandle, query: object): void;
76
85
  transform(bucket: BucketHandle, query: object): TransformIteratorHandle;
86
+ transactionTransform(tx: TransactionHandle, query: object): TransformIteratorHandle;
77
87
  transformClose(iter: TransformIteratorHandle): void;
78
88
  transformData(iter: TransformIteratorHandle): unknown | null;
79
89
  transformApply(iter: TransformIteratorHandle, replace: ByteBuffer | object | null): void;
90
+ transactionCommit(tx: TransactionHandle): void;
91
+ transactionRollback(tx: TransactionHandle): void;
92
+ transactionClose(tx: TransactionHandle): void;
80
93
  subscribe(bucket: BucketHandle, query: object): SubscriptionHandle;
81
94
  subscribePoll<T = unknown>(sub: SubscriptionHandle, maxEvents: number): SubscriptionBatch<T> | null;
82
95
  subscribeClose(sub: SubscriptionHandle): void;
83
- setReplicationCallback(bucket: BucketHandle, callback: (data: Uint8Array) => void): void;
84
- applyReplicationBatch(bucket: BucketHandle, data: ByteBuffer): void;
96
+ replicationCursor(bucket: BucketHandle): ReplicationCursor;
97
+ readReplicationBatch(bucket: BucketHandle, cursor: ReplicationCursor, maxBytes: number): ByteBuffer | null;
98
+ applyReplicationBatch(bucket: BucketHandle, data: ByteBuffer): ReplicationCursor;
85
99
  }
86
100
  export declare const albedo: AlbedoModule;
87
101
  export default albedo;
@@ -99,6 +113,50 @@ export declare const BSON: {
99
113
  * ```
100
114
  */
101
115
  export declare const ObjectId: ObjectIdConstructor;
116
+ type QueryInput = object | Query;
117
+ type TransformReplacement<T extends object> = T | ByteBuffer | null;
118
+ /**
119
+ * Wrapper around a native transaction handle providing
120
+ * write operations that are committed or rolled back together.
121
+ */
122
+ export declare class Transaction {
123
+ private handle;
124
+ constructor(handle: object);
125
+ private get nativeHandle();
126
+ /**
127
+ * Insert a document or raw byte buffer into the transaction.
128
+ */
129
+ insert(doc: object | ByteBuffer): void;
130
+ /**
131
+ * Delete documents matching the query from the transaction.
132
+ */
133
+ delete(query?: QueryInput): void;
134
+ /**
135
+ * Generator that allows reading and modifying matching documents
136
+ * within the transaction.
137
+ */
138
+ transformIterator<T extends object>(query?: QueryInput): Generator<T, undefined, TransformReplacement<T>>;
139
+ /**
140
+ * Apply a transformation function to matching documents in the transaction.
141
+ */
142
+ transform<T extends object>(query: QueryInput | undefined, fn: (doc: T) => TransformReplacement<T>): void;
143
+ /**
144
+ * Alias for `transform` that reads more naturally for document updates.
145
+ */
146
+ update<T extends object>(query: QueryInput | undefined, fn: (doc: T) => TransformReplacement<T>): void;
147
+ /**
148
+ * Commit the transaction.
149
+ */
150
+ commit(): void;
151
+ /**
152
+ * Roll back the transaction.
153
+ */
154
+ rollback(): void;
155
+ /**
156
+ * Close the transaction and release native resources.
157
+ */
158
+ close(): void;
159
+ }
102
160
  /**
103
161
  * Wrapper around a native Albedo bucket handle providing
104
162
  * methods for CRUD operations, indexing, iteration, and
@@ -157,6 +215,21 @@ export declare class Bucket {
157
215
  * ```
158
216
  */
159
217
  insert(doc: object | ByteBuffer): void;
218
+ /**
219
+ * Flush buffered bucket state through the native checkpoint mechanism.
220
+ */
221
+ checkpoint(): void;
222
+ /**
223
+ * Begin a manual transaction on this bucket.
224
+ */
225
+ beginTransaction(): Transaction;
226
+ /**
227
+ * Run a callback inside a transaction and commit it on success.
228
+ *
229
+ * If the callback throws, the transaction is rolled back before the
230
+ * original error is re-thrown.
231
+ */
232
+ tx<T>(fn: (tx: Transaction) => T): T;
160
233
  /**
161
234
  * Delete documents matching the query. If no query is provided,
162
235
  * all documents will be removed.
@@ -275,7 +348,7 @@ export declare class Bucket {
275
348
  * }
276
349
  * ```
277
350
  */
278
- transformIterator<T>(query?: object | Query): Generator<T, undefined, null | object>;
351
+ transformIterator<T extends object>(query?: object | Query): Generator<T, undefined, TransformReplacement<T>>;
279
352
  /**
280
353
  * Apply a transformation function to each document matching the
281
354
  * provided query. The predicate receives the current document and
@@ -294,19 +367,13 @@ export declare class Bucket {
294
367
  * });
295
368
  * ```
296
369
  */
297
- transform<T extends object>(query: object | Query | undefined, fn: (doc: T) => T | null): void;
370
+ transform<T extends object>(query: object | Query | undefined, fn: (doc: T) => TransformReplacement<T>): void;
298
371
  /**
299
- * Register a callback to receive replication data produced by the
300
- * bucket.
301
- * @param callback - invoked with raw replication bytes
302
- * @example
303
- * ```ts
304
- * bucket.setReplicationCallback(bytes => {
305
- * console.log('got replication', bytes.length);
306
- * });
307
- * ```
372
+ * Alias for `transform` that reads more naturally for document updates.
308
373
  */
309
- setReplicationCallback(callback: (data: Uint8Array) => void): void;
374
+ update<T extends object>(query: object | Query | undefined, fn: (doc: T) => TransformReplacement<T>): void;
375
+ replicationCursor(): ReplicationCursor;
376
+ readReplicationBatch(cursor: ReplicationCursor, maxBytes?: number): Uint8Array | null;
310
377
  /**
311
378
  * Apply a batch of replication operations to this bucket.
312
379
  * @param data - bytes produced by another bucket's replication
@@ -315,7 +382,7 @@ export declare class Bucket {
315
382
  * bucket.applyReplicationBatch(remoteBytes);
316
383
  * ```
317
384
  */
318
- applyReplicationBatch(data: Uint8Array): void;
385
+ applyReplicationBatch(data: Uint8Array): ReplicationCursor;
319
386
  }
320
387
  type BSONValue = any;
321
388
  type FilterOperators = {
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Query = exports.Bucket = exports.ObjectId = exports.BSON = exports.albedo = void 0;
3
+ exports.Query = exports.Bucket = exports.Transaction = exports.ObjectId = exports.BSON = exports.albedo = void 0;
4
4
  exports.where = where;
5
5
  const detect_libc_1 = require("detect-libc");
6
6
  function getNativeBinding() {
@@ -47,6 +47,106 @@ exports.BSON = {
47
47
  * ```
48
48
  */
49
49
  exports.ObjectId = exports.albedo.ObjectId;
50
+ function convertToQuery(query) {
51
+ if (query instanceof Query) {
52
+ return query.query;
53
+ }
54
+ return query || {};
55
+ }
56
+ function* iterateTransform(iter) {
57
+ try {
58
+ let data;
59
+ while ((data = exports.albedo.transformData(iter)) !== undefined) {
60
+ const newDoc = yield data;
61
+ exports.albedo.transformApply(iter, newDoc);
62
+ }
63
+ }
64
+ finally {
65
+ exports.albedo.transformClose(iter);
66
+ }
67
+ }
68
+ function applyTransform(iter, fn) {
69
+ try {
70
+ let data;
71
+ while ((data = exports.albedo.transformData(iter)) !== undefined) {
72
+ exports.albedo.transformApply(iter, fn(data));
73
+ }
74
+ }
75
+ finally {
76
+ exports.albedo.transformClose(iter);
77
+ }
78
+ }
79
+ function aggregateErrors(message, errors) {
80
+ return new AggregateError(errors, message);
81
+ }
82
+ /**
83
+ * Wrapper around a native transaction handle providing
84
+ * write operations that are committed or rolled back together.
85
+ */
86
+ class Transaction {
87
+ handle;
88
+ constructor(handle) {
89
+ this.handle = handle;
90
+ }
91
+ get nativeHandle() {
92
+ if (this.handle === null) {
93
+ throw new Error("Transaction is closed");
94
+ }
95
+ return this.handle;
96
+ }
97
+ /**
98
+ * Insert a document or raw byte buffer into the transaction.
99
+ */
100
+ insert(doc) {
101
+ exports.albedo.transactionInsert(this.nativeHandle, doc);
102
+ }
103
+ /**
104
+ * Delete documents matching the query from the transaction.
105
+ */
106
+ delete(query) {
107
+ exports.albedo.transactionDelete(this.nativeHandle, convertToQuery(query));
108
+ }
109
+ /**
110
+ * Generator that allows reading and modifying matching documents
111
+ * within the transaction.
112
+ */
113
+ transformIterator(query) {
114
+ return iterateTransform(exports.albedo.transactionTransform(this.nativeHandle, convertToQuery(query)));
115
+ }
116
+ /**
117
+ * Apply a transformation function to matching documents in the transaction.
118
+ */
119
+ transform(query, fn) {
120
+ applyTransform(exports.albedo.transactionTransform(this.nativeHandle, convertToQuery(query)), fn);
121
+ }
122
+ /**
123
+ * Alias for `transform` that reads more naturally for document updates.
124
+ */
125
+ update(query, fn) {
126
+ this.transform(query, fn);
127
+ }
128
+ /**
129
+ * Commit the transaction.
130
+ */
131
+ commit() {
132
+ exports.albedo.transactionCommit(this.nativeHandle);
133
+ }
134
+ /**
135
+ * Roll back the transaction.
136
+ */
137
+ rollback() {
138
+ exports.albedo.transactionRollback(this.nativeHandle);
139
+ }
140
+ /**
141
+ * Close the transaction and release native resources.
142
+ */
143
+ close() {
144
+ const handle = this.nativeHandle;
145
+ exports.albedo.transactionClose(handle);
146
+ this.handle = null;
147
+ }
148
+ }
149
+ exports.Transaction = Transaction;
50
150
  /**
51
151
  * Wrapper around a native Albedo bucket handle providing
52
152
  * methods for CRUD operations, indexing, iteration, and
@@ -114,6 +214,75 @@ class Bucket {
114
214
  insert(doc) {
115
215
  exports.albedo.insert(this.handle, doc);
116
216
  }
217
+ /**
218
+ * Flush buffered bucket state through the native checkpoint mechanism.
219
+ */
220
+ checkpoint() {
221
+ exports.albedo.checkpoint(this.handle);
222
+ }
223
+ /**
224
+ * Begin a manual transaction on this bucket.
225
+ */
226
+ beginTransaction() {
227
+ return new Transaction(exports.albedo.transactionBegin(this.handle));
228
+ }
229
+ /**
230
+ * Run a callback inside a transaction and commit it on success.
231
+ *
232
+ * If the callback throws, the transaction is rolled back before the
233
+ * original error is re-thrown.
234
+ */
235
+ tx(fn) {
236
+ const tx = this.beginTransaction();
237
+ let result;
238
+ try {
239
+ result = fn(tx);
240
+ }
241
+ catch (error) {
242
+ try {
243
+ tx.rollback();
244
+ }
245
+ catch (rollbackError) {
246
+ try {
247
+ tx.close();
248
+ }
249
+ catch (closeError) {
250
+ throw aggregateErrors("Transaction failed, rollback failed, and close failed", [error, rollbackError, closeError]);
251
+ }
252
+ throw aggregateErrors("Transaction failed and rollback failed", [
253
+ error,
254
+ rollbackError,
255
+ ]);
256
+ }
257
+ try {
258
+ tx.close();
259
+ }
260
+ catch (closeError) {
261
+ throw aggregateErrors("Transaction failed and close failed", [
262
+ error,
263
+ closeError,
264
+ ]);
265
+ }
266
+ throw error;
267
+ }
268
+ try {
269
+ tx.commit();
270
+ }
271
+ catch (commitError) {
272
+ try {
273
+ tx.close();
274
+ }
275
+ catch (closeError) {
276
+ throw aggregateErrors("Transaction commit and close both failed", [
277
+ commitError,
278
+ closeError,
279
+ ]);
280
+ }
281
+ throw commitError;
282
+ }
283
+ tx.close();
284
+ return result;
285
+ }
117
286
  /**
118
287
  * Delete documents matching the query. If no query is provided,
119
288
  * all documents will be removed.
@@ -126,7 +295,7 @@ class Bucket {
126
295
  * ```
127
296
  */
128
297
  delete(query) {
129
- exports.albedo.delete(this.handle, Bucket.convertToQuery(query));
298
+ exports.albedo.delete(this.handle, convertToQuery(query));
130
299
  }
131
300
  /**
132
301
  * Retrieve information about all indexes defined on the bucket.
@@ -172,7 +341,7 @@ class Bucket {
172
341
  * ```
173
342
  */
174
343
  *list(query) {
175
- const cursor = exports.albedo.list(this.handle, Bucket.convertToQuery(query));
344
+ const cursor = exports.albedo.list(this.handle, convertToQuery(query));
176
345
  try {
177
346
  let data;
178
347
  while ((data = exports.albedo.listData(cursor)) !== null) {
@@ -206,7 +375,7 @@ class Bucket {
206
375
  async *subscribe(query, options) {
207
376
  const pollingTimeout = options?.pollingTimeout ?? 50;
208
377
  const batchSize = options?.batchSize ?? 64;
209
- const subscription = exports.albedo.subscribe(this.handle, Bucket.convertToQuery(query));
378
+ const subscription = exports.albedo.subscribe(this.handle, convertToQuery(query));
210
379
  try {
211
380
  while (true) {
212
381
  const batch = exports.albedo.subscribePoll(subscription, batchSize);
@@ -262,10 +431,7 @@ class Bucket {
262
431
  * ```
263
432
  */
264
433
  static convertToQuery(query) {
265
- if (query instanceof Query) {
266
- return query.query;
267
- }
268
- return query || {};
434
+ return convertToQuery(query);
269
435
  }
270
436
  /**
271
437
  * Generator that allows reading and optionally modifying each
@@ -284,18 +450,7 @@ class Bucket {
284
450
  * ```
285
451
  */
286
452
  *transformIterator(query) {
287
- const queryObj = Bucket.convertToQuery(query);
288
- const iter = exports.albedo.transform(this.handle, queryObj);
289
- try {
290
- let data;
291
- while ((data = exports.albedo.transformData(iter)) !== undefined) {
292
- const newDoc = yield data;
293
- exports.albedo.transformApply(iter, newDoc);
294
- }
295
- }
296
- finally {
297
- exports.albedo.transformClose(iter);
298
- }
453
+ yield* iterateTransform(exports.albedo.transform(this.handle, convertToQuery(query)));
299
454
  }
300
455
  /**
301
456
  * Apply a transformation function to each document matching the
@@ -316,31 +471,19 @@ class Bucket {
316
471
  * ```
317
472
  */
318
473
  transform(query, fn) {
319
- const queryObj = Bucket.convertToQuery(query);
320
- const iter = exports.albedo.transform(this.handle, queryObj);
321
- try {
322
- let data;
323
- while ((data = exports.albedo.transformData(iter)) !== undefined) {
324
- exports.albedo.transformApply(iter, fn(data));
325
- }
326
- }
327
- finally {
328
- exports.albedo.transformClose(iter);
329
- }
474
+ applyTransform(exports.albedo.transform(this.handle, convertToQuery(query)), fn);
330
475
  }
331
476
  /**
332
- * Register a callback to receive replication data produced by the
333
- * bucket.
334
- * @param callback - invoked with raw replication bytes
335
- * @example
336
- * ```ts
337
- * bucket.setReplicationCallback(bytes => {
338
- * console.log('got replication', bytes.length);
339
- * });
340
- * ```
477
+ * Alias for `transform` that reads more naturally for document updates.
341
478
  */
342
- setReplicationCallback(callback) {
343
- exports.albedo.setReplicationCallback(this.handle, callback);
479
+ update(query, fn) {
480
+ this.transform(query, fn);
481
+ }
482
+ replicationCursor() {
483
+ return exports.albedo.replicationCursor(this.handle);
484
+ }
485
+ readReplicationBatch(cursor, maxBytes = 0) {
486
+ return exports.albedo.readReplicationBatch(this.handle, cursor, maxBytes);
344
487
  }
345
488
  /**
346
489
  * Apply a batch of replication operations to this bucket.
@@ -351,7 +494,7 @@ class Bucket {
351
494
  * ```
352
495
  */
353
496
  applyReplicationBatch(data) {
354
- exports.albedo.applyReplicationBatch(this.handle, data);
497
+ return exports.albedo.applyReplicationBatch(this.handle, data);
355
498
  }
356
499
  }
357
500
  exports.Bucket = Bucket;
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "albedo-node",
3
- "version": "0.7.2",
3
+ "version": "0.8.1",
4
4
  "description": "High-performance embedded database for Node.js with native bindings, BSON support, indexing, and replication",
5
5
  "keywords": [
6
6
  "albedo",