okengine 0.19.2 → 0.19.4

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.
Files changed (50) hide show
  1. package/package.json +1 -1
  2. package/site/content/docs/elements/channel/index.mdx +1 -1
  3. package/site/content/docs/elements/clock/index.mdx +106 -17
  4. package/site/content/docs/elements/clock/schedules.mdx +39 -11
  5. package/site/content/docs/elements/flow/consumers.mdx +129 -86
  6. package/site/content/docs/elements/flow/index.mdx +3 -3
  7. package/site/content/docs/elements/flow/routing.mdx +11 -8
  8. package/site/content/docs/elements/signal/broadcast.mdx +15 -14
  9. package/site/content/docs/elements/signal/index.mdx +99 -36
  10. package/site/content/docs/elements/signal/live.mdx +5 -13
  11. package/site/content/docs/elements/signal/once.mdx +88 -28
  12. package/site/content/docs/reference/errors.mdx +41 -27
  13. package/site/content/docs/understand/the-architecture.mdx +2 -2
  14. package/src/cli/competitor-mention-removal.test.ts +3 -3
  15. package/src/compiler/extract.test.ts +237 -15
  16. package/src/compiler/extract.ts +75 -4
  17. package/src/compiler/flow-path.test.ts +23 -0
  18. package/src/compiler/flow-path.ts +17 -1
  19. package/src/console/ui-next/dist/assets/{access-page-DceEWH9u.js → access-page-rO2HLPab.js} +1 -1
  20. package/src/console/ui-next/dist/assets/{agent-disclosure-tC9s2VFd.js → agent-disclosure-ae8w8JLx.js} +1 -1
  21. package/src/console/ui-next/dist/assets/{cache-glyph-C-naNQSR.js → cache-glyph-C-uHdh5d.js} +1 -1
  22. package/src/console/ui-next/dist/assets/{call-pii-button-DnZ_MlDn.js → call-pii-button-BJH54w4s.js} +1 -1
  23. package/src/console/ui-next/dist/assets/{collapsible-RekgR6Qz.js → collapsible-CaE8cs9p.js} +1 -1
  24. package/src/console/ui-next/dist/assets/{duration-tone-DugtWBS0.js → duration-tone-DdOQkQVK.js} +1 -1
  25. package/src/console/ui-next/dist/assets/{flows-page-DVmn1ZuQ.js → flows-page-DWeBszQQ.js} +1 -1
  26. package/src/console/ui-next/dist/assets/{highlighted-json-CvBGaiSD.js → highlighted-json-B2gBsC4B.js} +1 -1
  27. package/src/console/ui-next/dist/assets/{http-method-D7_OXbdC.js → http-method-BnMl0dJt.js} +1 -1
  28. package/src/console/ui-next/dist/assets/{index-DH0K2f6N.js → index-CBAP48v5.js} +3 -3
  29. package/src/console/ui-next/dist/assets/index-VxoEz295.css +2 -0
  30. package/src/console/ui-next/dist/assets/{observability-page-qJzF2nSN.js → observability-page-DVpcJEnW.js} +1 -1
  31. package/src/console/ui-next/dist/assets/{replica-lag-Vqk0pUBA.js → replica-lag-BUQf2Nf6.js} +1 -1
  32. package/src/console/ui-next/dist/assets/{request-meta-BtShi4sG.js → request-meta-CPkGDQgT.js} +1 -1
  33. package/src/console/ui-next/dist/assets/{store-page-CMsYH_vH.js → store-page-CwS_cY4V.js} +1 -1
  34. package/src/console/ui-next/dist/assets/{trace-detail-sheet-ycFB2uua.js → trace-detail-sheet-DNX7Y2bd.js} +1 -1
  35. package/src/console/ui-next/dist/assets/{tree-expand-toggle-iG1jcWgU.js → tree-expand-toggle-Bhdk27aF.js} +1 -1
  36. package/src/console/ui-next/dist/assets/{units-page-Bavchke4.js → units-page-CsBh0XbI.js} +1 -1
  37. package/src/console/ui-next/dist/assets/{vault-page-DxUFhiZI.js → vault-page-CsTlPtK9.js} +1 -1
  38. package/src/console/ui-next/dist/index.html +2 -2
  39. package/src/kernel/app.ts +87 -1
  40. package/src/kernel/errors-flow-name.ts +20 -4
  41. package/src/kernel/errors-once-signal.ts +25 -0
  42. package/src/kernel/errors.registry.test.ts +12 -2
  43. package/src/kernel/flow-name.test.ts +105 -20
  44. package/src/kernel/flow.ts +3 -3
  45. package/src/kernel/on.ts +0 -5
  46. package/src/kernel/once-signal.test.ts +71 -0
  47. package/src/kernel/stamp-http.test.ts +2 -1
  48. package/src/kernel/stamp-http.ts +5 -7
  49. package/src/kernel/unit.ts +1 -2
  50. 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.2",
3
+ "version": "0.19.4",
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": {
@@ -57,7 +57,7 @@ import { noteCreated } from "./signals";
57
57
 
58
58
  export const onCreated = on(
59
59
  noteCreated,
60
- flow({
60
+ flow("notes.onCreated", {
61
61
  do: async (payload, fx) => {
62
62
  await fx.send(noteCreatedMail, {
63
63
  to: "you@localhost",
@@ -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 for
62
- math). Map to ISO on the wire when the Flow returns a human-facing instant
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,19 +68,88 @@ 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
- `on(clockDecl, flow)` is the same species as Signal consumers and HTTP handlers. There is no
73
- separate job runner — see [Consumers · Clock Jobs](/docs/elements/flow/consumers#clock-jobs).
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 | 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 |
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
- 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**.
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>
87
+
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.
90
+
91
+ <Tabs items={["Inline", "Named"]}>
92
+
93
+ <Tab value="Inline">
94
+
95
+ One file — declare and bind together. The scheduler fires it; no other Flow
96
+ binds this cadence:
97
+
98
+ ```typescript title="src/flows/health/ping.ts"
99
+ import { on, flow, clock } from "okengine";
100
+
101
+ export const pingExternal = on(
102
+ clock.every("health.pingExternal", "30s"),
103
+ flow("health.pingExternal", {
104
+ plane: "operator",
105
+ do: async (_, fx) => {
106
+ await fx.call(pingUpstream);
107
+ },
108
+ }),
109
+ );
110
+ ```
111
+
112
+ </Tab>
113
+
114
+ <Tab value="Named">
115
+
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:
118
+
119
+ ```typescript title="src/clocks/metrics.ts"
120
+ import { clock } from "okengine";
121
+
122
+ export const tickClock = clock.every("metrics.tick", "1h");
123
+ ```
124
+
125
+ ```typescript title="src/flows/ops/metrics.ts"
126
+ import { on, flow } from "okengine";
127
+ import { tickClock } from "@/clocks/metrics";
128
+
129
+ export const sweep = on(
130
+ tickClock,
131
+ flow("ops.sweep", {
132
+ plane: "operator",
133
+ do: async (_, fx) => {
134
+ await fx.call(sweepMetrics);
135
+ },
136
+ }),
137
+ );
138
+
139
+ export const report = on(
140
+ tickClock,
141
+ flow("ops.report", {
142
+ plane: "operator",
143
+ do: async (_, fx) => {
144
+ await fx.call(reportMetrics);
145
+ },
146
+ }),
147
+ );
148
+ ```
149
+
150
+ </Tab>
151
+
152
+ </Tabs>
85
153
 
86
154
  ## Progressive Patterns
87
155
 
@@ -93,12 +161,17 @@ Same `clock` + `fx.clock` from a calendar cron to an interval, a durable pause,
93
161
 
94
162
  Five-field cron plus helpers. Prefer an app-wide zone so schedules stay short:
95
163
 
96
- ```typescript title="src/flows/reports/daily.ts"
97
- import { on, flow, clock } from "okengine";
164
+ ```typescript title="src/clocks/reports.ts"
165
+ import { clock } from "okengine";
98
166
 
99
167
  export const dailyReportClock = clock.daily("reports.daily", {
100
168
  at: "06:00",
101
169
  });
170
+ ```
171
+
172
+ ```typescript title="src/flows/reports/daily.ts"
173
+ import { on, flow } from "okengine";
174
+ import { dailyReportClock } from "@/clocks/reports";
102
175
 
103
176
  export const runDailyReport = on(
104
177
  dailyReportClock,
@@ -121,10 +194,15 @@ see [Schedules](/docs/elements/clock/schedules).
121
194
 
122
195
  Human durations — integer + unit, no weeks: `"200ms"` · `"30s"` · `"5m"` · `"1h"` · `"7d"`:
123
196
 
124
- ```typescript title="src/flows/health/ping.ts"
125
- import { on, flow, clock } from "okengine";
197
+ ```typescript title="src/clocks/health.ts"
198
+ import { clock } from "okengine";
126
199
 
127
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";
128
206
 
129
207
  export const pingExternal = on(
130
208
  pingClock,
@@ -279,6 +357,17 @@ export default defineConfig({
279
357
  declaration in source.
280
358
  </Accordion>
281
359
 
360
+ <Accordion title="OKE1070 — flow name defined twice">
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).
369
+ </Accordion>
370
+
282
371
  </Accordions>
283
372
 
284
373
  ## Learn more
@@ -288,7 +377,7 @@ export default defineConfig({
288
377
  - [Consumers · Clock Jobs](/docs/elements/flow/consumers#clock-jobs) — bind with `on(clockDecl, flow)`
289
378
  - [Workflows](/docs/elements/flow/workflows) — `durable: true` + `fx.step` around sleeps
290
379
  - [fx](/docs/reference/fx) — full `fx.clock` table
291
- - [Errors](/docs/reference/errors) — `ScheduleNotOverridableError` · `ClockResourceNotFoundError`
380
+ - [Errors](/docs/reference/errors) — `ScheduleNotOverridableError` · `ClockResourceNotFoundError` · OKE1070 · OKE1072
292
381
 
293
382
  ## Next
294
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/flows/reports/daily.ts"
29
- import { on, flow, clock } from "okengine";
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/flows/health/ping.ts"
116
- import { on, flow, clock } from "okengine";
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 { on, flow, clock } from "okengine";
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 the returned handle with `on(clockDecl, flow)`. The scheduler invokes `do` with **no
279
- payload** — destructure `_` and read time through `fx.clock`:
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, clock } from "okengine";
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