inibase 3.2.0 → 3.3.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.
- package/dist/file.d.ts +16 -0
- package/dist/file.js +151 -25
- package/dist/index.d.ts +9 -2
- package/dist/index.js +117 -45
- package/package.json +2 -2
package/dist/file.d.ts
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import { type ComparisonOperator, type Field } from "./index.js";
|
|
2
2
|
export declare const DURABLE: boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Run `fn` with a per-logical-task lock-owner store on its AsyncLocalStorage
|
|
5
|
+
* context. Every await continuation below `fn` then shares that store, so
|
|
6
|
+
* nested/reentrant acquisitions (put/delete id->line recursion, journal
|
|
7
|
+
* recovery re-locks, transaction flows) see the SAME store the outermost
|
|
8
|
+
* acquisition recorded as owner.
|
|
9
|
+
*
|
|
10
|
+
* Do NOT replace this with an enterWith() from inside lock(): that store is
|
|
11
|
+
* created in lock()'s own frame and (a) never propagates back to the caller's
|
|
12
|
+
* await continuation — every recursion then waits on its OWN lock until the
|
|
13
|
+
* watchdog (LOCK_WAIT_TIMEOUT) — and (b) can leak into sibling tasks, which
|
|
14
|
+
* then pass the writer lock as "reentrant" and re-race the Sep-2026 commit
|
|
15
|
+
* interleave (ENOENT renames, duplicate ids). `run()` gives clean per-op
|
|
16
|
+
* isolation and correct same-chain propagation.
|
|
17
|
+
*/
|
|
18
|
+
export declare const runWithLockStore: <T>(fn: () => T) => T;
|
|
3
19
|
export declare const lock: (folderPath: string, prefix?: string, ttl?: number) => Promise<void>;
|
|
4
20
|
/**
|
|
5
21
|
* Non-blocking lock acquisition (used by the read path and the open-time
|
package/dist/file.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1
2
|
import { access, appendFile, copyFile, constants as fsConstants, open, readFile, stat, unlink, } from "node:fs/promises";
|
|
2
3
|
import { hostname } from "node:os";
|
|
3
4
|
import { join, resolve } from "node:path";
|
|
@@ -31,6 +32,87 @@ const LOCK_RETRY_MS = 13;
|
|
|
31
32
|
// multiple times (e.g. post() -> get() -> sort-cache). Depth 1 is a real
|
|
32
33
|
// filesystem acquisition; deeper acquisitions just bump the counter.
|
|
33
34
|
const lockDepths = new Map();
|
|
35
|
+
// In-process serialization: sibling tasks (e.g. two concurrent HTTP requests
|
|
36
|
+
// posting to the same table) must QUEUE behind the holder instead of passing
|
|
37
|
+
// straight through the depth counter. That bypass is exactly what let
|
|
38
|
+
// concurrent writers share the fixed `.tmp/<column>` staging path and race the
|
|
39
|
+
// pagination rename (diverged column files, duplicate ids, lost rows in the
|
|
40
|
+
// Sep-2026 pteam incident). Reentrancy from the *same* logical task (post() ->
|
|
41
|
+
// get() -> ensureTableRecovered re-lock, transaction flows) still bumps the
|
|
42
|
+
// depth without touching the filesystem, proven by the AsyncLocalStorage
|
|
43
|
+
// context recorded at acquisition time.
|
|
44
|
+
const lockReleaseWaiters = new Map();
|
|
45
|
+
const lockOwners = new Map();
|
|
46
|
+
// Upper bound on in-process wait time: converts a wedged or undetectable
|
|
47
|
+
// holder into a visible error instead of a silent hang. 0 disables it.
|
|
48
|
+
const LOCK_WAIT_TIMEOUT_MS = Number(process.env.INIBASE_LOCK_WAIT_TIMEOUT_MS ?? 120_000);
|
|
49
|
+
const lockContext = new AsyncLocalStorage();
|
|
50
|
+
// Resolve once the holder releases so the waiter can re-race for the lock.
|
|
51
|
+
const waitForLockRelease = (resolvedPath) => new Promise((resolveWaiter, rejectWaiter) => {
|
|
52
|
+
let waiters = lockReleaseWaiters.get(resolvedPath);
|
|
53
|
+
if (!waiters) {
|
|
54
|
+
waiters = new Set();
|
|
55
|
+
lockReleaseWaiters.set(resolvedPath, waiters);
|
|
56
|
+
}
|
|
57
|
+
let timer = null;
|
|
58
|
+
const wake = (error) => {
|
|
59
|
+
waiters.delete(wake);
|
|
60
|
+
if (timer)
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
error ? rejectWaiter(error) : resolveWaiter();
|
|
63
|
+
};
|
|
64
|
+
waiters.add(wake);
|
|
65
|
+
if (LOCK_WAIT_TIMEOUT_MS > 0)
|
|
66
|
+
timer = setTimeout(() => wake(new Error(`LOCK_WAIT_TIMEOUT: ${resolvedPath}`)), LOCK_WAIT_TIMEOUT_MS);
|
|
67
|
+
});
|
|
68
|
+
// The AsyncLocalStorage store of this task chain (created on first acquire).
|
|
69
|
+
const currentLockStore = () => {
|
|
70
|
+
let store = lockContext.getStore();
|
|
71
|
+
if (!store) {
|
|
72
|
+
store = new Set();
|
|
73
|
+
lockContext.enterWith(store);
|
|
74
|
+
}
|
|
75
|
+
return store;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Run `fn` with a per-logical-task lock-owner store on its AsyncLocalStorage
|
|
79
|
+
* context. Every await continuation below `fn` then shares that store, so
|
|
80
|
+
* nested/reentrant acquisitions (put/delete id->line recursion, journal
|
|
81
|
+
* recovery re-locks, transaction flows) see the SAME store the outermost
|
|
82
|
+
* acquisition recorded as owner.
|
|
83
|
+
*
|
|
84
|
+
* Do NOT replace this with an enterWith() from inside lock(): that store is
|
|
85
|
+
* created in lock()'s own frame and (a) never propagates back to the caller's
|
|
86
|
+
* await continuation — every recursion then waits on its OWN lock until the
|
|
87
|
+
* watchdog (LOCK_WAIT_TIMEOUT) — and (b) can leak into sibling tasks, which
|
|
88
|
+
* then pass the writer lock as "reentrant" and re-race the Sep-2026 commit
|
|
89
|
+
* interleave (ENOENT renames, duplicate ids). `run()` gives clean per-op
|
|
90
|
+
* isolation and correct same-chain propagation.
|
|
91
|
+
*/
|
|
92
|
+
export const runWithLockStore = (fn) => {
|
|
93
|
+
if (lockContext.getStore())
|
|
94
|
+
return fn();
|
|
95
|
+
const store = new Set();
|
|
96
|
+
return lockContext.run(store, fn);
|
|
97
|
+
};
|
|
98
|
+
// True when this task already owns the lock (depth > 0 and the recorded owner
|
|
99
|
+
// context is this task's context). Bumps the depth and returns.
|
|
100
|
+
const isReentrant = (resolvedPath) => {
|
|
101
|
+
if (!lockDepths.get(resolvedPath))
|
|
102
|
+
return false;
|
|
103
|
+
if (lockOwners.get(resolvedPath) !== lockContext.getStore())
|
|
104
|
+
return false;
|
|
105
|
+
lockDepths.set(resolvedPath, lockDepths.get(resolvedPath) + 1);
|
|
106
|
+
return true;
|
|
107
|
+
};
|
|
108
|
+
const notifyLockReleased = (resolvedPath) => {
|
|
109
|
+
const waiters = lockReleaseWaiters.get(resolvedPath);
|
|
110
|
+
if (!waiters)
|
|
111
|
+
return;
|
|
112
|
+
lockReleaseWaiters.delete(resolvedPath);
|
|
113
|
+
for (const wake of waiters)
|
|
114
|
+
wake();
|
|
115
|
+
};
|
|
34
116
|
const lockFilePathFor = (folderPath, prefix) => join(folderPath, `${prefix ?? ""}.locked`);
|
|
35
117
|
const lockOwnerState = async (lockFilePath) => {
|
|
36
118
|
try {
|
|
@@ -67,12 +149,16 @@ const stealableLock = async (lockFilePath, mtimeMs, ttl) => {
|
|
|
67
149
|
export const lock = async (folderPath, prefix, ttl = DEFAULT_LOCK_TTL_MS) => {
|
|
68
150
|
const lockFilePath = lockFilePathFor(folderPath, prefix);
|
|
69
151
|
const resolvedPath = resolve(lockFilePath);
|
|
70
|
-
const depth = lockDepths.get(resolvedPath);
|
|
71
|
-
if (depth) {
|
|
72
|
-
lockDepths.set(resolvedPath, depth + 1);
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
152
|
for (;;) {
|
|
153
|
+
// Same logical task already holds it -> reentrant depth bump.
|
|
154
|
+
if (isReentrant(resolvedPath))
|
|
155
|
+
return;
|
|
156
|
+
// A different in-process task holds it -> wait for the release, then
|
|
157
|
+
// re-race for the filesystem lock like any process.
|
|
158
|
+
if (lockDepths.get(resolvedPath)) {
|
|
159
|
+
await waitForLockRelease(resolvedPath);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
76
162
|
try {
|
|
77
163
|
const lockFile = await open(lockFilePath, "wx");
|
|
78
164
|
try {
|
|
@@ -100,6 +186,7 @@ export const lock = async (folderPath, prefix, ttl = DEFAULT_LOCK_TTL_MS) => {
|
|
|
100
186
|
}
|
|
101
187
|
}
|
|
102
188
|
lockDepths.set(resolvedPath, 1);
|
|
189
|
+
lockOwners.set(resolvedPath, currentLockStore());
|
|
103
190
|
return;
|
|
104
191
|
}
|
|
105
192
|
catch (error) {
|
|
@@ -129,11 +216,10 @@ export const lock = async (folderPath, prefix, ttl = DEFAULT_LOCK_TTL_MS) => {
|
|
|
129
216
|
export const tryLock = async (folderPath, prefix, ttl = DEFAULT_LOCK_TTL_MS) => {
|
|
130
217
|
const lockFilePath = lockFilePathFor(folderPath, prefix);
|
|
131
218
|
const resolvedPath = resolve(lockFilePath);
|
|
132
|
-
|
|
133
|
-
if (depth) {
|
|
134
|
-
lockDepths.set(resolvedPath, depth + 1);
|
|
219
|
+
if (isReentrant(resolvedPath))
|
|
135
220
|
return true;
|
|
136
|
-
|
|
221
|
+
if (lockDepths.get(resolvedPath))
|
|
222
|
+
return false; // held by another in-process task: non-blocking
|
|
137
223
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
138
224
|
try {
|
|
139
225
|
const lockFile = await open(lockFilePath, "wx");
|
|
@@ -157,6 +243,7 @@ export const tryLock = async (folderPath, prefix, ttl = DEFAULT_LOCK_TTL_MS) =>
|
|
|
157
243
|
}
|
|
158
244
|
}
|
|
159
245
|
lockDepths.set(resolvedPath, 1);
|
|
246
|
+
lockOwners.set(resolvedPath, currentLockStore());
|
|
160
247
|
return true;
|
|
161
248
|
}
|
|
162
249
|
catch (error) {
|
|
@@ -183,12 +270,15 @@ export const unlock = async (folderPath, prefix) => {
|
|
|
183
270
|
return;
|
|
184
271
|
}
|
|
185
272
|
lockDepths.delete(resolvedPath);
|
|
273
|
+
lockOwners.delete(resolvedPath);
|
|
186
274
|
try {
|
|
187
275
|
await unlink(lockFilePath);
|
|
188
276
|
}
|
|
189
277
|
catch {
|
|
190
278
|
// Already released (stolen by a foreign-host stealer or cleaned up).
|
|
191
279
|
}
|
|
280
|
+
// Wake in-process waiters so they re-race for the released lock.
|
|
281
|
+
notifyLockReleased(resolvedPath);
|
|
192
282
|
};
|
|
193
283
|
export const write = async (filePath, data) => {
|
|
194
284
|
const handle = await open(filePath, "w");
|
|
@@ -242,6 +332,17 @@ export function escapeShellPath(filePath) {
|
|
|
242
332
|
// Escape double quotes and special shell characters
|
|
243
333
|
return `"${resolvedPath.replace(/(["\\$`])/g, "\\$1")}"`;
|
|
244
334
|
}
|
|
335
|
+
/**
|
|
336
|
+
* Unique-per-operation staging path: concurrent writers must never share the
|
|
337
|
+
* `.tmp/<column>` file. The fixed name let sibling operations clobber each
|
|
338
|
+
* other's in-progress temp and publish a temp that only contained one writer's
|
|
339
|
+
* content (column files following different winners -> diverged line counts).
|
|
340
|
+
* A pid + per-process counter suffix keeps every temp private; each pair is
|
|
341
|
+
* carried through the rename list and the journal, so commit/rollback/recovery
|
|
342
|
+
* still know exactly which files to move.
|
|
343
|
+
*/
|
|
344
|
+
let tempSeq = 0;
|
|
345
|
+
const tempPathFor = (filePath) => filePath.replace(/([^/]+)\/?$/, `.tmp/$1.${process.pid}-${(tempSeq++).toString(36)}`);
|
|
245
346
|
const _pipeline = async (filePath, rl, writeStream, transform) => {
|
|
246
347
|
if (filePath.endsWith(".gz"))
|
|
247
348
|
await pipeline(rl, transform, createGzip(), writeStream);
|
|
@@ -505,7 +606,7 @@ export async function get(filePath, lineNumbers, field, readWholeFile = false) {
|
|
|
505
606
|
* Note: If the file doesn't exist and replacements is an object, it creates a new file with the specified replacements.
|
|
506
607
|
*/
|
|
507
608
|
export const replace = async (filePath, replacements, totalItems) => {
|
|
508
|
-
const fileTempPath = filePath
|
|
609
|
+
const fileTempPath = tempPathFor(filePath);
|
|
509
610
|
const isReplacementsObject = isObject(replacements);
|
|
510
611
|
const isReplacementsLineNumbered = isReplacementsObject && !Number.isNaN(Number(Object.keys(replacements)[0]));
|
|
511
612
|
if (await isExists(filePath)) {
|
|
@@ -581,19 +682,33 @@ export const replace = async (filePath, replacements, totalItems) => {
|
|
|
581
682
|
const replacementsKeys = Object.keys(replacements)
|
|
582
683
|
.map(Number)
|
|
583
684
|
.toSorted((a, b) => a - b);
|
|
584
|
-
await write(fileTempPath,
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
685
|
+
await write(fileTempPath,
|
|
686
|
+
// First-write for a compressed column: see append() — the
|
|
687
|
+
// unique temp path hides the ".gz" suffix from write().
|
|
688
|
+
filePath.endsWith(".gz")
|
|
689
|
+
? await gzip(`${"\n".repeat(replacementsKeys[0] - 1) +
|
|
690
|
+
replacementsKeys
|
|
691
|
+
.map((lineNumber, index) => index === 0 ||
|
|
692
|
+
lineNumber - replacementsKeys[index - 1] - 1 === 0
|
|
693
|
+
? replacements[lineNumber]
|
|
694
|
+
: "\n".repeat(lineNumber - replacementsKeys[index - 1] - 1) + replacements[lineNumber])
|
|
695
|
+
.join("\n")}\n`)
|
|
696
|
+
: `${"\n".repeat(replacementsKeys[0] - 1) +
|
|
697
|
+
replacementsKeys
|
|
698
|
+
.map((lineNumber, index) => index === 0 ||
|
|
699
|
+
lineNumber - replacementsKeys[index - 1] - 1 === 0
|
|
700
|
+
? replacements[lineNumber]
|
|
701
|
+
: "\n".repeat(lineNumber - replacementsKeys[index - 1] - 1) + replacements[lineNumber])
|
|
702
|
+
.join("\n")}\n`);
|
|
592
703
|
}
|
|
593
704
|
else {
|
|
594
705
|
if (!totalItems)
|
|
595
706
|
throw new Error("INVALID_PARAMETERS");
|
|
596
|
-
await write(fileTempPath,
|
|
707
|
+
await write(fileTempPath,
|
|
708
|
+
// First-write for a compressed column: see append().
|
|
709
|
+
filePath.endsWith(".gz")
|
|
710
|
+
? await gzip(`${`${replacements}\n`.repeat(totalItems)}\n`)
|
|
711
|
+
: `${`${replacements}\n`.repeat(totalItems)}\n`);
|
|
597
712
|
}
|
|
598
713
|
return [fileTempPath, filePath];
|
|
599
714
|
}
|
|
@@ -612,7 +727,7 @@ export const replace = async (filePath, replacements, totalItems) => {
|
|
|
612
727
|
*
|
|
613
728
|
*/
|
|
614
729
|
export const append = async (filePath, data) => {
|
|
615
|
-
const fileTempPath = filePath
|
|
730
|
+
const fileTempPath = tempPathFor(filePath);
|
|
616
731
|
try {
|
|
617
732
|
if (await isExists(filePath)) {
|
|
618
733
|
await copyFile(filePath, fileTempPath);
|
|
@@ -627,7 +742,13 @@ export const append = async (filePath, data) => {
|
|
|
627
742
|
}
|
|
628
743
|
}
|
|
629
744
|
else
|
|
630
|
-
await write(fileTempPath,
|
|
745
|
+
await write(fileTempPath,
|
|
746
|
+
// Unique temp paths no longer end with the original extension
|
|
747
|
+
// (.tmp/<col>.<pid>-<seq>), so write()'s suffix check can't
|
|
748
|
+
// gzip a first-write for a compressed column. Do it here.
|
|
749
|
+
filePath.endsWith(".gz")
|
|
750
|
+
? await gzip(`${Array.isArray(data) ? data.join("\n") : data}\n`)
|
|
751
|
+
: `${Array.isArray(data) ? data.join("\n") : data}\n`);
|
|
631
752
|
return [fileTempPath, filePath];
|
|
632
753
|
}
|
|
633
754
|
catch {
|
|
@@ -643,7 +764,7 @@ export const append = async (filePath, data) => {
|
|
|
643
764
|
*
|
|
644
765
|
*/
|
|
645
766
|
export const prepend = async (filePath, data) => {
|
|
646
|
-
const fileTempPath = filePath
|
|
767
|
+
const fileTempPath = tempPathFor(filePath);
|
|
647
768
|
if (await isExists(filePath)) {
|
|
648
769
|
if (!filePath.endsWith(".gz")) {
|
|
649
770
|
let fileHandle = null;
|
|
@@ -673,7 +794,7 @@ export const prepend = async (filePath, data) => {
|
|
|
673
794
|
}
|
|
674
795
|
}
|
|
675
796
|
else {
|
|
676
|
-
const fileChildTempPath = filePath
|
|
797
|
+
const fileChildTempPath = `${tempPathFor(filePath)}.child`;
|
|
677
798
|
try {
|
|
678
799
|
await write(fileChildTempPath, `${Array.isArray(data) ? data.join("\n") : data}\n`);
|
|
679
800
|
const escapedFilePath = escapeShellPath(filePath);
|
|
@@ -691,7 +812,12 @@ export const prepend = async (filePath, data) => {
|
|
|
691
812
|
}
|
|
692
813
|
else {
|
|
693
814
|
try {
|
|
694
|
-
await write(fileTempPath,
|
|
815
|
+
await write(fileTempPath,
|
|
816
|
+
// First-write for a compressed column: see append() — the
|
|
817
|
+
// unique temp path hides the ".gz" suffix from write().
|
|
818
|
+
filePath.endsWith(".gz")
|
|
819
|
+
? await gzip(`${Array.isArray(data) ? data.join("\n") : data}\n`)
|
|
820
|
+
: `${Array.isArray(data) ? data.join("\n") : data}\n`);
|
|
695
821
|
}
|
|
696
822
|
catch {
|
|
697
823
|
return [fileTempPath, null];
|
|
@@ -714,7 +840,7 @@ export const remove = async (filePath, linesToDelete) => {
|
|
|
714
840
|
: [Number(linesToDelete)];
|
|
715
841
|
if (linesToDelete.some(Number.isNaN))
|
|
716
842
|
throw new Error("UNVALID_LINE_NUMBERS");
|
|
717
|
-
const fileTempPath = filePath
|
|
843
|
+
const fileTempPath = tempPathFor(filePath);
|
|
718
844
|
try {
|
|
719
845
|
const escapedFilePath = escapeShellPath(filePath);
|
|
720
846
|
const escapedFileTempPath = escapeShellPath(fileTempPath);
|
package/dist/index.d.ts
CHANGED
|
@@ -155,6 +155,7 @@ export default class Inibase {
|
|
|
155
155
|
* @param {TableConfig} [config]
|
|
156
156
|
*/
|
|
157
157
|
createTable(tableName: string, schema?: Schema, config?: TableConfig): Promise<void>;
|
|
158
|
+
private _createTable;
|
|
158
159
|
private replaceStringInFile;
|
|
159
160
|
/**
|
|
160
161
|
* Update table schema or config
|
|
@@ -166,6 +167,7 @@ export default class Inibase {
|
|
|
166
167
|
updateTable(tableName: string, schema?: Schema, config?: TableConfig & {
|
|
167
168
|
name?: string;
|
|
168
169
|
}): Promise<void>;
|
|
170
|
+
private _updateTable;
|
|
169
171
|
private updateTableLocked;
|
|
170
172
|
/**
|
|
171
173
|
* Get table schema and config
|
|
@@ -333,6 +335,7 @@ export default class Inibase {
|
|
|
333
335
|
* locked on first touch, in first-touch order.
|
|
334
336
|
*/
|
|
335
337
|
begin(tables?: string[]): Promise<void>;
|
|
338
|
+
private _begin;
|
|
336
339
|
/**
|
|
337
340
|
* Publish every staged mutation atomically: per table (sorted), the
|
|
338
341
|
* pagination rename comes first (the atomic publication point readers
|
|
@@ -371,6 +374,7 @@ export default class Inibase {
|
|
|
371
374
|
get<TData extends Record<string, any> & Partial<Data>>(tableName: string, where?: string | number | (string | number)[] | Criteria, options?: Options, onlyOne?: boolean, onlyLinesNumbers?: false, _whereIsLinesNumbers?: boolean): Promise<(Data & TData)[] | null>;
|
|
372
375
|
get<_TData extends Record<string, any> & Partial<Data>>(tableName: string, where: string | number | (string | number)[] | Criteria | undefined, options: Options | undefined, onlyOne: false | undefined, onlyLinesNumbers: true, _whereIsLinesNumbers?: boolean): Promise<number[] | null>;
|
|
373
376
|
get<_TData extends Record<string, any> & Partial<Data>>(tableName: string, where: string | number | (string | number)[] | Criteria | undefined, options: Options | undefined, onlyOne: true, onlyLinesNumbers: true, _whereIsLinesNumbers?: boolean): Promise<number | null>;
|
|
377
|
+
private _get;
|
|
374
378
|
private getOnce;
|
|
375
379
|
/**
|
|
376
380
|
* Create new item(s) in a table
|
|
@@ -385,6 +389,7 @@ export default class Inibase {
|
|
|
385
389
|
post<TData extends Record<string, any> & Partial<Data>>(tableName: string, data: (Data & TData)[], options?: Options, returnPostedData?: boolean): Promise<string[]>;
|
|
386
390
|
post<TData extends Record<string, any> & Partial<Data>>(tableName: string, data: Data & TData, options: Options | undefined, returnPostedData: true): Promise<(Data & TData) | null>;
|
|
387
391
|
post<TData extends Record<string, any> & Partial<Data>>(tableName: string, data: (Data & TData)[], options: Options | undefined, returnPostedData: true): Promise<(Data & TData)[] | null>;
|
|
392
|
+
private _post;
|
|
388
393
|
/**
|
|
389
394
|
* Update item(s) in a table
|
|
390
395
|
*
|
|
@@ -399,6 +404,7 @@ export default class Inibase {
|
|
|
399
404
|
put<TData extends Record<string, any> & Partial<Data>>(tableName: string, data: Data & TData, where: number | string | (number | string)[] | Criteria | undefined, options: Options | undefined, returnUpdatedData: true | boolean, _whereIsLinesNumbers?: boolean): Promise<(Data & TData) | null>;
|
|
400
405
|
put<TData extends Record<string, any> & Partial<Data>>(tableName: string, data: (Data & TData)[], where: number | string | (number | string)[] | Criteria | undefined, options: Options | undefined, returnUpdatedData: true | boolean, _whereIsLinesNumbers?: boolean): Promise<(Data & TData)[] | null>;
|
|
401
406
|
put<TData extends Record<string, any> & Partial<Data>>(tableName: string, data: (Data & TData) | (Data & TData)[], where: number | string | (number | string)[] | Criteria | undefined, options: Options | undefined, returnUpdatedData: true | boolean, _whereIsLinesNumbers?: boolean): Promise<(Data & TData) | (Data & TData)[] | null>;
|
|
407
|
+
private _put;
|
|
402
408
|
/**
|
|
403
409
|
* Delete item(s) in a table
|
|
404
410
|
*
|
|
@@ -407,6 +413,7 @@ export default class Inibase {
|
|
|
407
413
|
* @return {boolean | null} {(Promise<boolean | null>)}
|
|
408
414
|
*/
|
|
409
415
|
delete(tableName: string, where?: number | string | (number | string)[] | Criteria, _whereIsLinesNumbers?: boolean, _cascadeGuard?: Set<string>): Promise<boolean | null>;
|
|
416
|
+
private _delete;
|
|
410
417
|
/**
|
|
411
418
|
* Cascade delete: remove rows in other tables whose `table`-typed schema
|
|
412
419
|
* fields reference the given rows. Reference columns store the numeric
|
|
@@ -424,8 +431,8 @@ export default class Inibase {
|
|
|
424
431
|
* @param {(number | string | (number | string)[] | Criteria)} [where]
|
|
425
432
|
* @return {*} {Promise<number | Record<string, number>>}
|
|
426
433
|
*/
|
|
427
|
-
sum(tableName: string, columns: string, where?: number | string | (number | string)[] | Criteria): Promise<number>;
|
|
428
|
-
sum(tableName: string, columns: string[], where?: number | string | (number | string)[] | Criteria): Promise<Record<string, number>>;
|
|
434
|
+
sum(tableName: string, columns: string, where?: number | string | (number | string)[] | Criteria, options?: AggregateOptions): Promise<number>;
|
|
435
|
+
sum(tableName: string, columns: string[], where?: number | string | (number | string)[] | Criteria, options?: AggregateOptions): Promise<Record<string, number>>;
|
|
429
436
|
/**
|
|
430
437
|
* Element-wise aggregation over an array-of-objects column (`nested` mode).
|
|
431
438
|
*
|
package/dist/index.js
CHANGED
|
@@ -220,6 +220,9 @@ export default class Inibase {
|
|
|
220
220
|
* @param {TableConfig} [config]
|
|
221
221
|
*/
|
|
222
222
|
async createTable(tableName, schema, config) {
|
|
223
|
+
return File.runWithLockStore(() => this._createTable(tableName, schema, config));
|
|
224
|
+
}
|
|
225
|
+
async _createTable(tableName, schema, config) {
|
|
223
226
|
this.validateName(tableName);
|
|
224
227
|
// DDL does not participate in the write-ahead journal: schema surgery
|
|
225
228
|
// inside a transaction would escape the atomic publish/rollback scope.
|
|
@@ -291,6 +294,9 @@ export default class Inibase {
|
|
|
291
294
|
* @param {(TableConfig&{name?: string})} [config]
|
|
292
295
|
*/
|
|
293
296
|
async updateTable(tableName, schema, config) {
|
|
297
|
+
return File.runWithLockStore(() => this._updateTable(tableName, schema, config));
|
|
298
|
+
}
|
|
299
|
+
async _updateTable(tableName, schema, config) {
|
|
294
300
|
this.validateName(tableName);
|
|
295
301
|
// DDL does not participate in the write-ahead journal: schema surgery
|
|
296
302
|
// inside a transaction would escape the atomic publish/rollback scope.
|
|
@@ -2308,6 +2314,9 @@ export default class Inibase {
|
|
|
2308
2314
|
* locked on first touch, in first-touch order.
|
|
2309
2315
|
*/
|
|
2310
2316
|
async begin(tables = []) {
|
|
2317
|
+
return File.runWithLockStore(() => this._begin(tables));
|
|
2318
|
+
}
|
|
2319
|
+
async _begin(tables = []) {
|
|
2311
2320
|
if (this.transaction)
|
|
2312
2321
|
throw this.createError("INVALID_PARAMETERS");
|
|
2313
2322
|
await this.ensureDatabaseTmpDir();
|
|
@@ -2459,6 +2468,12 @@ export default class Inibase {
|
|
|
2459
2468
|
async get(tableName, where, options = {
|
|
2460
2469
|
page: 1,
|
|
2461
2470
|
perPage: 15,
|
|
2471
|
+
}, onlyOne, onlyLinesNumbers, _whereIsLinesNumbers) {
|
|
2472
|
+
return File.runWithLockStore(() => this._get(tableName, where, options, onlyOne, onlyLinesNumbers, _whereIsLinesNumbers));
|
|
2473
|
+
}
|
|
2474
|
+
async _get(tableName, where, options = {
|
|
2475
|
+
page: 1,
|
|
2476
|
+
perPage: 15,
|
|
2462
2477
|
}, onlyOne, onlyLinesNumbers, _whereIsLinesNumbers) {
|
|
2463
2478
|
this.validateName(tableName);
|
|
2464
2479
|
await this.ensureTableRecovered(tableName);
|
|
@@ -2631,9 +2646,13 @@ export default class Inibase {
|
|
|
2631
2646
|
}
|
|
2632
2647
|
else if (((Array.isArray(where) && where.every(Utils.isNumber)) ||
|
|
2633
2648
|
Utils.isNumber(where)) &&
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2649
|
+
// Numeric where is a line number only through the internal
|
|
2650
|
+
// recursion (flag set). An external numeric where is an id — even
|
|
2651
|
+
// on non-decodeID tables, where ids are NOT line numbers (prepend
|
|
2652
|
+
// layout / rows deleted); using them as lines edited the wrong row
|
|
2653
|
+
// and padded the column file (Sep-2026 rename bug). Ids are
|
|
2654
|
+
// resolved by the next branch.
|
|
2655
|
+
_whereIsLinesNumbers) {
|
|
2637
2656
|
// "where" in this case, is the line(s) number(s) and not id(s)
|
|
2638
2657
|
let lineNumbers = where;
|
|
2639
2658
|
if (!Array.isArray(lineNumbers))
|
|
@@ -2647,8 +2666,6 @@ export default class Inibase {
|
|
|
2647
2666
|
RETURN = RETURN[0];
|
|
2648
2667
|
}
|
|
2649
2668
|
else if ((!_whereIsLinesNumbers &&
|
|
2650
|
-
globalConfig[this.databasePath].tables?.get(tableName)?.config
|
|
2651
|
-
.decodeID &&
|
|
2652
2669
|
((Array.isArray(where) && where.every(Utils.isNumber)) ||
|
|
2653
2670
|
Utils.isNumber(where))) ||
|
|
2654
2671
|
(Array.isArray(where) && where.every(Utils.isValidID)) ||
|
|
@@ -2774,6 +2791,9 @@ export default class Inibase {
|
|
|
2774
2791
|
return onlyOne && Array.isArray(RETURN) ? RETURN[0] : RETURN;
|
|
2775
2792
|
}
|
|
2776
2793
|
async post(tableName, data, options, returnPostedData) {
|
|
2794
|
+
return File.runWithLockStore(() => this._post(tableName, data, options, returnPostedData));
|
|
2795
|
+
}
|
|
2796
|
+
async _post(tableName, data, options, returnPostedData) {
|
|
2777
2797
|
if (!options)
|
|
2778
2798
|
options = {
|
|
2779
2799
|
page: 1,
|
|
@@ -2871,7 +2891,7 @@ export default class Inibase {
|
|
|
2871
2891
|
// No read-your-writes yet: return the formatted staged rows
|
|
2872
2892
|
// (ids + defaults) instead of a committed-state read.
|
|
2873
2893
|
return (Array.isArray(clonedData) ? clonedData : clonedData);
|
|
2874
|
-
return this.get(tableName, globalConfig[this.databasePath].tables?.get(tableName)?.config.prepend
|
|
2894
|
+
return await this.get(tableName, globalConfig[this.databasePath].tables?.get(tableName)?.config.prepend
|
|
2875
2895
|
? Array.isArray(clonedData)
|
|
2876
2896
|
? clonedData.map((_, index) => index + 1).toReversed()
|
|
2877
2897
|
: 1
|
|
@@ -2896,13 +2916,13 @@ export default class Inibase {
|
|
|
2896
2916
|
// here so nothing leaks.
|
|
2897
2917
|
if (!txnStaged && renameList.length)
|
|
2898
2918
|
await Promise.allSettled(renameList
|
|
2899
|
-
.filter((pair) => Boolean(pair[
|
|
2919
|
+
.filter((pair) => Boolean(pair[0]))
|
|
2900
2920
|
.map(async ([tempPath, _]) => unlink(tempPath)));
|
|
2901
2921
|
}
|
|
2902
2922
|
else {
|
|
2903
2923
|
if (renameList.length)
|
|
2904
2924
|
await Promise.allSettled(renameList
|
|
2905
|
-
.filter((pair) => Boolean(pair[
|
|
2925
|
+
.filter((pair) => Boolean(pair[0]))
|
|
2906
2926
|
.map(async ([tempPath, _]) => unlink(tempPath)));
|
|
2907
2927
|
await File.unlock(join(tablePath, ".tmp"));
|
|
2908
2928
|
}
|
|
@@ -2911,6 +2931,12 @@ export default class Inibase {
|
|
|
2911
2931
|
async put(tableName, data, where, options = {
|
|
2912
2932
|
page: 1,
|
|
2913
2933
|
perPage: 15,
|
|
2934
|
+
}, returnUpdatedData, _whereIsLinesNumbers) {
|
|
2935
|
+
return File.runWithLockStore(() => this._put(tableName, data, where, options, returnUpdatedData, _whereIsLinesNumbers));
|
|
2936
|
+
}
|
|
2937
|
+
async _put(tableName, data, where, options = {
|
|
2938
|
+
page: 1,
|
|
2939
|
+
perPage: 15,
|
|
2914
2940
|
}, returnUpdatedData, _whereIsLinesNumbers) {
|
|
2915
2941
|
const renameList = [];
|
|
2916
2942
|
let txnStaged = false;
|
|
@@ -3001,13 +3027,13 @@ export default class Inibase {
|
|
|
3001
3027
|
// owns them.
|
|
3002
3028
|
if (!txnStaged && renameList.length)
|
|
3003
3029
|
await Promise.allSettled(renameList
|
|
3004
|
-
.filter((pair) => Boolean(pair[
|
|
3030
|
+
.filter((pair) => Boolean(pair[0]))
|
|
3005
3031
|
.map(async ([tempPath, _]) => unlink(tempPath)));
|
|
3006
3032
|
}
|
|
3007
3033
|
else {
|
|
3008
3034
|
if (renameList.length)
|
|
3009
3035
|
await Promise.allSettled(renameList
|
|
3010
|
-
.filter((pair) => Boolean(pair[
|
|
3036
|
+
.filter((pair) => Boolean(pair[0]))
|
|
3011
3037
|
.map(async ([tempPath, _]) => unlink(tempPath)));
|
|
3012
3038
|
await File.unlock(join(tablePath, ".tmp"));
|
|
3013
3039
|
}
|
|
@@ -3015,9 +3041,13 @@ export default class Inibase {
|
|
|
3015
3041
|
}
|
|
3016
3042
|
else if (((Array.isArray(where) && where.every(Utils.isNumber)) ||
|
|
3017
3043
|
Utils.isNumber(where)) &&
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3044
|
+
// Numeric where is a line number only through the internal
|
|
3045
|
+
// recursion (flag set). An external numeric where is an id — even
|
|
3046
|
+
// on non-decodeID tables, where ids are NOT line numbers (prepend
|
|
3047
|
+
// layout / rows deleted); using them as lines edited the wrong row
|
|
3048
|
+
// and padded the column file (Sep-2026 rename bug). Ids are
|
|
3049
|
+
// resolved by the next branch.
|
|
3050
|
+
_whereIsLinesNumbers) {
|
|
3021
3051
|
// "where" in this case, is the line(s) number(s) and not id(s)
|
|
3022
3052
|
await this.validateTableData(tableName, clonedData, true);
|
|
3023
3053
|
clonedData = this.formatData(clonedData, globalConfig[this.databasePath].tables?.get(tableName)?.schema ?? [], true);
|
|
@@ -3080,42 +3110,63 @@ export default class Inibase {
|
|
|
3080
3110
|
if (returnUpdatedData) {
|
|
3081
3111
|
if (this.transaction)
|
|
3082
3112
|
throw this.createError("INVALID_PARAMETERS");
|
|
3083
|
-
|
|
3113
|
+
// AWAITED readback: must complete inside the writer lock. An
|
|
3114
|
+
// async-tail `return this.get(...)` lets the finally below
|
|
3115
|
+
// unlock before the readback's file reads resume.
|
|
3116
|
+
return await this.get(tableName, where, options, !Array.isArray(where), undefined, true);
|
|
3084
3117
|
}
|
|
3085
3118
|
}
|
|
3086
3119
|
finally {
|
|
3087
3120
|
if (this.transaction) {
|
|
3088
3121
|
if (!txnStaged && renameList.length)
|
|
3089
3122
|
await Promise.allSettled(renameList
|
|
3090
|
-
.filter((pair) => Boolean(pair[
|
|
3123
|
+
.filter((pair) => Boolean(pair[0]))
|
|
3091
3124
|
.map(async ([tempPath, _]) => unlink(tempPath)));
|
|
3092
3125
|
}
|
|
3093
3126
|
else {
|
|
3094
3127
|
if (renameList.length)
|
|
3095
3128
|
await Promise.allSettled(renameList
|
|
3096
|
-
.filter((pair) => Boolean(pair[
|
|
3129
|
+
.filter((pair) => Boolean(pair[0]))
|
|
3097
3130
|
.map(async ([tempPath, _]) => unlink(tempPath)));
|
|
3098
3131
|
await File.unlock(join(tablePath, ".tmp"));
|
|
3099
3132
|
}
|
|
3100
3133
|
}
|
|
3101
3134
|
}
|
|
3102
3135
|
else if ((!_whereIsLinesNumbers &&
|
|
3103
|
-
globalConfig[this.databasePath].tables?.get(tableName)?.config
|
|
3104
|
-
.decodeID &&
|
|
3105
3136
|
((Array.isArray(where) && where.every(Utils.isNumber)) ||
|
|
3106
3137
|
Utils.isNumber(where))) ||
|
|
3107
3138
|
(Array.isArray(where) && where.every(Utils.isValidID)) ||
|
|
3108
3139
|
Utils.isValidID(where)) {
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3140
|
+
// Resolve id -> line(s) WHILE holding the writer lock: a post
|
|
3141
|
+
// landing between the resolution and the write would shift every
|
|
3142
|
+
// line of a prepend table and the update would hit the wrong row.
|
|
3143
|
+
// The recursive call re-takes the lock reentrantly (same task), so
|
|
3144
|
+
// the lock is held continuously from resolution to publish.
|
|
3145
|
+
let inlineLock = false;
|
|
3146
|
+
try {
|
|
3147
|
+
if (this.transaction)
|
|
3148
|
+
await this.ensureTxnLock(tableName);
|
|
3149
|
+
else {
|
|
3150
|
+
await File.lock(join(tablePath, ".tmp"));
|
|
3151
|
+
inlineLock = true;
|
|
3152
|
+
}
|
|
3153
|
+
const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
|
|
3154
|
+
if (lineNumbers)
|
|
3155
|
+
return await this.put(tableName, clonedData,
|
|
3156
|
+
// get() with onlyLinesNumbers always returns an array; a
|
|
3157
|
+
// single-id update must keep the scalar so the recursive
|
|
3158
|
+
// line-numbers branch returns a single row (matching the
|
|
3159
|
+
// shape of get(singleId)) instead of a one-element array.
|
|
3160
|
+
!Array.isArray(where) && Array.isArray(lineNumbers)
|
|
3161
|
+
? lineNumbers[0]
|
|
3162
|
+
: lineNumbers, options, returnUpdatedData, true);
|
|
3163
|
+
}
|
|
3164
|
+
finally {
|
|
3165
|
+
// The recursion released its own (reentrant) acquisition;
|
|
3166
|
+
// release ours too (transactions keep the lock until commit).
|
|
3167
|
+
if (inlineLock)
|
|
3168
|
+
await File.unlock(join(tablePath, ".tmp"));
|
|
3169
|
+
}
|
|
3119
3170
|
}
|
|
3120
3171
|
else if (Utils.isObject(where)) {
|
|
3121
3172
|
const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
|
|
@@ -3133,6 +3184,9 @@ export default class Inibase {
|
|
|
3133
3184
|
* @return {boolean | null} {(Promise<boolean | null>)}
|
|
3134
3185
|
*/
|
|
3135
3186
|
async delete(tableName, where, _whereIsLinesNumbers, _cascadeGuard) {
|
|
3187
|
+
return File.runWithLockStore(() => this._delete(tableName, where, _whereIsLinesNumbers, _cascadeGuard));
|
|
3188
|
+
}
|
|
3189
|
+
async _delete(tableName, where, _whereIsLinesNumbers, _cascadeGuard) {
|
|
3136
3190
|
this.validateName(tableName);
|
|
3137
3191
|
if (!this.transaction)
|
|
3138
3192
|
await this.ensureDatabaseRecovered();
|
|
@@ -3181,13 +3235,13 @@ export default class Inibase {
|
|
|
3181
3235
|
if (this.transaction) {
|
|
3182
3236
|
if (!txnStaged && renameList.length)
|
|
3183
3237
|
await Promise.allSettled(renameList
|
|
3184
|
-
.filter((pair) => Boolean(pair[
|
|
3238
|
+
.filter((pair) => Boolean(pair[0]))
|
|
3185
3239
|
.map(async ([tempPath, _]) => unlink(tempPath)));
|
|
3186
3240
|
}
|
|
3187
3241
|
else {
|
|
3188
3242
|
if (renameList.length)
|
|
3189
3243
|
await Promise.allSettled(renameList
|
|
3190
|
-
.filter((pair) => Boolean(pair[
|
|
3244
|
+
.filter((pair) => Boolean(pair[0]))
|
|
3191
3245
|
.map(async ([tempPath, _]) => unlink(tempPath)));
|
|
3192
3246
|
await File.unlock(join(tablePath, ".tmp"));
|
|
3193
3247
|
}
|
|
@@ -3195,9 +3249,13 @@ export default class Inibase {
|
|
|
3195
3249
|
}
|
|
3196
3250
|
if (((Array.isArray(where) && where.every(Utils.isNumber)) ||
|
|
3197
3251
|
Utils.isNumber(where)) &&
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3252
|
+
// Numeric where is a line number only through the internal
|
|
3253
|
+
// recursion (flag set). An external numeric where is an id — even
|
|
3254
|
+
// on non-decodeID tables, where ids are NOT line numbers (prepend
|
|
3255
|
+
// layout / rows deleted); using them as lines edited the wrong row
|
|
3256
|
+
// and padded the column file (Sep-2026 rename bug). Ids are
|
|
3257
|
+
// resolved by the next branch.
|
|
3258
|
+
_whereIsLinesNumbers) {
|
|
3201
3259
|
// "where" in this case, is the line(s) number(s) and not id(s)
|
|
3202
3260
|
const files = (await readdir(tablePath))?.filter((fileName) => fileName.endsWith(this.getFileExtension(tableName)));
|
|
3203
3261
|
if (files.length) {
|
|
@@ -3263,13 +3321,13 @@ export default class Inibase {
|
|
|
3263
3321
|
if (this.transaction) {
|
|
3264
3322
|
if (!txnStaged && renameList.length)
|
|
3265
3323
|
await Promise.allSettled(renameList
|
|
3266
|
-
.filter((pair) => Boolean(pair[
|
|
3324
|
+
.filter((pair) => Boolean(pair[0]))
|
|
3267
3325
|
.map(async ([tempPath, _]) => unlink(tempPath)));
|
|
3268
3326
|
}
|
|
3269
3327
|
else {
|
|
3270
3328
|
if (renameList.length)
|
|
3271
3329
|
await Promise.allSettled(renameList
|
|
3272
|
-
.filter((pair) => Boolean(pair[
|
|
3330
|
+
.filter((pair) => Boolean(pair[0]))
|
|
3273
3331
|
.map(async ([tempPath, _]) => unlink(tempPath)));
|
|
3274
3332
|
await File.unlock(join(tablePath, ".tmp"));
|
|
3275
3333
|
}
|
|
@@ -3277,20 +3335,34 @@ export default class Inibase {
|
|
|
3277
3335
|
}
|
|
3278
3336
|
}
|
|
3279
3337
|
if ((!_whereIsLinesNumbers &&
|
|
3280
|
-
globalConfig[this.databasePath].tables?.get(tableName)?.config
|
|
3281
|
-
.decodeID &&
|
|
3282
3338
|
((Array.isArray(where) && where.every(Utils.isNumber)) ||
|
|
3283
3339
|
Utils.isNumber(where))) ||
|
|
3284
3340
|
(Array.isArray(where) && where.every(Utils.isValidID)) ||
|
|
3285
3341
|
Utils.isValidID(where)) {
|
|
3286
|
-
|
|
3287
|
-
//
|
|
3288
|
-
//
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3342
|
+
// Resolve id -> line(s) WHILE holding the writer lock (see put():
|
|
3343
|
+
// a concurrent post between resolution and delete would shift the
|
|
3344
|
+
// lines of a prepend table and remove the wrong row).
|
|
3345
|
+
let inlineLock = false;
|
|
3346
|
+
try {
|
|
3347
|
+
if (this.transaction)
|
|
3348
|
+
await this.ensureTxnLock(tableName);
|
|
3349
|
+
else {
|
|
3350
|
+
await File.lock(join(tablePath, ".tmp"));
|
|
3351
|
+
inlineLock = true;
|
|
3352
|
+
}
|
|
3353
|
+
const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
|
|
3354
|
+
// Deleting a non-existent id must not fall through to the
|
|
3355
|
+
// "delete all rows" branch (this.delete(_, null, _) would truncate
|
|
3356
|
+
// the whole table), so resolve the id to line numbers first and
|
|
3357
|
+
// only delegate when something actually matched.
|
|
3358
|
+
if (lineNumbers)
|
|
3359
|
+
return await this.delete(tableName, lineNumbers, true, _cascadeGuard ?? new Set());
|
|
3360
|
+
return false;
|
|
3361
|
+
}
|
|
3362
|
+
finally {
|
|
3363
|
+
if (inlineLock)
|
|
3364
|
+
await File.unlock(join(tablePath, ".tmp"));
|
|
3365
|
+
}
|
|
3294
3366
|
}
|
|
3295
3367
|
if (Utils.isObject(where)) {
|
|
3296
3368
|
const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "inibase",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Karim Amahtil",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"benchmark": "./benchmark/run.js",
|
|
89
89
|
"benchmark:durability": "tsx ./benchmark/durability.ts",
|
|
90
90
|
"benchmark:computed": "tsx ./benchmark/computed.ts",
|
|
91
|
-
"test": "tsx ./tests/inibase.test.ts && tsx ./tests/inibase.advanced.test.ts && tsx ./tests/transaction.test.ts && tsx ./tests/durability.test.ts && tsx ./tests/expression.test.ts && tsx ./tests/utils.test.ts && tsx ./tests/sum.nested.test.ts && tsx ./tests/computed.child.test.ts",
|
|
91
|
+
"test": "tsx ./tests/inibase.test.ts && tsx ./tests/inibase.advanced.test.ts && tsx ./tests/transaction.test.ts && tsx ./tests/durability.test.ts && tsx ./tests/expression.test.ts && tsx ./tests/utils.test.ts && tsx ./tests/sum.nested.test.ts && tsx ./tests/computed.child.test.ts && tsx ./tests/hardening.test.ts",
|
|
92
92
|
"test:durability": "tsx ./tests/durability.test.ts",
|
|
93
93
|
"test:transaction": "tsx ./tests/transaction.test.ts",
|
|
94
94
|
"test:utils": "tsx ./tests/utils.test.ts",
|