okengine 0.19.0 → 0.19.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/site/content/docs/ai/meta.json +1 -1
- package/site/content/docs/ai/skills.mdx +1 -1
- package/site/content/docs/client/index.mdx +1 -2
- package/site/content/docs/elements/ai/agents.mdx +5 -2
- package/site/content/docs/elements/ai/index.mdx +4 -3
- package/site/content/docs/elements/ai/prompts.mdx +3 -2
- package/site/content/docs/elements/channel/email.mdx +5 -2
- package/site/content/docs/elements/channel/index.mdx +1 -1
- package/site/content/docs/elements/clock/index.mdx +23 -12
- package/site/content/docs/elements/clock/schedules.mdx +6 -3
- package/site/content/docs/elements/clock/sleep.mdx +10 -7
- package/site/content/docs/elements/flow/consumers.mdx +96 -58
- package/site/content/docs/elements/flow/index.mdx +15 -15
- package/site/content/docs/elements/flow/routing.mdx +11 -2
- package/site/content/docs/elements/gate/tenancy.mdx +5 -2
- package/site/content/docs/elements/signal/broadcast.mdx +6 -4
- package/site/content/docs/elements/signal/index.mdx +14 -1
- package/site/content/docs/elements/signal/live.mdx +3 -2
- package/site/content/docs/elements/signal/once.mdx +3 -2
- package/site/content/docs/elements/store/files.mdx +24 -16
- package/site/content/docs/elements/store/index.mdx +11 -8
- package/site/content/docs/elements/store/kv.mdx +24 -16
- package/site/content/docs/elements/store/search.mdx +18 -14
- package/site/content/docs/elements/store/sql.mdx +16 -12
- package/site/content/docs/elements/vault/config.mdx +5 -2
- package/site/content/docs/elements/vault/rotation.mdx +5 -2
- package/site/content/docs/elements/vault/secrets.mdx +5 -2
- package/site/content/docs/index.mdx +4 -19
- package/site/content/docs/plugins/anonymous.mdx +1 -1
- package/site/content/docs/plugins/cors.mdx +1 -1
- package/site/content/docs/plugins/csrf.mdx +1 -1
- package/site/content/docs/plugins/headers.mdx +1 -1
- package/site/content/docs/plugins/ip-allowlist.mdx +1 -1
- package/site/content/docs/plugins/maintenance-mode.mdx +1 -1
- package/site/content/docs/reference/cli.mdx +1 -1
- package/site/content/docs/reference/errors.mdx +1 -0
- package/site/content/docs/reference/fx.mdx +9 -8
- package/site/content/docs/reference/okid.mdx +1 -1
- package/site/content/docs/reference/plugins.mdx +1 -1
- package/site/content/docs/reference/security.mdx +2 -2
- package/site/content/docs/understand/meta.json +1 -1
- package/site/content/docs/understand/the-architecture.mdx +264 -0
- package/src/bench/REPORT.md +48 -17
- package/src/bench/g17-hybrid-search.bench.ts +13 -13
- package/src/compiler/extract.test.ts +76 -1
- package/src/compiler/extract.ts +106 -7
- package/src/compiler/search-writer-isolation.test.ts +0 -1
- package/src/console/ui-next/dist/assets/{access-page-DFeymU07.js → access-page-DceEWH9u.js} +1 -1
- package/src/console/ui-next/dist/assets/{agent-disclosure-U1rdfblp.js → agent-disclosure-tC9s2VFd.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-BeFJeqBG.js → cache-glyph-C-naNQSR.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-CVAONPii.js → call-pii-button-DnZ_MlDn.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-D2A6NJ-3.js → collapsible-RekgR6Qz.js} +1 -1
- package/src/console/ui-next/dist/assets/{duration-tone-Cgk_h5ja.js → duration-tone-DugtWBS0.js} +1 -1
- package/src/console/ui-next/dist/assets/flows-page-DVmn1ZuQ.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-MYZQtRnw.js → highlighted-json-CvBGaiSD.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-Jrh39p7A.js → http-method-D7_OXbdC.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-DXP2dBIF.js → index-DH0K2f6N.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-HvolXxTI.js → observability-page-qJzF2nSN.js} +1 -1
- package/src/console/ui-next/dist/assets/{replica-lag-CSh2dzrb.js → replica-lag-Vqk0pUBA.js} +1 -1
- package/src/console/ui-next/dist/assets/request-meta-BtShi4sG.js +1 -0
- package/src/console/ui-next/dist/assets/{store-page-eiKiHnNe.js → store-page-CMsYH_vH.js} +1 -1
- package/src/console/ui-next/dist/assets/{trace-detail-sheet-CZkMeKS-.js → trace-detail-sheet-ycFB2uua.js} +1 -1
- package/src/console/ui-next/dist/assets/{tree-expand-toggle-CW8y5A2h.js → tree-expand-toggle-iG1jcWgU.js} +1 -1
- package/src/console/ui-next/dist/assets/{units-page-B_RWJrEO.js → units-page-Bavchke4.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-CWrg-A68.js → vault-page-DxUFhiZI.js} +1 -1
- package/src/console/ui-next/dist/index.html +1 -1
- package/src/console/ui-next/src/features/flows/graph/element-map.test.ts +1 -1
- package/src/console/ui-next/src/features/flows/graph/element-map.ts +3 -3
- package/src/console/ui-next/src/features/flows/traces/trace-detail.test.ts +4 -5
- package/src/console/ui-next/src/features/flows/traces/trace-gates.ts +3 -6
- package/src/elements/gate/declare.ts +1 -1
- package/src/elements/gate.ts +1 -1
- package/src/elements/store/live-default.test.ts +8 -0
- package/src/elements/store/search-embed-flow.ts +2 -2
- package/src/elements/store/search-lsh.ts +35 -0
- package/src/elements/store/search-runtime.pglite.test.ts +186 -0
- package/src/elements/store/search-runtime.ts +30 -16
- package/src/elements/store/search.test.ts +83 -1
- package/src/elements/store.ts +2 -0
- package/src/full.ts +3 -0
- package/src/http.ts +3 -0
- package/src/index.ts +3 -0
- package/src/kernel/app.ts +30 -10
- package/src/kernel/boot.ts +1 -1
- package/src/kernel/cdc-payload.test.ts +224 -0
- package/src/kernel/cdc-payload.ts +146 -0
- package/src/kernel/errors-flow-name.ts +17 -0
- package/src/kernel/errors.registry.test.ts +6 -4
- package/src/kernel/flow-name.test.ts +104 -0
- package/src/kernel/flow.ts +3 -2
- package/src/kernel/fx-emit-types.test.ts +31 -0
- package/src/kernel/fx.test.ts +2 -1
- package/src/kernel/fx.ts +13 -3
- package/src/kernel/index.ts +2 -0
- package/src/kernel/on.ts +5 -0
- package/src/kernel/stamp-http.test.ts +13 -0
- package/src/kernel/stamp-http.ts +21 -3
- package/src/kernel/unit.ts +4 -2
- package/src/kernel-entry.ts +3 -0
- package/src/mcp/docs-index.ts +3 -3
- package/src/mcp/docs-mcp.test.ts +2 -2
- package/src/mcp/docs-tools.ts +2 -1
- package/site/content/docs/understand/the-anatomy.mdx +0 -132
- package/site/content/docs/understand/the-model.mdx +0 -32
- package/site/content/docs/understand/the-problem.mdx +0 -74
- package/site/content/docs/understand/the-vocabulary.mdx +0 -26
- package/src/console/ui-next/dist/assets/flows-page-Dss7941e.js +0 -1
- package/src/console/ui-next/dist/assets/request-meta-BatF8KrK.js +0 -1
- /package/site/content/docs/{ai → understand}/try-it.mdx +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "okengine",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.2",
|
|
4
4
|
"description": "One law. Eight elements. One contract. The backend model stays small; operational surfaces are derived from it instead of maintained separately.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -46,7 +46,7 @@ Contracts teach the _vocabulary_; [MCP](/docs/ai/mcp) grants the _hands_. An age
|
|
|
46
46
|
## Learn more
|
|
47
47
|
|
|
48
48
|
- [MCP](/docs/ai/mcp) — the runtime surface these contracts pair with
|
|
49
|
-
- [The
|
|
49
|
+
- [The Architecture](/docs/understand/the-architecture) — the eight elements in human terms
|
|
50
50
|
- [llms.txt](/docs/ai/llms-txt) — `/llms.txt`, `/llms.json`, `/llms/agents`, per-page markdown
|
|
51
51
|
|
|
52
52
|
## Next
|
|
@@ -184,8 +184,7 @@ dev so `createClient("")` stays same-origin behind the Vite proxy.
|
|
|
184
184
|
|
|
185
185
|
- [Routing](/docs/elements/flow/routing) — folders are the URL; `$routes` without `.adopt()`
|
|
186
186
|
- [HTTP](/docs/elements/flow/http) — verbs, `http.resource`, live SSE
|
|
187
|
-
- [The
|
|
188
|
-
- [The Model](/docs/understand/the-model) — same contract for Client, Console, MCP
|
|
187
|
+
- [The Architecture](/docs/understand/the-architecture) — generated barrel → client → test loop; same contract for Client, Console, MCP
|
|
189
188
|
- [Errors](/docs/reference/errors) — framework codes vs failure values
|
|
190
189
|
|
|
191
190
|
## Next
|
|
@@ -54,9 +54,12 @@ import { supportAgent } from "@/core/ai";
|
|
|
54
54
|
import { member } from "@/core/gate";
|
|
55
55
|
|
|
56
56
|
export const assist = on(
|
|
57
|
-
http
|
|
57
|
+
http
|
|
58
|
+
.post({
|
|
59
|
+
in: z.object({ query: z.string().min(1) }),
|
|
60
|
+
})
|
|
61
|
+
.gate(member),
|
|
58
62
|
flow({
|
|
59
|
-
in: z.object({ query: z.string().min(1) }),
|
|
60
63
|
do: async ({ query }, fx) => {
|
|
61
64
|
return await fx.run(supportAgent, {
|
|
62
65
|
message: `Help the member: ${query}`,
|
|
@@ -60,9 +60,10 @@ import { z } from "zod";
|
|
|
60
60
|
import { triage } from "@/core/ai";
|
|
61
61
|
|
|
62
62
|
export const classify = on(
|
|
63
|
-
http.post(
|
|
64
|
-
flow({
|
|
63
|
+
http.post({
|
|
65
64
|
in: z.object({ message: z.string().min(1) }),
|
|
65
|
+
}),
|
|
66
|
+
flow({
|
|
66
67
|
do: async ({ message }, fx) => {
|
|
67
68
|
return await fx.ask(triage, { message });
|
|
68
69
|
},
|
|
@@ -336,6 +337,6 @@ Compose does not pin inference — BYO URL + keys, or [OpenRouter](/docs/recipes
|
|
|
336
337
|
<Card
|
|
337
338
|
title="The Model"
|
|
338
339
|
description="Eight elements overview."
|
|
339
|
-
href="/docs/understand/the-
|
|
340
|
+
href="/docs/understand/the-architecture"
|
|
340
341
|
/>
|
|
341
342
|
</Cards>
|
|
@@ -56,9 +56,10 @@ import { z } from "zod";
|
|
|
56
56
|
import { classifyTicket } from "@/core/ai";
|
|
57
57
|
|
|
58
58
|
export const classify = on(
|
|
59
|
-
http.post(
|
|
60
|
-
flow({
|
|
59
|
+
http.post({
|
|
61
60
|
in: z.object({ message: z.string().min(1) }),
|
|
61
|
+
}),
|
|
62
|
+
flow({
|
|
62
63
|
do: async ({ message }, fx) => {
|
|
63
64
|
return await fx.ask(classifyTicket, { message });
|
|
64
65
|
},
|
|
@@ -66,9 +66,12 @@ import { z } from "zod";
|
|
|
66
66
|
import { passwordReset } from "@/core/channel";
|
|
67
67
|
|
|
68
68
|
export const reset = on(
|
|
69
|
-
http
|
|
69
|
+
http
|
|
70
|
+
.post({
|
|
71
|
+
in: z.object({ email: z.string().email() }),
|
|
72
|
+
})
|
|
73
|
+
.public(),
|
|
70
74
|
flow({
|
|
71
|
-
in: z.object({ email: z.string().email() }),
|
|
72
75
|
do: async ({ email }, fx) => {
|
|
73
76
|
await fx.send(passwordReset, {
|
|
74
77
|
to: email,
|
|
@@ -339,6 +339,6 @@ Env knobs: [Environment variables](/docs/reference/environment-variables). Local
|
|
|
339
339
|
<Card
|
|
340
340
|
title="The Model"
|
|
341
341
|
description="Eight elements overview."
|
|
342
|
-
href="/docs/understand/the-
|
|
342
|
+
href="/docs/understand/the-architecture"
|
|
343
343
|
/>
|
|
344
344
|
</Cards>
|
|
@@ -26,7 +26,7 @@ For developers scheduling work on okengine — one handle shape; drivers swap by
|
|
|
26
26
|
```typescript title="src/clocks/digest.ts"
|
|
27
27
|
import { clock } from "okengine";
|
|
28
28
|
|
|
29
|
-
export const digestClock = clock("notes.digest",
|
|
29
|
+
export const digestClock = clock.every("notes.digest", "1d");
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
</Step>
|
|
@@ -73,6 +73,16 @@ Under `drivers.clock.test = "frozen"`, advance time in tests instead of waiting
|
|
|
73
73
|
separate job runner — see [Consumers · Clock Jobs](/docs/elements/flow/consumers#clock-jobs).
|
|
74
74
|
</Callout>
|
|
75
75
|
|
|
76
|
+
## Inline or named export
|
|
77
|
+
|
|
78
|
+
| Style | When |
|
|
79
|
+
| --------------------------------------------- | ----------------------------------------------------------------- |
|
|
80
|
+
| `on(clock.every("name", "1h"), flow({ do }))` | Self-contained — nothing else needs the Clock handle |
|
|
81
|
+
| `export const x = clock.every("name", "1h")` | Another file (or a second `on()`) must reuse the same declaration |
|
|
82
|
+
|
|
83
|
+
Nameless `flow({ do })` inherits the Clock's declared name. File-tree `unit.export` and explicit
|
|
84
|
+
`flow("…")` win. Two nameless Flows inheriting the same name fail **OKE1070**.
|
|
85
|
+
|
|
76
86
|
## Progressive Patterns
|
|
77
87
|
|
|
78
88
|
Same `clock` + `fx.clock` from a calendar cron to an interval, a durable pause, and typed offsets:
|
|
@@ -114,7 +124,7 @@ Human durations — integer + unit, no weeks: `"200ms"` · `"30s"` · `"5m"` ·
|
|
|
114
124
|
```typescript title="src/flows/health/ping.ts"
|
|
115
125
|
import { on, flow, clock } from "okengine";
|
|
116
126
|
|
|
117
|
-
export const pingClock = clock("health.pingExternal",
|
|
127
|
+
export const pingClock = clock.every("health.pingExternal", "30s");
|
|
118
128
|
|
|
119
129
|
export const pingExternal = on(
|
|
120
130
|
pingClock,
|
|
@@ -140,10 +150,11 @@ import { on, flow, http } from "okengine";
|
|
|
140
150
|
import { z } from "zod";
|
|
141
151
|
|
|
142
152
|
export const start = on(
|
|
143
|
-
http.post(
|
|
153
|
+
http.post({
|
|
154
|
+
in: z.object({ email: z.string().email() }),
|
|
155
|
+
}),
|
|
144
156
|
flow({
|
|
145
157
|
durable: true,
|
|
146
|
-
in: z.object({ email: z.string().email() }),
|
|
147
158
|
do: async ({ email }, fx) => {
|
|
148
159
|
await fx.step("mark-trial", async () => {
|
|
149
160
|
await fx.call(startTrial, { email });
|
|
@@ -182,14 +193,14 @@ calendar day. Unknown strings parse as `0`.
|
|
|
182
193
|
|
|
183
194
|
## Capability Reference
|
|
184
195
|
|
|
185
|
-
| Surface | Signature | Purpose
|
|
186
|
-
| ------------- | -------------------------------------------------------- |
|
|
187
|
-
|
|
|
188
|
-
|
|
|
189
|
-
|
|
|
190
|
-
|
|
|
191
|
-
| Now / offsets | `fx.clock.now` · `ago` · `fromNow` · `duration` | Injectable time math
|
|
192
|
-
| Durable sleep | `fx.clock.sleep(label, duration)` | Park a durable Flow until wake
|
|
196
|
+
| Surface | Signature | Purpose | `do` input |
|
|
197
|
+
| ------------- | -------------------------------------------------------- | ------------------------------ | ---------- |
|
|
198
|
+
| Helpers | `clock.daily` · `hourly` · `weekly` · `monthly` · `cron` | Calendar presets + field bags | none (`_`) |
|
|
199
|
+
| Interval | `clock.every(name, duration, opts?)` | Fixed duration loop | none (`_`) |
|
|
200
|
+
| Per-tenant | `clock.perTenant(name, opts)` | One Store row per tenant | none (`_`) |
|
|
201
|
+
| Bare callable | `clock(name, { cron? \| every?, … })` | Same decl; lower-level form | none (`_`) |
|
|
202
|
+
| Now / offsets | `fx.clock.now` · `ago` · `fromNow` · `duration` | Injectable time math | — |
|
|
203
|
+
| Durable sleep | `fx.clock.sleep(label, duration)` | Park a durable Flow until wake | — |
|
|
193
204
|
|
|
194
205
|
At least one of `cron` or `every` is required on every declaration.
|
|
195
206
|
|
|
@@ -99,8 +99,10 @@ export const close = clock.monthly("billing.close", { on: [1, 15], at: "00:00" }
|
|
|
99
99
|
export const ping = clock.every("health.ping", "30s");
|
|
100
100
|
```
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
Prefer the named helpers above for fixed schedules. The bare callable remains fully
|
|
103
|
+
supported for the same `ClockDecl` shape when you need a raw string or a schedule
|
|
104
|
+
chosen programmatically: `clock("x", { cron: "0 6 * * *" })`, Bun nicknames
|
|
105
|
+
(`@hourly`), and `clock("x", { every: "30s" })`.
|
|
104
106
|
|
|
105
107
|
**Consequence:** Manifest / Store still see `cron: "0 6 * * *"` — helpers are declare-time sugar.
|
|
106
108
|
|
|
@@ -159,7 +161,8 @@ export const runInvoices = on(
|
|
|
159
161
|
);
|
|
160
162
|
```
|
|
161
163
|
|
|
162
|
-
Equivalent
|
|
164
|
+
Equivalent bare form (same decl; prefer `clock.perTenant` above):
|
|
165
|
+
`clock("invoices", { every: "1h", perTenant: true })`.
|
|
163
166
|
|
|
164
167
|
**Consequence:** ten tenants → ten leader-elected rows. Catch-up still fires **once per
|
|
165
168
|
row** after downtime — not a burst of missed hours per tenant.
|
|
@@ -30,10 +30,11 @@ import { on, flow, http } from "okengine";
|
|
|
30
30
|
import { z } from "zod";
|
|
31
31
|
|
|
32
32
|
export const start = on(
|
|
33
|
-
http.post(
|
|
33
|
+
http.post({
|
|
34
|
+
in: z.object({ email: z.string().email() }),
|
|
35
|
+
}),
|
|
34
36
|
flow({
|
|
35
37
|
durable: true,
|
|
36
|
-
in: z.object({ email: z.string().email() }),
|
|
37
38
|
do: async ({ email }, fx) => {
|
|
38
39
|
await fx.step("mark-trial", async () => {
|
|
39
40
|
await fx.call(startTrial, { email });
|
|
@@ -101,10 +102,11 @@ import { on, flow, http } from "okengine";
|
|
|
101
102
|
import { z } from "zod";
|
|
102
103
|
|
|
103
104
|
export const create = on(
|
|
104
|
-
http.post(
|
|
105
|
+
http.post({
|
|
106
|
+
in: z.object({ email: z.string().email() }),
|
|
107
|
+
}),
|
|
105
108
|
flow({
|
|
106
109
|
durable: true,
|
|
107
|
-
in: z.object({ email: z.string().email() }),
|
|
108
110
|
do: async ({ email }, fx) => {
|
|
109
111
|
await fx.step("create", async () => {
|
|
110
112
|
await fx.call(createWorkspace, { email });
|
|
@@ -256,11 +258,12 @@ import { on, flow, http } from "okengine";
|
|
|
256
258
|
import { z } from "zod";
|
|
257
259
|
|
|
258
260
|
export const create = on(
|
|
259
|
-
http.post(
|
|
260
|
-
flow({
|
|
261
|
-
durable: true,
|
|
261
|
+
http.post({
|
|
262
262
|
in: z.object({ email: z.string().email() }),
|
|
263
263
|
out: z.object({ trialId: z.string() }),
|
|
264
|
+
}),
|
|
265
|
+
flow({
|
|
266
|
+
durable: true,
|
|
264
267
|
do: async ({ email }, fx) => {
|
|
265
268
|
const trialId = await fx.step("create", async () => {
|
|
266
269
|
return await fx.call(createTrial, { email });
|
|
@@ -11,8 +11,8 @@ For developers wiring background work on okengine — bind the trigger, keep `do
|
|
|
11
11
|
|
|
12
12
|
<Callout title="The one rule">
|
|
13
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
|
|
15
|
-
access still goes through `fx`.
|
|
14
|
+
the Signal; `cron` / `every` live on the Clock; CDC input is `{ before, after }` plus `table` /
|
|
15
|
+
`action` / `id`. World access still goes through `fx`.
|
|
16
16
|
</Callout>
|
|
17
17
|
|
|
18
18
|
## Smallest Example
|
|
@@ -108,7 +108,7 @@ import { on, flow, clock } from "okengine";
|
|
|
108
108
|
import { lt } from "drizzle-orm";
|
|
109
109
|
import { db, metricLogs } from "@/schema";
|
|
110
110
|
|
|
111
|
-
export const cleanupClock = clock("metrics.cleanup",
|
|
111
|
+
export const cleanupClock = clock.every("metrics.cleanup", "1h");
|
|
112
112
|
|
|
113
113
|
export const cleanupMetrics = on(
|
|
114
114
|
cleanupClock,
|
|
@@ -127,7 +127,8 @@ export const cleanupMetrics = on(
|
|
|
127
127
|
|
|
128
128
|
<Tab value="CDC">
|
|
129
129
|
|
|
130
|
-
`db.table(handle).changed()` fires on insert, update, and delete. Input is `{ before, after }
|
|
130
|
+
`db.table(handle).changed()` fires on insert, update, and delete. Input is `{ before, after }`
|
|
131
|
+
plus `table`, `action`, and `id`:
|
|
131
132
|
|
|
132
133
|
```typescript title="src/flows/audit/users.ts"
|
|
133
134
|
import { on, flow } from "okengine";
|
|
@@ -137,14 +138,15 @@ import { users, auditLogs } from "@/schema";
|
|
|
137
138
|
export const onUserWrite = on(
|
|
138
139
|
db.table(users).changed(),
|
|
139
140
|
flow("audit.users", {
|
|
140
|
-
do: async ({
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
141
|
+
do: async ({ table, action, id }, fx) => {
|
|
142
|
+
await fx
|
|
143
|
+
.store(db)
|
|
144
|
+
.insert(auditLogs)
|
|
145
|
+
.values({
|
|
146
|
+
table,
|
|
147
|
+
recordId: String(id),
|
|
148
|
+
action,
|
|
149
|
+
});
|
|
148
150
|
},
|
|
149
151
|
}),
|
|
150
152
|
);
|
|
@@ -183,16 +185,19 @@ Omit `key` for competing consumers with no ordering.
|
|
|
183
185
|
|
|
184
186
|
## Trigger Reference
|
|
185
187
|
|
|
186
|
-
| Trigger | Signature
|
|
187
|
-
| ---------- |
|
|
188
|
-
| Signal | `on(signalHandle, flow)`
|
|
189
|
-
| Clock | `on(clockDecl, flow)`
|
|
190
|
-
| CDC any | `on(db.table(t).changed(), flow)`
|
|
191
|
-
| CDC column | `on(db.table(t).changed("col"), flow)`
|
|
188
|
+
| Trigger | Signature | Purpose | `do` input |
|
|
189
|
+
| ---------- | --------------------------------------------------- | ------------------------------------------- | -------------------------------------- |
|
|
190
|
+
| Signal | `on(signalHandle, flow)` or inline `signal.once(…)` | Queue (`once`) or fan-out (`broadcast`) | Payload (`schema`) |
|
|
191
|
+
| Clock | `on(clockDecl, flow)` or inline `clock.every(…)` | Interval or cron tick | none (`_`) |
|
|
192
|
+
| CDC any | `on(db.table(t).changed(), flow)` | Every insert / update / delete | `{ before, after, table, action, id }` |
|
|
193
|
+
| CDC column | `on(db.table(t).changed("col"), flow)` | Same writes; column stamped on the Manifest | `{ before, after, table, action, id }` |
|
|
192
194
|
|
|
193
195
|
`signal.live` is an HTTP SSE tape — bind it with [`http.live`](/docs/elements/flow/http#live-streams),
|
|
194
196
|
not as a worker. A Flow with no trigger is [call-only](/docs/elements/flow).
|
|
195
197
|
|
|
198
|
+
Inline `on(signal.once("…"), flow({ do }))` is supported; export the handle for typed `fx.emit`.
|
|
199
|
+
Nameless `flow({ do })` inherits the trigger name; a second nameless inherit fails **OKE1070**.
|
|
200
|
+
|
|
196
201
|
## Signal Consumers
|
|
197
202
|
|
|
198
203
|
<Callout title="Detailed section">
|
|
@@ -342,9 +347,11 @@ Broadcast does not use the `once` lease / DLQ path. Live uses the retained tape,
|
|
|
342
347
|
## Clock Jobs
|
|
343
348
|
|
|
344
349
|
<Callout title="Detailed section">
|
|
345
|
-
|
|
346
|
-
—
|
|
347
|
-
`
|
|
350
|
+
Prefer `clock.every` / `clock.daily` / `clock.cron` (and the other named helpers) for fixed
|
|
351
|
+
schedules — they compile to the same decl as the bare callable. The lower-level bare `clock(name,
|
|
352
|
+
options)` form still works when you need a raw expression or a schedule chosen programmatically;
|
|
353
|
+
missing both `cron` and `every` throws `clock("name"): require cron or every`. Bind the returned
|
|
354
|
+
handle with `on(clockDecl, flow)`.
|
|
348
355
|
</Callout>
|
|
349
356
|
|
|
350
357
|
Named clocks reconcile into the Store at boot. The scheduler leader-elects so N instances do not
|
|
@@ -359,7 +366,7 @@ Human durations: `"200ms"` · `"30s"` · `"5m"` · `"1h"` · `"7d"` (integer + u
|
|
|
359
366
|
```typescript title="src/flows/health/ping.ts"
|
|
360
367
|
import { on, flow, clock } from "okengine";
|
|
361
368
|
|
|
362
|
-
export const pingClock = clock("health.pingExternal",
|
|
369
|
+
export const pingClock = clock.every("health.pingExternal", "30s");
|
|
363
370
|
|
|
364
371
|
export const pingExternal = on(
|
|
365
372
|
pingClock,
|
|
@@ -381,8 +388,8 @@ Five-field cron (`m h dom mon dow`) plus an IANA `timezone` (default `"UTC"`):
|
|
|
381
388
|
```typescript title="src/flows/reports/daily.ts"
|
|
382
389
|
import { on, flow, clock } from "okengine";
|
|
383
390
|
|
|
384
|
-
export const dailyReportClock = clock("reports.daily", {
|
|
385
|
-
|
|
391
|
+
export const dailyReportClock = clock.daily("reports.daily", {
|
|
392
|
+
at: "06:00",
|
|
386
393
|
timezone: "Asia/Riyadh",
|
|
387
394
|
});
|
|
388
395
|
|
|
@@ -423,7 +430,8 @@ export const runInvoices = on(
|
|
|
423
430
|
);
|
|
424
431
|
```
|
|
425
432
|
|
|
426
|
-
Equivalent
|
|
433
|
+
Equivalent bare form (same decl; prefer `clock.perTenant` above):
|
|
434
|
+
`clock("invoices", { every: "1h", perTenant: true })`.
|
|
427
435
|
|
|
428
436
|
</Tab>
|
|
429
437
|
|
|
@@ -487,13 +495,43 @@ do not fire.
|
|
|
487
495
|
</Callout>
|
|
488
496
|
|
|
489
497
|
SQL writes through `fx.store` notify CDC after commit. The Flow input is always
|
|
490
|
-
`{ before, after }` (`CdcPayload`).
|
|
498
|
+
`{ before, after }` plus `table`, `action`, and `id` (`CdcPayload`). Destructure
|
|
499
|
+
only `{ before, after }` when that's all you need.
|
|
500
|
+
|
|
501
|
+
<Tabs items={["Audit log", "Bare", "Images"]}>
|
|
502
|
+
|
|
503
|
+
<Tab value="Audit log">
|
|
504
|
+
|
|
505
|
+
`table` is the real table name. `action` is `"created"` / `"updated"` / `"deleted"`.
|
|
506
|
+
`id` is the declared primary-key value — not a hardcoded `"id"` column:
|
|
507
|
+
|
|
508
|
+
```typescript title="src/flows/audit/users.ts"
|
|
509
|
+
import { on, flow } from "okengine";
|
|
510
|
+
import { db } from "@/core";
|
|
511
|
+
import { users, auditLogs } from "@/schema";
|
|
512
|
+
|
|
513
|
+
export const onUserWrite = on(
|
|
514
|
+
db.table(users).changed(),
|
|
515
|
+
flow("audit.users", {
|
|
516
|
+
do: async ({ table, action, id, before, after }, fx) => {
|
|
517
|
+
await fx
|
|
518
|
+
.store(db)
|
|
519
|
+
.insert(auditLogs)
|
|
520
|
+
.values({
|
|
521
|
+
table,
|
|
522
|
+
recordId: String(id),
|
|
523
|
+
action,
|
|
524
|
+
});
|
|
525
|
+
},
|
|
526
|
+
}),
|
|
527
|
+
);
|
|
528
|
+
```
|
|
491
529
|
|
|
492
|
-
|
|
530
|
+
</Tab>
|
|
493
531
|
|
|
494
|
-
<Tab value="
|
|
532
|
+
<Tab value="Bare">
|
|
495
533
|
|
|
496
|
-
Omit
|
|
534
|
+
Omit `table` / `action` / `id` when you only need the row images:
|
|
497
535
|
|
|
498
536
|
```typescript title="src/flows/search/reindex.ts"
|
|
499
537
|
import { on, flow } from "okengine";
|
|
@@ -505,24 +543,18 @@ export const reindexNotes = on(
|
|
|
505
543
|
flow("search.reindexNotes", {
|
|
506
544
|
plane: "operator",
|
|
507
545
|
do: async ({ before, after }, fx) => {
|
|
508
|
-
const id = String(after?.id ?? before?.id ?? "");
|
|
509
|
-
if (!id) return;
|
|
510
546
|
if (!after) {
|
|
511
|
-
await fx.call(dropNoteIndex, { id });
|
|
547
|
+
await fx.call(dropNoteIndex, { id: String(before?.id ?? "") });
|
|
512
548
|
return;
|
|
513
549
|
}
|
|
514
|
-
await fx.call(upsertNoteIndex, { id });
|
|
550
|
+
await fx.call(upsertNoteIndex, { id: String(after.id) });
|
|
515
551
|
},
|
|
516
552
|
}),
|
|
517
553
|
);
|
|
518
554
|
```
|
|
519
555
|
|
|
520
|
-
|
|
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:
|
|
556
|
+
`changed("status")` stamps `trigger.cdc.column` on the Manifest. Still receive the
|
|
557
|
+
same payload — filter in `do` when you only care about that field:
|
|
526
558
|
|
|
527
559
|
```typescript title="src/flows/tasks/on-status.ts"
|
|
528
560
|
import { on, flow } from "okengine";
|
|
@@ -533,10 +565,8 @@ export const onStatus = on(
|
|
|
533
565
|
db.table(tasks).changed("status"),
|
|
534
566
|
flow("tasks.onStatus", {
|
|
535
567
|
plane: "operator",
|
|
536
|
-
do: async ({ before, after }, fx) => {
|
|
568
|
+
do: async ({ before, after, id }, fx) => {
|
|
537
569
|
if (before?.status === after?.status) return;
|
|
538
|
-
const id = String(after?.id ?? before?.id ?? "");
|
|
539
|
-
if (!id) return;
|
|
540
570
|
await fx.emit(taskStatusChanged, {
|
|
541
571
|
id,
|
|
542
572
|
from: before?.status ?? null,
|
|
@@ -551,21 +581,21 @@ export const onStatus = on(
|
|
|
551
581
|
|
|
552
582
|
<Tab value="Images">
|
|
553
583
|
|
|
554
|
-
Op is inferred from which image is null
|
|
584
|
+
Op is inferred from which image is null — the same derivation as `action`:
|
|
555
585
|
|
|
556
|
-
| Write | `before` | `after` |
|
|
557
|
-
| ------ | ------------ | ------- |
|
|
558
|
-
| Insert | `null` | new row |
|
|
559
|
-
| Update | previous row | new row |
|
|
560
|
-
| Delete | previous row | `null` |
|
|
586
|
+
| Write | `before` | `after` | `action` |
|
|
587
|
+
| ------ | ------------ | ------- | ----------- |
|
|
588
|
+
| Insert | `null` | new row | `"created"` |
|
|
589
|
+
| Update | previous row | new row | `"updated"` |
|
|
590
|
+
| Delete | previous row | `null` | `"deleted"` |
|
|
561
591
|
|
|
562
592
|
```typescript
|
|
563
|
-
do: async ({ before, after }, fx) => {
|
|
564
|
-
if (
|
|
593
|
+
do: async ({ before, after, action }, fx) => {
|
|
594
|
+
if (action === "created") {
|
|
565
595
|
/* insert */
|
|
566
|
-
} else if (
|
|
596
|
+
} else if (action === "updated") {
|
|
567
597
|
/* update */
|
|
568
|
-
} else
|
|
598
|
+
} else {
|
|
569
599
|
/* delete */
|
|
570
600
|
}
|
|
571
601
|
};
|
|
@@ -578,8 +608,9 @@ do: async ({ before, after }, fx) => {
|
|
|
578
608
|
<Accordions>
|
|
579
609
|
|
|
580
610
|
<Accordion title="CDC payload">
|
|
581
|
-
|
|
582
|
-
`
|
|
611
|
+
`{ before, after }` are always present. `{ table, action, id }` are always populated — `id` is
|
|
612
|
+
the table's declared primary-key value, not a column assumed to be named `"id"`. There is no
|
|
613
|
+
`record` field and no `{ op }` (that stays on the live-query / outbox path).
|
|
583
614
|
|
|
584
615
|
Writes must go through `fx.store`. A raw SQL client bypasses the sink, so no consumer runs.
|
|
585
616
|
|
|
@@ -625,7 +656,13 @@ Clock drivers: **postgres** in `dev`/`prod`, **frozen** in `test`. Signal driver
|
|
|
625
656
|
<Accordion title="TypeError: on() expected a trigger or signal handle">
|
|
626
657
|
The first argument must be a Signal handle, a Clock handle, `db.table(…).changed()`, an HTTP
|
|
627
658
|
trigger, `internal`, or `mcp.tool(…)`. A bare interval string is not a trigger — wrap it in
|
|
628
|
-
`clock("name",
|
|
659
|
+
`clock.every("name", "1h")`.
|
|
660
|
+
</Accordion>
|
|
661
|
+
|
|
662
|
+
<Accordion title="OKE1070 — flow name defined twice">
|
|
663
|
+
Cause: `Flow "{flow}" is defined twice.` Two nameless `flow({ do })` bindings inherited the same
|
|
664
|
+
Signal or Clock name, or two explicit `flow("…")` calls share a name. Give at least one an
|
|
665
|
+
explicit `flow("…")` or a distinct tree export.
|
|
629
666
|
</Accordion>
|
|
630
667
|
|
|
631
668
|
<Accordion title="OKE1240 — emit with no subscriber">
|
|
@@ -644,8 +681,9 @@ Clock drivers: **postgres** in `dev`/`prod`, **frozen** in `test`. Signal driver
|
|
|
644
681
|
</Accordion>
|
|
645
682
|
|
|
646
683
|
<Accordion title="CDC do never sees record / op">
|
|
647
|
-
Input is `{ before, after }`. There is no `record` field.
|
|
648
|
-
which image is null.
|
|
684
|
+
Input is `{ before, after, table, action, id }`. There is no `record` field. `action` is
|
|
685
|
+
`"created"` / `"updated"` / `"deleted"` from which image is null. `{ op }` is live-query /
|
|
686
|
+
outbox only.
|
|
649
687
|
</Accordion>
|
|
650
688
|
|
|
651
689
|
<Accordion title="Cron fired 24 times after overnight downtime">
|
|
@@ -678,7 +716,7 @@ Clock drivers: **postgres** in `dev`/`prod`, **frozen** in `test`. Signal driver
|
|
|
678
716
|
- [Store · SQL](/docs/elements/store/sql) — tables CDC watches
|
|
679
717
|
- [HTTP · Live Streams](/docs/elements/flow/http#live-streams) — `signal.live` SSE
|
|
680
718
|
- [fx](/docs/reference/fx) — `fx.emit`, `fx.deadLetters`, `fx.clock`
|
|
681
|
-
- [Errors](/docs/reference/errors) — OKE1240 · OKE1250
|
|
719
|
+
- [Errors](/docs/reference/errors) — OKE1070 · OKE1240 · OKE1250
|
|
682
720
|
- [Workflows](/docs/elements/flow/workflows) — `durable: true` + `fx.step` on a consumer
|
|
683
721
|
|
|
684
722
|
## Next
|
|
@@ -179,14 +179,14 @@ const { chargeId } = await fx.call(chargeCard, { amount: 50 });
|
|
|
179
179
|
|
|
180
180
|
<FlowTriggers />
|
|
181
181
|
|
|
182
|
-
| Trigger | Bind | Starts when | `do` input
|
|
183
|
-
| --------- | ---------------------------------------------- | ------------------- |
|
|
184
|
-
| HTTP | `on(http.get(), flow)` | A request | Merged path / query / body
|
|
185
|
-
| Signal | `on(signalHandle, flow)` | `fx.emit` | Payload (`schema`)
|
|
186
|
-
| Clock | `on(clockDecl, flow)` | Scheduler tick | none (`_`)
|
|
187
|
-
| CDC | `on(db.table(t).changed(), flow)` | Committed SQL write | `{ before, after }`
|
|
188
|
-
| Call-only | `call("name", { in, out, do, … })` | `fx.call` | Callee `in`
|
|
189
|
-
| MCP | `on(mcp.tool("x", { in, out }).gate(…), flow)` | MCP `tools/call` | Tool args
|
|
182
|
+
| Trigger | Bind | Starts when | `do` input |
|
|
183
|
+
| --------- | ---------------------------------------------- | ------------------- | -------------------------------------- |
|
|
184
|
+
| HTTP | `on(http.get(), flow)` | A request | Merged path / query / body |
|
|
185
|
+
| Signal | `on(signalHandle, flow)` | `fx.emit` | Payload (`schema`) |
|
|
186
|
+
| Clock | `on(clockDecl, flow)` | Scheduler tick | none (`_`) |
|
|
187
|
+
| CDC | `on(db.table(t).changed(), flow)` | Committed SQL write | `{ before, after, table, action, id }` |
|
|
188
|
+
| Call-only | `call("name", { in, out, do, … })` | `fx.call` | Callee `in` |
|
|
189
|
+
| MCP | `on(mcp.tool("x", { in, out }).gate(…), flow)` | MCP `tools/call` | Tool args |
|
|
190
190
|
|
|
191
191
|
`signal.live` is an HTTP SSE tape — bind it with [`http.live`](/docs/elements/flow/http#live-streams), not as a worker.
|
|
192
192
|
|
|
@@ -366,7 +366,7 @@ A bare `404` with body `Not Found` means **no route matched** — not `fx.fail("
|
|
|
366
366
|
| `fx.ask(prompt, opts)` | `asks` | AI prompt |
|
|
367
367
|
| `fx.vault.get(secret)` | `secrets` | Declared secret (never a raw value in source) |
|
|
368
368
|
| `fx.call(flow, input?)` | `calls` | Another Flow — waits for return |
|
|
369
|
-
| `fx.id()` | — |
|
|
369
|
+
| `fx.id()` | — | OKID — 21-char native id from `okengine/okid` |
|
|
370
370
|
| `fx.clock.now()` | — | Deterministic time |
|
|
371
371
|
| `fx.fail(code, data)` | — | Typed failure value |
|
|
372
372
|
| `fx.step(name, fn)` | journal | Durable checkpoint |
|
|
@@ -524,9 +524,9 @@ Default is `true` once `gate.auth.tenant` is on.
|
|
|
524
524
|
</Accordion>
|
|
525
525
|
|
|
526
526
|
<Accordion title="TypeError: on() expected a trigger or signal handle">
|
|
527
|
-
First argument must be an HTTP trigger, Signal handle, Clock handle,
|
|
528
|
-
`
|
|
529
|
-
|
|
527
|
+
First argument must be an HTTP trigger, Signal handle, Clock handle, `db.table(…).changed()`,
|
|
528
|
+
`internal`, or `mcp.tool(…)`. A bare interval string is not a trigger — wrap it in
|
|
529
|
+
`clock.every("name", "1h")`.
|
|
530
530
|
</Accordion>
|
|
531
531
|
|
|
532
532
|
<Accordion title="TypeError: on() expected a flow() definition as the second argument">
|
|
@@ -582,7 +582,7 @@ Default is `true` once `gate.auth.tenant` is on.
|
|
|
582
582
|
|
|
583
583
|
## Learn more
|
|
584
584
|
|
|
585
|
-
- [The
|
|
585
|
+
- [The Architecture](/docs/understand/the-architecture) — `on`, trigger, `flow`, `do`, `fx`
|
|
586
586
|
- [HTTP](/docs/elements/flow/http) — verbs, envelopes, resources, live SSE
|
|
587
587
|
- [Routing](/docs/elements/flow/routing) — file-tree stamps, barrels, OKE1030 · OKE1040–1045
|
|
588
588
|
- [Consumers](/docs/elements/flow/consumers) — Signal / Clock / CDC
|
|
@@ -611,8 +611,8 @@ Default is `true` once `gate.auth.tenant` is on.
|
|
|
611
611
|
href="/docs/elements/flow/workflows"
|
|
612
612
|
/>
|
|
613
613
|
<Card
|
|
614
|
-
title="The
|
|
614
|
+
title="The Architecture"
|
|
615
615
|
description="Five pieces behind on(trigger, flow)."
|
|
616
|
-
href="/docs/understand/the-
|
|
616
|
+
href="/docs/understand/the-architecture"
|
|
617
617
|
/>
|
|
618
618
|
</Cards>
|
|
@@ -455,6 +455,10 @@ prefix). Wrong-unit prefixes fail generate.
|
|
|
455
455
|
Non-HTTP files still join the unit. A signal consumer in `notes/on-created.ts`
|
|
456
456
|
is `api.notes.onCreated` over RPC (`POST /_oke/notes/onCreated`), not HTTP.
|
|
457
457
|
|
|
458
|
+
Outside the tree, nameless `flow({ do })` inherits a named Signal or Clock trigger.
|
|
459
|
+
File-tree `unit.export` still wins. Two nameless inheritances fail **OKE1070**. HTTP has no
|
|
460
|
+
inherent trigger name — those Flows stay on the tree stamp or **OKE1045**.
|
|
461
|
+
|
|
458
462
|
HTTP flows that remain nameless after adopt fail **OKE1045**.
|
|
459
463
|
|
|
460
464
|
## Runtime Matching
|
|
@@ -512,6 +516,11 @@ plus a handwritten `http.get("/notes")`.
|
|
|
512
516
|
file so the tree can stamp `unit.export`. `export default` is not picked up.
|
|
513
517
|
</Accordion>
|
|
514
518
|
|
|
519
|
+
<Accordion title="OKE1070 — flow name defined twice">
|
|
520
|
+
Cause: `Flow "{flow}" is defined twice.` Two nameless Signal/Clock consumers inherited the same
|
|
521
|
+
name, or two explicit `flow("…")` calls collide. Give at least one a distinct name or tree export.
|
|
522
|
+
</Accordion>
|
|
523
|
+
|
|
515
524
|
<Accordion title="422 — path param missing from in">
|
|
516
525
|
`[id]` stamps `:id`. `in` must declare `id` (same key). A schema that expects `userId` while the
|
|
517
526
|
path is `:id` fails validation before `do`.
|
|
@@ -534,8 +543,8 @@ plus a handwritten `http.get("/notes")`.
|
|
|
534
543
|
|
|
535
544
|
- [HTTP](/docs/elements/flow/http) — verbs, envelopes, `http.resource`, live SSE
|
|
536
545
|
- [Client](/docs/client/calling) — `api.notes.get`, REST vs RPC, `$routes`
|
|
537
|
-
- [Errors](/docs/reference/errors) — OKE1040 · OKE1030 · OKE1041 · OKE1045
|
|
538
|
-
- [The
|
|
546
|
+
- [Errors](/docs/reference/errors) — OKE1040 · OKE1030 · OKE1041 · OKE1045 · OKE1070
|
|
547
|
+
- [The Architecture](/docs/understand/the-architecture) — derived routes, no hand-written table
|
|
539
548
|
- [Gate](/docs/elements/gate) — `.gate(...)` / `.public()` on the same trigger
|
|
540
549
|
|
|
541
550
|
## Next
|