@orelvis15/cavs-sdk 1.2.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 ADDED
@@ -0,0 +1,66 @@
1
+ # @orelvis15/cavs-sdk
2
+
3
+ Node.js / TypeScript SDK for CAVS. It loads the same compiled Rust core the
4
+ CAVS CLI uses through a stable C ABI (via [koffi](https://koffi.dev)) — it
5
+ does not shell out to the CLI.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm install @orelvis15/cavs-sdk
11
+ ```
12
+
13
+ Released builds ship the native library in per-platform packages
14
+ (`@orelvis15/cavs-sdk-linux-x64`, `@orelvis15/cavs-sdk-darwin-arm64`, `@orelvis15/cavs-sdk-win32-x64`, …),
15
+ resolved automatically. For local development against a source checkout:
16
+
17
+ ```sh
18
+ npm run native # builds cavs-ffi (release) and stages the lib into native/
19
+ npm test
20
+ ```
21
+
22
+ `CAVS_SDK_LIBRARY=/path/to/libcavs_sdk.dylib` overrides library resolution.
23
+
24
+ ## Quickstart
25
+
26
+ ```ts
27
+ import { CavsClient } from "@orelvis15/cavs-sdk";
28
+
29
+ const cavs = new CavsClient();
30
+ try {
31
+ const preview = await cavs.preview({
32
+ oldPath: "Build_v1",
33
+ newPath: "Build_v2",
34
+ policy: "balanced",
35
+ });
36
+ console.log(preview.recommendedRoute);
37
+ } finally {
38
+ cavs.close();
39
+ }
40
+ ```
41
+
42
+ ## API
43
+
44
+ `CavsClient` exposes `analyze`, `packDirectory`, `preview`, `createPlan`,
45
+ `applyPlan`, `verifyInstall`, `benchmark` and `estimateSavings`. Every method
46
+ returns a `Promise` and accepts an options object:
47
+
48
+ - `onProgress(event)` — stream `ProgressEvent`s. Progress runs the operation
49
+ synchronously (koffi can only invoke JS callbacks on the calling thread),
50
+ so use it for CLIs/scripts rather than hot request paths.
51
+ - `signal` — an `AbortSignal` that cancels the native job (non-progress
52
+ path, which runs off the event loop and polls for completion).
53
+
54
+ Failures reject with a `CavsError` carrying a stable `.code` (see
55
+ `ErrorCode`).
56
+
57
+ ## CI example
58
+
59
+ ```ts
60
+ const preview = await cavs.preview({
61
+ oldPath: process.env.OLD_BUILD!,
62
+ newPath: process.env.NEW_BUILD!,
63
+ routes: [],
64
+ });
65
+ await uploadReport(preview);
66
+ ```
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ // Run a CAVS update preview between two build directories.
4
+ //
5
+ // npm run build && node dist/examples/preview.js --old Build_v1 --new Build_v2
6
+ const index_1 = require("../src/index");
7
+ async function main() {
8
+ const args = process.argv.slice(2);
9
+ const oldPath = valueOf(args, "--old");
10
+ const newPath = valueOf(args, "--new");
11
+ if (!oldPath || !newPath) {
12
+ console.error("usage: preview --old <dir> --new <dir>");
13
+ process.exit(2);
14
+ }
15
+ const cavs = new index_1.CavsClient();
16
+ try {
17
+ const report = await cavs.preview({ oldPath, newPath, policy: "balanced" }, { onProgress: (e) => e.phase && console.error(` [${e.type}] ${e.phase}`) });
18
+ console.log(`Recommended route: ${report.recommendedRoute}`);
19
+ for (const r of report.routes) {
20
+ console.log(` ${r.name.padEnd(16)} ${r.networkBytes} bytes`);
21
+ }
22
+ }
23
+ finally {
24
+ cavs.close();
25
+ }
26
+ }
27
+ function valueOf(args, flag) {
28
+ const i = args.indexOf(flag);
29
+ return i >= 0 ? args[i + 1] : undefined;
30
+ }
31
+ void main();
@@ -0,0 +1,36 @@
1
+ import type { AnalyzeReport, AnalyzeRequest, ApplyPlanRequest, ApplyResult, BenchmarkReport, BenchmarkRequest, CallOptions, CreatePlanRequest, PackDirectoryRequest, PackResult, PlanResult, PreviewReport, PreviewRequest, SavingsReport, SavingsRequest, VerifyRequest, VerifyResult } from "./types";
2
+ /** Version of the native SDK. */
3
+ export declare function version(): string;
4
+ /** Native C ABI contract version. */
5
+ export declare function abiVersion(): string;
6
+ /**
7
+ * A CAVS client. Owns a native context; call {@link close} when done.
8
+ *
9
+ * Calls are serialized (a single native context carries one progress
10
+ * callback slot), so concurrent calls on one client run one at a time —
11
+ * create multiple clients for parallelism.
12
+ */
13
+ export declare class CavsClient {
14
+ private ctx;
15
+ private closed;
16
+ private queue;
17
+ constructor();
18
+ close(): void;
19
+ analyze(req: AnalyzeRequest, opts?: CallOptions): Promise<AnalyzeReport>;
20
+ packDirectory(req: PackDirectoryRequest, opts?: CallOptions): Promise<PackResult>;
21
+ preview(req: PreviewRequest, opts?: CallOptions): Promise<PreviewReport>;
22
+ createPlan(req: CreatePlanRequest, opts?: CallOptions): Promise<PlanResult>;
23
+ applyPlan(req: ApplyPlanRequest, opts?: CallOptions): Promise<ApplyResult>;
24
+ verifyInstall(req: VerifyRequest, opts?: CallOptions): Promise<VerifyResult>;
25
+ benchmark(req: BenchmarkRequest, opts?: CallOptions): Promise<BenchmarkReport>;
26
+ estimateSavings(req: SavingsRequest, opts?: CallOptions): Promise<SavingsReport>;
27
+ /** Serialize calls onto one native context. */
28
+ private call;
29
+ private execute;
30
+ /** Progress path: synchronous native call so callbacks fire on this
31
+ * thread (koffi cannot invoke JS callbacks from a foreign thread). */
32
+ private executeWithProgress;
33
+ /** Non-blocking path: run on a native worker thread, poll on a timer so
34
+ * the event loop stays free; AbortSignal cancels the native job. */
35
+ private executeAsJob;
36
+ }
@@ -0,0 +1,147 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CavsClient = void 0;
4
+ exports.version = version;
5
+ exports.abiVersion = abiVersion;
6
+ const native_1 = require("./native");
7
+ const errors_1 = require("./errors");
8
+ const SCHEMA_VERSION = "1.0";
9
+ const POLL_INTERVAL_MS = 1;
10
+ /** Version of the native SDK. */
11
+ function version() {
12
+ return native_1.native.version();
13
+ }
14
+ /** Native C ABI contract version. */
15
+ function abiVersion() {
16
+ return native_1.native.abiVersion();
17
+ }
18
+ /**
19
+ * A CAVS client. Owns a native context; call {@link close} when done.
20
+ *
21
+ * Calls are serialized (a single native context carries one progress
22
+ * callback slot), so concurrent calls on one client run one at a time —
23
+ * create multiple clients for parallelism.
24
+ */
25
+ class CavsClient {
26
+ ctx;
27
+ closed = false;
28
+ queue = Promise.resolve();
29
+ constructor() {
30
+ this.ctx = native_1.native.contextNew();
31
+ if (!this.ctx) {
32
+ throw new Error("cavs: failed to create native context");
33
+ }
34
+ }
35
+ close() {
36
+ if (this.closed) {
37
+ return;
38
+ }
39
+ this.closed = true;
40
+ native_1.native.contextFree(this.ctx);
41
+ }
42
+ analyze(req, opts) {
43
+ return this.call("analyze", req, opts);
44
+ }
45
+ packDirectory(req, opts) {
46
+ return this.call("packDirectory", req, opts);
47
+ }
48
+ preview(req, opts) {
49
+ return this.call("previewUpdate", req, opts);
50
+ }
51
+ createPlan(req, opts) {
52
+ return this.call("createPlan", req, opts);
53
+ }
54
+ applyPlan(req, opts) {
55
+ return this.call("applyPlan", req, opts);
56
+ }
57
+ verifyInstall(req, opts) {
58
+ return this.call("verifyInstall", req, opts);
59
+ }
60
+ benchmark(req, opts) {
61
+ return this.call("benchmark", req, opts);
62
+ }
63
+ estimateSavings(req, opts) {
64
+ return this.call("estimateSavings", req, opts);
65
+ }
66
+ /** Serialize calls onto one native context. */
67
+ call(op, req, opts) {
68
+ const run = this.queue.then(() => this.execute(op, req, opts));
69
+ // Keep the chain alive even if a call rejects.
70
+ this.queue = run.then(() => undefined, () => undefined);
71
+ return run;
72
+ }
73
+ async execute(op, req, opts) {
74
+ if (this.closed) {
75
+ throw new errors_1.CavsError(errors_1.ErrorCode.InvalidRequest, "client is closed");
76
+ }
77
+ const body = JSON.stringify({ schemaVersion: SCHEMA_VERSION, data: req });
78
+ if (opts?.onProgress) {
79
+ return this.executeWithProgress(op, body, opts.onProgress);
80
+ }
81
+ return this.executeAsJob(op, body, opts?.signal);
82
+ }
83
+ /** Progress path: synchronous native call so callbacks fire on this
84
+ * thread (koffi cannot invoke JS callbacks from a foreign thread). */
85
+ executeWithProgress(op, body, onProgress) {
86
+ const cbPtr = native_1.native.registerProgress((raw) => {
87
+ try {
88
+ onProgress(JSON.parse(raw));
89
+ }
90
+ catch {
91
+ /* a malformed event must not break the operation */
92
+ }
93
+ });
94
+ try {
95
+ native_1.native.setProgress(this.ctx, cbPtr);
96
+ const envelope = native_1.native.executeSync(this.ctx, op, body);
97
+ return parse(envelope);
98
+ }
99
+ finally {
100
+ native_1.native.setProgress(this.ctx, null);
101
+ native_1.native.unregister(cbPtr);
102
+ }
103
+ }
104
+ /** Non-blocking path: run on a native worker thread, poll on a timer so
105
+ * the event loop stays free; AbortSignal cancels the native job. */
106
+ async executeAsJob(op, body, signal) {
107
+ if (signal?.aborted) {
108
+ throw new errors_1.CavsError(errors_1.ErrorCode.Cancelled, "operation aborted before start", true);
109
+ }
110
+ const job = native_1.native.startJob(this.ctx, op, body);
111
+ if (!job) {
112
+ throw new errors_1.CavsError(errors_1.ErrorCode.InvalidRequest, "native library rejected the request");
113
+ }
114
+ let cancelled = false;
115
+ try {
116
+ for (;;) {
117
+ const envelope = native_1.native.pollJob(job);
118
+ if (envelope !== null) {
119
+ if (cancelled) {
120
+ throw new errors_1.CavsError(errors_1.ErrorCode.Cancelled, "operation aborted", true);
121
+ }
122
+ return parse(envelope);
123
+ }
124
+ if (signal?.aborted && !cancelled) {
125
+ native_1.native.cancelJob(job);
126
+ cancelled = true;
127
+ }
128
+ await sleep(POLL_INTERVAL_MS);
129
+ }
130
+ }
131
+ finally {
132
+ native_1.native.freeJob(job);
133
+ }
134
+ }
135
+ }
136
+ exports.CavsClient = CavsClient;
137
+ function parse(envelope) {
138
+ const env = JSON.parse(envelope);
139
+ if (!env.ok) {
140
+ const e = env.error;
141
+ throw new errors_1.CavsError(e?.code ?? "CAVS-E-UNKNOWN", e?.message ?? "operation failed", e?.recoverable ?? false, e?.details ?? {});
142
+ }
143
+ return env.data;
144
+ }
145
+ function sleep(ms) {
146
+ return new Promise((resolve) => setTimeout(resolve, ms));
147
+ }
@@ -0,0 +1,15 @@
1
+ /** A CAVS operation failure carrying the stable engine error code. */
2
+ export declare class CavsError extends Error {
3
+ readonly code: string;
4
+ readonly recoverable: boolean;
5
+ readonly details: Record<string, unknown>;
6
+ constructor(code: string, message: string, recoverable?: boolean, details?: Record<string, unknown>);
7
+ }
8
+ /** Well-known error codes (the engine defines the full set). */
9
+ export declare const ErrorCode: {
10
+ readonly PathNotFound: "CAVS-E-PATH-NOT-FOUND";
11
+ readonly PathTraversal: "CAVS-E-PATH-TRAVERSAL";
12
+ readonly InvalidRequest: "CAVS-E-INVALID-REQUEST";
13
+ readonly UnknownOperation: "CAVS-E-UNKNOWN-OPERATION";
14
+ readonly Cancelled: "CAVS-E-CANCELLED";
15
+ };
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ErrorCode = exports.CavsError = void 0;
4
+ /** A CAVS operation failure carrying the stable engine error code. */
5
+ class CavsError extends Error {
6
+ code;
7
+ recoverable;
8
+ details;
9
+ constructor(code, message, recoverable = false, details = {}) {
10
+ super(message);
11
+ this.name = "CavsError";
12
+ this.code = code;
13
+ this.recoverable = recoverable;
14
+ this.details = details;
15
+ }
16
+ }
17
+ exports.CavsError = CavsError;
18
+ /** Well-known error codes (the engine defines the full set). */
19
+ exports.ErrorCode = {
20
+ PathNotFound: "CAVS-E-PATH-NOT-FOUND",
21
+ PathTraversal: "CAVS-E-PATH-TRAVERSAL",
22
+ InvalidRequest: "CAVS-E-INVALID-REQUEST",
23
+ UnknownOperation: "CAVS-E-UNKNOWN-OPERATION",
24
+ Cancelled: "CAVS-E-CANCELLED",
25
+ };
@@ -0,0 +1,3 @@
1
+ export { CavsClient, version, abiVersion } from "./client";
2
+ export { CavsError, ErrorCode } from "./errors";
3
+ export * from "./types";
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.ErrorCode = exports.CavsError = exports.abiVersion = exports.version = exports.CavsClient = void 0;
18
+ var client_1 = require("./client");
19
+ Object.defineProperty(exports, "CavsClient", { enumerable: true, get: function () { return client_1.CavsClient; } });
20
+ Object.defineProperty(exports, "version", { enumerable: true, get: function () { return client_1.version; } });
21
+ Object.defineProperty(exports, "abiVersion", { enumerable: true, get: function () { return client_1.abiVersion; } });
22
+ var errors_1 = require("./errors");
23
+ Object.defineProperty(exports, "CavsError", { enumerable: true, get: function () { return errors_1.CavsError; } });
24
+ Object.defineProperty(exports, "ErrorCode", { enumerable: true, get: function () { return errors_1.ErrorCode; } });
25
+ __exportStar(require("./types"), exports);
@@ -0,0 +1,21 @@
1
+ import koffi from "koffi";
2
+ export type Pointer = unknown;
3
+ export type ProgressHandle = koffi.IKoffiRegisteredCallback;
4
+ export declare const native: {
5
+ version(): string;
6
+ abiVersion(): string;
7
+ capabilitiesJson(): string;
8
+ contextNew(): Pointer;
9
+ contextFree(ctx: Pointer): void;
10
+ /** Register a progress callback pointer; returns it so it can stay alive
11
+ * and be unregistered afterwards. Pass null to clear. */
12
+ registerProgress(fn: ((event: string) => void) | null): ProgressHandle | null;
13
+ setProgress(ctx: Pointer, cbPtr: ProgressHandle | null): void;
14
+ unregister(cbPtr: ProgressHandle | null): void;
15
+ executeSync(ctx: Pointer, op: string, req: string): string;
16
+ startJob(ctx: Pointer, op: string, req: string): Pointer | null;
17
+ /** Poll: the envelope JSON if finished, else null. */
18
+ pollJob(job: Pointer): string | null;
19
+ cancelJob(job: Pointer): void;
20
+ freeJob(job: Pointer): void;
21
+ };
@@ -0,0 +1,187 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.native = void 0;
40
+ // Native binding to the CAVS C ABI via koffi. Handles are opaque pointers.
41
+ const koffi_1 = __importDefault(require("koffi"));
42
+ const fs = __importStar(require("node:fs"));
43
+ const path = __importStar(require("node:path"));
44
+ function libraryFileName() {
45
+ switch (process.platform) {
46
+ case "darwin":
47
+ return "libcavs_sdk.dylib";
48
+ case "win32":
49
+ return "cavs_sdk.dll";
50
+ default:
51
+ return "libcavs_sdk.so";
52
+ }
53
+ }
54
+ function osArch() {
55
+ const os = process.platform === "darwin"
56
+ ? "darwin"
57
+ : process.platform === "win32"
58
+ ? "win32"
59
+ : "linux";
60
+ const arch = process.arch === "x64" ? "x64" : process.arch === "arm64" ? "arm64" : process.arch;
61
+ return `${os}-${arch}`;
62
+ }
63
+ /** Resolve the native library: CAVS_SDK_LIBRARY, then the per-platform
64
+ * package, then the local `native/` staging dir. */
65
+ function resolveLibraryPath() {
66
+ const override = process.env.CAVS_SDK_LIBRARY;
67
+ if (override && override.length > 0) {
68
+ return override;
69
+ }
70
+ const name = libraryFileName();
71
+ const candidates = [
72
+ // Published per-platform package: @orelvis15/cavs-sdk-<os>-<arch>.
73
+ tryResolvePlatformPackage(name),
74
+ // Local dev staging (npm run native).
75
+ path.join(__dirname, "..", "native", name),
76
+ path.join(__dirname, "..", "..", "native", name),
77
+ ].filter((p) => !!p);
78
+ for (const c of candidates) {
79
+ if (fs.existsSync(c)) {
80
+ return c;
81
+ }
82
+ }
83
+ throw new Error(`cavs: native library not found (looked for ${name}); set CAVS_SDK_LIBRARY or run \`npm run native\``);
84
+ }
85
+ function tryResolvePlatformPackage(name) {
86
+ try {
87
+ const pkg = `@orelvis15/cavs-sdk-${osArch()}`;
88
+ const dir = path.dirname(require.resolve(`${pkg}/package.json`));
89
+ return path.join(dir, name);
90
+ }
91
+ catch {
92
+ return undefined;
93
+ }
94
+ }
95
+ const lib = koffi_1.default.load(resolveLibraryPath());
96
+ // Opaque handle pointer types.
97
+ const CavsContext = koffi_1.default.pointer("CavsContext", koffi_1.default.opaque());
98
+ const CavsResult = koffi_1.default.pointer("CavsResult", koffi_1.default.opaque());
99
+ const CavsJob = koffi_1.default.pointer("CavsJob", koffi_1.default.opaque());
100
+ // Progress callback prototype: void (*)(const char*, void*).
101
+ const ProgressProto = koffi_1.default.proto("void CavsProgressCallback(const char* event, void* user)");
102
+ const ProgressPtr = koffi_1.default.pointer(ProgressProto);
103
+ const fns = {
104
+ version: lib.func("const char* cavs_sdk_version()"),
105
+ abiVersion: lib.func("const char* cavs_sdk_abi_version()"),
106
+ capabilities: lib.func("void* cavs_sdk_capabilities_json()"),
107
+ ctxNew: lib.func("CavsContext* cavs_context_new(const char*)"),
108
+ ctxFree: lib.func("void cavs_context_free(CavsContext*)"),
109
+ setProgress: lib.func("int cavs_context_set_progress_callback(CavsContext*, CavsProgressCallback*, void*)"),
110
+ execute: lib.func("CavsResult* cavs_execute_json(CavsContext*, const char*, const char*)"),
111
+ start: lib.func("CavsJob* cavs_start_json(CavsContext*, const char*, const char*)"),
112
+ poll: lib.func("CavsResult* cavs_job_poll(CavsJob*)"),
113
+ cancel: lib.func("int cavs_job_cancel(CavsJob*)"),
114
+ jobFree: lib.func("void cavs_job_free(CavsJob*)"),
115
+ resultJson: lib.func("const char* cavs_result_json(CavsResult*)"),
116
+ resultOk: lib.func("int cavs_result_ok(CavsResult*)"),
117
+ resultFree: lib.func("void cavs_result_free(CavsResult*)"),
118
+ stringFree: lib.func("void cavs_string_free(void*)"),
119
+ };
120
+ exports.native = {
121
+ version() {
122
+ return fns.version();
123
+ },
124
+ abiVersion() {
125
+ return fns.abiVersion();
126
+ },
127
+ capabilitiesJson() {
128
+ const ptr = fns.capabilities();
129
+ if (!ptr) {
130
+ return "";
131
+ }
132
+ const s = koffi_1.default.decode(ptr, "char", -1);
133
+ fns.stringFree(ptr);
134
+ return s;
135
+ },
136
+ contextNew() {
137
+ return fns.ctxNew(null);
138
+ },
139
+ contextFree(ctx) {
140
+ fns.ctxFree(ctx);
141
+ },
142
+ /** Register a progress callback pointer; returns it so it can stay alive
143
+ * and be unregistered afterwards. Pass null to clear. */
144
+ registerProgress(fn) {
145
+ if (!fn) {
146
+ return null;
147
+ }
148
+ return koffi_1.default.register((event) => fn(event), ProgressPtr);
149
+ },
150
+ setProgress(ctx, cbPtr) {
151
+ fns.setProgress(ctx, cbPtr, null);
152
+ },
153
+ unregister(cbPtr) {
154
+ if (cbPtr) {
155
+ koffi_1.default.unregister(cbPtr);
156
+ }
157
+ },
158
+ executeSync(ctx, op, req) {
159
+ const res = fns.execute(ctx, op, req);
160
+ return drainResult(res);
161
+ },
162
+ startJob(ctx, op, req) {
163
+ return fns.start(ctx, op, req) ?? null;
164
+ },
165
+ /** Poll: the envelope JSON if finished, else null. */
166
+ pollJob(job) {
167
+ const res = fns.poll(job);
168
+ if (!res) {
169
+ return null;
170
+ }
171
+ return drainResult(res);
172
+ },
173
+ cancelJob(job) {
174
+ fns.cancel(job);
175
+ },
176
+ freeJob(job) {
177
+ fns.jobFree(job);
178
+ },
179
+ };
180
+ function drainResult(res) {
181
+ if (!res) {
182
+ return '{"ok":false,"error":{"code":"CAVS-E-INTERNAL","message":"native returned null result","recoverable":false}}';
183
+ }
184
+ const json = fns.resultJson(res);
185
+ fns.resultFree(res);
186
+ return json;
187
+ }
@@ -0,0 +1,195 @@
1
+ export interface ProgressEvent {
2
+ type: string;
3
+ operation: string;
4
+ phase?: string;
5
+ currentBytes?: number;
6
+ totalBytes?: number;
7
+ percentage?: number;
8
+ message?: string;
9
+ }
10
+ export type RoutePolicy = "balanced" | "networkMin" | "cpuMin" | "ramMin" | "diskIoMin" | "hddFriendly" | "developerFast";
11
+ export interface CallOptions {
12
+ /** Stream progress events. Note: enabling progress runs the operation
13
+ * synchronously (see README); omit it for the non-blocking path. */
14
+ onProgress?: (event: ProgressEvent) => void;
15
+ /** Cancel the operation (non-progress path only). */
16
+ signal?: AbortSignal;
17
+ }
18
+ export interface AnalyzeRequest {
19
+ oldPath: string;
20
+ newPath: string;
21
+ engineHint?: string;
22
+ maxWorstFiles?: number;
23
+ }
24
+ export interface WorstFile {
25
+ path: string;
26
+ status: string;
27
+ isPack: boolean;
28
+ oldSizeBytes: number;
29
+ newSizeBytes: number;
30
+ estimatedDownloadBytes: number;
31
+ reuseRatio: number;
32
+ entropyBits: number;
33
+ }
34
+ export interface Recommendation {
35
+ severity: string;
36
+ kind: string;
37
+ title: string;
38
+ file?: string;
39
+ estimatedWastedBytes: number;
40
+ why: string;
41
+ fix: string;
42
+ expectedImprovement: string;
43
+ }
44
+ export interface AnalyzeReport {
45
+ summary: {
46
+ oldSizeBytes: number;
47
+ newSizeBytes: number;
48
+ estimatedUpdateBytes: number;
49
+ estimatedSteamPipeBytes: number;
50
+ cavsReuseRatio: number;
51
+ steamPipeReuseRatio: number;
52
+ filesUnchanged: number;
53
+ filesModified: number;
54
+ filesAdded: number;
55
+ filesDeleted: number;
56
+ worstFiles: WorstFile[];
57
+ };
58
+ engine: string;
59
+ warnings: string[];
60
+ recommendations: Recommendation[];
61
+ note: string;
62
+ }
63
+ export interface PackDirectoryRequest {
64
+ inputDir: string;
65
+ outputCavs: string;
66
+ profile?: string;
67
+ compression?: string;
68
+ signKeyPath?: string;
69
+ ignore?: string[];
70
+ }
71
+ export interface PackResult {
72
+ outputCavs: string;
73
+ totalSizeBytes: number;
74
+ chunkCount: number;
75
+ logicalChunks: number;
76
+ logicalRawBytes: number;
77
+ storedBytes: number;
78
+ merkleRoot: string;
79
+ filesPacked: number;
80
+ entriesIgnored: number;
81
+ signed: boolean;
82
+ profile: string;
83
+ elapsedMs: number;
84
+ }
85
+ export interface PreviewRequest {
86
+ oldPath: string;
87
+ newPath: string;
88
+ engineHint?: string;
89
+ routes?: string[];
90
+ policy?: RoutePolicy;
91
+ }
92
+ export interface Route {
93
+ name: string;
94
+ networkBytes: number;
95
+ diffMs?: number;
96
+ applyMs?: number | null;
97
+ available: boolean;
98
+ }
99
+ export interface PreviewReport {
100
+ recommendedRoute: string;
101
+ oldSizeBytes: number;
102
+ newSizeBytes: number;
103
+ routes: Route[];
104
+ explanation: string;
105
+ }
106
+ export interface CreatePlanRequest {
107
+ oldPath?: string;
108
+ oldSignature?: string;
109
+ newPath: string;
110
+ outputPlan: string;
111
+ planKind?: "portable" | "analysis";
112
+ blockKib?: number;
113
+ zstdLevel?: number;
114
+ }
115
+ export interface PlanResult {
116
+ planPath: string;
117
+ planBytes: number;
118
+ planKind: string;
119
+ mode: string;
120
+ operationCount: number;
121
+ copyOps: number;
122
+ inlineOps: number;
123
+ reusedBytes: number;
124
+ inlineBytes: number;
125
+ estimatedNetworkBytes: number;
126
+ expectedOutputSize: number;
127
+ files: number;
128
+ unchangedFiles: number;
129
+ deleted: number;
130
+ elapsedMs: number;
131
+ }
132
+ export interface ApplyPlanRequest {
133
+ oldPath: string;
134
+ planPath: string;
135
+ outputPath: string;
136
+ checkOld?: boolean;
137
+ deleteRemoved?: boolean;
138
+ }
139
+ export interface ApplyResult {
140
+ outputPath: string;
141
+ verified: boolean;
142
+ mode: string;
143
+ filesTotal: number;
144
+ filesWritten: number;
145
+ filesNoop: number;
146
+ dirsCreated: number;
147
+ symlinksCreated: number;
148
+ deleted: number;
149
+ bytesWritten: number;
150
+ bytesFromOld: number;
151
+ bytesFromBlob: number;
152
+ elapsedMs: number;
153
+ }
154
+ export interface VerifyRequest {
155
+ target: string;
156
+ signature?: string;
157
+ manifest?: string;
158
+ allowExtra?: boolean;
159
+ }
160
+ export interface VerifyResult {
161
+ verified: boolean;
162
+ filesChecked: number;
163
+ bytesChecked: number;
164
+ mismatches: {
165
+ modified: string[];
166
+ missing: string[];
167
+ extra: string[];
168
+ };
169
+ elapsedMs: number;
170
+ }
171
+ export interface BenchmarkRequest {
172
+ oldPath: string;
173
+ newPath: string;
174
+ engineHint?: string;
175
+ measureApply?: boolean;
176
+ }
177
+ export interface BenchmarkReport {
178
+ oldSizeBytes: number;
179
+ newSizeBytes: number;
180
+ recommendedRoute: string;
181
+ routes: Route[];
182
+ reuseRatio: number;
183
+ }
184
+ export interface SavingsRequest {
185
+ pricePerGb: number;
186
+ monthlyDownloads: number;
187
+ averageFullDownloadBytes: number;
188
+ averageCavsDownloadBytes: number;
189
+ }
190
+ export interface SavingsReport {
191
+ fullDownloadMonthlyCost: number;
192
+ cavsMonthlyCost: number;
193
+ estimatedMonthlySavings: number;
194
+ savingsPercent: number;
195
+ }
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ // Request and response shapes for the CAVS SDK. Field names match the
3
+ // engine's camelCase JSON exactly.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ const node_test_1 = require("node:test");
40
+ const strict_1 = __importDefault(require("node:assert/strict"));
41
+ const fs = __importStar(require("node:fs"));
42
+ const os = __importStar(require("node:os"));
43
+ const path = __importStar(require("node:path"));
44
+ const index_1 = require("../src/index");
45
+ function makeBuilds() {
46
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "cavs-node-"));
47
+ const oldDir = path.join(root, "Build_v1");
48
+ const newDir = path.join(root, "Build_v2");
49
+ fs.mkdirSync(path.join(oldDir, "data"), { recursive: true });
50
+ fs.mkdirSync(path.join(newDir, "data"), { recursive: true });
51
+ const base = Buffer.alloc(512 * 1024);
52
+ for (let i = 0; i < base.length; i++)
53
+ base[i] = i % 251;
54
+ fs.writeFileSync(path.join(oldDir, "data/asset.bin"), base);
55
+ const changed = Buffer.from(base);
56
+ for (let i = 300000; i < 304096; i++)
57
+ changed[i] ^= 0xff;
58
+ fs.writeFileSync(path.join(newDir, "data/asset.bin"), changed);
59
+ fs.writeFileSync(path.join(oldDir, "readme.txt"), "cavs sdk fixture\n");
60
+ fs.writeFileSync(path.join(newDir, "readme.txt"), "cavs sdk fixture\n");
61
+ fs.writeFileSync(path.join(newDir, "data/new_only.bin"), Buffer.alloc(64 * 1024));
62
+ const work = path.join(root, "work");
63
+ fs.mkdirSync(work);
64
+ return { oldDir, newDir, work };
65
+ }
66
+ function assertTreesEqual(a, b) {
67
+ let checked = 0;
68
+ const walk = (dir) => {
69
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
70
+ const full = path.join(dir, entry.name);
71
+ if (entry.isDirectory()) {
72
+ walk(full);
73
+ }
74
+ else {
75
+ const rel = path.relative(a, full);
76
+ strict_1.default.deepEqual(fs.readFileSync(full), fs.readFileSync(path.join(b, rel)), `differs: ${rel}`);
77
+ checked++;
78
+ }
79
+ }
80
+ };
81
+ walk(a);
82
+ strict_1.default.ok(checked > 0, "no files compared");
83
+ }
84
+ let client;
85
+ (0, node_test_1.before)(() => {
86
+ client = new index_1.CavsClient();
87
+ });
88
+ (0, node_test_1.after)(() => {
89
+ client?.close();
90
+ });
91
+ (0, node_test_1.test)("version and abi are semver", () => {
92
+ strict_1.default.equal((0, index_1.version)().split(".").length, 3);
93
+ strict_1.default.equal((0, index_1.abiVersion)(), "1.0.0");
94
+ });
95
+ (0, node_test_1.test)("estimateSavings", async () => {
96
+ const r = await client.estimateSavings({
97
+ pricePerGb: 0.08,
98
+ monthlyDownloads: 500000,
99
+ averageFullDownloadBytes: 65011712,
100
+ averageCavsDownloadBytes: 2631921,
101
+ });
102
+ strict_1.default.ok(r.savingsPercent > 90, `savingsPercent=${r.savingsPercent}`);
103
+ strict_1.default.ok(r.estimatedMonthlySavings > r.cavsMonthlyCost);
104
+ });
105
+ (0, node_test_1.test)("full pipeline: analyze → pack → plan → apply → preview → benchmark", async () => {
106
+ const { oldDir, newDir, work } = makeBuilds();
107
+ const an = await client.analyze({ oldPath: oldDir, newPath: newDir });
108
+ strict_1.default.ok(an.summary.newSizeBytes > 0);
109
+ const packOut = path.join(work, "v2.cavs");
110
+ const pk = await client.packDirectory({ inputDir: newDir, outputCavs: packOut });
111
+ strict_1.default.ok(pk.filesPacked >= 3);
112
+ strict_1.default.ok(fs.existsSync(packOut));
113
+ const planPath = path.join(work, "update.cavsplan");
114
+ const pl = await client.createPlan({ oldPath: oldDir, newPath: newDir, outputPlan: planPath });
115
+ strict_1.default.ok(pl.reusedBytes > 0, "plan found no reuse");
116
+ const outDir = path.join(work, "out");
117
+ const ap = await client.applyPlan({ oldPath: oldDir, planPath, outputPath: outDir });
118
+ strict_1.default.ok(ap.verified);
119
+ assertTreesEqual(newDir, outDir);
120
+ const pv = await client.preview({ oldPath: oldDir, newPath: newDir, policy: "balanced" });
121
+ strict_1.default.ok(pv.routes.length > 0);
122
+ strict_1.default.ok(pv.recommendedRoute.length > 0);
123
+ const bm = await client.benchmark({ oldPath: oldDir, newPath: newDir, measureApply: false });
124
+ strict_1.default.equal(bm.routes.length, 4);
125
+ });
126
+ (0, node_test_1.test)("error mapping: missing path", async () => {
127
+ await strict_1.default.rejects(client.analyze({ oldPath: "/no/such/old", newPath: "/no/such/new" }), (err) => {
128
+ strict_1.default.ok(err instanceof index_1.CavsError);
129
+ strict_1.default.equal(err.code, index_1.ErrorCode.PathNotFound);
130
+ return true;
131
+ });
132
+ });
133
+ (0, node_test_1.test)("progress events are emitted", async () => {
134
+ const { oldDir, newDir, work } = makeBuilds();
135
+ const events = [];
136
+ await client.createPlan({ oldPath: oldDir, newPath: newDir, outputPlan: path.join(work, "p.cavsplan") }, { onProgress: (e) => events.push(e.type) });
137
+ strict_1.default.ok(events.length >= 2, `expected >= 2 events, got ${events.length}`);
138
+ strict_1.default.ok(events.includes("started"));
139
+ });
140
+ (0, node_test_1.test)("AbortSignal cancels before start", async () => {
141
+ const { oldDir, newDir, work } = makeBuilds();
142
+ const ac = new AbortController();
143
+ ac.abort();
144
+ await strict_1.default.rejects(client.createPlan({ oldPath: oldDir, newPath: newDir, outputPlan: path.join(work, "c.cavsplan") }, { signal: ac.signal }), (err) => err instanceof index_1.CavsError && err.code === index_1.ErrorCode.Cancelled);
145
+ });
@@ -0,0 +1,96 @@
1
+ /*
2
+ * cavs_sdk.h — stable C ABI for the CAVS SDK native library.
3
+ *
4
+ * Generated/maintained alongside `core/cavs-ffi`. ABI version: 1.0.0.
5
+ *
6
+ * Library names by platform:
7
+ * Linux, BSD : libcavs_sdk.so
8
+ * macOS : libcavs_sdk.dylib
9
+ * Windows : cavs_sdk.dll
10
+ *
11
+ * Memory ownership:
12
+ * - Handles (CavsContext*, CavsResult*, CavsJob*) are freed by their
13
+ * matching *_free function, exactly once.
14
+ * - char* returned by cavs_sdk_capabilities_json() must be freed with
15
+ * cavs_string_free().
16
+ * - char* returned by cavs_sdk_version()/cavs_sdk_abi_version() is static
17
+ * and must NOT be freed.
18
+ * - char* returned by cavs_result_json()/error_code()/error_message() is
19
+ * owned by the CavsResult and freed by cavs_result_free().
20
+ *
21
+ * Threading:
22
+ * - cavs_execute_json() runs on the calling thread.
23
+ * - cavs_start_json() runs on a background thread; a registered progress
24
+ * callback may be invoked from that thread and must be thread-safe.
25
+ */
26
+ #ifndef CAVS_SDK_H
27
+ #define CAVS_SDK_H
28
+
29
+ #ifdef __cplusplus
30
+ extern "C" {
31
+ #endif
32
+
33
+ typedef struct CavsContext CavsContext;
34
+ typedef struct CavsResult CavsResult;
35
+ typedef struct CavsJob CavsJob;
36
+
37
+ typedef void (*CavsProgressCallback)(const char *event_json, void *user_data);
38
+
39
+ /* --- Version / capabilities --------------------------------------------- */
40
+
41
+ /* Static strings; do NOT free. */
42
+ const char *cavs_sdk_version(void);
43
+ const char *cavs_sdk_abi_version(void);
44
+
45
+ /* Heap string; free with cavs_string_free(). */
46
+ char *cavs_sdk_capabilities_json(void);
47
+
48
+ /* --- Context ------------------------------------------------------------- */
49
+
50
+ CavsContext *cavs_context_new(const char *options_json);
51
+ void cavs_context_free(CavsContext *ctx);
52
+
53
+ /* Returns 0 on success, -1 if ctx is NULL. Pass a NULL callback to clear. */
54
+ int cavs_context_set_progress_callback(CavsContext *ctx,
55
+ CavsProgressCallback callback,
56
+ void *user_data);
57
+
58
+ /* --- Synchronous execution ---------------------------------------------- */
59
+
60
+ /* Runs on the calling thread. Free the result with cavs_result_free(). */
61
+ CavsResult *cavs_execute_json(CavsContext *ctx,
62
+ const char *operation,
63
+ const char *request_json);
64
+
65
+ /* --- Asynchronous jobs --------------------------------------------------- */
66
+
67
+ CavsJob *cavs_start_json(CavsContext *ctx,
68
+ const char *operation,
69
+ const char *request_json);
70
+
71
+ /* Returns the result if finished, else NULL (still running). */
72
+ CavsResult *cavs_job_poll(CavsJob *job);
73
+
74
+ /* Request cooperative cancellation. Returns 0 on success, -1 if NULL. */
75
+ int cavs_job_cancel(CavsJob *job);
76
+
77
+ /* Frees the job; joins the worker thread first. */
78
+ void cavs_job_free(CavsJob *job);
79
+
80
+ /* --- Result accessors ---------------------------------------------------- */
81
+
82
+ const char *cavs_result_json(const CavsResult *result);
83
+ int cavs_result_ok(const CavsResult *result);
84
+ const char *cavs_result_error_code(const CavsResult *result);
85
+ const char *cavs_result_error_message(const CavsResult *result);
86
+ void cavs_result_free(CavsResult *result);
87
+
88
+ /* --- Misc ---------------------------------------------------------------- */
89
+
90
+ void cavs_string_free(char *ptr);
91
+
92
+ #ifdef __cplusplus
93
+ }
94
+ #endif
95
+
96
+ #endif /* CAVS_SDK_H */
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@orelvis15/cavs-sdk",
3
+ "version": "1.2.0",
4
+ "description": "Node.js SDK for CAVS — analyze, preview, pack, plan, apply, verify and benchmark game updates using the native Rust core.",
5
+ "license": "Apache-2.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/orelvis15/cavs-oss.git",
9
+ "directory": "sdks/node"
10
+ },
11
+ "main": "dist/src/index.js",
12
+ "types": "dist/src/index.d.ts",
13
+ "files": [
14
+ "dist",
15
+ "native/cavs_sdk.h"
16
+ ],
17
+ "engines": {
18
+ "node": ">=20"
19
+ },
20
+ "scripts": {
21
+ "build": "tsc -p tsconfig.json",
22
+ "native": "cargo build --release -p cavs-ffi --manifest-path ../../Cargo.toml && node scripts/stage-native.mjs",
23
+ "pretest": "npm run build",
24
+ "test": "node --test dist/test/",
25
+ "lint": "tsc -p tsconfig.json --noEmit"
26
+ },
27
+ "dependencies": {
28
+ "koffi": "^2.9.0"
29
+ },
30
+ "optionalDependencies": {
31
+ "@orelvis15/cavs-sdk-linux-x64": "1.2.0",
32
+ "@orelvis15/cavs-sdk-linux-arm64": "1.2.0",
33
+ "@orelvis15/cavs-sdk-darwin-x64": "1.2.0",
34
+ "@orelvis15/cavs-sdk-darwin-arm64": "1.2.0",
35
+ "@orelvis15/cavs-sdk-win32-x64": "1.2.0"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^20.14.0",
39
+ "typescript": "^5.5.0"
40
+ }
41
+ }