@camstack/system 1.2.288 → 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,4 +1,5 @@
|
|
|
1
1
|
const require_chunk = require("./chunk-Cek0wNdY.js");
|
|
2
|
+
const require_dist = require("./dist-KRK9zLWt.js");
|
|
2
3
|
require("./hub-cap-forward-DmHNjbB0.js");
|
|
3
4
|
let node_crypto = require("node:crypto");
|
|
4
5
|
node_crypto = require_chunk.__toESM(node_crypto);
|
|
@@ -20,6 +21,7 @@ let node_perf_hooks = require("node:perf_hooks");
|
|
|
20
21
|
let node_vm = require("node:vm");
|
|
21
22
|
node_vm = require_chunk.__toESM(node_vm);
|
|
22
23
|
let _camstack_types_addon = require("@camstack/types/addon");
|
|
24
|
+
let node_events = require("node:events");
|
|
23
25
|
let node_net = require("node:net");
|
|
24
26
|
let _trpc_client = require("@trpc/client");
|
|
25
27
|
//#region src/kernel/event-plane-report.ts
|
|
@@ -2613,6 +2615,577 @@ function createAddonDataPlaneFacility(args) {
|
|
|
2613
2615
|
};
|
|
2614
2616
|
}
|
|
2615
2617
|
//#endregion
|
|
2618
|
+
//#region src/kernel/moleculer/addon-peer-bytes-facility.ts
|
|
2619
|
+
/**
|
|
2620
|
+
* Kernel side of the addon→addon **byte transport** (`ctx.peerBytes`, D613).
|
|
2621
|
+
*
|
|
2622
|
+
* See {@link AddonPeerBytes} for what this exists for and what it deliberately
|
|
2623
|
+
* does not do. This file is the mechanism:
|
|
2624
|
+
*
|
|
2625
|
+
* - **One lazy loopback listener per addon process.** Bound on the first
|
|
2626
|
+
* `offer()`, never before, so an addon that never offers pays no port.
|
|
2627
|
+
* - **One token per offer.** 24 random bytes, base64url, ONE `GET`. A replay
|
|
2628
|
+
* is `offer-used` (410) for as long as the original ticket would have lived,
|
|
2629
|
+
* so a second take is told what happened rather than "no such thing".
|
|
2630
|
+
* `HEAD` answers the headers and consumes nothing.
|
|
2631
|
+
* - **Outstanding offers are bounded and swept on every mint**, which is also
|
|
2632
|
+
* what makes a per-frame misuse of this facility refuse by name instead of
|
|
2633
|
+
* working slowly.
|
|
2634
|
+
* - **Every write honours `write()`'s return and awaits `drain`** (D447), and
|
|
2635
|
+
* a peer that has taken nothing for {@link PEER_BYTES_STALL_MS} is cut. Every
|
|
2636
|
+
* exit — offered, taken, sent, cut, stalled, refused, disposed — is one log
|
|
2637
|
+
* line, carrying `tags: { deviceId }` when the offer named a device.
|
|
2638
|
+
* - **The consumer's response is handed over PAUSED**, which is the whole
|
|
2639
|
+
* reason `open()` lives in the framework rather than in each consumer: see
|
|
2640
|
+
* {@link PeerBytesBody}.
|
|
2641
|
+
*/
|
|
2642
|
+
/** The HOST a node id belongs to. `hub` → `hub`; `hub/runner-3` → `hub`. */
|
|
2643
|
+
function hostNodeIdOf(nodeId) {
|
|
2644
|
+
const slash = nodeId.indexOf("/");
|
|
2645
|
+
return slash === -1 ? nodeId : nodeId.slice(0, slash);
|
|
2646
|
+
}
|
|
2647
|
+
/** A body chunk, without a cast. A readable in binary mode yields Buffers; in
|
|
2648
|
+
* string mode, strings. Anything else is a broken stream and says so. */
|
|
2649
|
+
function asBuffer(chunk) {
|
|
2650
|
+
if (Buffer.isBuffer(chunk)) return chunk;
|
|
2651
|
+
if (typeof chunk === "string") return Buffer.from(chunk);
|
|
2652
|
+
throw new Error("peer-bytes: a body chunk was neither a Buffer nor a string");
|
|
2653
|
+
}
|
|
2654
|
+
function tokenOf(req) {
|
|
2655
|
+
const raw = req.url ?? "/";
|
|
2656
|
+
const query = raw.indexOf("?");
|
|
2657
|
+
return (query === -1 ? raw : raw.slice(0, query)).replace(/^\/+/, "");
|
|
2658
|
+
}
|
|
2659
|
+
/**
|
|
2660
|
+
* Write `bytes` to `res` in bounded chunks, honouring backpressure.
|
|
2661
|
+
*
|
|
2662
|
+
* D447, the consuming half stated from the producing side: a raw HTTP stream
|
|
2663
|
+
* writes no chunk the socket has not taken. `write()` returning false means the
|
|
2664
|
+
* kernel buffer is full and everything further is heap in THIS process — so we
|
|
2665
|
+
* wait for `drain`, and a peer that never drains within `stallMs` is cut rather
|
|
2666
|
+
* than held. Returns how many bytes reached the socket and why it stopped.
|
|
2667
|
+
*/
|
|
2668
|
+
async function writeBufferWithBackpressure(res, bytes, stallMs) {
|
|
2669
|
+
const waitForDrain = () => new Promise((resolve) => {
|
|
2670
|
+
const settle = (outcome) => {
|
|
2671
|
+
clearTimeout(timer);
|
|
2672
|
+
res.off("drain", onDrain);
|
|
2673
|
+
res.off("close", onClose);
|
|
2674
|
+
resolve(outcome);
|
|
2675
|
+
};
|
|
2676
|
+
const onDrain = () => {
|
|
2677
|
+
settle("drained");
|
|
2678
|
+
};
|
|
2679
|
+
const onClose = () => {
|
|
2680
|
+
settle("closed");
|
|
2681
|
+
};
|
|
2682
|
+
const timer = setTimeout(() => {
|
|
2683
|
+
settle("stalled");
|
|
2684
|
+
}, stallMs);
|
|
2685
|
+
timer.unref();
|
|
2686
|
+
res.on("drain", onDrain);
|
|
2687
|
+
res.on("close", onClose);
|
|
2688
|
+
});
|
|
2689
|
+
let sent = 0;
|
|
2690
|
+
while (sent < bytes.byteLength) {
|
|
2691
|
+
if (res.destroyed || res.writableEnded) return {
|
|
2692
|
+
sent,
|
|
2693
|
+
outcome: "peer-gone"
|
|
2694
|
+
};
|
|
2695
|
+
const end = Math.min(sent + require_dist.PEER_BYTES_CHUNK_BYTES, bytes.byteLength);
|
|
2696
|
+
const took = res.write(bytes.subarray(sent, end));
|
|
2697
|
+
sent = end;
|
|
2698
|
+
if (took) continue;
|
|
2699
|
+
const waited = await waitForDrain();
|
|
2700
|
+
if (waited === "drained") continue;
|
|
2701
|
+
return {
|
|
2702
|
+
sent,
|
|
2703
|
+
outcome: waited === "closed" ? "peer-gone" : "stalled"
|
|
2704
|
+
};
|
|
2705
|
+
}
|
|
2706
|
+
return {
|
|
2707
|
+
sent,
|
|
2708
|
+
outcome: "sent"
|
|
2709
|
+
};
|
|
2710
|
+
}
|
|
2711
|
+
function createAddonPeerBytesFacility(args) {
|
|
2712
|
+
const now = args.now ?? (() => Date.now());
|
|
2713
|
+
const ttlMs = args.ttlMs ?? 3e4;
|
|
2714
|
+
const maxOutstanding = args.maxOutstanding ?? 64;
|
|
2715
|
+
const stallMs = args.stallMs ?? 15e3;
|
|
2716
|
+
const host = args.host ?? "127.0.0.1";
|
|
2717
|
+
const hostNodeId = hostNodeIdOf(args.nodeId);
|
|
2718
|
+
const offers = /* @__PURE__ */ new Map();
|
|
2719
|
+
/** Tokens already taken, kept for their TTL so a replay is `offer-used`. */
|
|
2720
|
+
const taken = /* @__PURE__ */ new Map();
|
|
2721
|
+
let server = null;
|
|
2722
|
+
let baseUrl = null;
|
|
2723
|
+
let listening = null;
|
|
2724
|
+
const sweep = () => {
|
|
2725
|
+
const at = now();
|
|
2726
|
+
for (const [token, offer] of offers) if (offer.expiresAtMs <= at) {
|
|
2727
|
+
offers.delete(token);
|
|
2728
|
+
args.logger.info("peer-bytes: an offer expired untaken", {
|
|
2729
|
+
...offer.deviceId !== void 0 ? { tags: { deviceId: offer.deviceId } } : {},
|
|
2730
|
+
meta: {
|
|
2731
|
+
label: offer.label,
|
|
2732
|
+
outstanding: offers.size
|
|
2733
|
+
}
|
|
2734
|
+
});
|
|
2735
|
+
}
|
|
2736
|
+
for (const [token, expiresAtMs] of taken) if (expiresAtMs <= at) taken.delete(token);
|
|
2737
|
+
};
|
|
2738
|
+
const route = (req, res) => {
|
|
2739
|
+
const token = tokenOf(req);
|
|
2740
|
+
const offer = offers.get(token);
|
|
2741
|
+
if (offer === void 0) {
|
|
2742
|
+
const status = taken.has(token) ? 410 : 404;
|
|
2743
|
+
res.writeHead(status, { "x-camstack-peer-bytes": status === 410 ? "offer-used" : "no-offer" });
|
|
2744
|
+
res.end();
|
|
2745
|
+
return;
|
|
2746
|
+
}
|
|
2747
|
+
if (offer.expiresAtMs <= now()) {
|
|
2748
|
+
offers.delete(token);
|
|
2749
|
+
res.writeHead(410, { "x-camstack-peer-bytes": "offer-expired" }).end();
|
|
2750
|
+
return;
|
|
2751
|
+
}
|
|
2752
|
+
if (req.method === "GET") {
|
|
2753
|
+
offers.delete(token);
|
|
2754
|
+
taken.set(token, offer.expiresAtMs);
|
|
2755
|
+
args.logger.info("peer-bytes: an offer was taken", {
|
|
2756
|
+
...offer.deviceId !== void 0 ? { tags: { deviceId: offer.deviceId } } : {},
|
|
2757
|
+
meta: {
|
|
2758
|
+
label: offer.label,
|
|
2759
|
+
outstanding: offers.size
|
|
2760
|
+
}
|
|
2761
|
+
});
|
|
2762
|
+
}
|
|
2763
|
+
Promise.resolve(offer.handler(req, res)).catch((err) => {
|
|
2764
|
+
args.logger.warn("peer-bytes: an offer handler threw", {
|
|
2765
|
+
...offer.deviceId !== void 0 ? { tags: { deviceId: offer.deviceId } } : {},
|
|
2766
|
+
meta: {
|
|
2767
|
+
label: offer.label,
|
|
2768
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2769
|
+
}
|
|
2770
|
+
});
|
|
2771
|
+
if (!res.headersSent) res.writeHead(500);
|
|
2772
|
+
res.end();
|
|
2773
|
+
});
|
|
2774
|
+
};
|
|
2775
|
+
const listen = () => {
|
|
2776
|
+
if (listening !== null) return listening;
|
|
2777
|
+
listening = new Promise((resolve, reject) => {
|
|
2778
|
+
const created = (0, node_http.createServer)((req, res) => {
|
|
2779
|
+
route(req, res);
|
|
2780
|
+
});
|
|
2781
|
+
created.on("error", (err) => {
|
|
2782
|
+
listening = null;
|
|
2783
|
+
reject(err);
|
|
2784
|
+
});
|
|
2785
|
+
created.listen(0, host, () => {
|
|
2786
|
+
const address = created.address();
|
|
2787
|
+
if (address === null || typeof address === "string") {
|
|
2788
|
+
created.close();
|
|
2789
|
+
listening = null;
|
|
2790
|
+
reject(/* @__PURE__ */ new Error("the peer-bytes listener bound no TCP port"));
|
|
2791
|
+
return;
|
|
2792
|
+
}
|
|
2793
|
+
server = created;
|
|
2794
|
+
baseUrl = `http://${host}:${address.port}`;
|
|
2795
|
+
args.logger.info("peer-bytes: listener bound", { meta: {
|
|
2796
|
+
addonId: args.addonId,
|
|
2797
|
+
host,
|
|
2798
|
+
port: address.port,
|
|
2799
|
+
hostNodeId,
|
|
2800
|
+
ttlMs
|
|
2801
|
+
} });
|
|
2802
|
+
resolve(baseUrl);
|
|
2803
|
+
});
|
|
2804
|
+
});
|
|
2805
|
+
return listening;
|
|
2806
|
+
};
|
|
2807
|
+
const mint = async (options) => {
|
|
2808
|
+
sweep();
|
|
2809
|
+
const deviceTags = options.deviceId !== void 0 ? { tags: { deviceId: options.deviceId } } : {};
|
|
2810
|
+
if (offers.size >= maxOutstanding) {
|
|
2811
|
+
args.logger.warn("peer-bytes: offer refused — too many outstanding", {
|
|
2812
|
+
...deviceTags,
|
|
2813
|
+
meta: {
|
|
2814
|
+
label: options.label,
|
|
2815
|
+
outstanding: offers.size,
|
|
2816
|
+
maxOutstanding
|
|
2817
|
+
}
|
|
2818
|
+
});
|
|
2819
|
+
return {
|
|
2820
|
+
kind: "refused",
|
|
2821
|
+
code: "offer-exhausted",
|
|
2822
|
+
detail: `${String(offers.size)} offers outstanding against a bound of ${String(maxOutstanding)}`
|
|
2823
|
+
};
|
|
2824
|
+
}
|
|
2825
|
+
let base;
|
|
2826
|
+
try {
|
|
2827
|
+
base = baseUrl ?? await listen();
|
|
2828
|
+
} catch (err) {
|
|
2829
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
2830
|
+
args.logger.warn("peer-bytes: offer refused — no listener", {
|
|
2831
|
+
...deviceTags,
|
|
2832
|
+
meta: {
|
|
2833
|
+
label: options.label,
|
|
2834
|
+
error: detail
|
|
2835
|
+
}
|
|
2836
|
+
});
|
|
2837
|
+
return {
|
|
2838
|
+
kind: "refused",
|
|
2839
|
+
code: "listener-failed",
|
|
2840
|
+
detail
|
|
2841
|
+
};
|
|
2842
|
+
}
|
|
2843
|
+
const token = (0, node_crypto.randomBytes)(24).toString("base64url");
|
|
2844
|
+
const expiresAtMs = now() + (options.ttlMs ?? ttlMs);
|
|
2845
|
+
offers.set(token, {
|
|
2846
|
+
handler: options.handler,
|
|
2847
|
+
label: options.label,
|
|
2848
|
+
deviceId: options.deviceId,
|
|
2849
|
+
expiresAtMs
|
|
2850
|
+
});
|
|
2851
|
+
args.logger.info("peer-bytes: offer minted", {
|
|
2852
|
+
...deviceTags,
|
|
2853
|
+
meta: {
|
|
2854
|
+
label: options.label,
|
|
2855
|
+
declaredBytes: options.declaredBytes ?? null,
|
|
2856
|
+
expiresInMs: expiresAtMs - now(),
|
|
2857
|
+
outstanding: offers.size
|
|
2858
|
+
}
|
|
2859
|
+
});
|
|
2860
|
+
return {
|
|
2861
|
+
kind: "ticket",
|
|
2862
|
+
ticket: {
|
|
2863
|
+
url: `${base}/${token}`,
|
|
2864
|
+
hostNodeId,
|
|
2865
|
+
expiresAtMs,
|
|
2866
|
+
declaredBytes: options.declaredBytes ?? null
|
|
2867
|
+
}
|
|
2868
|
+
};
|
|
2869
|
+
};
|
|
2870
|
+
const peerBytes = {
|
|
2871
|
+
hostNodeId,
|
|
2872
|
+
get outstanding() {
|
|
2873
|
+
sweep();
|
|
2874
|
+
return offers.size;
|
|
2875
|
+
},
|
|
2876
|
+
offer: mint,
|
|
2877
|
+
offerBuffer: async (options) => {
|
|
2878
|
+
const deviceTags = options.deviceId !== void 0 ? { tags: { deviceId: options.deviceId } } : {};
|
|
2879
|
+
const handler = async (req, res) => {
|
|
2880
|
+
res.writeHead(200, {
|
|
2881
|
+
"content-type": options.contentType,
|
|
2882
|
+
"content-length": String(options.bytes.byteLength),
|
|
2883
|
+
"accept-ranges": "none"
|
|
2884
|
+
});
|
|
2885
|
+
if (req.method === "HEAD") {
|
|
2886
|
+
res.end();
|
|
2887
|
+
return;
|
|
2888
|
+
}
|
|
2889
|
+
const startedAt = now();
|
|
2890
|
+
const result = await writeBufferWithBackpressure(res, options.bytes, stallMs);
|
|
2891
|
+
if (result.outcome === "sent") {
|
|
2892
|
+
res.end();
|
|
2893
|
+
args.logger.info("peer-bytes: body sent", {
|
|
2894
|
+
...deviceTags,
|
|
2895
|
+
meta: {
|
|
2896
|
+
label: options.label,
|
|
2897
|
+
bytes: result.sent,
|
|
2898
|
+
tookMs: now() - startedAt,
|
|
2899
|
+
source: "buffer"
|
|
2900
|
+
}
|
|
2901
|
+
});
|
|
2902
|
+
return;
|
|
2903
|
+
}
|
|
2904
|
+
args.logger.warn("peer-bytes: body cut before it was complete", {
|
|
2905
|
+
...deviceTags,
|
|
2906
|
+
meta: {
|
|
2907
|
+
label: options.label,
|
|
2908
|
+
branch: result.outcome,
|
|
2909
|
+
sent: result.sent,
|
|
2910
|
+
of: options.bytes.byteLength,
|
|
2911
|
+
stallMs,
|
|
2912
|
+
source: "buffer"
|
|
2913
|
+
}
|
|
2914
|
+
});
|
|
2915
|
+
res.destroy();
|
|
2916
|
+
};
|
|
2917
|
+
return await mint({
|
|
2918
|
+
handler,
|
|
2919
|
+
label: options.label,
|
|
2920
|
+
declaredBytes: options.bytes.byteLength,
|
|
2921
|
+
...options.deviceId !== void 0 ? { deviceId: options.deviceId } : {},
|
|
2922
|
+
...options.ttlMs !== void 0 ? { ttlMs: options.ttlMs } : {}
|
|
2923
|
+
});
|
|
2924
|
+
},
|
|
2925
|
+
offerFile: async (options) => {
|
|
2926
|
+
const deviceTags = options.deviceId !== void 0 ? { tags: { deviceId: options.deviceId } } : {};
|
|
2927
|
+
let declaredBytes = null;
|
|
2928
|
+
try {
|
|
2929
|
+
declaredBytes = (await (0, node_fs_promises.stat)(options.path)).size;
|
|
2930
|
+
} catch {
|
|
2931
|
+
declaredBytes = null;
|
|
2932
|
+
}
|
|
2933
|
+
const handler = async (req, res) => {
|
|
2934
|
+
const headers = {
|
|
2935
|
+
"content-type": options.contentType,
|
|
2936
|
+
"accept-ranges": "none"
|
|
2937
|
+
};
|
|
2938
|
+
if (declaredBytes !== null) headers["content-length"] = String(declaredBytes);
|
|
2939
|
+
if (req.method === "HEAD") {
|
|
2940
|
+
res.writeHead(200, headers).end();
|
|
2941
|
+
return;
|
|
2942
|
+
}
|
|
2943
|
+
const startedAt = now();
|
|
2944
|
+
let stream;
|
|
2945
|
+
try {
|
|
2946
|
+
stream = (0, node_fs.createReadStream)(options.path);
|
|
2947
|
+
await (0, node_events.once)(stream, "open");
|
|
2948
|
+
} catch (err) {
|
|
2949
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
2950
|
+
args.logger.warn("peer-bytes: the offered file could not be opened", {
|
|
2951
|
+
...deviceTags,
|
|
2952
|
+
meta: {
|
|
2953
|
+
label: options.label,
|
|
2954
|
+
error: detail
|
|
2955
|
+
}
|
|
2956
|
+
});
|
|
2957
|
+
if (!res.headersSent) res.writeHead(503, { "x-camstack-peer-bytes": "file-unreadable" });
|
|
2958
|
+
res.end();
|
|
2959
|
+
return;
|
|
2960
|
+
}
|
|
2961
|
+
res.writeHead(200, headers);
|
|
2962
|
+
let sent = 0;
|
|
2963
|
+
let lastProgressAt = now();
|
|
2964
|
+
stream.pipe(res);
|
|
2965
|
+
stream.on("data", (chunk) => {
|
|
2966
|
+
sent += typeof chunk === "string" ? Buffer.byteLength(chunk) : chunk.byteLength;
|
|
2967
|
+
lastProgressAt = now();
|
|
2968
|
+
});
|
|
2969
|
+
const stallTimer = setInterval(() => {
|
|
2970
|
+
if (now() - lastProgressAt < stallMs) return;
|
|
2971
|
+
clearInterval(stallTimer);
|
|
2972
|
+
args.logger.warn("peer-bytes: body cut before it was complete", {
|
|
2973
|
+
...deviceTags,
|
|
2974
|
+
meta: {
|
|
2975
|
+
label: options.label,
|
|
2976
|
+
branch: "stalled",
|
|
2977
|
+
sent,
|
|
2978
|
+
stallMs,
|
|
2979
|
+
source: "file"
|
|
2980
|
+
}
|
|
2981
|
+
});
|
|
2982
|
+
stream.destroy();
|
|
2983
|
+
res.destroy();
|
|
2984
|
+
}, Math.max(1e3, Math.floor(stallMs / 4)));
|
|
2985
|
+
stallTimer.unref();
|
|
2986
|
+
const ended = await Promise.race([(0, node_events.once)(stream, "end").then(() => true), (0, node_events.once)(res, "close").then(() => false)]).catch(() => false);
|
|
2987
|
+
clearInterval(stallTimer);
|
|
2988
|
+
if (ended) {
|
|
2989
|
+
args.logger.info("peer-bytes: body sent", {
|
|
2990
|
+
...deviceTags,
|
|
2991
|
+
meta: {
|
|
2992
|
+
label: options.label,
|
|
2993
|
+
bytes: sent,
|
|
2994
|
+
tookMs: now() - startedAt,
|
|
2995
|
+
source: "file"
|
|
2996
|
+
}
|
|
2997
|
+
});
|
|
2998
|
+
return;
|
|
2999
|
+
}
|
|
3000
|
+
stream.destroy();
|
|
3001
|
+
args.logger.warn("peer-bytes: body cut before it was complete", {
|
|
3002
|
+
...deviceTags,
|
|
3003
|
+
meta: {
|
|
3004
|
+
label: options.label,
|
|
3005
|
+
branch: "peer-gone",
|
|
3006
|
+
sent,
|
|
3007
|
+
of: declaredBytes,
|
|
3008
|
+
source: "file"
|
|
3009
|
+
}
|
|
3010
|
+
});
|
|
3011
|
+
};
|
|
3012
|
+
return await mint({
|
|
3013
|
+
handler,
|
|
3014
|
+
label: options.label,
|
|
3015
|
+
...declaredBytes !== null ? { declaredBytes } : {},
|
|
3016
|
+
...options.deviceId !== void 0 ? { deviceId: options.deviceId } : {},
|
|
3017
|
+
...options.ttlMs !== void 0 ? { ttlMs: options.ttlMs } : {}
|
|
3018
|
+
});
|
|
3019
|
+
},
|
|
3020
|
+
open: async (ticket) => {
|
|
3021
|
+
if (ticket.hostNodeId !== hostNodeId) {
|
|
3022
|
+
args.logger.warn("peer-bytes: refusing a ticket minted on another node", { meta: {
|
|
3023
|
+
mintedOn: ticket.hostNodeId,
|
|
3024
|
+
here: hostNodeId
|
|
3025
|
+
} });
|
|
3026
|
+
return {
|
|
3027
|
+
kind: "refused",
|
|
3028
|
+
code: "cross-node",
|
|
3029
|
+
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`
|
|
3030
|
+
};
|
|
3031
|
+
}
|
|
3032
|
+
if (ticket.expiresAtMs <= now()) return {
|
|
3033
|
+
kind: "refused",
|
|
3034
|
+
code: "ticket-expired",
|
|
3035
|
+
detail: `this ticket expired ${String(now() - ticket.expiresAtMs)} ms ago`
|
|
3036
|
+
};
|
|
3037
|
+
let url;
|
|
3038
|
+
try {
|
|
3039
|
+
url = new URL(ticket.url);
|
|
3040
|
+
} catch {
|
|
3041
|
+
return {
|
|
3042
|
+
kind: "refused",
|
|
3043
|
+
code: "ticket-malformed",
|
|
3044
|
+
detail: `"${ticket.url}" is not a URL`
|
|
3045
|
+
};
|
|
3046
|
+
}
|
|
3047
|
+
if (url.protocol !== "http:") return {
|
|
3048
|
+
kind: "refused",
|
|
3049
|
+
code: "ticket-malformed",
|
|
3050
|
+
detail: `peer-bytes dials http, not "${url.protocol}"`
|
|
3051
|
+
};
|
|
3052
|
+
const req = (0, node_http.request)({
|
|
3053
|
+
hostname: url.hostname,
|
|
3054
|
+
port: url.port,
|
|
3055
|
+
path: url.pathname,
|
|
3056
|
+
method: "GET"
|
|
3057
|
+
});
|
|
3058
|
+
let response;
|
|
3059
|
+
try {
|
|
3060
|
+
response = await new Promise((resolve, reject) => {
|
|
3061
|
+
req.once("response", resolve);
|
|
3062
|
+
req.once("error", reject);
|
|
3063
|
+
req.end();
|
|
3064
|
+
});
|
|
3065
|
+
} catch (err) {
|
|
3066
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
3067
|
+
req.destroy();
|
|
3068
|
+
args.logger.warn("peer-bytes: the dial failed", { meta: { error: detail } });
|
|
3069
|
+
return {
|
|
3070
|
+
kind: "refused",
|
|
3071
|
+
code: "unreachable",
|
|
3072
|
+
detail
|
|
3073
|
+
};
|
|
3074
|
+
}
|
|
3075
|
+
response.pause();
|
|
3076
|
+
const status = response.statusCode ?? 0;
|
|
3077
|
+
if (status < 200 || status >= 300) {
|
|
3078
|
+
const reason = response.headers["x-camstack-peer-bytes"];
|
|
3079
|
+
response.resume();
|
|
3080
|
+
req.destroy();
|
|
3081
|
+
return {
|
|
3082
|
+
kind: "refused",
|
|
3083
|
+
code: "producer-refused",
|
|
3084
|
+
detail: typeof reason === "string" ? `the producer answered ${String(status)} (${reason})` : `the producer answered ${String(status)}`,
|
|
3085
|
+
status
|
|
3086
|
+
};
|
|
3087
|
+
}
|
|
3088
|
+
const lengthHeader = response.headers["content-length"];
|
|
3089
|
+
const declaredBytes = typeof lengthHeader === "string" && /^\d+$/.test(lengthHeader) ? Number(lengthHeader) : null;
|
|
3090
|
+
const contentTypeHeader = response.headers["content-type"];
|
|
3091
|
+
const stream = response;
|
|
3092
|
+
return {
|
|
3093
|
+
kind: "body",
|
|
3094
|
+
body: {
|
|
3095
|
+
status,
|
|
3096
|
+
contentType: typeof contentTypeHeader === "string" ? contentTypeHeader : null,
|
|
3097
|
+
declaredBytes,
|
|
3098
|
+
stream,
|
|
3099
|
+
cancel: (reason) => {
|
|
3100
|
+
args.logger.info("peer-bytes: the consumer gave up on a body", { meta: { reason } });
|
|
3101
|
+
response.destroy();
|
|
3102
|
+
req.destroy();
|
|
3103
|
+
},
|
|
3104
|
+
writeToFile: async (path, options) => {
|
|
3105
|
+
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(path), { recursive: true });
|
|
3106
|
+
const sink = (0, node_fs.createWriteStream)(path);
|
|
3107
|
+
const discard = async () => {
|
|
3108
|
+
sink.destroy();
|
|
3109
|
+
if (!sink.closed) await (0, node_events.once)(sink, "close").catch(() => void 0);
|
|
3110
|
+
await (0, node_fs_promises.rm)(path, { force: true });
|
|
3111
|
+
};
|
|
3112
|
+
let written = 0;
|
|
3113
|
+
let overrun = false;
|
|
3114
|
+
try {
|
|
3115
|
+
for await (const chunk of stream) {
|
|
3116
|
+
const buf = asBuffer(chunk);
|
|
3117
|
+
written += buf.byteLength;
|
|
3118
|
+
if (written > options.maxBytes) {
|
|
3119
|
+
overrun = true;
|
|
3120
|
+
break;
|
|
3121
|
+
}
|
|
3122
|
+
if (!sink.write(buf)) await (0, node_events.once)(sink, "drain");
|
|
3123
|
+
}
|
|
3124
|
+
} catch (err) {
|
|
3125
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
3126
|
+
response.destroy();
|
|
3127
|
+
req.destroy();
|
|
3128
|
+
await discard();
|
|
3129
|
+
return {
|
|
3130
|
+
kind: "refused",
|
|
3131
|
+
code: "transfer-failed",
|
|
3132
|
+
detail,
|
|
3133
|
+
bytes: written
|
|
3134
|
+
};
|
|
3135
|
+
}
|
|
3136
|
+
if (overrun) {
|
|
3137
|
+
response.destroy();
|
|
3138
|
+
req.destroy();
|
|
3139
|
+
await discard();
|
|
3140
|
+
return {
|
|
3141
|
+
kind: "refused",
|
|
3142
|
+
code: "too-large",
|
|
3143
|
+
detail: `the body passed ${String(options.maxBytes)} bytes and was aborted`,
|
|
3144
|
+
bytes: written
|
|
3145
|
+
};
|
|
3146
|
+
}
|
|
3147
|
+
try {
|
|
3148
|
+
sink.end();
|
|
3149
|
+
await (0, node_events.once)(sink, "close");
|
|
3150
|
+
} catch (err) {
|
|
3151
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
3152
|
+
await discard();
|
|
3153
|
+
return {
|
|
3154
|
+
kind: "refused",
|
|
3155
|
+
code: "write-failed",
|
|
3156
|
+
detail,
|
|
3157
|
+
bytes: written
|
|
3158
|
+
};
|
|
3159
|
+
}
|
|
3160
|
+
return {
|
|
3161
|
+
kind: "written",
|
|
3162
|
+
bytes: written
|
|
3163
|
+
};
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
};
|
|
3167
|
+
}
|
|
3168
|
+
};
|
|
3169
|
+
const dispose = async () => {
|
|
3170
|
+
offers.clear();
|
|
3171
|
+
taken.clear();
|
|
3172
|
+
const open = server;
|
|
3173
|
+
server = null;
|
|
3174
|
+
baseUrl = null;
|
|
3175
|
+
listening = null;
|
|
3176
|
+
if (open === null) return;
|
|
3177
|
+
await new Promise((resolve) => {
|
|
3178
|
+
open.close(() => resolve());
|
|
3179
|
+
open.closeAllConnections();
|
|
3180
|
+
});
|
|
3181
|
+
args.logger.info("peer-bytes: listener closed", { meta: { addonId: args.addonId } });
|
|
3182
|
+
};
|
|
3183
|
+
return {
|
|
3184
|
+
peerBytes,
|
|
3185
|
+
dispose
|
|
3186
|
+
};
|
|
3187
|
+
}
|
|
3188
|
+
//#endregion
|
|
2616
3189
|
//#region src/kernel/deps/npm-command.ts
|
|
2617
3190
|
/**
|
|
2618
3191
|
* npm invocation resolver — makes "is npm available?" a solved problem for
|
|
@@ -11190,6 +11763,12 @@ async function buildAddonContext(runtime, declaration, dataDir, options) {
|
|
|
11190
11763
|
...options?.dataPlaneSink !== void 0 ? { sink: options.dataPlaneSink } : {}
|
|
11191
11764
|
});
|
|
11192
11765
|
workerDisposerChain.add(() => dataPlaneFacility.dispose());
|
|
11766
|
+
const peerBytesFacility = createAddonPeerBytesFacility({
|
|
11767
|
+
addonId,
|
|
11768
|
+
logger: scopedLogger,
|
|
11769
|
+
nodeId
|
|
11770
|
+
});
|
|
11771
|
+
workerDisposerChain.add(() => peerBytesFacility.dispose());
|
|
11193
11772
|
return {
|
|
11194
11773
|
id: addonId,
|
|
11195
11774
|
logger: scopedLogger,
|
|
@@ -11198,6 +11777,7 @@ async function buildAddonContext(runtime, declaration, dataDir, options) {
|
|
|
11198
11777
|
dataDir: addonDataDir,
|
|
11199
11778
|
nodeDataDir,
|
|
11200
11779
|
dataPlane: dataPlaneFacility.dataPlane,
|
|
11780
|
+
peerBytes: peerBytesFacility.peerBytes,
|
|
11201
11781
|
api,
|
|
11202
11782
|
deps: new AddonDepsManager(addonDataDir, scopedLogger),
|
|
11203
11783
|
settings: settingsView,
|
|
@@ -12120,6 +12700,12 @@ Object.defineProperty(exports, "createAddonDataPlaneFacility", {
|
|
|
12120
12700
|
return createAddonDataPlaneFacility;
|
|
12121
12701
|
}
|
|
12122
12702
|
});
|
|
12703
|
+
Object.defineProperty(exports, "createAddonPeerBytesFacility", {
|
|
12704
|
+
enumerable: true,
|
|
12705
|
+
get: function() {
|
|
12706
|
+
return createAddonPeerBytesFacility;
|
|
12707
|
+
}
|
|
12708
|
+
});
|
|
12123
12709
|
Object.defineProperty(exports, "createAddonService", {
|
|
12124
12710
|
enumerable: true,
|
|
12125
12711
|
get: function() {
|
|
@@ -12396,6 +12982,12 @@ Object.defineProperty(exports, "heapSpaceField", {
|
|
|
12396
12982
|
return heapSpaceField;
|
|
12397
12983
|
}
|
|
12398
12984
|
});
|
|
12985
|
+
Object.defineProperty(exports, "hostNodeIdOf", {
|
|
12986
|
+
enumerable: true,
|
|
12987
|
+
get: function() {
|
|
12988
|
+
return hostNodeIdOf;
|
|
12989
|
+
}
|
|
12990
|
+
});
|
|
12399
12991
|
Object.defineProperty(exports, "hubMainRssBudget", {
|
|
12400
12992
|
enumerable: true,
|
|
12401
12993
|
get: function() {
|