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
|
@@ -7,22 +7,24 @@ source: "docs/spec/unified-theory.md"
|
|
|
7
7
|
|
|
8
8
|
Signal is how your backend **moves data when the producer should not wait**. An email job, a cache bust across instances, and a browser status feed share one handle shape — only the helper changes: `signal.once`, `signal.broadcast`, or `signal.live`.
|
|
9
9
|
|
|
10
|
-
For developers wiring async work on okengine — declare the physics, emit with `fx.emit`, bind workers with `on(
|
|
10
|
+
For developers wiring async work on okengine — declare the physics, emit with `fx.emit`, bind workers with `on(handle, flow("name", { do }))`.
|
|
11
11
|
|
|
12
12
|
<Callout title="The one rule">
|
|
13
|
-
Declare every signal with `signal.once`, `signal.broadcast`, or `signal.live`
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
Declare every signal with `signal.once`, `signal.broadcast`, or `signal.live` as an
|
|
14
|
+
exported const. Bind with `on(handle, flow("name", { do }))`. Physics and the **emit**
|
|
15
|
+
`schema` live on the Signal; the worker inherits the payload type, not `flow.in`.
|
|
16
16
|
</Callout>
|
|
17
17
|
|
|
18
18
|
<SignalDelivery />
|
|
19
19
|
|
|
20
20
|
## Smallest Example
|
|
21
21
|
|
|
22
|
-
<
|
|
22
|
+
<Callout title="One handle, three independent uses">
|
|
23
|
+
`orderPlaced` is a shared const. Declare it, bind a worker, and emit from a producer — different
|
|
24
|
+
files, different people, any order. None of these is a prerequisite step for the others.
|
|
25
|
+
</Callout>
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
### Declare the signal
|
|
27
|
+
### Declare
|
|
26
28
|
|
|
27
29
|
```typescript title="src/signals/orders.ts"
|
|
28
30
|
import { signal } from "okengine";
|
|
@@ -39,10 +41,7 @@ export const orderPlaced = signal.once("orders.placed", {
|
|
|
39
41
|
});
|
|
40
42
|
```
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
<Step>
|
|
45
|
-
### Bind a worker and emit
|
|
44
|
+
### Bind a worker
|
|
46
45
|
|
|
47
46
|
```typescript title="src/flows/orders/fulfill.ts"
|
|
48
47
|
import { on, flow } from "okengine";
|
|
@@ -58,8 +57,10 @@ export const fulfill = on(
|
|
|
58
57
|
);
|
|
59
58
|
```
|
|
60
59
|
|
|
60
|
+
### Emit
|
|
61
|
+
|
|
61
62
|
```typescript
|
|
62
|
-
// Inside any Flow:
|
|
63
|
+
// Inside any Flow — need not live next to the worker:
|
|
63
64
|
await fx.emit(orderPlaced, {
|
|
64
65
|
orderId: "ord_99",
|
|
65
66
|
amount: 150,
|
|
@@ -70,28 +71,14 @@ await fx.emit(orderPlaced, {
|
|
|
70
71
|
The compiler records `emits: ["orders.placed"]` on the producer. The HTTP request does not wait
|
|
71
72
|
for the worker.
|
|
72
73
|
|
|
73
|
-
</Step>
|
|
74
|
-
|
|
75
|
-
</Steps>
|
|
76
|
-
|
|
77
74
|
<Callout title="Live is not a worker">
|
|
78
75
|
`signal.live` is an HTTP SSE tape. Expose it with
|
|
79
76
|
[`http.live`](/docs/elements/flow/http#live-streams) — do not bind `on(liveSignal, flow)` as a
|
|
80
77
|
competing consumer.
|
|
81
78
|
</Callout>
|
|
82
79
|
|
|
83
|
-
## Inline or named export
|
|
84
|
-
|
|
85
|
-
| Style | When |
|
|
86
|
-
| --------------------------------------------- | --------------------------------------------------------------------------- |
|
|
87
|
-
| `on(signal.once("name", opts), flow({ do }))` | Self-contained — nothing else emits to this Signal |
|
|
88
|
-
| `export const x = signal.once<Payload>(…)` | Another file needs `fx.emit(x, payload)` with compile-time payload checking |
|
|
89
|
-
|
|
90
80
|
A string `fx.emit("name", payload)` still runs (runtime `schema` still applies) but does not
|
|
91
|
-
type-check the
|
|
92
|
-
|
|
93
|
-
Nameless `flow({ do })` inherits the Signal name; file-tree `unit.export` and explicit
|
|
94
|
-
`flow("…")` win. Two nameless inheritances of the same name fail **OKE1070**.
|
|
81
|
+
type-check. Import the exported const.
|
|
95
82
|
|
|
96
83
|
## Progressive Patterns
|
|
97
84
|
|
|
@@ -221,6 +208,66 @@ Fix: add `on(signal, …)` or mark `{ optional: true }`.
|
|
|
221
208
|
| `signal.broadcast` | Pub/sub | Every active subscriber | None (miss if offline) | Cache invalidation, fan-out |
|
|
222
209
|
| `signal.live` | SSE tape | HTTP clients via `http.live` | `Last-Event-ID` resume | Status feeds, progress |
|
|
223
210
|
|
|
211
|
+
## Competing consumers (once vs broadcast)
|
|
212
|
+
|
|
213
|
+
This is the most common mix-up. `signal.once` is a work queue: **exactly one** worker claims each
|
|
214
|
+
message. It is not fan-out.
|
|
215
|
+
|
|
216
|
+
Three differently-named Flows bound to the same `once` signal, then one emit:
|
|
217
|
+
|
|
218
|
+
```typescript title="src/flows/orders/side-effects.ts"
|
|
219
|
+
import { on, flow } from "okengine";
|
|
220
|
+
import { orderPlaced } from "@/signals/orders";
|
|
221
|
+
|
|
222
|
+
export const charge = on(
|
|
223
|
+
orderPlaced,
|
|
224
|
+
flow("orders.charge", {
|
|
225
|
+
do: async ({ orderId }, fx) => {
|
|
226
|
+
await fx.call(chargeOrder, { orderId });
|
|
227
|
+
},
|
|
228
|
+
}),
|
|
229
|
+
);
|
|
230
|
+
export const ship = on(
|
|
231
|
+
orderPlaced,
|
|
232
|
+
flow("orders.ship", {
|
|
233
|
+
do: async ({ orderId }, fx) => {
|
|
234
|
+
await fx.call(shipOrder, { orderId });
|
|
235
|
+
},
|
|
236
|
+
}),
|
|
237
|
+
);
|
|
238
|
+
export const notify = on(
|
|
239
|
+
orderPlaced,
|
|
240
|
+
flow("orders.notify", {
|
|
241
|
+
do: async ({ orderId }, fx) => {
|
|
242
|
+
await fx.call(notifyOrder, { orderId });
|
|
243
|
+
},
|
|
244
|
+
}),
|
|
245
|
+
);
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
await fx.emit(orderPlaced, { orderId: "ord_99", amount: 150, userId: "usr_1" });
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
| Expectation | Real result |
|
|
253
|
+
| ----------------------------------- | -------------------------------------------------------------------- |
|
|
254
|
+
| All three Flows run | **No.** Exactly one of the three runs |
|
|
255
|
+
| Always `orders.charge` (first `on`) | **No.** The winner is whichever claim lands first — not a fixed Flow |
|
|
256
|
+
| A sticky assignment to one Flow | **No.** There is no owner — only an exclusive claim per message |
|
|
257
|
+
|
|
258
|
+
**Consequence:** if every bound Flow should independently receive its own copy, that is
|
|
259
|
+
[`signal.broadcast`](/docs/elements/signal/broadcast) — switch the declaration. That is the correct
|
|
260
|
+
fix for this exact mistake.
|
|
261
|
+
|
|
262
|
+
Two or more **different** Flow definitions on the same `once` signal fail **OKE1071**.
|
|
263
|
+
|
|
264
|
+
Cause: `Once signal "{signal}" is bound to more than one Flow ({flows}).`
|
|
265
|
+
|
|
266
|
+
Load-balancing competing consumers is the **same** Flow on many process replicas — still one
|
|
267
|
+
`on()` in source.
|
|
268
|
+
|
|
269
|
+
Fix: `Use signal.broadcast if each flow should independently receive this event, or bind only one flow if these should compete for the same work.`
|
|
270
|
+
|
|
224
271
|
## The Capabilities of Signal
|
|
225
272
|
|
|
226
273
|
<Cards>
|
|
@@ -245,14 +292,14 @@ Fix: add `on(signal, …)` or mark `{ optional: true }`.
|
|
|
245
292
|
|
|
246
293
|
Optional second argument to `signal.once` / `signal.broadcast` / `signal.live`. Delivery is the helper name — not an option.
|
|
247
294
|
|
|
248
|
-
| Option | Type | Default | Meaning
|
|
249
|
-
| ------------- | ------------------------ | --------- |
|
|
250
|
-
| `schema` | Standard Schema | omitted | **Emit** contract — enforced at `fx.emit` (**OKE1250** on mismatch). Workers inherit the payload; do not put `in` on `flow(
|
|
251
|
-
| `retries` | `number` | `3` | Extra attempts after the first (`retries + 1` total) — `once` path
|
|
252
|
-
| `deadLetter` | `boolean` | `true` | Keep exhausted `once` messages; `false` marks them delivered
|
|
253
|
-
| `optional` | `boolean` | `false` | Allow emit with zero subscribers
|
|
254
|
-
| `retention` | `{ maxAge?, maxCount? }` | unbounded | **`signal.live` only** — type error on `once` / `broadcast`
|
|
255
|
-
| `description` | `string` | the name | Console / docs blurb
|
|
295
|
+
| Option | Type | Default | Meaning |
|
|
296
|
+
| ------------- | ------------------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
|
297
|
+
| `schema` | Standard Schema | omitted | **Emit** contract — enforced at `fx.emit` (**OKE1250** on mismatch). Workers inherit the payload; do not put `in` on `flow()`. |
|
|
298
|
+
| `retries` | `number` | `3` | Extra attempts after the first (`retries + 1` total) — `once` path |
|
|
299
|
+
| `deadLetter` | `boolean` | `true` | Keep exhausted `once` messages; `false` marks them delivered |
|
|
300
|
+
| `optional` | `boolean` | `false` | Allow emit with zero subscribers |
|
|
301
|
+
| `retention` | `{ maxAge?, maxCount? }` | unbounded | **`signal.live` only** — type error on `once` / `broadcast` |
|
|
302
|
+
| `description` | `string` | the name | Console / docs blurb |
|
|
256
303
|
|
|
257
304
|
**Consequence:** `deadLetter` is a boolean flag, not a queue name string.
|
|
258
305
|
|
|
@@ -320,6 +367,22 @@ export default defineConfig({
|
|
|
320
367
|
on the Signal. The consumer never ran.
|
|
321
368
|
</Accordion>
|
|
322
369
|
|
|
370
|
+
<Accordion title="OKE1070 — flow name defined twice">
|
|
371
|
+
Cause: `Flow "{flow}" is defined twice.` Two `flow("…")` strings collide. Give at least one a
|
|
372
|
+
distinct name.
|
|
373
|
+
</Accordion>
|
|
374
|
+
|
|
375
|
+
<Accordion title="OKE1072 — Signal flow unnamed">
|
|
376
|
+
Cause: `A signal flow on "{trigger}" has no name.`
|
|
377
|
+
Fix: pass an explicit name — `on(handle, flow("orders.fulfill", { do }))`.
|
|
378
|
+
</Accordion>
|
|
379
|
+
|
|
380
|
+
<Accordion title="OKE1071 — once signal bound to more than one Flow">
|
|
381
|
+
Cause: `Once signal "{signal}" is bound to more than one Flow ({flows}).` Use `signal.broadcast`
|
|
382
|
+
if each Flow should independently receive this event, or bind only one Flow. See [Competing
|
|
383
|
+
consumers](#competing-consumers-once-vs-broadcast).
|
|
384
|
+
</Accordion>
|
|
385
|
+
|
|
323
386
|
<Accordion title='oke boot: signal driver "postgres" / "nats"'>
|
|
324
387
|
Those ids are reserved but not bound for production yet. Use `"memory"` or `"redis"`, or inject a
|
|
325
388
|
custom `elements.signal` runtime.
|
|
@@ -342,7 +405,7 @@ export default defineConfig({
|
|
|
342
405
|
- [HTTP · Live Streams](/docs/elements/flow/http#live-streams) — `http.live` exposure
|
|
343
406
|
- [fx](/docs/reference/fx) — `fx.emit`, `fx.deadLetters`, `fx.live`
|
|
344
407
|
- [Client](/docs/client/live) — `api.live` for browsers
|
|
345
|
-
- [Errors](/docs/reference/errors) — OKE1240 · OKE1250 · OKE1210
|
|
408
|
+
- [Errors](/docs/reference/errors) — OKE1070 · OKE1071 · OKE1072 · OKE1240 · OKE1250 · OKE1210
|
|
346
409
|
|
|
347
410
|
## Next
|
|
348
411
|
|
|
@@ -21,10 +21,12 @@ from the typed client.
|
|
|
21
21
|
|
|
22
22
|
## Smallest Example
|
|
23
23
|
|
|
24
|
-
<
|
|
24
|
+
<Callout title="One handle, three independent uses">
|
|
25
|
+
`orderStatus` is a shared const. Declare it, expose SSE, and emit — different files, any order.
|
|
26
|
+
The firehose is not "step 2"; emit is not "step 3".
|
|
27
|
+
</Callout>
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
### Declare the live signal
|
|
29
|
+
### Declare
|
|
28
30
|
|
|
29
31
|
```typescript title="src/signals/orders.ts"
|
|
30
32
|
import { signal } from "okengine";
|
|
@@ -39,9 +41,6 @@ export const orderStatus = signal.live("order-status", {
|
|
|
39
41
|
});
|
|
40
42
|
```
|
|
41
43
|
|
|
42
|
-
</Step>
|
|
43
|
-
|
|
44
|
-
<Step>
|
|
45
44
|
### Expose SSE
|
|
46
45
|
|
|
47
46
|
```typescript title="src/flows/orders/firehose.ts"
|
|
@@ -52,9 +51,6 @@ import { orderStatus } from "@/signals/orders";
|
|
|
52
51
|
export const firehose = on(http.live(orderStatus).gate(member));
|
|
53
52
|
```
|
|
54
53
|
|
|
55
|
-
</Step>
|
|
56
|
-
|
|
57
|
-
<Step>
|
|
58
54
|
### Emit and subscribe
|
|
59
55
|
|
|
60
56
|
```typescript
|
|
@@ -71,10 +67,6 @@ curl -N http://localhost:6530/_oke/live/order-status \
|
|
|
71
67
|
Response `Content-Type` is `text/event-stream`. Frames are JSON `data:` lines
|
|
72
68
|
(optional `id:` for resume), then `data: [DONE]`.
|
|
73
69
|
|
|
74
|
-
</Step>
|
|
75
|
-
|
|
76
|
-
</Steps>
|
|
77
|
-
|
|
78
70
|
<Callout title="Pathless firehose">
|
|
79
71
|
`on(http.live(signal))` always mounts `GET /_oke/live/{name}` — there is no pathless file-tree
|
|
80
72
|
stamp for live. Custom paths use `http.get(path).live(signal)`. See [Exposure](#exposure).
|
|
@@ -21,10 +21,12 @@ For developers shipping jobs on okengine — declare the Signal, bind `on(signal
|
|
|
21
21
|
|
|
22
22
|
## Smallest Example
|
|
23
23
|
|
|
24
|
-
<
|
|
24
|
+
<Callout title="One handle, three independent uses">
|
|
25
|
+
`emailTask` is a shared const. Declare it, bind a worker, and emit — different files, any order.
|
|
26
|
+
Binding is not "step 2" after declare; emit is not "step 3".
|
|
27
|
+
</Callout>
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
### Define the once signal
|
|
29
|
+
### Declare
|
|
28
30
|
|
|
29
31
|
```typescript title="src/signals/email.ts"
|
|
30
32
|
import { signal } from "okengine";
|
|
@@ -37,10 +39,7 @@ export const emailTask = signal.once("tasks.email", {
|
|
|
37
39
|
});
|
|
38
40
|
```
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
<Step>
|
|
43
|
-
### Attach a worker and emit
|
|
42
|
+
### Bind a worker
|
|
44
43
|
|
|
45
44
|
```typescript title="src/flows/workers/email.ts"
|
|
46
45
|
import { on, flow } from "okengine";
|
|
@@ -57,6 +56,8 @@ export const processEmail = on(
|
|
|
57
56
|
);
|
|
58
57
|
```
|
|
59
58
|
|
|
59
|
+
### Emit
|
|
60
|
+
|
|
60
61
|
```typescript
|
|
61
62
|
await fx.emit(emailTask, { to: "alice@example.com", body: "Welcome" });
|
|
62
63
|
```
|
|
@@ -64,14 +65,67 @@ await fx.emit(emailTask, { to: "alice@example.com", body: "Welcome" });
|
|
|
64
65
|
The emit resolves when the outbox commits. The worker runs asynchronously — the producer does not
|
|
65
66
|
wait for `fx.send` to finish.
|
|
66
67
|
|
|
67
|
-
|
|
68
|
+
## Competing consumers (once vs broadcast)
|
|
68
69
|
|
|
69
|
-
|
|
70
|
+
This is the most common mix-up. `signal.once` is a competing-consumer work queue: **exactly one**
|
|
71
|
+
worker claims each message. It is not fan-out.
|
|
70
72
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
Three differently-named Flows bound to the same `once` signal:
|
|
74
|
+
|
|
75
|
+
```typescript title="src/flows/orders/side-effects.ts"
|
|
76
|
+
import { on, flow } from "okengine";
|
|
77
|
+
import { orderPlaced } from "@/signals/orders";
|
|
78
|
+
|
|
79
|
+
export const charge = on(
|
|
80
|
+
orderPlaced,
|
|
81
|
+
flow("orders.charge", {
|
|
82
|
+
do: async ({ orderId }, fx) => {
|
|
83
|
+
await fx.call(chargeOrder, { orderId });
|
|
84
|
+
},
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
export const ship = on(
|
|
89
|
+
orderPlaced,
|
|
90
|
+
flow("orders.ship", {
|
|
91
|
+
do: async ({ orderId }, fx) => {
|
|
92
|
+
await fx.call(shipOrder, { orderId });
|
|
93
|
+
},
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
export const notify = on(
|
|
98
|
+
orderPlaced,
|
|
99
|
+
flow("orders.notify", {
|
|
100
|
+
do: async ({ orderId }, fx) => {
|
|
101
|
+
await fx.call(notifyOrder, { orderId });
|
|
102
|
+
},
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
await fx.emit(orderPlaced, { orderId: "ord_99", amount: 150, userId: "usr_1" });
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
| Expectation | Real result |
|
|
112
|
+
| ----------------------------------- | -------------------------------------------------------------------- |
|
|
113
|
+
| All three Flows run | **No.** Exactly one of the three runs |
|
|
114
|
+
| Always `orders.charge` (first `on`) | **No.** The winner is whichever claim lands first — not a fixed Flow |
|
|
115
|
+
| A sticky assignment to one Flow | **No.** There is no owner — only an exclusive claim per message |
|
|
116
|
+
|
|
117
|
+
**Consequence:** if every bound Flow should independently receive its own copy, use
|
|
118
|
+
[`signal.broadcast`](/docs/elements/signal/broadcast). That is the correct fix for this exact
|
|
119
|
+
mistake.
|
|
120
|
+
|
|
121
|
+
Two or more **different** Flow definitions on the same `once` signal fail **OKE1071**.
|
|
122
|
+
|
|
123
|
+
Cause: `Once signal "{signal}" is bound to more than one Flow ({flows}).`
|
|
124
|
+
|
|
125
|
+
Fix: `Use signal.broadcast if each flow should independently receive this event, or bind only one flow if these should compete for the same work.`
|
|
126
|
+
|
|
127
|
+
Load-balancing competing consumers is the **same** Flow on many process replicas — still one
|
|
128
|
+
`on()` in source. That case is not a second Flow definition.
|
|
75
129
|
|
|
76
130
|
## Progressive Patterns
|
|
77
131
|
|
|
@@ -251,12 +305,11 @@ The compiler records `emits: ["tasks.email"]` on producers that call `fx.emit(em
|
|
|
251
305
|
|
|
252
306
|
<Tab value="Competing">
|
|
253
307
|
|
|
254
|
-
Two Flows on the same `once` Signal
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
import { on, flow } from "okengine";
|
|
258
|
-
import { orderPlaced } from "@/signals/orders";
|
|
308
|
+
Two different Flows on the same `once` Signal is the once-vs-broadcast mix-up — see
|
|
309
|
+
[Competing consumers](#competing-consumers-once-vs-broadcast). The bus would let only one claim
|
|
310
|
+
each message (race winner, not both, not a fixed Flow). The Manifest now fails **OKE1071**:
|
|
259
311
|
|
|
312
|
+
```typescript
|
|
260
313
|
export const fulfillA = on(
|
|
261
314
|
orderPlaced,
|
|
262
315
|
flow("orders.fulfillA", {
|
|
@@ -265,11 +318,6 @@ export const fulfillA = on(
|
|
|
265
318
|
},
|
|
266
319
|
}),
|
|
267
320
|
);
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
```typescript title="src/flows/orders/fulfill-b.ts"
|
|
271
|
-
import { on, flow } from "okengine";
|
|
272
|
-
import { orderPlaced } from "@/signals/orders";
|
|
273
321
|
|
|
274
322
|
export const fulfillB = on(
|
|
275
323
|
orderPlaced,
|
|
@@ -281,7 +329,8 @@ export const fulfillB = on(
|
|
|
281
329
|
);
|
|
282
330
|
```
|
|
283
331
|
|
|
284
|
-
**Consequence:**
|
|
332
|
+
**Consequence:** bind one Flow (replicas of that process still compete for claims), or switch the
|
|
333
|
+
declaration to [`signal.broadcast`](/docs/elements/signal/broadcast) so every Flow gets a copy.
|
|
285
334
|
|
|
286
335
|
</Tab>
|
|
287
336
|
|
|
@@ -531,9 +580,20 @@ See [Durable Workflows](/docs/elements/flow/workflows).
|
|
|
531
580
|
</Accordion>
|
|
532
581
|
|
|
533
582
|
<Accordion title="Both of two Flows ran on one once message">
|
|
534
|
-
That is broadcast physics, not once.
|
|
535
|
-
|
|
536
|
-
`signal.broadcast
|
|
583
|
+
That is broadcast physics, not once. Two different Flow definitions on one `once` signal now fail
|
|
584
|
+
**OKE1071** at boot. If you need every Flow to run, switch the declaration to
|
|
585
|
+
[`signal.broadcast`](/docs/elements/signal/broadcast).
|
|
586
|
+
</Accordion>
|
|
587
|
+
|
|
588
|
+
<Accordion title="OKE1071 — once signal bound to more than one Flow">
|
|
589
|
+
Cause: `Once signal "{signal}" is bound to more than one Flow ({flows}).` Use `signal.broadcast`
|
|
590
|
+
if each Flow should independently receive this event, or bind only one Flow. Replicas of one Flow
|
|
591
|
+
are still one `on()` in source.
|
|
592
|
+
</Accordion>
|
|
593
|
+
|
|
594
|
+
<Accordion title="OKE1072 — Signal flow unnamed">
|
|
595
|
+
Cause: `A signal flow on "{trigger}" has no name.`
|
|
596
|
+
Fix: pass an explicit name — `on(handle, flow("workers.email", { do }))`.
|
|
537
597
|
</Accordion>
|
|
538
598
|
|
|
539
599
|
<Accordion title="Messages stuck inflight">
|
|
@@ -574,7 +634,7 @@ See [Durable Workflows](/docs/elements/flow/workflows).
|
|
|
574
634
|
- [Consumers](/docs/elements/flow/consumers) — `on(signal)` next to Clock / CDC
|
|
575
635
|
- [Workflows](/docs/elements/flow/workflows) — `durable` + `fx.step` for idempotent side effects
|
|
576
636
|
- [fx](/docs/reference/fx) — `fx.emit`, `fx.deadLetters`
|
|
577
|
-
- [Errors](/docs/reference/errors) — OKE1240 · OKE1250 · OKE1001
|
|
637
|
+
- [Errors](/docs/reference/errors) — OKE1071 · OKE1072 · OKE1240 · OKE1250 · OKE1001
|
|
578
638
|
|
|
579
639
|
## Next
|
|
580
640
|
|
|
@@ -78,33 +78,35 @@ string. Custom app codes stay message-less until registered. Full catalogs:
|
|
|
78
78
|
|
|
79
79
|
## OKE numeric codes
|
|
80
80
|
|
|
81
|
-
| Code | Name
|
|
82
|
-
| ------ |
|
|
83
|
-
| `1001` | undeclared read
|
|
84
|
-
| `1002` | undeclared write
|
|
85
|
-
| `1003` | undeclared emit
|
|
86
|
-
| `1004` | undeclared send
|
|
87
|
-
| `1005` | undeclared ask
|
|
88
|
-
| `1006` | undeclared secret
|
|
89
|
-
| `1007` | undeclared call
|
|
90
|
-
| `1008` | undeclared fetch
|
|
91
|
-
| `1009` | undeclared embed
|
|
92
|
-
| `1020` | no effects declared
|
|
93
|
-
| `1030` | adopt barrel stale
|
|
94
|
-
| `1040` | HTTP path unresolved
|
|
95
|
-
| `1041` | HTTP route clash
|
|
96
|
-
| `1045` | HTTP flow unnamed
|
|
97
|
-
| `1050` | live exposure dup
|
|
98
|
-
| `1060` | MCP tool duplicate
|
|
99
|
-
| `1070` | flow name duplicate
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
104
|
-
| `
|
|
105
|
-
| `
|
|
106
|
-
| `
|
|
107
|
-
| `
|
|
81
|
+
| Code | Name | Cause | Fix |
|
|
82
|
+
| ------ | ---------------------- | ------------------------------------------------------ | --------------------------------------------------------- |
|
|
83
|
+
| `1001` | undeclared read | Flow reads a resource not in `effects.reads` | Add it to the flow's `effects.reads` |
|
|
84
|
+
| `1002` | undeclared write | Flow writes a resource not in `effects.writes` | Add it to the flow's `effects.writes` |
|
|
85
|
+
| `1003` | undeclared emit | Flow emits a signal not in `effects.emits` | Add it to the flow's `effects.emits` |
|
|
86
|
+
| `1004` | undeclared send | Flow sends a template not in `effects.sends` | Add it to the flow's `effects.sends` |
|
|
87
|
+
| `1005` | undeclared ask | Flow asks a prompt not in `effects.asks` | Add it to the flow's `effects.asks` |
|
|
88
|
+
| `1006` | undeclared secret | Flow reads a secret not in `effects.secrets` | Add it to the flow's `effects.secrets` |
|
|
89
|
+
| `1007` | undeclared call | Flow calls a flow not in `effects.calls` | Add it to the flow's `effects.calls` |
|
|
90
|
+
| `1008` | undeclared fetch | Flow fetches a host not in `effects.fetches` | Add the hostname to the flow's `effects.fetches` |
|
|
91
|
+
| `1009` | undeclared embed | Flow embeds with a model not in `effects.embeds` | Add it to the flow's `effects.embeds` |
|
|
92
|
+
| `1020` | no effects declared | Flow has no `effects` and no Manifest to infer from | Run `oke build` / `oke dev`, or declare effects |
|
|
93
|
+
| `1030` | adopt barrel stale | A `src/flows/<unit>` folder was not adopted | Run `oke dev` or `oke build` to regenerate `generated.ts` |
|
|
94
|
+
| `1040` | HTTP path unresolved | Pathless `http.get()` never received a file-tree stamp | Import `@/flows/generated`, or pass `http.get("/…")` |
|
|
95
|
+
| `1041` | HTTP route clash | Two HTTP flows share the same method + path | Give each flow a unique method + path |
|
|
96
|
+
| `1045` | HTTP flow unnamed | Adopted HTTP flow still has no `unit.export` | Export from `flows/<unit>/` or pass a named `flow` |
|
|
97
|
+
| `1050` | live exposure dup | Same signal, gates, and match on two GET routes | Change the gate or path-param filter |
|
|
98
|
+
| `1060` | MCP tool duplicate | Two MCP tool bindings share the same tool name | Give each MCP tool exposure a unique name |
|
|
99
|
+
| `1070` | flow name duplicate | Two Flows share the same Manifest / `fx.call` name | Give at least one an explicit `flow("…")` or tree export |
|
|
100
|
+
| `1071` | once-signal multi-flow | Two different Flows bound to the same `signal.once` | Use `signal.broadcast`, or bind only one Flow |
|
|
101
|
+
| `1072` | flow unnamed | Signal / Clock consumer still has no `unit.export` | Export from `flows/<unit>/` or pass a named `flow` |
|
|
102
|
+
| `1110` | schema missing | Domain table absent in `prod` — no auto-DDL | Run `oke db migrate` against this environment |
|
|
103
|
+
| `1210` | live resume gap | `Last-Event-ID` is not on the retained tape | Reconnect without the cursor; remaining tape replays |
|
|
104
|
+
| `1240` | orphan emit | Emit with zero subscribers and `optional` false | Add `on(signal, …)` or declare `optional: true` |
|
|
105
|
+
| `1250` | signal schema | Emit payload failed the signal's `schema` | Pass a payload that matches `schema`, or remove it |
|
|
106
|
+
| `1605` | channel schema | Send payload failed the template's `schema` | Fix template `data` payload or the template `schema` |
|
|
107
|
+
| `1810` | tenant required | Tenant-scoped op with no `fx.tenant.id` | `switchTenant`, signed `tid`, or tenant header |
|
|
108
|
+
| `1820` | tenant not member | Client-supplied tenant id is not a membership | Pick from `listTenants` or add the user as a member |
|
|
109
|
+
| `1830` | tenant unknown scope | Tenant role used an invented or `console:*` scope | Use a declared application scope |
|
|
108
110
|
|
|
109
111
|
<Callout title="Effects are usually inferred">
|
|
110
112
|
The 1001–1007 · 1008 · 1009 family exists for flows that declare effects explicitly. Most apps
|
|
@@ -172,6 +174,18 @@ Thrown by specific subsystems — each names its own cause:
|
|
|
172
174
|
A resource mount and a handwritten route share the same method + path. Drop one binding.
|
|
173
175
|
</Accordion>
|
|
174
176
|
|
|
177
|
+
<Accordion title="OKE1072 Signal or Clock flow unnamed">
|
|
178
|
+
Cause: `A {kind} flow on "{trigger}" has no name.`
|
|
179
|
+
Fix: pass an explicit name — `on(handle, flow("unit.export", { do }))`. See
|
|
180
|
+
[Signal](/docs/elements/signal) · [Clock](/docs/elements/clock).
|
|
181
|
+
</Accordion>
|
|
182
|
+
|
|
183
|
+
<Accordion title="OKE1071 once signal bound to more than one Flow">
|
|
184
|
+
Cause: `Once signal "{signal}" is bound to more than one Flow ({flows}).` Use `signal.broadcast`
|
|
185
|
+
if each Flow should get a copy, or bind only one Flow. See [Once · Competing
|
|
186
|
+
consumers](/docs/elements/signal/once#competing-consumers-once-vs-broadcast).
|
|
187
|
+
</Accordion>
|
|
188
|
+
|
|
175
189
|
<Accordion title="OKE1110 in production">
|
|
176
190
|
Domain tables missing — prod has no auto-DDL. Run `oke db migrate` against that environment
|
|
177
191
|
([CLI](/docs/reference/cli)).
|
|
@@ -168,8 +168,8 @@ flow({
|
|
|
168
168
|
```
|
|
169
169
|
|
|
170
170
|
Omit the name on tree files — the compiler stamps `unit.export` (e.g. `users.signup`).
|
|
171
|
-
Pass `flow("users.signup", { … })`
|
|
172
|
-
|
|
171
|
+
Pass `flow("users.signup", { … })` outside a unit folder, or for barrels / `fx.call`.
|
|
172
|
+
Nameless Signal / Clock consumers outside a unit fail **OKE1072**.
|
|
173
173
|
|
|
174
174
|
### `do` — the code that actually runs
|
|
175
175
|
|
|
@@ -45,9 +45,9 @@ const EXPRESS_ALLOW = ["src/plugins/headers.test.ts:"];
|
|
|
45
45
|
const OKID_PROMPT_ALLOW = [
|
|
46
46
|
"okid.md:",
|
|
47
47
|
"bun.lock:",
|
|
48
|
-
// Minified zod-core inside
|
|
49
|
-
// format validators whose names collide with peer library
|
|
50
|
-
// identifiers, not authored comparisons.
|
|
48
|
+
// Minified zod-core inside a locally built Console bundle (gitignored)
|
|
49
|
+
// registers string-format validators whose names collide with peer library
|
|
50
|
+
// names — vendor identifiers, not authored comparisons.
|
|
51
51
|
"src/console/ui-next/dist/",
|
|
52
52
|
];
|
|
53
53
|
|