Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com> Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
100 lines
2.9 KiB
JavaScript
100 lines
2.9 KiB
JavaScript
// Per-lease native work. A proven byte refusal remains valid as storage only shrinks.
|
|
import { FactFailure } from "./fact-error.mjs";
|
|
import { MAX_NATIVE_SOURCES } from "./limits.mjs";
|
|
|
|
const reuse = Object.freeze({
|
|
kind: "source_budget_refusal",
|
|
scope: "target_lease",
|
|
});
|
|
const refusalFields = new Set([
|
|
"scope",
|
|
"limit_bytes",
|
|
"observed_bytes_at_least",
|
|
"evidence_id",
|
|
"source_retained",
|
|
"source_complete",
|
|
]);
|
|
const retainRefusal = (error) => {
|
|
try {
|
|
if (!(error instanceof FactFailure)) return;
|
|
const own = Object.getOwnPropertyDescriptors(error);
|
|
if (
|
|
own.code?.value !== "resource_constraint" ||
|
|
typeof own.message?.value !== "string"
|
|
)
|
|
return;
|
|
const fields = Object.getOwnPropertyDescriptors(own.details?.value);
|
|
if (
|
|
Object.keys(fields).some((key) => !refusalFields.has(key)) ||
|
|
Object.values(fields).some(
|
|
(field) =>
|
|
!Object.hasOwn(field, "value") ||
|
|
(field.value !== null &&
|
|
!["string", "number", "boolean"].includes(typeof field.value)),
|
|
)
|
|
)
|
|
return;
|
|
const details = Object.fromEntries(
|
|
Object.entries(fields).map(([key, field]) => [key, field.value]),
|
|
);
|
|
if (
|
|
details.scope !== "native_source" ||
|
|
details.source_retained !== true ||
|
|
details.source_complete !== false ||
|
|
typeof details.evidence_id !== "string" ||
|
|
!details.evidence_id ||
|
|
!Number.isSafeInteger(details.limit_bytes) ||
|
|
details.limit_bytes < 1 ||
|
|
!Number.isSafeInteger(details.observed_bytes_at_least) ||
|
|
details.observed_bytes_at_least <= details.limit_bytes
|
|
)
|
|
return;
|
|
return new FactFailure(
|
|
own.code.value,
|
|
own.message.value,
|
|
Object.freeze(details),
|
|
);
|
|
} catch {
|
|
// Unknown host failures retain their original identity and remain uncached.
|
|
return;
|
|
}
|
|
};
|
|
|
|
export class NativeCaptureCache {
|
|
#entries = new Map();
|
|
clear() {
|
|
this.#entries.clear();
|
|
}
|
|
async read(key, load) {
|
|
let pending = this.#entries.get(key);
|
|
const reused = pending !== undefined;
|
|
if (!pending) {
|
|
pending = Promise.resolve()
|
|
.then(load)
|
|
.then(
|
|
(value) => ({ ok: true, value }),
|
|
(error) => {
|
|
const refusal = retainRefusal(error);
|
|
return {
|
|
ok: false,
|
|
error: refusal ?? error,
|
|
reusable: refusal !== undefined,
|
|
};
|
|
},
|
|
);
|
|
if (this.#entries.size < MAX_NATIVE_SOURCES)
|
|
this.#entries.set(key, pending);
|
|
}
|
|
const result = await pending;
|
|
if (result.ok) return result.value;
|
|
if (!result.reusable) {
|
|
if (this.#entries.get(key) === pending) this.#entries.delete(key);
|
|
throw result.error;
|
|
}
|
|
throw new FactFailure(result.error.code, result.error.message, {
|
|
...result.error.details,
|
|
...(reused ? { cached_observation: reuse } : {}),
|
|
});
|
|
}
|
|
}
|