okengine 0.19.3 → 0.19.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/site/content/docs/elements/channel/index.mdx +1 -1
- package/site/content/docs/elements/clock/index.mdx +53 -105
- package/site/content/docs/elements/clock/schedules.mdx +39 -11
- package/site/content/docs/elements/flow/consumers.mdx +54 -31
- package/site/content/docs/elements/flow/index.mdx +6 -9
- package/site/content/docs/elements/flow/routing.mdx +11 -15
- package/site/content/docs/elements/signal/broadcast.mdx +6 -1
- package/site/content/docs/elements/signal/index.mdx +21 -177
- package/site/content/docs/elements/signal/once.mdx +6 -1
- package/site/content/docs/reference/cli.mdx +14 -1
- package/site/content/docs/reference/errors.mdx +7 -0
- package/site/content/docs/understand/the-architecture.mdx +2 -2
- package/site/content/docs/understand/try-it.mdx +1 -1
- package/src/cli/competitor-mention-removal.test.ts +3 -3
- package/src/cli/dev-app-runner.ts +23 -2
- package/src/cli/dev.test.ts +47 -0
- package/src/cli/dev.ts +11 -0
- package/src/compiler/extract-skip.test.ts +43 -0
- package/src/compiler/extract.test.ts +190 -15
- package/src/compiler/extract.ts +57 -7
- package/src/compiler/flow-path.test.ts +36 -0
- package/src/compiler/flow-path.ts +35 -1
- package/src/compiler/generate-adopt.ts +12 -2
- package/src/console/ui-next/dist/assets/{access-page-BpugjHHY.js → access-page-BDbw40sZ.js} +1 -1
- package/src/console/ui-next/dist/assets/{agent-disclosure-qnsmtJhf.js → agent-disclosure-CVhs5xog.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-BcpWUM98.js → cache-glyph-CKIaJ1te.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-Bz5xiNmR.js → call-pii-button-CPH68-4L.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-Cuxn2WH8.js → collapsible-DYYX3bKy.js} +1 -1
- package/src/console/ui-next/dist/assets/{duration-tone-Bjnl3EaM.js → duration-tone-BnRJ32O5.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-DVujp-T2.js → flows-page-DwEDnJqd.js} +1 -1
- package/src/console/ui-next/dist/assets/{highlighted-json-Bql0qlqW.js → highlighted-json-BemWpaqn.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-BljvrfRg.js → http-method-CAboOgAE.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-D1vE656k.js → index-C3KoC-mS.js} +3 -3
- package/src/console/ui-next/dist/assets/index-VxoEz295.css +2 -0
- package/src/console/ui-next/dist/assets/{observability-page-Bdi0bLJl.js → observability-page-C2sNV7Qu.js} +1 -1
- package/src/console/ui-next/dist/assets/{replica-lag-CpkPfITG.js → replica-lag-DJQeKIw-.js} +1 -1
- package/src/console/ui-next/dist/assets/{request-meta-C4ZVNFVt.js → request-meta-DQ6HY-a4.js} +1 -1
- package/src/console/ui-next/dist/assets/{store-page-DAHtnesC.js → store-page-kB4rot2u.js} +1 -1
- package/src/console/ui-next/dist/assets/{trace-detail-sheet-B2c9QRBw.js → trace-detail-sheet-B2VrPfhu.js} +1 -1
- package/src/console/ui-next/dist/assets/{tree-expand-toggle-BP4tRC98.js → tree-expand-toggle-CdecFS0p.js} +1 -1
- package/src/console/ui-next/dist/assets/{units-page-Oi6--n09.js → units-page-B-Y68cys.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-Dt-cPkiU.js → vault-page-BcDSaubT.js} +1 -1
- package/src/console/ui-next/dist/index.html +2 -2
- package/src/kernel/app.ts +23 -1
- package/src/kernel/boot.ts +9 -5
- package/src/kernel/effects-stamping.test.ts +44 -0
- package/src/kernel/errors-flow-name.ts +20 -4
- package/src/kernel/errors.registry.test.ts +4 -2
- package/src/kernel/errors.ts +1 -1
- package/src/kernel/flow-name.test.ts +105 -20
- package/src/kernel/flow.ts +3 -3
- package/src/kernel/on.ts +0 -5
- package/src/kernel/stamp-http.test.ts +2 -1
- package/src/kernel/stamp-http.ts +5 -7
- package/src/kernel/unit.ts +1 -2
- package/src/console/ui-next/dist/assets/index-D0zS5rKO.css +0 -2
|
@@ -7,12 +7,12 @@ 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 />
|
|
@@ -77,167 +77,8 @@ for the worker.
|
|
|
77
77
|
competing consumer.
|
|
78
78
|
</Callout>
|
|
79
79
|
|
|
80
|
-
## Inline or named export
|
|
81
|
-
|
|
82
|
-
| Style | When |
|
|
83
|
-
| --------------------------------------------- | --------------------------------------------------------------------------- |
|
|
84
|
-
| `on(signal.once("name", opts), flow({ do }))` | Self-contained — nothing else needs the Signal handle |
|
|
85
|
-
| `export const x = signal.once<Payload>(…)` | Another file needs `fx.emit(x, payload)` with compile-time payload checking |
|
|
86
|
-
|
|
87
|
-
Both styles stamp the same Manifest `flow.trigger`. The choice is where the
|
|
88
|
-
declaration lives, not two runtimes.
|
|
89
|
-
|
|
90
80
|
A string `fx.emit("name", payload)` still runs (runtime `schema` still applies) but does not
|
|
91
|
-
type-check.
|
|
92
|
-
|
|
93
|
-
<Tabs items={["Inline", "Named"]}>
|
|
94
|
-
|
|
95
|
-
<Tab value="Inline">
|
|
96
|
-
|
|
97
|
-
One file — declare and bind together. Producers emit with the string name:
|
|
98
|
-
|
|
99
|
-
```typescript title="src/flows/hooks/inbound.ts"
|
|
100
|
-
import { on, flow, signal } from "okengine";
|
|
101
|
-
import { z } from "zod";
|
|
102
|
-
|
|
103
|
-
export const ingestWebhook = on(
|
|
104
|
-
signal.once("hooks.inbound", {
|
|
105
|
-
schema: z.object({ id: z.string() }),
|
|
106
|
-
retries: 3,
|
|
107
|
-
deadLetter: true,
|
|
108
|
-
}),
|
|
109
|
-
flow({
|
|
110
|
-
do: async ({ id }, fx) => {
|
|
111
|
-
await fx.call(persistHook, { id });
|
|
112
|
-
},
|
|
113
|
-
}),
|
|
114
|
-
);
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
```typescript
|
|
118
|
-
await fx.emit("hooks.inbound", { id: "h_1" });
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
</Tab>
|
|
122
|
-
|
|
123
|
-
<Tab value="Named">
|
|
124
|
-
|
|
125
|
-
Export the handle when another file must `fx.emit` with compile-time payload checking:
|
|
126
|
-
|
|
127
|
-
```typescript title="src/signals/orders.ts"
|
|
128
|
-
import { signal } from "okengine";
|
|
129
|
-
import { z } from "zod";
|
|
130
|
-
|
|
131
|
-
export const orderPlaced = signal.once("orders.placed", {
|
|
132
|
-
schema: z.object({ orderId: z.string() }),
|
|
133
|
-
});
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
```typescript title="src/flows/orders/fulfill.ts"
|
|
137
|
-
import { on, flow } from "okengine";
|
|
138
|
-
import { orderPlaced } from "@/signals/orders";
|
|
139
|
-
|
|
140
|
-
export const fulfill = on(
|
|
141
|
-
orderPlaced,
|
|
142
|
-
flow("orders.fulfill", {
|
|
143
|
-
do: async ({ orderId }, fx) => {
|
|
144
|
-
await fx.call(chargeAndShip, { orderId });
|
|
145
|
-
},
|
|
146
|
-
}),
|
|
147
|
-
);
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
```typescript
|
|
151
|
-
await fx.emit(orderPlaced, { orderId: "ord_99" });
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
</Tab>
|
|
155
|
-
|
|
156
|
-
</Tabs>
|
|
157
|
-
|
|
158
|
-
## Flow name
|
|
159
|
-
|
|
160
|
-
| Style | When |
|
|
161
|
-
| ----------------------------- | ----------------------------------------------------------------------------- |
|
|
162
|
-
| `flow({ do })` | No unit folder — Flow name is the Signal name (`hooks.inbound`) |
|
|
163
|
-
| `flow("orders.fulfill")` | Manifest / `fx.call` name must differ from the Signal |
|
|
164
|
-
| Tree `export const onCreated` | `src/flows/notes/on-created.ts` stamps `notes.onCreated` — overwrites inherit |
|
|
165
|
-
|
|
166
|
-
Explicit `flow("…")` and the file tree overwrite inherit. HTTP does not inherit a
|
|
167
|
-
name from the path — nameless HTTP stays for the tree or fails **OKE1045**. Two
|
|
168
|
-
Flows that land on the same name fail **OKE1070** (`Flow "{flow}" is defined twice.`).
|
|
169
|
-
|
|
170
|
-
<Tabs items={["Inherit", "Explicit", "Tree"]}>
|
|
171
|
-
|
|
172
|
-
<Tab value="Inherit">
|
|
173
|
-
|
|
174
|
-
A file directly in `src/flows/` (no unit folder) has nothing to stamp. The Flow
|
|
175
|
-
is named `hooks.inbound` — same as the Signal:
|
|
176
|
-
|
|
177
|
-
```typescript title="src/flows/hooks.ts"
|
|
178
|
-
import { on, flow, signal } from "okengine";
|
|
179
|
-
import { z } from "zod";
|
|
180
|
-
|
|
181
|
-
export const ingestWebhook = on(
|
|
182
|
-
signal.once("hooks.inbound", {
|
|
183
|
-
schema: z.object({ id: z.string() }),
|
|
184
|
-
retries: 3,
|
|
185
|
-
deadLetter: true,
|
|
186
|
-
}),
|
|
187
|
-
flow({
|
|
188
|
-
do: async ({ id }, fx) => {
|
|
189
|
-
await fx.call(persistHook, { id });
|
|
190
|
-
},
|
|
191
|
-
}),
|
|
192
|
-
);
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
</Tab>
|
|
196
|
-
|
|
197
|
-
<Tab value="Explicit">
|
|
198
|
-
|
|
199
|
-
The Signal stays `orders.placed`. The Flow is `orders.fulfill` — that is the
|
|
200
|
-
Manifest / `fx.call` name:
|
|
201
|
-
|
|
202
|
-
```typescript title="src/flows/fulfill.ts"
|
|
203
|
-
import { on, flow } from "okengine";
|
|
204
|
-
import { orderPlaced } from "@/signals/orders";
|
|
205
|
-
|
|
206
|
-
export const fulfill = on(
|
|
207
|
-
orderPlaced,
|
|
208
|
-
flow("orders.fulfill", {
|
|
209
|
-
do: async ({ orderId }, fx) => {
|
|
210
|
-
await fx.call(chargeAndShip, { orderId });
|
|
211
|
-
},
|
|
212
|
-
}),
|
|
213
|
-
);
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
</Tab>
|
|
217
|
-
|
|
218
|
-
<Tab value="Tree">
|
|
219
|
-
|
|
220
|
-
Unit folder + `export const` stamps `unit.export`. The Flow is `notes.onCreated`,
|
|
221
|
-
not `note-created`:
|
|
222
|
-
|
|
223
|
-
```typescript title="src/flows/notes/on-created.ts"
|
|
224
|
-
import { on, flow } from "okengine";
|
|
225
|
-
import { noteCreatedMail } from "@/core";
|
|
226
|
-
import { noteCreated } from "./signals";
|
|
227
|
-
|
|
228
|
-
export const onCreated = on(
|
|
229
|
-
noteCreated,
|
|
230
|
-
flow({
|
|
231
|
-
do: async ({ id }, fx) => {
|
|
232
|
-
await fx.send(noteCreatedMail, { to: "you@localhost", data: { id } });
|
|
233
|
-
},
|
|
234
|
-
}),
|
|
235
|
-
);
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
</Tab>
|
|
239
|
-
|
|
240
|
-
</Tabs>
|
|
81
|
+
type-check. Import the exported const.
|
|
241
82
|
|
|
242
83
|
## Progressive Patterns
|
|
243
84
|
|
|
@@ -451,14 +292,14 @@ Fix: `Use signal.broadcast if each flow should independently receive this event,
|
|
|
451
292
|
|
|
452
293
|
Optional second argument to `signal.once` / `signal.broadcast` / `signal.live`. Delivery is the helper name — not an option.
|
|
453
294
|
|
|
454
|
-
| Option | Type | Default | Meaning
|
|
455
|
-
| ------------- | ------------------------ | --------- |
|
|
456
|
-
| `schema` | Standard Schema | omitted | **Emit** contract — enforced at `fx.emit` (**OKE1250** on mismatch). Workers inherit the payload; do not put `in` on `flow(
|
|
457
|
-
| `retries` | `number` | `3` | Extra attempts after the first (`retries + 1` total) — `once` path
|
|
458
|
-
| `deadLetter` | `boolean` | `true` | Keep exhausted `once` messages; `false` marks them delivered
|
|
459
|
-
| `optional` | `boolean` | `false` | Allow emit with zero subscribers
|
|
460
|
-
| `retention` | `{ maxAge?, maxCount? }` | unbounded | **`signal.live` only** — type error on `once` / `broadcast`
|
|
461
|
-
| `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 |
|
|
462
303
|
|
|
463
304
|
**Consequence:** `deadLetter` is a boolean flag, not a queue name string.
|
|
464
305
|
|
|
@@ -527,9 +368,13 @@ export default defineConfig({
|
|
|
527
368
|
</Accordion>
|
|
528
369
|
|
|
529
370
|
<Accordion title="OKE1070 — flow name defined twice">
|
|
530
|
-
Cause: `Flow "{flow}" is defined twice.` Two
|
|
531
|
-
|
|
532
|
-
|
|
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 }))`.
|
|
533
378
|
</Accordion>
|
|
534
379
|
|
|
535
380
|
<Accordion title="OKE1071 — once signal bound to more than one Flow">
|
|
@@ -557,11 +402,10 @@ export default defineConfig({
|
|
|
557
402
|
- [Broadcast](/docs/elements/signal/broadcast) — ephemeral fan-out
|
|
558
403
|
- [Live](/docs/elements/signal/live) — SSE tapes and retention
|
|
559
404
|
- [Consumers](/docs/elements/flow/consumers) — `on(signal)` workers next to Clock / CDC
|
|
560
|
-
- [Routing](/docs/elements/flow/routing#names) — tree `unit.export` vs inherit
|
|
561
405
|
- [HTTP · Live Streams](/docs/elements/flow/http#live-streams) — `http.live` exposure
|
|
562
406
|
- [fx](/docs/reference/fx) — `fx.emit`, `fx.deadLetters`, `fx.live`
|
|
563
407
|
- [Client](/docs/client/live) — `api.live` for browsers
|
|
564
|
-
- [Errors](/docs/reference/errors) — OKE1070 · OKE1071 · OKE1240 · OKE1250 · OKE1210
|
|
408
|
+
- [Errors](/docs/reference/errors) — OKE1070 · OKE1071 · OKE1072 · OKE1240 · OKE1250 · OKE1210
|
|
565
409
|
|
|
566
410
|
## Next
|
|
567
411
|
|
|
@@ -591,6 +591,11 @@ See [Durable Workflows](/docs/elements/flow/workflows).
|
|
|
591
591
|
are still one `on()` in source.
|
|
592
592
|
</Accordion>
|
|
593
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 }))`.
|
|
597
|
+
</Accordion>
|
|
598
|
+
|
|
594
599
|
<Accordion title="Messages stuck inflight">
|
|
595
600
|
Wait for the 30s lease and the next drain/claim. There is no separate timeout daemon. A handler
|
|
596
601
|
still running past the lease can overlap with a reclaim — shorten the work or journal it.
|
|
@@ -629,7 +634,7 @@ See [Durable Workflows](/docs/elements/flow/workflows).
|
|
|
629
634
|
- [Consumers](/docs/elements/flow/consumers) — `on(signal)` next to Clock / CDC
|
|
630
635
|
- [Workflows](/docs/elements/flow/workflows) — `durable` + `fx.step` for idempotent side effects
|
|
631
636
|
- [fx](/docs/reference/fx) — `fx.emit`, `fx.deadLetters`
|
|
632
|
-
- [Errors](/docs/reference/errors) — OKE1071 · OKE1240 · OKE1250 · OKE1001
|
|
637
|
+
- [Errors](/docs/reference/errors) — OKE1071 · OKE1072 · OKE1240 · OKE1250 · OKE1001
|
|
633
638
|
|
|
634
639
|
## Next
|
|
635
640
|
|
|
@@ -34,9 +34,11 @@ cd notes
|
|
|
34
34
|
### Run the dev loop
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
|
-
|
|
37
|
+
bun run dev
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
`bun run dev` is the portable form (`bunx oke dev` is the same). Bare `oke` needs `node_modules/.bin` on PATH — PowerShell does not add it.
|
|
41
|
+
|
|
40
42
|
Compose comes up (pull / create / start progress streams into the boot status
|
|
41
43
|
lines), the backend listens on **6530**, Console on **6533**, app MCP on **6535**,
|
|
42
44
|
docs MCP on **6536**. Open `http://localhost:6533`. Quit with **Ctrl+C**.
|
|
@@ -177,6 +179,17 @@ oke db search-backfill notes --batch 500
|
|
|
177
179
|
|
|
178
180
|
<Accordions>
|
|
179
181
|
|
|
182
|
+
<Accordion title="oke is not recognized (Windows PowerShell)">
|
|
183
|
+
PowerShell has no local `oke` on PATH. Use `bun run dev` or `bunx oke dev`. New Cursor/VS Code
|
|
184
|
+
terminals inherit `node_modules/.bin` from `.vscode/settings.json`. Global: `bun install -g
|
|
185
|
+
okengine`.
|
|
186
|
+
</Accordion>
|
|
187
|
+
|
|
188
|
+
<Accordion title="OKE1020 on oke dev (Compose)">
|
|
189
|
+
The app child had no Manifest to stamp effects (`main.health`). `oke dev` now hands the parent
|
|
190
|
+
extract to the child. A failed extract appends `Manifest extract failed — …` (`oxc-parser`).
|
|
191
|
+
</Accordion>
|
|
192
|
+
|
|
180
193
|
<Accordion title="oke start: no entry found">
|
|
181
194
|
Set `package.json` `okengine.entry` or `main`, pass `--entry`, or keep a conventional
|
|
182
195
|
`src/app.ts`. Production imports that module; the app must call `createBunRuntime().serve` itself.
|
|
@@ -98,6 +98,7 @@ string. Custom app codes stay message-less until registered. Full catalogs:
|
|
|
98
98
|
| `1060` | MCP tool duplicate | Two MCP tool bindings share the same tool name | Give each MCP tool exposure a unique name |
|
|
99
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
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` |
|
|
101
102
|
| `1110` | schema missing | Domain table absent in `prod` — no auto-DDL | Run `oke db migrate` against this environment |
|
|
102
103
|
| `1210` | live resume gap | `Last-Event-ID` is not on the retained tape | Reconnect without the cursor; remaining tape replays |
|
|
103
104
|
| `1240` | orphan emit | Emit with zero subscribers and `optional` false | Add `on(signal, …)` or declare `optional: true` |
|
|
@@ -173,6 +174,12 @@ Thrown by specific subsystems — each names its own cause:
|
|
|
173
174
|
A resource mount and a handwritten route share the same method + path. Drop one binding.
|
|
174
175
|
</Accordion>
|
|
175
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
|
+
|
|
176
183
|
<Accordion title="OKE1071 once signal bound to more than one Flow">
|
|
177
184
|
Cause: `Once signal "{signal}" is bound to more than one Flow ({flows}).` Use `signal.broadcast`
|
|
178
185
|
if each Flow should get a copy, or bind only one Flow. See [Once · Competing
|
|
@@ -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
|
|
|
@@ -18,7 +18,7 @@ docker info
|
|
|
18
18
|
```bash
|
|
19
19
|
bunx create-oke@latest my-app
|
|
20
20
|
cd my-app
|
|
21
|
-
|
|
21
|
+
bun run dev
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
Open the Console at the address printed in the terminal and claim it with the code shown there. The Flows listed weren't configured anywhere — they were derived from the code you just scaffolded.
|
|
@@ -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
|
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
* - `PORT` — listen port (`0` = ephemeral)
|
|
12
12
|
* - `OKE_HOSTNAME` — listen hostname (default `127.0.0.1`)
|
|
13
13
|
* - `OKE_READY_PATH` — when set, write bound port here once listening
|
|
14
|
+
* - `OKE_ROOT_DIR` — project root for Manifest extract (defaults to cwd)
|
|
15
|
+
* - `OKE_MANIFEST_PATH` — optional JSON Manifest from the parent `oke dev`
|
|
16
|
+
* extract (avoids a second extract in the child; Windows-safe fallback)
|
|
14
17
|
*
|
|
15
18
|
* Soft reload must not clear the TTY — the parent `oke dev` board owns
|
|
16
19
|
* Docker status and the first-admin claim code.
|
|
@@ -19,6 +22,7 @@
|
|
|
19
22
|
import { resolve } from "node:path";
|
|
20
23
|
import { pathToFileURL } from "node:url";
|
|
21
24
|
import { installGracefulShutdown, type GracefulShutdownApp } from "../kernel/graceful-shutdown.ts";
|
|
25
|
+
import type { Manifest } from "../manifest/types.ts";
|
|
22
26
|
import { createBunRuntime } from "../runtime/bun.ts";
|
|
23
27
|
import { APP_PORT, type FetchApp } from "../runtime/types.ts";
|
|
24
28
|
import { formatAppReadyLine } from "../term.ts";
|
|
@@ -28,7 +32,7 @@ export const DEV_APP_SERVE_ID = "oke-dev-app";
|
|
|
28
32
|
|
|
29
33
|
/** App entry shape — FetchApp plus boot before serve. */
|
|
30
34
|
type BootableApp = FetchApp & {
|
|
31
|
-
boot(): Promise<unknown>;
|
|
35
|
+
boot(overrides?: { readonly rootDir?: string; readonly manifest?: Manifest }): Promise<unknown>;
|
|
32
36
|
stop(): Promise<void>;
|
|
33
37
|
readonly bootResult?: GracefulShutdownApp["bootResult"];
|
|
34
38
|
};
|
|
@@ -49,6 +53,20 @@ const readyPath = Bun.env["OKE_READY_PATH"];
|
|
|
49
53
|
// already the project root here (Bun.spawn's `cwd` option in dev.ts).
|
|
50
54
|
process.env["OKE_ROOT_DIR"] ??= process.cwd();
|
|
51
55
|
|
|
56
|
+
const manifestPath = Bun.env["OKE_MANIFEST_PATH"];
|
|
57
|
+
let manifest: Manifest | undefined;
|
|
58
|
+
if (manifestPath !== undefined && manifestPath.length > 0) {
|
|
59
|
+
try {
|
|
60
|
+
manifest = (await Bun.file(manifestPath).json()) as Manifest;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
console.error(
|
|
63
|
+
`oke dev-app-runner: OKE_MANIFEST_PATH unreadable — ${
|
|
64
|
+
err instanceof Error ? err.message : String(err)
|
|
65
|
+
}`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
52
70
|
const absoluteEntry = resolve(entry);
|
|
53
71
|
const mod = (await import(pathToFileURL(absoluteEntry).href)) as {
|
|
54
72
|
app?: BootableApp;
|
|
@@ -63,7 +81,10 @@ if (
|
|
|
63
81
|
process.exit(1);
|
|
64
82
|
}
|
|
65
83
|
|
|
66
|
-
await mod.app.boot(
|
|
84
|
+
await mod.app.boot({
|
|
85
|
+
rootDir: process.env["OKE_ROOT_DIR"],
|
|
86
|
+
...(manifest !== undefined ? { manifest } : {}),
|
|
87
|
+
});
|
|
67
88
|
const handle = createBunRuntime().serve(mod.app, {
|
|
68
89
|
port,
|
|
69
90
|
hostname,
|
package/src/cli/dev.test.ts
CHANGED
|
@@ -1070,8 +1070,55 @@ export const posts = store.schema.table("posts", {
|
|
|
1070
1070
|
});
|
|
1071
1071
|
|
|
1072
1072
|
describe("oke dev Console vault config", () => {
|
|
1073
|
+
let session: DevSession | undefined;
|
|
1074
|
+
|
|
1075
|
+
afterEach(() => {
|
|
1076
|
+
session?.stop();
|
|
1077
|
+
session = undefined;
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1073
1080
|
test("hands loaded oke.config to Console so drivers.vault is not the env default", async () => {
|
|
1074
1081
|
const src = await Bun.file(new URL("./dev.ts", import.meta.url)).text();
|
|
1075
1082
|
expect(src).toMatch(/okeConfig:\s*loadedConfig/);
|
|
1076
1083
|
});
|
|
1084
|
+
|
|
1085
|
+
test("writes OKE_MANIFEST_PATH for the app child when a Manifest is available", async () => {
|
|
1086
|
+
const runner = await Bun.file(new URL("./dev-app-runner.ts", import.meta.url)).text();
|
|
1087
|
+
expect(runner).toContain("OKE_MANIFEST_PATH");
|
|
1088
|
+
expect(runner).toMatch(/mod\.app\.boot\(\{/);
|
|
1089
|
+
|
|
1090
|
+
const dir = await mkdtemp(join(tmpdir(), "oke-dev-manifest-handoff-"));
|
|
1091
|
+
await Bun.write(join(dir, "src/app.ts"), "export {}\n");
|
|
1092
|
+
await Bun.write(join(dir, "oke.manifest.json"), JSON.stringify(LIVE_MANIFEST));
|
|
1093
|
+
let seen: Record<string, string> | undefined;
|
|
1094
|
+
const result = await runDev({
|
|
1095
|
+
stdinIsTTY: false,
|
|
1096
|
+
cwd: dir,
|
|
1097
|
+
silentClaim: true,
|
|
1098
|
+
keepAlive: false,
|
|
1099
|
+
appPort: 0,
|
|
1100
|
+
consolePort: 0,
|
|
1101
|
+
mcpPort: 0,
|
|
1102
|
+
docsMcpPort: 0,
|
|
1103
|
+
...stubCompose(),
|
|
1104
|
+
startApp: async (_entry, env) => {
|
|
1105
|
+
seen = env;
|
|
1106
|
+
return { stop() {} };
|
|
1107
|
+
},
|
|
1108
|
+
regenClient: async () => {},
|
|
1109
|
+
write: () => {},
|
|
1110
|
+
serveConsole: async () => ({ stop() {} }),
|
|
1111
|
+
serveMcp: async () => ({ stop() {} }),
|
|
1112
|
+
serveDocsMcp: async () => ({
|
|
1113
|
+
stop() {},
|
|
1114
|
+
port: 1,
|
|
1115
|
+
url: new URL("http://127.0.0.1:1"),
|
|
1116
|
+
}),
|
|
1117
|
+
});
|
|
1118
|
+
session = result.session;
|
|
1119
|
+
expect(result.code).toBe(0);
|
|
1120
|
+
expect(seen?.["OKE_MANIFEST_PATH"]).toBeTruthy();
|
|
1121
|
+
const dumped = JSON.parse(await Bun.file(seen!["OKE_MANIFEST_PATH"]!).text()) as Manifest;
|
|
1122
|
+
expect(dumped.app).toBe("dev-live");
|
|
1123
|
+
});
|
|
1077
1124
|
});
|
package/src/cli/dev.ts
CHANGED
|
@@ -1067,6 +1067,13 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
1067
1067
|
const seedManifest =
|
|
1068
1068
|
options.manifest !== undefined ? options.manifest : await tryLoadProjectManifest(cwd);
|
|
1069
1069
|
|
|
1070
|
+
let seedManifestPath: string | undefined;
|
|
1071
|
+
if (seedManifest) {
|
|
1072
|
+
seedManifestPath = join(tmpdir(), `oke-dev-manifest-${crypto.randomUUID()}.json`);
|
|
1073
|
+
await Bun.write(seedManifestPath, `${JSON.stringify(seedManifest)}\n`);
|
|
1074
|
+
env.OKE_MANIFEST_PATH = seedManifestPath;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1070
1077
|
async function refreshManifestInto(state: ConsoleState | null): Promise<void> {
|
|
1071
1078
|
if (!state) return;
|
|
1072
1079
|
try {
|
|
@@ -1401,6 +1408,10 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
1401
1408
|
consoleVite = null;
|
|
1402
1409
|
void vite?.stop();
|
|
1403
1410
|
void clearDevSessionLock(cwd);
|
|
1411
|
+
if (seedManifestPath) {
|
|
1412
|
+
void unlink(seedManifestPath).catch(() => {});
|
|
1413
|
+
seedManifestPath = undefined;
|
|
1414
|
+
}
|
|
1404
1415
|
if (dockerStarted) {
|
|
1405
1416
|
const started = dockerStarted;
|
|
1406
1417
|
dockerStarted = null;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manifest extract must not walk `node_modules` — Windows glob paths use `\`
|
|
3
|
+
* and a naive `includes("node_modules/")` miss lets framework fixtures throw
|
|
4
|
+
* during extract, which Docker-first `oke dev` surfaces as **OKE1020**.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from "bun:test";
|
|
8
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { extractManifest } from "./extract.ts";
|
|
12
|
+
|
|
13
|
+
describe("extractManifest — skip dependency trees", () => {
|
|
14
|
+
test("does not parse node_modules even when a poison file would fail extract", async () => {
|
|
15
|
+
const dir = await mkdtemp(join(tmpdir(), "oke-extract-skip-"));
|
|
16
|
+
try {
|
|
17
|
+
await mkdir(join(dir, "src/flows/main"), { recursive: true });
|
|
18
|
+
await writeFile(
|
|
19
|
+
join(dir, "src/flows/main/health.ts"),
|
|
20
|
+
`
|
|
21
|
+
import { on, flow, http } from "okengine/http";
|
|
22
|
+
export const health = on(
|
|
23
|
+
http.get("/health").public(),
|
|
24
|
+
flow({ do: () => ({ ok: true as const }) }),
|
|
25
|
+
);
|
|
26
|
+
`,
|
|
27
|
+
);
|
|
28
|
+
await mkdir(join(dir, "node_modules/okengine"), { recursive: true });
|
|
29
|
+
// Nameless Signal consumer — **OKE1072** if this file is scanned.
|
|
30
|
+
await writeFile(
|
|
31
|
+
join(dir, "node_modules/okengine/poison.ts"),
|
|
32
|
+
`
|
|
33
|
+
import { on, flow, signal } from "okengine";
|
|
34
|
+
export const inbound = on(signal.once("x"), flow({ do: () => ({}) }));
|
|
35
|
+
`,
|
|
36
|
+
);
|
|
37
|
+
const manifest = await extractManifest({ rootDir: dir });
|
|
38
|
+
expect(manifest.flows?.["main.health"]).toBeDefined();
|
|
39
|
+
} finally {
|
|
40
|
+
await rm(dir, { recursive: true, force: true });
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
});
|