okengine 0.19.3 → 0.19.5
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/elements/channel/index.mdx +1 -1
- package/site/content/docs/elements/clock/index.mdx +53 -105
- package/site/content/docs/elements/clock/schedules.mdx +39 -11
- package/site/content/docs/elements/flow/consumers.mdx +54 -31
- package/site/content/docs/elements/flow/index.mdx +6 -9
- package/site/content/docs/elements/flow/routing.mdx +11 -15
- package/site/content/docs/elements/signal/broadcast.mdx +6 -1
- package/site/content/docs/elements/signal/index.mdx +21 -177
- package/site/content/docs/elements/signal/once.mdx +6 -1
- package/site/content/docs/reference/cli.mdx +14 -1
- package/site/content/docs/reference/errors.mdx +7 -0
- package/site/content/docs/understand/the-architecture.mdx +2 -2
- package/site/content/docs/understand/try-it.mdx +1 -1
- package/src/cli/competitor-mention-removal.test.ts +3 -3
- package/src/cli/dev-app-runner.ts +23 -2
- package/src/cli/dev.test.ts +47 -0
- package/src/cli/dev.ts +11 -0
- package/src/compiler/extract-skip.test.ts +43 -0
- package/src/compiler/extract.test.ts +190 -15
- package/src/compiler/extract.ts +57 -7
- package/src/compiler/flow-path.test.ts +36 -0
- package/src/compiler/flow-path.ts +35 -1
- package/src/compiler/generate-adopt.ts +12 -2
- package/src/console/ui-next/dist/assets/{access-page-BpugjHHY.js → access-page-BDbw40sZ.js} +1 -1
- package/src/console/ui-next/dist/assets/{agent-disclosure-qnsmtJhf.js → agent-disclosure-CVhs5xog.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-BcpWUM98.js → cache-glyph-CKIaJ1te.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-Bz5xiNmR.js → call-pii-button-CPH68-4L.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-Cuxn2WH8.js → collapsible-DYYX3bKy.js} +1 -1
- package/src/console/ui-next/dist/assets/{duration-tone-Bjnl3EaM.js → duration-tone-BnRJ32O5.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-DVujp-T2.js → flows-page-DwEDnJqd.js} +1 -1
- package/src/console/ui-next/dist/assets/{highlighted-json-Bql0qlqW.js → highlighted-json-BemWpaqn.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-BljvrfRg.js → http-method-CAboOgAE.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-D1vE656k.js → index-C3KoC-mS.js} +3 -3
- package/src/console/ui-next/dist/assets/index-VxoEz295.css +2 -0
- package/src/console/ui-next/dist/assets/{observability-page-Bdi0bLJl.js → observability-page-C2sNV7Qu.js} +1 -1
- package/src/console/ui-next/dist/assets/{replica-lag-CpkPfITG.js → replica-lag-DJQeKIw-.js} +1 -1
- package/src/console/ui-next/dist/assets/{request-meta-C4ZVNFVt.js → request-meta-DQ6HY-a4.js} +1 -1
- package/src/console/ui-next/dist/assets/{store-page-DAHtnesC.js → store-page-kB4rot2u.js} +1 -1
- package/src/console/ui-next/dist/assets/{trace-detail-sheet-B2c9QRBw.js → trace-detail-sheet-B2VrPfhu.js} +1 -1
- package/src/console/ui-next/dist/assets/{tree-expand-toggle-BP4tRC98.js → tree-expand-toggle-CdecFS0p.js} +1 -1
- package/src/console/ui-next/dist/assets/{units-page-Oi6--n09.js → units-page-B-Y68cys.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-Dt-cPkiU.js → vault-page-BcDSaubT.js} +1 -1
- package/src/console/ui-next/dist/index.html +2 -2
- package/src/kernel/app.ts +23 -1
- package/src/kernel/boot.ts +9 -5
- package/src/kernel/effects-stamping.test.ts +44 -0
- package/src/kernel/errors-flow-name.ts +20 -4
- package/src/kernel/errors.registry.test.ts +4 -2
- package/src/kernel/errors.ts +1 -1
- package/src/kernel/flow-name.test.ts +105 -20
- package/src/kernel/flow.ts +3 -3
- package/src/kernel/on.ts +0 -5
- package/src/kernel/stamp-http.test.ts +2 -1
- package/src/kernel/stamp-http.ts +5 -7
- package/src/kernel/unit.ts +1 -2
- package/src/console/ui-next/dist/assets/index-D0zS5rKO.css +0 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "okengine",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.5",
|
|
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": {
|
|
@@ -5,7 +5,7 @@ icon: "Clock"
|
|
|
5
5
|
source: "docs/spec/unified-theory.md"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
Clock is how your backend **knows what time it is and when to run again**. A monthly invoice job, a 30s health ping, and a three-day trial reminder share one vocabulary: declare a named schedule, bind it with `on(clockDecl, flow)`, and read time only through `fx.clock`.
|
|
8
|
+
Clock is how your backend **knows what time it is and when to run again**. A monthly invoice job, a 30s health ping, and a three-day trial reminder share one vocabulary: declare a named schedule, bind it with `on(clockDecl, flow("name", { do }))`, and read time only through `fx.clock`.
|
|
9
9
|
|
|
10
10
|
For developers scheduling work on okengine — one handle shape; drivers swap by environment.
|
|
11
11
|
|
|
@@ -58,9 +58,8 @@ export const digest = on(
|
|
|
58
58
|
### See it tick
|
|
59
59
|
|
|
60
60
|
With `oke dev`, the scheduler reconciles `notes.digest` into the Store and leader-elects
|
|
61
|
-
before each fire. `do` receives no payload — read time with `fx.clock.now()` (epoch-ms
|
|
62
|
-
|
|
63
|
-
(`new Date(fx.clock.now()).toISOString()`).
|
|
61
|
+
before each fire. `do` receives no payload — read time with `fx.clock.now()` (epoch-ms).
|
|
62
|
+
Map to ISO on the wire with `new Date(fx.clock.now()).toISOString()`.
|
|
64
63
|
|
|
65
64
|
Under `drivers.clock.test = "frozen"`, advance time in tests instead of waiting a day.
|
|
66
65
|
|
|
@@ -69,35 +68,39 @@ Under `drivers.clock.test = "frozen"`, advance time in tests instead of waiting
|
|
|
69
68
|
</Steps>
|
|
70
69
|
|
|
71
70
|
<Callout title="Jobs are Flows">
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
Write the schedule inline or export it — [Inline or named export](#inline-or-named-export).
|
|
72
|
+
Every consumer still uses `flow("name", { do })`. See
|
|
73
|
+
[Consumers · Clock Jobs](/docs/elements/flow/consumers#clock-jobs).
|
|
74
74
|
</Callout>
|
|
75
75
|
|
|
76
76
|
## Inline or named export
|
|
77
77
|
|
|
78
|
-
| Style
|
|
79
|
-
|
|
|
80
|
-
| `on(clock.every("name", "1h"), flow({ do }))` |
|
|
81
|
-
| `export const x = clock.every("name", "1h")`
|
|
78
|
+
| Style | When |
|
|
79
|
+
| --------------------------------------------------------- | --------------------------------- |
|
|
80
|
+
| `on(clock.every("name", "1h"), flow("ops.ping", { do }))` | One Flow owns this schedule |
|
|
81
|
+
| `export const x = clock.every("name", "1h")` | Several Flows share this schedule |
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
<Callout title="Why Clock can be inline">
|
|
84
|
+
`fx.clock` is now / sleep / offsets — not an emit. Signal stays an exported const so producers can
|
|
85
|
+
`fx.emit(handle, payload)`. Export a Clock const only to share one schedule across named Flows.
|
|
86
|
+
</Callout>
|
|
85
87
|
|
|
86
|
-
|
|
88
|
+
Both styles pass a real `flow("name", { do })`. Nameless `flow({ do })` fails
|
|
89
|
+
**OKE1072** — the trigger's name is never the Flow's name.
|
|
87
90
|
|
|
88
91
|
<Tabs items={["Inline", "Named"]}>
|
|
89
92
|
|
|
90
93
|
<Tab value="Inline">
|
|
91
94
|
|
|
92
|
-
One file — declare and bind together. The scheduler fires it; no other
|
|
93
|
-
|
|
95
|
+
One file — declare and bind together. The scheduler fires it; no other Flow
|
|
96
|
+
binds this cadence:
|
|
94
97
|
|
|
95
98
|
```typescript title="src/flows/health/ping.ts"
|
|
96
99
|
import { on, flow, clock } from "okengine";
|
|
97
100
|
|
|
98
101
|
export const pingExternal = on(
|
|
99
102
|
clock.every("health.pingExternal", "30s"),
|
|
100
|
-
flow({
|
|
103
|
+
flow("health.pingExternal", {
|
|
101
104
|
plane: "operator",
|
|
102
105
|
do: async (_, fx) => {
|
|
103
106
|
await fx.call(pingUpstream);
|
|
@@ -110,79 +113,21 @@ export const pingExternal = on(
|
|
|
110
113
|
|
|
111
114
|
<Tab value="Named">
|
|
112
115
|
|
|
113
|
-
Export the handle when
|
|
114
|
-
|
|
116
|
+
Export the handle when a second `on()` must reuse the same schedule. Each Flow
|
|
117
|
+
keeps its own explicit name — **OKE1070** if they collide:
|
|
115
118
|
|
|
116
|
-
```typescript title="src/clocks/
|
|
119
|
+
```typescript title="src/clocks/metrics.ts"
|
|
117
120
|
import { clock } from "okengine";
|
|
118
121
|
|
|
119
|
-
export const
|
|
122
|
+
export const tickClock = clock.every("metrics.tick", "1h");
|
|
120
123
|
```
|
|
121
124
|
|
|
122
|
-
```typescript title="src/flows/
|
|
125
|
+
```typescript title="src/flows/ops/metrics.ts"
|
|
123
126
|
import { on, flow } from "okengine";
|
|
124
|
-
import {
|
|
125
|
-
|
|
126
|
-
export const digest = on(
|
|
127
|
-
digestClock,
|
|
128
|
-
flow("notes.digest", {
|
|
129
|
-
plane: "operator",
|
|
130
|
-
do: async (_, fx) => {
|
|
131
|
-
await fx.call(buildDigest, { at: fx.clock.now() });
|
|
132
|
-
},
|
|
133
|
-
}),
|
|
134
|
-
);
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
</Tab>
|
|
138
|
-
|
|
139
|
-
</Tabs>
|
|
140
|
-
|
|
141
|
-
## Flow name
|
|
142
|
-
|
|
143
|
-
| Style | When |
|
|
144
|
-
| ------------------------- | ---------------------------------------------------------------- |
|
|
145
|
-
| `flow({ do })` | No unit folder — Flow name is the Clock name (`metrics.cleanup`) |
|
|
146
|
-
| `flow("ops.sweep")` | Manifest / `fx.call` name must differ from the Clock |
|
|
147
|
-
| Tree `export const sweep` | `src/flows/ops/sweep.ts` stamps `ops.sweep` — overwrites inherit |
|
|
148
|
-
|
|
149
|
-
Explicit `flow("…")` and the file tree overwrite inherit. HTTP does not inherit a
|
|
150
|
-
name from the path — nameless HTTP stays for the tree or fails **OKE1045**. Two
|
|
151
|
-
Flows that land on the same name fail **OKE1070** (`Flow "{flow}" is defined twice.`).
|
|
152
|
-
|
|
153
|
-
<Tabs items={["Inherit", "Explicit", "Tree"]}>
|
|
154
|
-
|
|
155
|
-
<Tab value="Inherit">
|
|
156
|
-
|
|
157
|
-
A file directly in `src/flows/` (no unit folder) has nothing to stamp. The Flow
|
|
158
|
-
is named `metrics.cleanup` — same as the Clock:
|
|
159
|
-
|
|
160
|
-
```typescript title="src/flows/cleanup.ts"
|
|
161
|
-
import { on, flow, clock } from "okengine";
|
|
162
|
-
|
|
163
|
-
export const cleanupMetrics = on(
|
|
164
|
-
clock.every("metrics.cleanup", "1h"),
|
|
165
|
-
flow({
|
|
166
|
-
plane: "operator",
|
|
167
|
-
do: async (_, fx) => {
|
|
168
|
-
await fx.call(sweepMetrics);
|
|
169
|
-
},
|
|
170
|
-
}),
|
|
171
|
-
);
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
</Tab>
|
|
175
|
-
|
|
176
|
-
<Tab value="Explicit">
|
|
177
|
-
|
|
178
|
-
The Clock stays `metrics.cleanup`. The Flow is `ops.sweep` — that is the
|
|
179
|
-
Manifest / `fx.call` name:
|
|
180
|
-
|
|
181
|
-
```typescript title="src/flows/sweep.ts"
|
|
182
|
-
import { on, flow, clock } from "okengine";
|
|
127
|
+
import { tickClock } from "@/clocks/metrics";
|
|
183
128
|
|
|
184
129
|
export const sweep = on(
|
|
185
|
-
|
|
130
|
+
tickClock,
|
|
186
131
|
flow("ops.sweep", {
|
|
187
132
|
plane: "operator",
|
|
188
133
|
do: async (_, fx) => {
|
|
@@ -190,24 +135,13 @@ export const sweep = on(
|
|
|
190
135
|
},
|
|
191
136
|
}),
|
|
192
137
|
);
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
</Tab>
|
|
196
|
-
|
|
197
|
-
<Tab value="Tree">
|
|
198
|
-
|
|
199
|
-
Unit folder + `export const` stamps `unit.export`. The Flow is `ops.sweep`, not
|
|
200
|
-
the Clock name `cleanup`:
|
|
201
138
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
export const sweep = on(
|
|
206
|
-
clock.every("cleanup", "10m"),
|
|
207
|
-
flow({
|
|
139
|
+
export const report = on(
|
|
140
|
+
tickClock,
|
|
141
|
+
flow("ops.report", {
|
|
208
142
|
plane: "operator",
|
|
209
143
|
do: async (_, fx) => {
|
|
210
|
-
await fx.call(
|
|
144
|
+
await fx.call(reportMetrics);
|
|
211
145
|
},
|
|
212
146
|
}),
|
|
213
147
|
);
|
|
@@ -227,12 +161,17 @@ Same `clock` + `fx.clock` from a calendar cron to an interval, a durable pause,
|
|
|
227
161
|
|
|
228
162
|
Five-field cron plus helpers. Prefer an app-wide zone so schedules stay short:
|
|
229
163
|
|
|
230
|
-
```typescript title="src/
|
|
231
|
-
import {
|
|
164
|
+
```typescript title="src/clocks/reports.ts"
|
|
165
|
+
import { clock } from "okengine";
|
|
232
166
|
|
|
233
167
|
export const dailyReportClock = clock.daily("reports.daily", {
|
|
234
168
|
at: "06:00",
|
|
235
169
|
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
```typescript title="src/flows/reports/daily.ts"
|
|
173
|
+
import { on, flow } from "okengine";
|
|
174
|
+
import { dailyReportClock } from "@/clocks/reports";
|
|
236
175
|
|
|
237
176
|
export const runDailyReport = on(
|
|
238
177
|
dailyReportClock,
|
|
@@ -255,10 +194,15 @@ see [Schedules](/docs/elements/clock/schedules).
|
|
|
255
194
|
|
|
256
195
|
Human durations — integer + unit, no weeks: `"200ms"` · `"30s"` · `"5m"` · `"1h"` · `"7d"`:
|
|
257
196
|
|
|
258
|
-
```typescript title="src/
|
|
259
|
-
import {
|
|
197
|
+
```typescript title="src/clocks/health.ts"
|
|
198
|
+
import { clock } from "okengine";
|
|
260
199
|
|
|
261
200
|
export const pingClock = clock.every("health.pingExternal", "30s");
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
```typescript title="src/flows/health/ping.ts"
|
|
204
|
+
import { on, flow } from "okengine";
|
|
205
|
+
import { pingClock } from "@/clocks/health";
|
|
262
206
|
|
|
263
207
|
export const pingExternal = on(
|
|
264
208
|
pingClock,
|
|
@@ -414,9 +358,14 @@ export default defineConfig({
|
|
|
414
358
|
</Accordion>
|
|
415
359
|
|
|
416
360
|
<Accordion title="OKE1070 — flow name defined twice">
|
|
417
|
-
Cause: `Flow "{flow}" is defined twice.` Two
|
|
418
|
-
|
|
419
|
-
|
|
361
|
+
Cause: `Flow "{flow}" is defined twice.` Two `flow("…")` strings collide. Give at least one a
|
|
362
|
+
distinct name. Clock allows several consumers on one schedule — each needs its own name.
|
|
363
|
+
</Accordion>
|
|
364
|
+
|
|
365
|
+
<Accordion title="OKE1072 — Clock flow unnamed">
|
|
366
|
+
Cause: `A clock flow on "{trigger}" has no name.`
|
|
367
|
+
Fix: pass an explicit name — `on(clockDecl, flow("notes.digest", { do }))`.
|
|
368
|
+
Inline `clock.every` still needs `flow("…")` — [Inline or named export](#inline-or-named-export).
|
|
420
369
|
</Accordion>
|
|
421
370
|
|
|
422
371
|
</Accordions>
|
|
@@ -428,8 +377,7 @@ export default defineConfig({
|
|
|
428
377
|
- [Consumers · Clock Jobs](/docs/elements/flow/consumers#clock-jobs) — bind with `on(clockDecl, flow)`
|
|
429
378
|
- [Workflows](/docs/elements/flow/workflows) — `durable: true` + `fx.step` around sleeps
|
|
430
379
|
- [fx](/docs/reference/fx) — full `fx.clock` table
|
|
431
|
-
- [
|
|
432
|
-
- [Errors](/docs/reference/errors) — `ScheduleNotOverridableError` · `ClockResourceNotFoundError` · OKE1070
|
|
380
|
+
- [Errors](/docs/reference/errors) — `ScheduleNotOverridableError` · `ClockResourceNotFoundError` · OKE1070 · OKE1072
|
|
433
381
|
|
|
434
382
|
## Next
|
|
435
383
|
|
|
@@ -25,10 +25,15 @@ For developers running daily digests, health pings, and per-tenant billing loops
|
|
|
25
25
|
<Step>
|
|
26
26
|
### Declare a schedule and bind a Flow
|
|
27
27
|
|
|
28
|
-
```typescript title="src/
|
|
29
|
-
import {
|
|
28
|
+
```typescript title="src/clocks/reports.ts"
|
|
29
|
+
import { clock } from "okengine";
|
|
30
30
|
|
|
31
31
|
export const dailyReportClock = clock.daily("reports.daily", { at: "06:00" });
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```typescript title="src/flows/reports/daily.ts"
|
|
35
|
+
import { on, flow } from "okengine";
|
|
36
|
+
import { dailyReportClock } from "@/clocks/reports";
|
|
32
37
|
|
|
33
38
|
export const runDailyReport = on(
|
|
34
39
|
dailyReportClock,
|
|
@@ -112,10 +117,15 @@ chosen programmatically: `clock("x", { cron: "0 6 * * *" })`, Bun nicknames
|
|
|
112
117
|
|
|
113
118
|
Fixed duration loops — `"200ms"` · `"30s"` · `"5m"` · `"1h"` · `"7d"`:
|
|
114
119
|
|
|
115
|
-
```typescript title="src/
|
|
116
|
-
import {
|
|
120
|
+
```typescript title="src/clocks/health.ts"
|
|
121
|
+
import { clock } from "okengine";
|
|
117
122
|
|
|
118
123
|
export const pingClock = clock.every("health.pingExternal", "30s");
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
```typescript title="src/flows/health/ping.ts"
|
|
127
|
+
import { on, flow } from "okengine";
|
|
128
|
+
import { pingClock } from "@/clocks/health";
|
|
119
129
|
|
|
120
130
|
export const pingExternal = on(
|
|
121
131
|
pingClock,
|
|
@@ -146,9 +156,14 @@ Unknown duration strings parse as `0` ms and never become due. A `"d"` is exactl
|
|
|
146
156
|
template name is never ticked:
|
|
147
157
|
|
|
148
158
|
```typescript title="src/clocks/invoices.ts"
|
|
149
|
-
import {
|
|
159
|
+
import { clock } from "okengine";
|
|
150
160
|
|
|
151
161
|
export const invoicesClock = clock.perTenant("invoices", { every: "1h" });
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
```typescript title="src/flows/billing/invoices.ts"
|
|
165
|
+
import { on, flow } from "okengine";
|
|
166
|
+
import { invoicesClock } from "@/clocks/invoices";
|
|
152
167
|
|
|
153
168
|
export const runInvoices = on(
|
|
154
169
|
invoicesClock,
|
|
@@ -275,16 +290,22 @@ do not consult the zone for tick spacing; the zone still lands on the Store row.
|
|
|
275
290
|
|
|
276
291
|
## Binding & Input
|
|
277
292
|
|
|
278
|
-
Bind
|
|
279
|
-
|
|
293
|
+
Bind with `on(clockDecl, flow("name", { do }))`. One Flow can write `clock.every`
|
|
294
|
+
inside `on()` — [Clock · Inline or named export](/docs/elements/clock#inline-or-named-export).
|
|
295
|
+
`do` has no payload; read time through `fx.clock`:
|
|
296
|
+
|
|
297
|
+
```typescript title="src/clocks/metrics.ts"
|
|
298
|
+
import { clock } from "okengine";
|
|
299
|
+
|
|
300
|
+
export const cleanupClock = clock.every("metrics.cleanup", "1h");
|
|
301
|
+
```
|
|
280
302
|
|
|
281
303
|
```typescript title="src/flows/metrics/cleanup.ts"
|
|
282
|
-
import { on, flow
|
|
304
|
+
import { on, flow } from "okengine";
|
|
283
305
|
import { lt } from "drizzle-orm";
|
|
306
|
+
import { cleanupClock } from "@/clocks/metrics";
|
|
284
307
|
import { db, metricLogs } from "@/schema";
|
|
285
308
|
|
|
286
|
-
export const cleanupClock = clock.every("metrics.cleanup", "1h");
|
|
287
|
-
|
|
288
309
|
export const cleanup = on(
|
|
289
310
|
cleanupClock,
|
|
290
311
|
flow("metrics.cleanup", {
|
|
@@ -470,6 +491,12 @@ forms; on overlap days crontab fires **once** (first occurrence).
|
|
|
470
491
|
that is not in the reconciled Store — check spelling against your `clock()` declarations.
|
|
471
492
|
</Accordion>
|
|
472
493
|
|
|
494
|
+
<Accordion title="OKE1072 — Clock flow unnamed">
|
|
495
|
+
Cause: `A clock flow on "{trigger}" has no name.`
|
|
496
|
+
Fix: pass an explicit name — `on(clockDecl, flow("reports.runDaily", { do }))`.
|
|
497
|
+
Inline `clock.every` still needs `flow("…")` — [Clock · Inline or named export](/docs/elements/clock#inline-or-named-export).
|
|
498
|
+
</Accordion>
|
|
499
|
+
|
|
473
500
|
<Accordion title="DST gap / overlap warning on the row">
|
|
474
501
|
Informational. Pick a UTC cron, a non-ambiguous local hour, or accept the warning. The job still
|
|
475
502
|
schedules.
|
|
@@ -485,10 +512,11 @@ forms; on overlap days crontab fires **once** (first occurrence).
|
|
|
485
512
|
## Learn more
|
|
486
513
|
|
|
487
514
|
- [Clock overview](/docs/elements/clock) — `fx.clock` helpers and drivers
|
|
515
|
+
- [Clock · Inline or named export](/docs/elements/clock#inline-or-named-export) — one Flow vs shared schedule
|
|
488
516
|
- [Durable Sleep](/docs/elements/clock/sleep) — `fx.clock.sleep(label, duration)`
|
|
489
517
|
- [Consumers · Clock Jobs](/docs/elements/flow/consumers#clock-jobs) — bind with `on(clockDecl, flow)`
|
|
490
518
|
- [fx](/docs/reference/fx) — `fx.clock.now` / `ago` / `fromNow` / `duration`
|
|
491
|
-
- [Errors](/docs/reference/errors) — `ScheduleNotOverridableError` · `ClockResourceNotFoundError`
|
|
519
|
+
- [Errors](/docs/reference/errors) — `ScheduleNotOverridableError` · `ClockResourceNotFoundError` · OKE1072
|
|
492
520
|
|
|
493
521
|
## Next
|
|
494
522
|
|
|
@@ -96,15 +96,21 @@ export const processEmail = on(
|
|
|
96
96
|
|
|
97
97
|
<Tab value="Clock">
|
|
98
98
|
|
|
99
|
-
Name the schedule, then bind it.
|
|
99
|
+
Name the schedule, then bind it. One Flow can write `clock.every` inside `on()` —
|
|
100
|
+
[Clock · Inline or named export](/docs/elements/clock#inline-or-named-export).
|
|
101
|
+
|
|
102
|
+
```typescript title="src/clocks/metrics.ts"
|
|
103
|
+
import { clock } from "okengine";
|
|
104
|
+
|
|
105
|
+
export const cleanupClock = clock.every("metrics.cleanup", "1h");
|
|
106
|
+
```
|
|
100
107
|
|
|
101
108
|
```typescript title="src/flows/metrics/cleanup.ts"
|
|
102
|
-
import { on, flow
|
|
109
|
+
import { on, flow } from "okengine";
|
|
103
110
|
import { lt } from "drizzle-orm";
|
|
111
|
+
import { cleanupClock } from "@/clocks/metrics";
|
|
104
112
|
import { db, metricLogs } from "@/schema";
|
|
105
113
|
|
|
106
|
-
export const cleanupClock = clock.every("metrics.cleanup", "1h");
|
|
107
|
-
|
|
108
114
|
export const cleanupMetrics = on(
|
|
109
115
|
cleanupClock,
|
|
110
116
|
flow("metrics.cleanup", {
|
|
@@ -180,19 +186,19 @@ Omit `key` for competing consumers with no ordering.
|
|
|
180
186
|
|
|
181
187
|
## Trigger Reference
|
|
182
188
|
|
|
183
|
-
| Trigger | Signature
|
|
184
|
-
| ---------- |
|
|
185
|
-
| Signal | `on(
|
|
186
|
-
| Clock | `on(clockDecl, flow)` or inline `clock.every(…)`
|
|
187
|
-
| CDC any | `on(db.table(t).changed(), flow)`
|
|
188
|
-
| CDC column | `on(db.table(t).changed("col"), flow)`
|
|
189
|
+
| Trigger | Signature | Purpose | `do` input |
|
|
190
|
+
| ---------- | ---------------------------------------------------------------- | ------------------------------------------- | -------------------------------------- |
|
|
191
|
+
| Signal | `on(handle, flow("name", { do }))` | Queue (`once`) or fan-out (`broadcast`) | Payload (`schema`) |
|
|
192
|
+
| Clock | `on(clockDecl, flow("name", { do }))` or inline `clock.every(…)` | Interval or cron tick | none (`_`) |
|
|
193
|
+
| CDC any | `on(db.table(t).changed(), flow)` | Every insert / update / delete | `{ before, after, table, action, id }` |
|
|
194
|
+
| CDC column | `on(db.table(t).changed("col"), flow)` | Same writes; column stamped on the Manifest | `{ before, after, table, action, id }` |
|
|
189
195
|
|
|
190
196
|
`signal.live` is an HTTP SSE tape — bind it with [`http.live`](/docs/elements/flow/http#live-streams),
|
|
191
197
|
not as a worker. A Flow with no trigger is [call-only](/docs/elements/flow).
|
|
192
198
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
199
|
+
Signal is always an exported const (`fx.emit` needs the handle). Clock may write
|
|
200
|
+
`clock.every(…)` inside `on()` — [Clock · Inline or named
|
|
201
|
+
export](/docs/elements/clock#inline-or-named-export). **OKE1072** if nameless.
|
|
196
202
|
|
|
197
203
|
## Signal Consumers
|
|
198
204
|
|
|
@@ -336,10 +342,9 @@ Broadcast does not use the `once` lease / DLQ path. Live uses the retained tape,
|
|
|
336
342
|
</Accordion>
|
|
337
343
|
|
|
338
344
|
<Accordion title="Schema at emit">
|
|
339
|
-
Invalid payloads fail at `fx.emit` with **OKE1250** (`"{resource}": {detail}`) before any
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
declaration; they do not declare `in` on `flow({ do })`.
|
|
345
|
+
Invalid payloads fail at `fx.emit` with **OKE1250** (`"{resource}": {detail}`) before any consumer
|
|
346
|
+
runs. This is an **emit** contract — workers inherit the payload from the Signal; they do not
|
|
347
|
+
declare `in` on `flow()`.
|
|
343
348
|
</Accordion>
|
|
344
349
|
|
|
345
350
|
</Accordions>
|
|
@@ -347,11 +352,9 @@ Broadcast does not use the `once` lease / DLQ path. Live uses the retained tape,
|
|
|
347
352
|
## Clock Jobs
|
|
348
353
|
|
|
349
354
|
<Callout title="Detailed section">
|
|
350
|
-
Prefer `clock.every` / `
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
missing both `cron` and `every` throws `clock("name"): require cron or every`. Bind the returned
|
|
354
|
-
handle with `on(clockDecl, flow)`.
|
|
355
|
+
Prefer named helpers (`clock.every` / `daily` / `cron`). Bind with
|
|
356
|
+
`on(clockDecl, flow("name", { do }))`, or write `clock.every` inside `on()` —
|
|
357
|
+
[Clock · Inline or named export](/docs/elements/clock#inline-or-named-export).
|
|
355
358
|
</Callout>
|
|
356
359
|
|
|
357
360
|
Named clocks reconcile into the Store at boot. The scheduler leader-elects so N instances do not
|
|
@@ -363,10 +366,15 @@ double-fire. `do` receives no payload — read time through `fx.clock`.
|
|
|
363
366
|
|
|
364
367
|
Human durations: `"200ms"` · `"30s"` · `"5m"` · `"1h"` · `"7d"` (integer + unit, no weeks):
|
|
365
368
|
|
|
366
|
-
```typescript title="src/
|
|
367
|
-
import {
|
|
369
|
+
```typescript title="src/clocks/health.ts"
|
|
370
|
+
import { clock } from "okengine";
|
|
368
371
|
|
|
369
372
|
export const pingClock = clock.every("health.pingExternal", "30s");
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
```typescript title="src/flows/health/ping.ts"
|
|
376
|
+
import { on, flow } from "okengine";
|
|
377
|
+
import { pingClock } from "@/clocks/health";
|
|
370
378
|
|
|
371
379
|
export const pingExternal = on(
|
|
372
380
|
pingClock,
|
|
@@ -385,13 +393,18 @@ export const pingExternal = on(
|
|
|
385
393
|
|
|
386
394
|
Five-field cron (`m h dom mon dow`) plus an IANA `timezone` (default `"UTC"`):
|
|
387
395
|
|
|
388
|
-
```typescript title="src/
|
|
389
|
-
import {
|
|
396
|
+
```typescript title="src/clocks/reports.ts"
|
|
397
|
+
import { clock } from "okengine";
|
|
390
398
|
|
|
391
399
|
export const dailyReportClock = clock.daily("reports.daily", {
|
|
392
400
|
at: "06:00",
|
|
393
401
|
timezone: "Asia/Riyadh",
|
|
394
402
|
});
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
```typescript title="src/flows/reports/daily.ts"
|
|
406
|
+
import { on, flow } from "okengine";
|
|
407
|
+
import { dailyReportClock } from "@/clocks/reports";
|
|
395
408
|
|
|
396
409
|
export const runDailyReport = on(
|
|
397
410
|
dailyReportClock,
|
|
@@ -415,9 +428,14 @@ trigger when both are present.
|
|
|
415
428
|
is never ticked:
|
|
416
429
|
|
|
417
430
|
```typescript title="src/clocks/invoices.ts"
|
|
418
|
-
import {
|
|
431
|
+
import { clock } from "okengine";
|
|
419
432
|
|
|
420
433
|
export const invoicesClock = clock.perTenant("invoices", { every: "1h" });
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
```typescript title="src/flows/billing/invoices.ts"
|
|
437
|
+
import { on, flow } from "okengine";
|
|
438
|
+
import { invoicesClock } from "@/clocks/invoices";
|
|
421
439
|
|
|
422
440
|
export const runInvoices = on(
|
|
423
441
|
invoicesClock,
|
|
@@ -674,9 +692,13 @@ Clock drivers: **postgres** in `dev`/`prod`, **frozen** in `test`. Signal driver
|
|
|
674
692
|
</Accordion>
|
|
675
693
|
|
|
676
694
|
<Accordion title="OKE1070 — flow name defined twice">
|
|
677
|
-
Cause: `Flow "{flow}" is defined twice.` Two
|
|
678
|
-
|
|
679
|
-
|
|
695
|
+
Cause: `Flow "{flow}" is defined twice.` Two `flow("…")` strings share a name. Give at least one a
|
|
696
|
+
distinct name.
|
|
697
|
+
</Accordion>
|
|
698
|
+
|
|
699
|
+
<Accordion title="OKE1072 — Signal or Clock flow unnamed">
|
|
700
|
+
Cause: `A {kind} flow on "{trigger}" has no name.`
|
|
701
|
+
Fix: pass an explicit name — `on(handle, flow("unit.export", { do }))`.
|
|
680
702
|
</Accordion>
|
|
681
703
|
|
|
682
704
|
<Accordion title="OKE1071 — once signal bound to more than one Flow">
|
|
@@ -733,10 +755,11 @@ Clock drivers: **postgres** in `dev`/`prod`, **frozen** in `test`. Signal driver
|
|
|
733
755
|
- [Signal](/docs/elements/signal) — `once` / `broadcast` / `live` physics
|
|
734
756
|
- [Signal · Once](/docs/elements/signal/once) — leases, retries, partition keys
|
|
735
757
|
- [Clock](/docs/elements/clock) — schedules, `fx.clock.sleep`
|
|
758
|
+
- [Clock · Inline or named export](/docs/elements/clock#inline-or-named-export) — one Flow vs shared schedule
|
|
736
759
|
- [Store · SQL](/docs/elements/store/sql) — tables CDC watches
|
|
737
760
|
- [HTTP · Live Streams](/docs/elements/flow/http#live-streams) — `signal.live` SSE
|
|
738
761
|
- [fx](/docs/reference/fx) — `fx.emit`, `fx.deadLetters`, `fx.clock`
|
|
739
|
-
- [Errors](/docs/reference/errors) — OKE1070 · OKE1071 · OKE1240 · OKE1250
|
|
762
|
+
- [Errors](/docs/reference/errors) — OKE1070 · OKE1071 · OKE1072 · OKE1240 · OKE1250
|
|
740
763
|
- [Workflows](/docs/elements/flow/workflows) — `durable: true` + `fx.step` on a consumer
|
|
741
764
|
|
|
742
765
|
## Next
|
|
@@ -344,9 +344,9 @@ A bare `404` with body `Not Found` means **no route matched** — not `fx.fail("
|
|
|
344
344
|
</Accordion>
|
|
345
345
|
|
|
346
346
|
<Accordion title="Name stamping">
|
|
347
|
-
Prefer nameless `flow({ do })` on tree files —
|
|
348
|
-
Signal / Clock
|
|
349
|
-
([
|
|
347
|
+
Prefer nameless `flow({ do })` on HTTP tree files — `src/flows/notes/[id]/get.ts` +
|
|
348
|
+
`export const get` stamps `notes.get`. Signal / Clock workers pass `flow("name", { do })`
|
|
349
|
+
(**OKE1072**; Clock inline is [Clock · Inline or named export](/docs/elements/clock#inline-or-named-export)).
|
|
350
350
|
</Accordion>
|
|
351
351
|
|
|
352
352
|
</Accordions>
|
|
@@ -570,12 +570,9 @@ Default is `true` once `gate.auth.tenant` is on.
|
|
|
570
570
|
</Accordion>
|
|
571
571
|
|
|
572
572
|
<Accordion title="OKE1020 — no declared effects">
|
|
573
|
-
Cause: `Flow "{flow}" has no declared effects and no Manifest to derive them from.`
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
{}`. Real `fx` usage still needs inference (app source) or an explicit non-empty `effects` block
|
|
577
|
-
when extract cannot see the body. If extract failed (`Manifest extract failed`), install
|
|
578
|
-
`oxc-parser` and retry.
|
|
573
|
+
Cause: `Flow "{flow}" has no declared effects and no Manifest to derive them from.` Extract
|
|
574
|
+
failures append `Manifest extract failed — …`. Boot with `oke dev` / `oke build`; install
|
|
575
|
+
`oxc-parser` if extract cannot load. On Windows use `bun run dev` / `bunx oke dev`.
|
|
579
576
|
</Accordion>
|
|
580
577
|
|
|
581
578
|
</Accordions>
|
|
@@ -455,18 +455,9 @@ 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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
| Style | Flow name |
|
|
462
|
-
| ------------------------------- | ----------------------- |
|
|
463
|
-
| `flow({ do })` (no unit folder) | the Signal / Clock name |
|
|
464
|
-
| `flow("orders.fulfill")` | the string you passed |
|
|
465
|
-
| `src/flows/notes/on-created.ts` | `notes.onCreated` |
|
|
466
|
-
|
|
467
|
-
Worked examples: [Signal · Flow name](/docs/elements/signal#flow-name) ·
|
|
468
|
-
[Clock · Flow name](/docs/elements/clock#flow-name). Collision fails **OKE1070**.
|
|
469
|
-
HTTP has no trigger name of this kind — nameless HTTP stays for the tree or **OKE1045**.
|
|
458
|
+
Signal / Clock consumers pass an explicit `flow("…")` name (**OKE1072** if nameless
|
|
459
|
+
outside `src/flows/<unit>/`; **OKE1070** on collision). Clock may write `clock.every(…)`
|
|
460
|
+
inside `on()` — [Clock · Inline or named export](/docs/elements/clock#inline-or-named-export).
|
|
470
461
|
|
|
471
462
|
## Runtime Matching
|
|
472
463
|
|
|
@@ -524,8 +515,13 @@ plus a handwritten `http.get("/notes")`.
|
|
|
524
515
|
</Accordion>
|
|
525
516
|
|
|
526
517
|
<Accordion title="OKE1070 — flow name defined twice">
|
|
527
|
-
Cause: `Flow "{flow}" is defined twice.` Two
|
|
528
|
-
|
|
518
|
+
Cause: `Flow "{flow}" is defined twice.` Two explicit `flow("…")` calls collide, or two tree
|
|
519
|
+
exports stamp the same `unit.export`. Give at least one a distinct name or tree export.
|
|
520
|
+
</Accordion>
|
|
521
|
+
|
|
522
|
+
<Accordion title="OKE1072 — Signal or Clock flow unnamed">
|
|
523
|
+
Cause: `A {kind} flow on "{trigger}" has no name.`
|
|
524
|
+
Fix: pass an explicit name — `on(handle, flow("unit.export", { do }))`.
|
|
529
525
|
</Accordion>
|
|
530
526
|
|
|
531
527
|
<Accordion title="422 — path param missing from in">
|
|
@@ -550,7 +546,7 @@ plus a handwritten `http.get("/notes")`.
|
|
|
550
546
|
|
|
551
547
|
- [HTTP](/docs/elements/flow/http) — verbs, envelopes, `http.resource`, live SSE
|
|
552
548
|
- [Client](/docs/client/calling) — `api.notes.get`, REST vs RPC, `$routes`
|
|
553
|
-
- [Errors](/docs/reference/errors) — OKE1040 · OKE1030 · OKE1041 · OKE1045 · OKE1070
|
|
549
|
+
- [Errors](/docs/reference/errors) — OKE1040 · OKE1030 · OKE1041 · OKE1045 · OKE1070 · OKE1072
|
|
554
550
|
- [The Architecture](/docs/understand/the-architecture) — derived routes, no hand-written table
|
|
555
551
|
- [Gate](/docs/elements/gate) — `.gate(...)` / `.public()` on the same trigger
|
|
556
552
|
|
|
@@ -466,6 +466,11 @@ dead-letter queue for exhausted retries in the queue sense — see
|
|
|
466
466
|
Cause: `"{resource}": {detail}`. Align the payload with `schema` — no subscriber started.
|
|
467
467
|
</Accordion>
|
|
468
468
|
|
|
469
|
+
<Accordion title="OKE1072 — Signal flow unnamed">
|
|
470
|
+
Cause: `A signal flow on "{trigger}" has no name.`
|
|
471
|
+
Fix: pass an explicit name — `on(handle, flow("cache.purgeLocal", { do }))`.
|
|
472
|
+
</Accordion>
|
|
473
|
+
|
|
469
474
|
<Accordion title="Expecting retries / DLQ like a queue">
|
|
470
475
|
Use `signal.once`. Broadcast fan-out is not the lease + DLQ path documented under
|
|
471
476
|
[Once](/docs/elements/signal/once).
|
|
@@ -492,7 +497,7 @@ dead-letter queue for exhausted retries in the queue sense — see
|
|
|
492
497
|
- [Consumers](/docs/elements/flow/consumers) — binding `on(signal)`
|
|
493
498
|
- [HTTP · Live Streams](/docs/elements/flow/http#live-streams) — when the consumer is a browser
|
|
494
499
|
- [fx](/docs/reference/fx) — `fx.emit`
|
|
495
|
-
- [Errors](/docs/reference/errors) — OKE1240 · OKE1250
|
|
500
|
+
- [Errors](/docs/reference/errors) — OKE1072 · OKE1240 · OKE1250
|
|
496
501
|
|
|
497
502
|
## Next
|
|
498
503
|
|