okengine 0.13.0 → 0.15.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/manifest.v1.schema.json +7 -1
- package/package.json +4 -5
- package/site/content/docs/ai/mcp.mdx +27 -2
- package/site/content/docs/elements/ai.mdx +59 -11
- package/site/content/docs/elements/channel.mdx +1 -0
- package/site/content/docs/elements/clock.mdx +30 -12
- package/site/content/docs/elements/flow.mdx +9 -3
- package/site/content/docs/elements/gate.mdx +25 -22
- package/site/content/docs/elements/signal.mdx +71 -25
- package/site/content/docs/elements/store.mdx +87 -28
- package/site/content/docs/elements/vault.mdx +1 -0
- package/site/content/docs/get-started/basic-usage.mdx +22 -13
- package/site/content/docs/get-started/index.mdx +5 -0
- package/site/content/docs/get-started/installation.mdx +2 -2
- package/site/content/docs/get-started/introduction.mdx +11 -3
- package/site/content/docs/get-started/meta.json +1 -1
- package/site/content/docs/get-started/project-structure.mdx +932 -0
- package/site/content/docs/reference/cli.md +13 -10
- package/site/content/docs/reference/client.mdx +25 -21
- package/site/content/docs/reference/fx.mdx +27 -22
- package/site/content/docs/reference/security.md +1 -1
- package/src/auth/bindings.ts +14 -5
- package/src/auth/cross-plane.ts +10 -3
- package/src/cli/ai-setup/ai-setup.test.ts +62 -0
- package/src/cli/ai-setup/apply.ts +32 -1
- package/src/cli/ai-setup/recommend.test.ts +25 -0
- package/src/cli/ai-setup/recommend.ts +8 -3
- package/src/cli/attach-host-console.test.ts +3 -2
- package/src/cli/attach-host-console.ts +3 -0
- package/src/cli/build.test.ts +4 -2
- package/src/cli/db-seed.ts +48 -15
- package/src/cli/db.test.ts +104 -0
- package/src/cli/db.ts +122 -10
- package/src/cli/dev-controls.test.ts +20 -0
- package/src/cli/dev-controls.ts +13 -0
- package/src/cli/dev.test.ts +15 -4
- package/src/cli/dev.ts +102 -29
- package/src/cli/ensure-drizzle-config.ts +2 -1
- package/src/cli/tui/DevLive.tsx +12 -2
- package/src/cli/tui/keys.test.ts +2 -1
- package/src/cli/tui/keys.ts +3 -2
- package/src/compiler/aot.test.ts +1 -1
- package/src/compiler/effects-infer.ts +58 -3
- package/src/compiler/extract.test.ts +458 -29
- package/src/compiler/extract.ts +387 -69
- package/src/compiler/flow-path.test.ts +96 -0
- package/src/compiler/flow-path.ts +173 -0
- package/src/compiler/generate-adopt.test.ts +112 -3
- package/src/compiler/generate-adopt.ts +280 -26
- package/src/compiler/response.ts +45 -1
- package/src/compiler/schema-from-ast.ts +609 -0
- package/src/console/server/ai.ts +5 -2
- package/src/console/server/bind.ts +2 -2
- package/src/console/server/console.test.ts +3 -0
- package/src/console/server/flows.ts +121 -40
- package/src/console/server/invoke-user-flow.test.ts +3 -1
- package/src/console/server/invoke-user-flow.ts +17 -2
- package/src/console/server/iso-at.test.ts +9 -0
- package/src/console/server/iso-at.ts +11 -0
- package/src/console/server/security-headers.ts +6 -3
- package/src/console/server/serve.ts +17 -2
- package/src/console/server/spa-proxy.test.ts +1 -0
- package/src/console/server/sql-catalog.ts +147 -7
- package/src/console/server/sql-rls.test.ts +16 -0
- package/src/console/server/state.ts +8 -0
- package/src/console/server/store.test.ts +7 -4
- package/src/console/server/store.ts +142 -58
- package/src/console/ui-next/dist/assets/agent-disclosure-C0X1fbWF.js +1 -0
- package/src/console/ui-next/dist/assets/cache-glyph-F1FI122b.js +1 -0
- package/src/console/ui-next/dist/assets/{call-pii-button-bqkxMrJH.js → call-pii-button-DEDCl_j3.js} +1 -1
- package/src/console/ui-next/dist/assets/collapsible-LPqGvfoz.js +1 -0
- package/src/console/ui-next/dist/assets/{confirm-sheet-DDCRmG62.js → confirm-sheet-3ptDzXbz.js} +1 -1
- package/src/console/ui-next/dist/assets/{duration-tone-oiRxPVsZ.js → duration-tone-sC3lGABz.js} +1 -1
- package/src/console/ui-next/dist/assets/{explorer-empty-HjTnVQoR.js → explorer-empty-BvYrygyO.js} +1 -1
- package/src/console/ui-next/dist/assets/flows-page-Dluo1Bd7.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-CvDPvveV.js → highlighted-json-CS_O8L-r.js} +1 -1
- package/src/console/ui-next/dist/assets/http-method--sWDdXSB.js +1 -0
- package/src/console/ui-next/dist/assets/index-Ca3HZMVq.js +66 -0
- package/src/console/ui-next/dist/assets/index-UtTDRQpS.css +2 -0
- package/src/console/ui-next/dist/assets/{link-DF7SZ9Ek.js → link-COeyggt-.js} +1 -1
- package/src/console/ui-next/dist/assets/observability-page-HK9-BO8z.js +4 -0
- package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +1 -0
- package/src/console/ui-next/dist/assets/replica-lag-DBIFFf7d.js +18 -0
- package/src/console/ui-next/dist/assets/sheet-form-Dnwa5oOR.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-CoqwEIi0.js +1 -0
- package/src/console/ui-next/dist/assets/skeleton-BHcfDYcb.js +1 -0
- package/src/console/ui-next/dist/assets/store-page-BSohH3wM.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-CFIGRnnA.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-BgLl34w7.js +54 -0
- package/src/console/ui-next/dist/assets/units-page-B0cFE76A.js +1 -0
- package/src/console/ui-next/dist/assets/{use-vault-list-Cl79j_ku.js → use-vault-list-BLbZhzyF.js} +1 -1
- package/src/console/ui-next/dist/assets/vault-page-B1SbYe10.js +2 -0
- package/src/console/ui-next/dist/index.html +6 -6
- package/src/console/ui-next/seed-invoke-host.ts +2 -0
- package/src/console/ui-next/src/client.ts +35 -7
- package/src/console/ui-next/src/components/motion/table/index.tsx +16 -0
- package/src/console/ui-next/src/components/motion/table/types.ts +4 -0
- package/src/console/ui-next/src/components/ui/kbd.tsx +2 -2
- package/src/console/ui-next/src/features/flows/graph/build-flow-graph.test.ts +18 -0
- package/src/console/ui-next/src/features/flows/graph/build-flow-graph.ts +14 -1
- package/src/console/ui-next/src/features/flows/graph/neighborhood.test.ts +17 -0
- package/src/console/ui-next/src/features/flows/graph/neighborhood.ts +16 -3
- package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +3 -1
- package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/http-method.ts +19 -0
- package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +9 -3
- package/src/console/ui-next/src/features/flows/traces/trace-detail.test.ts +29 -2
- package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +10 -3
- package/src/console/ui-next/src/features/observability/lib/ask-count.test.ts +25 -0
- package/src/console/ui-next/src/features/observability/lib/ask-count.ts +4 -1
- package/src/console/ui-next/src/features/store/detail/browse-section.tsx +45 -1
- package/src/console/ui-next/src/features/store/detail/resource-panel.tsx +78 -16
- package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +155 -14
- package/src/console/ui-next/src/features/store/explorer/store-tree.tsx +33 -133
- package/src/console/ui-next/src/features/store/files/file-preview.tsx +4 -1
- package/src/console/ui-next/src/features/store/grid/rls-policy-sheet.tsx +326 -165
- package/src/console/ui-next/src/features/store/grid/sql-insert-sheet.tsx +6 -0
- package/src/console/ui-next/src/features/store/grid/store-data-grid.tsx +50 -3
- package/src/console/ui-next/src/features/store/lib/files-meta.test.ts +3 -0
- package/src/console/ui-next/src/features/store/lib/files-meta.ts +12 -0
- package/src/console/ui-next/src/features/store/lib/query-defaults.test.ts +25 -0
- package/src/console/ui-next/src/features/store/lib/query-defaults.ts +85 -3
- package/src/console/ui-next/src/features/store/lib/query-gate.ts +30 -4
- package/src/console/ui-next/src/features/store/lib/rls-policy.test.ts +139 -0
- package/src/console/ui-next/src/features/store/lib/rls-policy.ts +223 -21
- package/src/console/ui-next/src/features/store/lib/schema-graph.ts +4 -0
- package/src/console/ui-next/src/features/store/lib/sql-catalog.test.ts +24 -0
- package/src/console/ui-next/src/features/store/lib/sql-catalog.ts +19 -0
- package/src/console/ui-next/src/features/store/lib/store-tree.test.ts +6 -0
- package/src/console/ui-next/src/features/store/lib/store-tree.ts +13 -0
- package/src/console/ui-next/src/features/store/query/query-console.tsx +48 -14
- package/src/console/ui-next/src/features/store/query/query-gate-menu.tsx +19 -20
- package/src/console/ui-next/src/features/store/query/query-gate-parts.tsx +2 -1
- package/src/console/ui-next/src/features/store/query/query-results.tsx +110 -83
- package/src/console/ui-next/src/features/store/schema/schema-table-node.tsx +17 -0
- package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +112 -72
- package/src/console/ui-next/src/features/units/detail/effects-summary.tsx +12 -4
- package/src/console/ui-next/src/features/units/lib/contract-input.test.ts +206 -1
- package/src/console/ui-next/src/features/units/lib/contract-input.ts +55 -0
- package/src/console/ui-next/src/features/vault/lib/types.ts +1 -1
- package/src/docker/compose-health.test.ts +27 -0
- package/src/docker/compose-health.ts +19 -3
- package/src/docker/docker.test.ts +4 -1
- package/src/docker/dockerfile.ts +1 -1
- package/src/docker/recipes/pgdog.ts +2 -0
- package/src/drivers/ai-anthropic.ts +5 -0
- package/src/drivers/ai-ollama.ts +49 -30
- package/src/drivers/ai-openai-compatible.ts +57 -46
- package/src/drivers/ai-providers.test.ts +3 -0
- package/src/drivers/bun-native-completeness.test.ts +7 -9
- package/src/drivers/clock-postgres.ts +13 -9
- package/src/drivers/instances-postgres.ts +4 -6
- package/src/drivers/journal-postgres.ts +13 -9
- package/src/drivers/pg-rls.test.ts +87 -0
- package/src/drivers/pg-rls.ts +233 -7
- package/src/drivers/pglite.ts +26 -3
- package/src/drivers/postgres.test.ts +23 -0
- package/src/drivers/postgres.ts +133 -9
- package/src/drivers/redis.ts +11 -4
- package/src/drivers/signal-redis.ts +24 -14
- package/src/drivers/signal-types.ts +2 -1
- package/src/drivers/types.ts +10 -0
- package/src/elements/ai/declare.ts +109 -0
- package/src/elements/ai/errors.test.ts +5 -1
- package/src/elements/ai/errors.ts +30 -2
- package/src/elements/ai/eval.ts +4 -6
- package/src/elements/ai/mcp-client.test.ts +206 -0
- package/src/elements/ai/mcp-client.ts +362 -0
- package/src/elements/ai/mcp-http.ts +159 -0
- package/src/elements/ai/mcp-mock.ts +134 -0
- package/src/elements/ai/mcp-protocol.ts +234 -0
- package/src/elements/ai/mcp-stdio.test.ts +50 -0
- package/src/elements/ai/mcp-stdio.ts +212 -0
- package/src/elements/ai/mcp-transport.ts +70 -0
- package/src/elements/ai/runtime.ts +159 -29
- package/src/elements/ai.test.ts +139 -0
- package/src/elements/ai.ts +16 -0
- package/src/elements/clock/declare.ts +26 -1
- package/src/elements/clock/health.test.ts +43 -0
- package/src/elements/clock/runtime.ts +55 -2
- package/src/elements/clock/schedule.ts +71 -142
- package/src/elements/clock.test.ts +9 -40
- package/src/elements/clock.ts +6 -1
- package/src/elements/gate/boot.ts +5 -5
- package/src/elements/gate/declare.ts +36 -4
- package/src/elements/gate/flatten.ts +1 -1
- package/src/elements/gate/runtime.ts +1 -1
- package/src/elements/index.ts +2 -0
- package/src/elements/signal/declare.ts +2 -1
- package/src/elements/signal/runtime.ts +16 -1
- package/src/elements/signal.ts +1 -0
- package/src/elements/store/cache.test.ts +2 -0
- package/src/elements/store/cache.ts +3 -3
- package/src/elements/store/declare.ts +9 -6
- package/src/elements/store/emit-drizzle.ts +44 -8
- package/src/elements/store/resource.test.ts +1 -1
- package/src/elements/store/rls-identity.test.ts +95 -0
- package/src/elements/store/rls-identity.ts +116 -0
- package/src/elements/store/runtime.ts +4 -0
- package/src/elements/store/schema-decl.test.ts +37 -0
- package/src/elements/store/schema-decl.ts +161 -0
- package/src/elements/store/seed.ts +1 -1
- package/src/elements/store/sql-condition.test.ts +12 -0
- package/src/elements/store/sql-condition.ts +26 -2
- package/src/elements/store/sql-rls-isolation.test.ts +165 -0
- package/src/elements/store/sql-rls-stamp.test.ts +117 -0
- package/src/elements/store/sql-session.ts +81 -3
- package/src/elements/store/upsert-app.test.ts +1 -1
- package/src/elements/store.ts +14 -0
- package/src/elements/vault/builtin-adapter.ts +17 -0
- package/src/full.ts +2 -0
- package/src/http.ts +20 -3
- package/src/index.ts +14 -1
- package/src/kernel/adopt-barrel-fresh.test.ts +1 -1
- package/src/kernel/adopt-routes.ts +30 -2
- package/src/kernel/app.ts +227 -77
- package/src/kernel/auto-cache.test.ts +8 -8
- package/src/kernel/auto-registry.test.ts +54 -9
- package/src/kernel/boot-bind/ai.ts +24 -0
- package/src/kernel/boot-bind/clock.ts +2 -0
- package/src/kernel/boot.test.ts +4 -4
- package/src/kernel/boot.ts +5 -3
- package/src/kernel/effects-stamping.test.ts +3 -3
- package/src/kernel/element-registries.ts +15 -5
- package/src/kernel/errors.ts +24 -0
- package/src/kernel/flow-units.ts +64 -0
- package/src/kernel/flow.ts +29 -17
- package/src/kernel/fx-dead-letters.test.ts +77 -0
- package/src/kernel/fx.test.ts +22 -0
- package/src/kernel/fx.ts +135 -6
- package/src/kernel/horizontal-child.ts +4 -4
- package/src/kernel/http-path-pending.ts +20 -0
- package/src/kernel/http-resource.ts +4 -0
- package/src/kernel/http-stream.test.ts +174 -0
- package/src/kernel/index.ts +2 -0
- package/src/kernel/pipeline.test.ts +2 -2
- package/src/kernel/plugin-elements.test.ts +1 -1
- package/src/kernel/plugin-needs.test.ts +1 -1
- package/src/kernel/ready.test.ts +2 -2
- package/src/kernel/resource-mount.test.ts +14 -0
- package/src/kernel/run-duration.test.ts +2 -2
- package/src/kernel/stamp-http.test.ts +79 -0
- package/src/kernel/stamp-http.ts +75 -0
- package/src/kernel/triggers.ts +52 -27
- package/src/manifest/mcp-ref.ts +88 -0
- package/src/manifest/types.ts +41 -2
- package/src/manifest/validate.test.ts +20 -0
- package/src/mcp/docs-server.ts +1 -1
- package/src/mcp/server.ts +1 -1
- package/src/plugins/compression.test.ts +21 -0
- package/src/plugins/compression.ts +1 -0
- package/src/release/measure.ts +2 -2
- package/src/runtime/bun.ts +41 -4
- package/src/runtime/json-code-block.test.ts +2 -2
- package/src/runtime/serve.test.ts +4 -4
- package/src/term.test.ts +52 -4
- package/src/term.ts +125 -17
- package/src/test/reset-element-registries.ts +10 -3
- package/src/cli/ask-seed.test.ts +0 -96
- package/src/cli/ask-seed.ts +0 -82
- package/src/console/ui-next/dist/assets/cache-glyph-CLPBqZeb.js +0 -1
- package/src/console/ui-next/dist/assets/collapsible-DYb0xU8C.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-Dg8CTE29.js +0 -1
- package/src/console/ui-next/dist/assets/http-method-BJ92Z_ke.js +0 -1
- package/src/console/ui-next/dist/assets/index-Bp-R7jtM.js +0 -66
- package/src/console/ui-next/dist/assets/index-D4Ldtj79.css +0 -2
- package/src/console/ui-next/dist/assets/observability-page-DAnpEaq1.js +0 -4
- package/src/console/ui-next/dist/assets/query-gate-parts-1m8m1iNp.js +0 -2
- package/src/console/ui-next/dist/assets/react-B1ML8gxg.js +0 -1
- package/src/console/ui-next/dist/assets/replica-lag-C8_BRt2x.js +0 -18
- package/src/console/ui-next/dist/assets/sheet-form-D-ata7jy.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-DO4IsVqv.js +0 -1
- package/src/console/ui-next/dist/assets/skeleton-CL_X0GCj.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-v3LXdYpr.js +0 -45
- package/src/console/ui-next/dist/assets/trace-detail-sheet-D16lWQMt.js +0 -2
- package/src/console/ui-next/dist/assets/units-page-BtQ0bqMe.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-Ca-MvcmJ.js +0 -2
|
@@ -11,6 +11,7 @@ import type { ClockDecl } from "./declare.ts";
|
|
|
11
11
|
import { detectDstAmbiguity } from "./dst.ts";
|
|
12
12
|
import { parseDurationMs } from "./duration.ts";
|
|
13
13
|
import { tryAcquireLease } from "./leader.ts";
|
|
14
|
+
import { nextCronFireAt } from "./schedule.ts";
|
|
14
15
|
import {
|
|
15
16
|
createMemoryCronStore,
|
|
16
17
|
effectiveSchedule,
|
|
@@ -98,6 +99,13 @@ export interface ClockRuntime {
|
|
|
98
99
|
* @param name - Cron name
|
|
99
100
|
*/
|
|
100
101
|
dstAmbiguity(name: string): Promise<ReturnType<typeof detectDstAmbiguity>>;
|
|
102
|
+
/**
|
|
103
|
+
* Register in-process {@link Bun.cron} wakes for declared cron expressions.
|
|
104
|
+
* No-op on a frozen / time-travel runtime (tests drive {@link tick}).
|
|
105
|
+
*/
|
|
106
|
+
startWakes(): void;
|
|
107
|
+
/** Stop every in-process {@link Bun.cron} wake registered by {@link startWakes}. */
|
|
108
|
+
stopWakes(): void;
|
|
101
109
|
}
|
|
102
110
|
|
|
103
111
|
/**
|
|
@@ -115,6 +123,8 @@ export function createClockRuntime(options: CreateClockRuntimeOptions = {}): Clo
|
|
|
115
123
|
const handlers = new Map<string, CronHandler>();
|
|
116
124
|
/** The 1s boot scheduler must not stack ticks while a handler is still running. */
|
|
117
125
|
let ticking = false;
|
|
126
|
+
/** In-process {@link Bun.cron} jobs — precise wake; {@link tick} still owns the lease. */
|
|
127
|
+
const wakes: Array<{ stop(): void }> = [];
|
|
118
128
|
const driverId: ClockRuntime["driverId"] = timeTravel
|
|
119
129
|
? "frozen"
|
|
120
130
|
: store.kind === "file"
|
|
@@ -154,11 +164,18 @@ export function createClockRuntime(options: CreateClockRuntimeOptions = {}): Clo
|
|
|
154
164
|
|
|
155
165
|
// Claim the slot before the handler so a stacked 1s tick cannot re-enter.
|
|
156
166
|
const ranAt = now();
|
|
157
|
-
const
|
|
167
|
+
const sched = effectiveSchedule(fresh);
|
|
168
|
+
let nextRunAt = fresh.nextRunAt;
|
|
169
|
+
if (sched.every) {
|
|
170
|
+
const interval = parseDurationMs(sched.every);
|
|
171
|
+
if (interval > 0) nextRunAt = ranAt + interval;
|
|
172
|
+
} else if (sched.cron) {
|
|
173
|
+
nextRunAt = nextCronFireAt(sched.cron, ranAt, fresh.timezone) ?? fresh.nextRunAt;
|
|
174
|
+
}
|
|
158
175
|
await store.put({
|
|
159
176
|
...fresh,
|
|
160
177
|
lastRunAt: ranAt,
|
|
161
|
-
nextRunAt
|
|
178
|
+
nextRunAt,
|
|
162
179
|
});
|
|
163
180
|
|
|
164
181
|
const handler = handlers.get(name);
|
|
@@ -250,6 +267,42 @@ export function createClockRuntime(options: CreateClockRuntimeOptions = {}): Clo
|
|
|
250
267
|
if (!row?.effectiveCron) return null;
|
|
251
268
|
return detectDstAmbiguity(row.effectiveCron, row.timezone, now());
|
|
252
269
|
},
|
|
270
|
+
startWakes() {
|
|
271
|
+
runtime.stopWakes();
|
|
272
|
+
if (timeTravel) return;
|
|
273
|
+
for (const decl of declarations.values()) {
|
|
274
|
+
if (!decl.cron) continue;
|
|
275
|
+
try {
|
|
276
|
+
const job = (
|
|
277
|
+
Bun.cron as (
|
|
278
|
+
schedule: string,
|
|
279
|
+
handler: () => unknown,
|
|
280
|
+
options?: { readonly tz?: string },
|
|
281
|
+
) => { stop(): void; unref(): void }
|
|
282
|
+
)(
|
|
283
|
+
decl.cron,
|
|
284
|
+
() => {
|
|
285
|
+
void runtime.tick();
|
|
286
|
+
},
|
|
287
|
+
{ tz: decl.timezone },
|
|
288
|
+
);
|
|
289
|
+
job.unref();
|
|
290
|
+
wakes.push(job);
|
|
291
|
+
} catch {
|
|
292
|
+
/* invalid expression — 1s tick remains the fallback */
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
stopWakes() {
|
|
297
|
+
for (const job of wakes) {
|
|
298
|
+
try {
|
|
299
|
+
job.stop();
|
|
300
|
+
} catch {
|
|
301
|
+
/* already stopped */
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
wakes.length = 0;
|
|
305
|
+
},
|
|
253
306
|
};
|
|
254
307
|
|
|
255
308
|
return runtime;
|
|
@@ -1,13 +1,58 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Next-occurrence helpers for cron / every schedules (console §9.6 timeline).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Cron math uses {@link Bun.cron.parse} (5-field + nicknames + `{ tz }`).
|
|
5
|
+
* Interval (`every`) stays on {@link parseDurationMs}.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { parseDurationMs } from "./duration.ts";
|
|
9
9
|
import { effectiveSchedule, type CronRow } from "./reconcile.ts";
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Bun 1.4 `{ tz }` parse — `@types/bun` 1.3.14 only declares two arguments.
|
|
13
|
+
*
|
|
14
|
+
* @param expression - 5-field expression or nickname
|
|
15
|
+
* @param relativeMs - Exclusive start
|
|
16
|
+
* @param timezone - IANA zone
|
|
17
|
+
*/
|
|
18
|
+
function parseCronNext(expression: string, relativeMs: number, timezone: string): Date | null {
|
|
19
|
+
return (
|
|
20
|
+
Bun.cron.parse as (
|
|
21
|
+
expression: string,
|
|
22
|
+
relativeDate?: Date | number,
|
|
23
|
+
options?: { readonly tz?: string },
|
|
24
|
+
) => Date | null
|
|
25
|
+
)(expression, relativeMs, { tz: timezone });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Cap walk loops so a `* * * * *` range cannot hang the Console timeline. */
|
|
29
|
+
const MAX_CRON_FIRES = 10_000;
|
|
30
|
+
/** How far back {@link previousOccurrence} searches for a prior cron fire. */
|
|
31
|
+
const PREVIOUS_LOOKBACK_MS = 400 * 86_400_000;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Next matching UTC epoch-ms after `relativeMs`, or `undefined` when the
|
|
35
|
+
* expression is invalid or has no match within eight years.
|
|
36
|
+
*
|
|
37
|
+
* {@link Bun.cron.parse} is exclusive of `relativeMs`.
|
|
38
|
+
*
|
|
39
|
+
* @param cron - 5-field expression or nickname (`@hourly`)
|
|
40
|
+
* @param relativeMs - Search starts after this instant
|
|
41
|
+
* @param timezone - IANA zone passed as `{ tz }`
|
|
42
|
+
*/
|
|
43
|
+
export function nextCronFireAt(
|
|
44
|
+
cron: string,
|
|
45
|
+
relativeMs: number,
|
|
46
|
+
timezone: string,
|
|
47
|
+
): number | undefined {
|
|
48
|
+
try {
|
|
49
|
+
const next = parseCronNext(cron, relativeMs, timezone);
|
|
50
|
+
return next === null ? undefined : next.getTime();
|
|
51
|
+
} catch {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
11
56
|
/**
|
|
12
57
|
* Compute fire times in `[from, until)` for a reconciled cron row.
|
|
13
58
|
*
|
|
@@ -24,7 +69,7 @@ export function nextOccurrences(row: CronRow, from: number, until: number): read
|
|
|
24
69
|
return nextEveryOccurrences(row, sched.every, from, until);
|
|
25
70
|
}
|
|
26
71
|
if (sched.cron) {
|
|
27
|
-
return nextCronOccurrences(
|
|
72
|
+
return nextCronOccurrences(sched.cron, row.timezone, from, until);
|
|
28
73
|
}
|
|
29
74
|
return [];
|
|
30
75
|
}
|
|
@@ -41,25 +86,12 @@ export function previousOccurrence(row: CronRow, at: number): number | null {
|
|
|
41
86
|
const interval = parseDurationMs(sched.every);
|
|
42
87
|
if (interval <= 0) return null;
|
|
43
88
|
if (row.lastRunAt === undefined) return null;
|
|
44
|
-
// Floor to the slot boundary at or before `at`.
|
|
45
89
|
const origin = row.lastRunAt % interval;
|
|
46
90
|
const slot = at - ((at - origin) % interval);
|
|
47
91
|
return slot > 0 ? slot : null;
|
|
48
92
|
}
|
|
49
93
|
if (sched.cron) {
|
|
50
|
-
|
|
51
|
-
if (!parsed) return null;
|
|
52
|
-
// Walk back up to 400 days for a prior civil fire.
|
|
53
|
-
for (let day = 1; day <= 400; day++) {
|
|
54
|
-
const probe = at - day * 86_400_000;
|
|
55
|
-
const local = zonedParts(probe, sched.timezone);
|
|
56
|
-
if (local.hour === parsed.hour && local.minute === parsed.minute) {
|
|
57
|
-
// Align to that local wall time's UTC instant near `probe`.
|
|
58
|
-
const fires = civilFiresOnDay(local.date, parsed, sched.timezone);
|
|
59
|
-
const prior = fires.filter((t) => t <= at).pop();
|
|
60
|
-
if (prior !== undefined) return prior;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
94
|
+
return previousCronFire(sched.cron, row.timezone, at);
|
|
63
95
|
}
|
|
64
96
|
return null;
|
|
65
97
|
}
|
|
@@ -77,11 +109,10 @@ export function countMissedOccurrences(row: CronRow, from: number, until: number
|
|
|
77
109
|
if (sched.every) {
|
|
78
110
|
const interval = parseDurationMs(sched.every);
|
|
79
111
|
if (interval <= 0) return 0;
|
|
80
|
-
return Math.max(0, Math.floor((until - from) / interval)
|
|
112
|
+
return Math.max(0, Math.floor((until - from) / interval));
|
|
81
113
|
}
|
|
82
114
|
if (sched.cron) {
|
|
83
|
-
|
|
84
|
-
const fires = nextCronOccurrences(row, sched.cron, sched.timezone, from + 1, until + 1);
|
|
115
|
+
const fires = nextCronOccurrences(sched.cron, row.timezone, from + 1, until + 1);
|
|
85
116
|
return fires.filter((t) => t > from && t <= until).length;
|
|
86
117
|
}
|
|
87
118
|
return 0;
|
|
@@ -102,145 +133,43 @@ function nextEveryOccurrences(
|
|
|
102
133
|
: row.lastRunAt !== undefined
|
|
103
134
|
? row.lastRunAt + interval
|
|
104
135
|
: from;
|
|
105
|
-
// If still in the past relative to `from`, advance to first >= from.
|
|
106
136
|
while (t < from) t += interval;
|
|
107
137
|
while (t < until) {
|
|
108
138
|
out.push(t);
|
|
109
139
|
t += interval;
|
|
110
|
-
if (out.length >
|
|
140
|
+
if (out.length > MAX_CRON_FIRES) break;
|
|
111
141
|
}
|
|
112
142
|
return out;
|
|
113
143
|
}
|
|
114
144
|
|
|
115
145
|
function nextCronOccurrences(
|
|
116
|
-
row: CronRow,
|
|
117
146
|
cron: string,
|
|
118
147
|
timezone: string,
|
|
119
148
|
from: number,
|
|
120
149
|
until: number,
|
|
121
150
|
): readonly number[] {
|
|
122
|
-
const parsed = parseSimpleDaily(cron);
|
|
123
|
-
if (!parsed) {
|
|
124
|
-
if (row.nextRunAt !== undefined && row.nextRunAt >= from && row.nextRunAt < until) {
|
|
125
|
-
return [row.nextRunAt];
|
|
126
|
-
}
|
|
127
|
-
return [];
|
|
128
|
-
}
|
|
129
|
-
|
|
130
151
|
const out: number[] = [];
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
0,
|
|
139
|
-
0,
|
|
140
|
-
);
|
|
141
|
-
if (probe > until + 86_400_000) break;
|
|
142
|
-
const local = zonedParts(probe, timezone);
|
|
143
|
-
const fires = civilFiresOnDay(local.date, parsed, timezone);
|
|
144
|
-
for (const t of fires) {
|
|
145
|
-
if (t >= from && t < until) out.push(t);
|
|
146
|
-
}
|
|
147
|
-
if (out.length > 10_000) break;
|
|
152
|
+
// parse is exclusive of the cursor — step back 1ms so `from` itself can match.
|
|
153
|
+
let cursor = from - 1;
|
|
154
|
+
while (out.length < MAX_CRON_FIRES) {
|
|
155
|
+
const t = nextCronFireAt(cron, cursor, timezone);
|
|
156
|
+
if (t === undefined || t >= until) break;
|
|
157
|
+
if (t >= from) out.push(t);
|
|
158
|
+
cursor = t;
|
|
148
159
|
}
|
|
149
|
-
return out
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/** Parsed simple daily cron. */
|
|
153
|
-
interface SimpleDaily {
|
|
154
|
-
readonly minute: number;
|
|
155
|
-
readonly hour: number;
|
|
156
|
-
readonly dow?: number;
|
|
160
|
+
return out;
|
|
157
161
|
}
|
|
158
162
|
|
|
159
|
-
function
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const m = Number(minute);
|
|
170
|
-
const h = Number(hour);
|
|
171
|
-
if (m < 0 || m > 59 || h < 0 || h > 23) return null;
|
|
172
|
-
if (dow === "*") return { minute: m, hour: h };
|
|
173
|
-
if (/^\d+$/.test(dow)) {
|
|
174
|
-
const d = Number(dow);
|
|
175
|
-
if (d < 0 || d > 6) return null;
|
|
176
|
-
return { minute: m, hour: h, dow: d };
|
|
163
|
+
function previousCronFire(cron: string, timezone: string, at: number): number | null {
|
|
164
|
+
let cursor = at - PREVIOUS_LOOKBACK_MS;
|
|
165
|
+
let prev: number | null = null;
|
|
166
|
+
let steps = 0;
|
|
167
|
+
while (steps < MAX_CRON_FIRES) {
|
|
168
|
+
const t = nextCronFireAt(cron, cursor, timezone);
|
|
169
|
+
if (t === undefined || t >= at) break;
|
|
170
|
+
prev = t;
|
|
171
|
+
cursor = t;
|
|
172
|
+
steps++;
|
|
177
173
|
}
|
|
178
|
-
return
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function civilFiresOnDay(
|
|
182
|
-
dateStr: string,
|
|
183
|
-
parsed: SimpleDaily,
|
|
184
|
-
timezone: string,
|
|
185
|
-
): readonly number[] {
|
|
186
|
-
const matches: number[] = [];
|
|
187
|
-
const base = Date.parse(`${dateStr}T00:00:00.000Z`);
|
|
188
|
-
if (Number.isNaN(base)) return [];
|
|
189
|
-
|
|
190
|
-
for (let offsetMin = -14 * 60; offsetMin <= 14 * 60; offsetMin += 15) {
|
|
191
|
-
const utc = base + parsed.hour * 3_600_000 + parsed.minute * 60_000 - offsetMin * 60_000;
|
|
192
|
-
const local = zonedParts(utc, timezone);
|
|
193
|
-
if (local.date === dateStr && local.hour === parsed.hour && local.minute === parsed.minute) {
|
|
194
|
-
if (parsed.dow !== undefined) {
|
|
195
|
-
const localDow = zonedDow(utc, timezone);
|
|
196
|
-
if (localDow !== parsed.dow) continue;
|
|
197
|
-
}
|
|
198
|
-
if (!matches.includes(utc)) matches.push(utc);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
return matches;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
interface ZonedParts {
|
|
205
|
-
readonly date: string;
|
|
206
|
-
readonly hour: number;
|
|
207
|
-
readonly minute: number;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function zonedParts(utcMs: number, timezone: string): ZonedParts {
|
|
211
|
-
const fmt = new Intl.DateTimeFormat("en-US", {
|
|
212
|
-
timeZone: timezone,
|
|
213
|
-
year: "numeric",
|
|
214
|
-
month: "2-digit",
|
|
215
|
-
day: "2-digit",
|
|
216
|
-
hour: "2-digit",
|
|
217
|
-
minute: "2-digit",
|
|
218
|
-
hourCycle: "h23",
|
|
219
|
-
});
|
|
220
|
-
const parts = fmt.formatToParts(new Date(utcMs));
|
|
221
|
-
const get = (type: Intl.DateTimeFormatPartTypes): string =>
|
|
222
|
-
parts.find((p) => p.type === type)?.value ?? "0";
|
|
223
|
-
return {
|
|
224
|
-
date: `${get("year")}-${get("month")}-${get("day")}`,
|
|
225
|
-
hour: Number(get("hour")),
|
|
226
|
-
minute: Number(get("minute")),
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function zonedDow(utcMs: number, timezone: string): number {
|
|
231
|
-
const fmt = new Intl.DateTimeFormat("en-US", {
|
|
232
|
-
timeZone: timezone,
|
|
233
|
-
weekday: "short",
|
|
234
|
-
});
|
|
235
|
-
const w = fmt.format(new Date(utcMs));
|
|
236
|
-
const map: Record<string, number> = {
|
|
237
|
-
Sun: 0,
|
|
238
|
-
Mon: 1,
|
|
239
|
-
Tue: 2,
|
|
240
|
-
Wed: 3,
|
|
241
|
-
Thu: 4,
|
|
242
|
-
Fri: 5,
|
|
243
|
-
Sat: 6,
|
|
244
|
-
};
|
|
245
|
-
return map[w] ?? 0;
|
|
174
|
+
return prev;
|
|
246
175
|
}
|
|
@@ -466,8 +466,9 @@ describe("DST ambiguity", () => {
|
|
|
466
466
|
expect(doctorSrc).not.toMatch(/detectDstAmbiguity/);
|
|
467
467
|
});
|
|
468
468
|
|
|
469
|
-
test("overlap day:
|
|
470
|
-
// US fall-back 2026-11-01: 01:30 local occurs twice
|
|
469
|
+
test("overlap day: crontab fires once; next slot is the following day", async () => {
|
|
470
|
+
// US fall-back 2026-11-01: 01:30 local occurs twice. Bun.cron.parse
|
|
471
|
+
// matches crontab — a fixed wall time fires at the first occurrence only.
|
|
471
472
|
const from = Date.UTC(2026, 10, 1, 0, 0, 0);
|
|
472
473
|
const until = Date.UTC(2026, 10, 2, 0, 0, 0);
|
|
473
474
|
const fires = nextOccurrences(
|
|
@@ -481,13 +482,11 @@ describe("DST ambiguity", () => {
|
|
|
481
482
|
from,
|
|
482
483
|
until,
|
|
483
484
|
);
|
|
484
|
-
expect(fires
|
|
485
|
-
expect(fires[
|
|
485
|
+
expect(fires).toHaveLength(1);
|
|
486
|
+
expect(fires[0]).toBe(Date.UTC(2026, 10, 1, 5, 30, 0));
|
|
486
487
|
|
|
487
|
-
// Stub isDue (nextRunAt unset after first fire): does not walk civil fires →
|
|
488
|
-
// advancing across the overlap gap does not double-fire.
|
|
489
488
|
const store = createMemoryCronStore();
|
|
490
|
-
const rt = createTestClockRuntime(0, {
|
|
489
|
+
const rt = createTestClockRuntime(fires[0]! - 1, {
|
|
491
490
|
store,
|
|
492
491
|
instanceId: "dst-fire",
|
|
493
492
|
leaseMs: 1_000,
|
|
@@ -503,43 +502,13 @@ describe("DST ambiguity", () => {
|
|
|
503
502
|
rt.onCron("ambig", () => {
|
|
504
503
|
ran.push(rt.now());
|
|
505
504
|
});
|
|
505
|
+
rt.advance(2);
|
|
506
506
|
expect((await rt.tick()).ran).toEqual(["ambig"]);
|
|
507
507
|
rt.advance(3_600_000);
|
|
508
508
|
expect((await rt.tick()).ran).toEqual([]);
|
|
509
509
|
expect(ran).toHaveLength(1);
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
// instant, a second fire CAN occur. Leader lease is not a DST policy.
|
|
513
|
-
const store2 = createMemoryCronStore();
|
|
514
|
-
const rt2 = createTestClockRuntime(fires[0]! - 1, {
|
|
515
|
-
store: store2,
|
|
516
|
-
instanceId: "dst-risk",
|
|
517
|
-
leaseMs: 1_000,
|
|
518
|
-
});
|
|
519
|
-
rt2.register(
|
|
520
|
-
clock("ambig2", {
|
|
521
|
-
cron: "30 1 * * *",
|
|
522
|
-
timezone: "America/New_York",
|
|
523
|
-
}),
|
|
524
|
-
);
|
|
525
|
-
await rt2.reconcile();
|
|
526
|
-
await store2.put({
|
|
527
|
-
...(await store2.get("ambig2"))!,
|
|
528
|
-
nextRunAt: fires[0],
|
|
529
|
-
});
|
|
530
|
-
const ran2: number[] = [];
|
|
531
|
-
rt2.onCron("ambig2", () => {
|
|
532
|
-
ran2.push(rt2.now());
|
|
533
|
-
});
|
|
534
|
-
rt2.advance(2);
|
|
535
|
-
expect((await rt2.tick()).ran).toEqual(["ambig2"]);
|
|
536
|
-
rt2.advance(fires[1]! - rt2.now());
|
|
537
|
-
await store2.put({
|
|
538
|
-
...(await store2.get("ambig2"))!,
|
|
539
|
-
nextRunAt: fires[1],
|
|
540
|
-
});
|
|
541
|
-
expect((await rt2.tick()).ran).toEqual(["ambig2"]);
|
|
542
|
-
expect(ran2).toHaveLength(2);
|
|
510
|
+
const after = await store.get("ambig");
|
|
511
|
+
expect(after?.nextRunAt).toBeGreaterThan(fires[0]! + 3_600_000);
|
|
543
512
|
});
|
|
544
513
|
});
|
|
545
514
|
|
package/src/elements/clock.ts
CHANGED
|
@@ -50,7 +50,12 @@ export { runDurable, type DurableResult, type RunDurableOptions } from "./clock/
|
|
|
50
50
|
|
|
51
51
|
export { cronHealth, type CatchUpPolicy, type CronHealth } from "./clock/health.ts";
|
|
52
52
|
|
|
53
|
-
export {
|
|
53
|
+
export {
|
|
54
|
+
nextOccurrences,
|
|
55
|
+
previousOccurrence,
|
|
56
|
+
countMissedOccurrences,
|
|
57
|
+
nextCronFireAt,
|
|
58
|
+
} from "./clock/schedule.ts";
|
|
54
59
|
|
|
55
60
|
export {
|
|
56
61
|
pauseCron,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Gate boot audit — every HTTP trigger must declare auth posture.
|
|
3
3
|
*
|
|
4
|
-
* Fail loud: missing gate and missing
|
|
4
|
+
* Fail loud: missing gate and missing `.public()` → {@link GateBootError}.
|
|
5
5
|
* `unguardedHttp: "allow"` is honoured **only** when `env === "test"` — never a
|
|
6
|
-
* production-wide bypass. Migrate real apps with per-trigger
|
|
6
|
+
* production-wide bypass. Migrate real apps with per-trigger `.public()`.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { GATE_PUBLIC_NAME } from "./declare.ts";
|
|
@@ -19,7 +19,7 @@ export interface GatePostureGap {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
* Thrown when HTTP triggers omit both a gate chain and
|
|
22
|
+
* Thrown when HTTP triggers omit both a gate chain and `.public()`.
|
|
23
23
|
* Lists every gap (Vault-style — fail once with the full set).
|
|
24
24
|
*/
|
|
25
25
|
export class GateBootError extends Error {
|
|
@@ -28,7 +28,7 @@ export class GateBootError extends Error {
|
|
|
28
28
|
constructor(gaps: readonly GatePostureGap[]) {
|
|
29
29
|
const lines = gaps.map((g) => ` - ${g.flowId} ${g.method} ${g.path}`);
|
|
30
30
|
super(
|
|
31
|
-
`gate boot failed — ${gaps.length} HTTP trigger(s) missing auth posture (attach a gate or
|
|
31
|
+
`gate boot failed — ${gaps.length} HTTP trigger(s) missing auth posture (attach a gate or .public()):\n${lines.join("\n")}`,
|
|
32
32
|
);
|
|
33
33
|
this.name = "GateBootError";
|
|
34
34
|
this.gaps = gaps;
|
|
@@ -84,7 +84,7 @@ export function hasHttpGatePosture(
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
|
-
* Collect HTTP triggers with neither a gate nor
|
|
87
|
+
* Collect HTTP triggers with neither a gate nor `.public()`.
|
|
88
88
|
*
|
|
89
89
|
* @param bindings - Adopted / registered bindings
|
|
90
90
|
*/
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { RateStrategy } from "../../manifest/types.ts";
|
|
8
|
+
import { gateRegistry } from "../../kernel/element-registries.ts";
|
|
8
9
|
import { DEFAULT_RATE_STRATEGY } from "./constants.ts";
|
|
9
10
|
import { GATE_PUBLIC_NAME, type GateAllDecl, type GateMember } from "./flatten.ts";
|
|
10
11
|
|
|
@@ -89,6 +90,29 @@ export interface RateGateDecl {
|
|
|
89
90
|
/** Declared gate handle. */
|
|
90
91
|
export type GateDecl = PolicyGateDecl | RateGateDecl;
|
|
91
92
|
|
|
93
|
+
/**
|
|
94
|
+
* `gate.policy` / `gate.scope` / `gate.rate` push into the shared
|
|
95
|
+
* {@link gateRegistry} (`src/kernel/element-registries.ts`) so {@link oke}
|
|
96
|
+
* can auto-populate `gate.policies` with zero explicit array — mirrors the
|
|
97
|
+
* {@link on} trigger-drain registry (`src/kernel/on.ts`). `gate.public` is
|
|
98
|
+
* built into the runtime; `gate.all` is a composition of already-registered
|
|
99
|
+
* members.
|
|
100
|
+
*
|
|
101
|
+
* Snapshot of every policy / rate declared since the last reset.
|
|
102
|
+
*/
|
|
103
|
+
export function listGates(): readonly GateDecl[] {
|
|
104
|
+
return gateRegistry.slice();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Clear the gate registry (tests / fresh app adopt).
|
|
109
|
+
*
|
|
110
|
+
* @internal
|
|
111
|
+
*/
|
|
112
|
+
export function resetGates(): void {
|
|
113
|
+
gateRegistry.length = 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
92
116
|
/**
|
|
93
117
|
* Shape of the {@link gate} element namespace.
|
|
94
118
|
*/
|
|
@@ -151,9 +175,11 @@ export const gate: GateNamespace = {
|
|
|
151
175
|
);
|
|
152
176
|
}
|
|
153
177
|
if (typeof checkOrOptions === "function") {
|
|
154
|
-
|
|
178
|
+
const decl: PolicyGateDecl = { kind: "policy", name, check: checkOrOptions };
|
|
179
|
+
gateRegistry.push(decl);
|
|
180
|
+
return decl;
|
|
155
181
|
}
|
|
156
|
-
|
|
182
|
+
const decl: PolicyGateDecl = {
|
|
157
183
|
kind: "policy",
|
|
158
184
|
name,
|
|
159
185
|
check: checkOrOptions.check,
|
|
@@ -161,6 +187,8 @@ export const gate: GateNamespace = {
|
|
|
161
187
|
? { description: checkOrOptions.description }
|
|
162
188
|
: {}),
|
|
163
189
|
};
|
|
190
|
+
gateRegistry.push(decl);
|
|
191
|
+
return decl;
|
|
164
192
|
},
|
|
165
193
|
|
|
166
194
|
/**
|
|
@@ -172,12 +200,14 @@ export const gate: GateNamespace = {
|
|
|
172
200
|
if (name === GATE_PUBLIC_NAME) {
|
|
173
201
|
throw new TypeError('gate.scope: name "public" is reserved — use gate.public');
|
|
174
202
|
}
|
|
175
|
-
|
|
203
|
+
const decl: PolicyGateDecl = {
|
|
176
204
|
kind: "policy",
|
|
177
205
|
name,
|
|
178
206
|
check: ({ auth }) => auth.scopes.has(name),
|
|
179
207
|
scopes: [name],
|
|
180
208
|
};
|
|
209
|
+
gateRegistry.push(decl);
|
|
210
|
+
return decl;
|
|
181
211
|
},
|
|
182
212
|
|
|
183
213
|
public: {
|
|
@@ -201,7 +231,7 @@ export const gate: GateNamespace = {
|
|
|
201
231
|
}
|
|
202
232
|
const strategy = options.strategy ?? DEFAULT_RATE_STRATEGY;
|
|
203
233
|
const name = `rate:${strategy}:${options.max}/${options.per}`;
|
|
204
|
-
|
|
234
|
+
const decl: RateGateDecl = {
|
|
205
235
|
kind: "rate",
|
|
206
236
|
name,
|
|
207
237
|
strategy,
|
|
@@ -211,6 +241,8 @@ export const gate: GateNamespace = {
|
|
|
211
241
|
overridable: options.overridable ?? false,
|
|
212
242
|
...(options.description !== undefined ? { description: options.description } : {}),
|
|
213
243
|
};
|
|
244
|
+
gateRegistry.push(decl);
|
|
245
|
+
return decl;
|
|
214
246
|
},
|
|
215
247
|
|
|
216
248
|
/**
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import type { GateDecl } from "./declare.ts";
|
|
9
9
|
|
|
10
|
-
/** Reserved policy name for `gate.public` / `.
|
|
10
|
+
/** Reserved policy name for `gate.public` / `.public()`. */
|
|
11
11
|
export const GATE_PUBLIC_NAME = "public";
|
|
12
12
|
|
|
13
13
|
/** Member of `gate.all` — a policy/rate or a nested `all` handle. */
|
|
@@ -70,7 +70,7 @@ export interface GateRuntime {
|
|
|
70
70
|
*/
|
|
71
71
|
export function createGateRuntime(options: CreateGateRuntimeOptions = {}): GateRuntime {
|
|
72
72
|
const map = new Map<string, GateDecl>();
|
|
73
|
-
// Built-in sentinel — `.
|
|
73
|
+
// Built-in sentinel — `.public()` works without listing it in `oke({ gates })`.
|
|
74
74
|
map.set(gate.public.name, gate.public);
|
|
75
75
|
for (const g of options.gates ?? []) {
|
|
76
76
|
if (isGateAllDecl(g)) {
|
package/src/elements/index.ts
CHANGED
|
@@ -34,7 +34,8 @@ export interface SignalOptions {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* Declared signal handle — usable as `on(signal, flow)
|
|
37
|
+
* Declared signal handle — usable as `on(signal, flow)`, `fx.emit(signal, …)`,
|
|
38
|
+
* and `fx.deadLetters(signal)`.
|
|
38
39
|
*/
|
|
39
40
|
export interface SignalDecl<T = unknown> {
|
|
40
41
|
/** Signal name (manifest key). */
|
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
* emit / subscribe / live / drain for `fx` and tests.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
DeadLetter,
|
|
8
|
+
SignalBus,
|
|
9
|
+
SignalDriver,
|
|
10
|
+
SignalEmitOptions,
|
|
11
|
+
} from "../../drivers/signal-types.ts";
|
|
7
12
|
import type { SignalDecl } from "./declare.ts";
|
|
8
13
|
|
|
9
14
|
/** Options for {@link createSignalRuntime}. */
|
|
@@ -48,6 +53,12 @@ export interface SignalRuntime {
|
|
|
48
53
|
* @param options - Optional emit options (`key` for per-key once ordering)
|
|
49
54
|
*/
|
|
50
55
|
emit(name: string, payload?: unknown, options?: SignalEmitOptions): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Dead-lettered messages for one signal (auto-starts).
|
|
58
|
+
*
|
|
59
|
+
* @param name - Signal name
|
|
60
|
+
*/
|
|
61
|
+
deadLetters(name: string): Promise<readonly DeadLetter[]>;
|
|
51
62
|
/** Close the bus. */
|
|
52
63
|
close(): Promise<void>;
|
|
53
64
|
}
|
|
@@ -87,6 +98,10 @@ export function createSignalRuntime(options: CreateSignalRuntimeOptions): Signal
|
|
|
87
98
|
const b = await this.start();
|
|
88
99
|
await b.emit(name, payload, options);
|
|
89
100
|
},
|
|
101
|
+
async deadLetters(name) {
|
|
102
|
+
const b = await this.start();
|
|
103
|
+
return b.deadLetters(name);
|
|
104
|
+
},
|
|
90
105
|
async close() {
|
|
91
106
|
if (bus) {
|
|
92
107
|
await bus.close();
|
package/src/elements/signal.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
export { signal } from "./signal/declare.ts";
|
|
10
10
|
export type { SignalDecl, SignalOptions } from "./signal/declare.ts";
|
|
11
|
+
export type { DeadLetter, SignalFailureReason } from "../drivers/signal-types.ts";
|
|
11
12
|
|
|
12
13
|
export { createSignalRuntime } from "./signal/runtime.ts";
|
|
13
14
|
export type { CreateSignalRuntimeOptions, SignalRuntime } from "./signal/runtime.ts";
|