okengine 0.19.2 → 0.19.3
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/clock/index.mdx +144 -3
- package/site/content/docs/elements/flow/consumers.mdx +79 -59
- package/site/content/docs/elements/flow/index.mdx +2 -2
- package/site/content/docs/elements/flow/routing.mdx +12 -5
- package/site/content/docs/elements/signal/broadcast.mdx +9 -13
- package/site/content/docs/elements/signal/index.mdx +236 -17
- package/site/content/docs/elements/signal/live.mdx +5 -13
- package/site/content/docs/elements/signal/once.mdx +83 -28
- package/site/content/docs/reference/errors.mdx +34 -27
- package/src/compiler/extract.test.ts +47 -0
- package/src/compiler/extract.ts +28 -0
- package/src/console/ui-next/dist/assets/{access-page-DceEWH9u.js → access-page-BpugjHHY.js} +1 -1
- package/src/console/ui-next/dist/assets/{agent-disclosure-tC9s2VFd.js → agent-disclosure-qnsmtJhf.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-C-naNQSR.js → cache-glyph-BcpWUM98.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-DnZ_MlDn.js → call-pii-button-Bz5xiNmR.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-RekgR6Qz.js → collapsible-Cuxn2WH8.js} +1 -1
- package/src/console/ui-next/dist/assets/{duration-tone-DugtWBS0.js → duration-tone-Bjnl3EaM.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-DVmn1ZuQ.js → flows-page-DVujp-T2.js} +1 -1
- package/src/console/ui-next/dist/assets/{highlighted-json-CvBGaiSD.js → highlighted-json-Bql0qlqW.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-D7_OXbdC.js → http-method-BljvrfRg.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-DH0K2f6N.js → index-D1vE656k.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-qJzF2nSN.js → observability-page-Bdi0bLJl.js} +1 -1
- package/src/console/ui-next/dist/assets/{replica-lag-Vqk0pUBA.js → replica-lag-CpkPfITG.js} +1 -1
- package/src/console/ui-next/dist/assets/{request-meta-BtShi4sG.js → request-meta-C4ZVNFVt.js} +1 -1
- package/src/console/ui-next/dist/assets/{store-page-CMsYH_vH.js → store-page-DAHtnesC.js} +1 -1
- package/src/console/ui-next/dist/assets/{trace-detail-sheet-ycFB2uua.js → trace-detail-sheet-B2c9QRBw.js} +1 -1
- package/src/console/ui-next/dist/assets/{tree-expand-toggle-iG1jcWgU.js → tree-expand-toggle-BP4tRC98.js} +1 -1
- package/src/console/ui-next/dist/assets/{units-page-Bavchke4.js → units-page-Oi6--n09.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-DxUFhiZI.js → vault-page-Dt-cPkiU.js} +1 -1
- package/src/console/ui-next/dist/index.html +1 -1
- package/src/kernel/app.ts +64 -0
- package/src/kernel/errors-once-signal.ts +25 -0
- package/src/kernel/errors.registry.test.ts +8 -0
- package/src/kernel/once-signal.test.ts +71 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "okengine",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.3",
|
|
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": {
|
|
@@ -80,8 +80,142 @@ Under `drivers.clock.test = "frozen"`, advance time in tests instead of waiting
|
|
|
80
80
|
| `on(clock.every("name", "1h"), flow({ do }))` | Self-contained — nothing else needs the Clock handle |
|
|
81
81
|
| `export const x = clock.every("name", "1h")` | Another file (or a second `on()`) must reuse the same declaration |
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
Both styles stamp the same Manifest `flow.trigger`. The choice is where the
|
|
84
|
+
declaration lives, not two runtimes.
|
|
85
|
+
|
|
86
|
+
What the Flow is called is a separate choice — [Flow name](#flow-name).
|
|
87
|
+
|
|
88
|
+
<Tabs items={["Inline", "Named"]}>
|
|
89
|
+
|
|
90
|
+
<Tab value="Inline">
|
|
91
|
+
|
|
92
|
+
One file — declare and bind together. The scheduler fires it; no other file
|
|
93
|
+
imports the handle:
|
|
94
|
+
|
|
95
|
+
```typescript title="src/flows/health/ping.ts"
|
|
96
|
+
import { on, flow, clock } from "okengine";
|
|
97
|
+
|
|
98
|
+
export const pingExternal = on(
|
|
99
|
+
clock.every("health.pingExternal", "30s"),
|
|
100
|
+
flow({
|
|
101
|
+
plane: "operator",
|
|
102
|
+
do: async (_, fx) => {
|
|
103
|
+
await fx.call(pingUpstream);
|
|
104
|
+
},
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
</Tab>
|
|
110
|
+
|
|
111
|
+
<Tab value="Named">
|
|
112
|
+
|
|
113
|
+
Export the handle when another file (or a second `on()`) must reuse the same
|
|
114
|
+
declaration:
|
|
115
|
+
|
|
116
|
+
```typescript title="src/clocks/digest.ts"
|
|
117
|
+
import { clock } from "okengine";
|
|
118
|
+
|
|
119
|
+
export const digestClock = clock.every("notes.digest", "1d");
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
```typescript title="src/flows/notes/digest.ts"
|
|
123
|
+
import { on, flow } from "okengine";
|
|
124
|
+
import { digestClock } from "@/clocks/digest";
|
|
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";
|
|
183
|
+
|
|
184
|
+
export const sweep = on(
|
|
185
|
+
clock.every("metrics.cleanup", "1h"),
|
|
186
|
+
flow("ops.sweep", {
|
|
187
|
+
plane: "operator",
|
|
188
|
+
do: async (_, fx) => {
|
|
189
|
+
await fx.call(sweepMetrics);
|
|
190
|
+
},
|
|
191
|
+
}),
|
|
192
|
+
);
|
|
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
|
+
|
|
202
|
+
```typescript title="src/flows/ops/sweep.ts"
|
|
203
|
+
import { on, flow, clock } from "okengine";
|
|
204
|
+
|
|
205
|
+
export const sweep = on(
|
|
206
|
+
clock.every("cleanup", "10m"),
|
|
207
|
+
flow({
|
|
208
|
+
plane: "operator",
|
|
209
|
+
do: async (_, fx) => {
|
|
210
|
+
await fx.call(sweepMetrics);
|
|
211
|
+
},
|
|
212
|
+
}),
|
|
213
|
+
);
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
</Tab>
|
|
217
|
+
|
|
218
|
+
</Tabs>
|
|
85
219
|
|
|
86
220
|
## Progressive Patterns
|
|
87
221
|
|
|
@@ -279,6 +413,12 @@ export default defineConfig({
|
|
|
279
413
|
declaration in source.
|
|
280
414
|
</Accordion>
|
|
281
415
|
|
|
416
|
+
<Accordion title="OKE1070 — flow name defined twice">
|
|
417
|
+
Cause: `Flow "{flow}" is defined twice.` Two nameless consumers inherited the same Clock name, or
|
|
418
|
+
two explicit `flow("…")` calls collide. Give at least one a distinct `flow("…")` or tree export —
|
|
419
|
+
[Flow name](#flow-name).
|
|
420
|
+
</Accordion>
|
|
421
|
+
|
|
282
422
|
</Accordions>
|
|
283
423
|
|
|
284
424
|
## Learn more
|
|
@@ -288,7 +428,8 @@ export default defineConfig({
|
|
|
288
428
|
- [Consumers · Clock Jobs](/docs/elements/flow/consumers#clock-jobs) — bind with `on(clockDecl, flow)`
|
|
289
429
|
- [Workflows](/docs/elements/flow/workflows) — `durable: true` + `fx.step` around sleeps
|
|
290
430
|
- [fx](/docs/reference/fx) — full `fx.clock` table
|
|
291
|
-
- [
|
|
431
|
+
- [Routing](/docs/elements/flow/routing#names) — tree `unit.export` vs inherit
|
|
432
|
+
- [Errors](/docs/reference/errors) — `ScheduleNotOverridableError` · `ClockResourceNotFoundError` · OKE1070
|
|
292
433
|
|
|
293
434
|
## Next
|
|
294
435
|
|
|
@@ -17,9 +17,11 @@ For developers wiring background work on okengine — bind the trigger, keep `do
|
|
|
17
17
|
|
|
18
18
|
## Smallest Example
|
|
19
19
|
|
|
20
|
-
<
|
|
20
|
+
<Callout title="Bind and emit are independent">
|
|
21
|
+
The consumer and the producer share one Signal handle. They can live in different files, written
|
|
22
|
+
in any order — emit is not "step 2" after bind.
|
|
23
|
+
</Callout>
|
|
21
24
|
|
|
22
|
-
<Step>
|
|
23
25
|
### Bind a Signal consumer
|
|
24
26
|
|
|
25
27
|
```typescript title="src/flows/notifications/welcome.ts"
|
|
@@ -40,9 +42,6 @@ export const sendWelcome = on(
|
|
|
40
42
|
);
|
|
41
43
|
```
|
|
42
44
|
|
|
43
|
-
</Step>
|
|
44
|
-
|
|
45
|
-
<Step>
|
|
46
45
|
### Emit from any Flow
|
|
47
46
|
|
|
48
47
|
```typescript
|
|
@@ -52,10 +51,6 @@ await fx.emit(userSignedUp, { userId: "usr_123", email: "alice@example.com" });
|
|
|
52
51
|
The compiler records `emits: ["users.signed-up"]` on the producer. The consumer runs after the
|
|
53
52
|
emit commits — the HTTP request does not wait for the welcome mail.
|
|
54
53
|
|
|
55
|
-
</Step>
|
|
56
|
-
|
|
57
|
-
</Steps>
|
|
58
|
-
|
|
59
54
|
<Callout title="Jobs are consumers">
|
|
60
55
|
A named Clock bound with `on(clockDecl, flow)` is the same species — an asynchronous Flow. There
|
|
61
56
|
is no separate job runner. See [Clock jobs](#clock-jobs).
|
|
@@ -195,8 +190,9 @@ Omit `key` for competing consumers with no ordering.
|
|
|
195
190
|
`signal.live` is an HTTP SSE tape — bind it with [`http.live`](/docs/elements/flow/http#live-streams),
|
|
196
191
|
not as a worker. A Flow with no trigger is [call-only](/docs/elements/flow).
|
|
197
192
|
|
|
198
|
-
|
|
199
|
-
|
|
193
|
+
One-file inline vs exported handle: [Signal](/docs/elements/signal#inline-or-named-export) ·
|
|
194
|
+
[Clock](/docs/elements/clock#inline-or-named-export). Flow name (inherit / explicit / tree):
|
|
195
|
+
[Signal](/docs/elements/signal#flow-name) · [Clock](/docs/elements/clock#flow-name). Collision is **OKE1070**.
|
|
200
196
|
|
|
201
197
|
## Signal Consumers
|
|
202
198
|
|
|
@@ -212,8 +208,12 @@ Each emit is handled according to the Signal helper you declared. The Flow is th
|
|
|
212
208
|
|
|
213
209
|
<Tab value="Once">
|
|
214
210
|
|
|
215
|
-
Competing workers — exactly one consumer
|
|
216
|
-
dead-letter when `deadLetter` is true (default)
|
|
211
|
+
Competing workers — exactly one consumer claims each message. Failed attempts retry, then
|
|
212
|
+
dead-letter when `deadLetter` is true (default).
|
|
213
|
+
|
|
214
|
+
Two different Flows on one `once` signal fail **OKE1071** — see
|
|
215
|
+
[Once · Competing consumers](/docs/elements/signal/once#competing-consumers-once-vs-broadcast).
|
|
216
|
+
For every bound Flow to run, use [`signal.broadcast`](/docs/elements/signal/broadcast).
|
|
217
217
|
|
|
218
218
|
```typescript title="src/signals/orders.ts"
|
|
219
219
|
import { signal } from "okengine";
|
|
@@ -489,49 +489,34 @@ do not fire.
|
|
|
489
489
|
## CDC
|
|
490
490
|
|
|
491
491
|
<Callout title="Detailed section">
|
|
492
|
-
If you only need any-write, jump to
|
|
492
|
+
If you only need any-write, jump to Bare or enriched below. The handle is
|
|
493
493
|
`db.table(table).changed(column?)` — `db` is a `store.sql` declaration, `table` is a schema
|
|
494
494
|
handle. `changed("insert")` is **not** an op filter; it stamps a column named `insert`.
|
|
495
495
|
</Callout>
|
|
496
496
|
|
|
497
497
|
SQL writes through `fx.store` notify CDC after commit. The Flow input is always
|
|
498
|
-
`{ before, after
|
|
499
|
-
only `{ before, after }` when that's all you need.
|
|
498
|
+
`{ before, after, table, action, id }` (`CdcPayload`).
|
|
500
499
|
|
|
501
|
-
|
|
500
|
+
### Bare or enriched
|
|
502
501
|
|
|
503
|
-
|
|
502
|
+
| Style | When |
|
|
503
|
+
| ------------------------- | ---------------------------------------------------------------------- |
|
|
504
|
+
| `({ before, after })` | Bound to one table — images are enough (search reindex, listing cache) |
|
|
505
|
+
| `({ table, action, id })` | Log, route, or branch — kind of change and which record (audit log) |
|
|
504
506
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
+
Both styles receive the same object. There is no second dispatch path, no
|
|
508
|
+
performance difference, and no correctness difference — the choice is which
|
|
509
|
+
fields this handler destructures.
|
|
507
510
|
|
|
508
|
-
|
|
509
|
-
|
|
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
|
-
```
|
|
511
|
+
`{ table, action, id }` are always populated; omitting them from `do` does not
|
|
512
|
+
drop them from the payload.
|
|
529
513
|
|
|
530
|
-
|
|
514
|
+
<Tabs items={["Bare", "Enriched", "Images"]}>
|
|
531
515
|
|
|
532
516
|
<Tab value="Bare">
|
|
533
517
|
|
|
534
|
-
|
|
518
|
+
Bound to `notes` — the table is already in the trigger. Images decide upsert vs
|
|
519
|
+
drop; the row's `id` is on the surviving image:
|
|
535
520
|
|
|
536
521
|
```typescript title="src/flows/search/reindex.ts"
|
|
537
522
|
import { on, flow } from "okengine";
|
|
@@ -553,25 +538,30 @@ export const reindexNotes = on(
|
|
|
553
538
|
);
|
|
554
539
|
```
|
|
555
540
|
|
|
556
|
-
|
|
557
|
-
same payload — filter in `do` when you only care about that field:
|
|
541
|
+
</Tab>
|
|
558
542
|
|
|
559
|
-
|
|
543
|
+
<Tab value="Enriched">
|
|
544
|
+
|
|
545
|
+
`table` is the real table name. `action` is `"created"` / `"updated"` / `"deleted"`.
|
|
546
|
+
`id` is the declared primary-key value — not a hardcoded `"id"` column:
|
|
547
|
+
|
|
548
|
+
```typescript title="src/flows/audit/users.ts"
|
|
560
549
|
import { on, flow } from "okengine";
|
|
561
550
|
import { db } from "@/core";
|
|
562
|
-
import {
|
|
551
|
+
import { users, auditLogs } from "@/schema";
|
|
563
552
|
|
|
564
|
-
export const
|
|
565
|
-
db.table(
|
|
566
|
-
flow("
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
553
|
+
export const onUserWrite = on(
|
|
554
|
+
db.table(users).changed(),
|
|
555
|
+
flow("audit.users", {
|
|
556
|
+
do: async ({ table, action, id }, fx) => {
|
|
557
|
+
await fx
|
|
558
|
+
.store(db)
|
|
559
|
+
.insert(auditLogs)
|
|
560
|
+
.values({
|
|
561
|
+
table,
|
|
562
|
+
recordId: String(id),
|
|
563
|
+
action,
|
|
564
|
+
});
|
|
575
565
|
},
|
|
576
566
|
}),
|
|
577
567
|
);
|
|
@@ -605,6 +595,30 @@ do: async ({ before, after, action }, fx) => {
|
|
|
605
595
|
|
|
606
596
|
</Tabs>
|
|
607
597
|
|
|
598
|
+
`changed("status")` stamps `trigger.cdc.column` on the Manifest. Still the same
|
|
599
|
+
payload — filter in `do` when you only care about that field:
|
|
600
|
+
|
|
601
|
+
```typescript title="src/flows/tasks/on-status.ts"
|
|
602
|
+
import { on, flow } from "okengine";
|
|
603
|
+
import { db } from "@/core";
|
|
604
|
+
import { tasks } from "@/schema";
|
|
605
|
+
|
|
606
|
+
export const onStatus = on(
|
|
607
|
+
db.table(tasks).changed("status"),
|
|
608
|
+
flow("tasks.onStatus", {
|
|
609
|
+
plane: "operator",
|
|
610
|
+
do: async ({ before, after, id }, fx) => {
|
|
611
|
+
if (before?.status === after?.status) return;
|
|
612
|
+
await fx.emit(taskStatusChanged, {
|
|
613
|
+
id,
|
|
614
|
+
from: before?.status ?? null,
|
|
615
|
+
to: after?.status ?? null,
|
|
616
|
+
});
|
|
617
|
+
},
|
|
618
|
+
}),
|
|
619
|
+
);
|
|
620
|
+
```
|
|
621
|
+
|
|
608
622
|
<Accordions>
|
|
609
623
|
|
|
610
624
|
<Accordion title="CDC payload">
|
|
@@ -665,6 +679,12 @@ Clock drivers: **postgres** in `dev`/`prod`, **frozen** in `test`. Signal driver
|
|
|
665
679
|
explicit `flow("…")` or a distinct tree export.
|
|
666
680
|
</Accordion>
|
|
667
681
|
|
|
682
|
+
<Accordion title="OKE1071 — once signal bound to more than one Flow">
|
|
683
|
+
Cause: `Once signal "{signal}" is bound to more than one Flow ({flows}).` Use `signal.broadcast`
|
|
684
|
+
if each Flow should independently receive this event, or bind only one Flow. See [Once · Competing
|
|
685
|
+
consumers](/docs/elements/signal/once#competing-consumers-once-vs-broadcast).
|
|
686
|
+
</Accordion>
|
|
687
|
+
|
|
668
688
|
<Accordion title="OKE1240 — emit with no subscriber">
|
|
669
689
|
Cause: `Flow "{flow}" emits signal "{resource}" with no subscriber.` Add `on(signal, flow)` or
|
|
670
690
|
set `{ optional: true }` on the Signal (live firehoses, unused hooks).
|
|
@@ -716,7 +736,7 @@ Clock drivers: **postgres** in `dev`/`prod`, **frozen** in `test`. Signal driver
|
|
|
716
736
|
- [Store · SQL](/docs/elements/store/sql) — tables CDC watches
|
|
717
737
|
- [HTTP · Live Streams](/docs/elements/flow/http#live-streams) — `signal.live` SSE
|
|
718
738
|
- [fx](/docs/reference/fx) — `fx.emit`, `fx.deadLetters`, `fx.clock`
|
|
719
|
-
- [Errors](/docs/reference/errors) — OKE1070 · OKE1240 · OKE1250
|
|
739
|
+
- [Errors](/docs/reference/errors) — OKE1070 · OKE1071 · OKE1240 · OKE1250
|
|
720
740
|
- [Workflows](/docs/elements/flow/workflows) — `durable: true` + `fx.step` on a consumer
|
|
721
741
|
|
|
722
742
|
## Next
|
|
@@ -345,8 +345,8 @@ A bare `404` with body `Not Found` means **no route matched** — not `fx.fail("
|
|
|
345
345
|
|
|
346
346
|
<Accordion title="Name stamping">
|
|
347
347
|
Prefer nameless `flow({ do })` on tree files — the file stamps `unit.export`.
|
|
348
|
-
|
|
349
|
-
|
|
348
|
+
Signal / Clock inherit the trigger name only outside a unit folder
|
|
349
|
+
([Signal · Flow name](/docs/elements/signal#flow-name)). Nameless HTTP after adopt fails **OKE1045**.
|
|
350
350
|
</Accordion>
|
|
351
351
|
|
|
352
352
|
</Accordions>
|
|
@@ -455,11 +455,18 @@ 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
|
-
|
|
462
|
-
|
|
458
|
+
Nameless Signal / Clock consumers take the trigger name only when the tree does
|
|
459
|
+
not stamp `unit.export`:
|
|
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**.
|
|
463
470
|
|
|
464
471
|
## Runtime Matching
|
|
465
472
|
|
|
@@ -21,10 +21,12 @@ more `on(signal, flow)` subscribers, emit with `fx.emit`.
|
|
|
21
21
|
|
|
22
22
|
## Smallest Example
|
|
23
23
|
|
|
24
|
-
<
|
|
24
|
+
<Callout title="One handle, three independent uses">
|
|
25
|
+
`cacheInvalidated` is a shared const. Declare it, bind any number of subscribers, and emit —
|
|
26
|
+
different files, any order. Bind and emit do not have a required sequence.
|
|
27
|
+
</Callout>
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
### Define the broadcast signal
|
|
29
|
+
### Declare
|
|
28
30
|
|
|
29
31
|
```typescript title="src/signals/cache.ts"
|
|
30
32
|
import { signal } from "okengine";
|
|
@@ -35,9 +37,6 @@ export const cacheInvalidated = signal.broadcast("cache.invalidated", {
|
|
|
35
37
|
});
|
|
36
38
|
```
|
|
37
39
|
|
|
38
|
-
</Step>
|
|
39
|
-
|
|
40
|
-
<Step>
|
|
41
40
|
### Bind a subscriber
|
|
42
41
|
|
|
43
42
|
```typescript title="src/flows/cache/purge.ts"
|
|
@@ -54,10 +53,11 @@ export const purgeLocalCache = on(
|
|
|
54
53
|
);
|
|
55
54
|
```
|
|
56
55
|
|
|
57
|
-
|
|
56
|
+
Multiple Flows may bind the same handle — every one gets a copy. That is fan-out, not a race.
|
|
57
|
+
See [Once · Competing consumers](/docs/elements/signal/once#competing-consumers-once-vs-broadcast)
|
|
58
|
+
when you meant a work queue instead.
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
### Emit from any Flow
|
|
60
|
+
### Emit
|
|
61
61
|
|
|
62
62
|
```typescript title="src/flows/skus/[sku]/update.ts"
|
|
63
63
|
import { on, flow, http } from "okengine";
|
|
@@ -81,10 +81,6 @@ export const update = on(
|
|
|
81
81
|
The compiler records `emits: ["cache.invalidated"]` on the producer. Emit resolves when the
|
|
82
82
|
outbox commits — the HTTP request does not wait for every subscriber to finish.
|
|
83
83
|
|
|
84
|
-
</Step>
|
|
85
|
-
|
|
86
|
-
</Steps>
|
|
87
|
-
|
|
88
84
|
<Callout title="Same handle everywhere">
|
|
89
85
|
Import the declared Signal handle (or the same name) in every subscriber and producer. A typo in
|
|
90
86
|
the name creates a different Manifest entry — fan-out never crosses names.
|