okengine 0.21.0 → 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 -25
- 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/dev.test.ts +105 -2
- package/src/cli/dev.ts +37 -2
- 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 +31 -27
- package/src/client/live.ts +44 -101
- package/src/client/sse.ts +20 -66
- package/src/client/stream.ts +25 -67
- package/src/client/transport.test.ts +225 -0
- package/src/client/transport.ts +158 -90
- package/src/client/types.ts +29 -3
- package/src/client/wire.ts +119 -0
- package/src/compiler/aot.ts +3 -32
- package/src/compiler/dynamic.ts +13 -11
- 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/compiler/interpret.ts +45 -0
- package/src/console/server/flows-invoke.test.ts +2 -2
- package/src/console/ui-next/dist/assets/{access-page-tIbsiphz.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-BCC-DxKT.js → cache-glyph-rL0lHxat.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-jOmYISdc.js → call-pii-button-Deo4PP18.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-DC2xNaAb.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-gT1lsWQK.js → flows-page-D2E7BtKK.js} +1 -1
- package/src/console/ui-next/dist/assets/{highlighted-json-C2GZEJNI.js → highlighted-json-CPYBzDh0.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-BdYjcIrD.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-C4QdAF7J.js → replica-lag-BM27lXld.js} +3 -3
- package/src/console/ui-next/dist/assets/{request-meta-C43DHyld.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-Dcpo4Us_.js → trace-detail-sheet-Cmitq3da.js} +2 -2
- package/src/console/ui-next/dist/assets/{tree-expand-toggle-CKJTuv43.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-BwZ9YjTW.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/sql-session.ts +20 -4
- 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 +200 -20
- 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.test.ts +78 -0
- package/src/kernel/client-descriptor.ts +24 -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/errors-text.ts +99 -0
- package/src/kernel/errors.ts +90 -138
- 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-sql-handle.ts +305 -0
- package/src/kernel/fx.ts +42 -330
- 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/kernel/json-result.ts +59 -0
- package/src/kernel/project-out.ts +6 -1
- package/src/manifest/types.ts +8 -0
- package/src/okid-extended.ts +175 -0
- package/src/okid-shared.ts +103 -0
- package/src/okid.ts +30 -213
- 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/build-lib.ts +9 -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-CSKumwS2.js +0 -1
- package/src/console/ui-next/dist/assets/duration-tone-CwoV56jn.js +0 -9
- package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +0 -1
- package/src/console/ui-next/dist/assets/index-Cul17AcV.js +0 -63
- package/src/console/ui-next/dist/assets/observability-page-oY9vdYBk.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-CL0D9dOq.js +0 -41
- package/src/console/ui-next/dist/assets/units-page-1PlT19ft.js +0 -1
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* English cause/fix templates for {@link OKE_ERRORS}.
|
|
3
|
+
*
|
|
4
|
+
* Loaded on first read so the kernel edge profile does not carry the
|
|
5
|
+
* strings. Localization still prefers the i18n catalogs when a locale
|
|
6
|
+
* overrides `oke.{code}.cause` / `oke.{code}.fix`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** One registry template pair. */
|
|
10
|
+
export interface OkeErrorText {
|
|
11
|
+
/** One-line cause template. */
|
|
12
|
+
readonly cause: string;
|
|
13
|
+
/** Suggested fix template. */
|
|
14
|
+
readonly fix: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Keyed by {@link OKE_ERRORS} property name. */
|
|
18
|
+
export const OKE_ERROR_TEXT: Readonly<Record<string, OkeErrorText>> = {
|
|
19
|
+
UNDECLARED_READ: {
|
|
20
|
+
cause: 'Flow "{flow}" reads "{resource}" without declaring it.',
|
|
21
|
+
fix: 'Add "{resource}" to this flow\'s effects.reads.',
|
|
22
|
+
},
|
|
23
|
+
UNDECLARED_WRITE: {
|
|
24
|
+
cause: 'Flow "{flow}" writes "{resource}" without declaring it.',
|
|
25
|
+
fix: 'Add "{resource}" to this flow\'s effects.writes.',
|
|
26
|
+
},
|
|
27
|
+
UNDECLARED_EMIT: {
|
|
28
|
+
cause: 'Flow "{flow}" emits "{resource}" without declaring it.',
|
|
29
|
+
fix: 'Add "{resource}" to this flow\'s effects.emits.',
|
|
30
|
+
},
|
|
31
|
+
UNDECLARED_SEND: {
|
|
32
|
+
cause: 'Flow "{flow}" sends "{resource}" without declaring it.',
|
|
33
|
+
fix: 'Add "{resource}" to this flow\'s effects.sends.',
|
|
34
|
+
},
|
|
35
|
+
UNDECLARED_ASK: {
|
|
36
|
+
cause: 'Flow "{flow}" asks "{resource}" without declaring it.',
|
|
37
|
+
fix: 'Add "{resource}" to this flow\'s effects.asks.',
|
|
38
|
+
},
|
|
39
|
+
UNDECLARED_SECRET: {
|
|
40
|
+
cause: 'Flow "{flow}" reads secret "{resource}" without declaring it.',
|
|
41
|
+
fix: 'Add "{resource}" to this flow\'s effects.secrets.',
|
|
42
|
+
},
|
|
43
|
+
UNDECLARED_CALL: {
|
|
44
|
+
cause: 'Flow "{flow}" calls "{resource}" without declaring it.',
|
|
45
|
+
fix: 'Add "{resource}" to this flow\'s effects.calls.',
|
|
46
|
+
},
|
|
47
|
+
UNDECLARED_FETCH: {
|
|
48
|
+
cause: 'Flow "{flow}" fetches "{resource}" without declaring it.',
|
|
49
|
+
fix: 'Add "{resource}" to this flow\'s effects.fetches.',
|
|
50
|
+
},
|
|
51
|
+
UNDECLARED_EMBED: {
|
|
52
|
+
cause: 'Flow "{flow}" embeds with "{resource}" without declaring it.',
|
|
53
|
+
fix: 'Add "{resource}" to this flow\'s effects.embeds.',
|
|
54
|
+
},
|
|
55
|
+
NO_EFFECTS_DECLARED: {
|
|
56
|
+
cause: 'Flow "{flow}" has no declared effects and no Manifest to derive them from.{extract}',
|
|
57
|
+
fix:
|
|
58
|
+
"Add explicit `effects` to this flow, or boot with a Manifest (`oke build`) / " +
|
|
59
|
+
"`rootDir` so effects can be derived. If extract failed, ensure `oxc-parser` is " +
|
|
60
|
+
"installed (okengine dependency). dev+compose/prod refuse an open capability token.",
|
|
61
|
+
},
|
|
62
|
+
ADOPT_BARREL_STALE: {
|
|
63
|
+
cause: "src/flows/{unit} exists on disk but adopted no flows — the .adopt() barrel is stale.",
|
|
64
|
+
fix: "Run `oke dev` or `oke build` to regenerate `src/flows/index.ts`.",
|
|
65
|
+
},
|
|
66
|
+
HTTP_PATH_UNRESOLVED: {
|
|
67
|
+
cause: 'Flow "{flow}" bound {method} with no path — the file-tree stamp never ran.',
|
|
68
|
+
fix: 'Put the file under `src/flows/<unit>/` and import `@/flows`, or pass an explicit path to `http.{method}("/…")`.',
|
|
69
|
+
},
|
|
70
|
+
HTTP_ROUTE_DUPLICATE: {
|
|
71
|
+
cause: '{method} {path} is bound twice (flow "{flow}").',
|
|
72
|
+
fix: "Give each HTTP flow a unique method + path.",
|
|
73
|
+
},
|
|
74
|
+
HTTP_FLOW_UNNAMED: {
|
|
75
|
+
cause: "An HTTP flow on {method} {path} has no name.",
|
|
76
|
+
fix: 'Use `flow("unit.export", {…})` or export it from a `src/flows/<unit>/` file so the tree can stamp `unit.export`.',
|
|
77
|
+
},
|
|
78
|
+
LIVE_EXPOSURE_DUPLICATE: {
|
|
79
|
+
cause:
|
|
80
|
+
'Live signal "{signal}" is exposed twice with the same gates ({gates}) and match ({match}).',
|
|
81
|
+
fix: "Use a different gate or path-param filter, or drop the extra route.",
|
|
82
|
+
},
|
|
83
|
+
MCP_TOOL_DUPLICATE: {
|
|
84
|
+
cause: 'MCP tool "{tool}" is bound twice (flow "{flow}").',
|
|
85
|
+
fix: "Give each MCP tool exposure a unique tool name.",
|
|
86
|
+
},
|
|
87
|
+
ORPHAN_EMIT: {
|
|
88
|
+
cause: 'Flow "{flow}" emits signal "{resource}" with no subscriber.',
|
|
89
|
+
fix: "Add `on({resource}, …)` or mark the signal `{ optional: true }`.",
|
|
90
|
+
},
|
|
91
|
+
SIGNAL_SCHEMA: {
|
|
92
|
+
cause: '"{resource}": {detail}',
|
|
93
|
+
fix: "Fix schema payload.",
|
|
94
|
+
},
|
|
95
|
+
DOMAIN_SCHEMA_MISSING: {
|
|
96
|
+
cause: "domain table not found — migrations have not been applied.",
|
|
97
|
+
fix: "run `oke db migrate` against this environment.",
|
|
98
|
+
},
|
|
99
|
+
};
|
package/src/kernel/errors.ts
CHANGED
|
@@ -236,6 +236,9 @@ export const fail: FailFn = failImpl as FailFn;
|
|
|
236
236
|
* Permanent error registry. Codes are stable within their domain range after
|
|
237
237
|
* the domain-range renumber; each entry declares `domain` for range guards.
|
|
238
238
|
*
|
|
239
|
+
* Cause/fix templates load from {@link OKE_ERROR_TEXT} on first read so the
|
|
240
|
+
* edge profile does not carry the English strings.
|
|
241
|
+
*
|
|
239
242
|
* Ranges:
|
|
240
243
|
* - `1000–1099` — Kernel (Flow / Trigger / Gate-posture)
|
|
241
244
|
* - `1100–1199` — Store
|
|
@@ -248,168 +251,117 @@ export const fail: FailFn = failImpl as FailFn;
|
|
|
248
251
|
* - `1800–1899` — MCP + Tenancy
|
|
249
252
|
* - `1900–1999` — Compiler / Manifest
|
|
250
253
|
*/
|
|
251
|
-
|
|
254
|
+
const OKE_ERROR_ROWS = {
|
|
252
255
|
/** Flow reads a store resource not listed in `effects.reads`. */
|
|
253
|
-
UNDECLARED_READ: {
|
|
254
|
-
code: 1001,
|
|
255
|
-
domain: "kernel",
|
|
256
|
-
cause: 'Flow "{flow}" reads "{resource}" without declaring it.',
|
|
257
|
-
fix: 'Add "{resource}" to this flow\'s effects.reads.',
|
|
258
|
-
},
|
|
256
|
+
UNDECLARED_READ: { code: 1001, domain: "kernel" },
|
|
259
257
|
/** Flow writes a store resource not listed in `effects.writes`. */
|
|
260
|
-
UNDECLARED_WRITE: {
|
|
261
|
-
code: 1002,
|
|
262
|
-
domain: "kernel",
|
|
263
|
-
cause: 'Flow "{flow}" writes "{resource}" without declaring it.',
|
|
264
|
-
fix: 'Add "{resource}" to this flow\'s effects.writes.',
|
|
265
|
-
},
|
|
258
|
+
UNDECLARED_WRITE: { code: 1002, domain: "kernel" },
|
|
266
259
|
/** Flow emits a signal not listed in `effects.emits`. */
|
|
267
|
-
UNDECLARED_EMIT: {
|
|
268
|
-
code: 1003,
|
|
269
|
-
domain: "kernel",
|
|
270
|
-
cause: 'Flow "{flow}" emits "{resource}" without declaring it.',
|
|
271
|
-
fix: 'Add "{resource}" to this flow\'s effects.emits.',
|
|
272
|
-
},
|
|
260
|
+
UNDECLARED_EMIT: { code: 1003, domain: "kernel" },
|
|
273
261
|
/** Flow sends a channel template not listed in `effects.sends`. */
|
|
274
|
-
UNDECLARED_SEND: {
|
|
275
|
-
code: 1004,
|
|
276
|
-
domain: "kernel",
|
|
277
|
-
cause: 'Flow "{flow}" sends "{resource}" without declaring it.',
|
|
278
|
-
fix: 'Add "{resource}" to this flow\'s effects.sends.',
|
|
279
|
-
},
|
|
262
|
+
UNDECLARED_SEND: { code: 1004, domain: "kernel" },
|
|
280
263
|
/** Flow asks a prompt not listed in `effects.asks`. */
|
|
281
|
-
UNDECLARED_ASK: {
|
|
282
|
-
code: 1005,
|
|
283
|
-
domain: "kernel",
|
|
284
|
-
cause: 'Flow "{flow}" asks "{resource}" without declaring it.',
|
|
285
|
-
fix: 'Add "{resource}" to this flow\'s effects.asks.',
|
|
286
|
-
},
|
|
264
|
+
UNDECLARED_ASK: { code: 1005, domain: "kernel" },
|
|
287
265
|
/** Flow reads a secret not listed in `effects.secrets`. */
|
|
288
|
-
UNDECLARED_SECRET: {
|
|
289
|
-
code: 1006,
|
|
290
|
-
domain: "kernel",
|
|
291
|
-
cause: 'Flow "{flow}" reads secret "{resource}" without declaring it.',
|
|
292
|
-
fix: 'Add "{resource}" to this flow\'s effects.secrets.',
|
|
293
|
-
},
|
|
266
|
+
UNDECLARED_SECRET: { code: 1006, domain: "kernel" },
|
|
294
267
|
/** Flow calls another flow not listed in `effects.calls`. */
|
|
295
|
-
UNDECLARED_CALL: {
|
|
296
|
-
code: 1007,
|
|
297
|
-
domain: "kernel",
|
|
298
|
-
cause: 'Flow "{flow}" calls "{resource}" without declaring it.',
|
|
299
|
-
fix: 'Add "{resource}" to this flow\'s effects.calls.',
|
|
300
|
-
},
|
|
268
|
+
UNDECLARED_CALL: { code: 1007, domain: "kernel" },
|
|
301
269
|
/** Flow fetches a host not listed in `effects.fetches`. */
|
|
302
|
-
UNDECLARED_FETCH: {
|
|
303
|
-
code: 1008,
|
|
304
|
-
domain: "kernel",
|
|
305
|
-
cause: 'Flow "{flow}" fetches "{resource}" without declaring it.',
|
|
306
|
-
fix: 'Add "{resource}" to this flow\'s effects.fetches.',
|
|
307
|
-
},
|
|
270
|
+
UNDECLARED_FETCH: { code: 1008, domain: "kernel" },
|
|
308
271
|
/** Flow embeds via a model not listed in `effects.embeds`. */
|
|
309
|
-
UNDECLARED_EMBED: {
|
|
310
|
-
code: 1009,
|
|
311
|
-
domain: "kernel",
|
|
312
|
-
cause: 'Flow "{flow}" embeds with "{resource}" without declaring it.',
|
|
313
|
-
fix: 'Add "{resource}" to this flow\'s effects.embeds.',
|
|
314
|
-
},
|
|
272
|
+
UNDECLARED_EMBED: { code: 1009, domain: "kernel" },
|
|
315
273
|
/**
|
|
316
274
|
* Flow has no declared `effects` and no Manifest-derived effects were
|
|
317
275
|
* available to stamp at boot (dev+compose / prod — never a silent open token).
|
|
318
276
|
*/
|
|
319
|
-
NO_EFFECTS_DECLARED: {
|
|
320
|
-
code: 1020,
|
|
321
|
-
domain: "kernel",
|
|
322
|
-
cause: 'Flow "{flow}" has no declared effects and no Manifest to derive them from.{extract}',
|
|
323
|
-
fix:
|
|
324
|
-
"Add explicit `effects` to this flow, or boot with a Manifest (`oke build`) / " +
|
|
325
|
-
"`rootDir` so effects can be derived. If extract failed, ensure `oxc-parser` is " +
|
|
326
|
-
"installed (okengine dependency). dev+compose/prod refuse an open capability token.",
|
|
327
|
-
},
|
|
277
|
+
NO_EFFECTS_DECLARED: { code: 1020, domain: "kernel" },
|
|
328
278
|
/**
|
|
329
279
|
* A `src/flows/<unit>` folder exists on disk but no adopted flow carries
|
|
330
280
|
* that unit — the generated `.adopt()` barrel (`src/flows/index.ts`)
|
|
331
281
|
* is stale or was hand-edited. dev+compose / prod — never a silently-incomplete
|
|
332
282
|
* route table in a deploy-shaped environment.
|
|
333
283
|
*/
|
|
334
|
-
ADOPT_BARREL_STALE: {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
domain: "kernel",
|
|
346
|
-
cause: 'Flow "{flow}" bound {method} with no path — the file-tree stamp never ran.',
|
|
347
|
-
fix: 'Put the file under `src/flows/<unit>/` and import `@/flows`, or pass an explicit path to `http.{method}("/…")`.',
|
|
348
|
-
},
|
|
349
|
-
/**
|
|
350
|
-
* Two HTTP bindings share method + path — last-add-wins is the opposite of this DX.
|
|
351
|
-
*/
|
|
352
|
-
HTTP_ROUTE_DUPLICATE: {
|
|
353
|
-
code: 1041,
|
|
354
|
-
domain: "kernel",
|
|
355
|
-
cause: '{method} {path} is bound twice (flow "{flow}").',
|
|
356
|
-
fix: "Give each HTTP flow a unique method + path.",
|
|
357
|
-
},
|
|
358
|
-
/**
|
|
359
|
-
* Adopted HTTP flow still has no name (`flow({ do })` outside a unit).
|
|
360
|
-
*/
|
|
361
|
-
HTTP_FLOW_UNNAMED: {
|
|
362
|
-
code: 1045,
|
|
363
|
-
domain: "kernel",
|
|
364
|
-
cause: "An HTTP flow on {method} {path} has no name.",
|
|
365
|
-
fix: 'Use `flow("unit.export", {…})` or export it from a `src/flows/<unit>/` file so the tree can stamp `unit.export`.',
|
|
366
|
-
},
|
|
367
|
-
/**
|
|
368
|
-
* Two live HTTP exposures of the same signal share gates + match shape.
|
|
369
|
-
*/
|
|
370
|
-
LIVE_EXPOSURE_DUPLICATE: {
|
|
371
|
-
code: 1050,
|
|
372
|
-
domain: "kernel",
|
|
373
|
-
cause:
|
|
374
|
-
'Live signal "{signal}" is exposed twice with the same gates ({gates}) and match ({match}).',
|
|
375
|
-
fix: "Use a different gate or path-param filter, or drop the extra route.",
|
|
376
|
-
},
|
|
377
|
-
/**
|
|
378
|
-
* Two MCP tool bindings share the same exposed tool name.
|
|
379
|
-
*/
|
|
380
|
-
MCP_TOOL_DUPLICATE: {
|
|
381
|
-
code: 1060,
|
|
382
|
-
domain: "kernel",
|
|
383
|
-
cause: 'MCP tool "{tool}" is bound twice (flow "{flow}").',
|
|
384
|
-
fix: "Give each MCP tool exposure a unique tool name.",
|
|
385
|
-
},
|
|
284
|
+
ADOPT_BARREL_STALE: { code: 1030, domain: "kernel" },
|
|
285
|
+
/** `http.get()` was never stamped from the file tree — refuse a silent `/`. */
|
|
286
|
+
HTTP_PATH_UNRESOLVED: { code: 1040, domain: "kernel" },
|
|
287
|
+
/** Two HTTP bindings share method + path. */
|
|
288
|
+
HTTP_ROUTE_DUPLICATE: { code: 1041, domain: "kernel" },
|
|
289
|
+
/** Adopted HTTP flow still has no name (`flow({ do })` outside a unit). */
|
|
290
|
+
HTTP_FLOW_UNNAMED: { code: 1045, domain: "kernel" },
|
|
291
|
+
/** Two live HTTP exposures of the same signal share gates + match shape. */
|
|
292
|
+
LIVE_EXPOSURE_DUPLICATE: { code: 1050, domain: "kernel" },
|
|
293
|
+
/** Two MCP tool bindings share the same exposed tool name. */
|
|
294
|
+
MCP_TOOL_DUPLICATE: { code: 1060, domain: "kernel" },
|
|
386
295
|
/**
|
|
387
296
|
* Emit target has no subscriber (unified-theory §21).
|
|
388
297
|
* Thrown at emit when `optional` is false and nobody is subscribed.
|
|
389
298
|
*/
|
|
390
|
-
ORPHAN_EMIT: {
|
|
391
|
-
code: 1240,
|
|
392
|
-
domain: "signal",
|
|
393
|
-
cause: 'Flow "{flow}" emits signal "{resource}" with no subscriber.',
|
|
394
|
-
fix: "Add `on({resource}, …)` or mark the signal `{ optional: true }`.",
|
|
395
|
-
},
|
|
299
|
+
ORPHAN_EMIT: { code: 1240, domain: "signal" },
|
|
396
300
|
/** Emit payload failed the signal's declared Standard Schema. */
|
|
397
|
-
SIGNAL_SCHEMA: {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
code:
|
|
408
|
-
domain: "
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
301
|
+
SIGNAL_SCHEMA: { code: 1250, domain: "signal" },
|
|
302
|
+
/** Domain table/column missing under docker/prod (migrations not applied). */
|
|
303
|
+
DOMAIN_SCHEMA_MISSING: { code: 1110, domain: "store" },
|
|
304
|
+
} as const satisfies Record<string, { readonly code: number; readonly domain: OkeErrorDomain }>;
|
|
305
|
+
|
|
306
|
+
type OkeErrorRowTable = typeof OKE_ERROR_ROWS;
|
|
307
|
+
|
|
308
|
+
/** Registry entries with literal codes and lazy cause/fix. */
|
|
309
|
+
type OkeErrorTable = {
|
|
310
|
+
readonly [K in keyof OkeErrorRowTable]: OkeErrorDefinition & {
|
|
311
|
+
readonly code: OkeErrorRowTable[K]["code"];
|
|
312
|
+
readonly domain: OkeErrorRowTable[K]["domain"];
|
|
313
|
+
};
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Load English cause/fix templates. Computed stem so Bun.build cannot inline them.
|
|
318
|
+
*/
|
|
319
|
+
function loadErrorText(): typeof import("./errors-text.ts") {
|
|
320
|
+
return lazyRequire(import.meta.dir, ["errors", "text"].join("-"));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Read one cause or fix template.
|
|
325
|
+
*
|
|
326
|
+
* @param key - {@link OKE_ERRORS} property name
|
|
327
|
+
* @param part - `cause` or `fix`
|
|
328
|
+
*/
|
|
329
|
+
function errorPart(key: string, part: "cause" | "fix"): string {
|
|
330
|
+
const row = loadErrorText().OKE_ERROR_TEXT[key];
|
|
331
|
+
if (!row) throw new Error(`Missing OKE error text for ${key}.${part}`);
|
|
332
|
+
return row[part];
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Attach lazy cause/fix getters. Strings stay in the text chunk until read.
|
|
337
|
+
*
|
|
338
|
+
* @param rows - Code and domain table
|
|
339
|
+
*/
|
|
340
|
+
function attachErrorText(rows: OkeErrorRowTable): OkeErrorTable {
|
|
341
|
+
const out: Record<string, OkeErrorDefinition> = {};
|
|
342
|
+
for (const key of Object.keys(rows) as (keyof OkeErrorRowTable)[]) {
|
|
343
|
+
const row = rows[key];
|
|
344
|
+
out[key] = {
|
|
345
|
+
code: row.code,
|
|
346
|
+
domain: row.domain,
|
|
347
|
+
get cause() {
|
|
348
|
+
return errorPart(key, "cause");
|
|
349
|
+
},
|
|
350
|
+
get fix() {
|
|
351
|
+
return errorPart(key, "fix");
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
return out as OkeErrorTable;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Permanent error registry. Cause/fix load from the lazy text chunk.
|
|
360
|
+
*
|
|
361
|
+
* `@__PURE__` lets bundles that never read the registry drop the getter
|
|
362
|
+
* table. A plain call looks like a side effect and stays in every importer.
|
|
363
|
+
*/
|
|
364
|
+
export const OKE_ERRORS: OkeErrorTable = /* @__PURE__ */ attachErrorText(OKE_ERROR_ROWS);
|
|
413
365
|
|
|
414
366
|
/** Tenant error keys — definitions live in the lazy `errors-tenant` chunk. */
|
|
415
367
|
export type TenantOkeErrorKey = "TENANT_REQUIRED" | "TENANT_NOT_MEMBER" | "TENANT_UNKNOWN_SCOPE";
|
|
@@ -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
|
);
|