@rivus/platform 0.16.2
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 +21 -0
- package/README.md +5 -0
- package/dist/concurrency/index.d.ts +24 -0
- package/dist/concurrency/index.js +41 -0
- package/dist/filesystem/index.d.ts +5 -0
- package/dist/filesystem/index.js +11 -0
- package/dist/identity/index.d.ts +10 -0
- package/dist/identity/index.js +17 -0
- package/dist/persistence/index.d.ts +38 -0
- package/dist/persistence/index.js +140 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PerfectPan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/concurrency/serial-executor.d.ts
|
|
4
|
+
interface SerialExecutor {
|
|
5
|
+
run<T>(operation: () => Promise<T>): Promise<T>;
|
|
6
|
+
}
|
|
7
|
+
declare function createSerialExecutor(): SerialExecutor;
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region src/concurrency/periodic-effect-loop.d.ts
|
|
10
|
+
interface PeriodicEffectLoopOptions {
|
|
11
|
+
readonly intervalMs: number;
|
|
12
|
+
readonly onError?: (error: unknown) => void | Promise<void>;
|
|
13
|
+
readonly run: () => Effect.Effect<unknown, unknown>;
|
|
14
|
+
readonly sleep: (ms: number) => Effect.Effect<void, unknown>;
|
|
15
|
+
}
|
|
16
|
+
interface PeriodicEffectLoop {
|
|
17
|
+
running(): boolean;
|
|
18
|
+
start(): void;
|
|
19
|
+
stop(): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
declare function createEffectLoopDriver(loop: Effect.Effect<unknown, never>): PeriodicEffectLoop;
|
|
22
|
+
declare function createPeriodicEffectLoop(options: PeriodicEffectLoopOptions): PeriodicEffectLoop;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { type PeriodicEffectLoop, type PeriodicEffectLoopOptions, type SerialExecutor, createEffectLoopDriver, createPeriodicEffectLoop, createSerialExecutor };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Effect, Fiber } from "effect";
|
|
2
|
+
//#region src/concurrency/serial-executor.ts
|
|
3
|
+
function createSerialExecutor() {
|
|
4
|
+
let tail = Promise.resolve();
|
|
5
|
+
return { run: (operation) => {
|
|
6
|
+
const result = tail.then(operation, operation);
|
|
7
|
+
tail = result.then(() => void 0, () => void 0);
|
|
8
|
+
return result;
|
|
9
|
+
} };
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/concurrency/periodic-effect-loop.ts
|
|
13
|
+
function createEffectLoopDriver(loop) {
|
|
14
|
+
let running = false;
|
|
15
|
+
let fiber;
|
|
16
|
+
return {
|
|
17
|
+
running: () => running,
|
|
18
|
+
start: () => {
|
|
19
|
+
if (running) return;
|
|
20
|
+
running = true;
|
|
21
|
+
fiber = Effect.runFork(loop);
|
|
22
|
+
},
|
|
23
|
+
stop: async () => {
|
|
24
|
+
running = false;
|
|
25
|
+
const activeFiber = fiber;
|
|
26
|
+
fiber = void 0;
|
|
27
|
+
if (activeFiber) await Effect.runPromise(Fiber.interrupt(activeFiber));
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function createPeriodicEffectLoop(options) {
|
|
32
|
+
const reportError = (error) => {
|
|
33
|
+
const reported = options.onError?.(error);
|
|
34
|
+
return reported instanceof Promise ? Effect.promise(() => reported) : Effect.void;
|
|
35
|
+
};
|
|
36
|
+
const continueAfterReporting = (effect) => effect.pipe(Effect.asVoid, Effect.catchAll((error) => reportError(error)));
|
|
37
|
+
const cycle = Effect.suspend(() => continueAfterReporting(options.run()).pipe(Effect.flatMap(() => continueAfterReporting(options.sleep(options.intervalMs))), Effect.flatMap(() => cycle)));
|
|
38
|
+
return createEffectLoopDriver(cycle);
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
export { createEffectLoopDriver, createPeriodicEffectLoop, createSerialExecutor };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { isAbsolute, relative, sep } from "node:path";
|
|
2
|
+
//#region src/filesystem/path-boundary.ts
|
|
3
|
+
function isPathWithin(root, candidate) {
|
|
4
|
+
const child = relative(root, candidate);
|
|
5
|
+
return child === "" || !child.startsWith(`..${sep}`) && child !== ".." && !isAbsolute(child);
|
|
6
|
+
}
|
|
7
|
+
function assertPathWithin(root, candidate, message) {
|
|
8
|
+
if (!isPathWithin(root, candidate)) throw new Error(message);
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
11
|
+
export { assertPathWithin, isPathWithin };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/identity/stable-id.d.ts
|
|
2
|
+
declare function createStableId(prefix: string, value: unknown): string;
|
|
3
|
+
//#endregion
|
|
4
|
+
//#region src/identity/random-id.d.ts
|
|
5
|
+
declare function createRandomId(): string;
|
|
6
|
+
//#endregion
|
|
7
|
+
//#region src/identity/sha256-digest.d.ts
|
|
8
|
+
declare function createSha256Digest(value: string): string;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { createRandomId, createSha256Digest, createStableId };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
//#region src/identity/stable-id.ts
|
|
3
|
+
function createStableId(prefix, value) {
|
|
4
|
+
return `${prefix}:${createHash("sha256").update(JSON.stringify(value)).digest("hex")}`;
|
|
5
|
+
}
|
|
6
|
+
//#endregion
|
|
7
|
+
//#region src/identity/random-id.ts
|
|
8
|
+
function createRandomId() {
|
|
9
|
+
return randomUUID();
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/identity/sha256-digest.ts
|
|
13
|
+
function createSha256Digest(value) {
|
|
14
|
+
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
export { createRandomId, createSha256Digest, createStableId };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//#region src/persistence/persistence-value.d.ts
|
|
2
|
+
declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
3
|
+
declare function isNodeErrorWithCode(error: unknown, code: string): error is NodeJS.ErrnoException;
|
|
4
|
+
//#endregion
|
|
5
|
+
//#region src/persistence/jsonl/jsonl-snapshot-store.d.ts
|
|
6
|
+
declare class JsonlSnapshotStoreCorrupted extends Error {
|
|
7
|
+
readonly name = "JsonlSnapshotStoreCorrupted";
|
|
8
|
+
}
|
|
9
|
+
interface LoadJsonlSnapshotStoreOptions<T> {
|
|
10
|
+
readonly compactThreshold: number;
|
|
11
|
+
readonly errorLabel: string;
|
|
12
|
+
readonly filePath: string;
|
|
13
|
+
readonly isRecord: (value: unknown) => value is T;
|
|
14
|
+
readonly keyOf: (record: T) => string;
|
|
15
|
+
readonly validateSequence: (previous: T | undefined, record: T, lineNumber: number) => string | undefined;
|
|
16
|
+
readonly version: number;
|
|
17
|
+
}
|
|
18
|
+
declare function loadJsonlSnapshotStore<T>(options: LoadJsonlSnapshotStoreOptions<T>): Promise<ReadonlyArray<T>>;
|
|
19
|
+
declare function appendJsonlRecord<T>(filePath: string, version: number, record: T): Promise<void>;
|
|
20
|
+
declare function writeJsonlSnapshot<T>(filePath: string, version: number, records: ReadonlyArray<T>): Promise<void>;
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/persistence/files/read-persistence-file.d.ts
|
|
23
|
+
declare function readPersistenceFile(filePath: string): Promise<string | undefined>;
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/persistence/files/write-persistence-file.d.ts
|
|
26
|
+
declare function writePersistenceFile(filePath: string, value: unknown): Promise<void>;
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/persistence/files/write-atomic-text-file.d.ts
|
|
29
|
+
interface WriteAtomicTextFileOptions {
|
|
30
|
+
/** Mode for a newly created destination. Existing destination mode wins. */
|
|
31
|
+
readonly mode?: number;
|
|
32
|
+
/** Flush the temporary file and its parent directory before returning. */
|
|
33
|
+
readonly durable?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/** Replace one UTF-8 text file through a same-directory temporary file. */
|
|
36
|
+
declare function writeAtomicTextFile(filePath: string, contents: string, options?: WriteAtomicTextFileOptions): Promise<void>;
|
|
37
|
+
//#endregion
|
|
38
|
+
export { JsonlSnapshotStoreCorrupted, type LoadJsonlSnapshotStoreOptions, type WriteAtomicTextFileOptions, appendJsonlRecord, isNodeErrorWithCode, isRecord, loadJsonlSnapshotStore, readPersistenceFile, writeAtomicTextFile, writeJsonlSnapshot, writePersistenceFile };
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { dirname } from "node:path";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { appendFile, mkdir, open, readFile, rename, stat, truncate, unlink, writeFile } from "node:fs/promises";
|
|
4
|
+
//#region src/persistence/persistence-value.ts
|
|
5
|
+
function isRecord(value) {
|
|
6
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
function isNodeErrorWithCode(error, code) {
|
|
9
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/persistence/files/read-persistence-file.ts
|
|
13
|
+
async function readPersistenceFile(filePath) {
|
|
14
|
+
try {
|
|
15
|
+
return await readFile(filePath, "utf8");
|
|
16
|
+
} catch (error) {
|
|
17
|
+
if (isNodeErrorWithCode(error, "ENOENT")) return void 0;
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/persistence/jsonl/jsonl-snapshot-store.ts
|
|
23
|
+
var JsonlSnapshotStoreCorrupted = class extends Error {
|
|
24
|
+
name = "JsonlSnapshotStoreCorrupted";
|
|
25
|
+
};
|
|
26
|
+
async function loadJsonlSnapshotStore(options) {
|
|
27
|
+
const raw = await readPersistenceFile(options.filePath);
|
|
28
|
+
if (raw === void 0) return [];
|
|
29
|
+
const lines = raw.split("\n");
|
|
30
|
+
const latest = /* @__PURE__ */ new Map();
|
|
31
|
+
let lineCount = 0;
|
|
32
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
33
|
+
const line = lines[index] ?? "";
|
|
34
|
+
if (!line.trim()) continue;
|
|
35
|
+
lineCount += 1;
|
|
36
|
+
let parsed;
|
|
37
|
+
try {
|
|
38
|
+
parsed = JSON.parse(line);
|
|
39
|
+
} catch {
|
|
40
|
+
if (index === lines.length - 1) {
|
|
41
|
+
await truncateAtLine(options.filePath, lines, index);
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
throw new JsonlSnapshotStoreCorrupted(`${options.errorLabel} JSONL at line ${index + 1} is not valid JSON`);
|
|
45
|
+
}
|
|
46
|
+
if (!isRecord(parsed) || parsed.version !== options.version) throw new JsonlSnapshotStoreCorrupted(`unsupported ${options.errorLabel} version at line ${index + 1}`);
|
|
47
|
+
if (isRecord(parsed.snapshot)) {
|
|
48
|
+
if (!Array.isArray(parsed.snapshot.records)) throw new JsonlSnapshotStoreCorrupted(`invalid ${options.errorLabel} snapshot at line ${index + 1}`);
|
|
49
|
+
latest.clear();
|
|
50
|
+
for (const value of parsed.snapshot.records) {
|
|
51
|
+
if (!options.isRecord(value)) throw new JsonlSnapshotStoreCorrupted(`invalid ${options.errorLabel} snapshot at line ${index + 1}`);
|
|
52
|
+
latest.set(options.keyOf(value), structuredClone(value));
|
|
53
|
+
}
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (!options.isRecord(parsed.record)) throw new JsonlSnapshotStoreCorrupted(`invalid ${options.errorLabel} record at line ${index + 1}`);
|
|
57
|
+
const record = parsed.record;
|
|
58
|
+
const previous = latest.get(options.keyOf(record));
|
|
59
|
+
const invalid = options.validateSequence(previous, record, index + 1);
|
|
60
|
+
if (invalid) throw new JsonlSnapshotStoreCorrupted(`invalid ${options.errorLabel} ${invalid} at line ${index + 1}`);
|
|
61
|
+
latest.set(options.keyOf(record), structuredClone(record));
|
|
62
|
+
}
|
|
63
|
+
if (lineCount >= options.compactThreshold) await writeJsonlSnapshot(options.filePath, options.version, [...latest.values()]);
|
|
64
|
+
return [...latest.values()];
|
|
65
|
+
}
|
|
66
|
+
async function appendJsonlRecord(filePath, version, record) {
|
|
67
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
68
|
+
await appendFile(filePath, `${JSON.stringify({
|
|
69
|
+
record,
|
|
70
|
+
version
|
|
71
|
+
})}\n`, "utf8");
|
|
72
|
+
}
|
|
73
|
+
async function writeJsonlSnapshot(filePath, version, records) {
|
|
74
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
75
|
+
await writeFile(filePath, `${JSON.stringify({
|
|
76
|
+
snapshot: { records },
|
|
77
|
+
version
|
|
78
|
+
})}\n`, "utf8");
|
|
79
|
+
}
|
|
80
|
+
async function truncateAtLine(filePath, lines, lineIndex) {
|
|
81
|
+
let offset = 0;
|
|
82
|
+
for (let index = 0; index < lineIndex; index += 1) offset += (lines[index] ?? "").length + 1;
|
|
83
|
+
await truncate(filePath, offset);
|
|
84
|
+
}
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region src/persistence/files/write-atomic-text-file.ts
|
|
87
|
+
/** Replace one UTF-8 text file through a same-directory temporary file. */
|
|
88
|
+
async function writeAtomicTextFile(filePath, contents, options = {}) {
|
|
89
|
+
const directory = dirname(filePath);
|
|
90
|
+
await mkdir(directory, { recursive: true });
|
|
91
|
+
const existingMode = await readMode(filePath);
|
|
92
|
+
const temporaryPath = `${filePath}.${randomUUID()}.tmp`;
|
|
93
|
+
try {
|
|
94
|
+
await writeFile(temporaryPath, contents, {
|
|
95
|
+
encoding: "utf8",
|
|
96
|
+
flag: "wx",
|
|
97
|
+
mode: existingMode ?? options.mode
|
|
98
|
+
});
|
|
99
|
+
if (options.durable === true) await syncFile(temporaryPath);
|
|
100
|
+
await rename(temporaryPath, filePath);
|
|
101
|
+
if (options.durable === true) await syncDirectory(directory);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
await unlink(temporaryPath).catch(() => void 0);
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async function readMode(filePath) {
|
|
108
|
+
try {
|
|
109
|
+
return (await stat(filePath)).mode & 4095;
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (isMissing(error)) return void 0;
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async function syncFile(filePath) {
|
|
116
|
+
const handle = await open(filePath, "r");
|
|
117
|
+
try {
|
|
118
|
+
await handle.sync();
|
|
119
|
+
} finally {
|
|
120
|
+
await handle.close();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async function syncDirectory(directory) {
|
|
124
|
+
const handle = await open(directory, "r");
|
|
125
|
+
try {
|
|
126
|
+
await handle.sync();
|
|
127
|
+
} finally {
|
|
128
|
+
await handle.close();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function isMissing(error) {
|
|
132
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
133
|
+
}
|
|
134
|
+
//#endregion
|
|
135
|
+
//#region src/persistence/files/write-persistence-file.ts
|
|
136
|
+
async function writePersistenceFile(filePath, value) {
|
|
137
|
+
await writeAtomicTextFile(filePath, `${JSON.stringify(value)}\n`);
|
|
138
|
+
}
|
|
139
|
+
//#endregion
|
|
140
|
+
export { JsonlSnapshotStoreCorrupted, appendJsonlRecord, isNodeErrorWithCode, isRecord, loadJsonlSnapshotStore, readPersistenceFile, writeAtomicTextFile, writeJsonlSnapshot, writePersistenceFile };
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rivus/platform",
|
|
3
|
+
"version": "0.16.2",
|
|
4
|
+
"description": "Shared technical primitives for Rivus packages.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": "^24.11.0"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/PerfectPan/rivus-agent.git",
|
|
13
|
+
"directory": "packages/platform"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/PerfectPan/rivus-agent#readme",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/PerfectPan/rivus-agent/issues"
|
|
18
|
+
},
|
|
19
|
+
"exports": {
|
|
20
|
+
"./concurrency": {
|
|
21
|
+
"types": "./dist/concurrency/index.d.ts",
|
|
22
|
+
"import": "./dist/concurrency/index.js",
|
|
23
|
+
"default": "./dist/concurrency/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./filesystem": {
|
|
26
|
+
"types": "./dist/filesystem/index.d.ts",
|
|
27
|
+
"import": "./dist/filesystem/index.js",
|
|
28
|
+
"default": "./dist/filesystem/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./identity": {
|
|
31
|
+
"types": "./dist/identity/index.d.ts",
|
|
32
|
+
"import": "./dist/identity/index.js",
|
|
33
|
+
"default": "./dist/identity/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./persistence": {
|
|
36
|
+
"types": "./dist/persistence/index.d.ts",
|
|
37
|
+
"import": "./dist/persistence/index.js",
|
|
38
|
+
"default": "./dist/persistence/index.js"
|
|
39
|
+
},
|
|
40
|
+
"./package.json": "./package.json"
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"dist",
|
|
44
|
+
"README.md",
|
|
45
|
+
"LICENSE"
|
|
46
|
+
],
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "vp pack",
|
|
49
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
50
|
+
"test": "vp test --run"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"effect": "^3.21.4"
|
|
54
|
+
},
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"access": "public"
|
|
57
|
+
}
|
|
58
|
+
}
|