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.
- package/package.json +1 -1
- package/site/content/docs/elements/channel/index.mdx +1 -1
- package/site/content/docs/elements/clock/index.mdx +106 -17
- package/site/content/docs/elements/clock/schedules.mdx +39 -11
- package/site/content/docs/elements/flow/consumers.mdx +129 -86
- package/site/content/docs/elements/flow/index.mdx +3 -3
- package/site/content/docs/elements/flow/routing.mdx +11 -8
- package/site/content/docs/elements/signal/broadcast.mdx +15 -14
- package/site/content/docs/elements/signal/index.mdx +99 -36
- package/site/content/docs/elements/signal/live.mdx +5 -13
- package/site/content/docs/elements/signal/once.mdx +88 -28
- package/site/content/docs/reference/errors.mdx +41 -27
- package/site/content/docs/understand/the-architecture.mdx +2 -2
- package/src/cli/competitor-mention-removal.test.ts +3 -3
- package/src/compiler/extract.test.ts +237 -15
- package/src/compiler/extract.ts +75 -4
- package/src/compiler/flow-path.test.ts +23 -0
- package/src/compiler/flow-path.ts +17 -1
- package/src/console/ui-next/dist/assets/{access-page-DceEWH9u.js → access-page-rO2HLPab.js} +1 -1
- package/src/console/ui-next/dist/assets/{agent-disclosure-tC9s2VFd.js → agent-disclosure-ae8w8JLx.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-C-naNQSR.js → cache-glyph-C-uHdh5d.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-DnZ_MlDn.js → call-pii-button-BJH54w4s.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-RekgR6Qz.js → collapsible-CaE8cs9p.js} +1 -1
- package/src/console/ui-next/dist/assets/{duration-tone-DugtWBS0.js → duration-tone-DdOQkQVK.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-DVmn1ZuQ.js → flows-page-DWeBszQQ.js} +1 -1
- package/src/console/ui-next/dist/assets/{highlighted-json-CvBGaiSD.js → highlighted-json-B2gBsC4B.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-D7_OXbdC.js → http-method-BnMl0dJt.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-DH0K2f6N.js → index-CBAP48v5.js} +3 -3
- package/src/console/ui-next/dist/assets/index-VxoEz295.css +2 -0
- package/src/console/ui-next/dist/assets/{observability-page-qJzF2nSN.js → observability-page-DVpcJEnW.js} +1 -1
- package/src/console/ui-next/dist/assets/{replica-lag-Vqk0pUBA.js → replica-lag-BUQf2Nf6.js} +1 -1
- package/src/console/ui-next/dist/assets/{request-meta-BtShi4sG.js → request-meta-CPkGDQgT.js} +1 -1
- package/src/console/ui-next/dist/assets/{store-page-CMsYH_vH.js → store-page-CwS_cY4V.js} +1 -1
- package/src/console/ui-next/dist/assets/{trace-detail-sheet-ycFB2uua.js → trace-detail-sheet-DNX7Y2bd.js} +1 -1
- package/src/console/ui-next/dist/assets/{tree-expand-toggle-iG1jcWgU.js → tree-expand-toggle-Bhdk27aF.js} +1 -1
- package/src/console/ui-next/dist/assets/{units-page-Bavchke4.js → units-page-CsBh0XbI.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-DxUFhiZI.js → vault-page-CsTlPtK9.js} +1 -1
- package/src/console/ui-next/dist/index.html +2 -2
- package/src/kernel/app.ts +87 -1
- package/src/kernel/errors-flow-name.ts +20 -4
- package/src/kernel/errors-once-signal.ts +25 -0
- package/src/kernel/errors.registry.test.ts +12 -2
- 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/once-signal.test.ts +71 -0
- 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
|
@@ -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).
|
|
@@ -101,15 +96,21 @@ export const processEmail = on(
|
|
|
101
96
|
|
|
102
97
|
<Tab value="Clock">
|
|
103
98
|
|
|
104
|
-
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
|
+
```
|
|
105
107
|
|
|
106
108
|
```typescript title="src/flows/metrics/cleanup.ts"
|
|
107
|
-
import { on, flow
|
|
109
|
+
import { on, flow } from "okengine";
|
|
108
110
|
import { lt } from "drizzle-orm";
|
|
111
|
+
import { cleanupClock } from "@/clocks/metrics";
|
|
109
112
|
import { db, metricLogs } from "@/schema";
|
|
110
113
|
|
|
111
|
-
export const cleanupClock = clock.every("metrics.cleanup", "1h");
|
|
112
|
-
|
|
113
114
|
export const cleanupMetrics = on(
|
|
114
115
|
cleanupClock,
|
|
115
116
|
flow("metrics.cleanup", {
|
|
@@ -185,18 +186,19 @@ Omit `key` for competing consumers with no ordering.
|
|
|
185
186
|
|
|
186
187
|
## Trigger Reference
|
|
187
188
|
|
|
188
|
-
| Trigger | Signature
|
|
189
|
-
| ---------- |
|
|
190
|
-
| Signal | `on(
|
|
191
|
-
| Clock | `on(clockDecl, flow)` or inline `clock.every(…)`
|
|
192
|
-
| CDC any | `on(db.table(t).changed(), flow)`
|
|
193
|
-
| 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 }` |
|
|
194
195
|
|
|
195
196
|
`signal.live` is an HTTP SSE tape — bind it with [`http.live`](/docs/elements/flow/http#live-streams),
|
|
196
197
|
not as a worker. A Flow with no trigger is [call-only](/docs/elements/flow).
|
|
197
198
|
|
|
198
|
-
|
|
199
|
-
|
|
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.
|
|
200
202
|
|
|
201
203
|
## Signal Consumers
|
|
202
204
|
|
|
@@ -212,8 +214,12 @@ Each emit is handled according to the Signal helper you declared. The Flow is th
|
|
|
212
214
|
|
|
213
215
|
<Tab value="Once">
|
|
214
216
|
|
|
215
|
-
Competing workers — exactly one consumer
|
|
216
|
-
dead-letter when `deadLetter` is true (default)
|
|
217
|
+
Competing workers — exactly one consumer claims each message. Failed attempts retry, then
|
|
218
|
+
dead-letter when `deadLetter` is true (default).
|
|
219
|
+
|
|
220
|
+
Two different Flows on one `once` signal fail **OKE1071** — see
|
|
221
|
+
[Once · Competing consumers](/docs/elements/signal/once#competing-consumers-once-vs-broadcast).
|
|
222
|
+
For every bound Flow to run, use [`signal.broadcast`](/docs/elements/signal/broadcast).
|
|
217
223
|
|
|
218
224
|
```typescript title="src/signals/orders.ts"
|
|
219
225
|
import { signal } from "okengine";
|
|
@@ -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,
|
|
@@ -489,49 +507,34 @@ do not fire.
|
|
|
489
507
|
## CDC
|
|
490
508
|
|
|
491
509
|
<Callout title="Detailed section">
|
|
492
|
-
If you only need any-write, jump to
|
|
510
|
+
If you only need any-write, jump to Bare or enriched below. The handle is
|
|
493
511
|
`db.table(table).changed(column?)` — `db` is a `store.sql` declaration, `table` is a schema
|
|
494
512
|
handle. `changed("insert")` is **not** an op filter; it stamps a column named `insert`.
|
|
495
513
|
</Callout>
|
|
496
514
|
|
|
497
515
|
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.
|
|
516
|
+
`{ before, after, table, action, id }` (`CdcPayload`).
|
|
500
517
|
|
|
501
|
-
|
|
518
|
+
### Bare or enriched
|
|
502
519
|
|
|
503
|
-
|
|
520
|
+
| Style | When |
|
|
521
|
+
| ------------------------- | ---------------------------------------------------------------------- |
|
|
522
|
+
| `({ before, after })` | Bound to one table — images are enough (search reindex, listing cache) |
|
|
523
|
+
| `({ table, action, id })` | Log, route, or branch — kind of change and which record (audit log) |
|
|
504
524
|
|
|
505
|
-
|
|
506
|
-
|
|
525
|
+
Both styles receive the same object. There is no second dispatch path, no
|
|
526
|
+
performance difference, and no correctness difference — the choice is which
|
|
527
|
+
fields this handler destructures.
|
|
507
528
|
|
|
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
|
-
```
|
|
529
|
+
`{ table, action, id }` are always populated; omitting them from `do` does not
|
|
530
|
+
drop them from the payload.
|
|
529
531
|
|
|
530
|
-
|
|
532
|
+
<Tabs items={["Bare", "Enriched", "Images"]}>
|
|
531
533
|
|
|
532
534
|
<Tab value="Bare">
|
|
533
535
|
|
|
534
|
-
|
|
536
|
+
Bound to `notes` — the table is already in the trigger. Images decide upsert vs
|
|
537
|
+
drop; the row's `id` is on the surviving image:
|
|
535
538
|
|
|
536
539
|
```typescript title="src/flows/search/reindex.ts"
|
|
537
540
|
import { on, flow } from "okengine";
|
|
@@ -553,25 +556,30 @@ export const reindexNotes = on(
|
|
|
553
556
|
);
|
|
554
557
|
```
|
|
555
558
|
|
|
556
|
-
|
|
557
|
-
same payload — filter in `do` when you only care about that field:
|
|
559
|
+
</Tab>
|
|
558
560
|
|
|
559
|
-
|
|
561
|
+
<Tab value="Enriched">
|
|
562
|
+
|
|
563
|
+
`table` is the real table name. `action` is `"created"` / `"updated"` / `"deleted"`.
|
|
564
|
+
`id` is the declared primary-key value — not a hardcoded `"id"` column:
|
|
565
|
+
|
|
566
|
+
```typescript title="src/flows/audit/users.ts"
|
|
560
567
|
import { on, flow } from "okengine";
|
|
561
568
|
import { db } from "@/core";
|
|
562
|
-
import {
|
|
569
|
+
import { users, auditLogs } from "@/schema";
|
|
563
570
|
|
|
564
|
-
export const
|
|
565
|
-
db.table(
|
|
566
|
-
flow("
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
571
|
+
export const onUserWrite = on(
|
|
572
|
+
db.table(users).changed(),
|
|
573
|
+
flow("audit.users", {
|
|
574
|
+
do: async ({ table, action, id }, fx) => {
|
|
575
|
+
await fx
|
|
576
|
+
.store(db)
|
|
577
|
+
.insert(auditLogs)
|
|
578
|
+
.values({
|
|
579
|
+
table,
|
|
580
|
+
recordId: String(id),
|
|
581
|
+
action,
|
|
582
|
+
});
|
|
575
583
|
},
|
|
576
584
|
}),
|
|
577
585
|
);
|
|
@@ -605,6 +613,30 @@ do: async ({ before, after, action }, fx) => {
|
|
|
605
613
|
|
|
606
614
|
</Tabs>
|
|
607
615
|
|
|
616
|
+
`changed("status")` stamps `trigger.cdc.column` on the Manifest. Still the same
|
|
617
|
+
payload — filter in `do` when you only care about that field:
|
|
618
|
+
|
|
619
|
+
```typescript title="src/flows/tasks/on-status.ts"
|
|
620
|
+
import { on, flow } from "okengine";
|
|
621
|
+
import { db } from "@/core";
|
|
622
|
+
import { tasks } from "@/schema";
|
|
623
|
+
|
|
624
|
+
export const onStatus = on(
|
|
625
|
+
db.table(tasks).changed("status"),
|
|
626
|
+
flow("tasks.onStatus", {
|
|
627
|
+
plane: "operator",
|
|
628
|
+
do: async ({ before, after, id }, fx) => {
|
|
629
|
+
if (before?.status === after?.status) return;
|
|
630
|
+
await fx.emit(taskStatusChanged, {
|
|
631
|
+
id,
|
|
632
|
+
from: before?.status ?? null,
|
|
633
|
+
to: after?.status ?? null,
|
|
634
|
+
});
|
|
635
|
+
},
|
|
636
|
+
}),
|
|
637
|
+
);
|
|
638
|
+
```
|
|
639
|
+
|
|
608
640
|
<Accordions>
|
|
609
641
|
|
|
610
642
|
<Accordion title="CDC payload">
|
|
@@ -660,9 +692,19 @@ Clock drivers: **postgres** in `dev`/`prod`, **frozen** in `test`. Signal driver
|
|
|
660
692
|
</Accordion>
|
|
661
693
|
|
|
662
694
|
<Accordion title="OKE1070 — flow name defined twice">
|
|
663
|
-
Cause: `Flow "{flow}" is defined twice.` Two
|
|
664
|
-
|
|
665
|
-
|
|
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 }))`.
|
|
702
|
+
</Accordion>
|
|
703
|
+
|
|
704
|
+
<Accordion title="OKE1071 — once signal bound to more than one Flow">
|
|
705
|
+
Cause: `Once signal "{signal}" is bound to more than one Flow ({flows}).` Use `signal.broadcast`
|
|
706
|
+
if each Flow should independently receive this event, or bind only one Flow. See [Once · Competing
|
|
707
|
+
consumers](/docs/elements/signal/once#competing-consumers-once-vs-broadcast).
|
|
666
708
|
</Accordion>
|
|
667
709
|
|
|
668
710
|
<Accordion title="OKE1240 — emit with no subscriber">
|
|
@@ -713,10 +755,11 @@ Clock drivers: **postgres** in `dev`/`prod`, **frozen** in `test`. Signal driver
|
|
|
713
755
|
- [Signal](/docs/elements/signal) — `once` / `broadcast` / `live` physics
|
|
714
756
|
- [Signal · Once](/docs/elements/signal/once) — leases, retries, partition keys
|
|
715
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
|
|
716
759
|
- [Store · SQL](/docs/elements/store/sql) — tables CDC watches
|
|
717
760
|
- [HTTP · Live Streams](/docs/elements/flow/http#live-streams) — `signal.live` SSE
|
|
718
761
|
- [fx](/docs/reference/fx) — `fx.emit`, `fx.deadLetters`, `fx.clock`
|
|
719
|
-
- [Errors](/docs/reference/errors) — OKE1070 · OKE1240 · OKE1250
|
|
762
|
+
- [Errors](/docs/reference/errors) — OKE1070 · OKE1071 · OKE1072 · OKE1240 · OKE1250
|
|
720
763
|
- [Workflows](/docs/elements/flow/workflows) — `durable: true` + `fx.step` on a consumer
|
|
721
764
|
|
|
722
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
|
-
|
|
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>
|
|
@@ -455,11 +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
|
-
|
|
462
|
-
HTTP flows that remain nameless after adopt fail **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).
|
|
463
461
|
|
|
464
462
|
## Runtime Matching
|
|
465
463
|
|
|
@@ -517,8 +515,13 @@ plus a handwritten `http.get("/notes")`.
|
|
|
517
515
|
</Accordion>
|
|
518
516
|
|
|
519
517
|
<Accordion title="OKE1070 — flow name defined twice">
|
|
520
|
-
Cause: `Flow "{flow}" is defined twice.` Two
|
|
521
|
-
|
|
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 }))`.
|
|
522
525
|
</Accordion>
|
|
523
526
|
|
|
524
527
|
<Accordion title="422 — path param missing from in">
|
|
@@ -543,7 +546,7 @@ plus a handwritten `http.get("/notes")`.
|
|
|
543
546
|
|
|
544
547
|
- [HTTP](/docs/elements/flow/http) — verbs, envelopes, `http.resource`, live SSE
|
|
545
548
|
- [Client](/docs/client/calling) — `api.notes.get`, REST vs RPC, `$routes`
|
|
546
|
-
- [Errors](/docs/reference/errors) — OKE1040 · OKE1030 · OKE1041 · OKE1045 · OKE1070
|
|
549
|
+
- [Errors](/docs/reference/errors) — OKE1040 · OKE1030 · OKE1041 · OKE1045 · OKE1070 · OKE1072
|
|
547
550
|
- [The Architecture](/docs/understand/the-architecture) — derived routes, no hand-written table
|
|
548
551
|
- [Gate](/docs/elements/gate) — `.gate(...)` / `.public()` on the same trigger
|
|
549
552
|
|
|
@@ -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.
|
|
@@ -470,6 +466,11 @@ dead-letter queue for exhausted retries in the queue sense — see
|
|
|
470
466
|
Cause: `"{resource}": {detail}`. Align the payload with `schema` — no subscriber started.
|
|
471
467
|
</Accordion>
|
|
472
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
|
+
|
|
473
474
|
<Accordion title="Expecting retries / DLQ like a queue">
|
|
474
475
|
Use `signal.once`. Broadcast fan-out is not the lease + DLQ path documented under
|
|
475
476
|
[Once](/docs/elements/signal/once).
|
|
@@ -496,7 +497,7 @@ dead-letter queue for exhausted retries in the queue sense — see
|
|
|
496
497
|
- [Consumers](/docs/elements/flow/consumers) — binding `on(signal)`
|
|
497
498
|
- [HTTP · Live Streams](/docs/elements/flow/http#live-streams) — when the consumer is a browser
|
|
498
499
|
- [fx](/docs/reference/fx) — `fx.emit`
|
|
499
|
-
- [Errors](/docs/reference/errors) — OKE1240 · OKE1250
|
|
500
|
+
- [Errors](/docs/reference/errors) — OKE1072 · OKE1240 · OKE1250
|
|
500
501
|
|
|
501
502
|
## Next
|
|
502
503
|
|