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/README.md +387 -217
- package/build/src/datapack.d.ts +22 -3
- package/build/src/datapack.js +105 -41
- package/build/src/datapack.js.map +1 -1
- package/build/src/edinburgh.d.ts +31 -13
- package/build/src/edinburgh.js +148 -62
- package/build/src/edinburgh.js.map +1 -1
- package/build/src/indexes.d.ts +78 -56
- package/build/src/indexes.js +519 -284
- package/build/src/indexes.js.map +1 -1
- package/build/src/migrate-cli.d.ts +20 -0
- package/build/src/migrate-cli.js +122 -0
- package/build/src/migrate-cli.js.map +1 -0
- package/build/src/migrate.d.ts +33 -0
- package/build/src/migrate.js +225 -0
- package/build/src/migrate.js.map +1 -0
- package/build/src/models.d.ts +130 -25
- package/build/src/models.js +271 -169
- package/build/src/models.js.map +1 -1
- package/build/src/types.d.ts +24 -7
- package/build/src/types.js +49 -15
- package/build/src/types.js.map +1 -1
- package/build/src/utils.d.ts +6 -10
- package/build/src/utils.js +26 -32
- package/build/src/utils.js.map +1 -1
- package/package.json +5 -4
- package/src/datapack.ts +117 -46
- package/src/edinburgh.ts +156 -64
- package/src/indexes.ts +550 -287
- package/src/migrate-cli.ts +138 -0
- package/src/migrate.ts +267 -0
- package/src/models.ts +352 -184
- package/src/types.ts +59 -16
- package/src/utils.ts +32 -32
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
|
|
12
|
-
|
|
13
|
-
const
|
|
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
|
|
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 =
|
|
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'
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
280
|
-
|
|
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
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
|
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
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
42
|
-
} from
|
|
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
|
-
|
|
47
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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
|
-
* -
|
|
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:
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
}
|