albedo-node 0.6.3 โ†’ 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
@@ -8,8 +8,8 @@ This package wraps the core database engine with a concise TypeScript API, ships
8
8
 
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
- - ๐Ÿ” Generator-based iterators for queries and in-place document transforms
12
- - ๐Ÿ”„ Built-in replication callback and apply mechanisms
11
+ - ๐Ÿ” Generator-based iterators for queries, transforms, and live subscriptions
12
+ - ๐Ÿ”„ WAL-based replication helpers with explicit cursors and batches
13
13
  - ๐Ÿงช TypeScript typings and Bun-based test suite
14
14
 
15
15
  ## Installation
@@ -86,28 +86,81 @@ while (!step.done) {
86
86
  }
87
87
  ```
88
88
 
89
- ### Replication
89
+ ### Subscribing to change events
90
+
91
+ `Bucket.subscribe()` exposes the new oplog-backed subscription API as an async
92
+ generator. It yields individual change events rather than re-scanned documents.
93
+
94
+ ```ts
95
+ const bucket = Bucket.open("./example.bucket", {
96
+ wal: true,
97
+ write_durability: "all",
98
+ });
99
+
100
+ for await (const event of bucket.subscribe<{ name: string }>(
101
+ where("name", { $eq: "Ada" }),
102
+ { pollingTimeout: 50, batchSize: 64 },
103
+ )) {
104
+ console.log(event.op, event.seqno, event.doc);
105
+
106
+ if (event.op === "delete") {
107
+ console.log("deleted", event.doc_id.toString());
108
+ }
109
+ }
110
+ ```
111
+
112
+ Each yielded event has this shape:
90
113
 
91
114
  ```ts
92
- const primary = Bucket.open("./primary.bucket");
93
- const replica = Bucket.open("./replica.bucket");
115
+ type SubscriptionEvent<T = unknown> = {
116
+ seqno: bigint;
117
+ op: "insert" | "update" | "delete";
118
+ doc_id: ObjectId;
119
+ ts: bigint;
120
+ doc?: T;
121
+ };
122
+ ```
123
+
124
+ Notes:
94
125
 
95
- const batches: Uint8Array[] = [];
96
- primary.setReplicationCallback((data) => {
97
- batches.push(data);
126
+ - Subscriptions require WAL mode to be active.
127
+ - `doc` is present for inline insert and update payloads.
128
+ - Delete events still include `doc_id`, even when `doc` is absent.
129
+ - The generator closes the native subscription automatically when iteration stops.
130
+
131
+ ### Replication
132
+
133
+ ```ts
134
+ const primary = Bucket.open("./primary.bucket", {
135
+ wal: true,
136
+ write_durability: "all",
137
+ });
138
+ const replica = Bucket.open("./replica.bucket", {
139
+ wal: true,
140
+ write_durability: "all",
98
141
  });
99
142
 
143
+ const cursor = primary.replicationCursor();
144
+
100
145
  primary.insert({ name: "Replica", version: 1 });
101
- primary.close();
102
146
 
103
- // Apply the first batch to the replica bucket
104
- if (batches.length > 0) {
105
- 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);
106
151
  }
107
152
 
153
+ primary.close();
108
154
  replica.close();
109
155
  ```
110
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
+
111
164
  ## Query objects and BSON payloads
112
165
 
113
166
  Most bucket operations accept either:
@@ -139,21 +192,44 @@ Serialized documents that contain `_id` fields with a BSON ObjectId will be revi
139
192
  ## API reference
140
193
 
141
194
  - `default` export โ€” raw Albedo native module
142
- - `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.
143
196
  - `BSON.serialize(value)` / `BSON.deserialize(bytes)` โ€” helper wrappers
144
197
  - `Bucket`
145
- - `static open(path: string): Bucket`
198
+ - `static open(path: string, options?: OpenBucketOptions): Bucket`
146
199
  - `insert(doc: object | Uint8Array)`
200
+ - `checkpoint()`
147
201
  - `delete(query?: object | Query)`
148
202
  - `list<T>(query?: object | Query): Generator<T>`
203
+ - `subscribe<T>(query?: object | Query, options?: { pollingTimeout?: number; batchSize?: number }): AsyncGenerator<SubscriptionEvent<T>>`
149
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)`
150
208
  - `ensureIndex(name: string, options: { unique: boolean; sparse: boolean; reverse: boolean })`
151
209
  - `dropIndex(name: string)`
152
210
  - `indexes: Record<string, { name: string; unique: boolean; sparse: boolean; reverse: boolean }>`
153
- - `setReplicationCallback(cb: (data: Uint8Array) => void)`
154
- - `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 }`
155
214
  - accepts raw BSON `Uint8Array` payloads anywhere a query or document object is expected
156
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
+
226
+ - `SubscriptionEvent<T>`
227
+ - `seqno: bigint`
228
+ - `op: "insert" | "update" | "delete"`
229
+ - `doc_id: ObjectId`
230
+ - `ts: bigint`
231
+ - `doc?: T`
232
+
157
233
  - `Query`
158
234
  - `where(field, filter)` chains field predicates (e.g. `{ $eq: value }`, `{ $gt: 10 }`)
159
235
  - `sortBy(field, direction?)` sets sort order
package/dist/index.d.ts CHANGED
@@ -1,9 +1,24 @@
1
1
  type ByteBuffer = Uint8Array;
2
+ type BucketHandle = object;
3
+ type TransactionHandle = object;
4
+ type ListIteratorHandle = object;
5
+ type TransformIteratorHandle = object;
6
+ type SubscriptionHandle = object;
7
+ export interface ReplicationCursor {
8
+ generation: bigint;
9
+ next_frame_index: bigint;
10
+ }
2
11
  interface IndexOptions {
3
12
  unique: boolean;
4
13
  sparse: boolean;
5
14
  reverse: boolean;
6
15
  }
16
+ interface IndexInfo {
17
+ name: string;
18
+ unique: boolean;
19
+ sparse: boolean;
20
+ reverse: boolean;
21
+ }
7
22
  interface ObjectIdInstance {
8
23
  buffer: ByteBuffer;
9
24
  toString(): string;
@@ -12,6 +27,20 @@ interface ObjectIdConstructor {
12
27
  new (buffer?: ByteBuffer): ObjectIdInstance;
13
28
  fromString(str: string): ObjectIdInstance;
14
29
  }
30
+ export interface SubscriptionEvent<T = unknown> {
31
+ seqno: bigint;
32
+ op: "insert" | "update" | "delete";
33
+ doc_id: ObjectIdInstance;
34
+ ts: bigint;
35
+ doc?: T;
36
+ }
37
+ interface SubscriptionBatch<T = unknown> {
38
+ batch: Array<SubscriptionEvent<T>>;
39
+ }
40
+ export interface SubscribeOptions {
41
+ pollingTimeout?: number;
42
+ batchSize?: number;
43
+ }
15
44
  /**
16
45
  * Controls when fsync is called to guarantee write durability.
17
46
  */
@@ -34,11 +63,45 @@ interface OpenBucketOptions {
34
63
  write_durability?: WriteDurability;
35
64
  read_durability?: ReadDurability;
36
65
  }
37
- export declare const albedo: any;
66
+ interface AlbedoModule {
67
+ ObjectId: ObjectIdConstructor;
68
+ serialize(value: unknown): Uint8Array;
69
+ deserialize<T = unknown>(data: ByteBuffer): T;
70
+ open(path: string): BucketHandle;
71
+ open_with_options(path: string, options: OpenBucketOptions): BucketHandle;
72
+ close(bucket: BucketHandle): void;
73
+ list(bucket: BucketHandle, query: object): ListIteratorHandle;
74
+ listClose(cursor: ListIteratorHandle): void;
75
+ listData(cursor: ListIteratorHandle): unknown | null;
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;
80
+ ensureIndex(bucket: BucketHandle, name: string, options: IndexOptions): void;
81
+ listIndexes(bucket: BucketHandle): Record<string, IndexInfo>;
82
+ dropIndex(bucket: BucketHandle, name: string): void;
83
+ delete(bucket: BucketHandle, query: object): void;
84
+ transactionDelete(tx: TransactionHandle, query: object): void;
85
+ transform(bucket: BucketHandle, query: object): TransformIteratorHandle;
86
+ transactionTransform(tx: TransactionHandle, query: object): TransformIteratorHandle;
87
+ transformClose(iter: TransformIteratorHandle): void;
88
+ transformData(iter: TransformIteratorHandle): unknown | null;
89
+ transformApply(iter: TransformIteratorHandle, replace: ByteBuffer | object | null): void;
90
+ transactionCommit(tx: TransactionHandle): void;
91
+ transactionRollback(tx: TransactionHandle): void;
92
+ transactionClose(tx: TransactionHandle): void;
93
+ subscribe(bucket: BucketHandle, query: object): SubscriptionHandle;
94
+ subscribePoll<T = unknown>(sub: SubscriptionHandle, maxEvents: number): SubscriptionBatch<T> | null;
95
+ subscribeClose(sub: SubscriptionHandle): void;
96
+ replicationCursor(bucket: BucketHandle): ReplicationCursor;
97
+ readReplicationBatch(bucket: BucketHandle, cursor: ReplicationCursor, maxBytes: number): ByteBuffer | null;
98
+ applyReplicationBatch(bucket: BucketHandle, data: ByteBuffer): ReplicationCursor;
99
+ }
100
+ export declare const albedo: AlbedoModule;
38
101
  export default albedo;
39
102
  export declare const BSON: {
40
- serialize: any;
41
- deserialize: any;
103
+ serialize: (value: unknown) => Uint8Array;
104
+ deserialize: <T = unknown>(data: ByteBuffer) => T;
42
105
  };
43
106
  /**
44
107
  * Native ObjectId class constructor.
@@ -50,6 +113,50 @@ export declare const BSON: {
50
113
  * ```
51
114
  */
52
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
+ }
53
160
  /**
54
161
  * Wrapper around a native Albedo bucket handle providing
55
162
  * methods for CRUD operations, indexing, iteration, and
@@ -108,6 +215,21 @@ export declare class Bucket {
108
215
  * ```
109
216
  */
110
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;
111
233
  /**
112
234
  * Delete documents matching the query. If no query is provided,
113
235
  * all documents will be removed.
@@ -127,7 +249,7 @@ export declare class Bucket {
127
249
  * console.log(bucket.indexes);
128
250
  * ```
129
251
  */
130
- get indexes(): any;
252
+ get indexes(): Record<string, IndexInfo>;
131
253
  /**
132
254
  * Create or update an index on a field.
133
255
  * @param name - index name (field path)
@@ -159,29 +281,26 @@ export declare class Bucket {
159
281
  */
160
282
  list<T>(query?: object | Query): Generator<T>;
161
283
  /**
162
- * Async iterator that continuously polls for documents matching the
163
- * optional query. Unlike `list`, when there are no more results the
164
- * iterator waits for `pollingTimeout` milliseconds and retries,
165
- * making it suitable for watching a bucket for new data.
284
+ * Async iterator that continuously polls a change subscription.
166
285
  *
167
- * The native cursor is closed automatically when the consumer breaks
168
- * out of the loop or the iterator is otherwise disposed.
286
+ * The generator yields individual oplog events rather than rescanned
287
+ * documents, and automatically closes the native subscription when the
288
+ * consumer stops iterating.
169
289
  *
170
290
  * @param query - filter or `Query` object
171
291
  * @param options - polling configuration
172
- * @param options.pollingTimeout - ms to wait before retrying when
173
- * `listData` returns `null` (default `50`)
174
- * @yields each document deserialized from the bucket
292
+ * @param options.pollingTimeout - ms to wait before retrying when the
293
+ * subscription is idle (default `50`)
294
+ * @param options.batchSize - maximum number of change events to pull per
295
+ * native poll (default `64`)
175
296
  * @example
176
297
  * ```ts
177
- * for await (const user of bucket.stream<User>(where('active', { $eq: true }))) {
178
- * console.log(user);
298
+ * for await (const event of bucket.subscribe<User>(where('active', { $eq: true }))) {
299
+ * console.log(event.op, event.doc);
179
300
  * }
180
301
  * ```
181
302
  */
182
- stream<T>(query?: object | Query, options?: {
183
- pollingTimeout?: number;
184
- }): AsyncGenerator<T>;
303
+ subscribe<T>(query?: object | Query, options?: SubscribeOptions): AsyncGenerator<SubscriptionEvent<T>>;
185
304
  /**
186
305
  * Collect all documents matching the optional query into an array.
187
306
  * @param query - filter or `Query` object
@@ -229,7 +348,7 @@ export declare class Bucket {
229
348
  * }
230
349
  * ```
231
350
  */
232
- transformIterator<T>(query?: object | Query): Generator<T, undefined, null | object>;
351
+ transformIterator<T extends object>(query?: object | Query): Generator<T, undefined, TransformReplacement<T>>;
233
352
  /**
234
353
  * Apply a transformation function to each document matching the
235
354
  * provided query. The predicate receives the current document and
@@ -248,19 +367,13 @@ export declare class Bucket {
248
367
  * });
249
368
  * ```
250
369
  */
251
- 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;
252
371
  /**
253
- * Register a callback to receive replication data produced by the
254
- * bucket.
255
- * @param callback - invoked with raw replication bytes
256
- * @example
257
- * ```ts
258
- * bucket.setReplicationCallback(bytes => {
259
- * console.log('got replication', bytes.length);
260
- * });
261
- * ```
372
+ * Alias for `transform` that reads more naturally for document updates.
262
373
  */
263
- 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;
264
377
  /**
265
378
  * Apply a batch of replication operations to this bucket.
266
379
  * @param data - bytes produced by another bucket's replication
@@ -269,7 +382,7 @@ export declare class Bucket {
269
382
  * bucket.applyReplicationBatch(remoteBytes);
270
383
  * ```
271
384
  */
272
- applyReplicationBatch(data: Uint8Array): void;
385
+ applyReplicationBatch(data: Uint8Array): ReplicationCursor;
273
386
  }
274
387
  type BSONValue = any;
275
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) {
@@ -184,34 +353,36 @@ class Bucket {
184
353
  }
185
354
  }
186
355
  /**
187
- * Async iterator that continuously polls for documents matching the
188
- * optional query. Unlike `list`, when there are no more results the
189
- * iterator waits for `pollingTimeout` milliseconds and retries,
190
- * making it suitable for watching a bucket for new data.
356
+ * Async iterator that continuously polls a change subscription.
191
357
  *
192
- * The native cursor is closed automatically when the consumer breaks
193
- * out of the loop or the iterator is otherwise disposed.
358
+ * The generator yields individual oplog events rather than rescanned
359
+ * documents, and automatically closes the native subscription when the
360
+ * consumer stops iterating.
194
361
  *
195
362
  * @param query - filter or `Query` object
196
363
  * @param options - polling configuration
197
- * @param options.pollingTimeout - ms to wait before retrying when
198
- * `listData` returns `null` (default `50`)
199
- * @yields each document deserialized from the bucket
364
+ * @param options.pollingTimeout - ms to wait before retrying when the
365
+ * subscription is idle (default `50`)
366
+ * @param options.batchSize - maximum number of change events to pull per
367
+ * native poll (default `64`)
200
368
  * @example
201
369
  * ```ts
202
- * for await (const user of bucket.stream<User>(where('active', { $eq: true }))) {
203
- * console.log(user);
370
+ * for await (const event of bucket.subscribe<User>(where('active', { $eq: true }))) {
371
+ * console.log(event.op, event.doc);
204
372
  * }
205
373
  * ```
206
374
  */
207
- async *stream(query, options) {
375
+ async *subscribe(query, options) {
208
376
  const pollingTimeout = options?.pollingTimeout ?? 50;
209
- const cursor = exports.albedo.list(this.handle, Bucket.convertToQuery(query));
377
+ const batchSize = options?.batchSize ?? 64;
378
+ const subscription = exports.albedo.subscribe(this.handle, convertToQuery(query));
210
379
  try {
211
380
  while (true) {
212
- const data = exports.albedo.listData(cursor);
213
- if (data !== null) {
214
- yield data;
381
+ const batch = exports.albedo.subscribePoll(subscription, batchSize);
382
+ if (batch !== null) {
383
+ for (const event of batch.batch) {
384
+ yield event;
385
+ }
215
386
  }
216
387
  else {
217
388
  await new Promise((r) => setTimeout(r, pollingTimeout));
@@ -219,7 +390,7 @@ class Bucket {
219
390
  }
220
391
  }
221
392
  finally {
222
- exports.albedo.listClose(cursor);
393
+ exports.albedo.subscribeClose(subscription);
223
394
  }
224
395
  }
225
396
  /**
@@ -260,10 +431,7 @@ class Bucket {
260
431
  * ```
261
432
  */
262
433
  static convertToQuery(query) {
263
- if (query instanceof Query) {
264
- return query.query;
265
- }
266
- return query || {};
434
+ return convertToQuery(query);
267
435
  }
268
436
  /**
269
437
  * Generator that allows reading and optionally modifying each
@@ -282,18 +450,7 @@ class Bucket {
282
450
  * ```
283
451
  */
284
452
  *transformIterator(query) {
285
- const queryObj = Bucket.convertToQuery(query);
286
- const iter = exports.albedo.transform(this.handle, queryObj);
287
- try {
288
- let data;
289
- while ((data = exports.albedo.transformData(iter)) !== undefined) {
290
- const newDoc = yield data;
291
- exports.albedo.transformApply(iter, newDoc);
292
- }
293
- }
294
- finally {
295
- exports.albedo.transformClose(iter);
296
- }
453
+ yield* iterateTransform(exports.albedo.transform(this.handle, convertToQuery(query)));
297
454
  }
298
455
  /**
299
456
  * Apply a transformation function to each document matching the
@@ -314,31 +471,19 @@ class Bucket {
314
471
  * ```
315
472
  */
316
473
  transform(query, fn) {
317
- const queryObj = Bucket.convertToQuery(query);
318
- const iter = exports.albedo.transform(this.handle, queryObj);
319
- try {
320
- let data;
321
- while ((data = exports.albedo.transformData(iter)) !== undefined) {
322
- exports.albedo.transformApply(iter, fn(data));
323
- }
324
- }
325
- finally {
326
- exports.albedo.transformClose(iter);
327
- }
474
+ applyTransform(exports.albedo.transform(this.handle, convertToQuery(query)), fn);
328
475
  }
329
476
  /**
330
- * Register a callback to receive replication data produced by the
331
- * bucket.
332
- * @param callback - invoked with raw replication bytes
333
- * @example
334
- * ```ts
335
- * bucket.setReplicationCallback(bytes => {
336
- * console.log('got replication', bytes.length);
337
- * });
338
- * ```
477
+ * Alias for `transform` that reads more naturally for document updates.
339
478
  */
340
- setReplicationCallback(callback) {
341
- 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);
342
487
  }
343
488
  /**
344
489
  * Apply a batch of replication operations to this bucket.
@@ -349,7 +494,7 @@ class Bucket {
349
494
  * ```
350
495
  */
351
496
  applyReplicationBatch(data) {
352
- exports.albedo.applyReplicationBatch(this.handle, data);
497
+ return exports.albedo.applyReplicationBatch(this.handle, data);
353
498
  }
354
499
  }
355
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.6.3",
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",