albedo-node 0.6.3 โ†’ 0.7.2

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,7 +8,7 @@ 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
11
+ - ๐Ÿ” Generator-based iterators for queries, transforms, and live subscriptions
12
12
  - ๐Ÿ”„ Built-in replication callback and apply mechanisms
13
13
  - ๐Ÿงช TypeScript typings and Bun-based test suite
14
14
 
@@ -86,6 +86,48 @@ while (!step.done) {
86
86
  }
87
87
  ```
88
88
 
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:
113
+
114
+ ```ts
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:
125
+
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
+
89
131
  ### Replication
90
132
 
91
133
  ```ts
@@ -146,6 +188,7 @@ Serialized documents that contain `_id` fields with a BSON ObjectId will be revi
146
188
  - `insert(doc: object | Uint8Array)`
147
189
  - `delete(query?: object | Query)`
148
190
  - `list<T>(query?: object | Query): Generator<T>`
191
+ - `subscribe<T>(query?: object | Query, options?: { pollingTimeout?: number; batchSize?: number }): AsyncGenerator<SubscriptionEvent<T>>`
149
192
  - `transformIterator<T>(query?: object | Query): Generator<T, undefined, object | null>`
150
193
  - `ensureIndex(name: string, options: { unique: boolean; sparse: boolean; reverse: boolean })`
151
194
  - `dropIndex(name: string)`
@@ -154,6 +197,13 @@ Serialized documents that contain `_id` fields with a BSON ObjectId will be revi
154
197
  - `applyReplicationBatch(batch: Uint8Array)`
155
198
  - accepts raw BSON `Uint8Array` payloads anywhere a query or document object is expected
156
199
 
200
+ - `SubscriptionEvent<T>`
201
+ - `seqno: bigint`
202
+ - `op: "insert" | "update" | "delete"`
203
+ - `doc_id: ObjectId`
204
+ - `ts: bigint`
205
+ - `doc?: T`
206
+
157
207
  - `Query`
158
208
  - `where(field, filter)` chains field predicates (e.g. `{ $eq: value }`, `{ $gt: 10 }`)
159
209
  - `sortBy(field, direction?)` sets sort order
package/dist/index.d.ts CHANGED
@@ -1,9 +1,19 @@
1
1
  type ByteBuffer = Uint8Array;
2
+ type BucketHandle = object;
3
+ type ListIteratorHandle = object;
4
+ type TransformIteratorHandle = object;
5
+ type SubscriptionHandle = object;
2
6
  interface IndexOptions {
3
7
  unique: boolean;
4
8
  sparse: boolean;
5
9
  reverse: boolean;
6
10
  }
11
+ interface IndexInfo {
12
+ name: string;
13
+ unique: boolean;
14
+ sparse: boolean;
15
+ reverse: boolean;
16
+ }
7
17
  interface ObjectIdInstance {
8
18
  buffer: ByteBuffer;
9
19
  toString(): string;
@@ -12,6 +22,20 @@ interface ObjectIdConstructor {
12
22
  new (buffer?: ByteBuffer): ObjectIdInstance;
13
23
  fromString(str: string): ObjectIdInstance;
14
24
  }
25
+ export interface SubscriptionEvent<T = unknown> {
26
+ seqno: bigint;
27
+ op: "insert" | "update" | "delete";
28
+ doc_id: ObjectIdInstance;
29
+ ts: bigint;
30
+ doc?: T;
31
+ }
32
+ interface SubscriptionBatch<T = unknown> {
33
+ batch: Array<SubscriptionEvent<T>>;
34
+ }
35
+ export interface SubscribeOptions {
36
+ pollingTimeout?: number;
37
+ batchSize?: number;
38
+ }
15
39
  /**
16
40
  * Controls when fsync is called to guarantee write durability.
17
41
  */
@@ -34,11 +58,36 @@ interface OpenBucketOptions {
34
58
  write_durability?: WriteDurability;
35
59
  read_durability?: ReadDurability;
36
60
  }
37
- export declare const albedo: any;
61
+ interface AlbedoModule {
62
+ ObjectId: ObjectIdConstructor;
63
+ serialize(value: unknown): Uint8Array;
64
+ deserialize<T = unknown>(data: ByteBuffer): T;
65
+ open(path: string): BucketHandle;
66
+ open_with_options(path: string, options: OpenBucketOptions): BucketHandle;
67
+ close(bucket: BucketHandle): void;
68
+ list(bucket: BucketHandle, query: object): ListIteratorHandle;
69
+ listClose(cursor: ListIteratorHandle): void;
70
+ listData(cursor: ListIteratorHandle): unknown | null;
71
+ insert(bucket: BucketHandle, doc: ByteBuffer | object): void;
72
+ ensureIndex(bucket: BucketHandle, name: string, options: IndexOptions): void;
73
+ listIndexes(bucket: BucketHandle): Record<string, IndexInfo>;
74
+ dropIndex(bucket: BucketHandle, name: string): void;
75
+ delete(bucket: BucketHandle, query: object): void;
76
+ transform(bucket: BucketHandle, query: object): TransformIteratorHandle;
77
+ transformClose(iter: TransformIteratorHandle): void;
78
+ transformData(iter: TransformIteratorHandle): unknown | null;
79
+ transformApply(iter: TransformIteratorHandle, replace: ByteBuffer | object | null): void;
80
+ subscribe(bucket: BucketHandle, query: object): SubscriptionHandle;
81
+ subscribePoll<T = unknown>(sub: SubscriptionHandle, maxEvents: number): SubscriptionBatch<T> | null;
82
+ subscribeClose(sub: SubscriptionHandle): void;
83
+ setReplicationCallback(bucket: BucketHandle, callback: (data: Uint8Array) => void): void;
84
+ applyReplicationBatch(bucket: BucketHandle, data: ByteBuffer): void;
85
+ }
86
+ export declare const albedo: AlbedoModule;
38
87
  export default albedo;
39
88
  export declare const BSON: {
40
- serialize: any;
41
- deserialize: any;
89
+ serialize: (value: unknown) => Uint8Array;
90
+ deserialize: <T = unknown>(data: ByteBuffer) => T;
42
91
  };
43
92
  /**
44
93
  * Native ObjectId class constructor.
@@ -127,7 +176,7 @@ export declare class Bucket {
127
176
  * console.log(bucket.indexes);
128
177
  * ```
129
178
  */
130
- get indexes(): any;
179
+ get indexes(): Record<string, IndexInfo>;
131
180
  /**
132
181
  * Create or update an index on a field.
133
182
  * @param name - index name (field path)
@@ -159,29 +208,26 @@ export declare class Bucket {
159
208
  */
160
209
  list<T>(query?: object | Query): Generator<T>;
161
210
  /**
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.
211
+ * Async iterator that continuously polls a change subscription.
166
212
  *
167
- * The native cursor is closed automatically when the consumer breaks
168
- * out of the loop or the iterator is otherwise disposed.
213
+ * The generator yields individual oplog events rather than rescanned
214
+ * documents, and automatically closes the native subscription when the
215
+ * consumer stops iterating.
169
216
  *
170
217
  * @param query - filter or `Query` object
171
218
  * @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
219
+ * @param options.pollingTimeout - ms to wait before retrying when the
220
+ * subscription is idle (default `50`)
221
+ * @param options.batchSize - maximum number of change events to pull per
222
+ * native poll (default `64`)
175
223
  * @example
176
224
  * ```ts
177
- * for await (const user of bucket.stream<User>(where('active', { $eq: true }))) {
178
- * console.log(user);
225
+ * for await (const event of bucket.subscribe<User>(where('active', { $eq: true }))) {
226
+ * console.log(event.op, event.doc);
179
227
  * }
180
228
  * ```
181
229
  */
182
- stream<T>(query?: object | Query, options?: {
183
- pollingTimeout?: number;
184
- }): AsyncGenerator<T>;
230
+ subscribe<T>(query?: object | Query, options?: SubscribeOptions): AsyncGenerator<SubscriptionEvent<T>>;
185
231
  /**
186
232
  * Collect all documents matching the optional query into an array.
187
233
  * @param query - filter or `Query` object
package/dist/index.js CHANGED
@@ -184,34 +184,36 @@ class Bucket {
184
184
  }
185
185
  }
186
186
  /**
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.
187
+ * Async iterator that continuously polls a change subscription.
191
188
  *
192
- * The native cursor is closed automatically when the consumer breaks
193
- * out of the loop or the iterator is otherwise disposed.
189
+ * The generator yields individual oplog events rather than rescanned
190
+ * documents, and automatically closes the native subscription when the
191
+ * consumer stops iterating.
194
192
  *
195
193
  * @param query - filter or `Query` object
196
194
  * @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
195
+ * @param options.pollingTimeout - ms to wait before retrying when the
196
+ * subscription is idle (default `50`)
197
+ * @param options.batchSize - maximum number of change events to pull per
198
+ * native poll (default `64`)
200
199
  * @example
201
200
  * ```ts
202
- * for await (const user of bucket.stream<User>(where('active', { $eq: true }))) {
203
- * console.log(user);
201
+ * for await (const event of bucket.subscribe<User>(where('active', { $eq: true }))) {
202
+ * console.log(event.op, event.doc);
204
203
  * }
205
204
  * ```
206
205
  */
207
- async *stream(query, options) {
206
+ async *subscribe(query, options) {
208
207
  const pollingTimeout = options?.pollingTimeout ?? 50;
209
- const cursor = exports.albedo.list(this.handle, Bucket.convertToQuery(query));
208
+ const batchSize = options?.batchSize ?? 64;
209
+ const subscription = exports.albedo.subscribe(this.handle, Bucket.convertToQuery(query));
210
210
  try {
211
211
  while (true) {
212
- const data = exports.albedo.listData(cursor);
213
- if (data !== null) {
214
- yield data;
212
+ const batch = exports.albedo.subscribePoll(subscription, batchSize);
213
+ if (batch !== null) {
214
+ for (const event of batch.batch) {
215
+ yield event;
216
+ }
215
217
  }
216
218
  else {
217
219
  await new Promise((r) => setTimeout(r, pollingTimeout));
@@ -219,7 +221,7 @@ class Bucket {
219
221
  }
220
222
  }
221
223
  finally {
222
- exports.albedo.listClose(cursor);
224
+ exports.albedo.subscribeClose(subscription);
223
225
  }
224
226
  }
225
227
  /**
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.7.2",
4
4
  "description": "High-performance embedded database for Node.js with native bindings, BSON support, indexing, and replication",
5
5
  "keywords": [
6
6
  "albedo",