okengine 0.21.1 → 0.22.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/AGENTS.md +4 -4
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +15 -9
- package/manifest.v1.schema.json +10 -0
- package/package.json +22 -20
- package/site/content/docs/ai/skills.mdx +1 -1
- package/site/content/docs/client/calling.mdx +27 -19
- package/site/content/docs/client/index.mdx +1 -1
- package/site/content/docs/elements/channel/email.mdx +1 -1
- package/site/content/docs/elements/channel/index.mdx +1 -1
- package/site/content/docs/elements/flow/http.mdx +4 -4
- package/site/content/docs/elements/flow/index.mdx +3 -3
- package/site/content/docs/elements/flow/meta.json +1 -1
- package/site/content/docs/elements/gate/tenancy.mdx +1 -1
- package/site/content/docs/elements/vault/rotation.mdx +15 -0
- package/site/content/docs/plugins/otp.mdx +31 -31
- package/site/content/docs/recipes/mailpit.mdx +1 -1
- package/site/content/docs/recipes/meilisearch.mdx +1 -1
- package/site/content/docs/recipes/pgdog.mdx +1 -1
- package/site/content/docs/reference/configuration.mdx +2 -2
- package/site/content/docs/reference/errors.mdx +12 -2
- package/site/content/docs/reference/fx.mdx +28 -15
- package/site/content/docs/reference/idempotency.mdx +192 -0
- package/site/content/docs/reference/index.mdx +6 -1
- package/site/content/docs/reference/meta.json +2 -1
- package/site/content/docs/understand/meta.json +1 -1
- package/site/content/docs/{elements/flow → understand}/routing.mdx +11 -10
- package/site/content/docs/understand/the-architecture.mdx +14 -12
- package/site/content/docs/understand/try-it.mdx +3 -3
- package/src/cli/competitor-mention-removal.test.ts +3 -3
- package/src/cli/docker-cli.test.ts +1 -1
- package/src/cli/index.ts +9 -1
- package/src/cli/load-config.images.test.ts +10 -10
- package/src/cli/load-config.ts +1 -1
- package/src/cli/vault-cmd.test.ts +23 -0
- package/src/cli/vault-cmd.ts +7 -0
- package/src/client/create.ts +18 -1
- package/src/client/transport.test.ts +225 -0
- package/src/client/transport.ts +132 -19
- package/src/client/types.ts +29 -3
- package/src/compiler/effects-infer.ts +726 -16
- package/src/compiler/extract.test.ts +37 -0
- package/src/compiler/extract.ts +113 -12
- package/src/compiler/fixtures/skyport.expected.json +24 -0
- package/src/compiler/fx-follow.test.ts +164 -0
- package/src/compiler/fx-index.ts +399 -0
- package/src/console/server/flows-invoke.test.ts +2 -2
- package/src/console/ui-next/dist/assets/{access-page-Bgt9bq2r.js → access-page-Ze6ckjdd.js} +2 -2
- package/src/console/ui-next/dist/assets/agent-disclosure-Bkohc_8X.js +1 -0
- package/src/console/ui-next/dist/assets/{cache-glyph-92uM5MO7.js → cache-glyph-rL0lHxat.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-DtGVPtcs.js → call-pii-button-Deo4PP18.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-DN7l6zmC.js → collapsible-CoJ6amHf.js} +1 -1
- package/src/console/ui-next/dist/assets/{copy-inline-button-CAYD18cr.js → copy-inline-button-CWG5_ZOh.js} +1 -1
- package/src/console/ui-next/dist/assets/{detail-header-DVWNjWjg.js → detail-header-b_ZzIsmU.js} +1 -1
- package/src/console/ui-next/dist/assets/{dropdown-menu-4h2LOVXM.js → dropdown-menu-nwXEO1Ac.js} +1 -1
- package/src/console/ui-next/dist/assets/duration-tone-BbQ_8z50.js +9 -0
- package/src/console/ui-next/dist/assets/element-icons-CwyLpVXz.js +1 -0
- package/src/console/ui-next/dist/assets/{explorer-empty-CJs5A-wm.js → explorer-empty-C8sSCqYT.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-BMHs-IzK.js → flows-page-D2E7BtKK.js} +1 -1
- package/src/console/ui-next/dist/assets/{highlighted-json-BlAEVgNW.js → highlighted-json-CPYBzDh0.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-BDf7OAHv.js → http-method-DTPevKmM.js} +1 -1
- package/src/console/ui-next/dist/assets/index-vTuwmeQz.js +58 -0
- package/src/console/ui-next/dist/assets/observability-page-SJTSzHFJ.js +4 -0
- package/src/console/ui-next/dist/assets/{replica-lag-B3GLNVfF.js → replica-lag-BM27lXld.js} +3 -3
- package/src/console/ui-next/dist/assets/{request-meta-DMbnAe3f.js → request-meta-BjFT7DNl.js} +1 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-CUNegEV4.js +1 -0
- package/src/console/ui-next/dist/assets/store-page-Qj9ThRE-.js +41 -0
- package/src/console/ui-next/dist/assets/{trace-detail-sheet-Htk8Cm9t.js → trace-detail-sheet-Cmitq3da.js} +2 -2
- package/src/console/ui-next/dist/assets/{tree-expand-toggle-CFMPWX4f.js → tree-expand-toggle-CbDIB-7x.js} +2 -2
- package/src/console/ui-next/dist/assets/units-page-_PuYqFty.js +1 -0
- package/src/console/ui-next/dist/assets/{use-vault-list-CT4-gajj.js → use-vault-list-Cs45Czkt.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-CBYl0LW_.js → vault-page-BOMr0go9.js} +2 -2
- package/src/console/ui-next/dist/index.html +2 -2
- package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +9 -0
- package/src/docker/docker.test.ts +13 -13
- package/src/docker/images-config.test.ts +12 -12
- package/src/docker/stack-id.test.ts +1 -1
- package/src/drivers/journal-postgres.ts +168 -3
- package/src/drivers/meilisearch.ts +2 -0
- package/src/drivers/vault-builtin.test.ts +10 -0
- package/src/drivers/vault-builtin.ts +13 -1
- package/src/drivers/vault-remote-bag.ts +5 -1
- package/src/drivers/vault-types.ts +6 -1
- package/src/elements/store/emit-drizzle.ts +2 -2
- package/src/elements/store/live-default.test.ts +2 -0
- package/src/elements/store/live-http.test.ts +1 -0
- package/src/elements/store/resource-list-docs.test.ts +2 -2
- package/src/elements/store/resource.test.ts +3 -3
- package/src/elements/store/schema-decl.test.ts +1 -0
- package/src/elements/store/sql-condition.ts +24 -7
- package/src/elements/store/upsert-app.test.ts +1 -1
- package/src/elements/vault/audit.test.ts +137 -0
- package/src/elements/vault/audit.ts +106 -13
- package/src/elements/vault/boot-chain.ts +9 -1
- package/src/elements/vault/builtin-adapter.ts +33 -3
- package/src/i18n/catalogs/ar.ts +4 -0
- package/src/i18n/catalogs/en.ts +4 -0
- package/src/kernel/abort-scope.ts +33 -2
- package/src/kernel/app.ts +191 -18
- package/src/kernel/auto-cache.test.ts +6 -6
- package/src/kernel/boot-bind/vault.ts +1 -0
- package/src/kernel/boot.ts +19 -1
- package/src/kernel/builtin-errors.ts +8 -0
- package/src/kernel/client-descriptor.ts +1 -0
- package/src/kernel/concurrency.test.ts +33 -0
- package/src/kernel/effects-stamping.test.ts +2 -2
- package/src/kernel/errors-compiler.ts +20 -0
- package/src/kernel/external-effects.test.ts +36 -0
- package/src/kernel/flow.ts +24 -0
- package/src/kernel/fx-fetch.ts +5 -1
- package/src/kernel/fx.ts +5 -1
- package/src/kernel/idempotency-store.ts +578 -0
- package/src/kernel/idempotency.test.ts +556 -0
- package/src/kernel/idempotency.ts +304 -0
- package/src/kernel/index.ts +1 -0
- package/src/kernel/journal.ts +56 -0
- package/src/manifest/types.ts +8 -0
- package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
- package/src/plugins/otp.test.ts +65 -1
- package/src/plugins/otp.ts +59 -7
- package/src/release/absolute-regression.test.ts +63 -0
- package/src/release/http-graph.test.ts +25 -0
- package/src/release/http-graph.ts +90 -0
- package/src/release/index.ts +3 -0
- package/src/release/limits.ts +17 -2
- package/src/release/measure.ts +105 -14
- package/src/test/create-test-app.test.ts +3 -1
- package/src/test/create-test-app.ts +40 -0
- package/src/test/live-signals.test.ts +1 -0
- package/src/test/provisions.integration.test.ts +1 -0
- package/src/test/tenant-isolation.test.ts +1 -0
- package/src/console/ui-next/dist/assets/agent-disclosure-B43CXZZR.js +0 -1
- package/src/console/ui-next/dist/assets/duration-tone-BmIR9FV8.js +0 -9
- package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +0 -1
- package/src/console/ui-next/dist/assets/index-BKpaes3n.js +0 -63
- package/src/console/ui-next/dist/assets/observability-page-WnVLI-0j.js +0 -4
- package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-KvFDingJ.js +0 -41
- package/src/console/ui-next/dist/assets/units-page-C4NdNuxP.js +0 -1
|
@@ -51,6 +51,7 @@ export async function bindVault(options: BootOptions, env: ConfigEnv): Promise<V
|
|
|
51
51
|
env,
|
|
52
52
|
cwd: process.cwd(),
|
|
53
53
|
seed: {},
|
|
54
|
+
...(options.config?.vault?.audit === undefined ? {} : { audit: options.config.vault.audit }),
|
|
54
55
|
});
|
|
55
56
|
const vault = createVaultRuntime({
|
|
56
57
|
secrets: vaultSecrets,
|
package/src/kernel/boot.ts
CHANGED
|
@@ -601,6 +601,15 @@ export async function bootApplication(input: BootOptions = {}): Promise<BootResu
|
|
|
601
601
|
docker,
|
|
602
602
|
});
|
|
603
603
|
|
|
604
|
+
const needsIdempotencyJournal = (options.flows ?? []).some((f) => {
|
|
605
|
+
const mode = f.resolvedIdempotency?.mode;
|
|
606
|
+
return mode === "auto" || mode === "required";
|
|
607
|
+
});
|
|
608
|
+
if (!journal && needsIdempotencyJournal) {
|
|
609
|
+
journalBind ??= await loadBind<JournalBind>("journal");
|
|
610
|
+
journal = (await journalBind.bindJournal(options, env)).journal;
|
|
611
|
+
}
|
|
612
|
+
|
|
604
613
|
// 9. `.adopt()` barrel freshness — opt-in only (same `rootDir` gate as
|
|
605
614
|
// capability minting above; never a filesystem read the kernel performs
|
|
606
615
|
// on its own).
|
|
@@ -712,7 +721,12 @@ export async function mintCapabilities(
|
|
|
712
721
|
|
|
713
722
|
let manifest = options.manifest;
|
|
714
723
|
let extractError: string | undefined;
|
|
715
|
-
const needsManifest =
|
|
724
|
+
const needsManifest =
|
|
725
|
+
manifest === undefined &&
|
|
726
|
+
flows.some(
|
|
727
|
+
(f) =>
|
|
728
|
+
f.effects === undefined || (f.idempotency !== false && f.resolvedIdempotency === undefined),
|
|
729
|
+
);
|
|
716
730
|
if (needsManifest) {
|
|
717
731
|
const rootDir = options.rootDir ?? process.env["OKE_ROOT_DIR"];
|
|
718
732
|
if (rootDir) {
|
|
@@ -723,6 +737,10 @@ export async function mintCapabilities(
|
|
|
723
737
|
}
|
|
724
738
|
|
|
725
739
|
for (const f of flows) {
|
|
740
|
+
const manifestFlow = manifest?.flows?.[f.name];
|
|
741
|
+
if (manifestFlow?.idempotency) f.resolvedIdempotency = manifestFlow.idempotency;
|
|
742
|
+
if (manifestFlow?.usesRaw === true) f.usesRaw = true;
|
|
743
|
+
|
|
726
744
|
if (f.effects !== undefined) {
|
|
727
745
|
map.set(f.name, createCapabilityToken(f.name, f.effects));
|
|
728
746
|
continue;
|
|
@@ -21,6 +21,10 @@ export const BUILTIN_ERROR_STATUS = {
|
|
|
21
21
|
AuthFailed: 400,
|
|
22
22
|
InternalError: 500,
|
|
23
23
|
ServiceUnavailable: 503,
|
|
24
|
+
IdempotencyKeyMissing: 400,
|
|
25
|
+
IdempotencyKeyInvalid: 400,
|
|
26
|
+
IdempotencyKeyReused: 422,
|
|
27
|
+
IdempotencyInProgress: 409,
|
|
24
28
|
} as const;
|
|
25
29
|
|
|
26
30
|
/** Built-in error code (including `DatabaseError`). */
|
|
@@ -86,6 +90,10 @@ export type BuiltinErrorMap = {
|
|
|
86
90
|
};
|
|
87
91
|
readonly InternalError: Record<string, never>;
|
|
88
92
|
readonly ServiceUnavailable: { readonly retryAfter?: number };
|
|
93
|
+
readonly IdempotencyKeyMissing: Record<string, never>;
|
|
94
|
+
readonly IdempotencyKeyInvalid: Record<string, never>;
|
|
95
|
+
readonly IdempotencyKeyReused: Record<string, never>;
|
|
96
|
+
readonly IdempotencyInProgress: { readonly retryAfter?: number };
|
|
89
97
|
};
|
|
90
98
|
|
|
91
99
|
/**
|
|
@@ -56,6 +56,7 @@ export function buildClientDescriptor(
|
|
|
56
56
|
...(stamp.stream === true ? { stream: true as const } : {}),
|
|
57
57
|
...(stamp.matchKey !== undefined ? { matchKey: stamp.matchKey } : {}),
|
|
58
58
|
...(stamp.gates !== undefined && stamp.gates.length > 0 ? { gates: stamp.gates } : {}),
|
|
59
|
+
...(def?.resolvedIdempotency !== undefined ? { idempotency: def.resolvedIdempotency } : {}),
|
|
59
60
|
};
|
|
60
61
|
}
|
|
61
62
|
out[unit] = unitOut;
|
|
@@ -90,6 +90,39 @@ describe("fx.race — structured concurrency", () => {
|
|
|
90
90
|
test("empty race throws", async () => {
|
|
91
91
|
await expect(fxRace([])).rejects.toThrow(/empty/);
|
|
92
92
|
});
|
|
93
|
+
|
|
94
|
+
test("a gated effect does not start after the branch is aborted", async () => {
|
|
95
|
+
const { fx } = createFxContext({
|
|
96
|
+
flow: "t",
|
|
97
|
+
effects: { reads: ["sql:notes"] },
|
|
98
|
+
});
|
|
99
|
+
const store = fx.store("sql:notes") as { get(key: string): Promise<unknown> };
|
|
100
|
+
let sawAbort = false;
|
|
101
|
+
|
|
102
|
+
const winner = await fx.race([
|
|
103
|
+
async () => {
|
|
104
|
+
await new Promise<void>((resolve) => {
|
|
105
|
+
const signal = fx.signal;
|
|
106
|
+
if (signal.aborted) {
|
|
107
|
+
resolve();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
signal.addEventListener("abort", () => resolve(), { once: true });
|
|
111
|
+
});
|
|
112
|
+
try {
|
|
113
|
+
await store.get("n1");
|
|
114
|
+
} catch (err) {
|
|
115
|
+
sawAbort = isAbortError(err);
|
|
116
|
+
}
|
|
117
|
+
return "slow";
|
|
118
|
+
},
|
|
119
|
+
async () => "fast",
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
expect(winner).toBe("fast");
|
|
123
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
124
|
+
expect(sawAbort).toBe(true);
|
|
125
|
+
});
|
|
93
126
|
});
|
|
94
127
|
|
|
95
128
|
describe("fx.using — scoped cleanup", () => {
|
|
@@ -71,7 +71,7 @@ describe("boot-level: undeclared-effects flow, no manifest / rootDir", () => {
|
|
|
71
71
|
const create = buildUnannotatedCreateFlow(db);
|
|
72
72
|
const app = oke({ name: "stamp-open", gate: { policies: [gate.public] } }).adopt({ create });
|
|
73
73
|
Object.assign(app.$options, { stores: [db] });
|
|
74
|
-
await createTestApp(app
|
|
74
|
+
await createTestApp(app, { capability: "open" });
|
|
75
75
|
|
|
76
76
|
const res = await app.fetch(
|
|
77
77
|
new Request("http://localhost/notes", {
|
|
@@ -157,7 +157,7 @@ describe("boot-level: table-ref resolution stays backward compatible", () => {
|
|
|
157
157
|
create,
|
|
158
158
|
});
|
|
159
159
|
Object.assign(app.$options, { stores: [db] });
|
|
160
|
-
await createTestApp(app);
|
|
160
|
+
await createTestApp(app, { capability: "open" });
|
|
161
161
|
|
|
162
162
|
const res = await app.fetch(
|
|
163
163
|
new Request("http://localhost/notes", {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OKE1900 — kept off the kernel edge profile.
|
|
3
|
+
*
|
|
4
|
+
* Effect inference throws a plain `Error` (`OKE1900` in the message) from
|
|
5
|
+
* `okengine/compiler`. This definition is registry-only so the compiler
|
|
6
|
+
* chunk does not import the kernel error module.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { OkeErrorDefinition } from "./errors.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A Flow hides `fx` from effect inference, or an explicit `effects` block
|
|
13
|
+
* omits an effect the walk can see.
|
|
14
|
+
*/
|
|
15
|
+
export const FX_INFERENCE_OPAQUE: OkeErrorDefinition = {
|
|
16
|
+
code: 1900,
|
|
17
|
+
domain: "compiler",
|
|
18
|
+
cause: 'Flow "{flow}" hides fx from effect inference ({detail}).',
|
|
19
|
+
fix: "Name the parameter fx, keep chain aliases in the same function, and include every effect inference can see. An effects block may add keys; it cannot omit keys inference can see.",
|
|
20
|
+
};
|
|
@@ -85,6 +85,42 @@ describe("fx.fetch", () => {
|
|
|
85
85
|
globalThis.fetch = originalFetch;
|
|
86
86
|
}
|
|
87
87
|
});
|
|
88
|
+
|
|
89
|
+
test("fx.race aborts an in-flight fx.fetch", async () => {
|
|
90
|
+
const originalFetch = globalThis.fetch;
|
|
91
|
+
let signal: AbortSignal | undefined;
|
|
92
|
+
globalThis.fetch = (async (_input: unknown, init?: RequestInit) => {
|
|
93
|
+
signal = init?.signal ?? undefined;
|
|
94
|
+
return new Promise<Response>((_resolve, reject) => {
|
|
95
|
+
const onAbort = (): void => {
|
|
96
|
+
reject(new DOMException("aborted", "AbortError"));
|
|
97
|
+
};
|
|
98
|
+
if (init?.signal?.aborted) {
|
|
99
|
+
onAbort();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
init?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
103
|
+
});
|
|
104
|
+
}) as typeof fetch;
|
|
105
|
+
try {
|
|
106
|
+
const { fx } = createFxContext({
|
|
107
|
+
flow: "stripe.pull",
|
|
108
|
+
effects: { fetches: ["api.stripe.com"] },
|
|
109
|
+
});
|
|
110
|
+
const winner = await fx.race([
|
|
111
|
+
() => fx.fetch("https://api.stripe.com/v1/charges").then(() => "slow"),
|
|
112
|
+
async () => {
|
|
113
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
114
|
+
return "fast" as const;
|
|
115
|
+
},
|
|
116
|
+
]);
|
|
117
|
+
expect(winner).toBe("fast");
|
|
118
|
+
await new Promise((r) => setTimeout(r, 15));
|
|
119
|
+
expect(signal?.aborted).toBe(true);
|
|
120
|
+
} finally {
|
|
121
|
+
globalThis.fetch = originalFetch;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
88
124
|
});
|
|
89
125
|
|
|
90
126
|
describe("Channel failover — winning provider on EffectEntry", () => {
|
package/src/kernel/flow.ts
CHANGED
|
@@ -54,6 +54,16 @@ export interface FlowOptions<I = unknown, O = unknown> {
|
|
|
54
54
|
* (`"30s"`) adds a TTL on top of write invalidation.
|
|
55
55
|
*/
|
|
56
56
|
readonly cache?: boolean | string;
|
|
57
|
+
/**
|
|
58
|
+
* HTTP `Idempotency-Key` handling.
|
|
59
|
+
* Omitted is auto: the header is honored on an eligible mutating flow.
|
|
60
|
+
* `"required"` rejects a missing header. `false` ignores the header.
|
|
61
|
+
* A TTL string (`"24h"`) overrides the 24 hour default.
|
|
62
|
+
*/
|
|
63
|
+
readonly idempotency?:
|
|
64
|
+
| false
|
|
65
|
+
| "required"
|
|
66
|
+
| { readonly required?: boolean; readonly ttl?: string };
|
|
57
67
|
/** Declared service-level objective. */
|
|
58
68
|
readonly slo?: Slo;
|
|
59
69
|
/**
|
|
@@ -168,6 +178,19 @@ export interface FlowDef<
|
|
|
168
178
|
readonly liveCustomMatch: boolean;
|
|
169
179
|
/** Cache option. */
|
|
170
180
|
readonly cache: boolean | string | undefined;
|
|
181
|
+
/** Author idempotency option. */
|
|
182
|
+
readonly idempotency:
|
|
183
|
+
| false
|
|
184
|
+
| "required"
|
|
185
|
+
| { readonly required?: boolean; readonly ttl?: string }
|
|
186
|
+
| undefined;
|
|
187
|
+
/**
|
|
188
|
+
* Extracted idempotency stamp. Boot copies this from the Manifest.
|
|
189
|
+
* Runtime resolves it when extract has not run.
|
|
190
|
+
*/
|
|
191
|
+
resolvedIdempotency?: { readonly mode: "off" | "auto" | "required"; readonly ttl: string };
|
|
192
|
+
/** True when extract saw `fx.raw`. A throw then completes the idempotency row. */
|
|
193
|
+
usesRaw?: boolean;
|
|
171
194
|
/** SLO. */
|
|
172
195
|
readonly slo: Slo | undefined;
|
|
173
196
|
/** Plane (user vs operator). */
|
|
@@ -305,6 +328,7 @@ export function flow(
|
|
|
305
328
|
live: undefined,
|
|
306
329
|
liveCustomMatch: false,
|
|
307
330
|
cache: options.cache,
|
|
331
|
+
idempotency: options.idempotency,
|
|
308
332
|
slo: options.slo,
|
|
309
333
|
plane: options.plane,
|
|
310
334
|
breaking: false,
|
package/src/kernel/fx-fetch.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import type { EffectExternal } from "./effects.ts";
|
|
7
7
|
import { isDryRun, recordWouldHaveFired } from "./dry-run.ts";
|
|
8
|
+
import { requestSignal } from "./abort-scope.ts";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Hostname for `fx.fetch` capability / ledger resource.
|
|
@@ -48,7 +49,10 @@ export function runFxFetch(gated: Gated, url: string | URL, init?: RequestInit):
|
|
|
48
49
|
recordWouldHaveFired("fetch", host);
|
|
49
50
|
return new Response(null, { status: 204 });
|
|
50
51
|
}
|
|
51
|
-
return globalThis.fetch(url,
|
|
52
|
+
return globalThis.fetch(url, {
|
|
53
|
+
...init,
|
|
54
|
+
signal: requestSignal(init?.signal),
|
|
55
|
+
});
|
|
52
56
|
},
|
|
53
57
|
{ host, kind: "third-party" },
|
|
54
58
|
);
|
package/src/kernel/fx.ts
CHANGED
|
@@ -62,7 +62,7 @@ import {
|
|
|
62
62
|
touchDryRunStore,
|
|
63
63
|
} from "./dry-run.ts";
|
|
64
64
|
import type { FailFn } from "./errors.ts";
|
|
65
|
-
import { currentAbortSignal, linkAbort } from "./abort-scope.ts";
|
|
65
|
+
import { abortError, currentAbortSignal, linkAbort } from "./abort-scope.ts";
|
|
66
66
|
import type { FxRetryOptions, FxThunk } from "./concurrency.ts";
|
|
67
67
|
import { maskRedactedDeep, Redacted } from "./redacted.ts";
|
|
68
68
|
import type { JournalSession, JournalStepOptions } from "./journal.ts";
|
|
@@ -1201,6 +1201,8 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1201
1201
|
| EffectExternal
|
|
1202
1202
|
| ((result: T | undefined, error: unknown) => EffectExternal | undefined),
|
|
1203
1203
|
): Promise<T> {
|
|
1204
|
+
const signal = currentAbortSignal();
|
|
1205
|
+
if (signal.aborted) throw abortError(signal.reason);
|
|
1204
1206
|
capability.assert(kind, resource);
|
|
1205
1207
|
const execute = () => recordEffect(ledger, kind, resource, now, body, externalOf);
|
|
1206
1208
|
if (journal) {
|
|
@@ -1511,6 +1513,8 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1511
1513
|
* @param body - Work to run under the gate
|
|
1512
1514
|
*/
|
|
1513
1515
|
async function gatedSecret<T>(name: string, body: () => T | Promise<T>): Promise<T> {
|
|
1516
|
+
const signal = currentAbortSignal();
|
|
1517
|
+
if (signal.aborted) throw abortError(signal.reason);
|
|
1514
1518
|
capability.assert("secret", name);
|
|
1515
1519
|
const timestamp = now();
|
|
1516
1520
|
const t0 = performance.now();
|