edinburgh 0.5.0 → 0.6.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.
Files changed (72) hide show
  1. package/README.md +309 -246
  2. package/build/src/datapack.d.ts +9 -9
  3. package/build/src/datapack.js +9 -9
  4. package/build/src/edinburgh.d.ts +21 -7
  5. package/build/src/edinburgh.js +53 -67
  6. package/build/src/edinburgh.js.map +1 -1
  7. package/build/src/indexes.d.ts +85 -205
  8. package/build/src/indexes.js +150 -503
  9. package/build/src/indexes.js.map +1 -1
  10. package/build/src/migrate.js +8 -10
  11. package/build/src/migrate.js.map +1 -1
  12. package/build/src/models.d.ts +152 -107
  13. package/build/src/models.js +433 -144
  14. package/build/src/models.js.map +1 -1
  15. package/build/src/types.d.ts +30 -48
  16. package/build/src/types.js +25 -24
  17. package/build/src/types.js.map +1 -1
  18. package/build/src/utils.d.ts +4 -4
  19. package/build/src/utils.js +4 -4
  20. package/package.json +2 -2
  21. package/skill/AnyModelClass.md +7 -0
  22. package/skill/FindOptions.md +37 -0
  23. package/skill/Lifecycle Hooks.md +24 -0
  24. package/skill/{Model_delete.md → Lifecycle Hooks_delete.md } +1 -1
  25. package/skill/{Model_getPrimaryKeyHash.md → Lifecycle Hooks_getPrimaryKeyHash.md } +1 -1
  26. package/skill/{Model_isValid.md → Lifecycle Hooks_isValid.md } +1 -1
  27. package/skill/Lifecycle Hooks_migrate.md +26 -0
  28. package/skill/{Model_preCommit.md → Lifecycle Hooks_preCommit.md } +2 -2
  29. package/skill/{Model_preventPersist.md → Lifecycle Hooks_preventPersist.md } +1 -1
  30. package/skill/{Model_validate.md → Lifecycle Hooks_validate.md } +2 -2
  31. package/skill/ModelBase.md +7 -0
  32. package/skill/ModelClass.md +8 -0
  33. package/skill/SKILL.md +180 -132
  34. package/skill/Schema Evolution.md +19 -0
  35. package/skill/TypeWrapper_containsNull.md +11 -0
  36. package/skill/TypeWrapper_deserialize.md +9 -0
  37. package/skill/TypeWrapper_getError.md +11 -0
  38. package/skill/TypeWrapper_serialize.md +10 -0
  39. package/skill/TypeWrapper_serializeType.md +9 -0
  40. package/skill/array.md +2 -2
  41. package/skill/defineModel.md +3 -2
  42. package/skill/deleteEverything.md +8 -0
  43. package/skill/field.md +3 -3
  44. package/skill/link.md +3 -3
  45. package/skill/literal.md +1 -1
  46. package/skill/opt.md +1 -1
  47. package/skill/or.md +1 -1
  48. package/skill/record.md +1 -1
  49. package/skill/set.md +2 -2
  50. package/skill/setOnSaveCallback.md +5 -2
  51. package/skill/transact.md +1 -1
  52. package/src/datapack.ts +9 -9
  53. package/src/edinburgh.ts +68 -68
  54. package/src/indexes.ts +251 -599
  55. package/src/migrate.ts +9 -10
  56. package/src/models.ts +528 -231
  57. package/src/types.ts +36 -34
  58. package/src/utils.ts +4 -4
  59. package/skill/BaseIndex.md +0 -16
  60. package/skill/BaseIndex_batchProcess.md +0 -10
  61. package/skill/BaseIndex_find.md +0 -7
  62. package/skill/BaseIndex_find_2.md +0 -7
  63. package/skill/BaseIndex_find_3.md +0 -7
  64. package/skill/BaseIndex_find_4.md +0 -7
  65. package/skill/Model.md +0 -20
  66. package/skill/Model_batchProcess.md +0 -8
  67. package/skill/Model_migrate.md +0 -32
  68. package/skill/Model_replaceInto.md +0 -16
  69. package/skill/NonPrimaryIndex.md +0 -10
  70. package/skill/SecondaryIndex.md +0 -9
  71. package/skill/UniqueIndex.md +0 -9
  72. package/skill/dump.md +0 -8
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
 
@@ -16,7 +16,8 @@ typed fields, primary key access, and optional secondary and unique indexes.
16
16
 
17
17
  **Parameters:**
18
18
 
19
- - `cls: T` - - A plain class whose properties use E.field().
20
- - `opts?: { pk?: PK, unique?: UNIQUE, index?: INDEX, tableName?: string, override?: boolean }` - - Registration options.
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.
21
22
 
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,15 +14,15 @@ 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
- const User = E.defineModel(class {
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
  });
package/skill/link.md CHANGED
@@ -10,19 +10,19 @@ Create a link type wrapper for model relationships.
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
- const Author = E.defineModel(class {
20
+ const Author = E.defineModel("Author", class {
21
21
  id = E.field(E.identifier);
22
22
  posts = E.field(E.array(E.link(() => Book)));
23
23
  }, { pk: "id" });
24
24
 
25
- const Book = E.defineModel(class {
25
+ const Book = E.defineModel("Book", class {
26
26
  id = E.field(E.identifier);
27
27
  author = E.field(E.link(Author));
28
28
  }, { pk: "id" });
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,14 @@
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.
13
+
14
+ The callback is called within a new transaction context, allowing lazy-loads to happen. However, any
15
+ changes made to Edinburgh models will not be saved.
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
 
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 {
@@ -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,20 +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,
10
+ type ModelClass,
11
+ type AnyModelClass,
12
+ type ModelBase,
12
13
  defineModel,
14
+ deleteEverything,
13
15
  field,
14
- currentTxn,
15
16
  } from "./models.js";
16
17
 
17
- import type { Transaction, Change, Model } from "./models.js";
18
+ import type { Change, Model } from "./models.js";
18
19
 
19
20
  // Re-export public API from types (only factory functions and instances)
20
21
  export {
@@ -41,14 +42,33 @@ export {
41
42
  dump,
42
43
  } from "./indexes.js";
43
44
 
44
- export { BaseIndex, NonPrimaryIndex, UniqueIndex, SecondaryIndex } from './indexes.js';
45
+ export type { FindOptions, IndexRangeIterator } from './indexes.js';
45
46
 
46
47
  export { type Change } from './models.js';
47
- export type { Transaction } from './models.js';
48
+ export type { FieldConfig } from './models.js';
49
+ export { TypeWrapper } from './types.js';
48
50
  export { DatabaseError } from "olmdb/lowlevel";
49
51
  export { runMigration } from './migrate.js';
50
52
  export type { MigrationOptions, MigrationResult } from './migrate.js';
51
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
+
52
72
  let olmdbReady = false;
53
73
  let maxRetryCount = 6;
54
74
 
@@ -88,7 +108,7 @@ const STALE_INSTANCE_DESCRIPTOR = {
88
108
  * times.
89
109
  *
90
110
  * @template T - The return type of the transaction function.
91
- * @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.
92
112
  * @returns A promise that resolves with the function's return value.
93
113
  * @throws {DatabaseError} With code "RACING_TRANSACTION" if the transaction fails after retries due to conflicts.
94
114
  * @throws {DatabaseError} With code "TXN_LIMIT" if maximum number of transactions is reached.
@@ -114,17 +134,17 @@ const STALE_INSTANCE_DESCRIPTOR = {
114
134
  * ```
115
135
  */
116
136
  export async function transact<T>(fn: () => T): Promise<T> {
117
- while (initNeeded || pendingInit) {
118
- // Make sure only one async task is doing the inits, the rest should wait for it
137
+ while (pendingModelInits.size || pendingInit) {
119
138
  if (pendingInit) {
120
139
  await pendingInit;
121
140
  } else {
122
141
  pendingInit = (async () => {
123
142
  if (!olmdbReady) olmdbInit('.edinburgh');
124
143
  olmdbReady = true;
125
- initNeeded = false;
126
- for (const model of Object.values(modelRegistry)) {
127
- await model._loadCreateIndexes();
144
+ const models = [...pendingModelInits];
145
+ pendingModelInits.clear();
146
+ for (const model of models) {
147
+ await model._initialize();
128
148
  }
129
149
  })();
130
150
  await pendingInit;
@@ -135,7 +155,7 @@ export async function transact<T>(fn: () => T): Promise<T> {
135
155
  // try {
136
156
  for (let retryCount = 0; retryCount < maxRetryCount; retryCount++) {
137
157
  const txnId = lowlevel.startTransaction();
138
- const txn: Transaction = { id: txnId, instances: new Set(), instancesByPk: new Map() };
158
+ const txn: Transaction = { id: txnId, instances: new Map() };
139
159
  const onSaveItems: Map<Model<unknown>, Change> | undefined = onSaveCallback ? new Map() : undefined;
140
160
 
141
161
  let result: T | undefined;
@@ -144,16 +164,16 @@ export async function transact<T>(fn: () => T): Promise<T> {
144
164
  result = await fn();
145
165
 
146
166
  // Call preCommit() on all instances before writing.
147
- // Note: Set iteration visits newly added items, so preCommit() creating
167
+ // Note: Map iteration visits newly added items, so preCommit() creating
148
168
  // new instances is handled correctly.
149
- for (const instance of txn.instances) {
150
- instance.preCommit?.();
169
+ for (const instance of txn.instances.values()) {
170
+ if (instance._oldValues !== false) instance.preCommit?.();
151
171
  }
152
172
 
153
173
  // Save all modified instances before committing
154
174
  // This needs to happen inside txnStorage.run, because resolving default values
155
175
  // for identifiers requires database access.
156
- for (const instance of txn.instances) {
176
+ for (const instance of txn.instances.values()) {
157
177
  const change = instance._write(txn);
158
178
  if (onSaveItems && change) {
159
179
  onSaveItems.set(instance, change);
@@ -163,30 +183,36 @@ export async function transact<T>(fn: () => T): Promise<T> {
163
183
  } catch (e: any) {
164
184
  try { lowlevel.abortTransaction(txnId); } catch {}
165
185
  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);
172
- }
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
186
  }
177
187
 
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) {
188
+ if (onSaveItems?.size) {
189
+ // Perform writes, and start a new transaction at or past the point-in-time of our commit
190
+ const commitResult = lowlevel.commitTransaction(txnId, true);
191
+ if (typeof commitResult === 'object') {
192
+ const commitSeq = await commitResult;
193
+ if (commitSeq <= 0) continue; // Race condition - retry
194
+ // Run the callback within our new transaction context, so it can fetch linked lazy fields if needed
184
195
  onSaveCallback!(commitSeq, onSaveItems);
185
196
  }
186
- return result as T;
197
+ // else: only reads
198
+ }
199
+
200
+ // Make the instances read-only to make it clear that their transaction has ended.
201
+ for (const instance of txn.instances.values()) {
202
+ delete instance._oldValues;
203
+ Object.defineProperty(instance, "_txn", STALE_INSTANCE_DESCRIPTOR);
204
+ Object.freeze(instance);
187
205
  }
188
206
 
189
- // Race condition - retry
207
+ // Destroy the transaction object, to make sure things crash if they are used after
208
+ // this point, and to help the GC reclaim memory.
209
+ txn.id = txn.instances = undefined as any;
210
+
211
+
212
+ // Commit the transaction and actually end it
213
+ if ((await lowlevel.commitTransaction(txnId)) <= 0) continue; // Race condition - retry
214
+
215
+ return result as T;
190
216
  }
191
217
  throw new DatabaseError("Transaction keeps getting raced", "RACING_TRANSACTION");
192
218
  // } catch (e: Error | any) {
@@ -207,7 +233,7 @@ export function setMaxRetryCount(count: number) {
207
233
  maxRetryCount = count;
208
234
  }
209
235
 
210
- let onSaveCallback: ((commitId: number, items: Map<Model<any>, Change>) => void) | undefined;
236
+ let onSaveCallback: ((commitId: number, items: Map<Model<unknown>, Change>) => void) | undefined;
211
237
 
212
238
  /**
213
239
  * Set a callback function to be called after a model is saved and committed.
@@ -216,36 +242,10 @@ let onSaveCallback: ((commitId: number, items: Map<Model<any>, Change>) => void)
216
242
  * `transact()` commit that has changes, with the following arguments:
217
243
  * - A sequential number. Higher numbers have been committed after lower numbers.
218
244
  * - A map of model instances to their changes. The change can be "created", "deleted", or an object containing the old values.
245
+ *
246
+ * The callback is called within a new transaction context, allowing lazy-loads to happen. However, any
247
+ * changes made to Edinburgh models will not be saved.
219
248
  */
220
- export function setOnSaveCallback(callback: ((commitId: number, items: Map<Model<any>, Change>) => void) | undefined) {
249
+ export function setOnSaveCallback(callback: ((commitId: number, items: Map<Model<unknown>, Change>) => void) | undefined) {
221
250
  onSaveCallback = callback;
222
251
  }
223
-
224
-
225
- export async function deleteEverything(): Promise<void> {
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
- model._resetIndexes();
249
- await model._loadCreateIndexes();
250
- }
251
- }