edinburgh 0.3.0 → 0.4.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/src/datapack.ts CHANGED
@@ -8,12 +8,15 @@ const BASE64_CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuv
8
8
  const BASE64_LOOKUP = new Uint8Array(128).fill(255); // Use 255 as invalid marker;
9
9
  for (let i = 0; i < BASE64_CHARS.length; ++i) BASE64_LOOKUP[BASE64_CHARS.charCodeAt(i)] = i;
10
10
 
11
- const COLORS = ['\x1b[32m', '\x1b[33m', '\x1b[34m', '\x1b[35m']; // green, yellow, blue, magenta
12
- const RESET_COLOR = '\x1b[0m';
13
- const ERROR_COLOR = '\x1b[31m'; // red
11
+ const USE_COLORS = typeof process !== 'undefined' && process?.stdout?.isTTY;
12
+
13
+ const COLORS = USE_COLORS ? ['\x1b[32m', '\x1b[33m', '\x1b[34m', '\x1b[35m'] : ['']; // green, yellow, blue, magenta
14
+ const RESET_COLOR = USE_COLORS ? '\x1b[0m' : '';
15
+ const ERROR_COLOR = USE_COLORS ? '\x1b[31m' : ''; // red
14
16
 
15
17
  let toStringTermCount = 0;
16
- let useExtendedLogging = !!process.env.DATAPACK_EXTENDED_LOGGING;
18
+ let useExtendedLogging = typeof process !== 'undefined' ? !!process.env?.DATAPACK_EXTENDED_LOGGING : false;
19
+
17
20
 
18
21
  /**
19
22
  * A byte buffer for efficient reading and writing of primitive values and bit sequences.
@@ -23,7 +26,7 @@ let useExtendedLogging = !!process.env.DATAPACK_EXTENDED_LOGGING;
23
26
  * It supports both reading and writing operations with automatic buffer management.
24
27
  */
25
28
 
26
- export class DataPack {
29
+ export default class DataPack {
27
30
  private buffer: Uint8Array;
28
31
  private dataView?: DataView;
29
32
 
@@ -41,7 +44,7 @@ export class DataPack {
41
44
  * Create a new DataPack instance.
42
45
  * @param data - Optional initial data as Uint8Array or buffer size as number.
43
46
  */
44
- constructor(data: Uint8Array | number = 3900) {
47
+ constructor(data: Uint8Array | number = 1900) {
45
48
  if (data instanceof Uint8Array) {
46
49
  this.buffer = data;
47
50
  this.writePos = data.length;
@@ -123,6 +126,8 @@ export class DataPack {
123
126
  * 9: EOD
124
127
  * 10: identifier (6 byte positive int follows, represented as a base64 string of length 8)
125
128
  * 11: null-terminated string
129
+ * 12: Date/Time (varint with whole seconds since epoch follows)
130
+ * 13: custom type, followed by name string and data value
126
131
  * 5: short string (length in lower bits, 0-31)
127
132
  * 6: string (byte count of length in lower bits)
128
133
  * 7: blob (byte count of length in lower bits)
@@ -205,21 +210,21 @@ export class DataPack {
205
210
  this.buffer[this.writePos++] = (4 << 5) | 12;
206
211
  // Write a varint -- whole seconds should be plenty of resolution
207
212
  this.write(Math.floor(data.getTime()/1000));
208
- } else if (typeof data === 'object' && data.constructor === Object) {
209
- // Type 4, subtype 6: object start
210
- this.buffer[this.writePos++] = (4 << 5) | 6;
211
- for (const [key, value] of Object.entries(data)) {
212
- this.write(key);
213
- this.write(value);
213
+ } else if (typeof data === 'object') {
214
+ if (data instanceof CustomData) {
215
+ this.writeCustom(data.name, data.data);
216
+ } else if (typeof data.toDataPack === 'function') {
217
+ data.toDataPack(this);
218
+ } else {
219
+ this.writeAsObject(data);
214
220
  }
215
- this.buffer[this.writePos++] = (4 << 5) | 9; // EOD
216
221
  } else {
217
222
  throw new Error(`Unsupported data type: ${typeof data}`);
218
223
  }
219
224
  return this;
220
225
  }
221
226
 
222
- read(): any {
227
+ read(customConverters?: {[name: string]: ((data: any) => any)} | undefined): any {
223
228
  if (this.readPos > this.writePos) {
224
229
  throw new Error('Not enough data');
225
230
  }
@@ -251,7 +256,7 @@ export class DataPack {
251
256
  }
252
257
 
253
258
  case 4: {
254
- // Special values and container starts
259
+ // Floats, special values and collections
255
260
  switch (subtype) {
256
261
  case 0: {
257
262
  // float64
@@ -269,19 +274,19 @@ export class DataPack {
269
274
  // Array start
270
275
  const result = [];
271
276
  while (true) {
272
- const nextValue = this.read();
277
+ const nextValue = this.read(customConverters);
273
278
  if (nextValue === EOD) break;
274
279
  result.push(nextValue);
275
280
  }
276
281
  return result;
277
282
  }
278
283
  case 6: {
279
- // Object start
280
- const result: any = {};
284
+ // Plain object or custom class instance
285
+ let result: any = {};
281
286
  while (true) {
282
- const key = this.read();
287
+ const key = this.read(customConverters);
283
288
  if (key === EOD) break;
284
- const value = this.read();
289
+ const value = this.read(customConverters);
285
290
  result[key] = value;
286
291
  }
287
292
  return result;
@@ -290,9 +295,9 @@ export class DataPack {
290
295
  // Map start
291
296
  const result = new Map();
292
297
  while (true) {
293
- const key = this.read();
298
+ const key = this.read(customConverters);
294
299
  if (key === EOD) break;
295
- const value = this.read();
300
+ const value = this.read(customConverters);
296
301
  result.set(key, value);
297
302
  }
298
303
  return result;
@@ -301,7 +306,7 @@ export class DataPack {
301
306
  // Set start
302
307
  const result = new Set();
303
308
  while (true) {
304
- const value = this.read();
309
+ const value = this.read(customConverters);
305
310
  if (value === EOD) break;
306
311
  result.add(value);
307
312
  }
@@ -310,7 +315,7 @@ export class DataPack {
310
315
  case 9: return EOD;
311
316
  case 10: {
312
317
  // Identifier (6 byte positive int follows, represented as a base64 string of length 8)
313
- --this.readPos;
318
+ --this.readPos; // readIdentifier() will read the header byte again
314
319
  return this.readIdentifier();
315
320
  }
316
321
  case 11: {
@@ -330,6 +335,12 @@ export class DataPack {
330
335
  const seconds = this.readPositiveInt();
331
336
  return new Date(seconds * 1000);
332
337
  }
338
+ case 13: {
339
+ // Custom type, followed by name string and data value
340
+ const name = this.readString();
341
+ const data = this.read();
342
+ return (customConverters && name in customConverters) ? customConverters[name](data) : new CustomData(name, data);
343
+ }
333
344
  default: throw new Error(`Unknown type 4 subtype: ${subtype}`);
334
345
  }
335
346
  }
@@ -430,19 +441,24 @@ export class DataPack {
430
441
  return result;
431
442
  }
432
443
 
433
- writeIdentifier(id: string): DataPack {
434
- if (id.length !== 8) {
435
- throw new Error(`Identifier must be exactly 8 characters, got ${id.length}`);
436
- }
437
-
438
- // Convert base64 string to 48-bit number
439
- let value, num = 0;
440
- for (let i = 0; i < 8; i++) {
441
- const char = id.charCodeAt(i);
442
- if (char > 127 || (value = BASE64_LOOKUP[char]) === 255) {
443
- throw new Error(`Invalid base64 character: ${id[i]}`);
444
+ writeIdentifier(id: string | number): DataPack {
445
+ let num: number = 0;
446
+ if (typeof id === 'number') {
447
+ num = id;
448
+ } else {
449
+ if (id.length !== 8) {
450
+ throw new Error(`Identifier must be exactly 8 characters, got ${id.length}`);
451
+ }
452
+
453
+ // Convert base64 string to 48-bit number
454
+ let value;
455
+ for (let i = 0; i < 8; i++) {
456
+ const char = id.charCodeAt(i);
457
+ if (char > 127 || (value = BASE64_LOOKUP[char]) === 255) {
458
+ throw new Error(`Invalid base64 character: ${id[i]}`);
459
+ }
460
+ num = num * 64 + value;
444
461
  }
445
- num = num * 64 + value;
446
462
  }
447
463
 
448
464
  // Write type 4, subtype 10 header
@@ -451,13 +467,28 @@ export class DataPack {
451
467
 
452
468
  // Write the 6-byte number in big-endian format
453
469
  for (let i = 5; i >= 0; i--) {
454
- this.buffer[this.writePos++] = Math.floor(num / Math.pow(256, i)) & 0xFF;
470
+ this.buffer[this.writePos + i] = num % 256;
471
+ num = Math.floor(num / 256);
455
472
  }
473
+ this.writePos += 6;
456
474
 
457
475
  return this;
458
476
  }
459
477
 
460
478
  readIdentifier(): string {
479
+ let num = this.readIdentifierNumber();
480
+
481
+ // Convert 48-bit number back to 8-character base64 string
482
+ let id = '';
483
+ for (let i = 0; i < 8; i++) {
484
+ id = BASE64_CHARS[num % 64] + id;
485
+ num = Math.floor(num / 64);
486
+ }
487
+
488
+ return id;
489
+ }
490
+
491
+ readIdentifierNumber(): number {
461
492
  // Read the 6-byte number in big-endian format
462
493
  if (this.readPos + 7 > this.writePos) this.notEnoughData('identifier');
463
494
 
@@ -470,15 +501,7 @@ export class DataPack {
470
501
  for (let i = 0; i < 6; i++) {
471
502
  num = (num * 256) + this.buffer[this.readPos++];
472
503
  }
473
-
474
- // Convert 48-bit number back to 8-character base64 string
475
- let id = '';
476
- for (let i = 0; i < 8; i++) {
477
- id = BASE64_CHARS[num % 64] + id;
478
- num = Math.floor(num / 64);
479
- }
480
-
481
- return id;
504
+ return num;
482
505
  }
483
506
 
484
507
  /**
@@ -503,6 +526,10 @@ export class DataPack {
503
526
  return copyBuffer ? this.buffer.slice(startPos, endPos) : this.buffer.subarray(startPos, endPos);
504
527
  }
505
528
 
529
+ toBuffer(): ArrayBuffer {
530
+ return this.buffer.buffer.slice(0, this.writePos) as ArrayBuffer;
531
+ }
532
+
506
533
  clone(copyBuffer: boolean, readPos: number = 0, writePos: number = this.writePos): DataPack {
507
534
  if (copyBuffer) {
508
535
  return new DataPack(this.buffer.slice(readPos, writePos));
@@ -514,6 +541,23 @@ export class DataPack {
514
541
  }
515
542
  }
516
543
 
544
+ writeCustom(name: string, data: any) {
545
+ this.buffer[this.writePos++] = (4 << 5) | 13;
546
+ this.write(name);
547
+ this.write(data);
548
+ }
549
+
550
+ writeAsObject(obj: Record<string, any>) {
551
+ // Type 4, subtype 6: object start
552
+ this.buffer[this.writePos++] = (4 << 5) | 6;
553
+ for (const key of Object.keys(obj)) {
554
+ this.write(key);
555
+ this.write(obj[key]);
556
+ }
557
+ this.buffer[this.writePos++] = (4 << 5) | 9; // EOD
558
+ return this;
559
+ }
560
+
517
561
  /**
518
562
  * Write a collection (array, set, object, or map) using a callback to add items/fields.
519
563
  * @param type 'array' | 'set' | 'object' | 'map'
@@ -640,6 +684,22 @@ export class DataPack {
640
684
  }
641
685
  return id;
642
686
  }
687
+
688
+ static createBuffer(...args: any) {
689
+ const pack = new DataPack();
690
+ for (const arg of args) {
691
+ pack.write(arg);
692
+ }
693
+ return pack.toBuffer();
694
+ }
695
+
696
+ static createUint8Array(...args: any) {
697
+ const pack = new DataPack();
698
+ for (const arg of args) {
699
+ pack.write(arg);
700
+ }
701
+ return pack.toUint8Array(false);
702
+ }
643
703
  }
644
704
 
645
705
  function toText(v: any): string {
@@ -653,3 +713,14 @@ function toText(v: any): string {
653
713
  }
654
714
  return JSON.stringify(v);
655
715
  }
716
+
717
+
718
+ class CustomData {
719
+ constructor(public name: string, public data: any) {}
720
+ }
721
+
722
+ export default interface DataPack {
723
+ CustomData: typeof CustomData;
724
+ }
725
+
726
+ DataPack.prototype.CustomData = CustomData;
package/src/edinburgh.ts CHANGED
@@ -1,6 +1,10 @@
1
- import * as olmdb from "olmdb";
2
- import { Model, INSTANCES_SYMBOL, resetModelCaches } from "./models.js";
3
- import { INSTANCES_BY_PK_SYMBOL } from "./indexes.js";
1
+ import * as lowlevel from "olmdb/lowlevel";
2
+ import { init as olmdbInit, DatabaseError } from "olmdb/lowlevel";
3
+ import { modelRegistry, txnStorage, currentTxn } from "./models.js";
4
+
5
+ let initNeeded = false;
6
+ export function scheduleInit() { initNeeded = true; }
7
+
4
8
 
5
9
  // Re-export public API from models
6
10
  export {
@@ -9,7 +13,7 @@ export {
9
13
  field,
10
14
  } from "./models.js";
11
15
 
12
- import type { ChangedModel } from "./models.js";
16
+ import type { Transaction, Change, Model } from "./models.js";
13
17
 
14
18
  // Re-export public API from types (only factory functions and instances)
15
19
  export {
@@ -37,14 +41,40 @@ export {
37
41
  dump,
38
42
  } from "./indexes.js";
39
43
 
40
- export {
41
- setLogLevel
42
- } from "./utils.js";
44
+ export { BaseIndex, UniqueIndex, PrimaryIndex, SecondaryIndex } from './indexes.js';
45
+
46
+ export { type Change } from './models.js';
47
+ export type { Transaction } from './models.js';
48
+ export { DatabaseError } from "olmdb/lowlevel";
49
+ export { runMigration } from './migrate.js';
50
+ export type { MigrationOptions, MigrationResult } from './migrate.js';
51
+
52
+ let olmdbReady = false;
53
+ let maxRetryCount = 6;
54
+
55
+ /**
56
+ * Initialize the database with the specified directory path.
57
+ * This function may be called multiple times with the same parameters. If it is not called before the first transact(),
58
+ * the database will be automatically initialized with the default directory.
59
+ *
60
+ * @example
61
+ * ```typescript
62
+ * init("./my-database");
63
+ * ```
64
+ */
65
+ export function init(dbDir: string): void {
66
+ olmdbReady = true;
67
+ olmdbInit(dbDir);
68
+ }
69
+
70
+ let pendingInit: Promise<void> | undefined;
43
71
 
44
- export { BaseIndex, UniqueIndex, PrimaryIndex } from './indexes.js';
45
72
 
46
- // Re-export from OLMDB
47
- export { init, onCommit, onRevert, getTransactionData, setTransactionData, DatabaseError } from "olmdb";
73
+ const STALE_INSTANCE_DESCRIPTOR = {
74
+ get() {
75
+ throw new DatabaseError("The transaction for this model instance has ended", "STALE_INSTANCE");
76
+ },
77
+ };
48
78
 
49
79
  /**
50
80
  * Executes a function within a database transaction context.
@@ -54,15 +84,13 @@ export { init, onCommit, onRevert, getTransactionData, setTransactionData, Datab
54
84
  *
55
85
  * Transactions have a consistent view of the database, and changes made within a transaction are
56
86
  * isolated from other transactions until they are committed. In case a commit clashes with changes
57
- * made by another transaction, the transaction function will automatically be re-executed up to 10
87
+ * made by another transaction, the transaction function will automatically be re-executed up to 6
58
88
  * times.
59
89
  *
60
90
  * @template T - The return type of the transaction function.
61
- * @param fn - The function to execute within the transaction context.
91
+ * @param fn - The function to execute within the transaction context. Receives a Transaction instance.
62
92
  * @returns A promise that resolves with the function's return value.
63
- * @throws {TypeError} If nested transactions are attempted.
64
93
  * @throws {DatabaseError} With code "RACING_TRANSACTION" if the transaction fails after retries due to conflicts.
65
- * @throws {DatabaseError} With code "TRANSACTION_FAILED" if the transaction fails for other reasons.
66
94
  * @throws {DatabaseError} With code "TXN_LIMIT" if maximum number of transactions is reached.
67
95
  * @throws {DatabaseError} With code "LMDB-{code}" for LMDB-specific errors.
68
96
  *
@@ -70,7 +98,6 @@ export { init, onCommit, onRevert, getTransactionData, setTransactionData, Datab
70
98
  * ```typescript
71
99
  * const paid = await E.transact(() => {
72
100
  * const user = User.pk.get("john_doe");
73
- * // This is concurrency-safe - the function will rerun if it is raced by another transaction
74
101
  * if (user.credits > 0) {
75
102
  * user.credits--;
76
103
  * return true;
@@ -81,78 +108,143 @@ export { init, onCommit, onRevert, getTransactionData, setTransactionData, Datab
81
108
  * ```typescript
82
109
  * // Transaction with automatic retry on conflicts
83
110
  * await E.transact(() => {
84
- * const counter = Counter.load("global") || new Counter({id: "global", value: 0});
111
+ * const counter = Counter.pk.get("global") || new Counter({id: "global", value: 0});
85
112
  * counter.value++;
86
113
  * });
87
114
  * ```
88
115
  */
89
116
  export async function transact<T>(fn: () => T): Promise<T> {
90
- try {
91
- const onSaveQueue: ChangedModel[] | undefined = onSaveCallback ? [] : undefined;
92
- return await olmdb.transact(async (): Promise<T> => {
93
- const instances = new Set<Model<any>>();
94
- olmdb.setTransactionData(INSTANCES_SYMBOL, instances);
95
- olmdb.setTransactionData(INSTANCES_BY_PK_SYMBOL, new Map());
96
-
97
- const savedInstances: Set<Model<any>> = new Set();
117
+ while (initNeeded || pendingInit) {
118
+ // Make sure only one async task is doing the inits, the rest should wait for it
119
+ if (pendingInit) {
120
+ await pendingInit;
121
+ } else {
122
+ pendingInit = (async () => {
123
+ if (!olmdbReady) olmdbInit('.edinburgh');
124
+ olmdbReady = true;
125
+
126
+ initNeeded = false;
127
+ for (const model of Object.values(modelRegistry)) {
128
+ await model._delayedInit();
129
+ }
130
+ pendingInit = undefined;
131
+ })();
132
+ }
133
+ }
134
+
135
+ // try {
136
+ for (let retryCount = 0; retryCount < maxRetryCount; retryCount++) {
137
+ const txnId = lowlevel.startTransaction();
138
+ const txn: Transaction = { id: txnId, instances: new Set(), instancesByPk: new Map() };
139
+ const onSaveItems: Map<Model<unknown>, Change> | undefined = onSaveCallback ? new Map() : undefined;
140
+
141
+ let result: T | undefined;
98
142
  try {
99
- const result = await fn();
100
- // Save all modified instances before committing.
101
- while(instances.size > 0) {
102
- // Back referencing can cause models to be scheduled for save() a second time,
103
- // which is why we require the outer loop.
104
- for (const instance of instances) {
105
- instance._onCommit(onSaveQueue);
106
- savedInstances.add(instance);
107
- instances.delete(instance);
143
+ await txnStorage.run(txn, async function() {
144
+ result = await fn();
145
+
146
+ // Call preCommit() on all instances before writing.
147
+ // Note: Set iteration visits newly added items, so preCommit() creating
148
+ // new instances is handled correctly.
149
+ for (const instance of txn.instances) {
150
+ instance.preCommit?.();
151
+ }
152
+
153
+ // Save all modified instances before committing
154
+ // This needs to happen inside txnStorage.run, because resolving default values
155
+ // for identifiers requires database access.
156
+ for (const instance of txn.instances) {
157
+ const change = instance._write(txn);
158
+ if (onSaveItems && change) {
159
+ onSaveItems.set(instance, change);
160
+ }
108
161
  }
162
+ });
163
+ } catch (e: any) {
164
+ try { lowlevel.abortTransaction(txnId); } catch {}
165
+ throw e;
166
+ } finally {
167
+ // Make the instances read-only to make it clear that their transaction has ended.
168
+ for (const instance of txn.instances) {
169
+ delete instance._oldValues;
170
+ Object.defineProperty(instance, "_txn", STALE_INSTANCE_DESCRIPTOR);
171
+ Object.freeze(instance);
109
172
  }
110
- if (onSaveQueue?.length) {
111
- olmdb.onCommit((commitId: number) => {
112
- if (onSaveCallback) onSaveCallback(commitId, onSaveQueue);
113
- });
173
+ // Destroy the transaction object, to make sure things crash if they are used after
174
+ // this point, and to help the GC reclaim memory.
175
+ txn.id = txn.instances = txn.instancesByPk = undefined as any;
176
+ }
177
+
178
+ const commitResult = lowlevel.commitTransaction(txnId);
179
+ const commitSeq = typeof commitResult === 'number' ? commitResult : await commitResult;
180
+
181
+ if (commitSeq > 0) {
182
+ // Success
183
+ if (onSaveItems?.size) {
184
+ onSaveCallback!(commitSeq, onSaveItems);
114
185
  }
115
-
116
- return result;
117
- } catch (error) {
118
- // Discard changes on all saved and still unsaved instances
119
- for (const instance of savedInstances) instance.preventPersist();
120
- for (const instance of instances) instance.preventPersist();
121
- throw error;
186
+ return result as T;
122
187
  }
123
- });
124
- } catch (e: Error | any) {
125
- // This hackery is required to provide useful stack traces. Without this,
126
- // both Bun and Node (even with --async-stack-traces) don't show which
127
- // line called the transact(), which is pretty important info when validation
128
- // fails, for instance. Though the line numbers in Bun still don't really
129
- // make sense. Probably this bug: https://github.com/oven-sh/bun/issues/15859
130
- e.stack += "\nat async:\n" + new Error().stack?.replace(/^.*?\n/, '');
131
- throw e;
132
- }
188
+
189
+ // Race condition - retry
190
+ }
191
+ throw new DatabaseError("Transaction keeps getting raced", "RACING_TRANSACTION");
192
+ // } catch (e: Error | any) {
193
+ // // This hackery is required to provide useful stack traces.
194
+ // const callerStack = new Error().stack?.replace(/^.*?\n/, '');
195
+ // e.stack += "\nat async:\n" + callerStack;
196
+ // throw e;
197
+ // }
133
198
  }
134
199
 
135
- let onSaveCallback: ((commitId: number, items: ChangedModel[]) => void) | undefined;
200
+ /**
201
+ * Set the maximum number of retries for a transaction in case of conflicts.
202
+ * The default value is 6. Setting it to 0 will disable retries and cause transactions to fail immediately on conflict.
203
+ *
204
+ * @param count The maximum number of retries for a transaction.
205
+ */
206
+ export function setMaxRetryCount(count: number) {
207
+ maxRetryCount = count;
208
+ }
136
209
 
210
+ let onSaveCallback: ((commitId: number, items: Map<Model<any>, Change>) => void) | undefined;
211
+
137
212
  /**
138
213
  * Set a callback function to be called after a model is saved and committed.
139
214
  *
140
215
  * @param callback The callback function to set. It gets called after each successful
141
216
  * `transact()` commit that has changes, with the following arguments:
142
217
  * - A sequential number. Higher numbers have been committed after lower numbers.
143
- * - An array of model instances that have been modified, created, or deleted.
144
- * You can used its {@link Model.changed} property to figure out what changed.
218
+ * - A map of model instances to their changes. The change can be "created", "deleted", or an object containing the old values.
145
219
  */
146
- export function setOnSaveCallback(callback: ((commitId: number, items: ChangedModel[]) => void) | undefined) {
220
+ export function setOnSaveCallback(callback: ((commitId: number, items: Map<Model<any>, Change>) => void) | undefined) {
147
221
  onSaveCallback = callback;
148
222
  }
149
223
 
150
224
 
151
225
  export async function deleteEverything(): Promise<void> {
152
- await olmdb.transact(() => {
153
- for (const {key} of olmdb.scan()) {
154
- olmdb.del(key);
155
- }
156
- });
157
- await resetModelCaches();
226
+ let done = false;
227
+ while (!done) {
228
+ await transact(() => {
229
+ const txn = currentTxn();
230
+ const iteratorId = lowlevel.createIterator(txn.id, undefined, undefined, false);
231
+ const deadline = Date.now() + 150;
232
+ let count = 0;
233
+ try {
234
+ while (true) {
235
+ const raw = lowlevel.readIterator(iteratorId);
236
+ if (!raw) { done = true; break; }
237
+ lowlevel.del(txn.id, raw.key);
238
+ if (++count >= 4096 || Date.now() >= deadline) break;
239
+ }
240
+ } finally {
241
+ lowlevel.closeIterator(iteratorId);
242
+ }
243
+ });
244
+ }
245
+ // Re-init indexes since metadata was deleted
246
+ for (const model of Object.values(modelRegistry)) {
247
+ if (!model.fields) continue;
248
+ await model._delayedInit(true);
249
+ }
158
250
  }