inibase 2.0.1 → 3.1.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/README.md +211 -2
- package/dist/expression.d.ts +159 -0
- package/dist/expression.js +495 -0
- package/dist/file.d.ts +20 -1
- package/dist/file.js +211 -16
- package/dist/index.d.ts +177 -1
- package/dist/index.js +1414 -150
- package/dist/journal.d.ts +109 -0
- package/dist/journal.js +263 -0
- package/dist/utils.js +54 -0
- package/package.json +7 -2
package/dist/file.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { access, appendFile, copyFile, constants as fsConstants, open, readFile, stat, unlink,
|
|
1
|
+
import { access, appendFile, copyFile, constants as fsConstants, open, readFile, stat, unlink, } from "node:fs/promises";
|
|
2
|
+
import { hostname } from "node:os";
|
|
2
3
|
import { join, resolve } from "node:path";
|
|
3
4
|
import { createInterface } from "node:readline";
|
|
4
5
|
import { Transform } from "node:stream";
|
|
@@ -6,37 +7,231 @@ import { pipeline } from "node:stream/promises";
|
|
|
6
7
|
import { createGunzip, createGzip } from "node:zlib";
|
|
7
8
|
import Inison from "inison";
|
|
8
9
|
import { globalConfig, } from "./index.js";
|
|
10
|
+
import { recover } from "./journal.js";
|
|
9
11
|
import { detectFieldType, isArrayOfObjects, isNumber, isObject, isStringified, isValidID, } from "./utils.js";
|
|
10
12
|
import { compare, decodeID, encodeID, exec, gunzip, gzip, } from "./utils.server.js";
|
|
11
|
-
// Locks older than this are
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
// Locks older than this are candidates for removal. Same-host locks are only
|
|
14
|
+
// stolen when the recorded owner PID is provably dead; foreign-host locks fall
|
|
15
|
+
// back to this TTL (NFS has no reliable cross-host liveness check).
|
|
16
|
+
const DEFAULT_LOCK_TTL_MS = Number(process.env.INIBASE_LOCK_TTL_MS ?? 60_000);
|
|
17
|
+
// Durability knob: `full` (default) fsyncs every temp file, journal entry and
|
|
18
|
+
// directory touch before acknowledging a mutation. `none` skips all fsync
|
|
19
|
+
// calls but keeps the write-ahead journal protocol unchanged, so a *process*
|
|
20
|
+
// crash (page cache survives) still recovers atomically — only power-loss
|
|
21
|
+
// durability is lost. The ACID claim documented in the README holds at `full`.
|
|
22
|
+
const DURABILITY = process.env.INIBASE_DURABILITY ?? "full";
|
|
23
|
+
export const DURABLE = DURABILITY !== "none";
|
|
24
|
+
/** fsync a handle, or no-op when `INIBASE_DURABILITY=none` is set. */
|
|
25
|
+
const maybeSync = async (handle) => {
|
|
26
|
+
if (DURABLE)
|
|
27
|
+
await handle.sync();
|
|
28
|
+
};
|
|
29
|
+
const LOCK_RETRY_MS = 13;
|
|
30
|
+
// Per-process reentrancy: the same process may acquire the same lock file
|
|
31
|
+
// multiple times (e.g. post() -> get() -> sort-cache). Depth 1 is a real
|
|
32
|
+
// filesystem acquisition; deeper acquisitions just bump the counter.
|
|
33
|
+
const lockDepths = new Map();
|
|
34
|
+
const lockFilePathFor = (folderPath, prefix) => join(folderPath, `${prefix ?? ""}.locked`);
|
|
35
|
+
const lockOwnerState = async (lockFilePath) => {
|
|
16
36
|
try {
|
|
17
|
-
|
|
37
|
+
const metadata = JSON.parse(await readFile(lockFilePath, "utf8"));
|
|
38
|
+
if (typeof metadata.pid === "number" && metadata.host === hostname()) {
|
|
39
|
+
try {
|
|
40
|
+
process.kill(metadata.pid, 0);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
return error?.code === "ESRCH" ? "dead" : "unknown";
|
|
44
|
+
}
|
|
45
|
+
return "alive";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// No/invalid metadata (e.g. crash between create and metadata write).
|
|
50
|
+
}
|
|
51
|
+
return "unknown"; // foreign host or unparsable metadata
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* A lock may be stolen when its owner is provably dead on this host
|
|
55
|
+
* (immediately — a same-host crash must not wedge writers or block crash
|
|
56
|
+
* recovery) or, for foreign/unknown owners where no liveness check exists
|
|
57
|
+
* (e.g. NFS), once the recorded lock is older than the TTL.
|
|
58
|
+
*/
|
|
59
|
+
const stealableLock = async (lockFilePath, mtimeMs, ttl) => {
|
|
60
|
+
const state = await lockOwnerState(lockFilePath);
|
|
61
|
+
if (state === "alive")
|
|
62
|
+
return false;
|
|
63
|
+
if (state === "dead")
|
|
64
|
+
return true;
|
|
65
|
+
return Date.now() - mtimeMs > ttl;
|
|
66
|
+
};
|
|
67
|
+
export const lock = async (folderPath, prefix, ttl = DEFAULT_LOCK_TTL_MS) => {
|
|
68
|
+
const lockFilePath = lockFilePathFor(folderPath, prefix);
|
|
69
|
+
const resolvedPath = resolve(lockFilePath);
|
|
70
|
+
const depth = lockDepths.get(resolvedPath);
|
|
71
|
+
if (depth) {
|
|
72
|
+
lockDepths.set(resolvedPath, depth + 1);
|
|
18
73
|
return;
|
|
19
74
|
}
|
|
20
|
-
|
|
21
|
-
|
|
75
|
+
for (;;) {
|
|
76
|
+
try {
|
|
77
|
+
const lockFile = await open(lockFilePath, "wx");
|
|
78
|
+
try {
|
|
79
|
+
await lockFile.writeFile(JSON.stringify({
|
|
80
|
+
pid: process.pid,
|
|
81
|
+
host: hostname(),
|
|
82
|
+
startedAt: Date.now(),
|
|
83
|
+
}));
|
|
84
|
+
await maybeSync(lockFile);
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
await lockFile.close();
|
|
88
|
+
}
|
|
89
|
+
// The global (prefix-less) lock is the writer lock: run crash
|
|
90
|
+
// recovery for this table while we exclusively hold it. Locked
|
|
91
|
+
// calls with a prefix are read-side helpers (e.g. sort cache) and
|
|
92
|
+
// must never roll back an in-flight transaction.
|
|
93
|
+
if (!prefix) {
|
|
94
|
+
try {
|
|
95
|
+
await recover(folderPath);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Recovery is best-effort at lock time; reads retry on
|
|
99
|
+
// torn state, writers re-check before mutating.
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
lockDepths.set(resolvedPath, 1);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
const message = String(error?.message ?? error);
|
|
107
|
+
if (message.split(":")[0] !== "EEXIST")
|
|
108
|
+
throw error;
|
|
22
109
|
const lockStat = await stat(lockFilePath).catch(() => null);
|
|
23
|
-
|
|
110
|
+
// Someone else released the lock between our failed open and the
|
|
111
|
+
// stat: retry immediately instead of counting down the TTL.
|
|
112
|
+
if (!lockStat)
|
|
113
|
+
continue;
|
|
114
|
+
if (await stealableLock(lockFilePath, lockStat.mtimeMs, ttl)) {
|
|
24
115
|
await unlink(lockFilePath).catch(() => { });
|
|
25
|
-
|
|
116
|
+
}
|
|
117
|
+
await new Promise((resolvePromise) => setTimeout(() => resolvePromise(), LOCK_RETRY_MS));
|
|
26
118
|
}
|
|
27
119
|
}
|
|
28
|
-
|
|
29
|
-
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Non-blocking lock acquisition (used by the read path and the open-time
|
|
123
|
+
* recovery sweep). Returns true when the lock was acquired (running crash
|
|
124
|
+
* recovery for prefix-less locks, exactly like `lock`), false when another
|
|
125
|
+
* process holds it. A single stale-lock steal (dead same-host owner, or aged
|
|
126
|
+
* foreign/unknown owner) is attempted so a crashed owner can't wedge readers
|
|
127
|
+
* behind it forever.
|
|
128
|
+
*/
|
|
129
|
+
export const tryLock = async (folderPath, prefix, ttl = DEFAULT_LOCK_TTL_MS) => {
|
|
130
|
+
const lockFilePath = lockFilePathFor(folderPath, prefix);
|
|
131
|
+
const resolvedPath = resolve(lockFilePath);
|
|
132
|
+
const depth = lockDepths.get(resolvedPath);
|
|
133
|
+
if (depth) {
|
|
134
|
+
lockDepths.set(resolvedPath, depth + 1);
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
138
|
+
try {
|
|
139
|
+
const lockFile = await open(lockFilePath, "wx");
|
|
140
|
+
try {
|
|
141
|
+
await lockFile.writeFile(JSON.stringify({
|
|
142
|
+
pid: process.pid,
|
|
143
|
+
host: hostname(),
|
|
144
|
+
startedAt: Date.now(),
|
|
145
|
+
}));
|
|
146
|
+
await maybeSync(lockFile);
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
await lockFile.close();
|
|
150
|
+
}
|
|
151
|
+
if (!prefix) {
|
|
152
|
+
try {
|
|
153
|
+
await recover(folderPath);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// Best-effort at lock time, mirroring `lock`.
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
lockDepths.set(resolvedPath, 1);
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
if (String(error?.message ?? error).split(":")[0] !== "EEXIST")
|
|
164
|
+
throw error;
|
|
165
|
+
const lockStat = await stat(lockFilePath).catch(() => null);
|
|
166
|
+
if (!lockStat)
|
|
167
|
+
continue; // released between open and stat
|
|
168
|
+
if (attempt === 0 &&
|
|
169
|
+
(await stealableLock(lockFilePath, lockStat.mtimeMs, ttl)))
|
|
170
|
+
await unlink(lockFilePath).catch(() => { });
|
|
171
|
+
else
|
|
172
|
+
return false; // genuinely held -> don't wait
|
|
173
|
+
}
|
|
30
174
|
}
|
|
175
|
+
return false;
|
|
31
176
|
};
|
|
32
177
|
export const unlock = async (folderPath, prefix) => {
|
|
178
|
+
const lockFilePath = lockFilePathFor(folderPath, prefix);
|
|
179
|
+
const resolvedPath = resolve(lockFilePath);
|
|
180
|
+
const depth = lockDepths.get(resolvedPath);
|
|
181
|
+
if (depth && depth > 1) {
|
|
182
|
+
lockDepths.set(resolvedPath, depth - 1);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
lockDepths.delete(resolvedPath);
|
|
33
186
|
try {
|
|
34
|
-
await unlink(
|
|
187
|
+
await unlink(lockFilePath);
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
// Already released (stolen by a foreign-host stealer or cleaned up).
|
|
35
191
|
}
|
|
36
|
-
catch { }
|
|
37
192
|
};
|
|
38
193
|
export const write = async (filePath, data) => {
|
|
39
|
-
await
|
|
194
|
+
const handle = await open(filePath, "w");
|
|
195
|
+
try {
|
|
196
|
+
await handle.writeFile(filePath.endsWith(".gz") ? await gzip(data) : data);
|
|
197
|
+
await maybeSync(handle);
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
await handle.close();
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
/**
|
|
204
|
+
* fsync an existing file. Used to flush temp files (written via streams or
|
|
205
|
+
* shell pipelines) before they are renamed into place.
|
|
206
|
+
*/
|
|
207
|
+
export const syncFile = async (filePath) => {
|
|
208
|
+
let handle = null;
|
|
209
|
+
try {
|
|
210
|
+
handle = await open(filePath, "r+");
|
|
211
|
+
await maybeSync(handle);
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// Unsupported filesystem/platform: best effort.
|
|
215
|
+
}
|
|
216
|
+
finally {
|
|
217
|
+
await handle?.close();
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
/**
|
|
221
|
+
* fsync a directory so that renames performed inside it are durable.
|
|
222
|
+
*/
|
|
223
|
+
export const syncDir = async (dirPath) => {
|
|
224
|
+
let handle = null;
|
|
225
|
+
try {
|
|
226
|
+
handle = await open(dirPath, "r");
|
|
227
|
+
await maybeSync(handle);
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
// Directory fsync is not supported on every platform/filesystem.
|
|
231
|
+
}
|
|
232
|
+
finally {
|
|
233
|
+
await handle?.close();
|
|
234
|
+
}
|
|
40
235
|
};
|
|
41
236
|
export const read = async (filePath) => filePath.endsWith(".gz")
|
|
42
237
|
? (await gunzip(await readFile(filePath, "utf8"))).toString()
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import "dotenv/config";
|
|
2
|
+
import { type ComputedFieldSpec } from "./expression.js";
|
|
3
|
+
import { type JournalFileOp } from "./journal.js";
|
|
2
4
|
export interface Data {
|
|
3
5
|
id?: string | number;
|
|
4
6
|
[key: string]: any;
|
|
@@ -15,6 +17,10 @@ export type Field = {
|
|
|
15
17
|
unique?: boolean | number | string;
|
|
16
18
|
children?: FieldType | FieldType[] | Schema;
|
|
17
19
|
regex?: string;
|
|
20
|
+
/** Computed-fields expression (see the README). Either a raw expression
|
|
21
|
+
* string, or the persisted `{ expr, ast }` spec written by Inibase once
|
|
22
|
+
* the expression has been compiled. */
|
|
23
|
+
computed?: string | ComputedFieldSpec;
|
|
18
24
|
};
|
|
19
25
|
export type Schema = Field[];
|
|
20
26
|
export interface Options {
|
|
@@ -33,6 +39,27 @@ export interface TableObject {
|
|
|
33
39
|
schema?: Schema;
|
|
34
40
|
config: TableConfig;
|
|
35
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Per-table state maintained while a database transaction is open: the writer
|
|
44
|
+
* lock is held for the whole transaction, pagination state is staged in
|
|
45
|
+
* memory (the live files only change at commit()), and `staged` collects the
|
|
46
|
+
* journaled ops that commit() publishes (one per table per transaction).
|
|
47
|
+
*/
|
|
48
|
+
export interface TxnTableEntry {
|
|
49
|
+
locked: boolean;
|
|
50
|
+
/** Live pagination path the first staged op builds on. */
|
|
51
|
+
paginationFrom: string;
|
|
52
|
+
/** Staged last-id / row count (chained across ops in the txn). */
|
|
53
|
+
lastId: number;
|
|
54
|
+
total: number;
|
|
55
|
+
staged: {
|
|
56
|
+
ops: JournalFileOp[];
|
|
57
|
+
pagination: {
|
|
58
|
+
from: string;
|
|
59
|
+
to: string;
|
|
60
|
+
} | null;
|
|
61
|
+
}[];
|
|
62
|
+
}
|
|
36
63
|
export type ComparisonOperator = "=" | "!=" | ">" | "<" | ">=" | "<=" | "*" | "!*" | "[]" | "![]";
|
|
37
64
|
export type pageInfo = {
|
|
38
65
|
total?: number;
|
|
@@ -51,7 +78,7 @@ declare global {
|
|
|
51
78
|
entries<T extends object>(o: T): Entries<T>;
|
|
52
79
|
}
|
|
53
80
|
}
|
|
54
|
-
export declare const ERROR_CODES: readonly ["GROUP_UNIQUE", "FIELD_UNIQUE", "FIELD_REQUIRED", "NO_SCHEMA", "TABLE_EMPTY", "INVALID_ID", "INVALID_TYPE", "INVALID_PARAMETERS", "NO_ENV", "TABLE_EXISTS", "TABLE_NOT_EXISTS", "INVALID_REGEX_MATCH", "INVALID_NAME"];
|
|
81
|
+
export declare const ERROR_CODES: readonly ["GROUP_UNIQUE", "FIELD_UNIQUE", "FIELD_REQUIRED", "NO_SCHEMA", "TABLE_EMPTY", "INVALID_ID", "INVALID_TYPE", "INVALID_PARAMETERS", "NO_ENV", "TABLE_EXISTS", "TABLE_NOT_EXISTS", "INVALID_REGEX_MATCH", "INVALID_NAME", "COMPUTED_FIELD_SYNTAX", "COMPUTED_FIELD_UNKNOWN_FIELD", "COMPUTED_FIELD_INVALID_LINK", "COMPUTED_FIELD_INVALID_TARGET", "COMPUTED_FIELD_CONFLICT", "COMPUTED_FIELD_CYCLE", "COMPUTED_FIELD_SETTABLE", "COMPUTED_FIELD_DANGLING_LINK", "COMPUTED_FIELD_ARITHMETIC"];
|
|
55
82
|
export type ErrorCode = (typeof ERROR_CODES)[number];
|
|
56
83
|
export type ErrorLang = "en" | "ar" | "fr" | "es";
|
|
57
84
|
export declare const globalConfig: {
|
|
@@ -78,9 +105,22 @@ export default class Inibase {
|
|
|
78
105
|
* resolve numeric ids to line numbers arithmetically instead of scanning
|
|
79
106
|
* the id file. Set false by any partial row deletion. */
|
|
80
107
|
private idDensity;
|
|
108
|
+
/** Per-table computed-plan cache (null = table has no computed fields).
|
|
109
|
+
* A plan depends only on the table's persisted schema (compiled ASTs are
|
|
110
|
+
* id-based and rename-proof), so it is rebuilt only when the schema
|
|
111
|
+
* changes: see the invalidation in `getTable`'s reload branch,
|
|
112
|
+
* `createTable` and `updateTableLocked`. Bounded by the number of tables. */
|
|
113
|
+
private readonly computedPlanCache;
|
|
81
114
|
private databasePath;
|
|
82
115
|
private uniqueMap;
|
|
83
116
|
private schemaFileExtension;
|
|
117
|
+
/**
|
|
118
|
+
* Open database transaction (see begin/commit/rollback). Holds the
|
|
119
|
+
* database lock (`<db>/.tmp/.locked`) for its whole lifetime and the
|
|
120
|
+
* per-table writer lock of every table it mutates, so mutations stage into
|
|
121
|
+
* the database journal and publish only on commit().
|
|
122
|
+
*/
|
|
123
|
+
private transaction;
|
|
84
124
|
constructor(database: string, mainFolder?: string, language?: ErrorLang);
|
|
85
125
|
createError(name: ErrorCode, variable?: string | number | (string | number)[]): Error;
|
|
86
126
|
private validateName;
|
|
@@ -116,6 +156,7 @@ export default class Inibase {
|
|
|
116
156
|
updateTable(tableName: string, schema?: Schema, config?: TableConfig & {
|
|
117
157
|
name?: string;
|
|
118
158
|
}): Promise<void>;
|
|
159
|
+
private updateTableLocked;
|
|
119
160
|
/**
|
|
120
161
|
* Get table schema and config
|
|
121
162
|
*
|
|
@@ -137,6 +178,57 @@ export default class Inibase {
|
|
|
137
178
|
private joinPathesContents;
|
|
138
179
|
private _processSchemaDataHelper;
|
|
139
180
|
private processSchemaData;
|
|
181
|
+
/**
|
|
182
|
+
* Extract the raw expression from a field's `computed` property (string or
|
|
183
|
+
* persisted `{ expr, ast }` spec).
|
|
184
|
+
*/
|
|
185
|
+
private computedExprOf;
|
|
186
|
+
/**
|
|
187
|
+
* Compile every `computed` expression of a schema (which must already have
|
|
188
|
+
* ids assigned) into its persisted `{ expr, ast }` form, in dependency
|
|
189
|
+
* order. Throws `COMPUTED_FIELD_CYCLE` on cyclic fields. Used by DDL so the
|
|
190
|
+
* on-disk schema always carries compiled ASTs.
|
|
191
|
+
*/
|
|
192
|
+
private compileComputedFields;
|
|
193
|
+
/**
|
|
194
|
+
* Build the evaluation plan (topological order + id index) for a table's
|
|
195
|
+
* computed fields. Throws on cycles or unresolvable expressions.
|
|
196
|
+
*/
|
|
197
|
+
private buildComputedPlan;
|
|
198
|
+
/** Id index of a table's schema (link targets are re-resolved at write
|
|
199
|
+
* time, so renames never retarget a compiled expression). */
|
|
200
|
+
private tableFieldIndex;
|
|
201
|
+
/**
|
|
202
|
+
* Evaluate a batch of (merged) rows against the table's computed fields.
|
|
203
|
+
* Returns `lineNo -> { computedKey -> value }` in dependency order.
|
|
204
|
+
*
|
|
205
|
+
* Batched link-hop reads: when the plan contains any link hop, a dry
|
|
206
|
+
* collection pass records every (table, column, id) triple the rows'
|
|
207
|
+
* expressions need (no file I/O), each distinct triple is then resolved
|
|
208
|
+
* exactly once — deduplicated across rows and fields — and a final pass
|
|
209
|
+
* evaluates against the warm cache. Plans without hops run the single
|
|
210
|
+
* evaluation pass unchanged.
|
|
211
|
+
*/
|
|
212
|
+
private evaluateComputedRows;
|
|
213
|
+
private createLinkReader;
|
|
214
|
+
/** Evaluate every computed field of one row (topological order) and merge
|
|
215
|
+
* the results back into the row so dependent fields see them. */
|
|
216
|
+
private evaluateRowComputed;
|
|
217
|
+
private evaluateNode;
|
|
218
|
+
/** Evaluate a path (`ids`, dot-separated link hops) against the current
|
|
219
|
+
* frame. Returns `null` when an intermediate link value is missing. */
|
|
220
|
+
private evaluatePath;
|
|
221
|
+
/** Id index lookup with a shared per-evaluation cache. */
|
|
222
|
+
private indexFor;
|
|
223
|
+
/** Read a single column of one linked row via `get`, or null when the
|
|
224
|
+
* row does not exist (dangling link). */
|
|
225
|
+
private readLinkedRow;
|
|
226
|
+
private coerceNumber;
|
|
227
|
+
private applyBinaryOp;
|
|
228
|
+
private aggregate;
|
|
229
|
+
/** Merge evaluated per-line computed values into a `pathesContents` map
|
|
230
|
+
* as line-numbered replace records (encoded cells). */
|
|
231
|
+
private mergeComputedLineRecords;
|
|
140
232
|
private isSimpleField;
|
|
141
233
|
private processSimpleField;
|
|
142
234
|
/**
|
|
@@ -162,6 +254,89 @@ export default class Inibase {
|
|
|
162
254
|
* @param {string} tableName
|
|
163
255
|
*/
|
|
164
256
|
clearCache(tableName: string): Promise<void>;
|
|
257
|
+
/**
|
|
258
|
+
* Commit a multi-file mutation crash-atomically:
|
|
259
|
+
* 1. fsync every freshly-written temp file;
|
|
260
|
+
* 2. write the journal `begin` entry and fsync it;
|
|
261
|
+
* 3. rename the pagination metadata file first (atomic publication point:
|
|
262
|
+
* the row count flips in a single rename, which is what lock-free
|
|
263
|
+
* readers observe) and then swap each live file aside (backup) and
|
|
264
|
+
* rename the temp into place;
|
|
265
|
+
* 4. write the journal `commit` marker and fsync it;
|
|
266
|
+
* 5. discard backups/temps + the journal, fsync the directories.
|
|
267
|
+
*
|
|
268
|
+
* On any failure before `commit`, the journal is rolled back so the table
|
|
269
|
+
* is left exactly as it was. `renameList` entries are [tempPath, livePath]
|
|
270
|
+
* pairs; a null tempPath means a pure removal (live file taken out).
|
|
271
|
+
*/
|
|
272
|
+
private commitFiles;
|
|
273
|
+
/**
|
|
274
|
+
* Runs crash recovery for a table (and any crashed database transaction)
|
|
275
|
+
* before a read. Mutation paths get the same guarantee implicitly (the
|
|
276
|
+
* writer lock runs recovery on acquire); reads call this explicitly
|
|
277
|
+
* because they never take the table lock.
|
|
278
|
+
*/
|
|
279
|
+
private ensureTableRecovered;
|
|
280
|
+
/**
|
|
281
|
+
* Blocking database-journal recovery, used by mutation paths (writers are
|
|
282
|
+
* serialized with live transactions on the database lock anyway).
|
|
283
|
+
*/
|
|
284
|
+
private ensureDatabaseRecovered;
|
|
285
|
+
private ensureDatabaseTmpDir;
|
|
286
|
+
/** Staged per-table entry of the open transaction, or null when none. */
|
|
287
|
+
private txnTableEntry;
|
|
288
|
+
/** Lock a table for the open transaction (idempotent per transaction). */
|
|
289
|
+
private ensureTxnLock;
|
|
290
|
+
/**
|
|
291
|
+
* Resolve the pagination state a DML op should build on. Outside a
|
|
292
|
+
* transaction this reads the live pagination file (as before); inside a
|
|
293
|
+
* transaction the first touch reads it once and the entry keeps the staged
|
|
294
|
+
* id/count so chained ops (guarded to one per table) and commit() stay
|
|
295
|
+
* consistent without publishing anything early.
|
|
296
|
+
*/
|
|
297
|
+
private resolvePagination;
|
|
298
|
+
/**
|
|
299
|
+
* Stage one table mutation into the open transaction: fsync its temps and
|
|
300
|
+
* append an `op` entry to the database journal (no live file is touched;
|
|
301
|
+
* commit() performs the actual renames). One staged mutation per table per
|
|
302
|
+
* transaction (multi-table atomicity; a second touch of the same table
|
|
303
|
+
* would need read-your-writes composition).
|
|
304
|
+
*/
|
|
305
|
+
private stageTxnOp;
|
|
306
|
+
/**
|
|
307
|
+
* Begin a database transaction. Mutations issued while the transaction is
|
|
308
|
+
* open (post/put/delete, including cascade deletes) are staged into the
|
|
309
|
+
* database journal and published atomically at commit(); rollback()
|
|
310
|
+
* discards them without touching any live file.
|
|
311
|
+
*
|
|
312
|
+
* @param tables Optional table names to pre-lock at begin() in sorted
|
|
313
|
+
* order (the deadlock-free way to span tables). Tables not listed are
|
|
314
|
+
* locked on first touch, in first-touch order.
|
|
315
|
+
*/
|
|
316
|
+
begin(tables?: string[]): Promise<void>;
|
|
317
|
+
/**
|
|
318
|
+
* Publish every staged mutation atomically: per table (sorted), the
|
|
319
|
+
* pagination rename comes first (the atomic publication point readers
|
|
320
|
+
* observe) and then live->backup + tmp->live swaps, before a single fsynced
|
|
321
|
+
* `commit` marker makes the whole transaction durable. A crash at any
|
|
322
|
+
* point is recovered by the journal rule (no marker -> roll back all
|
|
323
|
+
* tables, marker -> roll forward all tables).
|
|
324
|
+
*/
|
|
325
|
+
commit(): Promise<void>;
|
|
326
|
+
/**
|
|
327
|
+
* Discard the open transaction: temps and the journal are removed and no
|
|
328
|
+
* live file is touched (nothing is published before commit()).
|
|
329
|
+
*/
|
|
330
|
+
rollback(): Promise<void>;
|
|
331
|
+
/**
|
|
332
|
+
* Snapshot the identity (dev:inode:mtime:size) of every column file and
|
|
333
|
+
* the pagination file. Reading data and then re-verifying this snapshot
|
|
334
|
+
* lets lock-free readers detect an in-flight writer commit and retry
|
|
335
|
+
* instead of returning a torn row set.
|
|
336
|
+
*/
|
|
337
|
+
private snapshotTableFiles;
|
|
338
|
+
/** True when every snapshotted file is still present and unchanged. */
|
|
339
|
+
private verifyTableFiles;
|
|
165
340
|
/**
|
|
166
341
|
* Retrieve item(s) from a table
|
|
167
342
|
*
|
|
@@ -177,6 +352,7 @@ export default class Inibase {
|
|
|
177
352
|
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>;
|
|
178
353
|
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>;
|
|
179
354
|
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>;
|
|
355
|
+
private getOnce;
|
|
180
356
|
/**
|
|
181
357
|
* Create new item(s) in a table
|
|
182
358
|
*
|