@stoolap/node 0.4.1 → 0.5.0-beta.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
@@ -97,7 +97,8 @@ const db = await Database.open('file:///absolute/path/to/db');
97
97
  | `exec(sql)` | `Promise<void>` | Execute a DDL statement |
98
98
  | `query(sql, params?)` | `Promise<Object[]>` | Query rows as objects |
99
99
  | `queryOne(sql, params?)` | `Promise<Object \| null>` | Query single row |
100
- | `queryRaw(sql, params?)` | `Promise<{columns, rows}>` | Query in columnar format |
100
+ | `queryRaw(sql, params?)` | `Promise<{columns, rows}>` | Query as arrays of values |
101
+ | `iterate(sql, params?)` | `RowCursor` | Cursor over rows, `for await` or `for...of` |
101
102
  | `begin()` | `Promise<Transaction>` | Begin a transaction |
102
103
  | `close()` | `Promise<void>` | Close the database |
103
104
 
@@ -113,7 +114,8 @@ Sync methods run on the main thread. Faster for simple operations but block the
113
114
  | `execSync(sql)` | `void` | Execute a DDL statement |
114
115
  | `querySync(sql, params?)` | `Object[]` | Query rows as objects |
115
116
  | `queryOneSync(sql, params?)` | `Object \| null` | Query single row |
116
- | `queryRawSync(sql, params?)` | `{columns, rows}` | Query in columnar format |
117
+ | `queryRawSync(sql, params?)` | `{columns, rows}` | Query as arrays of values |
118
+ | `iterate(sql, params?)` | `RowCursor` | Cursor over rows, fetched in chunks |
117
119
  | `executeBatchSync(sql, paramsArray)` | `RunResult` | Execute with multiple param sets |
118
120
  | `beginSync()` | `Transaction` | Begin a transaction |
119
121
  | `prepare(sql)` | `PreparedStatement` | Create a prepared statement |
@@ -185,7 +187,9 @@ Controls the durability vs. performance trade-off:
185
187
  | `wal_compression` | `on` | LZ4 compression for WAL entries |
186
188
  | `volume_compression` | `on` | LZ4 compression for cold volume files |
187
189
  | `compression` | `on` | Shorthand: set both `wal_compression` and `volume_compression` |
188
- | `keep_snapshots` | `5` | Number of backup snapshot files to retain |
190
+ | `keep_snapshots` | `3` | Number of backup snapshot files to retain |
191
+ | `hot_max_rows` | `262144` | Committed hot rows per table that trigger an early seal (`0` = off) |
192
+ | `hot_max_bytes` | `0` | Hot row bytes per table above which commits wait for a seal (`0` = off) |
189
193
 
190
194
  #### Cloning
191
195
 
@@ -207,7 +211,7 @@ await db.close();
207
211
 
208
212
  #### Raw Query Format
209
213
 
210
- `queryRaw` / `queryRawSync` return `{ columns: string[], rows: any[][] }` instead of an array of objects. Faster when you don't need named keys.
214
+ `queryRaw` / `queryRawSync` return `{ columns: string[], rows: any[][] }`, one array per row, instead of an array of objects. Faster when you don't need named keys.
211
215
 
212
216
  ```js
213
217
  const raw = db.queryRawSync('SELECT id, name, email FROM users ORDER BY id');
@@ -215,6 +219,25 @@ console.log(raw.columns); // ['id', 'name', 'email']
215
219
  console.log(raw.rows); // [[1, 'Alice', 'alice@example.com'], [2, 'Bob', 'bob@example.com']]
216
220
  ```
217
221
 
222
+ #### Iterating Rows
223
+
224
+ `iterate` opens a cursor and fetches rows in chunks (32 rows, doubling to 256) as the loop advances, so a large result never has to be materialized at once. The same cursor works with `for...of` and `for await...of`, and it closes itself when the loop ends, breaks or throws. `Database`, `Transaction` and `PreparedStatement` all have it.
225
+
226
+ ```js
227
+ for (const user of db.iterate('SELECT * FROM users WHERE active = $1', [true])) {
228
+ if (user.balance < 0) break; // the cursor is closed on break
229
+ }
230
+
231
+ for await (const row of db.iterate('SELECT * FROM events WHERE day = :day', { day: '2026-10-21' })) {
232
+ await ship(row);
233
+ }
234
+
235
+ const stmt = db.prepare('SELECT * FROM users WHERE age > :min');
236
+ for (const user of stmt.iterate({ min: 30 })) { /* ... */ }
237
+ ```
238
+
239
+ A cursor opened on a transaction is closed by `commit` and `rollback`; one opened on a database is closed with it. Call `cursor.close()` to end an iteration early without breaking out of a loop.
240
+
218
241
  #### Batch Execution
219
242
 
220
243
  Execute the same SQL with multiple parameter sets in a single call. Automatically wraps in a transaction.
@@ -254,7 +277,8 @@ All methods mirror `Database` but without the `sql` parameter (it's bound at pre
254
277
  | `execute(params?)` | `executeSync(params?)` | Execute DML statement |
255
278
  | `query(params?)` | `querySync(params?)` | Query rows as objects |
256
279
  | `queryOne(params?)` | `queryOneSync(params?)` | Query single row |
257
- | `queryRaw(params?)` | `queryRawSync(params?)` | Query in columnar format |
280
+ | `queryRaw(params?)` | `queryRawSync(params?)` | Query as arrays of values |
281
+ | `iterate(params?)` | — | Cursor over rows, `for await` or `for...of` |
258
282
  | | `executeBatchSync(paramsArray)` | Execute with multiple param sets |
259
283
  | | `finalize()` | Release the prepared statement |
260
284
 
@@ -303,7 +327,8 @@ console.log(result.changes); // 3
303
327
  | `execute(sql, params?)` | `executeSync(sql, params?)` | Execute DML statement |
304
328
  | `query(sql, params?)` | `querySync(sql, params?)` | Query rows as objects |
305
329
  | `queryOne(sql, params?)` | `queryOneSync(sql, params?)` | Query single row |
306
- | `queryRaw(sql, params?)` | `queryRawSync(sql, params?)` | Query in columnar format |
330
+ | `queryRaw(sql, params?)` | `queryRawSync(sql, params?)` | Query as arrays of values |
331
+ | `iterate(sql, params?)` | — | Cursor over rows, closed by commit/rollback |
307
332
  | `commit()` | `commitSync()` | Commit the transaction |
308
333
  | `rollback()` | `rollbackSync()` | Rollback the transaction |
309
334
  | | `executeBatchSync(sql, paramsArray)` | Execute with multiple param sets |
@@ -377,6 +402,8 @@ db.querySync(
377
402
  );
378
403
  ```
379
404
 
405
+ Synchronous ad hoc statements pass named parameters to the engine as they are: the SQL is not rewritten, a `:name` inside a string literal is left alone, a name used twice is bound once, keys the SQL does not mention are ignored and a name the object lacks binds `NULL`. A prepared statement, and the asynchronous ad hoc methods, resolve `:name` placeholders in the driver and bind by position, which is the engine's fastest path.
406
+
380
407
  ### Error Handling
381
408
 
382
409
  All methods throw on errors (invalid SQL, constraint violations, etc.):
package/index.d.ts CHANGED
@@ -88,7 +88,10 @@ export declare class Database {
88
88
  * Auto-wraps in a transaction: begin, execute all, commit.
89
89
  * Returns { changes: total_rows_affected }.
90
90
  */
91
- executeBatchSync(sql: string, paramsArray: any[][]): RunResult;
91
+ executeBatchSync(
92
+ sql: string,
93
+ paramsArray: any[][] | Record<string, any>[],
94
+ ): RunResult;
92
95
  /** Create a prepared statement (synchronous). */
93
96
  prepare(sql: string): PreparedStatement;
94
97
  /** Begin a transaction. Returns Promise<Transaction>. */
@@ -97,11 +100,33 @@ export declare class Database {
97
100
  beginSync(): Transaction;
98
101
  /** Close the database. Returns Promise<void>. */
99
102
  close(): Promise<void>;
103
+ /**
104
+ * Open a cursor over the query's rows. Iterate it with `for...of` or
105
+ * `for await...of`; rows are fetched in chunks as the loop advances and
106
+ * the cursor closes itself when the loop ends, breaks or throws.
107
+ */
108
+ iterate(sql: string, params?: any[] | Record<string, any>): RowCursor;
100
109
  /** Close the database synchronously. */
101
110
  closeSync(): void;
102
111
  }
103
112
  export type JsDatabase = Database;
104
113
 
114
+ /**
115
+ * A cursor over an open result set, from `iterate()`. Iterable both
116
+ * synchronously and with `for await`. Rows cross the native boundary in
117
+ * chunks of 32 to 256 rows.
118
+ */
119
+ export declare class RowCursor
120
+ implements Iterable<Record<string, any>>, AsyncIterable<Record<string, any>>
121
+ {
122
+ [Symbol.iterator](): Iterator<Record<string, any>>;
123
+ [Symbol.asyncIterator](): AsyncIterator<Record<string, any>>;
124
+ /** End the iteration early and release the result set. */
125
+ close(): void;
126
+ /** True once the cursor was closed or its result set was exhausted. */
127
+ get closed(): boolean;
128
+ }
129
+
105
130
  export declare class PreparedStatement {
106
131
  /** Execute the statement (DML). Returns Promise<{ changes: number }>. */
107
132
  execute(params?: any[] | Record<string, any>): Promise<RunResult>;
@@ -132,7 +157,9 @@ export declare class PreparedStatement {
132
157
  * Execute the prepared SQL with multiple param sets in a single call.
133
158
  * Returns { changes: total_rows_affected }.
134
159
  */
135
- executeBatchSync(paramsArray: any[][]): RunResult;
160
+ executeBatchSync(paramsArray: any[][] | Record<string, any>[]): RunResult;
161
+ /** Open a cursor over the statement's rows; see `Database.iterate`. */
162
+ iterate(params?: any[] | Record<string, any>): RowCursor;
136
163
  /** Get the SQL text of this prepared statement. */
137
164
  get sql(): string;
138
165
  /** Release the prepared statement. */
@@ -188,7 +215,15 @@ export declare class Transaction {
188
215
  * Execute the same SQL with multiple param sets in a single call.
189
216
  * Returns { changes: total_rows_affected }.
190
217
  */
191
- executeBatchSync(sql: string, paramsArray: any[][]): RunResult;
218
+ executeBatchSync(
219
+ sql: string,
220
+ paramsArray: any[][] | Record<string, any>[],
221
+ ): RunResult;
222
+ /**
223
+ * Open a cursor over the query's rows inside the transaction; see
224
+ * `Database.iterate`. Commit and rollback close open cursors.
225
+ */
226
+ iterate(sql: string, params?: any[] | Record<string, any>): RowCursor;
192
227
  /** Rollback the transaction synchronously. */
193
228
  rollbackSync(): void;
194
229
  }
package/index.js CHANGED
@@ -3,11 +3,13 @@
3
3
  const { Database } = require("./lib/database");
4
4
  const { Transaction } = require("./lib/transaction");
5
5
  const { PreparedStatement } = require("./lib/statement");
6
+ const { RowCursor } = require("./lib/iterator");
6
7
 
7
- module.exports = { Database, Transaction, PreparedStatement };
8
+ module.exports = { Database, Transaction, PreparedStatement, RowCursor };
8
9
  module.exports.Database = Database;
9
10
  module.exports.PreparedStatement = PreparedStatement;
10
11
  module.exports.Transaction = Transaction;
12
+ module.exports.RowCursor = RowCursor;
11
13
  // Legacy aliases
12
14
  module.exports.JsDatabase = Database;
13
15
  module.exports.JsPreparedStatement = PreparedStatement;
package/lib/database.js CHANGED
@@ -1,9 +1,15 @@
1
1
  "use strict";
2
2
 
3
- const { loadStoolap, processParams, rewriteNamedParams } = require("./ffi");
3
+ const {
4
+ loadStoolap,
5
+ processParams,
6
+ rewriteNamedParams,
7
+ splitNamed,
8
+ } = require("./ffi");
4
9
  const { parseRows, parseRaw } = require("./parse");
5
10
  const { Transaction } = require("./transaction");
6
11
  const { PreparedStatement, encodeBatch } = require("./statement");
12
+ const { RowCursor } = require("./iterator");
7
13
 
8
14
  function normalizeDsn(dbPath) {
9
15
  let dsn = dbPath || ":memory:";
@@ -25,6 +31,7 @@ class Database {
25
31
  #closeWaiter = null;
26
32
  #closingPromise = null;
27
33
  #children = new Set();
34
+ #cursors = new Set(); /* WeakRef<RowCursor> */
28
35
 
29
36
  constructor(ptr, stoolap) {
30
37
  this.#ptr = ptr;
@@ -39,13 +46,33 @@ class Database {
39
46
  this.#children.delete(child);
40
47
  }
41
48
 
49
+ _trackCursor(ref) {
50
+ this.#cursors.add(ref);
51
+ }
52
+
53
+ _untrackCursor(ref) {
54
+ this.#cursors.delete(ref);
55
+ }
56
+
42
57
  #closeChildren() {
58
+ for (const ref of this.#cursors) {
59
+ const cursor = ref.deref();
60
+ if (cursor) cursor._invalidate();
61
+ }
62
+ this.#cursors.clear();
43
63
  for (const child of this.#children) {
44
64
  child._invalidate();
45
65
  }
46
66
  this.#children.clear();
47
67
  }
48
68
 
69
+ #childrenPending() {
70
+ for (const child of this.#children) {
71
+ if (child._hasPending && child._hasPending()) return true;
72
+ }
73
+ return false;
74
+ }
75
+
49
76
  static async open(dbPath) {
50
77
  const stoolap = loadStoolap();
51
78
  const ptr = await stoolap.dbOpenAsync(normalizeDsn(dbPath));
@@ -123,6 +150,16 @@ class Database {
123
150
  );
124
151
  }
125
152
 
153
+
154
+ _rowsChunk(rowsPtr, maxRows, buf) {
155
+ this.#checkOpen();
156
+ return this.#stoolap.rowsChunk(rowsPtr, maxRows, buf);
157
+ }
158
+
159
+ _rowsChunkAsync(rowsPtr, maxRows) {
160
+ return this.#runAsync(() => this.#stoolap.rowsChunkAsync(rowsPtr, maxRows));
161
+ }
162
+
126
163
  _executeBatchBufferSync(sql, batchBuf) {
127
164
  this.#checkOpen();
128
165
  return this.#stoolap.dbExecBatchBuf(this.#ptr, sql, batchBuf);
@@ -152,19 +189,13 @@ class Database {
152
189
 
153
190
  executeSync(sql, params) {
154
191
  this.#checkOpen();
155
- if (!params) return this.#stoolap.dbExec(this.#ptr, sql);
156
- if (Array.isArray(params)) {
157
- if (params.length === 0) return this.#stoolap.dbExec(this.#ptr, sql);
158
- return this.#stoolap.dbExec(this.#ptr, sql, params);
192
+ if (params && !Array.isArray(params)) {
193
+ const [names, values] = splitNamed(params);
194
+ return this.#stoolap.dbExecNamed(this.#ptr, sql, names, values);
159
195
  }
160
- const p = processParams(sql, params);
161
- return p.values
162
- ? this.#stoolap.dbExec(this.#ptr, p.sql, p.values)
163
- : this.#stoolap.dbExec(this.#ptr, sql);
196
+ return this.#stoolap.dbExec(this.#ptr, sql, params);
164
197
  }
165
198
 
166
- // -- Exec (DDL) --
167
-
168
199
  async exec(sql) {
169
200
  await this._execAsync(sql);
170
201
  }
@@ -194,27 +225,21 @@ class Database {
194
225
  return parseRows(buf);
195
226
  }
196
227
 
228
+ /* One FFI call. mode: 0 = rows, 1 = first row, 2 = raw buffer */
229
+ #queryAll(sql, params, mode) {
230
+ if (params && !Array.isArray(params)) {
231
+ const [names, values] = splitNamed(params);
232
+ return this.#stoolap.dbQueryNamedAll(this.#ptr, null, sql, names, values, mode);
233
+ }
234
+ return this.#stoolap.dbQueryAll(this.#ptr, null, sql, params, mode);
235
+ }
236
+
197
237
  querySync(sql, params) {
198
238
  this.#checkOpen();
199
- let result;
200
- if (!params) {
201
- result = this.#stoolap.dbQuery(this.#ptr, sql);
202
- } else if (Array.isArray(params)) {
203
- result =
204
- params.length === 0
205
- ? this.#stoolap.dbQuery(this.#ptr, sql)
206
- : this.#stoolap.dbQuery(this.#ptr, sql, params);
207
- } else {
208
- const p = processParams(sql, params);
209
- result = p.values
210
- ? this.#stoolap.dbQuery(this.#ptr, p.sql, p.values)
211
- : this.#stoolap.dbQuery(this.#ptr, sql);
212
- }
239
+ const result = this.#queryAll(sql, params, 0);
213
240
  return Array.isArray(result) ? result : parseRows(result);
214
241
  }
215
242
 
216
- // -- QueryOne --
217
-
218
243
  async queryOne(sql, params) {
219
244
  const rows = await this.query(sql, params);
220
245
  return rows.length === 0 ? null : rows[0];
@@ -222,19 +247,14 @@ class Database {
222
247
 
223
248
  queryOneSync(sql, params) {
224
249
  this.#checkOpen();
225
- if (!params) return this.#stoolap.dbQueryOne(this.#ptr, sql);
226
- if (Array.isArray(params)) {
227
- if (params.length === 0) return this.#stoolap.dbQueryOne(this.#ptr, sql);
228
- return this.#stoolap.dbQueryOne(this.#ptr, sql, params);
250
+ const result = this.#queryAll(sql, params, 1);
251
+ if (result instanceof ArrayBuffer) {
252
+ const rows = parseRows(result);
253
+ return rows.length === 0 ? null : rows[0];
229
254
  }
230
- const p = processParams(sql, params);
231
- return p.values
232
- ? this.#stoolap.dbQueryOne(this.#ptr, p.sql, p.values)
233
- : this.#stoolap.dbQueryOne(this.#ptr, sql);
255
+ return result;
234
256
  }
235
257
 
236
- // -- QueryRaw --
237
-
238
258
  async queryRaw(sql, params) {
239
259
  let buf;
240
260
  if (!params) {
@@ -255,25 +275,25 @@ class Database {
255
275
 
256
276
  queryRawSync(sql, params) {
257
277
  this.#checkOpen();
258
- let buf;
259
- if (!params) {
260
- buf = this.#stoolap.dbQueryBuf(this.#ptr, sql);
261
- } else if (Array.isArray(params)) {
262
- buf =
263
- params.length === 0
264
- ? this.#stoolap.dbQueryBuf(this.#ptr, sql)
265
- : this.#stoolap.dbQueryBuf(this.#ptr, sql, params);
278
+ return parseRaw(this.#queryAll(sql, params, 2));
279
+ }
280
+
281
+ /**
282
+ * Open a cursor over the query's rows. Iterate it with `for...of` or
283
+ * `for await...of`; rows are fetched in chunks as the loop advances.
284
+ */
285
+ iterate(sql, params) {
286
+ this.#checkOpen();
287
+ let ptr;
288
+ if (params && !Array.isArray(params)) {
289
+ const [names, values] = splitNamed(params);
290
+ ptr = this.#stoolap.rowsOpenNamed(this.#ptr, null, sql, names, values);
266
291
  } else {
267
- const p = processParams(sql, params);
268
- buf = p.values
269
- ? this.#stoolap.dbQueryBuf(this.#ptr, p.sql, p.values)
270
- : this.#stoolap.dbQueryBuf(this.#ptr, sql);
292
+ ptr = this.#stoolap.rowsOpen(this.#ptr, null, sql, params);
271
293
  }
272
- return parseRaw(buf);
294
+ return new RowCursor(this.#stoolap, ptr, this);
273
295
  }
274
296
 
275
- // -- Batch --
276
-
277
297
  executeBatchSync(sql, paramsArray) {
278
298
  this.#checkOpen();
279
299
  if (paramsArray.length > 0 && Array.isArray(paramsArray[0])) {
@@ -298,6 +318,7 @@ class Database {
298
318
 
299
319
  prepare(sql) {
300
320
  this.#checkOpen();
321
+ if (typeof sql !== "string") throw new TypeError("sql must be a string");
301
322
  const prepared = rewriteNamedParams(sql);
302
323
  const [stmtPtr, queryOneIntBound] = this.#stoolap.dbPrepare(
303
324
  this.#ptr,
@@ -309,7 +330,6 @@ class Database {
309
330
  queryOneIntBound,
310
331
  this,
311
332
  sql,
312
- prepared.sql,
313
333
  prepared.names,
314
334
  );
315
335
  this._trackChild(stmt);
@@ -338,30 +358,35 @@ class Database {
338
358
  async close() {
339
359
  if (this.#closingPromise) return this.#closingPromise;
340
360
  if (!this.#ptr) return;
341
-
342
- this.#closeChildren();
343
361
  const ptr = this.#ptr;
344
362
  this.#ptr = null;
345
-
346
- const wait =
363
+ /* let queued work on the handle, its statements and its transactions
364
+ * land before anything is released */
365
+ const settling = [];
366
+ for (const child of this.#children) {
367
+ if (child._settle) settling.push(child._settle());
368
+ }
369
+ const wait = Promise.all(settling).then(() =>
347
370
  this.#pendingAsync === 0
348
- ? Promise.resolve()
371
+ ? undefined
349
372
  : new Promise((resolve) => {
350
373
  this.#closeWaiter = resolve;
351
- });
352
-
374
+ }),
375
+ );
353
376
  this.#closingPromise = wait
354
- .then(() => this.#stoolap.dbCloseAsync(ptr))
377
+ .then(() => {
378
+ this.#closeChildren();
379
+ return this.#stoolap.dbCloseAsync(ptr);
380
+ })
355
381
  .finally(() => {
356
382
  this.#closingPromise = null;
357
383
  this.#closeWaiter = null;
358
384
  });
359
-
360
385
  return this.#closingPromise;
361
386
  }
362
387
 
363
388
  closeSync() {
364
- if (this.#closingPromise || this.#pendingAsync > 0) {
389
+ if (this.#closingPromise || this.#pendingAsync > 0 || this.#childrenPending()) {
365
390
  throw new Error("Database has pending async operations");
366
391
  }
367
392
  if (this.#ptr) {
package/lib/ffi.js CHANGED
@@ -53,6 +53,7 @@ function loadStoolap() {
53
53
 
54
54
  _stoolap = require("../build/Release/stoolap.node");
55
55
  _stoolap.loadLibrary(findLibraryPath());
56
+ _stoolap.setRowFactoryMaker(require("./parse").makeRowFactory);
56
57
  return _stoolap;
57
58
  }
58
59
 
@@ -161,6 +162,9 @@ function processParams(sql, params) {
161
162
  return { sql: rewritten, values };
162
163
  }
163
164
 
165
+ /* Rewrite :name placeholders to $n for a prepared statement. The engine binds
166
+ * prepared statements faster by position than by name, so a statement keeps
167
+ * its parameter names and binds an object's values positionally. */
164
168
  function rewriteNamedParams(sql) {
165
169
  const seen = new Map();
166
170
  const names = [];
@@ -171,13 +175,21 @@ function rewriteNamedParams(sql) {
171
175
  }
172
176
  return `$${seen.get(name)}`;
173
177
  });
174
-
175
178
  return names.length === 0 ? { sql, names: null } : { sql: rewritten, names };
176
179
  }
177
180
 
181
+ /* Split a named-parameter object into parallel name and value arrays. */
182
+ function splitNamed(params) {
183
+ const names = Object.keys(params);
184
+ const values = new Array(names.length);
185
+ for (let i = 0; i < names.length; i++) values[i] = params[names[i]];
186
+ return [names, values];
187
+ }
188
+
178
189
  module.exports = {
179
190
  loadStoolap,
180
191
  processParams,
181
192
  rewriteNamedParams,
193
+ splitNamed,
182
194
  findLibraryPath,
183
195
  };
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+
3
+ const { parseRows, parseHeader, makeRowFactory, parseRowsCached } = require("./parse");
4
+
5
+ const FIRST_CHUNK = 32;
6
+ const MAX_CHUNK = 256;
7
+
8
+ /**
9
+ * A row cursor over an open result set. Rows cross the FFI boundary in
10
+ * packed chunks (32 rows, doubling to 256), so a scan costs one call per
11
+ * chunk instead of one per row. Iterate it synchronously or with
12
+ * `for await`; the cursor closes itself when iteration ends, breaks or
13
+ * throws, and `close()` ends it early.
14
+ */
15
+ class RowCursor {
16
+ #stoolap = null;
17
+ #ptr = null;
18
+ #owner = null;
19
+ #chunk = FIRST_CHUNK;
20
+ #buf = null;
21
+ #meta = null;
22
+ #factory = null;
23
+ #pending = false;
24
+ #closeWanted = false;
25
+ #ref = null;
26
+
27
+ constructor(stoolap, ptr, owner) {
28
+ this.#stoolap = stoolap;
29
+ this.#ptr = ptr;
30
+ this.#owner = owner;
31
+ /* the owner holds only a weak reference: an abandoned cursor can be
32
+ * collected, and the native handle's finalizer closes it */
33
+ this.#ref = new WeakRef(this);
34
+ owner._trackCursor(this.#ref);
35
+ }
36
+
37
+ #release() {
38
+ if (this.#ptr) {
39
+ this.#stoolap.rowsClose(this.#ptr);
40
+ this.#ptr = null;
41
+ }
42
+ this.#closeWanted = false;
43
+ this.#owner._untrackCursor(this.#ref);
44
+ }
45
+
46
+ /** Called by the owner when it closes. A chunk in flight closes the cursor when it lands. */
47
+ _invalidate() {
48
+ if (this.#pending) this.#closeWanted = true;
49
+ else this.#release();
50
+ }
51
+
52
+ close() {
53
+ if (this.#pending) this.#closeWanted = true;
54
+ else this.#release();
55
+ }
56
+
57
+ get closed() {
58
+ return this.#ptr === null;
59
+ }
60
+
61
+ #parse(buf) {
62
+ if (!this.#meta) {
63
+ this.#meta = parseHeader(buf);
64
+ this.#factory = makeRowFactory(this.#meta.colNames);
65
+ }
66
+ const rows = this.#factory
67
+ ? parseRowsCached(buf, this.#meta.headerOff, this.#factory, this.#meta.colCount)
68
+ : parseRows(buf);
69
+ if (this.#chunk < MAX_CHUNK) this.#chunk *= 2;
70
+ return rows;
71
+ }
72
+
73
+ *[Symbol.iterator]() {
74
+ try {
75
+ while (this.#ptr) {
76
+ const buf = this.#owner._rowsChunk(this.#ptr, this.#chunk, this.#buf);
77
+ if (!buf) break;
78
+ this.#buf = buf;
79
+ const rows = this.#parse(buf);
80
+ for (let i = 0; i < rows.length; i++) yield rows[i];
81
+ }
82
+ } finally {
83
+ this.close();
84
+ }
85
+ }
86
+
87
+ async *[Symbol.asyncIterator]() {
88
+ try {
89
+ while (this.#ptr) {
90
+ this.#pending = true;
91
+ let buf;
92
+ try {
93
+ buf = await this.#owner._rowsChunkAsync(this.#ptr, this.#chunk);
94
+ } finally {
95
+ this.#pending = false;
96
+ }
97
+ if (this.#closeWanted) {
98
+ this.#release();
99
+ break;
100
+ }
101
+ if (!buf) break;
102
+ const rows = this.#parse(buf);
103
+ for (let i = 0; i < rows.length; i++) yield rows[i];
104
+ }
105
+ } finally {
106
+ this.close();
107
+ }
108
+ }
109
+ }
110
+
111
+ module.exports = { RowCursor };