edinburgh 0.4.6 → 0.6.0

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.
Files changed (77) hide show
  1. package/README.md +403 -461
  2. package/build/src/datapack.d.ts +9 -9
  3. package/build/src/datapack.js +10 -10
  4. package/build/src/datapack.js.map +1 -1
  5. package/build/src/edinburgh.d.ts +21 -10
  6. package/build/src/edinburgh.js +33 -55
  7. package/build/src/edinburgh.js.map +1 -1
  8. package/build/src/indexes.d.ts +99 -288
  9. package/build/src/indexes.js +253 -636
  10. package/build/src/indexes.js.map +1 -1
  11. package/build/src/migrate.js +17 -39
  12. package/build/src/migrate.js.map +1 -1
  13. package/build/src/models.d.ts +177 -113
  14. package/build/src/models.js +487 -259
  15. package/build/src/models.js.map +1 -1
  16. package/build/src/types.d.ts +41 -51
  17. package/build/src/types.js +39 -52
  18. package/build/src/types.js.map +1 -1
  19. package/build/src/utils.d.ts +4 -4
  20. package/build/src/utils.js +4 -4
  21. package/package.json +1 -3
  22. package/skill/AnyModelClass.md +7 -0
  23. package/skill/FindOptions.md +37 -0
  24. package/skill/Lifecycle Hooks.md +24 -0
  25. package/skill/{Model_delete.md → Lifecycle Hooks_delete.md } +2 -2
  26. package/skill/{Model_getPrimaryKeyHash.md → Lifecycle Hooks_getPrimaryKeyHash.md } +1 -1
  27. package/skill/{Model_isValid.md → Lifecycle Hooks_isValid.md } +1 -1
  28. package/skill/Lifecycle Hooks_migrate.md +26 -0
  29. package/skill/{Model_preCommit.md → Lifecycle Hooks_preCommit.md } +3 -5
  30. package/skill/{Model_preventPersist.md → Lifecycle Hooks_preventPersist.md } +2 -2
  31. package/skill/{Model_validate.md → Lifecycle Hooks_validate.md } +2 -2
  32. package/skill/ModelBase.md +7 -0
  33. package/skill/ModelClass.md +8 -0
  34. package/skill/SKILL.md +253 -215
  35. package/skill/Schema Evolution.md +19 -0
  36. package/skill/TypeWrapper_containsNull.md +11 -0
  37. package/skill/TypeWrapper_deserialize.md +9 -0
  38. package/skill/TypeWrapper_getError.md +11 -0
  39. package/skill/TypeWrapper_serialize.md +10 -0
  40. package/skill/TypeWrapper_serializeType.md +9 -0
  41. package/skill/array.md +2 -2
  42. package/skill/defineModel.md +23 -0
  43. package/skill/deleteEverything.md +8 -0
  44. package/skill/field.md +4 -4
  45. package/skill/link.md +12 -10
  46. package/skill/literal.md +1 -1
  47. package/skill/opt.md +1 -1
  48. package/skill/or.md +1 -1
  49. package/skill/record.md +1 -1
  50. package/skill/set.md +2 -2
  51. package/skill/setOnSaveCallback.md +2 -2
  52. package/skill/transact.md +3 -3
  53. package/src/datapack.ts +10 -10
  54. package/src/edinburgh.ts +46 -58
  55. package/src/indexes.ts +338 -802
  56. package/src/migrate.ts +15 -37
  57. package/src/models.ts +617 -314
  58. package/src/types.ts +61 -54
  59. package/src/utils.ts +4 -4
  60. package/skill/BaseIndex.md +0 -16
  61. package/skill/BaseIndex_batchProcess.md +0 -10
  62. package/skill/BaseIndex_find.md +0 -7
  63. package/skill/Model.md +0 -22
  64. package/skill/Model_findAll.md +0 -12
  65. package/skill/Model_migrate.md +0 -34
  66. package/skill/Model_replaceInto.md +0 -16
  67. package/skill/PrimaryIndex.md +0 -8
  68. package/skill/PrimaryIndex_get.md +0 -17
  69. package/skill/PrimaryIndex_getLazy.md +0 -13
  70. package/skill/SecondaryIndex.md +0 -9
  71. package/skill/UniqueIndex.md +0 -9
  72. package/skill/UniqueIndex_get.md +0 -17
  73. package/skill/dump.md +0 -8
  74. package/skill/index.md +0 -32
  75. package/skill/primary.md +0 -26
  76. package/skill/registerModel.md +0 -26
  77. package/skill/unique.md +0 -32
@@ -0,0 +1,19 @@
1
+ ### Schema Evolution
2
+
3
+ Edinburgh tracks the schema version of each model automatically. When you add, remove, or
4
+ change the types of fields, or add/remove indexes, Edinburgh detects the new schema version.
5
+
6
+ **Lazy migration:** Changes to non-key field values are migrated lazily, when a row with an
7
+ old schema version is read from disk, it is deserialized using the old schema and optionally
8
+ transformed by the static `migrate()` function. This happens transparently on every read
9
+ and requires no downtime or batch processing.
10
+
11
+ **Batch migration (via `npx migrate-edinburgh` or `runMigration()`):** Certain schema changes
12
+ require an explicit migration run:
13
+ - Adding or removing secondary/unique indexes
14
+ - Changing the fields or types of an existing index
15
+ - A `migrate()` function that changes values used in secondary index fields
16
+
17
+ The batch migration tool populates new indexes, deletes orphaned ones, and updates index
18
+ entries whose values were changed by `migrate()`. It does *not* rewrite primary data rows
19
+ (lazy migration handles that).
@@ -0,0 +1,11 @@
1
+ #### typeWrapper.containsNull · method
2
+
3
+ Check if indexing should be skipped for this field value.
4
+
5
+ **Signature:** `(value: T) => boolean`
6
+
7
+ **Parameters:**
8
+
9
+ - `value: T`
10
+
11
+ **Returns:** true if indexing should be skipped.
@@ -0,0 +1,9 @@
1
+ #### typeWrapper.deserialize · abstract method
2
+
3
+ Deserialize a value from a Pack into an object property.
4
+
5
+ **Signature:** `(pack: DataPack) => T`
6
+
7
+ **Parameters:**
8
+
9
+ - `pack: DataPack` - The Pack instance to read from.
@@ -0,0 +1,11 @@
1
+ #### typeWrapper.getError · abstract method
2
+
3
+ Validate a value.
4
+
5
+ **Signature:** `(value: T) => void | DatabaseError`
6
+
7
+ **Parameters:**
8
+
9
+ - `value: T` - The value to validate.
10
+
11
+ **Returns:** - A DatabaseError if validation fails.
@@ -0,0 +1,10 @@
1
+ #### typeWrapper.serialize · abstract method
2
+
3
+ Serialize a value from an object property to a Pack.
4
+
5
+ **Signature:** `(value: T, pack: DataPack) => void`
6
+
7
+ **Parameters:**
8
+
9
+ - `value: T` - The value to serialize.
10
+ - `pack: DataPack` - The Pack instance to write to.
@@ -0,0 +1,9 @@
1
+ #### typeWrapper.serializeType · method
2
+
3
+ Serialize type metadata to a Pack (for schema serialization).
4
+
5
+ **Signature:** `(pack: DataPack) => void`
6
+
7
+ **Parameters:**
8
+
9
+ - `pack: DataPack` - The Pack instance to write to.
package/skill/array.md CHANGED
@@ -10,8 +10,8 @@ Create an array type wrapper with optional length constraints.
10
10
 
11
11
  **Parameters:**
12
12
 
13
- - `inner: TypeWrapper<T>` - - Type wrapper for array elements.
14
- - `opts: {min?: number, max?: number}` (optional) - - Optional constraints (min/max length).
13
+ - `inner: TypeWrapper<T>` - Type wrapper for array elements.
14
+ - `opts: {min?: number, max?: number}` (optional) - Optional constraints (min/max length).
15
15
 
16
16
  **Returns:** An array type instance.
17
17
 
@@ -0,0 +1,23 @@
1
+ ### defineModel · function
2
+
3
+ Register a model class with the Edinburgh ORM system.
4
+
5
+ Converts a plain class into a fully-featured model with database persistence,
6
+ typed fields, primary key access, and optional secondary and unique indexes.
7
+
8
+ **Signature:** `<T extends new () => any, const PK extends (keyof FieldsOf<T> & string) | readonly (keyof FieldsOf<T> & string)[], const UNIQUE extends Record<string, (keyof FieldsOf<T> & string) | readonly (keyof FieldsOf<T> & string)[] | ((instance: any) => any)>, const INDEX extends Record<string, (keyof FieldsOf<T> & string) | ...`
9
+
10
+ **Type Parameters:**
11
+
12
+ - `T extends new () => any`
13
+ - `PK extends (keyof FieldsOf<T> & string) | readonly (keyof FieldsOf<T> & string)[]`
14
+ - `UNIQUE extends Record<string, (keyof FieldsOf<T> & string) | readonly (keyof FieldsOf<T> & string)[] | ((instance: any) => any)>`
15
+ - `INDEX extends Record<string, (keyof FieldsOf<T> & string) | readonly (keyof FieldsOf<T> & string)[] | ((instance: any) => any)>`
16
+
17
+ **Parameters:**
18
+
19
+ - `tableName: string` - The database table name for this model.
20
+ - `cls: T` - A plain class whose properties use E.field().
21
+ - `opts?: { pk?: PK, unique?: UNIQUE, index?: INDEX, override?: boolean }` - Registration options.
22
+
23
+ **Returns:** The enhanced model constructor.
@@ -0,0 +1,8 @@
1
+ ### deleteEverything · function
2
+
3
+ Delete every key/value entry in the database and reinitialize all registered models.
4
+
5
+ This clears rows, index metadata, and schema-version records. It is mainly useful
6
+ for tests, local resets, or tooling that needs a completely empty database.
7
+
8
+ **Signature:** `() => Promise<void>`
package/skill/field.md CHANGED
@@ -14,16 +14,16 @@ This allows for both runtime introspection and compile-time type safety.
14
14
 
15
15
  **Parameters:**
16
16
 
17
- - `type: TypeWrapper<T>` - - The type wrapper for this field.
18
- - `options: Partial<FieldConfig<T>>` (optional) - - Additional field configuration options.
17
+ - `type: TypeWrapper<T>` - The type wrapper for this field.
18
+ - `options: Partial<FieldConfig<T>>` (optional) - Additional field configuration options.
19
19
 
20
20
  **Returns:** The field value (typed as T, but actually returns FieldConfig<T>).
21
21
 
22
22
  **Examples:**
23
23
 
24
24
  ```typescript
25
- class User extends E.Model<User> {
25
+ const User = E.defineModel("User", class {
26
26
  name = E.field(E.string, {description: "User's full name"});
27
27
  age = E.field(E.opt(E.number), {description: "User's age", default: 25});
28
- }
28
+ });
29
29
  ```
package/skill/link.md CHANGED
@@ -2,26 +2,28 @@
2
2
 
3
3
  Create a link type wrapper for model relationships.
4
4
 
5
- **Signature:** `<const T extends typeof Model<any>>(TargetModel: T) => TypeWrapper<InstanceType<T>>`
5
+ **Signature:** `{ <const T extends new (...args: any[]) => Model<any>>(TargetModel: T): TypeWrapper<InstanceType<T>>; <const T extends new (...args: any[]) => Model<any>>(TargetModel: () => T): TypeWrapper<...>; }`
6
6
 
7
7
  **Type Parameters:**
8
8
 
9
- - `T extends typeof Model<any>` - The target model class.
9
+ - `T extends new (...args: any[]) => Model<any>` - The target model class.
10
10
 
11
11
  **Parameters:**
12
12
 
13
- - `TargetModel: T` - - The model class this link points to.
13
+ - `TargetModel: T` - The model class this link points to.
14
14
 
15
15
  **Returns:** A link type instance.
16
16
 
17
17
  **Examples:**
18
18
 
19
19
  ```typescript
20
- class User extends E.Model<User> {
21
- posts = E.field(E.array(E.link(Post, 'author')));
22
- }
23
-
24
- class Post extends E.Model<Post> {
25
- author = E.field(E.link(User));
26
- }
20
+ const Author = E.defineModel("Author", class {
21
+ id = E.field(E.identifier);
22
+ posts = E.field(E.array(E.link(() => Book)));
23
+ }, { pk: "id" });
24
+
25
+ const Book = E.defineModel("Book", class {
26
+ id = E.field(E.identifier);
27
+ author = E.field(E.link(Author));
28
+ }, { pk: "id" });
27
29
  ```
package/skill/literal.md CHANGED
@@ -10,7 +10,7 @@ Create a literal type wrapper for a constant value.
10
10
 
11
11
  **Parameters:**
12
12
 
13
- - `value: T` - - The literal value.
13
+ - `value: T` - The literal value.
14
14
 
15
15
  **Returns:** A literal type instance.
16
16
 
package/skill/opt.md CHANGED
@@ -10,7 +10,7 @@ Create an optional type wrapper (allows undefined).
10
10
 
11
11
  **Parameters:**
12
12
 
13
- - `inner: T` - - The inner type to make optional.
13
+ - `inner: T` - The inner type to make optional.
14
14
 
15
15
  **Returns:** A union type that accepts the inner type or undefined.
16
16
 
package/skill/or.md CHANGED
@@ -10,7 +10,7 @@ Create a union type wrapper from multiple type choices.
10
10
 
11
11
  **Parameters:**
12
12
 
13
- - `choices: T` - - The type choices for the union.
13
+ - `choices: T` - The type choices for the union.
14
14
 
15
15
  **Returns:** A union type instance.
16
16
 
package/skill/record.md CHANGED
@@ -10,7 +10,7 @@ Create a Record type wrapper for key-value objects with string or number keys.
10
10
 
11
11
  **Parameters:**
12
12
 
13
- - `inner: TypeWrapper<T>` - - Type wrapper for record values.
13
+ - `inner: TypeWrapper<T>` - Type wrapper for record values.
14
14
 
15
15
  **Returns:** A record type instance.
16
16
 
package/skill/set.md CHANGED
@@ -10,8 +10,8 @@ Create a Set type wrapper with optional length constraints.
10
10
 
11
11
  **Parameters:**
12
12
 
13
- - `inner: TypeWrapper<T>` - - Type wrapper for set elements.
14
- - `opts: {min?: number, max?: number}` (optional) - - Optional constraints (min/max length).
13
+ - `inner: TypeWrapper<T>` - Type wrapper for set elements.
14
+ - `opts: {min?: number, max?: number}` (optional) - Optional constraints (min/max length).
15
15
 
16
16
  **Returns:** A set type instance.
17
17
 
@@ -2,11 +2,11 @@
2
2
 
3
3
  Set a callback function to be called after a model is saved and committed.
4
4
 
5
- **Signature:** `(callback: (commitId: number, items: Map<Model<any>, Change>) => void) => void`
5
+ **Signature:** `(callback: (commitId: number, items: Map<ModelBase, Change>) => void) => void`
6
6
 
7
7
  **Parameters:**
8
8
 
9
- - `callback: ((commitId: number, items: Map<Model<any>, Change>) => void) | undefined` - The callback function to set. It gets called after each successful
9
+ - `callback: ((commitId: number, items: Map<Model<unknown>, Change>) => void) | undefined` - The callback function to set. It gets called after each successful
10
10
  `transact()` commit that has changes, with the following arguments:
11
11
  - A sequential number. Higher numbers have been committed after lower numbers.
12
12
  - A map of model instances to their changes. The change can be "created", "deleted", or an object containing the old values.
package/skill/transact.md CHANGED
@@ -18,7 +18,7 @@ times.
18
18
 
19
19
  **Parameters:**
20
20
 
21
- - `fn: () => T` - - The function to execute within the transaction context. Receives a Transaction instance.
21
+ - `fn: () => T` - The function to execute within the transaction context. Receives a Transaction instance.
22
22
 
23
23
  **Returns:** A promise that resolves with the function's return value.
24
24
 
@@ -32,7 +32,7 @@ times.
32
32
 
33
33
  ```typescript
34
34
  const paid = await E.transact(() => {
35
- const user = User.pk.get("john_doe");
35
+ const user = User.get("john_doe");
36
36
  if (user.credits > 0) {
37
37
  user.credits--;
38
38
  return true;
@@ -43,7 +43,7 @@ const paid = await E.transact(() => {
43
43
  ```typescript
44
44
  // Transaction with automatic retry on conflicts
45
45
  await E.transact(() => {
46
- const counter = Counter.pk.get("global") || new Counter({id: "global", value: 0});
46
+ const counter = Counter.get("global") || new Counter({id: "global", value: 0});
47
47
  counter.value++;
48
48
  });
49
49
  ```
package/src/datapack.ts CHANGED
@@ -46,7 +46,7 @@ export default class DataPack {
46
46
 
47
47
  /**
48
48
  * Create a new DataPack instance.
49
- * @param data - Optional initial data as Uint8Array or buffer size as number.
49
+ * @param data Optional initial data as Uint8Array or buffer size as number.
50
50
  */
51
51
  constructor(data: Uint8Array | number = 1900) {
52
52
  if (data instanceof Uint8Array) {
@@ -59,10 +59,10 @@ export default class DataPack {
59
59
 
60
60
  /**
61
61
  * Helper function to write a multi-byte integer with length prefix
62
- * @param value - The value to write
63
- * @param headerType - The type bits (0-7) for the header
64
- * @param invertBytes - Whether to invert bytes (for negative numbers)
65
- * @param invertByteCount - Whether to invert the byte count (for type 0)
62
+ * @param value The value to write
63
+ * @param headerType The type bits (0-7) for the header
64
+ * @param invertBytes Whether to invert bytes (for negative numbers)
65
+ * @param invertByteCount Whether to invert the byte count (for type 0)
66
66
  */
67
67
  private writeMultiByteNumber(value: number, headerType: number, invertBytes: boolean = false, invertByteCount: boolean = false): void {
68
68
  let byteCount = 0;
@@ -87,8 +87,8 @@ export default class DataPack {
87
87
 
88
88
  /**
89
89
  * Helper function to read a multi-byte integer with length prefix
90
- * @param byteCount - Number of bytes to read
91
- * @param invertBytes - Whether to invert bytes (for negative numbers)
90
+ * @param byteCount Number of bytes to read
91
+ * @param invertBytes Whether to invert bytes (for negative numbers)
92
92
  * @returns The read value
93
93
  */
94
94
  private readMultiByteNumber(byteCount: number, invertBytes: boolean = false): number {
@@ -187,7 +187,7 @@ export default class DataPack {
187
187
  this.ensureCapacity(data.length);
188
188
  this.buffer.set(data, this.writePos);
189
189
  this.writePos += data.length;
190
- } else if (Array.isArray(data)) {
190
+ } else if (Array.isArray(data) || (typeof data.length === 'number' && typeof data[Symbol.iterator] === 'function')) {
191
191
  // Type 4, subtype 5: array start
192
192
  this.buffer[this.writePos++] = (4 << 5) | 5;
193
193
  for (const item of data) {
@@ -383,7 +383,7 @@ export default class DataPack {
383
383
 
384
384
  /**
385
385
  * Ensure the buffer has capacity for additional bytes.
386
- * @param bytesNeeded - Number of additional bytes needed.
386
+ * @param bytesNeeded Number of additional bytes needed.
387
387
  */
388
388
  private ensureCapacity(bytesNeeded: number): void {
389
389
  const needed = this.writePos + bytesNeeded;
@@ -511,7 +511,7 @@ export default class DataPack {
511
511
  /**
512
512
  * Like writeString but writes without a length prefix and with a null terminator, for ordered storage.
513
513
  * Can be read with {@link read} or {@link readString} just like any other string.
514
- * @param str - The string to write. May not contain null characters.
514
+ * @param str The string to write. May not contain null characters.
515
515
  */
516
516
  writeOrderedString(str: string): DataPack {
517
517
  const utf8Bytes = new TextEncoder().encode(str);
package/src/edinburgh.ts CHANGED
@@ -1,19 +1,21 @@
1
1
  import * as lowlevel from "olmdb/lowlevel";
2
2
  import { init as olmdbInit, DatabaseError } from "olmdb/lowlevel";
3
- import { modelRegistry, txnStorage, currentTxn } from "./models.js";
4
-
5
- let initNeeded = true;
6
- export function scheduleInit() { initNeeded = true; }
3
+ import { AsyncLocalStorage } from "node:async_hooks";
4
+ import { pendingModelInits } from "./models.js";
7
5
 
8
6
 
9
7
  // Re-export public API from models
10
8
  export {
11
9
  Model,
12
- registerModel,
10
+ type ModelClass,
11
+ type AnyModelClass,
12
+ type ModelBase,
13
+ defineModel,
14
+ deleteEverything,
13
15
  field,
14
16
  } from "./models.js";
15
17
 
16
- import type { Transaction, Change, Model } from "./models.js";
18
+ import type { Change, Model } from "./models.js";
17
19
 
18
20
  // Re-export public API from types (only factory functions and instances)
19
21
  export {
@@ -37,20 +39,36 @@ export {
37
39
 
38
40
  // Re-export public API from indexes
39
41
  export {
40
- index,
41
- primary,
42
- unique,
43
42
  dump,
44
43
  } from "./indexes.js";
45
44
 
46
- export { BaseIndex, UniqueIndex, PrimaryIndex, SecondaryIndex } from './indexes.js';
45
+ export type { FindOptions, IndexRangeIterator } from './indexes.js';
47
46
 
48
47
  export { type Change } from './models.js';
49
- export type { Transaction } from './models.js';
48
+ export type { FieldConfig } from './models.js';
49
+ export { TypeWrapper } from './types.js';
50
50
  export { DatabaseError } from "olmdb/lowlevel";
51
51
  export { runMigration } from './migrate.js';
52
52
  export type { MigrationOptions, MigrationResult } from './migrate.js';
53
53
 
54
+ export interface Transaction {
55
+ id: number;
56
+ instances: Map<number, Model<unknown>>; // pkHash => instance
57
+ }
58
+
59
+ export const txnStorage = new AsyncLocalStorage<Transaction>();
60
+
61
+ /**
62
+ * Returns the current transaction from AsyncLocalStorage.
63
+ * Throws if called outside a transact() callback.
64
+ * @internal
65
+ */
66
+ export function currentTxn(): Transaction {
67
+ const txn = txnStorage.getStore();
68
+ if (!txn) throw new DatabaseError("No active transaction. Operations must be performed within a transact() callback.", 'NO_TRANSACTION');
69
+ return txn;
70
+ }
71
+
54
72
  let olmdbReady = false;
55
73
  let maxRetryCount = 6;
56
74
 
@@ -90,7 +108,7 @@ const STALE_INSTANCE_DESCRIPTOR = {
90
108
  * times.
91
109
  *
92
110
  * @template T - The return type of the transaction function.
93
- * @param fn - The function to execute within the transaction context. Receives a Transaction instance.
111
+ * @param fn The function to execute within the transaction context. Receives a Transaction instance.
94
112
  * @returns A promise that resolves with the function's return value.
95
113
  * @throws {DatabaseError} With code "RACING_TRANSACTION" if the transaction fails after retries due to conflicts.
96
114
  * @throws {DatabaseError} With code "TXN_LIMIT" if maximum number of transactions is reached.
@@ -99,7 +117,7 @@ const STALE_INSTANCE_DESCRIPTOR = {
99
117
  * @example
100
118
  * ```typescript
101
119
  * const paid = await E.transact(() => {
102
- * const user = User.pk.get("john_doe");
120
+ * const user = User.get("john_doe");
103
121
  * if (user.credits > 0) {
104
122
  * user.credits--;
105
123
  * return true;
@@ -110,24 +128,23 @@ const STALE_INSTANCE_DESCRIPTOR = {
110
128
  * ```typescript
111
129
  * // Transaction with automatic retry on conflicts
112
130
  * await E.transact(() => {
113
- * const counter = Counter.pk.get("global") || new Counter({id: "global", value: 0});
131
+ * const counter = Counter.get("global") || new Counter({id: "global", value: 0});
114
132
  * counter.value++;
115
133
  * });
116
134
  * ```
117
135
  */
118
136
  export async function transact<T>(fn: () => T): Promise<T> {
119
- while (initNeeded || pendingInit) {
120
- // Make sure only one async task is doing the inits, the rest should wait for it
137
+ while (pendingModelInits.size || pendingInit) {
121
138
  if (pendingInit) {
122
139
  await pendingInit;
123
140
  } else {
124
141
  pendingInit = (async () => {
125
142
  if (!olmdbReady) olmdbInit('.edinburgh');
126
143
  olmdbReady = true;
127
- initNeeded = false;
128
- for (const model of Object.values(modelRegistry)) {
129
- model.initFields();
130
- await model._loadCreateIndexes();
144
+ const models = [...pendingModelInits];
145
+ pendingModelInits.clear();
146
+ for (const model of models) {
147
+ await model._initialize();
131
148
  }
132
149
  })();
133
150
  await pendingInit;
@@ -138,7 +155,7 @@ export async function transact<T>(fn: () => T): Promise<T> {
138
155
  // try {
139
156
  for (let retryCount = 0; retryCount < maxRetryCount; retryCount++) {
140
157
  const txnId = lowlevel.startTransaction();
141
- const txn: Transaction = { id: txnId, instances: new Set(), instancesByPk: new Map() };
158
+ const txn: Transaction = { id: txnId, instances: new Map() };
142
159
  const onSaveItems: Map<Model<unknown>, Change> | undefined = onSaveCallback ? new Map() : undefined;
143
160
 
144
161
  let result: T | undefined;
@@ -147,16 +164,16 @@ export async function transact<T>(fn: () => T): Promise<T> {
147
164
  result = await fn();
148
165
 
149
166
  // Call preCommit() on all instances before writing.
150
- // Note: Set iteration visits newly added items, so preCommit() creating
167
+ // Note: Map iteration visits newly added items, so preCommit() creating
151
168
  // new instances is handled correctly.
152
- for (const instance of txn.instances) {
153
- instance.preCommit?.();
169
+ for (const instance of txn.instances.values()) {
170
+ if (instance._oldValues !== false) instance.preCommit?.();
154
171
  }
155
172
 
156
173
  // Save all modified instances before committing
157
174
  // This needs to happen inside txnStorage.run, because resolving default values
158
175
  // for identifiers requires database access.
159
- for (const instance of txn.instances) {
176
+ for (const instance of txn.instances.values()) {
160
177
  const change = instance._write(txn);
161
178
  if (onSaveItems && change) {
162
179
  onSaveItems.set(instance, change);
@@ -168,14 +185,14 @@ export async function transact<T>(fn: () => T): Promise<T> {
168
185
  throw e;
169
186
  } finally {
170
187
  // Make the instances read-only to make it clear that their transaction has ended.
171
- for (const instance of txn.instances) {
188
+ for (const instance of txn.instances.values()) {
172
189
  delete instance._oldValues;
173
190
  Object.defineProperty(instance, "_txn", STALE_INSTANCE_DESCRIPTOR);
174
191
  Object.freeze(instance);
175
192
  }
176
193
  // Destroy the transaction object, to make sure things crash if they are used after
177
194
  // this point, and to help the GC reclaim memory.
178
- txn.id = txn.instances = txn.instancesByPk = undefined as any;
195
+ txn.id = txn.instances = undefined as any;
179
196
  }
180
197
 
181
198
  const commitResult = lowlevel.commitTransaction(txnId);
@@ -210,7 +227,7 @@ export function setMaxRetryCount(count: number) {
210
227
  maxRetryCount = count;
211
228
  }
212
229
 
213
- let onSaveCallback: ((commitId: number, items: Map<Model<any>, Change>) => void) | undefined;
230
+ let onSaveCallback: ((commitId: number, items: Map<Model<unknown>, Change>) => void) | undefined;
214
231
 
215
232
  /**
216
233
  * Set a callback function to be called after a model is saved and committed.
@@ -220,35 +237,6 @@ let onSaveCallback: ((commitId: number, items: Map<Model<any>, Change>) => void)
220
237
  * - A sequential number. Higher numbers have been committed after lower numbers.
221
238
  * - A map of model instances to their changes. The change can be "created", "deleted", or an object containing the old values.
222
239
  */
223
- export function setOnSaveCallback(callback: ((commitId: number, items: Map<Model<any>, Change>) => void) | undefined) {
240
+ export function setOnSaveCallback(callback: ((commitId: number, items: Map<Model<unknown>, Change>) => void) | undefined) {
224
241
  onSaveCallback = callback;
225
242
  }
226
-
227
-
228
- export async function deleteEverything(): Promise<void> {
229
- let done = false;
230
- while (!done) {
231
- await transact(() => {
232
- const txn = currentTxn();
233
- const iteratorId = lowlevel.createIterator(txn.id, undefined, undefined, false);
234
- const deadline = Date.now() + 150;
235
- let count = 0;
236
- try {
237
- while (true) {
238
- const raw = lowlevel.readIterator(iteratorId);
239
- if (!raw) { done = true; break; }
240
- lowlevel.del(txn.id, raw.key);
241
- if (++count >= 4096 || Date.now() >= deadline) break;
242
- }
243
- } finally {
244
- lowlevel.closeIterator(iteratorId);
245
- }
246
- });
247
- }
248
- // Re-init indexes since metadata was deleted
249
- for (const model of Object.values(modelRegistry)) {
250
- if (!model.fields) continue;
251
- model.initFields(true);
252
- await model._loadCreateIndexes();
253
- }
254
- }