okengine 0.21.1 → 0.23.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 +11 -4
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +15 -9
- package/manifest.v1.schema.json +54 -0
- package/package.json +53 -38
- package/site/content/docs/ai/mcp.mdx +13 -9
- package/site/content/docs/ai/skills.mdx +2 -2
- package/site/content/docs/client/calling.mdx +27 -19
- package/site/content/docs/client/index.mdx +1 -1
- package/site/content/docs/elements/ai/agents.mdx +61 -13
- package/site/content/docs/elements/ai/decide.mdx +536 -0
- package/site/content/docs/elements/ai/events.mdx +373 -0
- package/site/content/docs/elements/ai/index.mdx +9 -3
- package/site/content/docs/elements/ai/meta.json +1 -1
- package/site/content/docs/elements/ai/prompts.mdx +4 -2
- package/site/content/docs/elements/channel/email.mdx +1 -1
- package/site/content/docs/elements/channel/index.mdx +1 -1
- package/site/content/docs/elements/flow/http.mdx +4 -4
- package/site/content/docs/elements/flow/index.mdx +3 -3
- package/site/content/docs/elements/flow/meta.json +1 -1
- package/site/content/docs/elements/gate/auth.mdx +4 -1
- package/site/content/docs/elements/gate/tenancy.mdx +1 -1
- package/site/content/docs/elements/store/index.mdx +1 -1
- package/site/content/docs/elements/store/kv.mdx +5 -5
- package/site/content/docs/elements/vault/rotation.mdx +15 -0
- package/site/content/docs/plugins/otp.mdx +31 -31
- package/site/content/docs/recipes/dragonfly.mdx +5 -5
- package/site/content/docs/recipes/index.mdx +6 -6
- package/site/content/docs/recipes/mailpit.mdx +1 -1
- package/site/content/docs/recipes/meilisearch.mdx +1 -1
- package/site/content/docs/recipes/pgdog.mdx +1 -1
- package/site/content/docs/recipes/postgres.mdx +12 -8
- package/site/content/docs/recipes/redis.mdx +4 -4
- package/site/content/docs/reference/cli.mdx +2 -1
- package/site/content/docs/reference/configuration.mdx +11 -9
- package/site/content/docs/reference/errors.mdx +12 -2
- package/site/content/docs/reference/fx.mdx +35 -21
- package/site/content/docs/reference/idempotency.mdx +192 -0
- package/site/content/docs/reference/index.mdx +6 -1
- package/site/content/docs/reference/meta.json +2 -1
- package/site/content/docs/understand/meta.json +1 -1
- package/site/content/docs/{elements/flow → understand}/routing.mdx +11 -10
- package/site/content/docs/understand/the-architecture.mdx +14 -12
- package/site/content/docs/understand/try-it.mdx +3 -3
- package/src/auth/api-key-sql.test.ts +44 -0
- package/src/auth/api-key-sql.ts +1 -0
- package/src/auth/api-keys.ts +30 -12
- package/src/auth/config.ts +6 -2
- package/src/auth/gate-auth.test.ts +20 -0
- package/src/cli/agents-md.test.ts +25 -0
- package/src/cli/ask-vault-gaps.test.ts +38 -1
- package/src/cli/ask-vault-gaps.ts +35 -2
- package/src/cli/competitor-mention-removal.test.ts +3 -3
- package/src/cli/decide.ts +174 -0
- package/src/cli/decision-lock-watch.test.ts +45 -0
- package/src/cli/decision-lock-watch.ts +51 -0
- package/src/cli/dev-app-runner.ts +34 -8
- package/src/cli/dev-auth-secret.test.ts +49 -0
- package/src/cli/dev-auth-secret.ts +49 -0
- package/src/cli/dev-hot.test.ts +41 -0
- package/src/cli/dev-hot.ts +48 -0
- package/src/cli/dev.test.ts +1 -1
- package/src/cli/dev.ts +24 -6
- package/src/cli/docker-cli.test.ts +1 -1
- package/src/cli/eval.ts +142 -1
- package/src/cli/index.ts +14 -1
- package/src/cli/load-config.images.test.ts +12 -12
- package/src/cli/load-config.ts +2 -2
- package/src/cli/mcp-from-console.ts +48 -0
- package/src/cli/registry.ts +35 -1
- package/src/cli/vault-cmd.test.ts +23 -0
- package/src/cli/vault-cmd.ts +7 -0
- package/src/client/agent.ts +307 -0
- package/src/client/create.ts +18 -1
- package/src/client/transport.test.ts +245 -0
- package/src/client/transport.ts +133 -19
- package/src/client/types.ts +29 -3
- package/src/client-react/index.ts +7 -1
- package/src/client-react/use-agent-run.test.ts +271 -0
- package/src/client-react/use-agent-run.ts +193 -0
- package/src/compiler/ai-approval.test.ts +36 -0
- package/src/compiler/ai-repair.test.ts +22 -0
- package/src/compiler/decisions.extract.test.ts +164 -0
- package/src/compiler/effects-infer.ts +753 -17
- package/src/compiler/extract.test.ts +37 -0
- package/src/compiler/extract.ts +381 -24
- package/src/compiler/fixtures/skyport.expected.json +24 -0
- package/src/compiler/fx-follow.test.ts +164 -0
- package/src/compiler/fx-index.ts +399 -0
- package/src/compiler/response.ts +35 -2
- package/src/config/index.ts +21 -3
- package/src/console/server/ai-runs-flows.ts +269 -0
- package/src/console/server/ai-runs.test.ts +323 -0
- package/src/console/server/ai-runs.ts +562 -0
- package/src/console/server/ai.ts +12 -1
- package/src/console/server/app.ts +10 -1
- package/src/console/server/decisions-flows.ts +157 -0
- package/src/console/server/decisions.test.ts +66 -0
- package/src/console/server/decisions.ts +229 -0
- package/src/console/server/flows-invoke.test.ts +2 -2
- package/src/console/server/flows.ts +50 -2
- package/src/console/server/invoke-user-flow.ts +9 -0
- package/src/console/server/runs-ingest.ts +16 -0
- package/src/console/server/serve.ts +5 -0
- package/src/console/server/state.ts +25 -1
- package/src/console/server/store-stats.test.ts +47 -0
- package/src/console/server/store-stats.ts +92 -18
- package/src/console/ui-next/dist/assets/access-page-DDKkhT9v.js +4 -0
- package/src/console/ui-next/dist/assets/agent-disclosure-62Xts8Xi.js +1 -0
- package/src/console/ui-next/dist/assets/{cache-glyph-92uM5MO7.js → cache-glyph-DyeoKHKA.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-DtGVPtcs.js → call-pii-button-DvfpLXsU.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-DN7l6zmC.js → collapsible-BY03SeCg.js} +1 -1
- package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
- package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
- package/src/console/ui-next/dist/assets/decisions-page-CgkKlA56.js +1 -0
- package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
- package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
- package/src/console/ui-next/dist/assets/duration-tone-Y6HLjyJ5.js +9 -0
- package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
- package/src/console/ui-next/dist/assets/flows-page-DLPfNy-_.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-BlAEVgNW.js → highlighted-json-D7nNzpgB.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-BDf7OAHv.js → http-method-DdL19zzh.js} +1 -1
- package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
- package/src/console/ui-next/dist/assets/index-C0lc_s9d.js +58 -0
- package/src/console/ui-next/dist/assets/observability-page-CRlcyLCC.js +8 -0
- package/src/console/ui-next/dist/assets/{replica-lag-B3GLNVfF.js → replica-lag-Bb3FWUu9.js} +7 -7
- package/src/console/ui-next/dist/assets/request-meta-DYBMWhX8.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
- package/src/console/ui-next/dist/assets/store-page-CcE-SXC8.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-CKMSEQ4s.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-QOfk0M0H.js +55 -0
- package/src/console/ui-next/dist/assets/units-page-DKC1CDDd.js +1 -0
- package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
- package/src/console/ui-next/dist/assets/vault-page-B54v4Yit.js +2 -0
- package/src/console/ui-next/dist/index.html +4 -3
- package/src/console/ui-next/seed-parked-approval.ts +126 -0
- package/src/console/ui-next/src/client.ts +224 -1
- package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
- package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
- package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
- package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
- package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
- package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
- package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
- package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
- package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
- package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
- package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
- package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
- package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
- package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
- package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
- package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
- package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
- package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
- package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
- package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
- package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
- package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
- package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
- package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
- package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
- package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
- package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
- package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
- package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +15 -2
- package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
- package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
- package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
- package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
- package/src/console/ui-next/src/router.tsx +17 -0
- package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
- package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
- package/src/console/ui-next/vite.config.ts +9 -0
- package/src/docker/docker.test.ts +13 -13
- package/src/docker/images-config.test.ts +12 -12
- package/src/docker/recipes/dragonfly.ts +3 -2
- package/src/docker/recipes/redis.ts +1 -1
- package/src/docker/stack-id.test.ts +1 -1
- package/src/drivers/ai-anthropic.ts +212 -21
- package/src/drivers/ai-mock.ts +34 -1
- package/src/drivers/ai-openai-compatible.ts +67 -26
- package/src/drivers/ai-preconnect.ts +18 -0
- package/src/drivers/ai-stream.test.ts +62 -0
- package/src/drivers/ai-types.ts +20 -0
- package/src/drivers/journal-postgres.ts +455 -3
- package/src/drivers/meilisearch.ts +2 -0
- package/src/drivers/memory-ddl.test.ts +18 -0
- package/src/drivers/memory.ts +21 -1
- package/src/drivers/pg-rls.test.ts +31 -0
- package/src/drivers/pg-rls.ts +15 -0
- package/src/drivers/postgres.test.ts +9 -0
- package/src/drivers/postgres.ts +49 -11
- package/src/drivers/vault-builtin.test.ts +10 -0
- package/src/drivers/vault-builtin.ts +13 -1
- package/src/drivers/vault-remote-bag.ts +5 -1
- package/src/drivers/vault-types.ts +6 -1
- package/src/elements/ai/agent-event-slot.ts +27 -0
- package/src/elements/ai/approval-http.ts +207 -0
- package/src/elements/ai/approval.test.ts +741 -0
- package/src/elements/ai/approval.ts +228 -0
- package/src/elements/ai/decisions/bind.ts +163 -0
- package/src/elements/ai/decisions/certificate.test.ts +291 -0
- package/src/elements/ai/decisions/certificate.ts +377 -0
- package/src/elements/ai/decisions/certify.ts +314 -0
- package/src/elements/ai/decisions/decide.test.ts +953 -0
- package/src/elements/ai/decisions/e2e.test.ts +399 -0
- package/src/elements/ai/decisions/export.test.ts +120 -0
- package/src/elements/ai/decisions/export.ts +146 -0
- package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
- package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
- package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
- package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
- package/src/elements/ai/decisions/http.test.ts +154 -0
- package/src/elements/ai/decisions/http.ts +240 -0
- package/src/elements/ai/decisions/labels.test.ts +282 -0
- package/src/elements/ai/decisions/labels.ts +257 -0
- package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
- package/src/elements/ai/decisions/openrouter.ts +36 -0
- package/src/elements/ai/decisions/provider.ts +102 -0
- package/src/elements/ai/decisions/typesafe.ts +36 -0
- package/src/elements/ai/declare.ts +252 -1
- package/src/elements/ai/events.test.ts +379 -0
- package/src/elements/ai/events.ts +138 -0
- package/src/elements/ai/run-events.test.ts +461 -0
- package/src/elements/ai/run-events.ts +522 -0
- package/src/elements/ai/runtime.ts +1186 -105
- package/src/elements/ai/stream-turn.ts +214 -0
- package/src/elements/ai/stream.live.test.ts +76 -0
- package/src/elements/ai/subagent.test.ts +136 -0
- package/src/elements/ai.test.ts +497 -3
- package/src/elements/ai.ts +16 -1
- package/src/elements/clock/durable.ts +27 -1
- package/src/elements/store/emit-drizzle.ts +2 -2
- package/src/elements/store/live-default.test.ts +2 -0
- package/src/elements/store/live-http.test.ts +1 -0
- package/src/elements/store/resource-list-docs.test.ts +2 -2
- package/src/elements/store/resource.test.ts +3 -3
- package/src/elements/store/schema-decl.test.ts +1 -0
- package/src/elements/store/sql-condition.ts +24 -7
- package/src/elements/store/sql-session.test.ts +51 -0
- package/src/elements/store/sql-session.ts +111 -1
- package/src/elements/store/upsert-app.test.ts +1 -1
- package/src/elements/vault/audit.test.ts +137 -0
- package/src/elements/vault/audit.ts +106 -13
- package/src/elements/vault/boot-chain.ts +9 -1
- package/src/elements/vault/builtin-adapter.ts +33 -3
- package/src/i18n/catalogs/ar.ts +9 -0
- package/src/i18n/catalogs/en.ts +9 -0
- package/src/index.ts +2 -0
- package/src/kernel/abort-scope.ts +33 -2
- package/src/kernel/agent-event-store.ts +448 -0
- package/src/kernel/app.ts +294 -19
- package/src/kernel/auto-cache.test.ts +6 -6
- package/src/kernel/boot-bind/ai.ts +3 -0
- package/src/kernel/boot-bind/store.test.ts +26 -0
- package/src/kernel/boot-bind/store.ts +82 -6
- package/src/kernel/boot-bind/vault.ts +1 -0
- package/src/kernel/boot.ts +34 -5
- package/src/kernel/builtin-errors.ts +10 -0
- package/src/kernel/capability.ts +3 -0
- package/src/kernel/client-descriptor.ts +1 -0
- package/src/kernel/concurrency.test.ts +33 -0
- package/src/kernel/decision-label-store.ts +441 -0
- package/src/kernel/effects-stamping.test.ts +2 -2
- package/src/kernel/effects.test.ts +2 -1
- package/src/kernel/effects.ts +13 -1
- package/src/kernel/element-registries.ts +3 -0
- package/src/kernel/errors-compiler.ts +20 -0
- package/src/kernel/errors-text.ts +4 -0
- package/src/kernel/errors.ts +2 -0
- package/src/kernel/external-effects.test.ts +36 -0
- package/src/kernel/flow.test.ts +2 -3
- package/src/kernel/flow.ts +24 -0
- package/src/kernel/fx-decide.ts +761 -0
- package/src/kernel/fx-fetch.ts +5 -1
- package/src/kernel/fx.test.ts +12 -5
- package/src/kernel/fx.ts +316 -29
- package/src/kernel/http-frame.test.ts +94 -0
- package/src/kernel/http-frame.ts +203 -0
- package/src/kernel/idempotency-store.ts +578 -0
- package/src/kernel/idempotency.test.ts +556 -0
- package/src/kernel/idempotency.ts +304 -0
- package/src/kernel/index.ts +1 -0
- package/src/kernel/journal.ts +160 -0
- package/src/kernel/json-result.ts +10 -0
- package/src/kernel/pipeline.test.ts +45 -0
- package/src/kernel/sse-id.ts +32 -0
- package/src/manifest/diff.ts +1 -0
- package/src/manifest/types.ts +37 -2
- package/src/mcp/ai-tools.test.ts +202 -0
- package/src/mcp/authorization.ts +66 -0
- package/src/mcp/session.ts +3 -0
- package/src/mcp/tools.ts +168 -0
- package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
- package/src/plugins/otp.test.ts +65 -1
- package/src/plugins/otp.ts +59 -7
- package/src/release/absolute-regression.test.ts +63 -0
- package/src/release/http-graph.test.ts +27 -0
- package/src/release/http-graph.ts +95 -0
- package/src/release/index.ts +3 -0
- package/src/release/limits.ts +17 -2
- package/src/release/measure.ts +144 -28
- package/src/runs/collect.ts +4 -1
- package/src/runs/parquet.test.ts +27 -1
- package/src/runs/parquet.ts +19 -0
- package/src/runs/types.ts +31 -0
- package/src/runtime/json-code-block.test.ts +106 -47
- package/src/runtime/json-code-block.ts +1209 -46
- package/src/term.test.ts +13 -0
- package/src/term.ts +81 -49
- package/src/test/create-test-app.test.ts +3 -1
- package/src/test/create-test-app.ts +40 -0
- package/src/test/live-signals.test.ts +1 -0
- package/src/test/provisions.integration.test.ts +1 -0
- package/src/test/tenant-isolation.test.ts +1 -0
- package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
- package/src/console/ui-next/dist/assets/access-page-Bgt9bq2r.js +0 -4
- package/src/console/ui-next/dist/assets/agent-disclosure-B43CXZZR.js +0 -1
- package/src/console/ui-next/dist/assets/copy-inline-button-CAYD18cr.js +0 -1
- package/src/console/ui-next/dist/assets/detail-header-DVWNjWjg.js +0 -1
- package/src/console/ui-next/dist/assets/dropdown-menu-4h2LOVXM.js +0 -1
- package/src/console/ui-next/dist/assets/duration-tone-BmIR9FV8.js +0 -9
- package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +0 -1
- package/src/console/ui-next/dist/assets/explorer-empty-CJs5A-wm.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-BMHs-IzK.js +0 -1
- package/src/console/ui-next/dist/assets/index-BKpaes3n.js +0 -63
- package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
- package/src/console/ui-next/dist/assets/observability-page-WnVLI-0j.js +0 -4
- package/src/console/ui-next/dist/assets/request-meta-DMbnAe3f.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-KvFDingJ.js +0 -41
- package/src/console/ui-next/dist/assets/trace-detail-sheet-Htk8Cm9t.js +0 -2
- package/src/console/ui-next/dist/assets/tree-expand-toggle-CFMPWX4f.js +0 -55
- package/src/console/ui-next/dist/assets/units-page-C4NdNuxP.js +0 -1
- package/src/console/ui-next/dist/assets/use-vault-list-CT4-gajj.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-CBYl0LW_.js +0 -2
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Effect inference follows `fx` into helpers and same-function chain aliases.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { extractFromSources } from "./extract.ts";
|
|
7
|
+
|
|
8
|
+
const header = `
|
|
9
|
+
import { on, flow, http, store } from "okengine";
|
|
10
|
+
export const db = store.sql("app");
|
|
11
|
+
export const notes = store.schema.table("notes", { id: field.text().primaryKey() });
|
|
12
|
+
`;
|
|
13
|
+
|
|
14
|
+
describe("extractManifest — fx helpers and chain aliases", () => {
|
|
15
|
+
test("a same-file helper's store read is part of the flow", async () => {
|
|
16
|
+
const manifest = await extractFromSources({
|
|
17
|
+
"src/flows/notes/get.ts": `
|
|
18
|
+
${header}
|
|
19
|
+
async function load(fx: Fx) {
|
|
20
|
+
await fx.store(db).select().from(notes);
|
|
21
|
+
}
|
|
22
|
+
export const get = on(http.get("/notes"), flow("notes.get", {
|
|
23
|
+
do: async (_input, fx) => {
|
|
24
|
+
await load(fx);
|
|
25
|
+
return { ok: true };
|
|
26
|
+
},
|
|
27
|
+
}));
|
|
28
|
+
`,
|
|
29
|
+
});
|
|
30
|
+
expect(manifest.flows?.["notes.get"]?.effects?.reads).toEqual(["sql:notes"]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("an imported helper's store read is part of the flow", async () => {
|
|
34
|
+
const manifest = await extractFromSources({
|
|
35
|
+
"src/flows/notes/_shared.ts": `
|
|
36
|
+
${header}
|
|
37
|
+
export async function load(fx: Fx) {
|
|
38
|
+
await fx.store(db).select().from(notes);
|
|
39
|
+
}
|
|
40
|
+
`,
|
|
41
|
+
"src/flows/notes/get.ts": `
|
|
42
|
+
import { on, flow, http } from "okengine";
|
|
43
|
+
import { load } from "./_shared";
|
|
44
|
+
export const get = on(http.get("/notes"), flow("notes.get", {
|
|
45
|
+
do: async (_input, fx) => load(fx),
|
|
46
|
+
}));
|
|
47
|
+
`,
|
|
48
|
+
});
|
|
49
|
+
expect(manifest.flows?.["notes.get"]?.effects?.reads).toEqual(["sql:notes"]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("a same-function chain alias records the read", async () => {
|
|
53
|
+
const manifest = await extractFromSources({
|
|
54
|
+
"src/flows/notes/get.ts": `
|
|
55
|
+
${header}
|
|
56
|
+
export const get = on(http.get("/notes"), flow("notes.get", {
|
|
57
|
+
do: async (_input, fx) => {
|
|
58
|
+
const q = fx.store(db);
|
|
59
|
+
const rows = await q.select().from(notes);
|
|
60
|
+
return rows;
|
|
61
|
+
},
|
|
62
|
+
}));
|
|
63
|
+
`,
|
|
64
|
+
});
|
|
65
|
+
expect(manifest.flows?.["notes.get"]?.effects?.reads).toEqual(["sql:notes"]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("an as-cast store alias records the index write", async () => {
|
|
69
|
+
const manifest = await extractFromSources({
|
|
70
|
+
"src/flows/search/index.ts": `
|
|
71
|
+
import { on, flow, http, store } from "okengine";
|
|
72
|
+
export const taskIndex = store.index("tasks");
|
|
73
|
+
export const sync = on(http.post("/sync"), flow("search.sync", {
|
|
74
|
+
do: async (_input, fx) => {
|
|
75
|
+
const idx = fx.store(taskIndex) as {
|
|
76
|
+
driverId: string;
|
|
77
|
+
upsert: (id: string, doc: unknown) => Promise<void>;
|
|
78
|
+
};
|
|
79
|
+
if (idx.driverId === "meilisearch") {
|
|
80
|
+
await idx.upsert("1", { id: "1" });
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
}));
|
|
84
|
+
`,
|
|
85
|
+
});
|
|
86
|
+
expect(manifest.flows?.["search.sync"]?.effects?.writes).toEqual(["index:tasks"]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("a chain alias that escapes fails extract", async () => {
|
|
90
|
+
const bodies = [
|
|
91
|
+
`const q = fx.store(db); return q;`,
|
|
92
|
+
`const q = fx.store(db); await other(q);`,
|
|
93
|
+
`let q = fx.store(db); q = fx.store(db);`,
|
|
94
|
+
`const q = fx.store(db); const bag = { q }; return bag;`,
|
|
95
|
+
`const q = fx.store(db); const bag = [q]; return bag;`,
|
|
96
|
+
];
|
|
97
|
+
for (const body of bodies) {
|
|
98
|
+
await expect(
|
|
99
|
+
extractFromSources({
|
|
100
|
+
"src/flows/notes/get.ts": `
|
|
101
|
+
${header}
|
|
102
|
+
export const get = on(http.get("/notes"), flow("notes.get", {
|
|
103
|
+
do: async (_input, fx) => {
|
|
104
|
+
${body}
|
|
105
|
+
},
|
|
106
|
+
}));
|
|
107
|
+
`,
|
|
108
|
+
}),
|
|
109
|
+
).rejects.toThrow(/OKE1900/);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("renaming or destructuring fx fails extract", async () => {
|
|
114
|
+
const bodies = [
|
|
115
|
+
`do: async (input, ctx) => ({ ok: true })`,
|
|
116
|
+
`do: async (_input, fx) => { const ctx = fx; return ctx.id(); }`,
|
|
117
|
+
`do: async (_input, fx) => { const { store } = fx; return store; }`,
|
|
118
|
+
];
|
|
119
|
+
for (const body of bodies) {
|
|
120
|
+
await expect(
|
|
121
|
+
extractFromSources({
|
|
122
|
+
"src/flows/notes/get.ts": `
|
|
123
|
+
${header}
|
|
124
|
+
export const get = on(http.get("/notes"), flow("notes.get", { ${body} }));
|
|
125
|
+
`,
|
|
126
|
+
}),
|
|
127
|
+
).rejects.toThrow(/OKE1900/);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("an effects block must cover inferred reads and may add keys", async () => {
|
|
132
|
+
await expect(
|
|
133
|
+
extractFromSources({
|
|
134
|
+
"src/flows/notes/get.ts": `
|
|
135
|
+
${header}
|
|
136
|
+
async function load(fx: Fx) {
|
|
137
|
+
await fx.store(db).select().from(notes);
|
|
138
|
+
}
|
|
139
|
+
export const get = on(http.get("/notes"), flow("notes.get", {
|
|
140
|
+
effects: { reads: ["sql:other"] },
|
|
141
|
+
do: async (_input, fx) => load(fx),
|
|
142
|
+
}));
|
|
143
|
+
`,
|
|
144
|
+
}),
|
|
145
|
+
).rejects.toThrow(/OKE1900/);
|
|
146
|
+
|
|
147
|
+
const manifest = await extractFromSources({
|
|
148
|
+
"src/flows/notes/get.ts": `
|
|
149
|
+
${header}
|
|
150
|
+
async function load(fx: Fx) {
|
|
151
|
+
await fx.store(db).select().from(notes);
|
|
152
|
+
}
|
|
153
|
+
export const get = on(http.get("/notes"), flow("notes.get", {
|
|
154
|
+
effects: { reads: ["sql:notes"], writes: ["kv:extra"] },
|
|
155
|
+
do: async (_input, fx) => load(fx),
|
|
156
|
+
}));
|
|
157
|
+
`,
|
|
158
|
+
});
|
|
159
|
+
expect(manifest.flows?.["notes.get"]?.effects).toMatchObject({
|
|
160
|
+
reads: ["sql:notes"],
|
|
161
|
+
writes: ["kv:extra"],
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One index of project functions that receive `fx`, built from ASTs
|
|
3
|
+
* extract already parsed. Inference follows a call that passes `fx`
|
|
4
|
+
* through this index — it does not re-parse or walk the filesystem.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
identifierName,
|
|
9
|
+
stringArg,
|
|
10
|
+
type AstNode,
|
|
11
|
+
type FxCalleeResolution,
|
|
12
|
+
} from "./effects-infer.ts";
|
|
13
|
+
|
|
14
|
+
/** One `tsconfig` `paths` entry (`@/*` → `./src/*`). */
|
|
15
|
+
export interface TsconfigPath {
|
|
16
|
+
readonly pattern: string;
|
|
17
|
+
readonly targets: readonly string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Resolve a callee in one indexed file. */
|
|
21
|
+
export interface FxIndex {
|
|
22
|
+
/**
|
|
23
|
+
* Resolve `callee` as seen from `file`.
|
|
24
|
+
*
|
|
25
|
+
* @param file - File that contains the call
|
|
26
|
+
* @param callee - Call callee node
|
|
27
|
+
*/
|
|
28
|
+
resolve(file: string, callee: AstNode): FxCalleeResolution;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface NamedImport {
|
|
32
|
+
readonly kind: "named";
|
|
33
|
+
readonly from: string;
|
|
34
|
+
readonly imported: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface NamespaceImport {
|
|
38
|
+
readonly kind: "namespace";
|
|
39
|
+
readonly from: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface IntrinsicImport {
|
|
43
|
+
readonly kind: "intrinsic";
|
|
44
|
+
readonly name: "liveQuery" | "applySearchEmbedCdc";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type ImportBinding = NamedImport | NamespaceImport | IntrinsicImport;
|
|
48
|
+
|
|
49
|
+
interface LocalExport {
|
|
50
|
+
readonly kind: "local";
|
|
51
|
+
readonly binding: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface Reexport {
|
|
55
|
+
readonly kind: "reexport";
|
|
56
|
+
readonly from: string;
|
|
57
|
+
readonly imported: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface FileFx {
|
|
61
|
+
readonly functions: Map<string, AstNode>;
|
|
62
|
+
readonly imports: Map<string, ImportBinding>;
|
|
63
|
+
readonly exports: Map<string, LocalExport | Reexport>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const INTRINSIC_SOURCES: Readonly<
|
|
67
|
+
Record<string, Readonly<Record<string, "liveQuery" | "applySearchEmbedCdc">>>
|
|
68
|
+
> = {
|
|
69
|
+
okengine: { liveQuery: "liveQuery" },
|
|
70
|
+
"okengine/store": { liveQuery: "liveQuery", applySearchEmbedCdc: "applySearchEmbedCdc" },
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Read `compilerOptions.paths` from `tsconfig.json` when `rootDir` is set.
|
|
75
|
+
*
|
|
76
|
+
* @param rootDir - App root
|
|
77
|
+
*/
|
|
78
|
+
export async function readTsconfigPaths(rootDir: string): Promise<readonly TsconfigPath[]> {
|
|
79
|
+
const file = Bun.file(`${rootDir}/tsconfig.json`);
|
|
80
|
+
if (!(await file.exists())) return [];
|
|
81
|
+
let parsed: unknown;
|
|
82
|
+
try {
|
|
83
|
+
parsed = JSON.parse(await file.text()) as unknown;
|
|
84
|
+
} catch {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
if (typeof parsed !== "object" || parsed === null) return [];
|
|
88
|
+
const options = (parsed as { compilerOptions?: unknown }).compilerOptions;
|
|
89
|
+
if (typeof options !== "object" || options === null) return [];
|
|
90
|
+
const paths = (options as { paths?: unknown }).paths;
|
|
91
|
+
if (typeof paths !== "object" || paths === null) return [];
|
|
92
|
+
const out: TsconfigPath[] = [];
|
|
93
|
+
for (const [pattern, target] of Object.entries(paths)) {
|
|
94
|
+
if (!Array.isArray(target)) continue;
|
|
95
|
+
const targets = target.filter((item): item is string => typeof item === "string");
|
|
96
|
+
if (targets.length > 0) out.push({ pattern, targets });
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Index top-level functions and import/export bindings for `fx` following.
|
|
103
|
+
*
|
|
104
|
+
* @param files - Parsed sources (path → program)
|
|
105
|
+
* @param paths - `tsconfig` paths, empty when there is no root
|
|
106
|
+
*/
|
|
107
|
+
export function buildFxIndex(
|
|
108
|
+
files: readonly { readonly path: string; readonly program: AstNode }[],
|
|
109
|
+
paths: readonly TsconfigPath[] = [],
|
|
110
|
+
): FxIndex {
|
|
111
|
+
const known = new Set(files.map((file) => normalizePosix(file.path)));
|
|
112
|
+
const byFile = new Map<string, FileFx>();
|
|
113
|
+
for (const file of files) {
|
|
114
|
+
byFile.set(normalizePosix(file.path), indexFile(file.program));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function resolveModule(fromFile: string, spec: string): string | undefined {
|
|
118
|
+
if (spec.startsWith(".")) {
|
|
119
|
+
const dir = posixDir(fromFile);
|
|
120
|
+
return matchFile(normalizePosix(dir.length > 0 ? `${dir}/${spec}` : spec), known);
|
|
121
|
+
}
|
|
122
|
+
for (const entry of paths) {
|
|
123
|
+
const star = entry.pattern.indexOf("*");
|
|
124
|
+
if (star < 0) {
|
|
125
|
+
if (spec !== entry.pattern) continue;
|
|
126
|
+
for (const target of entry.targets) {
|
|
127
|
+
const hit = matchFile(normalizePosix(target), known);
|
|
128
|
+
if (hit) return hit;
|
|
129
|
+
}
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const prefix = entry.pattern.slice(0, star);
|
|
133
|
+
const suffix = entry.pattern.slice(star + 1);
|
|
134
|
+
if (!spec.startsWith(prefix) || !spec.endsWith(suffix)) continue;
|
|
135
|
+
const mid = spec.slice(prefix.length, spec.length - suffix.length);
|
|
136
|
+
for (const target of entry.targets) {
|
|
137
|
+
const hit = matchFile(normalizePosix(target.replace("*", mid)), known);
|
|
138
|
+
if (hit) return hit;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function resolveExport(file: string, exportName: string, seen: Set<string>): FxCalleeResolution {
|
|
145
|
+
const key = `${file}#${exportName}`;
|
|
146
|
+
if (seen.has(key)) return { kind: "unresolved", label: exportName };
|
|
147
|
+
seen.add(key);
|
|
148
|
+
const indexed = byFile.get(file);
|
|
149
|
+
if (!indexed) return { kind: "unresolved", label: exportName };
|
|
150
|
+
const exported = indexed.exports.get(exportName);
|
|
151
|
+
if (!exported) {
|
|
152
|
+
const fn = indexed.functions.get(exportName);
|
|
153
|
+
if (fn) return { kind: "function", fn, file };
|
|
154
|
+
return { kind: "unresolved", label: exportName };
|
|
155
|
+
}
|
|
156
|
+
if (exported.kind === "local") return resolveBinding(file, exported.binding, seen);
|
|
157
|
+
const target = resolveModule(file, exported.from);
|
|
158
|
+
if (!target) return { kind: "unresolved", label: exportName };
|
|
159
|
+
return resolveExport(target, exported.imported, seen);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function resolveBinding(file: string, name: string, seen: Set<string>): FxCalleeResolution {
|
|
163
|
+
const key = `${file}#local:${name}`;
|
|
164
|
+
if (seen.has(key)) return { kind: "unresolved", label: name };
|
|
165
|
+
seen.add(key);
|
|
166
|
+
const indexed = byFile.get(file);
|
|
167
|
+
if (!indexed) return { kind: "unresolved", label: name };
|
|
168
|
+
const fn = indexed.functions.get(name);
|
|
169
|
+
if (fn) return { kind: "function", fn, file };
|
|
170
|
+
const imported = indexed.imports.get(name);
|
|
171
|
+
if (!imported) return { kind: "unresolved", label: name };
|
|
172
|
+
if (imported.kind === "intrinsic") return { kind: "intrinsic", name: imported.name };
|
|
173
|
+
if (imported.kind === "namespace") return { kind: "unresolved", label: name };
|
|
174
|
+
const target = resolveModule(file, imported.from);
|
|
175
|
+
if (!target) return { kind: "unresolved", label: name };
|
|
176
|
+
return resolveExport(target, imported.imported, seen);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
resolve(file, callee) {
|
|
181
|
+
const from = normalizePosix(file);
|
|
182
|
+
const seen = new Set<string>();
|
|
183
|
+
if (callee.type === "Identifier") {
|
|
184
|
+
const name = identifierName(callee) ?? "callee";
|
|
185
|
+
return resolveBinding(from, name, seen);
|
|
186
|
+
}
|
|
187
|
+
if (callee.type === "MemberExpression") {
|
|
188
|
+
const member = callee as AstNode & {
|
|
189
|
+
object: AstNode;
|
|
190
|
+
property: AstNode;
|
|
191
|
+
computed?: boolean;
|
|
192
|
+
};
|
|
193
|
+
if (member.computed === true) return { kind: "unresolved", label: "callee" };
|
|
194
|
+
const objectName = identifierName(member.object);
|
|
195
|
+
const prop = identifierName(member.property);
|
|
196
|
+
if (!objectName || !prop) return { kind: "unresolved", label: "callee" };
|
|
197
|
+
const indexed = byFile.get(from);
|
|
198
|
+
const imported = indexed?.imports.get(objectName);
|
|
199
|
+
if (!imported || imported.kind !== "namespace") {
|
|
200
|
+
return { kind: "unresolved", label: prop };
|
|
201
|
+
}
|
|
202
|
+
const target = resolveModule(from, imported.from);
|
|
203
|
+
if (!target) return { kind: "unresolved", label: prop };
|
|
204
|
+
return resolveExport(target, prop, seen);
|
|
205
|
+
}
|
|
206
|
+
return { kind: "unresolved", label: "callee" };
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function indexFile(program: AstNode): FileFx {
|
|
212
|
+
const functions = new Map<string, AstNode>();
|
|
213
|
+
const imports = new Map<string, ImportBinding>();
|
|
214
|
+
const exports = new Map<string, LocalExport | Reexport>();
|
|
215
|
+
const body = (program as AstNode & { body?: AstNode[] }).body ?? [];
|
|
216
|
+
for (const stmt of body) indexStmt(stmt, functions, imports, exports);
|
|
217
|
+
return { functions, imports, exports };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function indexStmt(
|
|
221
|
+
stmt: AstNode,
|
|
222
|
+
functions: Map<string, AstNode>,
|
|
223
|
+
imports: Map<string, ImportBinding>,
|
|
224
|
+
exports: Map<string, LocalExport | Reexport>,
|
|
225
|
+
): void {
|
|
226
|
+
if (stmt.type === "ImportDeclaration") {
|
|
227
|
+
indexImport(stmt, imports);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (stmt.type === "FunctionDeclaration") {
|
|
231
|
+
rememberFunction(stmt, functions, exports, true);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (stmt.type === "VariableDeclaration") {
|
|
235
|
+
rememberVars(stmt, functions, exports, false);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (stmt.type === "ExportDefaultDeclaration") {
|
|
239
|
+
const decl = (stmt as AstNode & { declaration?: AstNode }).declaration;
|
|
240
|
+
if (decl && isFunctionNode(decl)) {
|
|
241
|
+
functions.set("default", decl);
|
|
242
|
+
exports.set("default", { kind: "local", binding: "default" });
|
|
243
|
+
}
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (stmt.type !== "ExportNamedDeclaration") return;
|
|
247
|
+
const decl = (stmt as AstNode & { declaration?: AstNode | null }).declaration;
|
|
248
|
+
const source = stringArg((stmt as AstNode & { source?: AstNode }).source);
|
|
249
|
+
if (decl?.type === "FunctionDeclaration") rememberFunction(decl, functions, exports, true);
|
|
250
|
+
if (decl?.type === "VariableDeclaration") rememberVars(decl, functions, exports, true);
|
|
251
|
+
const specifiers = (stmt as AstNode & { specifiers?: AstNode[] }).specifiers ?? [];
|
|
252
|
+
for (const spec of specifiers) {
|
|
253
|
+
const local = exportedLocalName(spec);
|
|
254
|
+
const exported = exportedPublicName(spec) ?? local;
|
|
255
|
+
if (!local || !exported) continue;
|
|
256
|
+
if (source) {
|
|
257
|
+
exports.set(exported, { kind: "reexport", from: source, imported: local });
|
|
258
|
+
} else {
|
|
259
|
+
exports.set(exported, { kind: "local", binding: local });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function indexImport(stmt: AstNode, imports: Map<string, ImportBinding>): void {
|
|
265
|
+
const source = stringArg((stmt as AstNode & { source?: AstNode }).source);
|
|
266
|
+
if (!source) return;
|
|
267
|
+
const specifiers = (stmt as AstNode & { specifiers?: AstNode[] }).specifiers ?? [];
|
|
268
|
+
const intrinsics = INTRINSIC_SOURCES[source];
|
|
269
|
+
for (const spec of specifiers) {
|
|
270
|
+
if (spec.type === "ImportNamespaceSpecifier") {
|
|
271
|
+
const local = identifierName((spec as AstNode & { local?: AstNode }).local);
|
|
272
|
+
if (local) imports.set(local, { kind: "namespace", from: source });
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (spec.type === "ImportDefaultSpecifier") {
|
|
276
|
+
const local = identifierName((spec as AstNode & { local?: AstNode }).local);
|
|
277
|
+
if (local) imports.set(local, { kind: "named", from: source, imported: "default" });
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (spec.type !== "ImportSpecifier") continue;
|
|
281
|
+
const local = identifierName((spec as AstNode & { local?: AstNode }).local);
|
|
282
|
+
const imported =
|
|
283
|
+
identifierName((spec as AstNode & { imported?: AstNode }).imported) ??
|
|
284
|
+
stringArg((spec as AstNode & { imported?: AstNode }).imported) ??
|
|
285
|
+
local;
|
|
286
|
+
if (!local || !imported) continue;
|
|
287
|
+
const intrinsic = intrinsics?.[imported];
|
|
288
|
+
if (intrinsic) {
|
|
289
|
+
imports.set(local, { kind: "intrinsic", name: intrinsic });
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
imports.set(local, { kind: "named", from: source, imported });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function rememberFunction(
|
|
297
|
+
fn: AstNode,
|
|
298
|
+
functions: Map<string, AstNode>,
|
|
299
|
+
exports: Map<string, LocalExport | Reexport>,
|
|
300
|
+
exportIt: boolean,
|
|
301
|
+
): void {
|
|
302
|
+
const name = identifierName((fn as AstNode & { id?: AstNode }).id);
|
|
303
|
+
if (!name) return;
|
|
304
|
+
functions.set(name, fn);
|
|
305
|
+
if (exportIt) exports.set(name, { kind: "local", binding: name });
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function rememberVars(
|
|
309
|
+
decl: AstNode,
|
|
310
|
+
functions: Map<string, AstNode>,
|
|
311
|
+
exports: Map<string, LocalExport | Reexport>,
|
|
312
|
+
exportIt: boolean,
|
|
313
|
+
): void {
|
|
314
|
+
const declarations = (decl as AstNode & { declarations?: AstNode[] }).declarations ?? [];
|
|
315
|
+
for (const item of declarations) {
|
|
316
|
+
const name = identifierName((item as AstNode & { id?: AstNode }).id);
|
|
317
|
+
if (!name) continue;
|
|
318
|
+
const init = (item as AstNode & { init?: AstNode | null }).init;
|
|
319
|
+
if (init && isFunctionNode(unwrapValue(init))) functions.set(name, unwrapValue(init));
|
|
320
|
+
if (exportIt) exports.set(name, { kind: "local", binding: name });
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function exportedLocalName(spec: AstNode): string | undefined {
|
|
325
|
+
return (
|
|
326
|
+
identifierName((spec as AstNode & { local?: AstNode }).local) ??
|
|
327
|
+
stringArg((spec as AstNode & { local?: AstNode }).local)
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function exportedPublicName(spec: AstNode): string | undefined {
|
|
332
|
+
return (
|
|
333
|
+
identifierName((spec as AstNode & { exported?: AstNode }).exported) ??
|
|
334
|
+
stringArg((spec as AstNode & { exported?: AstNode }).exported)
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function isFunctionNode(node: AstNode): boolean {
|
|
339
|
+
return (
|
|
340
|
+
node.type === "ArrowFunctionExpression" ||
|
|
341
|
+
node.type === "FunctionExpression" ||
|
|
342
|
+
node.type === "FunctionDeclaration"
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function unwrapValue(node: AstNode): AstNode {
|
|
347
|
+
let current = node;
|
|
348
|
+
for (let i = 0; i < 8; i++) {
|
|
349
|
+
if (
|
|
350
|
+
current.type === "ParenthesizedExpression" ||
|
|
351
|
+
current.type === "TSAsExpression" ||
|
|
352
|
+
current.type === "TSTypeAssertion" ||
|
|
353
|
+
current.type === "TSSatisfiesExpression" ||
|
|
354
|
+
current.type === "TSNonNullExpression" ||
|
|
355
|
+
current.type === "AwaitExpression"
|
|
356
|
+
) {
|
|
357
|
+
const inner = (current as AstNode & { expression?: AstNode; argument?: AstNode }).expression;
|
|
358
|
+
const arg = (current as AstNode & { argument?: AstNode }).argument;
|
|
359
|
+
const next = inner ?? arg;
|
|
360
|
+
if (!next) break;
|
|
361
|
+
current = next;
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
return current;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function matchFile(path: string, known: ReadonlySet<string>): string | undefined {
|
|
370
|
+
const stripped = path.replace(/\.(ts|tsx|mts|cts)$/, "");
|
|
371
|
+
const candidates = [
|
|
372
|
+
path,
|
|
373
|
+
stripped,
|
|
374
|
+
`${stripped}.ts`,
|
|
375
|
+
`${stripped}.tsx`,
|
|
376
|
+
`${stripped}.mts`,
|
|
377
|
+
`${stripped}/index.ts`,
|
|
378
|
+
`${stripped}/index.tsx`,
|
|
379
|
+
];
|
|
380
|
+
for (const candidate of candidates) {
|
|
381
|
+
if (known.has(candidate)) return candidate;
|
|
382
|
+
}
|
|
383
|
+
return undefined;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function normalizePosix(path: string): string {
|
|
387
|
+
const parts: string[] = [];
|
|
388
|
+
for (const part of path.replaceAll("\\", "/").split("/")) {
|
|
389
|
+
if (part.length === 0 || part === ".") continue;
|
|
390
|
+
if (part === "..") parts.pop();
|
|
391
|
+
else parts.push(part);
|
|
392
|
+
}
|
|
393
|
+
return parts.join("/");
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function posixDir(file: string): string {
|
|
397
|
+
const slash = file.lastIndexOf("/");
|
|
398
|
+
return slash < 0 ? "" : file.slice(0, slash);
|
|
399
|
+
}
|
package/src/compiler/response.ts
CHANGED
|
@@ -70,6 +70,11 @@ export function encodeFailure(failure: FlowFailure): Response {
|
|
|
70
70
|
});
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/** SSE id reader. Kept off the edge bundle until a stream chunk is encoded. */
|
|
74
|
+
function loadSseId(): typeof import("../kernel/sse-id.ts") {
|
|
75
|
+
return lazyRequire(`${import.meta.dir}/../kernel`, ["sse", "id"].join("-"));
|
|
76
|
+
}
|
|
77
|
+
|
|
73
78
|
function loadFxLiveStream(): typeof import("../kernel/fx-live-stream.ts") {
|
|
74
79
|
return lazyRequire(`${import.meta.dir}/../kernel`, ["fx", "live", "stream"].join("-"));
|
|
75
80
|
}
|
|
@@ -133,6 +138,24 @@ export async function encodeExecuteResult(result: {
|
|
|
133
138
|
return encodeSuccess(result.output);
|
|
134
139
|
}
|
|
135
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Durable park thrown from a streamed agent after the interrupt frame.
|
|
143
|
+
*
|
|
144
|
+
* Duck-typed so this encoder does not import the journal module.
|
|
145
|
+
*
|
|
146
|
+
* @param err - Value thrown by the chunk iterator
|
|
147
|
+
*/
|
|
148
|
+
function isStreamPark(err: unknown): err is { readonly wakeAt: number; readonly label: string } {
|
|
149
|
+
return (
|
|
150
|
+
err instanceof Error &&
|
|
151
|
+
err.name === "JournalSuspend" &&
|
|
152
|
+
"wakeAt" in err &&
|
|
153
|
+
typeof err.wakeAt === "number" &&
|
|
154
|
+
"label" in err &&
|
|
155
|
+
typeof err.label === "string"
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
136
159
|
function encodeSseStream(carrier: JsonStreamResult): Response {
|
|
137
160
|
const encoder = new TextEncoder();
|
|
138
161
|
let finalized = false;
|
|
@@ -147,8 +170,12 @@ function encodeSseStream(carrier: JsonStreamResult): Response {
|
|
|
147
170
|
for await (const chunk of carrier.chunks) {
|
|
148
171
|
const frame = loadFxJson().isSseFrame(chunk)
|
|
149
172
|
? chunk
|
|
150
|
-
: { data: chunk as unknown, id:
|
|
173
|
+
: { data: chunk as unknown, id: loadSseId().readSseId(chunk) };
|
|
151
174
|
const lines: string[] = [];
|
|
175
|
+
if ("comment" in frame && frame.comment && frame.data === undefined) {
|
|
176
|
+
controller.enqueue(encoder.encode(`: ${frame.comment}\n\n`));
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
152
179
|
if (frame.id !== undefined) lines.push(`id: ${frame.id}`);
|
|
153
180
|
lines.push(`data: ${JSON.stringify(frame.data)}`);
|
|
154
181
|
controller.enqueue(encoder.encode(`${lines.join("\n")}\n\n`));
|
|
@@ -156,7 +183,13 @@ function encodeSseStream(carrier: JsonStreamResult): Response {
|
|
|
156
183
|
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
157
184
|
controller.close();
|
|
158
185
|
} catch (err) {
|
|
159
|
-
|
|
186
|
+
if (isStreamPark(err)) {
|
|
187
|
+
carrier.parked = { wakeAt: err.wakeAt, label: err.label };
|
|
188
|
+
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
189
|
+
controller.close();
|
|
190
|
+
} else {
|
|
191
|
+
controller.error(err);
|
|
192
|
+
}
|
|
160
193
|
} finally {
|
|
161
194
|
await finish();
|
|
162
195
|
}
|
package/src/config/index.ts
CHANGED
|
@@ -508,12 +508,30 @@ export function resolveDriverId(
|
|
|
508
508
|
env: ConfigEnv,
|
|
509
509
|
defaults?: EnvDriverMap,
|
|
510
510
|
): string | undefined {
|
|
511
|
+
return driverRefId(resolveDriverRef(map, env, defaults));
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Resolve the rich driver ref for an env (`pool`, `replicas`, `url`).
|
|
516
|
+
*
|
|
517
|
+
* Same merge rules as {@link resolveDriverId}. A string pin has no pool or
|
|
518
|
+
* replicas. With `defaults`, an unset env key keeps that driver's default
|
|
519
|
+
* pin rather than a sibling environment.
|
|
520
|
+
*
|
|
521
|
+
* @param map - Env → driver map (or bare ref)
|
|
522
|
+
* @param env - Active environment
|
|
523
|
+
* @param defaults - Real default map for this specific driver (per-key merge)
|
|
524
|
+
*/
|
|
525
|
+
export function resolveDriverRef(
|
|
526
|
+
map: EnvDriverInput | undefined,
|
|
527
|
+
env: ConfigEnv,
|
|
528
|
+
defaults?: EnvDriverMap,
|
|
529
|
+
): DriverRef | undefined {
|
|
511
530
|
const normalized = normalizeEnvDriverMap(map);
|
|
512
531
|
if (defaults) {
|
|
513
532
|
const merged = mergeEnvDriverMap(normalized, defaults);
|
|
514
|
-
return
|
|
533
|
+
return merged[env];
|
|
515
534
|
}
|
|
516
535
|
if (!normalized) return undefined;
|
|
517
|
-
|
|
518
|
-
return driverRefId(ref);
|
|
536
|
+
return normalized[env] ?? normalized.dev ?? normalized.prod ?? normalized.test;
|
|
519
537
|
}
|