@dbx-tools/falkor-db 0.0.0-stage → 0.9.43
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/LICENSE +202 -0
- package/README.md +109 -2
- package/index.ts +22 -0
- package/lib/index.d.ts +18 -0
- package/lib/index.js +16 -0
- package/lib/src/falkor.d.ts +60 -0
- package/lib/src/falkor.js +172 -0
- package/lib/src/persistence/manager.d.ts +71 -0
- package/lib/src/persistence/manager.js +286 -0
- package/lib/src/persistence/manifest.d.ts +32 -0
- package/lib/src/persistence/manifest.js +89 -0
- package/lib/src/persistence/redis-info.d.ts +30 -0
- package/lib/src/persistence/redis-info.js +58 -0
- package/lib/src/persistence/snapshot.d.ts +21 -0
- package/lib/src/persistence/snapshot.js +35 -0
- package/lib/src/persistence/volume.d.ts +43 -0
- package/lib/src/persistence/volume.js +58 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/package.json +77 -4
- package/src/falkor.ts +236 -0
- package/src/persistence/manager.ts +322 -0
- package/src/persistence/manifest.ts +113 -0
- package/src/persistence/redis-info.ts +80 -0
- package/src/persistence/snapshot.ts +46 -0
- package/src/persistence/volume.ts +84 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redis persistence metadata parsing and completed-RDB detection.
|
|
3
|
+
*
|
|
4
|
+
* Reuse this module whenever FalkorDB persistence decisions depend on `INFO
|
|
5
|
+
* persistence`; do not infer snapshot completion from filesystem timestamps
|
|
6
|
+
* alone or trigger `BGSAVE` merely to discover whether the graph changed.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import { asyncUtils } from "@dbx-tools/shared-core";
|
|
11
|
+
/** Parse the fields used by the backup manager from `INFO persistence`. */
|
|
12
|
+
export function parsePersistenceInfo(raw) {
|
|
13
|
+
const values = new Map();
|
|
14
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
15
|
+
if (!line || line.startsWith("#"))
|
|
16
|
+
continue;
|
|
17
|
+
const separator = line.indexOf(":");
|
|
18
|
+
if (separator < 0)
|
|
19
|
+
continue;
|
|
20
|
+
values.set(line.slice(0, separator), line.slice(separator + 1));
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
rdbBgSaveInProgress: integer(values, "rdb_bgsave_in_progress") !== 0,
|
|
24
|
+
rdbLastSaveTime: integer(values, "rdb_last_save_time"),
|
|
25
|
+
rdbLastBgSaveStatus: values.get("rdb_last_bgsave_status") ?? "unknown",
|
|
26
|
+
rdbChangesSinceLastSave: integer(values, "rdb_changes_since_last_save"),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** Read and parse the current Redis persistence state. */
|
|
30
|
+
export async function getPersistenceInfo(redis) {
|
|
31
|
+
return parsePersistenceInfo(await redis.info("persistence"));
|
|
32
|
+
}
|
|
33
|
+
/** Wait until a requested background save finishes successfully. */
|
|
34
|
+
export async function waitForBackgroundSave(redis, previousSaveTime, options = {}) {
|
|
35
|
+
let completed;
|
|
36
|
+
for await (const info of asyncUtils.poll(() => getPersistenceInfo(redis), {
|
|
37
|
+
intervalMs: options.intervalMs ?? 250,
|
|
38
|
+
timeoutMs: options.timeoutMs ?? 30_000,
|
|
39
|
+
predicate: (value) => {
|
|
40
|
+
if (!value.rdbBgSaveInProgress && value.rdbLastBgSaveStatus !== "ok") {
|
|
41
|
+
throw new Error(`Redis background save failed: ${value.rdbLastBgSaveStatus}`);
|
|
42
|
+
}
|
|
43
|
+
completed = value;
|
|
44
|
+
return value.rdbBgSaveInProgress || value.rdbLastSaveTime <= previousSaveTime;
|
|
45
|
+
},
|
|
46
|
+
})) {
|
|
47
|
+
completed = info;
|
|
48
|
+
}
|
|
49
|
+
if (!completed || completed.rdbLastSaveTime <= previousSaveTime) {
|
|
50
|
+
throw new Error("Redis background save completed without a newer RDB timestamp");
|
|
51
|
+
}
|
|
52
|
+
return completed;
|
|
53
|
+
}
|
|
54
|
+
function integer(values, key) {
|
|
55
|
+
const value = Number.parseInt(values.get(key) ?? "0", 10);
|
|
56
|
+
return Number.isFinite(value) ? value : 0;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVkaXMtaW5mby5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9wZXJzaXN0ZW5jZS9yZWRpcy1pbmZvLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7OztHQVFHO0FBRUgsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLHdCQUF3QixDQUFDO0FBZ0JwRCwyRUFBMkU7QUFDM0UsTUFBTSxVQUFVLG9CQUFvQixDQUFDLEdBQVc7SUFDOUMsTUFBTSxNQUFNLEdBQUcsSUFBSSxHQUFHLEVBQWtCLENBQUM7SUFDekMsS0FBSyxNQUFNLElBQUksSUFBSSxHQUFHLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxFQUFFLENBQUM7UUFDdEMsSUFBSSxDQUFDLElBQUksSUFBSSxJQUFJLENBQUMsVUFBVSxDQUFDLEdBQUcsQ0FBQztZQUFFLFNBQVM7UUFDNUMsTUFBTSxTQUFTLEdBQUcsSUFBSSxDQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUMsQ0FBQztRQUNwQyxJQUFJLFNBQVMsR0FBRyxDQUFDO1lBQUUsU0FBUztRQUM1QixNQUFNLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQyxFQUFFLFNBQVMsQ0FBQyxFQUFFLElBQUksQ0FBQyxLQUFLLENBQUMsU0FBUyxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFDbEUsQ0FBQztJQUNELE9BQU87UUFDTCxtQkFBbUIsRUFBRSxPQUFPLENBQUMsTUFBTSxFQUFFLHdCQUF3QixDQUFDLEtBQUssQ0FBQztRQUNwRSxlQUFlLEVBQUUsT0FBTyxDQUFDLE1BQU0sRUFBRSxvQkFBb0IsQ0FBQztRQUN0RCxtQkFBbUIsRUFBRSxNQUFNLENBQUMsR0FBRyxDQUFDLHdCQUF3QixDQUFDLElBQUksU0FBUztRQUN0RSx1QkFBdUIsRUFBRSxPQUFPLENBQUMsTUFBTSxFQUFFLDZCQUE2QixDQUFDO0tBQ3hFLENBQUM7QUFDSixDQUFDO0FBRUQsMERBQTBEO0FBQzFELE1BQU0sQ0FBQyxLQUFLLFVBQVUsa0JBQWtCLENBQ3RDLEtBQTZCO0lBRTdCLE9BQU8sb0JBQW9CLENBQUMsTUFBTSxLQUFLLENBQUMsSUFBSSxDQUFDLGFBQWEsQ0FBQyxDQUFDLENBQUM7QUFDL0QsQ0FBQztBQUVELG9FQUFvRTtBQUNwRSxNQUFNLENBQUMsS0FBSyxVQUFVLHFCQUFxQixDQUN6QyxLQUE2QixFQUM3QixnQkFBd0IsRUFDeEIsVUFBdUQsRUFBRTtJQUV6RCxJQUFJLFNBQTJDLENBQUM7SUFDaEQsSUFBSSxLQUFLLEVBQUUsTUFBTSxJQUFJLElBQUksVUFBVSxDQUFDLElBQUksQ0FBQyxHQUFHLEVBQUUsQ0FBQyxrQkFBa0IsQ0FBQyxLQUFLLENBQUMsRUFBRTtRQUN4RSxVQUFVLEVBQUUsT0FBTyxDQUFDLFVBQVUsSUFBSSxHQUFHO1FBQ3JDLFNBQVMsRUFBRSxPQUFPLENBQUMsU0FBUyxJQUFJLE1BQU07UUFDdEMsU0FBUyxFQUFFLENBQUMsS0FBSyxFQUFFLEVBQUU7WUFDbkIsSUFBSSxDQUFDLEtBQUssQ0FBQyxtQkFBbUIsSUFBSSxLQUFLLENBQUMsbUJBQW1CLEtBQUssSUFBSSxFQUFFLENBQUM7Z0JBQ3JFLE1BQU0sSUFBSSxLQUFLLENBQUMsaUNBQWlDLEtBQUssQ0FBQyxtQkFBbUIsRUFBRSxDQUFDLENBQUM7WUFDaEYsQ0FBQztZQUNELFNBQVMsR0FBRyxLQUFLLENBQUM7WUFDbEIsT0FBTyxLQUFLLENBQUMsbUJBQW1CLElBQUksS0FBSyxDQUFDLGVBQWUsSUFBSSxnQkFBZ0IsQ0FBQztRQUNoRixDQUFDO0tBQ0YsQ0FBQyxFQUFFLENBQUM7UUFDSCxTQUFTLEdBQUcsSUFBSSxDQUFDO0lBQ25CLENBQUM7SUFDRCxJQUFJLENBQUMsU0FBUyxJQUFJLFNBQVMsQ0FBQyxlQUFlLElBQUksZ0JBQWdCLEVBQUUsQ0FBQztRQUNoRSxNQUFNLElBQUksS0FBSyxDQUFDLCtEQUErRCxDQUFDLENBQUM7SUFDbkYsQ0FBQztJQUNELE9BQU8sU0FBUyxDQUFDO0FBQ25CLENBQUM7QUFFRCxTQUFTLE9BQU8sQ0FBQyxNQUFtQyxFQUFFLEdBQVc7SUFDL0QsTUFBTSxLQUFLLEdBQUcsTUFBTSxDQUFDLFFBQVEsQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxJQUFJLEdBQUcsRUFBRSxFQUFFLENBQUMsQ0FBQztJQUMxRCxPQUFPLE1BQU0sQ0FBQyxRQUFRLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBQzVDLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvKipcbiAqIFJlZGlzIHBlcnNpc3RlbmNlIG1ldGFkYXRhIHBhcnNpbmcgYW5kIGNvbXBsZXRlZC1SREIgZGV0ZWN0aW9uLlxuICpcbiAqIFJldXNlIHRoaXMgbW9kdWxlIHdoZW5ldmVyIEZhbGtvckRCIHBlcnNpc3RlbmNlIGRlY2lzaW9ucyBkZXBlbmQgb24gYElORk9cbiAqIHBlcnNpc3RlbmNlYDsgZG8gbm90IGluZmVyIHNuYXBzaG90IGNvbXBsZXRpb24gZnJvbSBmaWxlc3lzdGVtIHRpbWVzdGFtcHNcbiAqIGFsb25lIG9yIHRyaWdnZXIgYEJHU0FWRWAgbWVyZWx5IHRvIGRpc2NvdmVyIHdoZXRoZXIgdGhlIGdyYXBoIGNoYW5nZWQuXG4gKlxuICogQG1vZHVsZVxuICovXG5cbmltcG9ydCB7IGFzeW5jVXRpbHMgfSBmcm9tIFwiQGRieC10b29scy9zaGFyZWQtY29yZVwiO1xuXG4vKiogUmVkaXMgcGVyc2lzdGVuY2UgZmllbGRzIHJlcXVpcmVkIGJ5IGR1cmFibGUgc25hcHNob3Qgb3JjaGVzdHJhdGlvbi4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgUmVkaXNQZXJzaXN0ZW5jZUluZm8ge1xuICByZGJCZ1NhdmVJblByb2dyZXNzOiBib29sZWFuO1xuICByZGJMYXN0U2F2ZVRpbWU6IG51bWJlcjtcbiAgcmRiTGFzdEJnU2F2ZVN0YXR1czogc3RyaW5nO1xuICByZGJDaGFuZ2VzU2luY2VMYXN0U2F2ZTogbnVtYmVyO1xufVxuXG4vKiogTWluaW1hbCBSZWRpcyBjbGllbnQgc3VyZmFjZSB1c2VkIGJ5IHRoZSBwZXJzaXN0ZW5jZSBtYW5hZ2VyLiAqL1xuZXhwb3J0IGludGVyZmFjZSBSZWRpc1BlcnNpc3RlbmNlQ2xpZW50IHtcbiAgaW5mbyhzZWN0aW9uOiBcInBlcnNpc3RlbmNlXCIpOiBQcm9taXNlPHN0cmluZz47XG4gIGJnU2F2ZSgpOiBQcm9taXNlPHVua25vd24+O1xufVxuXG4vKiogUGFyc2UgdGhlIGZpZWxkcyB1c2VkIGJ5IHRoZSBiYWNrdXAgbWFuYWdlciBmcm9tIGBJTkZPIHBlcnNpc3RlbmNlYC4gKi9cbmV4cG9ydCBmdW5jdGlvbiBwYXJzZVBlcnNpc3RlbmNlSW5mbyhyYXc6IHN0cmluZyk6IFJlZGlzUGVyc2lzdGVuY2VJbmZvIHtcbiAgY29uc3QgdmFsdWVzID0gbmV3IE1hcDxzdHJpbmcsIHN0cmluZz4oKTtcbiAgZm9yIChjb25zdCBsaW5lIG9mIHJhdy5zcGxpdCgvXFxyP1xcbi8pKSB7XG4gICAgaWYgKCFsaW5lIHx8IGxpbmUuc3RhcnRzV2l0aChcIiNcIikpIGNvbnRpbnVlO1xuICAgIGNvbnN0IHNlcGFyYXRvciA9IGxpbmUuaW5kZXhPZihcIjpcIik7XG4gICAgaWYgKHNlcGFyYXRvciA8IDApIGNvbnRpbnVlO1xuICAgIHZhbHVlcy5zZXQobGluZS5zbGljZSgwLCBzZXBhcmF0b3IpLCBsaW5lLnNsaWNlKHNlcGFyYXRvciArIDEpKTtcbiAgfVxuICByZXR1cm4ge1xuICAgIHJkYkJnU2F2ZUluUHJvZ3Jlc3M6IGludGVnZXIodmFsdWVzLCBcInJkYl9iZ3NhdmVfaW5fcHJvZ3Jlc3NcIikgIT09IDAsXG4gICAgcmRiTGFzdFNhdmVUaW1lOiBpbnRlZ2VyKHZhbHVlcywgXCJyZGJfbGFzdF9zYXZlX3RpbWVcIiksXG4gICAgcmRiTGFzdEJnU2F2ZVN0YXR1czogdmFsdWVzLmdldChcInJkYl9sYXN0X2Jnc2F2ZV9zdGF0dXNcIikgPz8gXCJ1bmtub3duXCIsXG4gICAgcmRiQ2hhbmdlc1NpbmNlTGFzdFNhdmU6IGludGVnZXIodmFsdWVzLCBcInJkYl9jaGFuZ2VzX3NpbmNlX2xhc3Rfc2F2ZVwiKSxcbiAgfTtcbn1cblxuLyoqIFJlYWQgYW5kIHBhcnNlIHRoZSBjdXJyZW50IFJlZGlzIHBlcnNpc3RlbmNlIHN0YXRlLiAqL1xuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGdldFBlcnNpc3RlbmNlSW5mbyhcbiAgcmVkaXM6IFJlZGlzUGVyc2lzdGVuY2VDbGllbnQsXG4pOiBQcm9taXNlPFJlZGlzUGVyc2lzdGVuY2VJbmZvPiB7XG4gIHJldHVybiBwYXJzZVBlcnNpc3RlbmNlSW5mbyhhd2FpdCByZWRpcy5pbmZvKFwicGVyc2lzdGVuY2VcIikpO1xufVxuXG4vKiogV2FpdCB1bnRpbCBhIHJlcXVlc3RlZCBiYWNrZ3JvdW5kIHNhdmUgZmluaXNoZXMgc3VjY2Vzc2Z1bGx5LiAqL1xuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIHdhaXRGb3JCYWNrZ3JvdW5kU2F2ZShcbiAgcmVkaXM6IFJlZGlzUGVyc2lzdGVuY2VDbGllbnQsXG4gIHByZXZpb3VzU2F2ZVRpbWU6IG51bWJlcixcbiAgb3B0aW9uczogeyBpbnRlcnZhbE1zPzogbnVtYmVyOyB0aW1lb3V0TXM/OiBudW1iZXIgfSA9IHt9LFxuKTogUHJvbWlzZTxSZWRpc1BlcnNpc3RlbmNlSW5mbz4ge1xuICBsZXQgY29tcGxldGVkOiBSZWRpc1BlcnNpc3RlbmNlSW5mbyB8IHVuZGVmaW5lZDtcbiAgZm9yIGF3YWl0IChjb25zdCBpbmZvIG9mIGFzeW5jVXRpbHMucG9sbCgoKSA9PiBnZXRQZXJzaXN0ZW5jZUluZm8ocmVkaXMpLCB7XG4gICAgaW50ZXJ2YWxNczogb3B0aW9ucy5pbnRlcnZhbE1zID8/IDI1MCxcbiAgICB0aW1lb3V0TXM6IG9wdGlvbnMudGltZW91dE1zID8/IDMwXzAwMCxcbiAgICBwcmVkaWNhdGU6ICh2YWx1ZSkgPT4ge1xuICAgICAgaWYgKCF2YWx1ZS5yZGJCZ1NhdmVJblByb2dyZXNzICYmIHZhbHVlLnJkYkxhc3RCZ1NhdmVTdGF0dXMgIT09IFwib2tcIikge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYFJlZGlzIGJhY2tncm91bmQgc2F2ZSBmYWlsZWQ6ICR7dmFsdWUucmRiTGFzdEJnU2F2ZVN0YXR1c31gKTtcbiAgICAgIH1cbiAgICAgIGNvbXBsZXRlZCA9IHZhbHVlO1xuICAgICAgcmV0dXJuIHZhbHVlLnJkYkJnU2F2ZUluUHJvZ3Jlc3MgfHwgdmFsdWUucmRiTGFzdFNhdmVUaW1lIDw9IHByZXZpb3VzU2F2ZVRpbWU7XG4gICAgfSxcbiAgfSkpIHtcbiAgICBjb21wbGV0ZWQgPSBpbmZvO1xuICB9XG4gIGlmICghY29tcGxldGVkIHx8IGNvbXBsZXRlZC5yZGJMYXN0U2F2ZVRpbWUgPD0gcHJldmlvdXNTYXZlVGltZSkge1xuICAgIHRocm93IG5ldyBFcnJvcihcIlJlZGlzIGJhY2tncm91bmQgc2F2ZSBjb21wbGV0ZWQgd2l0aG91dCBhIG5ld2VyIFJEQiB0aW1lc3RhbXBcIik7XG4gIH1cbiAgcmV0dXJuIGNvbXBsZXRlZDtcbn1cblxuZnVuY3Rpb24gaW50ZWdlcih2YWx1ZXM6IFJlYWRvbmx5TWFwPHN0cmluZywgc3RyaW5nPiwga2V5OiBzdHJpbmcpOiBudW1iZXIge1xuICBjb25zdCB2YWx1ZSA9IE51bWJlci5wYXJzZUludCh2YWx1ZXMuZ2V0KGtleSkgPz8gXCIwXCIsIDEwKTtcbiAgcmV0dXJuIE51bWJlci5pc0Zpbml0ZSh2YWx1ZSkgPyB2YWx1ZSA6IDA7XG59XG4iXX0=
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Immutable local RDB staging, hashing, and restore verification.
|
|
3
|
+
*
|
|
4
|
+
* The live `dump.rdb` is always copied before upload so a later Redis rename
|
|
5
|
+
* cannot change the bytes being transferred. Reuse these helpers instead of
|
|
6
|
+
* hashing or uploading the live database file directly.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
/** Local immutable snapshot metadata. */
|
|
11
|
+
export interface LocalSnapshot {
|
|
12
|
+
path: string;
|
|
13
|
+
size: number;
|
|
14
|
+
sha256: string;
|
|
15
|
+
}
|
|
16
|
+
/** Copy a live RDB into an immutable staging path and hash the copied bytes. */
|
|
17
|
+
export declare function stageSnapshot(source: string, destination: string): Promise<LocalSnapshot>;
|
|
18
|
+
/** Compute a file's lowercase SHA-256 digest without buffering it in memory. */
|
|
19
|
+
export declare function sha256File(path: string): Promise<string>;
|
|
20
|
+
/** Fail when a restored snapshot does not match the durable manifest. */
|
|
21
|
+
export declare function verifySnapshot(path: string, expectedSha256: string): Promise<void>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Immutable local RDB staging, hashing, and restore verification.
|
|
3
|
+
*
|
|
4
|
+
* The live `dump.rdb` is always copied before upload so a later Redis rename
|
|
5
|
+
* cannot change the bytes being transferred. Reuse these helpers instead of
|
|
6
|
+
* hashing or uploading the live database file directly.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import { createReadStream } from "node:fs";
|
|
12
|
+
import { copyFile, mkdir, stat } from "node:fs/promises";
|
|
13
|
+
import { dirname } from "node:path";
|
|
14
|
+
/** Copy a live RDB into an immutable staging path and hash the copied bytes. */
|
|
15
|
+
export async function stageSnapshot(source, destination) {
|
|
16
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
17
|
+
await copyFile(source, destination);
|
|
18
|
+
const [details, sha256] = await Promise.all([stat(destination), sha256File(destination)]);
|
|
19
|
+
return { path: destination, size: details.size, sha256 };
|
|
20
|
+
}
|
|
21
|
+
/** Compute a file's lowercase SHA-256 digest without buffering it in memory. */
|
|
22
|
+
export async function sha256File(path) {
|
|
23
|
+
const hash = createHash("sha256");
|
|
24
|
+
for await (const chunk of createReadStream(path))
|
|
25
|
+
hash.update(chunk);
|
|
26
|
+
return hash.digest("hex");
|
|
27
|
+
}
|
|
28
|
+
/** Fail when a restored snapshot does not match the durable manifest. */
|
|
29
|
+
export async function verifySnapshot(path, expectedSha256) {
|
|
30
|
+
const actual = await sha256File(path);
|
|
31
|
+
if (actual !== expectedSha256.toLowerCase()) {
|
|
32
|
+
throw new Error(`FalkorDB snapshot checksum mismatch: expected ${expectedSha256}, got ${actual}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic25hcHNob3QuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvcGVyc2lzdGVuY2Uvc25hcHNob3QudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7Ozs7O0dBUUc7QUFFSCxPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0sYUFBYSxDQUFDO0FBQ3pDLE9BQU8sRUFBRSxnQkFBZ0IsRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUMzQyxPQUFPLEVBQUUsUUFBUSxFQUFFLEtBQUssRUFBRSxJQUFJLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUN6RCxPQUFPLEVBQUUsT0FBTyxFQUFFLE1BQU0sV0FBVyxDQUFDO0FBU3BDLGdGQUFnRjtBQUNoRixNQUFNLENBQUMsS0FBSyxVQUFVLGFBQWEsQ0FBQyxNQUFjLEVBQUUsV0FBbUI7SUFDckUsTUFBTSxLQUFLLENBQUMsT0FBTyxDQUFDLFdBQVcsQ0FBQyxFQUFFLEVBQUUsU0FBUyxFQUFFLElBQUksRUFBRSxDQUFDLENBQUM7SUFDdkQsTUFBTSxRQUFRLENBQUMsTUFBTSxFQUFFLFdBQVcsQ0FBQyxDQUFDO0lBQ3BDLE1BQU0sQ0FBQyxPQUFPLEVBQUUsTUFBTSxDQUFDLEdBQUcsTUFBTSxPQUFPLENBQUMsR0FBRyxDQUFDLENBQUMsSUFBSSxDQUFDLFdBQVcsQ0FBQyxFQUFFLFVBQVUsQ0FBQyxXQUFXLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFDMUYsT0FBTyxFQUFFLElBQUksRUFBRSxXQUFXLEVBQUUsSUFBSSxFQUFFLE9BQU8sQ0FBQyxJQUFJLEVBQUUsTUFBTSxFQUFFLENBQUM7QUFDM0QsQ0FBQztBQUVELGdGQUFnRjtBQUNoRixNQUFNLENBQUMsS0FBSyxVQUFVLFVBQVUsQ0FBQyxJQUFZO0lBQzNDLE1BQU0sSUFBSSxHQUFHLFVBQVUsQ0FBQyxRQUFRLENBQUMsQ0FBQztJQUNsQyxJQUFJLEtBQUssRUFBRSxNQUFNLEtBQUssSUFBSSxnQkFBZ0IsQ0FBQyxJQUFJLENBQUM7UUFBRSxJQUFJLENBQUMsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDO0lBQ3JFLE9BQU8sSUFBSSxDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQztBQUM1QixDQUFDO0FBRUQseUVBQXlFO0FBQ3pFLE1BQU0sQ0FBQyxLQUFLLFVBQVUsY0FBYyxDQUFDLElBQVksRUFBRSxjQUFzQjtJQUN2RSxNQUFNLE1BQU0sR0FBRyxNQUFNLFVBQVUsQ0FBQyxJQUFJLENBQUMsQ0FBQztJQUN0QyxJQUFJLE1BQU0sS0FBSyxjQUFjLENBQUMsV0FBVyxFQUFFLEVBQUUsQ0FBQztRQUM1QyxNQUFNLElBQUksS0FBSyxDQUNiLGlEQUFpRCxjQUFjLFNBQVMsTUFBTSxFQUFFLENBQ2pGLENBQUM7SUFDSixDQUFDO0FBQ0gsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogSW1tdXRhYmxlIGxvY2FsIFJEQiBzdGFnaW5nLCBoYXNoaW5nLCBhbmQgcmVzdG9yZSB2ZXJpZmljYXRpb24uXG4gKlxuICogVGhlIGxpdmUgYGR1bXAucmRiYCBpcyBhbHdheXMgY29waWVkIGJlZm9yZSB1cGxvYWQgc28gYSBsYXRlciBSZWRpcyByZW5hbWVcbiAqIGNhbm5vdCBjaGFuZ2UgdGhlIGJ5dGVzIGJlaW5nIHRyYW5zZmVycmVkLiBSZXVzZSB0aGVzZSBoZWxwZXJzIGluc3RlYWQgb2ZcbiAqIGhhc2hpbmcgb3IgdXBsb2FkaW5nIHRoZSBsaXZlIGRhdGFiYXNlIGZpbGUgZGlyZWN0bHkuXG4gKlxuICogQG1vZHVsZVxuICovXG5cbmltcG9ydCB7IGNyZWF0ZUhhc2ggfSBmcm9tIFwibm9kZTpjcnlwdG9cIjtcbmltcG9ydCB7IGNyZWF0ZVJlYWRTdHJlYW0gfSBmcm9tIFwibm9kZTpmc1wiO1xuaW1wb3J0IHsgY29weUZpbGUsIG1rZGlyLCBzdGF0IH0gZnJvbSBcIm5vZGU6ZnMvcHJvbWlzZXNcIjtcbmltcG9ydCB7IGRpcm5hbWUgfSBmcm9tIFwibm9kZTpwYXRoXCI7XG5cbi8qKiBMb2NhbCBpbW11dGFibGUgc25hcHNob3QgbWV0YWRhdGEuICovXG5leHBvcnQgaW50ZXJmYWNlIExvY2FsU25hcHNob3Qge1xuICBwYXRoOiBzdHJpbmc7XG4gIHNpemU6IG51bWJlcjtcbiAgc2hhMjU2OiBzdHJpbmc7XG59XG5cbi8qKiBDb3B5IGEgbGl2ZSBSREIgaW50byBhbiBpbW11dGFibGUgc3RhZ2luZyBwYXRoIGFuZCBoYXNoIHRoZSBjb3BpZWQgYnl0ZXMuICovXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gc3RhZ2VTbmFwc2hvdChzb3VyY2U6IHN0cmluZywgZGVzdGluYXRpb246IHN0cmluZyk6IFByb21pc2U8TG9jYWxTbmFwc2hvdD4ge1xuICBhd2FpdCBta2RpcihkaXJuYW1lKGRlc3RpbmF0aW9uKSwgeyByZWN1cnNpdmU6IHRydWUgfSk7XG4gIGF3YWl0IGNvcHlGaWxlKHNvdXJjZSwgZGVzdGluYXRpb24pO1xuICBjb25zdCBbZGV0YWlscywgc2hhMjU2XSA9IGF3YWl0IFByb21pc2UuYWxsKFtzdGF0KGRlc3RpbmF0aW9uKSwgc2hhMjU2RmlsZShkZXN0aW5hdGlvbildKTtcbiAgcmV0dXJuIHsgcGF0aDogZGVzdGluYXRpb24sIHNpemU6IGRldGFpbHMuc2l6ZSwgc2hhMjU2IH07XG59XG5cbi8qKiBDb21wdXRlIGEgZmlsZSdzIGxvd2VyY2FzZSBTSEEtMjU2IGRpZ2VzdCB3aXRob3V0IGJ1ZmZlcmluZyBpdCBpbiBtZW1vcnkuICovXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gc2hhMjU2RmlsZShwYXRoOiBzdHJpbmcpOiBQcm9taXNlPHN0cmluZz4ge1xuICBjb25zdCBoYXNoID0gY3JlYXRlSGFzaChcInNoYTI1NlwiKTtcbiAgZm9yIGF3YWl0IChjb25zdCBjaHVuayBvZiBjcmVhdGVSZWFkU3RyZWFtKHBhdGgpKSBoYXNoLnVwZGF0ZShjaHVuayk7XG4gIHJldHVybiBoYXNoLmRpZ2VzdChcImhleFwiKTtcbn1cblxuLyoqIEZhaWwgd2hlbiBhIHJlc3RvcmVkIHNuYXBzaG90IGRvZXMgbm90IG1hdGNoIHRoZSBkdXJhYmxlIG1hbmlmZXN0LiAqL1xuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIHZlcmlmeVNuYXBzaG90KHBhdGg6IHN0cmluZywgZXhwZWN0ZWRTaGEyNTY6IHN0cmluZyk6IFByb21pc2U8dm9pZD4ge1xuICBjb25zdCBhY3R1YWwgPSBhd2FpdCBzaGEyNTZGaWxlKHBhdGgpO1xuICBpZiAoYWN0dWFsICE9PSBleHBlY3RlZFNoYTI1Ni50b0xvd2VyQ2FzZSgpKSB7XG4gICAgdGhyb3cgbmV3IEVycm9yKFxuICAgICAgYEZhbGtvckRCIHNuYXBzaG90IGNoZWNrc3VtIG1pc21hdGNoOiBleHBlY3RlZCAke2V4cGVjdGVkU2hhMjU2fSwgZ290ICR7YWN0dWFsfWAsXG4gICAgKTtcbiAgfVxufVxuIl19
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable snapshot storage abstraction and Databricks Volume adapter.
|
|
3
|
+
*
|
|
4
|
+
* Backup policy depends only on {@link VolumeStorage}. The Databricks adapter
|
|
5
|
+
* receives an initialized {@link DatabricksFileSystem}, keeping profile and
|
|
6
|
+
* application authentication in `@dbx-tools/databricks` rather than this
|
|
7
|
+
* package. Other durable stores can implement the same narrow contract.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
import { DatabricksFileSystem } from "@dbx-tools/databricks/databricks-fs";
|
|
12
|
+
/** One entry in durable snapshot storage. */
|
|
13
|
+
export interface VolumeEntry {
|
|
14
|
+
name: string;
|
|
15
|
+
path: string;
|
|
16
|
+
size?: number;
|
|
17
|
+
}
|
|
18
|
+
/** Storage contract used by restore, upload, manifest, and retention policy. */
|
|
19
|
+
export interface VolumeStorage {
|
|
20
|
+
exists(path: string): Promise<boolean>;
|
|
21
|
+
download(remote: string, local: string): Promise<void>;
|
|
22
|
+
upload(local: string, remote: string, options?: {
|
|
23
|
+
overwrite?: boolean;
|
|
24
|
+
}): Promise<void>;
|
|
25
|
+
readJson<T>(path: string): Promise<T | null>;
|
|
26
|
+
writeJson(path: string, value: unknown): Promise<void>;
|
|
27
|
+
list(path: string): Promise<VolumeEntry[]>;
|
|
28
|
+
delete(path: string): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
/** Stream local snapshots to and from a rooted Unity Catalog Volume filesystem. */
|
|
31
|
+
export declare class DatabricksVolumeStorage implements VolumeStorage {
|
|
32
|
+
private readonly fileSystem;
|
|
33
|
+
constructor(fileSystem: DatabricksFileSystem);
|
|
34
|
+
exists(path: string): Promise<boolean>;
|
|
35
|
+
download(remote: string, local: string): Promise<void>;
|
|
36
|
+
upload(local: string, remote: string, options?: {
|
|
37
|
+
overwrite?: boolean;
|
|
38
|
+
}): Promise<void>;
|
|
39
|
+
readJson<T>(path: string): Promise<T | null>;
|
|
40
|
+
writeJson(path: string, value: unknown): Promise<void>;
|
|
41
|
+
list(path: string): Promise<VolumeEntry[]>;
|
|
42
|
+
delete(path: string): Promise<void>;
|
|
43
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable snapshot storage abstraction and Databricks Volume adapter.
|
|
3
|
+
*
|
|
4
|
+
* Backup policy depends only on {@link VolumeStorage}. The Databricks adapter
|
|
5
|
+
* receives an initialized {@link DatabricksFileSystem}, keeping profile and
|
|
6
|
+
* application authentication in `@dbx-tools/databricks` rather than this
|
|
7
|
+
* package. Other durable stores can implement the same narrow contract.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
import { createReadStream, createWriteStream } from "node:fs";
|
|
12
|
+
import { mkdir } from "node:fs/promises";
|
|
13
|
+
import { dirname } from "node:path";
|
|
14
|
+
import { Readable } from "node:stream";
|
|
15
|
+
import { pipeline } from "node:stream/promises";
|
|
16
|
+
/** Stream local snapshots to and from a rooted Unity Catalog Volume filesystem. */
|
|
17
|
+
export class DatabricksVolumeStorage {
|
|
18
|
+
fileSystem;
|
|
19
|
+
constructor(fileSystem) {
|
|
20
|
+
this.fileSystem = fileSystem;
|
|
21
|
+
}
|
|
22
|
+
async exists(path) {
|
|
23
|
+
return this.fileSystem.exists(path);
|
|
24
|
+
}
|
|
25
|
+
async download(remote, local) {
|
|
26
|
+
await mkdir(dirname(local), { recursive: true });
|
|
27
|
+
const source = await this.fileSystem.readStream(remote);
|
|
28
|
+
await pipeline(Readable.fromWeb(source), createWriteStream(local));
|
|
29
|
+
}
|
|
30
|
+
async upload(local, remote, options = {}) {
|
|
31
|
+
const source = Readable.toWeb(createReadStream(local));
|
|
32
|
+
await this.fileSystem.writeStream(remote, source, options);
|
|
33
|
+
}
|
|
34
|
+
async readJson(path) {
|
|
35
|
+
if (!(await this.fileSystem.exists(path)))
|
|
36
|
+
return null;
|
|
37
|
+
const text = await this.fileSystem.readFile(path, { encoding: "utf-8" });
|
|
38
|
+
return JSON.parse(text);
|
|
39
|
+
}
|
|
40
|
+
async writeJson(path, value) {
|
|
41
|
+
await this.fileSystem.writeFile(path, `${JSON.stringify(value, null, 2)}\n`, {
|
|
42
|
+
overwrite: true,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
async list(path) {
|
|
46
|
+
if (!(await this.fileSystem.exists(path)))
|
|
47
|
+
return [];
|
|
48
|
+
return (await this.fileSystem.readdir(path)).map((entry) => ({
|
|
49
|
+
name: entry.name,
|
|
50
|
+
path: `${path.replace(/\/$/, "")}/${entry.name}`,
|
|
51
|
+
size: entry.size,
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
async delete(path) {
|
|
55
|
+
await this.fileSystem.deleteFile(path, { force: true });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidm9sdW1lLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL3BlcnNpc3RlbmNlL3ZvbHVtZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7Ozs7O0dBU0c7QUFFSCxPQUFPLEVBQUUsZ0JBQWdCLEVBQUUsaUJBQWlCLEVBQUUsTUFBTSxTQUFTLENBQUM7QUFDOUQsT0FBTyxFQUFFLEtBQUssRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBQ3pDLE9BQU8sRUFBRSxPQUFPLEVBQUUsTUFBTSxXQUFXLENBQUM7QUFDcEMsT0FBTyxFQUFFLFFBQVEsRUFBRSxNQUFNLGFBQWEsQ0FBQztBQUN2QyxPQUFPLEVBQUUsUUFBUSxFQUFFLE1BQU0sc0JBQXNCLENBQUM7QUFxQmhELG1GQUFtRjtBQUNuRixNQUFNLE9BQU8sdUJBQXVCO0lBQ0w7SUFBN0IsWUFBNkIsVUFBZ0M7UUFBaEMsZUFBVSxHQUFWLFVBQVUsQ0FBc0I7SUFBRyxDQUFDO0lBRWpFLEtBQUssQ0FBQyxNQUFNLENBQUMsSUFBWTtRQUN2QixPQUFPLElBQUksQ0FBQyxVQUFVLENBQUMsTUFBTSxDQUFDLElBQUksQ0FBQyxDQUFDO0lBQ3RDLENBQUM7SUFFRCxLQUFLLENBQUMsUUFBUSxDQUFDLE1BQWMsRUFBRSxLQUFhO1FBQzFDLE1BQU0sS0FBSyxDQUFDLE9BQU8sQ0FBQyxLQUFLLENBQUMsRUFBRSxFQUFFLFNBQVMsRUFBRSxJQUFJLEVBQUUsQ0FBQyxDQUFDO1FBQ2pELE1BQU0sTUFBTSxHQUFHLE1BQU0sSUFBSSxDQUFDLFVBQVUsQ0FBQyxVQUFVLENBQUMsTUFBTSxDQUFDLENBQUM7UUFDeEQsTUFBTSxRQUFRLENBQUMsUUFBUSxDQUFDLE9BQU8sQ0FBQyxNQUFNLENBQUMsRUFBRSxpQkFBaUIsQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDO0lBQ3JFLENBQUM7SUFFRCxLQUFLLENBQUMsTUFBTSxDQUNWLEtBQWEsRUFDYixNQUFjLEVBQ2QsVUFBbUMsRUFBRTtRQUVyQyxNQUFNLE1BQU0sR0FBRyxRQUFRLENBQUMsS0FBSyxDQUFDLGdCQUFnQixDQUFDLEtBQUssQ0FBQyxDQUEwQyxDQUFDO1FBQ2hHLE1BQU0sSUFBSSxDQUFDLFVBQVUsQ0FBQyxXQUFXLENBQUMsTUFBTSxFQUFFLE1BQU0sRUFBRSxPQUFPLENBQUMsQ0FBQztJQUM3RCxDQUFDO0lBRUQsS0FBSyxDQUFDLFFBQVEsQ0FBSSxJQUFZO1FBQzVCLElBQUksQ0FBQyxDQUFDLE1BQU0sSUFBSSxDQUFDLFVBQVUsQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFDLENBQUM7WUFBRSxPQUFPLElBQUksQ0FBQztRQUN2RCxNQUFNLElBQUksR0FBRyxNQUFNLElBQUksQ0FBQyxVQUFVLENBQUMsUUFBUSxDQUFDLElBQUksRUFBRSxFQUFFLFFBQVEsRUFBRSxPQUFPLEVBQUUsQ0FBQyxDQUFDO1FBQ3pFLE9BQU8sSUFBSSxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQU0sQ0FBQztJQUMvQixDQUFDO0lBRUQsS0FBSyxDQUFDLFNBQVMsQ0FBQyxJQUFZLEVBQUUsS0FBYztRQUMxQyxNQUFNLElBQUksQ0FBQyxVQUFVLENBQUMsU0FBUyxDQUFDLElBQUksRUFBRSxHQUFHLElBQUksQ0FBQyxTQUFTLENBQUMsS0FBSyxFQUFFLElBQUksRUFBRSxDQUFDLENBQUMsSUFBSSxFQUFFO1lBQzNFLFNBQVMsRUFBRSxJQUFJO1NBQ2hCLENBQUMsQ0FBQztJQUNMLENBQUM7SUFFRCxLQUFLLENBQUMsSUFBSSxDQUFDLElBQVk7UUFDckIsSUFBSSxDQUFDLENBQUMsTUFBTSxJQUFJLENBQUMsVUFBVSxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsQ0FBQztZQUFFLE9BQU8sRUFBRSxDQUFDO1FBQ3JELE9BQU8sQ0FBQyxNQUFNLElBQUksQ0FBQyxVQUFVLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFDLENBQUMsS0FBSyxFQUFFLEVBQUUsQ0FBQyxDQUFDO1lBQzNELElBQUksRUFBRSxLQUFLLENBQUMsSUFBSTtZQUNoQixJQUFJLEVBQUUsR0FBRyxJQUFJLENBQUMsT0FBTyxDQUFDLEtBQUssRUFBRSxFQUFFLENBQUMsSUFBSSxLQUFLLENBQUMsSUFBSSxFQUFFO1lBQ2hELElBQUksRUFBRSxLQUFLLENBQUMsSUFBSTtTQUNqQixDQUFDLENBQUMsQ0FBQztJQUNOLENBQUM7SUFFRCxLQUFLLENBQUMsTUFBTSxDQUFDLElBQVk7UUFDdkIsTUFBTSxJQUFJLENBQUMsVUFBVSxDQUFDLFVBQVUsQ0FBQyxJQUFJLEVBQUUsRUFBRSxLQUFLLEVBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQztJQUMxRCxDQUFDO0NBQ0YiLCJzb3VyY2VzQ29udGVudCI6WyIvKipcbiAqIER1cmFibGUgc25hcHNob3Qgc3RvcmFnZSBhYnN0cmFjdGlvbiBhbmQgRGF0YWJyaWNrcyBWb2x1bWUgYWRhcHRlci5cbiAqXG4gKiBCYWNrdXAgcG9saWN5IGRlcGVuZHMgb25seSBvbiB7QGxpbmsgVm9sdW1lU3RvcmFnZX0uIFRoZSBEYXRhYnJpY2tzIGFkYXB0ZXJcbiAqIHJlY2VpdmVzIGFuIGluaXRpYWxpemVkIHtAbGluayBEYXRhYnJpY2tzRmlsZVN5c3RlbX0sIGtlZXBpbmcgcHJvZmlsZSBhbmRcbiAqIGFwcGxpY2F0aW9uIGF1dGhlbnRpY2F0aW9uIGluIGBAZGJ4LXRvb2xzL2RhdGFicmlja3NgIHJhdGhlciB0aGFuIHRoaXNcbiAqIHBhY2thZ2UuIE90aGVyIGR1cmFibGUgc3RvcmVzIGNhbiBpbXBsZW1lbnQgdGhlIHNhbWUgbmFycm93IGNvbnRyYWN0LlxuICpcbiAqIEBtb2R1bGVcbiAqL1xuXG5pbXBvcnQgeyBjcmVhdGVSZWFkU3RyZWFtLCBjcmVhdGVXcml0ZVN0cmVhbSB9IGZyb20gXCJub2RlOmZzXCI7XG5pbXBvcnQgeyBta2RpciB9IGZyb20gXCJub2RlOmZzL3Byb21pc2VzXCI7XG5pbXBvcnQgeyBkaXJuYW1lIH0gZnJvbSBcIm5vZGU6cGF0aFwiO1xuaW1wb3J0IHsgUmVhZGFibGUgfSBmcm9tIFwibm9kZTpzdHJlYW1cIjtcbmltcG9ydCB7IHBpcGVsaW5lIH0gZnJvbSBcIm5vZGU6c3RyZWFtL3Byb21pc2VzXCI7XG5pbXBvcnQgeyBEYXRhYnJpY2tzRmlsZVN5c3RlbSB9IGZyb20gXCJAZGJ4LXRvb2xzL2RhdGFicmlja3MvZGF0YWJyaWNrcy1mc1wiO1xuXG4vKiogT25lIGVudHJ5IGluIGR1cmFibGUgc25hcHNob3Qgc3RvcmFnZS4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgVm9sdW1lRW50cnkge1xuICBuYW1lOiBzdHJpbmc7XG4gIHBhdGg6IHN0cmluZztcbiAgc2l6ZT86IG51bWJlcjtcbn1cblxuLyoqIFN0b3JhZ2UgY29udHJhY3QgdXNlZCBieSByZXN0b3JlLCB1cGxvYWQsIG1hbmlmZXN0LCBhbmQgcmV0ZW50aW9uIHBvbGljeS4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgVm9sdW1lU3RvcmFnZSB7XG4gIGV4aXN0cyhwYXRoOiBzdHJpbmcpOiBQcm9taXNlPGJvb2xlYW4+O1xuICBkb3dubG9hZChyZW1vdGU6IHN0cmluZywgbG9jYWw6IHN0cmluZyk6IFByb21pc2U8dm9pZD47XG4gIHVwbG9hZChsb2NhbDogc3RyaW5nLCByZW1vdGU6IHN0cmluZywgb3B0aW9ucz86IHsgb3ZlcndyaXRlPzogYm9vbGVhbiB9KTogUHJvbWlzZTx2b2lkPjtcbiAgcmVhZEpzb248VD4ocGF0aDogc3RyaW5nKTogUHJvbWlzZTxUIHwgbnVsbD47XG4gIHdyaXRlSnNvbihwYXRoOiBzdHJpbmcsIHZhbHVlOiB1bmtub3duKTogUHJvbWlzZTx2b2lkPjtcbiAgbGlzdChwYXRoOiBzdHJpbmcpOiBQcm9taXNlPFZvbHVtZUVudHJ5W10+O1xuICBkZWxldGUocGF0aDogc3RyaW5nKTogUHJvbWlzZTx2b2lkPjtcbn1cblxuLyoqIFN0cmVhbSBsb2NhbCBzbmFwc2hvdHMgdG8gYW5kIGZyb20gYSByb290ZWQgVW5pdHkgQ2F0YWxvZyBWb2x1bWUgZmlsZXN5c3RlbS4gKi9cbmV4cG9ydCBjbGFzcyBEYXRhYnJpY2tzVm9sdW1lU3RvcmFnZSBpbXBsZW1lbnRzIFZvbHVtZVN0b3JhZ2Uge1xuICBjb25zdHJ1Y3Rvcihwcml2YXRlIHJlYWRvbmx5IGZpbGVTeXN0ZW06IERhdGFicmlja3NGaWxlU3lzdGVtKSB7fVxuXG4gIGFzeW5jIGV4aXN0cyhwYXRoOiBzdHJpbmcpOiBQcm9taXNlPGJvb2xlYW4+IHtcbiAgICByZXR1cm4gdGhpcy5maWxlU3lzdGVtLmV4aXN0cyhwYXRoKTtcbiAgfVxuXG4gIGFzeW5jIGRvd25sb2FkKHJlbW90ZTogc3RyaW5nLCBsb2NhbDogc3RyaW5nKTogUHJvbWlzZTx2b2lkPiB7XG4gICAgYXdhaXQgbWtkaXIoZGlybmFtZShsb2NhbCksIHsgcmVjdXJzaXZlOiB0cnVlIH0pO1xuICAgIGNvbnN0IHNvdXJjZSA9IGF3YWl0IHRoaXMuZmlsZVN5c3RlbS5yZWFkU3RyZWFtKHJlbW90ZSk7XG4gICAgYXdhaXQgcGlwZWxpbmUoUmVhZGFibGUuZnJvbVdlYihzb3VyY2UpLCBjcmVhdGVXcml0ZVN0cmVhbShsb2NhbCkpO1xuICB9XG5cbiAgYXN5bmMgdXBsb2FkKFxuICAgIGxvY2FsOiBzdHJpbmcsXG4gICAgcmVtb3RlOiBzdHJpbmcsXG4gICAgb3B0aW9uczogeyBvdmVyd3JpdGU/OiBib29sZWFuIH0gPSB7fSxcbiAgKTogUHJvbWlzZTx2b2lkPiB7XG4gICAgY29uc3Qgc291cmNlID0gUmVhZGFibGUudG9XZWIoY3JlYXRlUmVhZFN0cmVhbShsb2NhbCkpIGFzIGdsb2JhbFRoaXMuUmVhZGFibGVTdHJlYW08VWludDhBcnJheT47XG4gICAgYXdhaXQgdGhpcy5maWxlU3lzdGVtLndyaXRlU3RyZWFtKHJlbW90ZSwgc291cmNlLCBvcHRpb25zKTtcbiAgfVxuXG4gIGFzeW5jIHJlYWRKc29uPFQ+KHBhdGg6IHN0cmluZyk6IFByb21pc2U8VCB8IG51bGw+IHtcbiAgICBpZiAoIShhd2FpdCB0aGlzLmZpbGVTeXN0ZW0uZXhpc3RzKHBhdGgpKSkgcmV0dXJuIG51bGw7XG4gICAgY29uc3QgdGV4dCA9IGF3YWl0IHRoaXMuZmlsZVN5c3RlbS5yZWFkRmlsZShwYXRoLCB7IGVuY29kaW5nOiBcInV0Zi04XCIgfSk7XG4gICAgcmV0dXJuIEpTT04ucGFyc2UodGV4dCkgYXMgVDtcbiAgfVxuXG4gIGFzeW5jIHdyaXRlSnNvbihwYXRoOiBzdHJpbmcsIHZhbHVlOiB1bmtub3duKTogUHJvbWlzZTx2b2lkPiB7XG4gICAgYXdhaXQgdGhpcy5maWxlU3lzdGVtLndyaXRlRmlsZShwYXRoLCBgJHtKU09OLnN0cmluZ2lmeSh2YWx1ZSwgbnVsbCwgMil9XFxuYCwge1xuICAgICAgb3ZlcndyaXRlOiB0cnVlLFxuICAgIH0pO1xuICB9XG5cbiAgYXN5bmMgbGlzdChwYXRoOiBzdHJpbmcpOiBQcm9taXNlPFZvbHVtZUVudHJ5W10+IHtcbiAgICBpZiAoIShhd2FpdCB0aGlzLmZpbGVTeXN0ZW0uZXhpc3RzKHBhdGgpKSkgcmV0dXJuIFtdO1xuICAgIHJldHVybiAoYXdhaXQgdGhpcy5maWxlU3lzdGVtLnJlYWRkaXIocGF0aCkpLm1hcCgoZW50cnkpID0+ICh7XG4gICAgICBuYW1lOiBlbnRyeS5uYW1lLFxuICAgICAgcGF0aDogYCR7cGF0aC5yZXBsYWNlKC9cXC8kLywgXCJcIil9LyR7ZW50cnkubmFtZX1gLFxuICAgICAgc2l6ZTogZW50cnkuc2l6ZSxcbiAgICB9KSk7XG4gIH1cblxuICBhc3luYyBkZWxldGUocGF0aDogc3RyaW5nKTogUHJvbWlzZTx2b2lkPiB7XG4gICAgYXdhaXQgdGhpcy5maWxlU3lzdGVtLmRlbGV0ZUZpbGUocGF0aCwgeyBmb3JjZTogdHJ1ZSB9KTtcbiAgfVxufVxuIl19
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["../src/falkor.ts","../src/persistence/manager.ts","../src/persistence/manifest.ts","../src/persistence/redis-info.ts","../src/persistence/snapshot.ts","../src/persistence/volume.ts","../index.ts"],"version":"5.9.3"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,79 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dbx-tools/falkor-db",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
"description": "Embedded FalkorDBLite with change-aware durable backups to Databricks Volumes",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/reggie-db/dbx-tools.git",
|
|
7
|
+
"directory": "packages/js/node/falkor-db"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "projen build",
|
|
11
|
+
"compile": "projen compile",
|
|
12
|
+
"default": "projen default",
|
|
13
|
+
"package": "projen package",
|
|
14
|
+
"post-compile": "projen post-compile",
|
|
15
|
+
"pre-compile": "projen pre-compile",
|
|
16
|
+
"prepack": "projen prepack",
|
|
17
|
+
"test": "projen test",
|
|
18
|
+
"watch": "projen watch",
|
|
19
|
+
"projen": "projen"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^24.6.0",
|
|
23
|
+
"typescript": "^5.9.3"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@dbx-tools/databricks": "^0.9.43",
|
|
27
|
+
"@dbx-tools/shared-core": "^0.9.43",
|
|
28
|
+
"falkordb": "^6.6.0",
|
|
29
|
+
"falkordblite": "0.3.0",
|
|
30
|
+
"redis": "6.3.0"
|
|
31
|
+
},
|
|
32
|
+
"main": "./lib/index.js",
|
|
33
|
+
"license": "Apache-2.0",
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public",
|
|
36
|
+
"main": "./lib/index.js",
|
|
37
|
+
"types": "./lib/index.d.ts",
|
|
38
|
+
"exports": {
|
|
39
|
+
".": {
|
|
40
|
+
"types": "./lib/index.d.ts",
|
|
41
|
+
"default": "./lib/index.js"
|
|
42
|
+
},
|
|
43
|
+
"./falkor": {
|
|
44
|
+
"types": "./lib/src/falkor.d.ts",
|
|
45
|
+
"default": "./lib/src/falkor.js"
|
|
46
|
+
},
|
|
47
|
+
"./package.json": "./package.json"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"version": "0.9.43",
|
|
51
|
+
"types": "./lib/index.d.ts",
|
|
52
|
+
"dbxToolsConfig": {
|
|
53
|
+
"tags": [
|
|
54
|
+
"node"
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
"type": "module",
|
|
58
|
+
"exports": {
|
|
59
|
+
".": {
|
|
60
|
+
"types": "./lib/index.d.ts",
|
|
61
|
+
"default": "./lib/index.js"
|
|
62
|
+
},
|
|
63
|
+
"./falkor": {
|
|
64
|
+
"types": "./lib/src/falkor.d.ts",
|
|
65
|
+
"default": "./lib/src/falkor.js"
|
|
66
|
+
},
|
|
67
|
+
"./package.json": "./package.json"
|
|
68
|
+
},
|
|
69
|
+
"files": [
|
|
70
|
+
"index.ts",
|
|
71
|
+
"src",
|
|
72
|
+
"lib"
|
|
73
|
+
],
|
|
74
|
+
"optionalDependencies": {
|
|
75
|
+
"@falkordblite/darwin-arm64": "8.2.3-falkordb.4.16.3",
|
|
76
|
+
"@falkordblite/linux-x64": "8.2.3-falkordb.4.16.3"
|
|
77
|
+
},
|
|
78
|
+
"//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"bunx projen\"."
|
|
79
|
+
}
|
package/src/falkor.ts
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Embedded local FalkorDB with optional durable Volume recovery.
|
|
3
|
+
*
|
|
4
|
+
* This module owns the reusable lifecycle: restore before process start, a
|
|
5
|
+
* private Unix socket, Redis change-aware RDB policy, completed-snapshot
|
|
6
|
+
* observation, and `NOSAVE` shutdown. Applications should reuse
|
|
7
|
+
* {@link DurableFalkorDB} rather than spawning Redis, exposing a TCP port, or
|
|
8
|
+
* attaching backup logic around FalkorDBLite themselves.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync } from "node:fs";
|
|
14
|
+
import { createRequire } from "node:module";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import { dirname, join } from "node:path";
|
|
17
|
+
import { log } from "@dbx-tools/shared-core";
|
|
18
|
+
import { FalkorDB as FalkorClient, type Graph } from "falkordb";
|
|
19
|
+
import {
|
|
20
|
+
BinaryManager,
|
|
21
|
+
ConfigGenerator,
|
|
22
|
+
ServerManager,
|
|
23
|
+
unregisterServer,
|
|
24
|
+
type FalkorDBLiteOptions,
|
|
25
|
+
} from "falkordblite";
|
|
26
|
+
import { createClient } from "redis";
|
|
27
|
+
import {
|
|
28
|
+
FalkorPersistenceManager,
|
|
29
|
+
type FalkorPersistenceOptions,
|
|
30
|
+
type FalkorPersistenceStatus,
|
|
31
|
+
} from "./persistence/manager.ts";
|
|
32
|
+
import type { VolumeStorage } from "./persistence/volume.ts";
|
|
33
|
+
|
|
34
|
+
const require = createRequire(import.meta.url);
|
|
35
|
+
const logger = log.logger("falkor-db");
|
|
36
|
+
|
|
37
|
+
/** Configuration for {@link DurableFalkorDB.open}. */
|
|
38
|
+
export interface DurableFalkorDBOptions extends Pick<
|
|
39
|
+
FalkorDBLiteOptions,
|
|
40
|
+
| "redisServerPath"
|
|
41
|
+
| "modulePath"
|
|
42
|
+
| "maxMemory"
|
|
43
|
+
| "logLevel"
|
|
44
|
+
| "logFile"
|
|
45
|
+
| "timeout"
|
|
46
|
+
| "inheritStdio"
|
|
47
|
+
> {
|
|
48
|
+
/** Local active-database directory. Defaults to `FALKORDB_DATA_DIR`. */
|
|
49
|
+
dataDir?: string;
|
|
50
|
+
/** Redis snapshot interval. Defaults to `FALKORDB_SNAPSHOT_SECONDS` or 300. */
|
|
51
|
+
snapshotSeconds?: number;
|
|
52
|
+
/** Minimum writes in an interval. Defaults to `FALKORDB_SNAPSHOT_MIN_CHANGES` or 1. */
|
|
53
|
+
snapshotMinChanges?: number;
|
|
54
|
+
/** Extra redis.conf settings. `save`, TCP, module, and data paths remain owned here. */
|
|
55
|
+
redisConfig?: Readonly<Record<string, string>>;
|
|
56
|
+
/** Optional durable snapshot destination, typically a Databricks Volume. */
|
|
57
|
+
storage?: VolumeStorage;
|
|
58
|
+
/** Durable-backup and shutdown policy. */
|
|
59
|
+
persistence?: Omit<FalkorPersistenceOptions, "dataDir" | "storage">;
|
|
60
|
+
/** Install SIGINT/SIGTERM handlers. Defaults to true. */
|
|
61
|
+
handleSignals?: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Embedded FalkorDB graph client plus durable persistence lifecycle. */
|
|
65
|
+
export class DurableFalkorDB {
|
|
66
|
+
private closing: Promise<void> | undefined;
|
|
67
|
+
private readonly signalHandlers = new Map<NodeJS.Signals, () => void>();
|
|
68
|
+
|
|
69
|
+
private constructor(
|
|
70
|
+
private readonly graphClient: FalkorClient,
|
|
71
|
+
private readonly adminClient: ReturnType<typeof createClient>,
|
|
72
|
+
private readonly server: ServerManager,
|
|
73
|
+
private readonly persistence: FalkorPersistenceManager,
|
|
74
|
+
) {}
|
|
75
|
+
|
|
76
|
+
/** Restore durable state, start FalkorDBLite, and connect through its Unix socket. */
|
|
77
|
+
static async open(options: DurableFalkorDBOptions = {}): Promise<DurableFalkorDB> {
|
|
78
|
+
const dataDir = options.dataDir ?? process.env.FALKORDB_DATA_DIR ?? defaultDataDirectory();
|
|
79
|
+
const snapshotSeconds = positiveInteger(
|
|
80
|
+
options.snapshotSeconds ?? process.env.FALKORDB_SNAPSHOT_SECONDS ?? 300,
|
|
81
|
+
"snapshotSeconds",
|
|
82
|
+
);
|
|
83
|
+
const snapshotMinChanges = positiveInteger(
|
|
84
|
+
options.snapshotMinChanges ?? process.env.FALKORDB_SNAPSHOT_MIN_CHANGES ?? 1,
|
|
85
|
+
"snapshotMinChanges",
|
|
86
|
+
);
|
|
87
|
+
const persistence = new FalkorPersistenceManager({
|
|
88
|
+
...options.persistence,
|
|
89
|
+
dataDir,
|
|
90
|
+
storage: options.storage,
|
|
91
|
+
});
|
|
92
|
+
await persistence.restore();
|
|
93
|
+
|
|
94
|
+
const binaries = resolveBinaries(options);
|
|
95
|
+
const config = new ConfigGenerator({
|
|
96
|
+
dbDir: dataDir,
|
|
97
|
+
falkordbModulePath: binaries.modulePath,
|
|
98
|
+
maxMemory: options.maxMemory,
|
|
99
|
+
logLevel: options.logLevel,
|
|
100
|
+
logFile: options.logFile,
|
|
101
|
+
additionalConfig: {
|
|
102
|
+
...options.redisConfig,
|
|
103
|
+
port: "0",
|
|
104
|
+
save: `${snapshotSeconds} ${snapshotMinChanges}`,
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
const server = new ServerManager({
|
|
108
|
+
redisServerPath: binaries.redisServerPath,
|
|
109
|
+
config: config.generate(),
|
|
110
|
+
socketPath: config.getSocketPath(),
|
|
111
|
+
startupTimeoutMs: options.timeout,
|
|
112
|
+
inheritStdio: options.inheritStdio,
|
|
113
|
+
});
|
|
114
|
+
await server.start();
|
|
115
|
+
unregisterServer(server);
|
|
116
|
+
|
|
117
|
+
let graphClient: FalkorClient | undefined;
|
|
118
|
+
let adminClient: ReturnType<typeof createClient> | undefined;
|
|
119
|
+
try {
|
|
120
|
+
graphClient = await FalkorClient.connect({ socket: { path: server.socketPath } });
|
|
121
|
+
adminClient = createClient({
|
|
122
|
+
socket: { path: server.socketPath, tls: false, reconnectStrategy: false },
|
|
123
|
+
});
|
|
124
|
+
adminClient.on("error", (error) => {
|
|
125
|
+
logger.debug("Redis administration connection error", { error });
|
|
126
|
+
});
|
|
127
|
+
await adminClient.connect();
|
|
128
|
+
await persistence.start(adminClient);
|
|
129
|
+
const database = new DurableFalkorDB(graphClient, adminClient, server, persistence);
|
|
130
|
+
if (options.handleSignals ?? true) database.installSignalHandlers();
|
|
131
|
+
return database;
|
|
132
|
+
} catch (error) {
|
|
133
|
+
persistence.stop();
|
|
134
|
+
adminClient?.destroy();
|
|
135
|
+
await graphClient?.close().catch(() => undefined);
|
|
136
|
+
await server.stop(false);
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Select a graph using the exact `falkordb` package {@link Graph} type. */
|
|
142
|
+
selectGraph(graphId: string): Graph {
|
|
143
|
+
return this.graphClient.selectGraph(graphId);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** List graphs in the embedded database. */
|
|
147
|
+
list(): Promise<string[]> {
|
|
148
|
+
return this.graphClient.list();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Current local/durable persistence health fields. */
|
|
152
|
+
get persistenceStatus(): Readonly<FalkorPersistenceStatus> {
|
|
153
|
+
return this.persistence.status;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Unix socket used by the embedded Redis/FalkorDB process. */
|
|
157
|
+
get socketPath(): string {
|
|
158
|
+
return this.server.socketPath;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Embedded Redis process id, when running. */
|
|
162
|
+
get pid(): number | undefined {
|
|
163
|
+
return this.server.getPid();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Stop polling and terminate with `SHUTDOWN NOSAVE`. */
|
|
167
|
+
close(): Promise<void> {
|
|
168
|
+
this.closing ??= this.closeOnce();
|
|
169
|
+
return this.closing;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private async closeOnce(): Promise<void> {
|
|
173
|
+
this.removeSignalHandlers();
|
|
174
|
+
await this.persistence.prepareShutdown();
|
|
175
|
+
await this.graphClient.close().catch(() => undefined);
|
|
176
|
+
await this.adminClient.close().catch(() => this.adminClient.destroy());
|
|
177
|
+
await this.server.stop(false);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private installSignalHandlers(): void {
|
|
181
|
+
for (const signal of ["SIGINT", "SIGTERM"] as const) {
|
|
182
|
+
const handler = () => {
|
|
183
|
+
void this.close().finally(() => process.kill(process.pid, signal));
|
|
184
|
+
};
|
|
185
|
+
this.signalHandlers.set(signal, handler);
|
|
186
|
+
process.once(signal, handler);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private removeSignalHandlers(): void {
|
|
191
|
+
for (const [signal, handler] of this.signalHandlers) process.off(signal, handler);
|
|
192
|
+
this.signalHandlers.clear();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
interface ResolvedBinaries {
|
|
197
|
+
redisServerPath: string;
|
|
198
|
+
modulePath: string;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function resolveBinaries(options: DurableFalkorDBOptions): ResolvedBinaries {
|
|
202
|
+
if (options.redisServerPath && options.modulePath) {
|
|
203
|
+
return { redisServerPath: options.redisServerPath, modulePath: options.modulePath };
|
|
204
|
+
}
|
|
205
|
+
const platform = BinaryManager.detectPlatform();
|
|
206
|
+
const packageName = `@falkordblite/${platform}`;
|
|
207
|
+
let packageDirectory: string;
|
|
208
|
+
try {
|
|
209
|
+
packageDirectory = dirname(require.resolve(`${packageName}/package.json`));
|
|
210
|
+
} catch (error) {
|
|
211
|
+
throw new Error(
|
|
212
|
+
`Missing ${packageName}; install the platform package or provide redisServerPath and modulePath`,
|
|
213
|
+
{ cause: error },
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
const redisServerPath = options.redisServerPath ?? join(packageDirectory, "bin", "redis-server");
|
|
217
|
+
const modulePath = options.modulePath ?? join(packageDirectory, "bin", "falkordb.so");
|
|
218
|
+
if (!existsSync(redisServerPath) || !existsSync(modulePath)) {
|
|
219
|
+
throw new Error(`FalkorDBLite platform package ${packageName} is missing its bundled binaries`);
|
|
220
|
+
}
|
|
221
|
+
return { redisServerPath, modulePath };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function defaultDataDirectory(): string {
|
|
225
|
+
return join(tmpdir(), process.env.DATABRICKS_APP_NAME ?? "dbx-tools", "falkordb");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function positiveInteger(value: number | string, name: string): number {
|
|
229
|
+
const parsed = typeof value === "number" ? value : Number.parseInt(value, 10);
|
|
230
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1) {
|
|
231
|
+
throw new TypeError(`${name} must be a positive integer`);
|
|
232
|
+
}
|
|
233
|
+
return parsed;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export type { Graph } from "falkordb";
|