@camstack/system 1.2.289 → 1.2.290
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/addon-runner.js +1 -1
- package/dist/addon-runner.mjs +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/autotrack/index.js +1 -1
- package/dist/builtins/autotrack/index.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
- package/dist/builtins/camera-grid/index.js +1 -1
- package/dist/builtins/camera-grid/index.mjs +1 -1
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
- package/dist/builtins/device-manager/device-manager.addon.js +2 -2
- package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +1 -1
- package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
- package/dist/builtins/local-network/local-network.addon.js +1 -1
- package/dist/builtins/local-network/local-network.addon.mjs +1 -1
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-config/system-config.addon.js +1 -1
- package/dist/builtins/system-config/system-config.addon.mjs +1 -1
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{dist-DKFb2936.mjs → dist-BkjMEa2F.mjs} +230 -10
- package/dist/{dist-BYnHJU96.js → dist-KRK9zLWt.js} +247 -9
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -3
- package/dist/index.mjs +4 -4
- package/dist/kernel/moleculer/addon-peer-bytes-facility.d.ts +24 -0
- package/dist/{manifest-system-deps-C9T7784z.mjs → manifest-system-deps-D_5UXqDb.mjs} +585 -5
- package/dist/{manifest-system-deps-B8ZUclN4.js → manifest-system-deps-Dm7QhyvL.js} +592 -0
- package/dist/{retired-settings-keys-C6rFqHR7.js → retired-settings-keys-BSLMlTcz.js} +1 -1
- package/dist/{retired-settings-keys-tuNxNNpR.mjs → retired-settings-keys-DNK-p6aa.mjs} +1 -1
- package/package.json +1 -1
|
@@ -1,22 +1,24 @@
|
|
|
1
|
+
import { C as PEER_BYTES_CHUNK_BYTES } from "./dist-BkjMEa2F.mjs";
|
|
1
2
|
import "./hub-cap-forward-BdT320dq.mjs";
|
|
2
3
|
import * as crypto$1 from "node:crypto";
|
|
3
4
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
4
5
|
import { ensureBinary, ensureFfmpeg, ensurePython, installPythonPackages, installPythonRequirements } from "@camstack/types/node";
|
|
5
|
-
import { unlink } from "node:fs/promises";
|
|
6
|
+
import { mkdir, rm, stat, unlink } from "node:fs/promises";
|
|
6
7
|
import * as path$1 from "node:path";
|
|
7
|
-
import { isAbsolute, join } from "node:path";
|
|
8
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
8
9
|
import * as fs from "node:fs";
|
|
9
|
-
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
10
|
+
import { createReadStream, createWriteStream, existsSync, readFileSync, readdirSync } from "node:fs";
|
|
10
11
|
import { execFile } from "node:child_process";
|
|
11
12
|
import * as os from "node:os";
|
|
12
13
|
import { tmpdir } from "node:os";
|
|
13
14
|
import { promisify } from "node:util";
|
|
14
|
-
import { createServer } from "node:http";
|
|
15
|
+
import { createServer, request } from "node:http";
|
|
15
16
|
import * as v8 from "node:v8";
|
|
16
17
|
import { Session } from "node:inspector/promises";
|
|
17
18
|
import { monitorEventLoopDelay } from "node:perf_hooks";
|
|
18
19
|
import * as vm from "node:vm";
|
|
19
20
|
import { BOOT_RECOVERY_BACKOFF_MS, DATAPLANE_SECRET_HEADER, DEVICE_CHILDREN_BATCH_MAX, DeviceType, DisposerChain, EventCategory, ReadinessRegistry, asJsonObject, asString, createDeviceProxy, createEventBusSliceSource, deviceOpsCapability, emitReadiness, errMsg, expandCapMethods, readNodePin, scopeKey, sleep, withCapInputDefaults } from "@camstack/types/addon";
|
|
21
|
+
import { once } from "node:events";
|
|
20
22
|
import { connect, createConnection, createServer as createServer$1 } from "node:net";
|
|
21
23
|
import { TRPCClientError, createTRPCClient } from "@trpc/client";
|
|
22
24
|
//#region src/kernel/event-plane-report.ts
|
|
@@ -2610,6 +2612,577 @@ function createAddonDataPlaneFacility(args) {
|
|
|
2610
2612
|
};
|
|
2611
2613
|
}
|
|
2612
2614
|
//#endregion
|
|
2615
|
+
//#region src/kernel/moleculer/addon-peer-bytes-facility.ts
|
|
2616
|
+
/**
|
|
2617
|
+
* Kernel side of the addon→addon **byte transport** (`ctx.peerBytes`, D613).
|
|
2618
|
+
*
|
|
2619
|
+
* See {@link AddonPeerBytes} for what this exists for and what it deliberately
|
|
2620
|
+
* does not do. This file is the mechanism:
|
|
2621
|
+
*
|
|
2622
|
+
* - **One lazy loopback listener per addon process.** Bound on the first
|
|
2623
|
+
* `offer()`, never before, so an addon that never offers pays no port.
|
|
2624
|
+
* - **One token per offer.** 24 random bytes, base64url, ONE `GET`. A replay
|
|
2625
|
+
* is `offer-used` (410) for as long as the original ticket would have lived,
|
|
2626
|
+
* so a second take is told what happened rather than "no such thing".
|
|
2627
|
+
* `HEAD` answers the headers and consumes nothing.
|
|
2628
|
+
* - **Outstanding offers are bounded and swept on every mint**, which is also
|
|
2629
|
+
* what makes a per-frame misuse of this facility refuse by name instead of
|
|
2630
|
+
* working slowly.
|
|
2631
|
+
* - **Every write honours `write()`'s return and awaits `drain`** (D447), and
|
|
2632
|
+
* a peer that has taken nothing for {@link PEER_BYTES_STALL_MS} is cut. Every
|
|
2633
|
+
* exit — offered, taken, sent, cut, stalled, refused, disposed — is one log
|
|
2634
|
+
* line, carrying `tags: { deviceId }` when the offer named a device.
|
|
2635
|
+
* - **The consumer's response is handed over PAUSED**, which is the whole
|
|
2636
|
+
* reason `open()` lives in the framework rather than in each consumer: see
|
|
2637
|
+
* {@link PeerBytesBody}.
|
|
2638
|
+
*/
|
|
2639
|
+
/** The HOST a node id belongs to. `hub` → `hub`; `hub/runner-3` → `hub`. */
|
|
2640
|
+
function hostNodeIdOf(nodeId) {
|
|
2641
|
+
const slash = nodeId.indexOf("/");
|
|
2642
|
+
return slash === -1 ? nodeId : nodeId.slice(0, slash);
|
|
2643
|
+
}
|
|
2644
|
+
/** A body chunk, without a cast. A readable in binary mode yields Buffers; in
|
|
2645
|
+
* string mode, strings. Anything else is a broken stream and says so. */
|
|
2646
|
+
function asBuffer(chunk) {
|
|
2647
|
+
if (Buffer.isBuffer(chunk)) return chunk;
|
|
2648
|
+
if (typeof chunk === "string") return Buffer.from(chunk);
|
|
2649
|
+
throw new Error("peer-bytes: a body chunk was neither a Buffer nor a string");
|
|
2650
|
+
}
|
|
2651
|
+
function tokenOf(req) {
|
|
2652
|
+
const raw = req.url ?? "/";
|
|
2653
|
+
const query = raw.indexOf("?");
|
|
2654
|
+
return (query === -1 ? raw : raw.slice(0, query)).replace(/^\/+/, "");
|
|
2655
|
+
}
|
|
2656
|
+
/**
|
|
2657
|
+
* Write `bytes` to `res` in bounded chunks, honouring backpressure.
|
|
2658
|
+
*
|
|
2659
|
+
* D447, the consuming half stated from the producing side: a raw HTTP stream
|
|
2660
|
+
* writes no chunk the socket has not taken. `write()` returning false means the
|
|
2661
|
+
* kernel buffer is full and everything further is heap in THIS process — so we
|
|
2662
|
+
* wait for `drain`, and a peer that never drains within `stallMs` is cut rather
|
|
2663
|
+
* than held. Returns how many bytes reached the socket and why it stopped.
|
|
2664
|
+
*/
|
|
2665
|
+
async function writeBufferWithBackpressure(res, bytes, stallMs) {
|
|
2666
|
+
const waitForDrain = () => new Promise((resolve) => {
|
|
2667
|
+
const settle = (outcome) => {
|
|
2668
|
+
clearTimeout(timer);
|
|
2669
|
+
res.off("drain", onDrain);
|
|
2670
|
+
res.off("close", onClose);
|
|
2671
|
+
resolve(outcome);
|
|
2672
|
+
};
|
|
2673
|
+
const onDrain = () => {
|
|
2674
|
+
settle("drained");
|
|
2675
|
+
};
|
|
2676
|
+
const onClose = () => {
|
|
2677
|
+
settle("closed");
|
|
2678
|
+
};
|
|
2679
|
+
const timer = setTimeout(() => {
|
|
2680
|
+
settle("stalled");
|
|
2681
|
+
}, stallMs);
|
|
2682
|
+
timer.unref();
|
|
2683
|
+
res.on("drain", onDrain);
|
|
2684
|
+
res.on("close", onClose);
|
|
2685
|
+
});
|
|
2686
|
+
let sent = 0;
|
|
2687
|
+
while (sent < bytes.byteLength) {
|
|
2688
|
+
if (res.destroyed || res.writableEnded) return {
|
|
2689
|
+
sent,
|
|
2690
|
+
outcome: "peer-gone"
|
|
2691
|
+
};
|
|
2692
|
+
const end = Math.min(sent + PEER_BYTES_CHUNK_BYTES, bytes.byteLength);
|
|
2693
|
+
const took = res.write(bytes.subarray(sent, end));
|
|
2694
|
+
sent = end;
|
|
2695
|
+
if (took) continue;
|
|
2696
|
+
const waited = await waitForDrain();
|
|
2697
|
+
if (waited === "drained") continue;
|
|
2698
|
+
return {
|
|
2699
|
+
sent,
|
|
2700
|
+
outcome: waited === "closed" ? "peer-gone" : "stalled"
|
|
2701
|
+
};
|
|
2702
|
+
}
|
|
2703
|
+
return {
|
|
2704
|
+
sent,
|
|
2705
|
+
outcome: "sent"
|
|
2706
|
+
};
|
|
2707
|
+
}
|
|
2708
|
+
function createAddonPeerBytesFacility(args) {
|
|
2709
|
+
const now = args.now ?? (() => Date.now());
|
|
2710
|
+
const ttlMs = args.ttlMs ?? 3e4;
|
|
2711
|
+
const maxOutstanding = args.maxOutstanding ?? 64;
|
|
2712
|
+
const stallMs = args.stallMs ?? 15e3;
|
|
2713
|
+
const host = args.host ?? "127.0.0.1";
|
|
2714
|
+
const hostNodeId = hostNodeIdOf(args.nodeId);
|
|
2715
|
+
const offers = /* @__PURE__ */ new Map();
|
|
2716
|
+
/** Tokens already taken, kept for their TTL so a replay is `offer-used`. */
|
|
2717
|
+
const taken = /* @__PURE__ */ new Map();
|
|
2718
|
+
let server = null;
|
|
2719
|
+
let baseUrl = null;
|
|
2720
|
+
let listening = null;
|
|
2721
|
+
const sweep = () => {
|
|
2722
|
+
const at = now();
|
|
2723
|
+
for (const [token, offer] of offers) if (offer.expiresAtMs <= at) {
|
|
2724
|
+
offers.delete(token);
|
|
2725
|
+
args.logger.info("peer-bytes: an offer expired untaken", {
|
|
2726
|
+
...offer.deviceId !== void 0 ? { tags: { deviceId: offer.deviceId } } : {},
|
|
2727
|
+
meta: {
|
|
2728
|
+
label: offer.label,
|
|
2729
|
+
outstanding: offers.size
|
|
2730
|
+
}
|
|
2731
|
+
});
|
|
2732
|
+
}
|
|
2733
|
+
for (const [token, expiresAtMs] of taken) if (expiresAtMs <= at) taken.delete(token);
|
|
2734
|
+
};
|
|
2735
|
+
const route = (req, res) => {
|
|
2736
|
+
const token = tokenOf(req);
|
|
2737
|
+
const offer = offers.get(token);
|
|
2738
|
+
if (offer === void 0) {
|
|
2739
|
+
const status = taken.has(token) ? 410 : 404;
|
|
2740
|
+
res.writeHead(status, { "x-camstack-peer-bytes": status === 410 ? "offer-used" : "no-offer" });
|
|
2741
|
+
res.end();
|
|
2742
|
+
return;
|
|
2743
|
+
}
|
|
2744
|
+
if (offer.expiresAtMs <= now()) {
|
|
2745
|
+
offers.delete(token);
|
|
2746
|
+
res.writeHead(410, { "x-camstack-peer-bytes": "offer-expired" }).end();
|
|
2747
|
+
return;
|
|
2748
|
+
}
|
|
2749
|
+
if (req.method === "GET") {
|
|
2750
|
+
offers.delete(token);
|
|
2751
|
+
taken.set(token, offer.expiresAtMs);
|
|
2752
|
+
args.logger.info("peer-bytes: an offer was taken", {
|
|
2753
|
+
...offer.deviceId !== void 0 ? { tags: { deviceId: offer.deviceId } } : {},
|
|
2754
|
+
meta: {
|
|
2755
|
+
label: offer.label,
|
|
2756
|
+
outstanding: offers.size
|
|
2757
|
+
}
|
|
2758
|
+
});
|
|
2759
|
+
}
|
|
2760
|
+
Promise.resolve(offer.handler(req, res)).catch((err) => {
|
|
2761
|
+
args.logger.warn("peer-bytes: an offer handler threw", {
|
|
2762
|
+
...offer.deviceId !== void 0 ? { tags: { deviceId: offer.deviceId } } : {},
|
|
2763
|
+
meta: {
|
|
2764
|
+
label: offer.label,
|
|
2765
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2766
|
+
}
|
|
2767
|
+
});
|
|
2768
|
+
if (!res.headersSent) res.writeHead(500);
|
|
2769
|
+
res.end();
|
|
2770
|
+
});
|
|
2771
|
+
};
|
|
2772
|
+
const listen = () => {
|
|
2773
|
+
if (listening !== null) return listening;
|
|
2774
|
+
listening = new Promise((resolve, reject) => {
|
|
2775
|
+
const created = createServer((req, res) => {
|
|
2776
|
+
route(req, res);
|
|
2777
|
+
});
|
|
2778
|
+
created.on("error", (err) => {
|
|
2779
|
+
listening = null;
|
|
2780
|
+
reject(err);
|
|
2781
|
+
});
|
|
2782
|
+
created.listen(0, host, () => {
|
|
2783
|
+
const address = created.address();
|
|
2784
|
+
if (address === null || typeof address === "string") {
|
|
2785
|
+
created.close();
|
|
2786
|
+
listening = null;
|
|
2787
|
+
reject(/* @__PURE__ */ new Error("the peer-bytes listener bound no TCP port"));
|
|
2788
|
+
return;
|
|
2789
|
+
}
|
|
2790
|
+
server = created;
|
|
2791
|
+
baseUrl = `http://${host}:${address.port}`;
|
|
2792
|
+
args.logger.info("peer-bytes: listener bound", { meta: {
|
|
2793
|
+
addonId: args.addonId,
|
|
2794
|
+
host,
|
|
2795
|
+
port: address.port,
|
|
2796
|
+
hostNodeId,
|
|
2797
|
+
ttlMs
|
|
2798
|
+
} });
|
|
2799
|
+
resolve(baseUrl);
|
|
2800
|
+
});
|
|
2801
|
+
});
|
|
2802
|
+
return listening;
|
|
2803
|
+
};
|
|
2804
|
+
const mint = async (options) => {
|
|
2805
|
+
sweep();
|
|
2806
|
+
const deviceTags = options.deviceId !== void 0 ? { tags: { deviceId: options.deviceId } } : {};
|
|
2807
|
+
if (offers.size >= maxOutstanding) {
|
|
2808
|
+
args.logger.warn("peer-bytes: offer refused — too many outstanding", {
|
|
2809
|
+
...deviceTags,
|
|
2810
|
+
meta: {
|
|
2811
|
+
label: options.label,
|
|
2812
|
+
outstanding: offers.size,
|
|
2813
|
+
maxOutstanding
|
|
2814
|
+
}
|
|
2815
|
+
});
|
|
2816
|
+
return {
|
|
2817
|
+
kind: "refused",
|
|
2818
|
+
code: "offer-exhausted",
|
|
2819
|
+
detail: `${String(offers.size)} offers outstanding against a bound of ${String(maxOutstanding)}`
|
|
2820
|
+
};
|
|
2821
|
+
}
|
|
2822
|
+
let base;
|
|
2823
|
+
try {
|
|
2824
|
+
base = baseUrl ?? await listen();
|
|
2825
|
+
} catch (err) {
|
|
2826
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
2827
|
+
args.logger.warn("peer-bytes: offer refused — no listener", {
|
|
2828
|
+
...deviceTags,
|
|
2829
|
+
meta: {
|
|
2830
|
+
label: options.label,
|
|
2831
|
+
error: detail
|
|
2832
|
+
}
|
|
2833
|
+
});
|
|
2834
|
+
return {
|
|
2835
|
+
kind: "refused",
|
|
2836
|
+
code: "listener-failed",
|
|
2837
|
+
detail
|
|
2838
|
+
};
|
|
2839
|
+
}
|
|
2840
|
+
const token = randomBytes(24).toString("base64url");
|
|
2841
|
+
const expiresAtMs = now() + (options.ttlMs ?? ttlMs);
|
|
2842
|
+
offers.set(token, {
|
|
2843
|
+
handler: options.handler,
|
|
2844
|
+
label: options.label,
|
|
2845
|
+
deviceId: options.deviceId,
|
|
2846
|
+
expiresAtMs
|
|
2847
|
+
});
|
|
2848
|
+
args.logger.info("peer-bytes: offer minted", {
|
|
2849
|
+
...deviceTags,
|
|
2850
|
+
meta: {
|
|
2851
|
+
label: options.label,
|
|
2852
|
+
declaredBytes: options.declaredBytes ?? null,
|
|
2853
|
+
expiresInMs: expiresAtMs - now(),
|
|
2854
|
+
outstanding: offers.size
|
|
2855
|
+
}
|
|
2856
|
+
});
|
|
2857
|
+
return {
|
|
2858
|
+
kind: "ticket",
|
|
2859
|
+
ticket: {
|
|
2860
|
+
url: `${base}/${token}`,
|
|
2861
|
+
hostNodeId,
|
|
2862
|
+
expiresAtMs,
|
|
2863
|
+
declaredBytes: options.declaredBytes ?? null
|
|
2864
|
+
}
|
|
2865
|
+
};
|
|
2866
|
+
};
|
|
2867
|
+
const peerBytes = {
|
|
2868
|
+
hostNodeId,
|
|
2869
|
+
get outstanding() {
|
|
2870
|
+
sweep();
|
|
2871
|
+
return offers.size;
|
|
2872
|
+
},
|
|
2873
|
+
offer: mint,
|
|
2874
|
+
offerBuffer: async (options) => {
|
|
2875
|
+
const deviceTags = options.deviceId !== void 0 ? { tags: { deviceId: options.deviceId } } : {};
|
|
2876
|
+
const handler = async (req, res) => {
|
|
2877
|
+
res.writeHead(200, {
|
|
2878
|
+
"content-type": options.contentType,
|
|
2879
|
+
"content-length": String(options.bytes.byteLength),
|
|
2880
|
+
"accept-ranges": "none"
|
|
2881
|
+
});
|
|
2882
|
+
if (req.method === "HEAD") {
|
|
2883
|
+
res.end();
|
|
2884
|
+
return;
|
|
2885
|
+
}
|
|
2886
|
+
const startedAt = now();
|
|
2887
|
+
const result = await writeBufferWithBackpressure(res, options.bytes, stallMs);
|
|
2888
|
+
if (result.outcome === "sent") {
|
|
2889
|
+
res.end();
|
|
2890
|
+
args.logger.info("peer-bytes: body sent", {
|
|
2891
|
+
...deviceTags,
|
|
2892
|
+
meta: {
|
|
2893
|
+
label: options.label,
|
|
2894
|
+
bytes: result.sent,
|
|
2895
|
+
tookMs: now() - startedAt,
|
|
2896
|
+
source: "buffer"
|
|
2897
|
+
}
|
|
2898
|
+
});
|
|
2899
|
+
return;
|
|
2900
|
+
}
|
|
2901
|
+
args.logger.warn("peer-bytes: body cut before it was complete", {
|
|
2902
|
+
...deviceTags,
|
|
2903
|
+
meta: {
|
|
2904
|
+
label: options.label,
|
|
2905
|
+
branch: result.outcome,
|
|
2906
|
+
sent: result.sent,
|
|
2907
|
+
of: options.bytes.byteLength,
|
|
2908
|
+
stallMs,
|
|
2909
|
+
source: "buffer"
|
|
2910
|
+
}
|
|
2911
|
+
});
|
|
2912
|
+
res.destroy();
|
|
2913
|
+
};
|
|
2914
|
+
return await mint({
|
|
2915
|
+
handler,
|
|
2916
|
+
label: options.label,
|
|
2917
|
+
declaredBytes: options.bytes.byteLength,
|
|
2918
|
+
...options.deviceId !== void 0 ? { deviceId: options.deviceId } : {},
|
|
2919
|
+
...options.ttlMs !== void 0 ? { ttlMs: options.ttlMs } : {}
|
|
2920
|
+
});
|
|
2921
|
+
},
|
|
2922
|
+
offerFile: async (options) => {
|
|
2923
|
+
const deviceTags = options.deviceId !== void 0 ? { tags: { deviceId: options.deviceId } } : {};
|
|
2924
|
+
let declaredBytes = null;
|
|
2925
|
+
try {
|
|
2926
|
+
declaredBytes = (await stat(options.path)).size;
|
|
2927
|
+
} catch {
|
|
2928
|
+
declaredBytes = null;
|
|
2929
|
+
}
|
|
2930
|
+
const handler = async (req, res) => {
|
|
2931
|
+
const headers = {
|
|
2932
|
+
"content-type": options.contentType,
|
|
2933
|
+
"accept-ranges": "none"
|
|
2934
|
+
};
|
|
2935
|
+
if (declaredBytes !== null) headers["content-length"] = String(declaredBytes);
|
|
2936
|
+
if (req.method === "HEAD") {
|
|
2937
|
+
res.writeHead(200, headers).end();
|
|
2938
|
+
return;
|
|
2939
|
+
}
|
|
2940
|
+
const startedAt = now();
|
|
2941
|
+
let stream;
|
|
2942
|
+
try {
|
|
2943
|
+
stream = createReadStream(options.path);
|
|
2944
|
+
await once(stream, "open");
|
|
2945
|
+
} catch (err) {
|
|
2946
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
2947
|
+
args.logger.warn("peer-bytes: the offered file could not be opened", {
|
|
2948
|
+
...deviceTags,
|
|
2949
|
+
meta: {
|
|
2950
|
+
label: options.label,
|
|
2951
|
+
error: detail
|
|
2952
|
+
}
|
|
2953
|
+
});
|
|
2954
|
+
if (!res.headersSent) res.writeHead(503, { "x-camstack-peer-bytes": "file-unreadable" });
|
|
2955
|
+
res.end();
|
|
2956
|
+
return;
|
|
2957
|
+
}
|
|
2958
|
+
res.writeHead(200, headers);
|
|
2959
|
+
let sent = 0;
|
|
2960
|
+
let lastProgressAt = now();
|
|
2961
|
+
stream.pipe(res);
|
|
2962
|
+
stream.on("data", (chunk) => {
|
|
2963
|
+
sent += typeof chunk === "string" ? Buffer.byteLength(chunk) : chunk.byteLength;
|
|
2964
|
+
lastProgressAt = now();
|
|
2965
|
+
});
|
|
2966
|
+
const stallTimer = setInterval(() => {
|
|
2967
|
+
if (now() - lastProgressAt < stallMs) return;
|
|
2968
|
+
clearInterval(stallTimer);
|
|
2969
|
+
args.logger.warn("peer-bytes: body cut before it was complete", {
|
|
2970
|
+
...deviceTags,
|
|
2971
|
+
meta: {
|
|
2972
|
+
label: options.label,
|
|
2973
|
+
branch: "stalled",
|
|
2974
|
+
sent,
|
|
2975
|
+
stallMs,
|
|
2976
|
+
source: "file"
|
|
2977
|
+
}
|
|
2978
|
+
});
|
|
2979
|
+
stream.destroy();
|
|
2980
|
+
res.destroy();
|
|
2981
|
+
}, Math.max(1e3, Math.floor(stallMs / 4)));
|
|
2982
|
+
stallTimer.unref();
|
|
2983
|
+
const ended = await Promise.race([once(stream, "end").then(() => true), once(res, "close").then(() => false)]).catch(() => false);
|
|
2984
|
+
clearInterval(stallTimer);
|
|
2985
|
+
if (ended) {
|
|
2986
|
+
args.logger.info("peer-bytes: body sent", {
|
|
2987
|
+
...deviceTags,
|
|
2988
|
+
meta: {
|
|
2989
|
+
label: options.label,
|
|
2990
|
+
bytes: sent,
|
|
2991
|
+
tookMs: now() - startedAt,
|
|
2992
|
+
source: "file"
|
|
2993
|
+
}
|
|
2994
|
+
});
|
|
2995
|
+
return;
|
|
2996
|
+
}
|
|
2997
|
+
stream.destroy();
|
|
2998
|
+
args.logger.warn("peer-bytes: body cut before it was complete", {
|
|
2999
|
+
...deviceTags,
|
|
3000
|
+
meta: {
|
|
3001
|
+
label: options.label,
|
|
3002
|
+
branch: "peer-gone",
|
|
3003
|
+
sent,
|
|
3004
|
+
of: declaredBytes,
|
|
3005
|
+
source: "file"
|
|
3006
|
+
}
|
|
3007
|
+
});
|
|
3008
|
+
};
|
|
3009
|
+
return await mint({
|
|
3010
|
+
handler,
|
|
3011
|
+
label: options.label,
|
|
3012
|
+
...declaredBytes !== null ? { declaredBytes } : {},
|
|
3013
|
+
...options.deviceId !== void 0 ? { deviceId: options.deviceId } : {},
|
|
3014
|
+
...options.ttlMs !== void 0 ? { ttlMs: options.ttlMs } : {}
|
|
3015
|
+
});
|
|
3016
|
+
},
|
|
3017
|
+
open: async (ticket) => {
|
|
3018
|
+
if (ticket.hostNodeId !== hostNodeId) {
|
|
3019
|
+
args.logger.warn("peer-bytes: refusing a ticket minted on another node", { meta: {
|
|
3020
|
+
mintedOn: ticket.hostNodeId,
|
|
3021
|
+
here: hostNodeId
|
|
3022
|
+
} });
|
|
3023
|
+
return {
|
|
3024
|
+
kind: "refused",
|
|
3025
|
+
code: "cross-node",
|
|
3026
|
+
detail: `this ticket was minted on node "${ticket.hostNodeId}" and this process runs on "${hostNodeId}" — peer-bytes is loopback only, so there is nothing to dial`
|
|
3027
|
+
};
|
|
3028
|
+
}
|
|
3029
|
+
if (ticket.expiresAtMs <= now()) return {
|
|
3030
|
+
kind: "refused",
|
|
3031
|
+
code: "ticket-expired",
|
|
3032
|
+
detail: `this ticket expired ${String(now() - ticket.expiresAtMs)} ms ago`
|
|
3033
|
+
};
|
|
3034
|
+
let url;
|
|
3035
|
+
try {
|
|
3036
|
+
url = new URL(ticket.url);
|
|
3037
|
+
} catch {
|
|
3038
|
+
return {
|
|
3039
|
+
kind: "refused",
|
|
3040
|
+
code: "ticket-malformed",
|
|
3041
|
+
detail: `"${ticket.url}" is not a URL`
|
|
3042
|
+
};
|
|
3043
|
+
}
|
|
3044
|
+
if (url.protocol !== "http:") return {
|
|
3045
|
+
kind: "refused",
|
|
3046
|
+
code: "ticket-malformed",
|
|
3047
|
+
detail: `peer-bytes dials http, not "${url.protocol}"`
|
|
3048
|
+
};
|
|
3049
|
+
const req = request({
|
|
3050
|
+
hostname: url.hostname,
|
|
3051
|
+
port: url.port,
|
|
3052
|
+
path: url.pathname,
|
|
3053
|
+
method: "GET"
|
|
3054
|
+
});
|
|
3055
|
+
let response;
|
|
3056
|
+
try {
|
|
3057
|
+
response = await new Promise((resolve, reject) => {
|
|
3058
|
+
req.once("response", resolve);
|
|
3059
|
+
req.once("error", reject);
|
|
3060
|
+
req.end();
|
|
3061
|
+
});
|
|
3062
|
+
} catch (err) {
|
|
3063
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
3064
|
+
req.destroy();
|
|
3065
|
+
args.logger.warn("peer-bytes: the dial failed", { meta: { error: detail } });
|
|
3066
|
+
return {
|
|
3067
|
+
kind: "refused",
|
|
3068
|
+
code: "unreachable",
|
|
3069
|
+
detail
|
|
3070
|
+
};
|
|
3071
|
+
}
|
|
3072
|
+
response.pause();
|
|
3073
|
+
const status = response.statusCode ?? 0;
|
|
3074
|
+
if (status < 200 || status >= 300) {
|
|
3075
|
+
const reason = response.headers["x-camstack-peer-bytes"];
|
|
3076
|
+
response.resume();
|
|
3077
|
+
req.destroy();
|
|
3078
|
+
return {
|
|
3079
|
+
kind: "refused",
|
|
3080
|
+
code: "producer-refused",
|
|
3081
|
+
detail: typeof reason === "string" ? `the producer answered ${String(status)} (${reason})` : `the producer answered ${String(status)}`,
|
|
3082
|
+
status
|
|
3083
|
+
};
|
|
3084
|
+
}
|
|
3085
|
+
const lengthHeader = response.headers["content-length"];
|
|
3086
|
+
const declaredBytes = typeof lengthHeader === "string" && /^\d+$/.test(lengthHeader) ? Number(lengthHeader) : null;
|
|
3087
|
+
const contentTypeHeader = response.headers["content-type"];
|
|
3088
|
+
const stream = response;
|
|
3089
|
+
return {
|
|
3090
|
+
kind: "body",
|
|
3091
|
+
body: {
|
|
3092
|
+
status,
|
|
3093
|
+
contentType: typeof contentTypeHeader === "string" ? contentTypeHeader : null,
|
|
3094
|
+
declaredBytes,
|
|
3095
|
+
stream,
|
|
3096
|
+
cancel: (reason) => {
|
|
3097
|
+
args.logger.info("peer-bytes: the consumer gave up on a body", { meta: { reason } });
|
|
3098
|
+
response.destroy();
|
|
3099
|
+
req.destroy();
|
|
3100
|
+
},
|
|
3101
|
+
writeToFile: async (path, options) => {
|
|
3102
|
+
await mkdir(dirname(path), { recursive: true });
|
|
3103
|
+
const sink = createWriteStream(path);
|
|
3104
|
+
const discard = async () => {
|
|
3105
|
+
sink.destroy();
|
|
3106
|
+
if (!sink.closed) await once(sink, "close").catch(() => void 0);
|
|
3107
|
+
await rm(path, { force: true });
|
|
3108
|
+
};
|
|
3109
|
+
let written = 0;
|
|
3110
|
+
let overrun = false;
|
|
3111
|
+
try {
|
|
3112
|
+
for await (const chunk of stream) {
|
|
3113
|
+
const buf = asBuffer(chunk);
|
|
3114
|
+
written += buf.byteLength;
|
|
3115
|
+
if (written > options.maxBytes) {
|
|
3116
|
+
overrun = true;
|
|
3117
|
+
break;
|
|
3118
|
+
}
|
|
3119
|
+
if (!sink.write(buf)) await once(sink, "drain");
|
|
3120
|
+
}
|
|
3121
|
+
} catch (err) {
|
|
3122
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
3123
|
+
response.destroy();
|
|
3124
|
+
req.destroy();
|
|
3125
|
+
await discard();
|
|
3126
|
+
return {
|
|
3127
|
+
kind: "refused",
|
|
3128
|
+
code: "transfer-failed",
|
|
3129
|
+
detail,
|
|
3130
|
+
bytes: written
|
|
3131
|
+
};
|
|
3132
|
+
}
|
|
3133
|
+
if (overrun) {
|
|
3134
|
+
response.destroy();
|
|
3135
|
+
req.destroy();
|
|
3136
|
+
await discard();
|
|
3137
|
+
return {
|
|
3138
|
+
kind: "refused",
|
|
3139
|
+
code: "too-large",
|
|
3140
|
+
detail: `the body passed ${String(options.maxBytes)} bytes and was aborted`,
|
|
3141
|
+
bytes: written
|
|
3142
|
+
};
|
|
3143
|
+
}
|
|
3144
|
+
try {
|
|
3145
|
+
sink.end();
|
|
3146
|
+
await once(sink, "close");
|
|
3147
|
+
} catch (err) {
|
|
3148
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
3149
|
+
await discard();
|
|
3150
|
+
return {
|
|
3151
|
+
kind: "refused",
|
|
3152
|
+
code: "write-failed",
|
|
3153
|
+
detail,
|
|
3154
|
+
bytes: written
|
|
3155
|
+
};
|
|
3156
|
+
}
|
|
3157
|
+
return {
|
|
3158
|
+
kind: "written",
|
|
3159
|
+
bytes: written
|
|
3160
|
+
};
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
};
|
|
3164
|
+
}
|
|
3165
|
+
};
|
|
3166
|
+
const dispose = async () => {
|
|
3167
|
+
offers.clear();
|
|
3168
|
+
taken.clear();
|
|
3169
|
+
const open = server;
|
|
3170
|
+
server = null;
|
|
3171
|
+
baseUrl = null;
|
|
3172
|
+
listening = null;
|
|
3173
|
+
if (open === null) return;
|
|
3174
|
+
await new Promise((resolve) => {
|
|
3175
|
+
open.close(() => resolve());
|
|
3176
|
+
open.closeAllConnections();
|
|
3177
|
+
});
|
|
3178
|
+
args.logger.info("peer-bytes: listener closed", { meta: { addonId: args.addonId } });
|
|
3179
|
+
};
|
|
3180
|
+
return {
|
|
3181
|
+
peerBytes,
|
|
3182
|
+
dispose
|
|
3183
|
+
};
|
|
3184
|
+
}
|
|
3185
|
+
//#endregion
|
|
2613
3186
|
//#region src/kernel/deps/npm-command.ts
|
|
2614
3187
|
/**
|
|
2615
3188
|
* npm invocation resolver — makes "is npm available?" a solved problem for
|
|
@@ -11187,6 +11760,12 @@ async function buildAddonContext(runtime, declaration, dataDir, options) {
|
|
|
11187
11760
|
...options?.dataPlaneSink !== void 0 ? { sink: options.dataPlaneSink } : {}
|
|
11188
11761
|
});
|
|
11189
11762
|
workerDisposerChain.add(() => dataPlaneFacility.dispose());
|
|
11763
|
+
const peerBytesFacility = createAddonPeerBytesFacility({
|
|
11764
|
+
addonId,
|
|
11765
|
+
logger: scopedLogger,
|
|
11766
|
+
nodeId
|
|
11767
|
+
});
|
|
11768
|
+
workerDisposerChain.add(() => peerBytesFacility.dispose());
|
|
11190
11769
|
return {
|
|
11191
11770
|
id: addonId,
|
|
11192
11771
|
logger: scopedLogger,
|
|
@@ -11195,6 +11774,7 @@ async function buildAddonContext(runtime, declaration, dataDir, options) {
|
|
|
11195
11774
|
dataDir: addonDataDir,
|
|
11196
11775
|
nodeDataDir,
|
|
11197
11776
|
dataPlane: dataPlaneFacility.dataPlane,
|
|
11777
|
+
peerBytes: peerBytesFacility.peerBytes,
|
|
11198
11778
|
api,
|
|
11199
11779
|
deps: new AddonDepsManager(addonDataDir, scopedLogger),
|
|
11200
11780
|
settings: settingsView,
|
|
@@ -11691,4 +12271,4 @@ async function defaultRun(command, args) {
|
|
|
11691
12271
|
await execFileAsync(command, [...args], { timeout: INSTALL_TIMEOUT_MS });
|
|
11692
12272
|
}
|
|
11693
12273
|
//#endregion
|
|
11694
|
-
export { buildNativeCapProxy as $,
|
|
12274
|
+
export { buildNativeCapProxy as $, strandedMb as $t, createUdsLogger as A, readSocketQueues as An, HEAP_RECLAIM_TRIGGER_MB as At, UDS_NO_ROUTE_PREFIX as B, MAX_CAP_ACTIONS_PER_PEER as Bn, ZERO_LOOP_DELAY as Bt, deviceCapResolverLink as C, EMPTY_SOCKET_PLANE_BASELINE as Cn, resolveNpmInvocation as Ct, createUdsEventBridge as D, createSocketPlaneReader as Dn, createAddonDataPlaneFacility as Dt, createParentUnownedCallHandler as E, createSocketPlaneMeter as En, hostNodeIdOf as Et, callWithServiceDiscovery as F, socketDirectionBytes as Fn, HUB_RSS_BUDGET_ENV as Ft, getCapUsageRegistry as G, createEventPlaneMeter as Gn, describeRss as Gt, CapUsageRegistry as H, createCapActionCensus as Hn, createDeferredHeapWatchSink as Ht, CapRouteError as I, socketDirectionMessages as In, RSS_BUDGET_REANNOUNCE_MIN_MS as It, UdsLocalTransportServer as J, formatEventPlane as Jn, parseRssBudgetMb as Jt, createLocalTransport as K, createEventPlaneReader as Kn, hubMainRssBudget as Kt, classifyCapRoute as L, CAP_ACTION_OVERFLOW_LABEL as Ln, RSS_BUDGET_RELEASE_RATIO as Lt, AGENT_CAP_FWD_ACTION as M, createSocketDirectionCounters as Mn, HEAP_WATCH_WARN_RATIO as Mt, AGENT_CAP_FWD_SERVICE as N, recordSocketFrame as Nn, HUB_MAIN_HEAP_WATCH_LABEL as Nt, createUdsEventBus as O, diffSocketPlane as On, HEAP_RECLAIM_MIN_INTERVAL_MS as Ot, CapRouteResolver as P, sampleSocketDirection as Pn, HUB_MAIN_RSS_BUDGET_MB as Pt, encodeFrame as Q, startRunnerHeapWatch as Qt, LocalChildClient as R, CAP_ACTION_UNLABELLED as Rn, RUNNER_HEAP_WATCH_INTERVAL_MS as Rt, buildLinkChain as S, formatMeshQueue as Sn, buildNpmChildEnv as St, localProviderLink as T, SOCKET_QUEUE_TOP_N as Tn, createAddonPeerBytesFacility as Tt, PARENT_ROUTED_PROVIDER_ADDON_ID as U, diffCapActionCensus as Un, createLoopDelayMeter as Ut, readMoleculerFanoutMode as V, capActionLabel as Vn, buildHeapSample as Vt, __resetCapUsageRegistryForTests as W, EVENT_PLANE_TOP_N as Wn, createV8Reclaimer as Wt, localEndpointPath as X, shouldReclaim as Xt, SocketChannel as Y, reclaimIntervalMs as Yt, FrameDecoder as Z, startHeapWatch as Zt, AddonDepsManager as _, OUTBOUND_QUEUE_THRESHOLD_BYTES as _n, CapabilityHandle as _t, createUdsAddonContext as a, createInspectorArrayBufferCensus as an, setWorkerNativeCapsChangeListener as at, brokerCallForCap as b, MESH_QUEUE_TOP_N as bn, installManifestNativeDeps as bt, getOrInitReadinessRegistryForClient as c, DECISIVE_HEAP_SPACES as cn, NATIVE_PROVIDER_SERVICE_INFIX as ct, clusterEventTopic as d, formatHeapSpaces as dn, capBareAction as dt, ARRAY_BUFFER_CENSUS_COOLDOWN_MS as en, buildUdsNativeCapProxy as et, getBrokerEventBus as f, heapSpaceField as fn, capServiceName as ft, subscribePassthrough as g, OUTBOUND_QUEUE_SUSTAIN_MS as gn, DeviceRegistry as gt, setNodeEventInterest as h, OUTBOUND_QUEUE_COOLDOWN_MS as hn, serializeTypedArrays as ht, createAddonContext as i, createArrayBufferCensusGate as in, mountNativeCapService as it, createUdsLoggerWithControl as j, EMPTY_SOCKET_DIRECTION as jn, HEAP_WATCH_INTERVAL_MS as jt, udsChildLogToWorkerEntry as k, formatSocketPlane as kn, HEAP_RECLAIM_STEADY_STATE_INTERVAL_MS as kt, setHubConnected as l, HEAP_SPACE_REPORT_MIN_MB as ln, capActionName as lt, registerEventBusService as m, selectReportedSpaces as mn, deserializeTypedArrays as mt, installManifestPythonDeps as n, ARRAY_BUFFER_CENSUS_THRESHOLD_MB as nn, getWorkerNativeCapProvider as nt, getWorkerDeviceRegistry as o, formatArrayBufferCensus as on, createAddonService as ot, getMoleculerEventStats as p, readHeapSpaces as pn, parseCapAction as pt, UdsLocalTransportClient as q, diffEventPlane as qn, nextRssBudgetState as qt, adaptBrokerToCluster as r, ARRAY_BUFFER_RELEASE_MIN_DROP_RATIO as rn, getWorkerNativeCapSnapshot as rt, getOrInitReadinessRegistry as s, isArrayBufferCensusReport as sn, validateProviderRegistrations as st, installManifestSystemDeps as t, ARRAY_BUFFER_CENSUS_SUSTAIN_MS as tn, createBrokerDeviceManagerApi as tt, EVENT_TOPIC_PREFIX as u, emitHeapDiagnosticReport as un, capActionSuffix as ut, createKernelHwAccel as v, createOutboundQueueGate as vn, CapabilityUnavailableError as vt, ipcParentLink as w, SOCKET_PLANE_TOP_N as wn, runNpm as wt, brokerTransportLink as x, createMeshQueueReader as xn, resolveAddonClass as xt, resolveHwAccel as y, formatOutboundQueueVerdict as yn, copyBundledNativeModules as yt, LocalChildRegistry as z, EMPTY_CAP_ACTION_READING as zn, RUNNER_RSS_BUDGET_ENV as zt };
|