okengine 0.23.0 → 0.23.2
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/package.json +1 -1
- package/site/content/docs/ai/mcp.mdx +4 -4
- package/site/content/docs/elements/channel/index.mdx +4 -4
- package/site/content/docs/elements/channel/receipts.mdx +7 -5
- package/site/content/docs/elements/channel/sms.mdx +2 -1
- package/site/content/docs/elements/flow/index.mdx +22 -18
- package/site/content/docs/elements/signal/broadcast.mdx +4 -2
- package/site/content/docs/elements/signal/index.mdx +15 -14
- package/site/content/docs/plugins/otp.mdx +3 -3
- package/site/content/docs/reference/configuration.mdx +16 -0
- package/site/content/docs/reference/errors.mdx +31 -19
- package/site/content/docs/reference/fx.mdx +12 -4
- package/src/auth/tenants.ts +17 -0
- package/src/cli/doctor-diff.test.ts +29 -1
- package/src/cli/doctor-diff.ts +35 -0
- package/src/cli/manifest-pr-diff.ts +16 -0
- package/src/compiler/aot.test.ts +3 -2
- package/src/compiler/aot.ts +45 -1
- package/src/compiler/dynamic.ts +1 -1
- package/src/compiler/effects-fetch.test.ts +77 -0
- package/src/compiler/effects-infer.ts +87 -16
- package/src/compiler/effects-join.test.ts +48 -0
- package/src/compiler/extract.test.ts +28 -0
- package/src/compiler/extract.ts +11 -1
- package/src/compiler/fx-index.ts +58 -4
- package/src/compiler/http-parse.test.ts +101 -0
- package/src/compiler/http-parse.ts +188 -9
- package/src/compiler/interpret.ts +25 -2
- package/src/console/index.ts +1 -1
- package/src/console/ui-next/dist/assets/{access-page-DDKkhT9v.js → access-page-zBMhZnFm.js} +1 -1
- package/src/console/ui-next/dist/assets/{agent-disclosure-62Xts8Xi.js → agent-disclosure-CBj9f7Ju.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-DyeoKHKA.js → cache-glyph-D3MY49h3.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-DvfpLXsU.js → call-pii-button-L9JxdBhp.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-BY03SeCg.js → collapsible-BvFaX-Zt.js} +1 -1
- package/src/console/ui-next/dist/assets/{decisions-page-CgkKlA56.js → decisions-page-DgB76m-X.js} +1 -1
- package/src/console/ui-next/dist/assets/{duration-tone-Y6HLjyJ5.js → duration-tone-l1DSJ2Vz.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-DLPfNy-_.js → flows-page-Bb9t1lu6.js} +1 -1
- package/src/console/ui-next/dist/assets/{highlighted-json-D7nNzpgB.js → highlighted-json-DMvsi3Ti.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-DdL19zzh.js → http-method-BwhJBTcQ.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-C0lc_s9d.js → index-RhV2jT_7.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-CRlcyLCC.js → observability-page-BRfUILBq.js} +1 -1
- package/src/console/ui-next/dist/assets/{replica-lag-Bb3FWUu9.js → replica-lag-BsceAzIG.js} +1 -1
- package/src/console/ui-next/dist/assets/{request-meta-DYBMWhX8.js → request-meta-C6lTyCXp.js} +1 -1
- package/src/console/ui-next/dist/assets/{store-page-CcE-SXC8.js → store-page-YmE806bJ.js} +1 -1
- package/src/console/ui-next/dist/assets/{trace-detail-sheet-CKMSEQ4s.js → trace-detail-sheet-Di4irS49.js} +1 -1
- package/src/console/ui-next/dist/assets/{tree-expand-toggle-QOfk0M0H.js → tree-expand-toggle-DYeL6Rye.js} +1 -1
- package/src/console/ui-next/dist/assets/{units-page-DKC1CDDd.js → units-page-07agasRA.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-B54v4Yit.js → vault-page-CHBck4n_.js} +1 -1
- package/src/console/ui-next/dist/index.html +1 -1
- package/src/console/ui-next/seed-parked-approval.ts +5 -1
- package/src/drivers/journal-postgres.test.ts +106 -2
- package/src/drivers/journal-postgres.ts +475 -14
- package/src/drivers/postgres.test.ts +60 -0
- package/src/drivers/postgres.ts +79 -5
- package/src/drivers/signal-compete.test.ts +238 -0
- package/src/drivers/signal-postgres.test.ts +170 -0
- package/src/drivers/signal-postgres.ts +486 -213
- package/src/drivers/signal-redis.test.ts +224 -0
- package/src/drivers/signal-redis.ts +934 -93
- package/src/drivers/signal-types.ts +12 -0
- package/src/elements/ai/approval.test.ts +15 -3
- package/src/elements/ai/approval.ts +15 -9
- package/src/elements/ai/mcp-protocol.ts +5 -6
- package/src/elements/ai/runtime.ts +20 -19
- package/src/elements/channel/runtime.ts +42 -4
- package/src/elements/channel/sql-ledger.test.ts +404 -0
- package/src/elements/channel/sql-ledger.ts +395 -0
- package/src/elements/clock/chaos-child.ts +12 -2
- package/src/elements/clock/durable.ts +11 -0
- package/src/elements/signal/runtime.ts +9 -0
- package/src/elements/signal.test.ts +1 -0
- package/src/elements/store/cache-bus.test.ts +121 -0
- package/src/elements/store/cache-bus.ts +278 -0
- package/src/elements/store/cache.test.ts +179 -15
- package/src/elements/store/cache.ts +357 -51
- package/src/elements/store/prepare-row.test.ts +6 -2
- package/src/elements/store/runtime.ts +44 -2
- package/src/elements/store/sql-errors.test.ts +131 -0
- package/src/elements/store/sql-errors.ts +39 -16
- package/src/elements/store/sql-nested-tx.test.ts +344 -0
- package/src/elements/store/sql-session.test.ts +2 -0
- package/src/elements/store/sql-session.ts +275 -9
- package/src/elements/store.ts +1 -0
- package/src/kernel/app.ts +207 -43
- package/src/kernel/auto-cache.test.ts +228 -0
- package/src/kernel/auto-registry.test.ts +5 -1
- package/src/kernel/boot-bind/channel.ts +63 -2
- package/src/kernel/boot-bind/honor-config.test.ts +40 -10
- package/src/kernel/boot-bind/signal.ts +72 -25
- package/src/kernel/boot-bind/store.ts +16 -1
- package/src/kernel/boot.test.ts +2 -2
- package/src/kernel/boot.ts +54 -10
- package/src/kernel/boundary-contract.ts +7 -0
- package/src/kernel/builtin-errors.ts +2 -0
- package/src/kernel/call.test.ts +1 -0
- package/src/kernel/errors-compiler.ts +11 -0
- package/src/kernel/errors-text.ts +16 -0
- package/src/kernel/errors.ts +8 -0
- package/src/kernel/flow.ts +33 -3
- package/src/kernel/fx-call-types.test.ts +32 -0
- package/src/kernel/fx-decide.ts +11 -7
- package/src/kernel/fx-sql-handle.ts +52 -7
- package/src/kernel/fx.test.ts +1 -3
- package/src/kernel/fx.ts +27 -16
- package/src/kernel/horizontal-child.ts +54 -1
- package/src/kernel/horizontal.integration.test.ts +131 -7
- package/src/kernel/idempotency.test.ts +4 -4
- package/src/kernel/journal-boot.test.ts +83 -6
- package/src/kernel/journal.test.ts +158 -0
- package/src/kernel/journal.ts +515 -54
- package/src/kernel/pipeline-tenant.ts +5 -1
- package/src/kernel/pipeline.ts +5 -0
- package/src/kernel/signal-tx.ts +28 -0
- package/src/kernel/store-transaction.test.ts +95 -0
- package/src/mcp/authorization.ts +5 -11
- package/src/mcp/confirmation.ts +203 -38
- package/src/mcp/docs-server.ts +26 -4
- package/src/mcp/index.ts +4 -0
- package/src/mcp/mcp.test.ts +404 -33
- package/src/mcp/protocol.ts +8 -2
- package/src/mcp/server.ts +31 -5
- package/src/mcp/session.ts +7 -0
- package/src/mcp/tools.ts +68 -19
- package/src/mcp/versions.ts +60 -0
- package/src/plugins/index.ts +1 -0
- package/src/plugins/mena.ts +56 -0
- package/src/runtime/bun.ts +5 -0
- package/src/runtime/types.ts +5 -0
package/src/kernel/app.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { bindAgentApprovalFlows } from "../elements/ai/approval-http.ts";
|
|
10
10
|
import { compileRoute } from "../compiler/dynamic.ts";
|
|
11
|
+
import { HttpBodyRejected, parseBody, type ParseBodyOptions } from "../compiler/http-parse.ts";
|
|
11
12
|
import type { CompiledRoute } from "../compiler/aot.ts";
|
|
12
13
|
import { encodeExecuteResult, encodeFailure } from "../compiler/response.ts";
|
|
13
14
|
import { validate } from "../validation/standard-schema.ts";
|
|
@@ -87,9 +88,12 @@ import {
|
|
|
87
88
|
createJournal,
|
|
88
89
|
createJournalSlot,
|
|
89
90
|
createMemoryJournalStore,
|
|
91
|
+
clearJournalHeartbeats,
|
|
90
92
|
hasJournalLease,
|
|
91
93
|
isJournalLeaseBusy,
|
|
92
94
|
isJournalSuspend,
|
|
95
|
+
isLostJournalLease,
|
|
96
|
+
JournalLeaseBusy,
|
|
93
97
|
JOURNAL_DEFAULT_LEASE_MS,
|
|
94
98
|
type JournalSession,
|
|
95
99
|
type JournalStore,
|
|
@@ -368,6 +372,19 @@ export interface OkeOptions {
|
|
|
368
372
|
};
|
|
369
373
|
};
|
|
370
374
|
};
|
|
375
|
+
/**
|
|
376
|
+
* Tier-1 read cache. Omitted caches pure store-read flows. `{ auto: false }`
|
|
377
|
+
* turns that off. A flow can still opt in with `cache: true` or a duration,
|
|
378
|
+
* and `cache: false` always disables. The key includes tenant, locale,
|
|
379
|
+
* scopes, and membership roles.
|
|
380
|
+
*/
|
|
381
|
+
readonly cache?: {
|
|
382
|
+
readonly auto?: boolean;
|
|
383
|
+
/** Auto-cache entry cap. Default 10_000. */
|
|
384
|
+
readonly maxEntries?: number;
|
|
385
|
+
/** Auto-cache TTL when a flow sets none. Default 60_000. */
|
|
386
|
+
readonly defaultTtlMs?: number;
|
|
387
|
+
};
|
|
371
388
|
/** Channel runtime options. */
|
|
372
389
|
readonly channel?: BootOptions["channel"];
|
|
373
390
|
/** AI runtime options. */
|
|
@@ -389,6 +406,15 @@ export interface OkeOptions {
|
|
|
389
406
|
readonly schedulerIntervalMs?: number;
|
|
390
407
|
/** Durable-run lease duration ms (default 30_000). Forwarded to boot. */
|
|
391
408
|
readonly journalLeaseMs?: number;
|
|
409
|
+
/** Request body cap in bytes. Default 1 MiB. A route `maxBodySize` overrides it. */
|
|
410
|
+
readonly maxRequestBodySize?: number;
|
|
411
|
+
/**
|
|
412
|
+
* Opt-in durable code version. Unset stamps nothing and checks nothing.
|
|
413
|
+
* When set, new runs are stamped `app:<version>`. A stored stamp without
|
|
414
|
+
* the `app:` prefix (0.23.1 and earlier) is treated as compatible and
|
|
415
|
+
* restamped. An `app:` stamp that differs fails the run with OKE1076.
|
|
416
|
+
*/
|
|
417
|
+
readonly codeVersion?: string;
|
|
392
418
|
}
|
|
393
419
|
|
|
394
420
|
/** Readiness probe state — see `GET /_/ready`. */
|
|
@@ -598,6 +624,8 @@ export interface OkeApp<D extends Record<string, unknown> = {}, R extends AppRou
|
|
|
598
624
|
readonly runId?: string;
|
|
599
625
|
/** Tenant identity for cron / `fx.call` (propagated, unlike auth). */
|
|
600
626
|
readonly tenant?: { readonly id: string | null };
|
|
627
|
+
/** Explicit locale override for {@link Fx.t} / channel sends. */
|
|
628
|
+
readonly locale?: string;
|
|
601
629
|
},
|
|
602
630
|
): Promise<ExecuteResult>;
|
|
603
631
|
/**
|
|
@@ -792,6 +820,7 @@ function registerHttpRoute(
|
|
|
792
820
|
smart: { add(method: string, path: string, value: Binding): void },
|
|
793
821
|
compiled: WeakMap<Binding, CompiledRoute>,
|
|
794
822
|
aot: boolean,
|
|
823
|
+
maxRequestBodySize?: number,
|
|
795
824
|
): void {
|
|
796
825
|
const trigger = binding.trigger;
|
|
797
826
|
if (trigger.kind !== "http") return;
|
|
@@ -823,7 +852,7 @@ function registerHttpRoute(
|
|
|
823
852
|
seenLiveExposures.set(exposure, binding.flow.name);
|
|
824
853
|
}
|
|
825
854
|
smart.add(trigger.method, trigger.path, binding);
|
|
826
|
-
compiled.set(binding, compileHttpBinding(binding, aot));
|
|
855
|
+
compiled.set(binding, compileHttpBinding(binding, aot, maxRequestBodySize));
|
|
827
856
|
}
|
|
828
857
|
|
|
829
858
|
/**
|
|
@@ -1114,7 +1143,15 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1114
1143
|
for (const b of adopted) {
|
|
1115
1144
|
if (b.trigger.kind === "http") {
|
|
1116
1145
|
assertHttpBindingReady(b);
|
|
1117
|
-
registerHttpRoute(
|
|
1146
|
+
registerHttpRoute(
|
|
1147
|
+
b,
|
|
1148
|
+
seenHttpRoutes,
|
|
1149
|
+
seenLiveExposures,
|
|
1150
|
+
smart,
|
|
1151
|
+
compiled,
|
|
1152
|
+
aot,
|
|
1153
|
+
options.maxRequestBodySize,
|
|
1154
|
+
);
|
|
1118
1155
|
}
|
|
1119
1156
|
if (b.trigger.kind === "mcp") {
|
|
1120
1157
|
registerMcpTool(b, seenMcpTools, mcpToolsByName);
|
|
@@ -1137,7 +1174,15 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1137
1174
|
registerFlow(b.flow);
|
|
1138
1175
|
if (b.trigger.kind === "http") {
|
|
1139
1176
|
assertHttpBindingReady(b);
|
|
1140
|
-
registerHttpRoute(
|
|
1177
|
+
registerHttpRoute(
|
|
1178
|
+
b,
|
|
1179
|
+
seenHttpRoutes,
|
|
1180
|
+
seenLiveExposures,
|
|
1181
|
+
smart,
|
|
1182
|
+
compiled,
|
|
1183
|
+
aot,
|
|
1184
|
+
options.maxRequestBodySize,
|
|
1185
|
+
);
|
|
1141
1186
|
}
|
|
1142
1187
|
if (b.trigger.kind === "mcp") {
|
|
1143
1188
|
registerMcpTool(b, seenMcpTools, mcpToolsByName);
|
|
@@ -1389,6 +1434,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1389
1434
|
journalStore: store,
|
|
1390
1435
|
runId,
|
|
1391
1436
|
...(hasJournalLease(store) ? { lease: { instanceId, leaseMs } } : {}),
|
|
1437
|
+
...(options.codeVersion ? { codeVersion: options.codeVersion } : {}),
|
|
1392
1438
|
now,
|
|
1393
1439
|
fx: {
|
|
1394
1440
|
...durableResumeFx(),
|
|
@@ -1411,7 +1457,13 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1411
1457
|
});
|
|
1412
1458
|
} catch (err) {
|
|
1413
1459
|
if (isJournalLeaseBusy(err)) return;
|
|
1414
|
-
|
|
1460
|
+
console.warn(
|
|
1461
|
+
JSON.stringify({
|
|
1462
|
+
event: "journal.resume.failed",
|
|
1463
|
+
runId,
|
|
1464
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1465
|
+
}),
|
|
1466
|
+
);
|
|
1415
1467
|
}
|
|
1416
1468
|
} finally {
|
|
1417
1469
|
inflightRuns.delete(runId);
|
|
@@ -1587,6 +1639,19 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1587
1639
|
startScheduler: overrides?.startScheduler ?? options.startScheduler,
|
|
1588
1640
|
schedulerIntervalMs: overrides?.schedulerIntervalMs ?? options.schedulerIntervalMs,
|
|
1589
1641
|
journalLeaseMs: overrides?.journalLeaseMs ?? options.journalLeaseMs,
|
|
1642
|
+
codeVersion: overrides?.codeVersion ?? options.codeVersion,
|
|
1643
|
+
...(options.cache
|
|
1644
|
+
? {
|
|
1645
|
+
cache: {
|
|
1646
|
+
...(options.cache.maxEntries !== undefined
|
|
1647
|
+
? { maxEntries: options.cache.maxEntries }
|
|
1648
|
+
: {}),
|
|
1649
|
+
...(options.cache.defaultTtlMs !== undefined
|
|
1650
|
+
? { defaultTtlMs: options.cache.defaultTtlMs }
|
|
1651
|
+
: {}),
|
|
1652
|
+
},
|
|
1653
|
+
}
|
|
1654
|
+
: {}),
|
|
1590
1655
|
bindings: adopted,
|
|
1591
1656
|
flows: [...flowsByName.values()],
|
|
1592
1657
|
onCronFire: overrides?.onCronFire ?? handleCronFire,
|
|
@@ -1970,6 +2035,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1970
2035
|
// Hold the run lease for the request's lifetime — a crash mid-run
|
|
1971
2036
|
// leaves an expired lease another instance can reclaim and resume.
|
|
1972
2037
|
...(hasJournalLease(store) ? { lease: { instanceId, leaseMs } } : {}),
|
|
2038
|
+
...(options.codeVersion ? { codeVersion: options.codeVersion } : {}),
|
|
1973
2039
|
});
|
|
1974
2040
|
};
|
|
1975
2041
|
|
|
@@ -2085,6 +2151,16 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
2085
2151
|
: undefined;
|
|
2086
2152
|
// Revealed PII must not hit a prior masked entry or land in cache.
|
|
2087
2153
|
const reveal = extras?.trustedInvoke === true && extras.revealPii === true;
|
|
2154
|
+
const cacheAuto = options.cache?.auto !== false;
|
|
2155
|
+
const cacheCaller = {
|
|
2156
|
+
userId: fx.auth.userId,
|
|
2157
|
+
tenantId: fx.tenant.id,
|
|
2158
|
+
locale: fx.locale,
|
|
2159
|
+
scopes: fx.auth.scopes,
|
|
2160
|
+
roles: principals?.cacheRoles ?? [],
|
|
2161
|
+
verified: fx.auth.verified === true,
|
|
2162
|
+
operatorId: fx.operator.id,
|
|
2163
|
+
};
|
|
2088
2164
|
const cacheOk =
|
|
2089
2165
|
cache !== undefined &&
|
|
2090
2166
|
storeRt !== undefined &&
|
|
@@ -2092,12 +2168,13 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
2092
2168
|
!reveal &&
|
|
2093
2169
|
cache.autoCacheEligible({
|
|
2094
2170
|
cache: flowDef.cache,
|
|
2171
|
+
auto: cacheAuto,
|
|
2095
2172
|
durable: flowDef.durable,
|
|
2096
2173
|
effects: cacheEffects,
|
|
2097
2174
|
});
|
|
2098
2175
|
const dims =
|
|
2099
2176
|
cacheOk && cache && cacheEffects
|
|
2100
|
-
? cache.tier1DimsByResource(cacheEffects, flowDef.name, input,
|
|
2177
|
+
? cache.tier1DimsByResource(cacheEffects, flowDef.name, input, cacheCaller)
|
|
2101
2178
|
: undefined;
|
|
2102
2179
|
if (cacheOk && cache && dims && storeRt) {
|
|
2103
2180
|
const keys = cache.tier1KeysForReads(cacheEffects, dims);
|
|
@@ -2128,43 +2205,79 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
2128
2205
|
journalSession?.rewind();
|
|
2129
2206
|
return flowDef.do(input as never, fx);
|
|
2130
2207
|
};
|
|
2131
|
-
const
|
|
2208
|
+
const flightKey =
|
|
2209
|
+
cacheOk && cache && dims && cacheEffects
|
|
2210
|
+
? cache.tier1KeysForReads(cacheEffects, dims)[0]
|
|
2211
|
+
: undefined;
|
|
2212
|
+
const flightResources = (cacheEffects?.reads ?? []).filter(
|
|
2213
|
+
cache?.isStoreResourceRef ?? (() => false),
|
|
2214
|
+
);
|
|
2215
|
+
const generation =
|
|
2216
|
+
storeRt && flightKey
|
|
2217
|
+
? Object.fromEntries(
|
|
2218
|
+
flightResources.map((resource) => [
|
|
2219
|
+
resource,
|
|
2220
|
+
storeRt.cache.generationOf(resource),
|
|
2221
|
+
]),
|
|
2222
|
+
)
|
|
2223
|
+
: undefined;
|
|
2224
|
+
let raw: unknown;
|
|
2225
|
+
let cacheable = true;
|
|
2226
|
+
try {
|
|
2227
|
+
const load = () => (flowDef.retry ? fxRetry(run, flowDef.retry) : run());
|
|
2228
|
+
if (flightKey && storeRt) {
|
|
2229
|
+
const flight = await storeRt.cache.coalesce(flightKey, flightResources, load);
|
|
2230
|
+
raw = flight.value;
|
|
2231
|
+
cacheable =
|
|
2232
|
+
flight.cacheable &&
|
|
2233
|
+
(generation === undefined || storeRt.cache.sameGeneration(generation));
|
|
2234
|
+
} else {
|
|
2235
|
+
raw = await load();
|
|
2236
|
+
}
|
|
2237
|
+
} finally {
|
|
2238
|
+
if (cache && storeRt && cacheEffects) {
|
|
2239
|
+
const ledgerFx = cache.effectsFromLedger(ledger.entries);
|
|
2240
|
+
const writeEffects: Effects = {
|
|
2241
|
+
writes: [
|
|
2242
|
+
...new Set([...(cacheEffects.writes ?? []), ...(ledgerFx.writes ?? [])]),
|
|
2243
|
+
].filter(cache.isStoreResourceRef),
|
|
2244
|
+
};
|
|
2245
|
+
if ((writeEffects.writes?.length ?? 0) > 0) {
|
|
2246
|
+
storeRt.onWriteEffects(writeEffects);
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2132
2250
|
const output = isFlowFailure(raw) ? raw : await projectFlowOut(flowDef.out, raw);
|
|
2133
2251
|
if (!isFlowFailure(output) && cache && storeRt && cacheEffects) {
|
|
2134
2252
|
const ledgerFx = cache.effectsFromLedger(ledger.entries);
|
|
2135
|
-
const
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
}
|
|
2140
|
-
|
|
2141
|
-
storeRt.onWriteEffects(writeEffects);
|
|
2142
|
-
}
|
|
2143
|
-
const mergedReads = [
|
|
2144
|
-
...new Set([...(cacheEffects.reads ?? []), ...(ledgerFx.reads ?? [])]),
|
|
2145
|
-
].filter(cache.isStoreResourceRef);
|
|
2146
|
-
if (mergedReads.length > 0) {
|
|
2147
|
-
learnedTier1Reads.set(flowDef.name, mergedReads);
|
|
2253
|
+
const observed = cache.mergeEffects(cacheEffects, ledgerFx);
|
|
2254
|
+
const storeReads = (observed.reads ?? []).filter(cache.isStoreResourceRef);
|
|
2255
|
+
if (cache.autoCachePure(observed) && storeReads.length > 0) {
|
|
2256
|
+
learnedTier1Reads.set(flowDef.name, storeReads);
|
|
2257
|
+
} else {
|
|
2258
|
+
learnedTier1Reads.delete(flowDef.name);
|
|
2148
2259
|
}
|
|
2149
|
-
const putEffects: Effects = {
|
|
2150
|
-
reads: mergedReads,
|
|
2151
|
-
...(ledgerFx.writes ? { writes: ledgerFx.writes } : {}),
|
|
2152
|
-
...(ledgerFx.asks ? { asks: ledgerFx.asks } : {}),
|
|
2153
|
-
};
|
|
2154
2260
|
const storeAfter =
|
|
2155
2261
|
!reveal &&
|
|
2156
2262
|
cache.autoCacheEligible({
|
|
2157
2263
|
cache: flowDef.cache,
|
|
2264
|
+
auto: cacheAuto,
|
|
2158
2265
|
durable: flowDef.durable,
|
|
2159
|
-
effects:
|
|
2266
|
+
effects: observed,
|
|
2160
2267
|
});
|
|
2161
|
-
if (
|
|
2268
|
+
if (
|
|
2269
|
+
cacheable &&
|
|
2270
|
+
storeAfter &&
|
|
2271
|
+
output !== undefined &&
|
|
2272
|
+
!loadFx().isJsonStreamResult(output)
|
|
2273
|
+
) {
|
|
2162
2274
|
const ttlMs =
|
|
2163
2275
|
typeof flowDef.cache === "string" ? cache.parseTtlMs(flowDef.cache) : undefined;
|
|
2276
|
+
const putEffects: Effects = { reads: storeReads };
|
|
2164
2277
|
storeRt.putTier1(
|
|
2165
2278
|
putEffects,
|
|
2166
2279
|
output,
|
|
2167
|
-
cache.tier1DimsByResource(putEffects, flowDef.name, input,
|
|
2280
|
+
cache.tier1DimsByResource(putEffects, flowDef.name, input, cacheCaller),
|
|
2168
2281
|
ttlMs,
|
|
2169
2282
|
);
|
|
2170
2283
|
if (!cacheOk) telemetry.cacheMisses += 1;
|
|
@@ -2215,6 +2328,9 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
2215
2328
|
const journal = openJournal();
|
|
2216
2329
|
const resumeId = claimed.kind === "reclaimed" ? claimed.row.runId : undefined;
|
|
2217
2330
|
try {
|
|
2331
|
+
if (resumeId && inflightRuns.has(resumeId)) {
|
|
2332
|
+
throw new JournalLeaseBusy(resumeId);
|
|
2333
|
+
}
|
|
2218
2334
|
const session = resumeId
|
|
2219
2335
|
? await journal.resume(resumeId)
|
|
2220
2336
|
: await journal.start(flowDef.name, validated);
|
|
@@ -2319,6 +2435,9 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
2319
2435
|
};
|
|
2320
2436
|
return undefined;
|
|
2321
2437
|
}
|
|
2438
|
+
if (flowDef.durable && journalSession && isLostJournalLease(err)) {
|
|
2439
|
+
return fail("JournalLeaseBusy", {});
|
|
2440
|
+
}
|
|
2322
2441
|
throw err;
|
|
2323
2442
|
}
|
|
2324
2443
|
},
|
|
@@ -2367,13 +2486,22 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
2367
2486
|
});
|
|
2368
2487
|
} else if (!sleeping && isTerminalFailure(result)) {
|
|
2369
2488
|
const terminalErr = result.failure ?? result.ctx.error;
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2489
|
+
const lost =
|
|
2490
|
+
isLostJournalLease(terminalErr) ||
|
|
2491
|
+
isJournalLeaseBusy(terminalErr) ||
|
|
2492
|
+
(terminalErr != null &&
|
|
2493
|
+
typeof terminalErr === "object" &&
|
|
2494
|
+
"error" in terminalErr &&
|
|
2495
|
+
(terminalErr as { error?: { code?: string } }).error?.code === "JournalLeaseBusy");
|
|
2496
|
+
if (!lost) {
|
|
2497
|
+
await runCompensationPhase({
|
|
2498
|
+
flow: flowDef,
|
|
2499
|
+
input: ctx.input,
|
|
2500
|
+
session: journalSession,
|
|
2501
|
+
fx,
|
|
2502
|
+
error: terminalErr,
|
|
2503
|
+
});
|
|
2504
|
+
}
|
|
2377
2505
|
} else if (parked && sleeping) {
|
|
2378
2506
|
await journalSession.commit("sleeping", { wakeAt: sleeping.wakeAt });
|
|
2379
2507
|
} else if (!sleeping) {
|
|
@@ -2487,6 +2615,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
2487
2615
|
},
|
|
2488
2616
|
async stop() {
|
|
2489
2617
|
if (!bootResult) return;
|
|
2618
|
+
clearJournalHeartbeats();
|
|
2490
2619
|
bootResult.stopScheduler();
|
|
2491
2620
|
// Realtime bridge down before connections close.
|
|
2492
2621
|
{
|
|
@@ -2514,7 +2643,11 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
2514
2643
|
/** Shared identity store when `gate.auth` is enabled. */
|
|
2515
2644
|
identities: gateConfig.auth?.identities,
|
|
2516
2645
|
async resumeDurable(now) {
|
|
2517
|
-
|
|
2646
|
+
// Due selection uses one sample. The run itself must not: a frozen
|
|
2647
|
+
// closure wrote leases that were already expired by the time persist ran.
|
|
2648
|
+
const sample = (): number => bootResult?.clock?.now() ?? options.fx?.now?.() ?? Date.now();
|
|
2649
|
+
const t = now ?? sample();
|
|
2650
|
+
const live = typeof now === "number" ? (): number => now : sample;
|
|
2518
2651
|
const { store, instanceId, leaseMs } = activeJournal();
|
|
2519
2652
|
if (hasJournalLease(store)) {
|
|
2520
2653
|
// Shared store is the wake schedule — claim due sleeps across all
|
|
@@ -2523,14 +2656,14 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
2523
2656
|
const due = await store.claimDueSleep(instanceId, t, leaseMs);
|
|
2524
2657
|
if (!due) break;
|
|
2525
2658
|
sleepingRuns.delete(due.id);
|
|
2526
|
-
await resumeDurableRun(due.id, due.flow, due.input,
|
|
2659
|
+
await resumeDurableRun(due.id, due.flow, due.input, live);
|
|
2527
2660
|
}
|
|
2528
2661
|
// Crash sweep: the boot orphan scan is once-only, so each tick also
|
|
2529
2662
|
// reclaims `running` runs whose holder's lease has expired (same
|
|
2530
2663
|
// takeover physics as Clock: lease expiry + next tick).
|
|
2531
2664
|
for (const orphan of await store.listOrphans(t)) {
|
|
2532
2665
|
if (orphan.status !== "running") continue;
|
|
2533
|
-
await resumeDurableRun(orphan.id, orphan.flow, orphan.input,
|
|
2666
|
+
await resumeDurableRun(orphan.id, orphan.flow, orphan.input, live);
|
|
2534
2667
|
}
|
|
2535
2668
|
return;
|
|
2536
2669
|
}
|
|
@@ -2543,7 +2676,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
2543
2676
|
input: sleeper.input,
|
|
2544
2677
|
journalStore: store,
|
|
2545
2678
|
runId,
|
|
2546
|
-
now:
|
|
2679
|
+
now: live,
|
|
2547
2680
|
fx: durableResumeFx(),
|
|
2548
2681
|
});
|
|
2549
2682
|
if (result.status === "sleeping") {
|
|
@@ -2684,9 +2817,15 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
2684
2817
|
flowLabel = target.name;
|
|
2685
2818
|
let internalInput: unknown;
|
|
2686
2819
|
try {
|
|
2687
|
-
internalInput = await request
|
|
2688
|
-
|
|
2689
|
-
|
|
2820
|
+
internalInput = await parseBody(request, {
|
|
2821
|
+
maxBytes: options.maxRequestBodySize,
|
|
2822
|
+
jsonContentType: "required",
|
|
2823
|
+
});
|
|
2824
|
+
} catch (err) {
|
|
2825
|
+
if (err instanceof HttpBodyRejected) {
|
|
2826
|
+
return respond(encodeFailure(bodyFailure(err, request)));
|
|
2827
|
+
}
|
|
2828
|
+
throw err;
|
|
2690
2829
|
}
|
|
2691
2830
|
const internalResult = await execute(
|
|
2692
2831
|
target,
|
|
@@ -2753,7 +2892,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
2753
2892
|
|
|
2754
2893
|
let route = compiled.get(binding);
|
|
2755
2894
|
if (!route && binding.trigger.kind === "http") {
|
|
2756
|
-
route = compileHttpBinding(binding, aot);
|
|
2895
|
+
route = compileHttpBinding(binding, aot, options.maxRequestBodySize);
|
|
2757
2896
|
compiled.set(binding, route);
|
|
2758
2897
|
}
|
|
2759
2898
|
|
|
@@ -2859,7 +2998,31 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
2859
2998
|
return app;
|
|
2860
2999
|
}
|
|
2861
3000
|
|
|
2862
|
-
function
|
|
3001
|
+
function httpBodyOptions(trigger: HttpTrigger, maxRequestBodySize?: number): ParseBodyOptions {
|
|
3002
|
+
const contract = trigger.contract as
|
|
3003
|
+
| { readonly maxBodySize?: number; readonly jsonContentType?: "required" | "any" }
|
|
3004
|
+
| undefined;
|
|
3005
|
+
return {
|
|
3006
|
+
maxBytes: contract?.maxBodySize ?? maxRequestBodySize,
|
|
3007
|
+
...(contract?.jsonContentType ? { jsonContentType: contract.jsonContentType } : {}),
|
|
3008
|
+
};
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
function bodyFailure(err: HttpBodyRejected, request: Request): ReturnType<typeof fail> {
|
|
3012
|
+
if (err.code === "InvalidQuery") {
|
|
3013
|
+
return fail("InvalidQuery", { reason: err.reason ?? "malformed_body" });
|
|
3014
|
+
}
|
|
3015
|
+
if (err.code === "UnsupportedMediaType") {
|
|
3016
|
+
return fail("UnsupportedMediaType", { contentType: request.headers.get("content-type") ?? "" });
|
|
3017
|
+
}
|
|
3018
|
+
return fail("PayloadTooLarge", {});
|
|
3019
|
+
}
|
|
3020
|
+
|
|
3021
|
+
function compileHttpBinding(
|
|
3022
|
+
binding: Binding,
|
|
3023
|
+
aot: boolean,
|
|
3024
|
+
maxRequestBodySize?: number,
|
|
3025
|
+
): CompiledRoute {
|
|
2863
3026
|
const trigger = binding.trigger as HttpTrigger;
|
|
2864
3027
|
const hookFns: Array<(...args: never[]) => unknown> = [];
|
|
2865
3028
|
for (const list of Object.values(binding.flow.hooks)) {
|
|
@@ -2875,6 +3038,7 @@ function compileHttpBinding(binding: Binding, aot: boolean): CompiledRoute {
|
|
|
2875
3038
|
handler: binding.flow.do as (...args: never[]) => unknown,
|
|
2876
3039
|
hooks: hookFns,
|
|
2877
3040
|
schema: binding.flow.in,
|
|
3041
|
+
body: httpBodyOptions(trigger, maxRequestBodySize),
|
|
2878
3042
|
},
|
|
2879
3043
|
aot,
|
|
2880
3044
|
);
|