okengine 0.18.4 → 0.19.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 +18 -10
- package/README.md +2 -2
- package/package.json +51 -50
- package/site/content/docs/ai/index.mdx +25 -6
- package/site/content/docs/ai/llms-txt.mdx +1 -1
- package/site/content/docs/ai/meta.json +1 -1
- package/site/content/docs/ai/skills.mdx +4 -1
- package/site/content/docs/ai/try-it.mdx +58 -0
- package/site/content/docs/client/auth.mdx +208 -0
- package/site/content/docs/client/calling.mdx +451 -0
- package/site/content/docs/client/index.mdx +209 -0
- package/site/content/docs/client/live.mdx +234 -0
- package/site/content/docs/client/meta.json +5 -0
- package/site/content/docs/client/react.mdx +249 -0
- package/site/content/docs/elements/ai/agents.mdx +232 -0
- package/site/content/docs/elements/ai/index.mdx +341 -0
- package/site/content/docs/elements/ai/mcp.mdx +275 -0
- package/site/content/docs/elements/ai/meta.json +5 -0
- package/site/content/docs/elements/ai/models.mdx +283 -0
- package/site/content/docs/elements/ai/prompts.mdx +256 -0
- package/site/content/docs/elements/channel/email.mdx +266 -0
- package/site/content/docs/elements/channel/index.mdx +344 -0
- package/site/content/docs/elements/channel/meta.json +5 -0
- package/site/content/docs/elements/channel/push.mdx +218 -0
- package/site/content/docs/elements/channel/receipts.mdx +206 -0
- package/site/content/docs/elements/channel/sms.mdx +264 -0
- package/site/content/docs/elements/channel/whatsapp.mdx +220 -0
- package/site/content/docs/elements/clock/index.mdx +300 -0
- package/site/content/docs/elements/clock/meta.json +5 -0
- package/site/content/docs/elements/clock/schedules.mdx +508 -0
- package/site/content/docs/elements/clock/sleep.mdx +445 -0
- package/site/content/docs/elements/flow/consumers.mdx +707 -0
- package/site/content/docs/elements/flow/http.mdx +1080 -0
- package/site/content/docs/elements/flow/index.mdx +618 -0
- package/site/content/docs/elements/flow/meta.json +5 -0
- package/site/content/docs/elements/flow/routing.mdx +559 -0
- package/site/content/docs/elements/flow/workflows.mdx +640 -0
- package/site/content/docs/elements/gate/auth.mdx +371 -0
- package/site/content/docs/elements/gate/authorization.mdx +298 -0
- package/site/content/docs/elements/gate/index.mdx +418 -0
- package/site/content/docs/elements/gate/meta.json +5 -0
- package/site/content/docs/elements/gate/rate-limits.mdx +365 -0
- package/site/content/docs/elements/gate/rls.mdx +303 -0
- package/site/content/docs/elements/gate/tenancy.mdx +372 -0
- package/site/content/docs/elements/index.mdx +52 -11
- package/site/content/docs/elements/signal/broadcast.mdx +517 -0
- package/site/content/docs/elements/signal/index.mdx +352 -0
- package/site/content/docs/elements/signal/live.mdx +590 -0
- package/site/content/docs/elements/signal/meta.json +5 -0
- package/site/content/docs/elements/signal/once.mdx +596 -0
- package/site/content/docs/elements/store/files.mdx +659 -0
- package/site/content/docs/elements/store/index.mdx +310 -0
- package/site/content/docs/elements/store/kv.mdx +629 -0
- package/site/content/docs/elements/store/meta.json +5 -0
- package/site/content/docs/elements/store/search.mdx +946 -0
- package/site/content/docs/elements/store/sql.mdx +937 -0
- package/site/content/docs/elements/vault/config.mdx +256 -0
- package/site/content/docs/elements/vault/index.mdx +356 -0
- package/site/content/docs/elements/vault/meta.json +5 -0
- package/site/content/docs/elements/vault/rotation.mdx +273 -0
- package/site/content/docs/elements/vault/secrets.mdx +285 -0
- package/site/content/docs/index.mdx +78 -30
- package/site/content/docs/meta.json +11 -8
- package/site/content/docs/plugins/cors.mdx +2 -1
- package/site/content/docs/plugins/csrf.mdx +4 -1
- package/site/content/docs/plugins/headers.mdx +2 -1
- package/site/content/docs/plugins/ip-allowlist.mdx +2 -1
- package/site/content/docs/plugins/magic-link.mdx +6 -0
- package/site/content/docs/plugins/maintenance-mode.mdx +2 -1
- package/site/content/docs/plugins/oauth.mdx +1 -1
- package/site/content/docs/plugins/otp.mdx +12 -1
- package/site/content/docs/plugins/passkey.mdx +39 -15
- package/site/content/docs/plugins/two-factor.mdx +91 -34
- package/site/content/docs/plugins/username.mdx +1 -1
- package/site/content/docs/recipes/caddy.mdx +3 -3
- package/site/content/docs/recipes/index.mdx +18 -28
- package/site/content/docs/recipes/mailpit.mdx +1 -1
- package/site/content/docs/recipes/meilisearch.mdx +3 -3
- package/site/content/docs/recipes/meta.json +1 -4
- package/site/content/docs/recipes/nginx.mdx +2 -2
- package/site/content/docs/recipes/openrouter.mdx +283 -0
- package/site/content/docs/recipes/pgdog.mdx +3 -3
- package/site/content/docs/recipes/postgres.mdx +1 -2
- package/site/content/docs/recipes/rustfs.mdx +2 -2
- package/site/content/docs/recipes/traefik.mdx +3 -3
- package/site/content/docs/reference/cli.mdx +231 -0
- package/site/content/docs/reference/configuration.mdx +30 -10
- package/site/content/docs/reference/environment-variables.mdx +93 -20
- package/site/content/docs/reference/errors.mdx +132 -29
- package/site/content/docs/reference/fx.mdx +137 -73
- package/site/content/docs/reference/index.mdx +13 -21
- package/site/content/docs/reference/meta.json +4 -5
- package/site/content/docs/reference/okid.mdx +42 -17
- package/site/content/docs/reference/plugins.mdx +4 -3
- package/site/content/docs/reference/security.mdx +197 -0
- package/site/content/docs/understand/meta.json +5 -0
- package/site/content/docs/understand/the-anatomy.mdx +132 -0
- package/site/content/docs/understand/the-model.mdx +32 -0
- package/site/content/docs/understand/the-problem.mdx +74 -0
- package/site/content/docs/understand/the-vocabulary.mdx +26 -0
- package/src/auth/api-keys.ts +2 -1
- package/src/auth/bindings.ts +64 -16
- package/src/auth/gate-auth.test.ts +6 -1
- package/src/auth/identity.ts +152 -5
- package/src/auth/index.ts +29 -0
- package/src/auth/invites.ts +2 -1
- package/src/auth/method-context.ts +41 -0
- package/src/auth/oauth-as/crypto.ts +2 -1
- package/src/auth/oauth-as/stores.ts +2 -1
- package/src/auth/operator.ts +2 -1
- package/src/auth/sessions-jwt.test.ts +77 -0
- package/src/auth/sessions.ts +20 -4
- package/src/auth/tenants.ts +3 -2
- package/src/auth/two-factor-challenge.test.ts +61 -0
- package/src/auth/two-factor-challenge.ts +284 -0
- package/src/auth/verification.ts +5 -0
- package/src/bench/REPORT.md +49 -0
- package/src/bench/g03-signal-once.bench.ts +1 -1
- package/src/bench/g10-observability-contention.bench.ts +1 -1
- package/src/bench/g17-hybrid-search.bench.ts +572 -0
- package/src/bench/load-app.ts +2 -10
- package/src/cli/ai-setup/ai-setup.test.ts +331 -42
- package/src/cli/ai-setup/apply.ts +294 -52
- package/src/cli/ai-setup/catalog.ts +238 -1342
- package/src/cli/ai-setup/index.ts +14 -99
- package/src/cli/ai-setup/prompts.ts +60 -443
- package/src/cli/ask-seed.test.ts +96 -0
- package/src/cli/ask-seed.ts +82 -0
- package/src/cli/ask-vault-gaps.test.ts +84 -0
- package/src/cli/ask-vault-gaps.ts +146 -0
- package/src/cli/build.ts +2 -2
- package/src/cli/client-add.test.ts +26 -1
- package/src/cli/client-add.ts +106 -27
- package/src/cli/competitor-mention-removal.test.ts +1 -1
- package/src/cli/db-seed.ts +2 -0
- package/src/cli/db.ts +150 -11
- package/src/cli/dev.ts +119 -211
- package/src/cli/docker-clean.ts +2 -2
- package/src/cli/docker-cli.test.ts +1 -1
- package/src/cli/doctor-diff.ts +4 -2
- package/src/cli/doctor-pii.test.ts +1 -1
- package/src/cli/doctor.ts +14 -1
- package/src/cli/hero-meta.test.ts +4 -2
- package/src/cli/load-config.images.test.ts +8 -10
- package/src/cli/load-config.ts +1 -1
- package/src/cli/registry.ts +22 -10
- package/src/cli/replay.ts +3 -1
- package/src/cli/start.ts +1 -1
- package/src/cli/tui/keys.ts +2 -3
- package/src/client/auth/cookies.ts +74 -0
- package/src/client/auth/create-auth-client.ts +636 -0
- package/src/client/auth/denials.ts +99 -0
- package/src/client/auth/session.ts +283 -0
- package/src/client/auth.test.ts +251 -0
- package/src/client/auth.ts +39 -114
- package/src/client/create-with-session.ts +245 -0
- package/src/client/create.ts +51 -5
- package/src/client/index.ts +11 -1
- package/src/client/live.ts +11 -79
- package/src/client/notes-contract.test.ts +5 -10
- package/src/client/sse.ts +137 -0
- package/src/client/stream.ts +147 -0
- package/src/client/transport.test.ts +32 -0
- package/src/client/transport.ts +123 -26
- package/src/client/types.ts +118 -14
- package/src/client-react/index.ts +185 -24
- package/src/client-react/use-live-query.ts +13 -3
- package/src/compiler/aot.test.ts +7 -4
- package/src/compiler/effects-embed.test.ts +56 -0
- package/src/compiler/effects-fetch.test.ts +43 -0
- package/src/compiler/effects-infer.ts +58 -2
- package/src/compiler/extract.test.ts +258 -44
- package/src/compiler/extract.ts +566 -48
- package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +7 -4
- package/src/compiler/fixtures/skyport/src/flows/bookings/signals.ts +2 -8
- package/src/compiler/fixtures/skyport.expected.json +4 -4
- package/src/compiler/fixtures/triggers/five-triggers.ts +14 -10
- package/src/compiler/response.ts +2 -2
- package/src/compiler/search-writer-isolation.test.ts +40 -0
- package/src/config/index.ts +11 -0
- package/src/console/server/app.ts +17 -23
- package/src/console/server/bind.ts +4 -0
- package/src/console/server/console.test.ts +2 -2
- package/src/console/server/flows-invoke.test.ts +50 -34
- package/src/console/server/flows.ts +436 -255
- package/src/console/server/runs-ingest.test.ts +6 -3
- package/src/console/server/serve.ts +1 -1
- package/src/console/server/signals.test.ts +1 -5
- package/src/console/server/signals.ts +8 -6
- package/src/console/server/state.ts +6 -1
- package/src/console/server/store.ts +1 -1
- package/src/console/ui-next/dist/assets/FileExportIcon-Ck-5od4R.js +1 -0
- package/src/console/ui-next/dist/assets/MoreHorizontalCircle01Icon-gMNGsE37.js +1 -0
- package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +1 -0
- package/src/console/ui-next/dist/assets/UnavailableIcon-D9cvHVPr.js +1 -0
- package/src/console/ui-next/dist/assets/UserIcon-DaE2PB5_.js +1 -0
- package/src/console/ui-next/dist/assets/access-page-DFeymU07.js +4 -0
- package/src/console/ui-next/dist/assets/agent-disclosure-U1rdfblp.js +1 -0
- package/src/console/ui-next/dist/assets/cache-glyph-BeFJeqBG.js +1 -0
- package/src/console/ui-next/dist/assets/call-pii-button-CVAONPii.js +1 -0
- package/src/console/ui-next/dist/assets/collapsible-D2A6NJ-3.js +1 -0
- package/src/console/ui-next/dist/assets/copy-inline-button-CAYD18cr.js +1 -0
- package/src/console/ui-next/dist/assets/dagre.esm-B1_XeuLP.js +1 -0
- package/src/console/ui-next/dist/assets/detail-header-DVWNjWjg.js +1 -0
- package/src/console/ui-next/dist/assets/dropdown-menu-4h2LOVXM.js +1 -0
- package/src/console/ui-next/dist/assets/duration-tone-Cgk_h5ja.js +9 -0
- package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-empty-CJs5A-wm.js +1 -0
- package/src/console/ui-next/dist/assets/flows-page-Dss7941e.js +1 -0
- package/src/console/ui-next/dist/assets/highlighted-json-MYZQtRnw.js +154 -0
- package/src/console/ui-next/dist/assets/http-method-Jrh39p7A.js +1 -0
- package/src/console/ui-next/dist/assets/index-D0zS5rKO.css +2 -0
- package/src/console/ui-next/dist/assets/index-DXP2dBIF.js +63 -0
- package/src/console/ui-next/dist/assets/observability-page-HvolXxTI.js +4 -0
- package/src/console/ui-next/dist/assets/react-dom-Ddte4I-Q.js +9 -0
- package/src/console/ui-next/dist/assets/replica-lag-CSh2dzrb.js +18 -0
- package/src/console/ui-next/dist/assets/request-meta-BatF8KrK.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +1 -0
- package/src/console/ui-next/dist/assets/{sql-BskegiFM.js → sql-BsFa4tDR.js} +1 -1
- package/src/console/ui-next/dist/assets/store-page-eiKiHnNe.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-CZkMeKS-.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-CW8y5A2h.js +55 -0
- package/src/console/ui-next/dist/assets/units-page-B_RWJrEO.js +1 -0
- package/src/console/ui-next/dist/assets/{use-vault-list-uk4WVboC.js → use-vault-list-CT4-gajj.js} +1 -1
- package/src/console/ui-next/dist/assets/vault-page-CWrg-A68.js +2 -0
- package/src/console/ui-next/dist/assets/xyflow-CSyC6ryz.css +1 -0
- package/src/console/ui-next/dist/assets/xyflow-yApv7D4e.js +7 -0
- package/src/console/ui-next/dist/index.html +6 -13
- package/src/console/ui-next/seed-invoke-host.ts +21 -18
- package/src/console/ui-next/src/client.ts +7 -2
- package/src/console/ui-next/src/features/flows/graph/element-map.ts +2 -0
- package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +12 -2
- package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +4 -0
- package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +8 -0
- package/src/console/ui-next/src/features/flows/traces/waterfall-bars.ts +4 -0
- package/src/console/ui-next/src/features/flows/traces/waterfall-tooltip.ts +9 -3
- package/src/console/ui-next/src/features/store/detail/reveal-cell.tsx +7 -11
- package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +2 -2
- package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +2 -0
- package/src/console/ui-next/src/features/store/lib/grid-model.test.ts +25 -1
- package/src/console/ui-next/src/features/store/lib/grid-model.ts +36 -1
- package/src/console/ui-next/src/features/store/query/query-results.tsx +3 -3
- package/src/console/ui-next/src/features/units/detail/effects-summary.tsx +2 -0
- package/src/console/ui-next/src/features/units/lib/call-read-safe.ts +3 -1
- package/src/console/ui-next/src/features/units/lib/fields-from-schema.ts +6 -0
- package/src/docker/ai-model-status.test.ts +1 -12
- package/src/docker/ai-model-status.ts +10 -38
- package/src/docker/compose-up.test.ts +100 -0
- package/src/docker/compose-up.ts +292 -0
- package/src/docker/compose.ts +4 -14
- package/src/docker/derive.ts +9 -34
- package/src/docker/docker.test.ts +16 -261
- package/src/docker/images-config.test.ts +13 -25
- package/src/docker/index.ts +1 -24
- package/src/docker/recipes/index.ts +0 -23
- package/src/docker/recipes/pgdog.ts +6 -4
- package/src/docker/stack-id.test.ts +2 -2
- package/src/docker/stack-id.ts +0 -1
- package/src/docker/types.ts +2 -2
- package/src/drivers/ai-anthropic.ts +10 -0
- package/src/drivers/ai-openai-compatible.ts +34 -7
- package/src/drivers/ai-providers.test.ts +0 -107
- package/src/drivers/ai-stream.test.ts +0 -34
- package/src/drivers/ai-types.ts +17 -8
- package/src/drivers/channel-fcm.ts +6 -1
- package/src/drivers/channel-msegat.ts +6 -1
- package/src/drivers/channel-resend.ts +5 -1
- package/src/drivers/channel-smtp.ts +5 -1
- package/src/drivers/channel-sndr.ts +10 -1
- package/src/drivers/channel-taqnyat-mail.ts +5 -1
- package/src/drivers/channel-taqnyat-whatsapp.ts +6 -1
- package/src/drivers/channel-taqnyat.ts +6 -1
- package/src/drivers/channel-types.ts +10 -0
- package/src/drivers/channel-unifonic.ts +6 -1
- package/src/drivers/channel-wa-cloud.ts +6 -1
- package/src/drivers/channel-webpush.ts +6 -1
- package/src/drivers/external.ts +33 -0
- package/src/drivers/index.ts +0 -10
- package/src/drivers/meilisearch.ts +6 -0
- package/src/drivers/oauth-types.ts +4 -0
- package/src/drivers/postgres.test.ts +74 -3
- package/src/drivers/postgres.ts +175 -6
- package/src/drivers/signal-types.ts +5 -5
- package/src/drivers/types.ts +7 -0
- package/src/drivers/vault-types.ts +5 -0
- package/src/elements/ai/declare.ts +17 -4
- package/src/elements/ai/eval.ts +1 -1
- package/src/elements/ai/mcp-http.ts +1 -1
- package/src/elements/ai/pii.ts +1 -1
- package/src/elements/ai/providers.test.ts +289 -0
- package/src/elements/ai/providers.ts +169 -0
- package/src/elements/ai/runtime.ts +68 -5
- package/src/elements/ai/schema.ts +2 -2
- package/src/elements/ai.test.ts +7 -7
- package/src/elements/ai.ts +17 -1
- package/src/elements/channel/runtime.ts +105 -12
- package/src/elements/clock/cron-fields.test.ts +144 -0
- package/src/elements/clock/cron-fields.ts +185 -0
- package/src/elements/clock/declare.ts +247 -3
- package/src/elements/clock.ts +21 -1
- package/src/elements/index.ts +15 -0
- package/src/elements/signal/chaos-child.ts +1 -2
- package/src/elements/signal/declare.ts +65 -37
- package/src/elements/signal/delivery-modes.test.ts +11 -31
- package/src/elements/signal/dry-run-replay.test.ts +1 -7
- package/src/elements/signal/dry-run-write-isolation.test.ts +1 -7
- package/src/elements/signal/key-ordering.test.ts +6 -26
- package/src/elements/signal/lease-reclaim.test.ts +2 -6
- package/src/elements/signal/optional-emit.test.ts +6 -9
- package/src/elements/signal/order-lifecycle.test.ts +4 -17
- package/src/elements/signal/orphan-messages.test.ts +4 -15
- package/src/elements/signal/reconcile.test.ts +2 -2
- package/src/elements/signal/runtime.ts +1 -1
- package/src/elements/signal/schema-emit.test.ts +4 -8
- package/src/elements/signal.test.ts +24 -49
- package/src/elements/signal.ts +9 -2
- package/src/elements/store/domain-ddl.test.ts +1 -1
- package/src/elements/store/live-http.test.ts +9 -4
- package/src/elements/store/prepare-row.test.ts +123 -0
- package/src/elements/store/resource.ts +43 -12
- package/src/elements/store/schema-decl.ts +117 -0
- package/src/elements/store/search-backfill.ts +205 -0
- package/src/elements/store/search-bind.ts +66 -0
- package/src/elements/store/search-bm25.ts +78 -0
- package/src/elements/store/search-ddl.ts +156 -0
- package/src/elements/store/search-embed-flow.ts +141 -0
- package/src/elements/store/search-errors.ts +34 -0
- package/src/elements/store/search-fusion.ts +112 -0
- package/src/elements/store/search-lsh.ts +179 -0
- package/src/elements/store/search-runtime.ts +300 -0
- package/src/elements/store/search.test.ts +144 -0
- package/src/elements/store/sql-session.test.ts +1 -0
- package/src/elements/store/sql-session.ts +83 -3
- package/src/elements/store/table.ts +23 -2
- package/src/elements/store/upsert-app.test.ts +1 -3
- package/src/elements/store.ts +50 -0
- package/src/full.ts +4 -2
- package/src/http.ts +4 -1
- package/src/i18n/catalogs/ar.ts +14 -6
- package/src/i18n/catalogs/en.ts +14 -6
- package/src/index.ts +6 -1
- package/src/kernel/adopt-barrel-fresh.test.ts +7 -7
- package/src/kernel/adopt-routes.ts +9 -1
- package/src/kernel/app-auth.ts +5 -0
- package/src/kernel/app.ts +102 -9
- package/src/kernel/auto-cache.test.ts +6 -12
- package/src/kernel/auto-registry.test.ts +3 -3
- package/src/kernel/boot-bind/ai.test.ts +22 -19
- package/src/kernel/boot-bind/ai.ts +5 -43
- package/src/kernel/boot-bind/clock.ts +23 -5
- package/src/kernel/boot-bind/honor-config.test.ts +4 -4
- package/src/kernel/boot.test.ts +11 -6
- package/src/kernel/boot.ts +66 -16
- package/src/kernel/boundary-contract.ts +93 -0
- package/src/kernel/budget.test.ts +1 -1
- package/src/kernel/call.ts +89 -0
- package/src/kernel/capability.ts +6 -0
- package/src/kernel/client-descriptor.ts +123 -0
- package/src/kernel/clock-timezone.test.ts +80 -0
- package/src/kernel/correlation.test.ts +1 -1
- package/src/kernel/dry-run.ts +11 -8
- package/src/kernel/effects-stamping.test.ts +42 -9
- package/src/kernel/effects.test.ts +4 -2
- package/src/kernel/effects.ts +51 -9
- package/src/kernel/errors-channel.ts +17 -0
- package/src/kernel/errors-live-resume.ts +3 -2
- package/src/kernel/errors-tenant.ts +7 -4
- package/src/kernel/errors.registry-helpers.ts +96 -0
- package/src/kernel/errors.registry.test.ts +139 -25
- package/src/kernel/errors.ts +102 -23
- package/src/kernel/external-effects.test.ts +196 -0
- package/src/kernel/flow.test.ts +13 -4
- package/src/kernel/flow.ts +69 -71
- package/src/kernel/fx-ask-telemetry.test.ts +2 -2
- package/src/kernel/fx-dead-letters.test.ts +3 -3
- package/src/kernel/fx-fetch.ts +55 -0
- package/src/kernel/fx-live-stream.ts +2 -2
- package/src/kernel/fx-live.test.ts +8 -8
- package/src/kernel/fx.test.ts +17 -11
- package/src/kernel/fx.ts +180 -57
- package/src/kernel/horizontal-child.ts +1 -1
- package/src/kernel/http-query.test.ts +3 -6
- package/src/kernel/index.ts +13 -3
- package/src/kernel/instance-id.ts +1 -1
- package/src/kernel/instances.test.ts +40 -0
- package/src/kernel/instances.ts +47 -9
- package/src/kernel/live-http.test.ts +6 -7
- package/src/kernel/live-http.ts +6 -2
- package/src/kernel/live-resume.test.ts +1 -1
- package/src/kernel/mcp-tool.test.ts +3 -3
- package/src/kernel/on.ts +98 -4
- package/src/kernel/pipeline.test.ts +12 -15
- package/src/kernel/plugin-elements.test.ts +1 -1
- package/src/kernel/stamp-http.test.ts +3 -3
- package/src/kernel/triggers.ts +210 -95
- package/src/kernel-entry.ts +0 -1
- package/src/manifest/diff.ts +11 -1
- package/src/manifest/fixtures/skyport.excerpt.json +1 -1
- package/src/manifest/types.ts +46 -2
- package/src/mcp/docs-index.ts +3 -3
- package/src/mcp/docs-mcp.test.ts +2 -2
- package/src/mcp/docs-tools.ts +1 -1
- package/src/okid.test.ts +56 -0
- package/src/okid.ts +55 -11
- package/src/plugins/anonymous.ts +8 -4
- package/src/plugins/auth/shared.ts +27 -3
- package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
- package/src/plugins/auth-methods.security.test.ts +561 -23
- package/src/plugins/config-source.ts +1 -1
- package/src/plugins/csrf.test.ts +65 -0
- package/src/plugins/index.ts +2 -0
- package/src/plugins/magic-link.ts +36 -16
- package/src/plugins/oauth/shared.ts +3 -1
- package/src/plugins/oauth.ts +22 -14
- package/src/plugins/otp.ts +37 -21
- package/src/plugins/passkey-webauthn.ts +9 -6
- package/src/plugins/passkey.ts +98 -26
- package/src/plugins/pre-account-hijack.test.ts +223 -0
- package/src/plugins/two-factor.ts +466 -37
- package/src/plugins/username.ts +19 -7
- package/src/release/build-lib.ts +1 -0
- package/src/release/limits.ts +2 -2
- package/src/release/measure.ts +1 -1
- package/src/runs/collect.test.ts +0 -2
- package/src/runtime/json-code-block.test.ts +250 -20
- package/src/runtime/json-code-block.ts +1261 -41
- package/src/term.test.ts +1 -1
- package/src/test/create-test-app.test.ts +12 -12
- package/src/test/live-signals.test.ts +6 -6
- package/src/test/provisions.integration.test.ts +10 -14
- package/src/test/reset-element-registries.ts +1 -1
- package/src/test/tenant-isolation.test.ts +12 -6
- package/site/content/docs/deployment/docker-swarm.mdx +0 -164
- package/site/content/docs/deployment/docker.mdx +0 -227
- package/site/content/docs/deployment/index.mdx +0 -83
- package/site/content/docs/deployment/kubernetes.mdx +0 -176
- package/site/content/docs/deployment/meta.json +0 -5
- package/site/content/docs/deployment/reverse-proxy.mdx +0 -234
- package/site/content/docs/elements/ai.mdx +0 -385
- package/site/content/docs/elements/channel.mdx +0 -346
- package/site/content/docs/elements/clock.mdx +0 -244
- package/site/content/docs/elements/flow.mdx +0 -420
- package/site/content/docs/elements/gate.mdx +0 -436
- package/site/content/docs/elements/signal.mdx +0 -380
- package/site/content/docs/elements/store.mdx +0 -1099
- package/site/content/docs/elements/vault.mdx +0 -405
- package/site/content/docs/get-started/basic-usage.mdx +0 -173
- package/site/content/docs/get-started/index.mdx +0 -43
- package/site/content/docs/get-started/installation.mdx +0 -220
- package/site/content/docs/get-started/introduction.mdx +0 -132
- package/site/content/docs/get-started/meta.json +0 -13
- package/site/content/docs/get-started/project-structure.mdx +0 -925
- package/site/content/docs/get-started/testing.mdx +0 -328
- package/site/content/docs/get-started/why.mdx +0 -162
- package/site/content/docs/recipes/llama-cpp.mdx +0 -151
- package/site/content/docs/recipes/ollama.mdx +0 -142
- package/site/content/docs/recipes/sglang.mdx +0 -105
- package/site/content/docs/recipes/vllm.mdx +0 -106
- package/site/content/docs/reference/cli.md +0 -232
- package/site/content/docs/reference/client.mdx +0 -469
- package/site/content/docs/reference/security.md +0 -74
- package/src/cli/ai-setup/detect-ollama.ts +0 -213
- package/src/cli/ai-setup/recommend.test.ts +0 -225
- package/src/cli/ai-setup/recommend.ts +0 -225
- package/src/cli/dev-controls.test.ts +0 -122
- package/src/cli/dev-controls.ts +0 -164
- package/src/cli/tui/DevLive.tsx +0 -126
- package/src/console/ui-next/dist/assets/access-page-De7Lc2JC.js +0 -4
- package/src/console/ui-next/dist/assets/agent-disclosure-BP0Y0Sux.js +0 -1
- package/src/console/ui-next/dist/assets/cache-glyph-B5X-NM0-.js +0 -1
- package/src/console/ui-next/dist/assets/call-pii-button-ChAUiLo9.js +0 -1
- package/src/console/ui-next/dist/assets/collapsible-DGnOM2ph.js +0 -1
- package/src/console/ui-next/dist/assets/copy-inline-button-DBHhgHKP.js +0 -1
- package/src/console/ui-next/dist/assets/dagre.esm-ZwcdTuZZ.js +0 -1
- package/src/console/ui-next/dist/assets/detail-header-DhHM1iaZ.js +0 -1
- package/src/console/ui-next/dist/assets/dropdown-menu-_NjTEo5_.js +0 -1
- package/src/console/ui-next/dist/assets/duration-tone-sC3lGABz.js +0 -9
- package/src/console/ui-next/dist/assets/element-icons-BVXtRyd3.js +0 -1
- package/src/console/ui-next/dist/assets/explorer-empty-2uIhBu0_.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-RGy7VEA_.js +0 -1
- package/src/console/ui-next/dist/assets/highlighted-json-Awq7gYdu.js +0 -154
- package/src/console/ui-next/dist/assets/http-method-_UHM2ODJ.js +0 -1
- package/src/console/ui-next/dist/assets/index-Ck88Jmv8.css +0 -2
- package/src/console/ui-next/dist/assets/index-_rgpdVzo.js +0 -66
- package/src/console/ui-next/dist/assets/link-BX6Vqztd.js +0 -1
- package/src/console/ui-next/dist/assets/observability-page-Ds6pcnh-.js +0 -4
- package/src/console/ui-next/dist/assets/preload-helper-oH4irX4C.js +0 -1
- package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +0 -1
- package/src/console/ui-next/dist/assets/react-dom-Bph1y7z7.js +0 -9
- package/src/console/ui-next/dist/assets/replica-lag-DKRrbvdo.js +0 -18
- package/src/console/ui-next/dist/assets/request-meta-DV0ywz7t.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-JQIZlWfm.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-DKxNTe_m.js +0 -1
- package/src/console/ui-next/dist/assets/skeleton-D-czQJT6.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-02xOiqIK.js +0 -41
- package/src/console/ui-next/dist/assets/trace-detail-sheet-B09O8rA6.js +0 -2
- package/src/console/ui-next/dist/assets/tree-expand-toggle-BtyhmWb4.js +0 -55
- package/src/console/ui-next/dist/assets/units-page-4rHOePuE.js +0 -1
- package/src/console/ui-next/dist/assets/useMutation-B8EO02Ej.js +0 -1
- package/src/console/ui-next/dist/assets/useRender-BE2A9BWC.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-DISPgxLM.js +0 -2
- package/src/console/ui-next/dist/assets/xyflow-D7n4g6go.js +0 -7
- package/src/console/ui-next/dist/assets/xyflow-DZ0Ws1xk.css +0 -1
- package/src/docker/ollama-pull.ts +0 -232
- package/src/docker/recipes/llama-cpp.ts +0 -298
- package/src/docker/recipes/ollama.ts +0 -44
- package/src/docker/recipes/sglang.ts +0 -55
- package/src/docker/recipes/vllm.ts +0 -44
- package/src/drivers/ai-ollama-tools.integration.test.ts +0 -109
- package/src/drivers/ai-ollama.integration.test.ts +0 -184
- package/src/drivers/ai-ollama.ts +0 -389
- package/src/drivers/ollama.ts +0 -14
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Consumers"
|
|
3
|
+
description: "Asynchronous Flows for Signal emissions, named Clock ticks, and SQL row changes."
|
|
4
|
+
icon: "Radio"
|
|
5
|
+
source: "docs/spec/unified-theory.md"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Consumers are Flows that run when something else happens — a Signal emit, a named Clock tick, or a SQL row change — instead of waiting for an HTTP request.
|
|
9
|
+
|
|
10
|
+
For developers wiring background work on okengine — bind the trigger, keep `do` on `fx`.
|
|
11
|
+
|
|
12
|
+
<Callout title="The one rule">
|
|
13
|
+
Bind with `on(signal)`, `on(clockDecl)`, or `on(db.table(…).changed())`. Delivery physics live on
|
|
14
|
+
the Signal; `cron` / `every` live on the Clock; CDC input is always `{ before, after }`. World
|
|
15
|
+
access still goes through `fx`.
|
|
16
|
+
</Callout>
|
|
17
|
+
|
|
18
|
+
## Smallest Example
|
|
19
|
+
|
|
20
|
+
<Steps>
|
|
21
|
+
|
|
22
|
+
<Step>
|
|
23
|
+
### Bind a Signal consumer
|
|
24
|
+
|
|
25
|
+
```typescript title="src/flows/notifications/welcome.ts"
|
|
26
|
+
import { on, flow } from "okengine";
|
|
27
|
+
import { userSignedUp } from "@/signals";
|
|
28
|
+
import { welcomeEmail } from "@/channels/welcome";
|
|
29
|
+
|
|
30
|
+
export const sendWelcome = on(
|
|
31
|
+
userSignedUp,
|
|
32
|
+
flow("notifications.welcome", {
|
|
33
|
+
do: async ({ userId, email }, fx) => {
|
|
34
|
+
await fx.send(welcomeEmail, {
|
|
35
|
+
to: email,
|
|
36
|
+
data: { userId },
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
</Step>
|
|
44
|
+
|
|
45
|
+
<Step>
|
|
46
|
+
### Emit from any Flow
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
await fx.emit(userSignedUp, { userId: "usr_123", email: "alice@example.com" });
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The compiler records `emits: ["users.signed-up"]` on the producer. The consumer runs after the
|
|
53
|
+
emit commits — the HTTP request does not wait for the welcome mail.
|
|
54
|
+
|
|
55
|
+
</Step>
|
|
56
|
+
|
|
57
|
+
</Steps>
|
|
58
|
+
|
|
59
|
+
<Callout title="Jobs are consumers">
|
|
60
|
+
A named Clock bound with `on(clockDecl, flow)` is the same species — an asynchronous Flow. There
|
|
61
|
+
is no separate job runner. See [Clock jobs](#clock-jobs).
|
|
62
|
+
</Callout>
|
|
63
|
+
|
|
64
|
+
## Progressive Patterns
|
|
65
|
+
|
|
66
|
+
Explore consumers from a typed queue worker to a cron job and a table-change handler:
|
|
67
|
+
|
|
68
|
+
<Tabs items={["Signal", "Clock", "CDC", "Ordered"]}>
|
|
69
|
+
|
|
70
|
+
<Tab value="Signal">
|
|
71
|
+
|
|
72
|
+
Declare delivery physics on the Signal, then bind the worker with `on(handle, flow)`:
|
|
73
|
+
|
|
74
|
+
```typescript title="src/signals/email.ts"
|
|
75
|
+
import { signal } from "okengine";
|
|
76
|
+
import { z } from "zod";
|
|
77
|
+
|
|
78
|
+
export const emailTask = signal.once("tasks.email", {
|
|
79
|
+
schema: z.object({ to: z.string().email(), body: z.string() }),
|
|
80
|
+
retries: 3,
|
|
81
|
+
deadLetter: true,
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```typescript title="src/flows/workers/email.ts"
|
|
86
|
+
import { on, flow } from "okengine";
|
|
87
|
+
import { emailTask } from "@/signals/email";
|
|
88
|
+
import { rawEmail } from "@/channels/email";
|
|
89
|
+
|
|
90
|
+
export const processEmail = on(
|
|
91
|
+
emailTask,
|
|
92
|
+
flow("workers.email", {
|
|
93
|
+
do: async ({ to, body }, fx) => {
|
|
94
|
+
await fx.send(rawEmail, { to, body });
|
|
95
|
+
},
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
</Tab>
|
|
101
|
+
|
|
102
|
+
<Tab value="Clock">
|
|
103
|
+
|
|
104
|
+
Name the schedule, then bind it. Omit `timezone` for `"UTC"`:
|
|
105
|
+
|
|
106
|
+
```typescript title="src/flows/metrics/cleanup.ts"
|
|
107
|
+
import { on, flow, clock } from "okengine";
|
|
108
|
+
import { lt } from "drizzle-orm";
|
|
109
|
+
import { db, metricLogs } from "@/schema";
|
|
110
|
+
|
|
111
|
+
export const cleanupClock = clock("metrics.cleanup", { every: "1h" });
|
|
112
|
+
|
|
113
|
+
export const cleanupMetrics = on(
|
|
114
|
+
cleanupClock,
|
|
115
|
+
flow("metrics.cleanup", {
|
|
116
|
+
do: async (_, fx) => {
|
|
117
|
+
await fx
|
|
118
|
+
.store(db)
|
|
119
|
+
.delete(metricLogs)
|
|
120
|
+
.where(lt(metricLogs.timestamp, fx.clock.ago("7d")));
|
|
121
|
+
},
|
|
122
|
+
}),
|
|
123
|
+
);
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
</Tab>
|
|
127
|
+
|
|
128
|
+
<Tab value="CDC">
|
|
129
|
+
|
|
130
|
+
`db.table(handle).changed()` fires on insert, update, and delete. Input is `{ before, after }`:
|
|
131
|
+
|
|
132
|
+
```typescript title="src/flows/audit/users.ts"
|
|
133
|
+
import { on, flow } from "okengine";
|
|
134
|
+
import { db } from "@/core";
|
|
135
|
+
import { users, auditLogs } from "@/schema";
|
|
136
|
+
|
|
137
|
+
export const onUserWrite = on(
|
|
138
|
+
db.table(users).changed(),
|
|
139
|
+
flow("audit.users", {
|
|
140
|
+
do: async ({ before, after }, fx) => {
|
|
141
|
+
const action = !before ? "created" : !after ? "deleted" : "updated";
|
|
142
|
+
const recordId = String(after?.id ?? before?.id ?? "");
|
|
143
|
+
await fx.store(db).insert(auditLogs).values({
|
|
144
|
+
table: "users",
|
|
145
|
+
recordId,
|
|
146
|
+
action,
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
</Tab>
|
|
154
|
+
|
|
155
|
+
<Tab value="Ordered">
|
|
156
|
+
|
|
157
|
+
`signal.once` plus `fx.emit(…, { key })` serializes work per key. Same key never runs
|
|
158
|
+
concurrently — the in-flight visibility lease is the lock:
|
|
159
|
+
|
|
160
|
+
```typescript title="src/flows/orders/ship.ts"
|
|
161
|
+
import { on, flow } from "okengine";
|
|
162
|
+
import { orderPlaced } from "@/signals/orders";
|
|
163
|
+
|
|
164
|
+
export const shipOrder = on(
|
|
165
|
+
orderPlaced,
|
|
166
|
+
flow("orders.ship", {
|
|
167
|
+
do: async ({ orderId }, fx) => {
|
|
168
|
+
await fx.call(fulfillOrder, { orderId });
|
|
169
|
+
},
|
|
170
|
+
}),
|
|
171
|
+
);
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
await fx.emit(orderPlaced, { orderId: "ord_1", userId: "usr_1" }, { key: "usr_1" });
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Omit `key` for competing consumers with no ordering.
|
|
179
|
+
|
|
180
|
+
</Tab>
|
|
181
|
+
|
|
182
|
+
</Tabs>
|
|
183
|
+
|
|
184
|
+
## Trigger Reference
|
|
185
|
+
|
|
186
|
+
| Trigger | Signature | Purpose | `do` input |
|
|
187
|
+
| ---------- | -------------------------------------- | ------------------------------------------- | ------------------- |
|
|
188
|
+
| Signal | `on(signalHandle, flow)` | Queue (`once`) or fan-out (`broadcast`) | Payload (`schema`) |
|
|
189
|
+
| Clock | `on(clockDecl, flow)` | Interval or cron tick | none (`_`) |
|
|
190
|
+
| CDC any | `on(db.table(t).changed(), flow)` | Every insert / update / delete | `{ before, after }` |
|
|
191
|
+
| CDC column | `on(db.table(t).changed("col"), flow)` | Same writes; column stamped on the Manifest | `{ before, after }` |
|
|
192
|
+
|
|
193
|
+
`signal.live` is an HTTP SSE tape — bind it with [`http.live`](/docs/elements/flow/http#live-streams),
|
|
194
|
+
not as a worker. A Flow with no trigger is [call-only](/docs/elements/flow).
|
|
195
|
+
|
|
196
|
+
## Signal Consumers
|
|
197
|
+
|
|
198
|
+
<Callout title="Detailed section">
|
|
199
|
+
If you only need a worker, jump to Once below. Physics live on `signal.once` / `broadcast` /
|
|
200
|
+
`live` — same idea as `http.get` / `http.post`. Defaults: `retries: 3`, `deadLetter: true`,
|
|
201
|
+
`optional: false`.
|
|
202
|
+
</Callout>
|
|
203
|
+
|
|
204
|
+
Each emit is handled according to the Signal helper you declared. The Flow is the subscriber.
|
|
205
|
+
|
|
206
|
+
<Tabs items={["Once", "Broadcast", "Optional"]}>
|
|
207
|
+
|
|
208
|
+
<Tab value="Once">
|
|
209
|
+
|
|
210
|
+
Competing workers — exactly one consumer processes each message. Failed attempts retry, then
|
|
211
|
+
dead-letter when `deadLetter` is true (default):
|
|
212
|
+
|
|
213
|
+
```typescript title="src/signals/orders.ts"
|
|
214
|
+
import { signal } from "okengine";
|
|
215
|
+
import { z } from "zod";
|
|
216
|
+
|
|
217
|
+
export const orderPlaced = signal.once("orders.placed", {
|
|
218
|
+
schema: z.object({
|
|
219
|
+
orderId: z.string(),
|
|
220
|
+
amount: z.number(),
|
|
221
|
+
userId: z.string(),
|
|
222
|
+
}),
|
|
223
|
+
retries: 5,
|
|
224
|
+
deadLetter: true,
|
|
225
|
+
});
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
```typescript title="src/flows/orders/fulfill.ts"
|
|
229
|
+
import { on, flow } from "okengine";
|
|
230
|
+
import { orderPlaced } from "@/signals/orders";
|
|
231
|
+
|
|
232
|
+
export const fulfill = on(
|
|
233
|
+
orderPlaced,
|
|
234
|
+
flow("orders.fulfill", {
|
|
235
|
+
do: async ({ orderId }, fx) => {
|
|
236
|
+
await fx.call(chargeAndShip, { orderId });
|
|
237
|
+
},
|
|
238
|
+
}),
|
|
239
|
+
);
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Visibility lease defaults to **30s**. An inflight worker that dies is reclaimed on the next claim.
|
|
243
|
+
|
|
244
|
+
</Tab>
|
|
245
|
+
|
|
246
|
+
<Tab value="Broadcast">
|
|
247
|
+
|
|
248
|
+
Every subscribed Flow gets a copy. Offline listeners miss the event — there is no replay tape:
|
|
249
|
+
|
|
250
|
+
```typescript title="src/signals/cache.ts"
|
|
251
|
+
import { signal } from "okengine";
|
|
252
|
+
import { z } from "zod";
|
|
253
|
+
|
|
254
|
+
export const catalogChanged = signal.broadcast("catalog.changed", {
|
|
255
|
+
schema: z.object({ sku: z.string() }),
|
|
256
|
+
});
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
```typescript title="src/flows/cache/invalidate.ts"
|
|
260
|
+
import { on, flow } from "okengine";
|
|
261
|
+
import { catalogChanged } from "@/signals/cache";
|
|
262
|
+
|
|
263
|
+
export const invalidate = on(
|
|
264
|
+
catalogChanged,
|
|
265
|
+
flow("cache.invalidate", {
|
|
266
|
+
do: async ({ sku }, fx) => {
|
|
267
|
+
await fx.cache.delete(`sku:${sku}`);
|
|
268
|
+
},
|
|
269
|
+
}),
|
|
270
|
+
);
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
A second Flow bound to the same Signal also runs. That is the fan-out.
|
|
274
|
+
|
|
275
|
+
</Tab>
|
|
276
|
+
|
|
277
|
+
<Tab value="Optional">
|
|
278
|
+
|
|
279
|
+
Emit with zero subscribers throws **OKE1240** unless `optional: true`. Use that for live
|
|
280
|
+
firehoses and hook Signals that may have no worker yet:
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
export const webhook = signal.once("hooks.inbound", {
|
|
284
|
+
optional: true,
|
|
285
|
+
schema: z.object({ id: z.string() }),
|
|
286
|
+
});
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**OKE1240** cause: `Flow "{flow}" emits signal "{resource}" with no subscriber.`
|
|
290
|
+
Fix: add `on(signal, …)` or mark `{ optional: true }`.
|
|
291
|
+
|
|
292
|
+
</Tab>
|
|
293
|
+
|
|
294
|
+
</Tabs>
|
|
295
|
+
|
|
296
|
+
<Accordions>
|
|
297
|
+
|
|
298
|
+
<Accordion title="Signal Options">
|
|
299
|
+
Optional second argument to `signal.once` / `broadcast` / `live`. Delivery is the helper name.
|
|
300
|
+
|
|
301
|
+
| Option | Type | Default | Meaning |
|
|
302
|
+
| ------------- | ------------------------ | --------- | ------------------------------------------------------------ |
|
|
303
|
+
| `schema` | Standard Schema | omitted | Enforced at `fx.emit` (**OKE1250** on mismatch) |
|
|
304
|
+
| `retries` | `number` | `3` | Extra attempts after the first (`retries + 1` total) |
|
|
305
|
+
| `deadLetter` | `boolean` | `true` | Keep exhausted `once` messages; `false` marks them delivered |
|
|
306
|
+
| `optional` | `boolean` | `false` | Allow emit with zero subscribers |
|
|
307
|
+
| `retention` | `{ maxAge?, maxCount? }` | unbounded | **`signal.live` only** — type error on `once` / `broadcast` |
|
|
308
|
+
| `description` | `string` | the name | Console / docs blurb |
|
|
309
|
+
|
|
310
|
+
</Accordion>
|
|
311
|
+
|
|
312
|
+
<Accordion title="Ordering">
|
|
313
|
+
Pass `{ key }` on emit. No two `once` messages sharing `(signal, key)` are claimed at once.
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
await fx.emit(emailTask, payload, { key: user.id });
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Same key → FIFO. Different keys run in parallel. Omit `key` for a pure competing pool.
|
|
320
|
+
|
|
321
|
+
</Accordion>
|
|
322
|
+
|
|
323
|
+
<Accordion title="Retries & dead letters">
|
|
324
|
+
`once` retries then DLQ. After `retries + 1` handler invocations the message is dead when
|
|
325
|
+
`deadLetter: true`. Inspect with `fx.deadLetters(signal)`.
|
|
326
|
+
|
|
327
|
+
`deadLetter: false` marks the message delivered after the last attempt — nothing lands in the DLQ.
|
|
328
|
+
|
|
329
|
+
Broadcast does not use the `once` lease / DLQ path. Live uses the retained tape, not this worker.
|
|
330
|
+
|
|
331
|
+
</Accordion>
|
|
332
|
+
|
|
333
|
+
<Accordion title="Schema at emit">
|
|
334
|
+
Invalid payloads fail at `fx.emit` with **OKE1250** (`"{resource}": {detail}`) before any
|
|
335
|
+
consumer runs. Fix the payload or the Signal's `schema`. This is an **emit** contract — not
|
|
336
|
+
an HTTP invoke contract on `flow()`. Workers inherit the typed payload from the Signal
|
|
337
|
+
declaration; they do not declare `in` on `flow({ do })`.
|
|
338
|
+
</Accordion>
|
|
339
|
+
|
|
340
|
+
</Accordions>
|
|
341
|
+
|
|
342
|
+
## Clock Jobs
|
|
343
|
+
|
|
344
|
+
<Callout title="Detailed section">
|
|
345
|
+
If you only need an interval, jump to the example below. `clock(name)` requires `cron` or `every`
|
|
346
|
+
— missing both throws `clock("name"): require cron or every`. Bind the returned handle with
|
|
347
|
+
`on(clockDecl, flow)`.
|
|
348
|
+
</Callout>
|
|
349
|
+
|
|
350
|
+
Named clocks reconcile into the Store at boot. The scheduler leader-elects so N instances do not
|
|
351
|
+
double-fire. `do` receives no payload — read time through `fx.clock`.
|
|
352
|
+
|
|
353
|
+
<Tabs items={["Interval", "Cron", "Per-tenant"]}>
|
|
354
|
+
|
|
355
|
+
<Tab value="Interval">
|
|
356
|
+
|
|
357
|
+
Human durations: `"200ms"` · `"30s"` · `"5m"` · `"1h"` · `"7d"` (integer + unit, no weeks):
|
|
358
|
+
|
|
359
|
+
```typescript title="src/flows/health/ping.ts"
|
|
360
|
+
import { on, flow, clock } from "okengine";
|
|
361
|
+
|
|
362
|
+
export const pingClock = clock("health.pingExternal", { every: "30s" });
|
|
363
|
+
|
|
364
|
+
export const pingExternal = on(
|
|
365
|
+
pingClock,
|
|
366
|
+
flow("health.pingExternal", {
|
|
367
|
+
plane: "operator",
|
|
368
|
+
do: async (_, fx) => {
|
|
369
|
+
await fx.call(pingUpstream);
|
|
370
|
+
},
|
|
371
|
+
}),
|
|
372
|
+
);
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
</Tab>
|
|
376
|
+
|
|
377
|
+
<Tab value="Cron">
|
|
378
|
+
|
|
379
|
+
Five-field cron (`m h dom mon dow`) plus an IANA `timezone` (default `"UTC"`):
|
|
380
|
+
|
|
381
|
+
```typescript title="src/flows/reports/daily.ts"
|
|
382
|
+
import { on, flow, clock } from "okengine";
|
|
383
|
+
|
|
384
|
+
export const dailyReportClock = clock("reports.daily", {
|
|
385
|
+
cron: "0 6 * * *",
|
|
386
|
+
timezone: "Asia/Riyadh",
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
export const runDailyReport = on(
|
|
390
|
+
dailyReportClock,
|
|
391
|
+
flow("reports.runDaily", {
|
|
392
|
+
plane: "operator",
|
|
393
|
+
do: async (_, fx) => {
|
|
394
|
+
await fx.call(buildDailyReport, { at: fx.clock.now() });
|
|
395
|
+
},
|
|
396
|
+
}),
|
|
397
|
+
);
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
You may set `cron` and `every` together. Extract records the cron expression as the Manifest
|
|
401
|
+
trigger when both are present.
|
|
402
|
+
|
|
403
|
+
</Tab>
|
|
404
|
+
|
|
405
|
+
<Tab value="Per-tenant">
|
|
406
|
+
|
|
407
|
+
`clock.perTenant` expands one Store row per tenant (`{name}#{tenantId}`). The bare template name
|
|
408
|
+
is never ticked:
|
|
409
|
+
|
|
410
|
+
```typescript title="src/clocks/invoices.ts"
|
|
411
|
+
import { on, flow, clock } from "okengine";
|
|
412
|
+
|
|
413
|
+
export const invoicesClock = clock.perTenant("invoices", { every: "1h" });
|
|
414
|
+
|
|
415
|
+
export const runInvoices = on(
|
|
416
|
+
invoicesClock,
|
|
417
|
+
flow("billing.invoices", {
|
|
418
|
+
plane: "operator",
|
|
419
|
+
do: async (_, fx) => {
|
|
420
|
+
await fx.call(closeOpenInvoices);
|
|
421
|
+
},
|
|
422
|
+
}),
|
|
423
|
+
);
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
Equivalent: `clock("invoices", { every: "1h", perTenant: true })`.
|
|
427
|
+
|
|
428
|
+
</Tab>
|
|
429
|
+
|
|
430
|
+
</Tabs>
|
|
431
|
+
|
|
432
|
+
<Accordions>
|
|
433
|
+
|
|
434
|
+
<Accordion title="Clock Options">
|
|
435
|
+
Second argument to `clock(name, options)` / `clock.perTenant(name, options)`.
|
|
436
|
+
|
|
437
|
+
| Option | Type | Default | Meaning |
|
|
438
|
+
| ------------- | ----------- | -------- | ---------------------------------------------- |
|
|
439
|
+
| `cron` | `string` | — | Five-field cron (`m h dom mon dow`) |
|
|
440
|
+
| `every` | `string` | — | Interval (`"10s"`, `"1h"`, `"7d"`, …) |
|
|
441
|
+
| `timezone` | IANA string | `"UTC"` | Zone for cron (intervals are duration-based) |
|
|
442
|
+
| `overridable` | `boolean` | `false` | Console may override the schedule in the Store |
|
|
443
|
+
| `perTenant` | `boolean` | `false` | Expand `{name}#{tenantId}` rows |
|
|
444
|
+
| `description` | `string` | the name | Console / docs blurb |
|
|
445
|
+
|
|
446
|
+
At least one of `cron` or `every` is required.
|
|
447
|
+
|
|
448
|
+
</Accordion>
|
|
449
|
+
|
|
450
|
+
<Accordion title="Leader lock">
|
|
451
|
+
Dev/prod clock driver is **postgres** (test is **frozen**). A short lease (default **30s**)
|
|
452
|
+
means only one instance runs each tick.
|
|
453
|
+
|
|
454
|
+
**Consequence:** three pods calling `runNow` still execute the Flow once. After the lease
|
|
455
|
+
expires, another instance may take the next tick.
|
|
456
|
+
|
|
457
|
+
`file` (`.oke/crons.json`) elects across processes on one machine. `memory` is single-process.
|
|
458
|
+
|
|
459
|
+
</Accordion>
|
|
460
|
+
|
|
461
|
+
<Accordion title="Catch-up policy one">
|
|
462
|
+
Catch-up is `"one"`: after 5 hours down on an hourly clock, the next tick fires **once**.
|
|
463
|
+
Missed slots are visible as `missedRuns` — they are not replayed as a burst.
|
|
464
|
+
|
|
465
|
+
**Consequence:** a digest that missed the night still runs once at boot, not 24 times.
|
|
466
|
+
|
|
467
|
+
</Accordion>
|
|
468
|
+
|
|
469
|
+
<Accordion title="DST & overrides">
|
|
470
|
+
Cron + a DST zone that lands in a spring-forward gap or fall-back overlap attaches a
|
|
471
|
+
**warning** on the Store row (`gap` / `overlap`). UTC never warns. The scheduler still ticks.
|
|
472
|
+
|
|
473
|
+
`overridable: true` lets Console edit the effective cron/every. Without it, a Console edit
|
|
474
|
+
fails with `ScheduleNotOverridableError`. Removed declarations become `orphaned` rows and
|
|
475
|
+
do not fire.
|
|
476
|
+
|
|
477
|
+
</Accordion>
|
|
478
|
+
|
|
479
|
+
</Accordions>
|
|
480
|
+
|
|
481
|
+
## CDC
|
|
482
|
+
|
|
483
|
+
<Callout title="Detailed section">
|
|
484
|
+
If you only need any-write, jump to the example below. The handle is
|
|
485
|
+
`db.table(table).changed(column?)` — `db` is a `store.sql` declaration, `table` is a schema
|
|
486
|
+
handle. `changed("insert")` is **not** an op filter; it stamps a column named `insert`.
|
|
487
|
+
</Callout>
|
|
488
|
+
|
|
489
|
+
SQL writes through `fx.store` notify CDC after commit. The Flow input is always
|
|
490
|
+
`{ before, after }` (`CdcPayload`).
|
|
491
|
+
|
|
492
|
+
<Tabs items={["Any write", "Column", "Images"]}>
|
|
493
|
+
|
|
494
|
+
<Tab value="Any write">
|
|
495
|
+
|
|
496
|
+
Omit the argument to react to every insert, update, and delete on the table:
|
|
497
|
+
|
|
498
|
+
```typescript title="src/flows/search/reindex.ts"
|
|
499
|
+
import { on, flow } from "okengine";
|
|
500
|
+
import { db } from "@/core";
|
|
501
|
+
import { notes } from "@/schema";
|
|
502
|
+
|
|
503
|
+
export const reindexNotes = on(
|
|
504
|
+
db.table(notes).changed(),
|
|
505
|
+
flow("search.reindexNotes", {
|
|
506
|
+
plane: "operator",
|
|
507
|
+
do: async ({ before, after }, fx) => {
|
|
508
|
+
const id = String(after?.id ?? before?.id ?? "");
|
|
509
|
+
if (!id) return;
|
|
510
|
+
if (!after) {
|
|
511
|
+
await fx.call(dropNoteIndex, { id });
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
await fx.call(upsertNoteIndex, { id });
|
|
515
|
+
},
|
|
516
|
+
}),
|
|
517
|
+
);
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
</Tab>
|
|
521
|
+
|
|
522
|
+
<Tab value="Column">
|
|
523
|
+
|
|
524
|
+
`changed("status")` stamps `trigger.cdc.column` on the Manifest (Console, extract). Still
|
|
525
|
+
receive `{ before, after }` — filter in `do` when you only care about that field:
|
|
526
|
+
|
|
527
|
+
```typescript title="src/flows/tasks/on-status.ts"
|
|
528
|
+
import { on, flow } from "okengine";
|
|
529
|
+
import { db } from "@/core";
|
|
530
|
+
import { tasks } from "@/schema";
|
|
531
|
+
|
|
532
|
+
export const onStatus = on(
|
|
533
|
+
db.table(tasks).changed("status"),
|
|
534
|
+
flow("tasks.onStatus", {
|
|
535
|
+
plane: "operator",
|
|
536
|
+
do: async ({ before, after }, fx) => {
|
|
537
|
+
if (before?.status === after?.status) return;
|
|
538
|
+
const id = String(after?.id ?? before?.id ?? "");
|
|
539
|
+
if (!id) return;
|
|
540
|
+
await fx.emit(taskStatusChanged, {
|
|
541
|
+
id,
|
|
542
|
+
from: before?.status ?? null,
|
|
543
|
+
to: after?.status ?? null,
|
|
544
|
+
});
|
|
545
|
+
},
|
|
546
|
+
}),
|
|
547
|
+
);
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
</Tab>
|
|
551
|
+
|
|
552
|
+
<Tab value="Images">
|
|
553
|
+
|
|
554
|
+
Op is inferred from which image is null:
|
|
555
|
+
|
|
556
|
+
| Write | `before` | `after` |
|
|
557
|
+
| ------ | ------------ | ------- |
|
|
558
|
+
| Insert | `null` | new row |
|
|
559
|
+
| Update | previous row | new row |
|
|
560
|
+
| Delete | previous row | `null` |
|
|
561
|
+
|
|
562
|
+
```typescript
|
|
563
|
+
do: async ({ before, after }, fx) => {
|
|
564
|
+
if (!before && after) {
|
|
565
|
+
/* insert */
|
|
566
|
+
} else if (before && after) {
|
|
567
|
+
/* update */
|
|
568
|
+
} else if (before && !after) {
|
|
569
|
+
/* delete */
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
</Tab>
|
|
575
|
+
|
|
576
|
+
</Tabs>
|
|
577
|
+
|
|
578
|
+
<Accordions>
|
|
579
|
+
|
|
580
|
+
<Accordion title="CDC payload">
|
|
581
|
+
Kernel input is `{ before, after }` — not `{ record }`, not `{ op }`. Read the primary key from
|
|
582
|
+
`after?.id ?? before?.id`.
|
|
583
|
+
|
|
584
|
+
Writes must go through `fx.store`. A raw SQL client bypasses the sink, so no consumer runs.
|
|
585
|
+
|
|
586
|
+
</Accordion>
|
|
587
|
+
|
|
588
|
+
<Accordion title="Outbox">
|
|
589
|
+
On RLS-capable SQL (`postgres` / `pglite`) the same write is appended to `oke_cdc_outbox`
|
|
590
|
+
for multi-host delivery. Pending backlog is a doctor finding (`cdc_outbox_backlog`).
|
|
591
|
+
|
|
592
|
+
Live **queries** (`store.resource({ live: true })` / `http.get(path).live(table)`) share this
|
|
593
|
+
CDC path but classify per subscriber — see [HTTP · Live Streams](/docs/elements/flow/http#live-streams).
|
|
594
|
+
|
|
595
|
+
</Accordion>
|
|
596
|
+
|
|
597
|
+
</Accordions>
|
|
598
|
+
|
|
599
|
+
## Execution
|
|
600
|
+
|
|
601
|
+
Consumers share the Flow species with HTTP. The differences are the trigger and how failure
|
|
602
|
+
is retried.
|
|
603
|
+
|
|
604
|
+
| Kind | Start | Failure | Time |
|
|
605
|
+
| ------------------ | ------------------- | -------------------------------------- | ---------------- |
|
|
606
|
+
| Signal `once` | `fx.emit` | Signal `retries` then DLQ | `fx.clock.now()` |
|
|
607
|
+
| Signal `broadcast` | `fx.emit` | Per-subscriber; no `once` DLQ | `fx.clock.now()` |
|
|
608
|
+
| Clock | scheduler tick | Flow `retry` if set; no catch-up burst | `fx.clock.*` |
|
|
609
|
+
| CDC | committed SQL write | Flow `retry` if set | `fx.clock.now()` |
|
|
610
|
+
|
|
611
|
+
Mark long work `durable: true` and wrap side effects in `fx.step` — see
|
|
612
|
+
[Workflows](/docs/elements/flow/workflows).
|
|
613
|
+
|
|
614
|
+
Clock drivers: **postgres** in `dev`/`prod`, **frozen** in `test`. Signal drivers: **redis** in
|
|
615
|
+
`dev`/`prod`, **memory** in `test`.
|
|
616
|
+
|
|
617
|
+
## Troubleshooting
|
|
618
|
+
|
|
619
|
+
<Accordions>
|
|
620
|
+
|
|
621
|
+
<Accordion title='TypeError: clock("name"): require cron or every'>
|
|
622
|
+
`clock(name)` needs `{cron}` and/or `{every}`. Empty options throw at declaration, before `on()`.
|
|
623
|
+
</Accordion>
|
|
624
|
+
|
|
625
|
+
<Accordion title="TypeError: on() expected a trigger or signal handle">
|
|
626
|
+
The first argument must be a Signal handle, a Clock handle, `db.table(…).changed()`, an HTTP
|
|
627
|
+
trigger, `internal`, or `mcp.tool(…)`. A bare interval string is not a trigger — wrap it in
|
|
628
|
+
`clock("name", { every: "1h" })`.
|
|
629
|
+
</Accordion>
|
|
630
|
+
|
|
631
|
+
<Accordion title="OKE1240 — emit with no subscriber">
|
|
632
|
+
Cause: `Flow "{flow}" emits signal "{resource}" with no subscriber.` Add `on(signal, flow)` or
|
|
633
|
+
set `{ optional: true }` on the Signal (live firehoses, unused hooks).
|
|
634
|
+
</Accordion>
|
|
635
|
+
|
|
636
|
+
<Accordion title="OKE1250 — emit failed schema">
|
|
637
|
+
Cause: `"{resource}": {detail}`. The payload failed the Signal's Standard Schema at emit. Fix the
|
|
638
|
+
payload; the consumer never ran.
|
|
639
|
+
</Accordion>
|
|
640
|
+
|
|
641
|
+
<Accordion title='changed("insert") never fires on inserts only'>
|
|
642
|
+
`changed()` takes an optional **column** name, not an op. `changed("insert")` waits for a column
|
|
643
|
+
named `insert`. Use `changed()` and branch on `before` / `after` being null.
|
|
644
|
+
</Accordion>
|
|
645
|
+
|
|
646
|
+
<Accordion title="CDC do never sees record / op">
|
|
647
|
+
Input is `{ before, after }`. There is no `record` field. Infer insert / update / delete from
|
|
648
|
+
which image is null.
|
|
649
|
+
</Accordion>
|
|
650
|
+
|
|
651
|
+
<Accordion title="Cron fired 24 times after overnight downtime">
|
|
652
|
+
It should not. Catch-up is `"one"` — one fire per overdue clock, then `nextRunAt` advances. If you
|
|
653
|
+
see a burst, you likely bound several clocks (or `clock.perTenant` expanded many tenants), not a
|
|
654
|
+
replay of missed hourly slots.
|
|
655
|
+
</Accordion>
|
|
656
|
+
|
|
657
|
+
<Accordion title="Two pods ran the same job">
|
|
658
|
+
Clock leader election needs a shared Store (`drivers.clock` postgres, or `file` on one host).
|
|
659
|
+
`memory` does not coordinate across processes. Check that both instances share `DATABASE_URL`.
|
|
660
|
+
</Accordion>
|
|
661
|
+
|
|
662
|
+
<Accordion title="ScheduleNotOverridableError from Console">
|
|
663
|
+
The clock was declared without `overridable: true`. Add it and redeploy, or edit the declaration
|
|
664
|
+
in source instead of Console.
|
|
665
|
+
</Accordion>
|
|
666
|
+
|
|
667
|
+
<Accordion title="tz is not a clock option">
|
|
668
|
+
The field is `timezone` (IANA), default `"UTC"`. `{ tz: "Asia/Riyadh" }` is ignored.
|
|
669
|
+
</Accordion>
|
|
670
|
+
|
|
671
|
+
</Accordions>
|
|
672
|
+
|
|
673
|
+
## Learn more
|
|
674
|
+
|
|
675
|
+
- [Signal](/docs/elements/signal) — `once` / `broadcast` / `live` physics
|
|
676
|
+
- [Signal · Once](/docs/elements/signal/once) — leases, retries, partition keys
|
|
677
|
+
- [Clock](/docs/elements/clock) — schedules, `fx.clock.sleep`
|
|
678
|
+
- [Store · SQL](/docs/elements/store/sql) — tables CDC watches
|
|
679
|
+
- [HTTP · Live Streams](/docs/elements/flow/http#live-streams) — `signal.live` SSE
|
|
680
|
+
- [fx](/docs/reference/fx) — `fx.emit`, `fx.deadLetters`, `fx.clock`
|
|
681
|
+
- [Errors](/docs/reference/errors) — OKE1240 · OKE1250
|
|
682
|
+
- [Workflows](/docs/elements/flow/workflows) — `durable: true` + `fx.step` on a consumer
|
|
683
|
+
|
|
684
|
+
## Next
|
|
685
|
+
|
|
686
|
+
<Cards>
|
|
687
|
+
<Card
|
|
688
|
+
title="Durable Workflows"
|
|
689
|
+
description="Step journaling and multi-step distributed execution."
|
|
690
|
+
href="/docs/elements/flow/workflows"
|
|
691
|
+
/>
|
|
692
|
+
<Card
|
|
693
|
+
title="Signal Element"
|
|
694
|
+
description="Delivery physics — once, broadcast, and live tapes."
|
|
695
|
+
href="/docs/elements/signal"
|
|
696
|
+
/>
|
|
697
|
+
<Card
|
|
698
|
+
title="Clock Element"
|
|
699
|
+
description="Named schedules, intervals, and durable sleep."
|
|
700
|
+
href="/docs/elements/clock"
|
|
701
|
+
/>
|
|
702
|
+
<Card
|
|
703
|
+
title="HTTP"
|
|
704
|
+
description="Synchronous REST, QUERY, resources, and live SSE."
|
|
705
|
+
href="/docs/elements/flow/http"
|
|
706
|
+
/>
|
|
707
|
+
</Cards>
|