okengine 0.19.0 → 0.19.2
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/ai/meta.json +1 -1
- package/site/content/docs/ai/skills.mdx +1 -1
- package/site/content/docs/client/index.mdx +1 -2
- package/site/content/docs/elements/ai/agents.mdx +5 -2
- package/site/content/docs/elements/ai/index.mdx +4 -3
- package/site/content/docs/elements/ai/prompts.mdx +3 -2
- package/site/content/docs/elements/channel/email.mdx +5 -2
- package/site/content/docs/elements/channel/index.mdx +1 -1
- package/site/content/docs/elements/clock/index.mdx +23 -12
- package/site/content/docs/elements/clock/schedules.mdx +6 -3
- package/site/content/docs/elements/clock/sleep.mdx +10 -7
- package/site/content/docs/elements/flow/consumers.mdx +96 -58
- package/site/content/docs/elements/flow/index.mdx +15 -15
- package/site/content/docs/elements/flow/routing.mdx +11 -2
- package/site/content/docs/elements/gate/tenancy.mdx +5 -2
- package/site/content/docs/elements/signal/broadcast.mdx +6 -4
- package/site/content/docs/elements/signal/index.mdx +14 -1
- package/site/content/docs/elements/signal/live.mdx +3 -2
- package/site/content/docs/elements/signal/once.mdx +3 -2
- package/site/content/docs/elements/store/files.mdx +24 -16
- package/site/content/docs/elements/store/index.mdx +11 -8
- package/site/content/docs/elements/store/kv.mdx +24 -16
- package/site/content/docs/elements/store/search.mdx +18 -14
- package/site/content/docs/elements/store/sql.mdx +16 -12
- package/site/content/docs/elements/vault/config.mdx +5 -2
- package/site/content/docs/elements/vault/rotation.mdx +5 -2
- package/site/content/docs/elements/vault/secrets.mdx +5 -2
- package/site/content/docs/index.mdx +4 -19
- package/site/content/docs/plugins/anonymous.mdx +1 -1
- package/site/content/docs/plugins/cors.mdx +1 -1
- package/site/content/docs/plugins/csrf.mdx +1 -1
- package/site/content/docs/plugins/headers.mdx +1 -1
- package/site/content/docs/plugins/ip-allowlist.mdx +1 -1
- package/site/content/docs/plugins/maintenance-mode.mdx +1 -1
- package/site/content/docs/reference/cli.mdx +1 -1
- package/site/content/docs/reference/errors.mdx +1 -0
- package/site/content/docs/reference/fx.mdx +9 -8
- package/site/content/docs/reference/okid.mdx +1 -1
- package/site/content/docs/reference/plugins.mdx +1 -1
- package/site/content/docs/reference/security.mdx +2 -2
- package/site/content/docs/understand/meta.json +1 -1
- package/site/content/docs/understand/the-architecture.mdx +264 -0
- package/src/bench/REPORT.md +48 -17
- package/src/bench/g17-hybrid-search.bench.ts +13 -13
- package/src/compiler/extract.test.ts +76 -1
- package/src/compiler/extract.ts +106 -7
- package/src/compiler/search-writer-isolation.test.ts +0 -1
- package/src/console/ui-next/dist/assets/{access-page-DFeymU07.js → access-page-DceEWH9u.js} +1 -1
- package/src/console/ui-next/dist/assets/{agent-disclosure-U1rdfblp.js → agent-disclosure-tC9s2VFd.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-BeFJeqBG.js → cache-glyph-C-naNQSR.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-CVAONPii.js → call-pii-button-DnZ_MlDn.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-D2A6NJ-3.js → collapsible-RekgR6Qz.js} +1 -1
- package/src/console/ui-next/dist/assets/{duration-tone-Cgk_h5ja.js → duration-tone-DugtWBS0.js} +1 -1
- package/src/console/ui-next/dist/assets/flows-page-DVmn1ZuQ.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-MYZQtRnw.js → highlighted-json-CvBGaiSD.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-Jrh39p7A.js → http-method-D7_OXbdC.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-DXP2dBIF.js → index-DH0K2f6N.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-HvolXxTI.js → observability-page-qJzF2nSN.js} +1 -1
- package/src/console/ui-next/dist/assets/{replica-lag-CSh2dzrb.js → replica-lag-Vqk0pUBA.js} +1 -1
- package/src/console/ui-next/dist/assets/request-meta-BtShi4sG.js +1 -0
- package/src/console/ui-next/dist/assets/{store-page-eiKiHnNe.js → store-page-CMsYH_vH.js} +1 -1
- package/src/console/ui-next/dist/assets/{trace-detail-sheet-CZkMeKS-.js → trace-detail-sheet-ycFB2uua.js} +1 -1
- package/src/console/ui-next/dist/assets/{tree-expand-toggle-CW8y5A2h.js → tree-expand-toggle-iG1jcWgU.js} +1 -1
- package/src/console/ui-next/dist/assets/{units-page-B_RWJrEO.js → units-page-Bavchke4.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-CWrg-A68.js → vault-page-DxUFhiZI.js} +1 -1
- package/src/console/ui-next/dist/index.html +1 -1
- package/src/console/ui-next/src/features/flows/graph/element-map.test.ts +1 -1
- package/src/console/ui-next/src/features/flows/graph/element-map.ts +3 -3
- package/src/console/ui-next/src/features/flows/traces/trace-detail.test.ts +4 -5
- package/src/console/ui-next/src/features/flows/traces/trace-gates.ts +3 -6
- package/src/elements/gate/declare.ts +1 -1
- package/src/elements/gate.ts +1 -1
- package/src/elements/store/live-default.test.ts +8 -0
- package/src/elements/store/search-embed-flow.ts +2 -2
- package/src/elements/store/search-lsh.ts +35 -0
- package/src/elements/store/search-runtime.pglite.test.ts +186 -0
- package/src/elements/store/search-runtime.ts +30 -16
- package/src/elements/store/search.test.ts +83 -1
- package/src/elements/store.ts +2 -0
- package/src/full.ts +3 -0
- package/src/http.ts +3 -0
- package/src/index.ts +3 -0
- package/src/kernel/app.ts +30 -10
- package/src/kernel/boot.ts +1 -1
- package/src/kernel/cdc-payload.test.ts +224 -0
- package/src/kernel/cdc-payload.ts +146 -0
- package/src/kernel/errors-flow-name.ts +17 -0
- package/src/kernel/errors.registry.test.ts +6 -4
- package/src/kernel/flow-name.test.ts +104 -0
- package/src/kernel/flow.ts +3 -2
- package/src/kernel/fx-emit-types.test.ts +31 -0
- package/src/kernel/fx.test.ts +2 -1
- package/src/kernel/fx.ts +13 -3
- package/src/kernel/index.ts +2 -0
- package/src/kernel/on.ts +5 -0
- package/src/kernel/stamp-http.test.ts +13 -0
- package/src/kernel/stamp-http.ts +21 -3
- package/src/kernel/unit.ts +4 -2
- package/src/kernel-entry.ts +3 -0
- package/src/mcp/docs-index.ts +3 -3
- package/src/mcp/docs-mcp.test.ts +2 -2
- package/src/mcp/docs-tools.ts +2 -1
- package/site/content/docs/understand/the-anatomy.mdx +0 -132
- package/site/content/docs/understand/the-model.mdx +0 -32
- package/site/content/docs/understand/the-problem.mdx +0 -74
- package/site/content/docs/understand/the-vocabulary.mdx +0 -26
- package/src/console/ui-next/dist/assets/flows-page-Dss7941e.js +0 -1
- package/src/console/ui-next/dist/assets/request-meta-BatF8KrK.js +0 -1
- /package/site/content/docs/{ai → understand}/try-it.mdx +0 -0
|
@@ -267,9 +267,12 @@ import { z } from "zod";
|
|
|
267
267
|
import { member } from "@/core/gate";
|
|
268
268
|
|
|
269
269
|
export const switchTenant = on(
|
|
270
|
-
http
|
|
270
|
+
http
|
|
271
|
+
.post({
|
|
272
|
+
in: z.object({ tenantId: z.string() }),
|
|
273
|
+
})
|
|
274
|
+
.gate(member),
|
|
271
275
|
flow({
|
|
272
|
-
in: z.object({ tenantId: z.string() }),
|
|
273
276
|
do: async ({ tenantId }, fx) => fx.auth.switchTenant(tenantId),
|
|
274
277
|
}),
|
|
275
278
|
);
|
|
@@ -65,9 +65,10 @@ import { z } from "zod";
|
|
|
65
65
|
import { cacheInvalidated } from "@/signals/cache";
|
|
66
66
|
|
|
67
67
|
export const update = on(
|
|
68
|
-
http.patch(
|
|
69
|
-
flow({
|
|
68
|
+
http.patch({
|
|
70
69
|
in: z.object({ sku: z.string(), title: z.string().min(1) }),
|
|
70
|
+
}),
|
|
71
|
+
flow({
|
|
71
72
|
do: async ({ sku, title }, fx) => {
|
|
72
73
|
// … persist the change …
|
|
73
74
|
await fx.emit(cacheInvalidated, { key: `sku:${sku}` });
|
|
@@ -284,9 +285,10 @@ import { z } from "zod";
|
|
|
284
285
|
import { catalogChanged } from "@/signals/catalog";
|
|
285
286
|
|
|
286
287
|
export const create = on(
|
|
287
|
-
http.post(
|
|
288
|
-
flow({
|
|
288
|
+
http.post({
|
|
289
289
|
in: z.object({ sku: z.string(), title: z.string().min(1) }),
|
|
290
|
+
}),
|
|
291
|
+
flow({
|
|
290
292
|
do: async ({ sku, title }, fx) => {
|
|
291
293
|
// … write …
|
|
292
294
|
await fx.emit(catalogChanged, { sku });
|
|
@@ -80,6 +80,19 @@ for the worker.
|
|
|
80
80
|
competing consumer.
|
|
81
81
|
</Callout>
|
|
82
82
|
|
|
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
|
+
A string `fx.emit("name", payload)` still runs (runtime `schema` still applies) but does not
|
|
91
|
+
type-check the payload.
|
|
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**.
|
|
95
|
+
|
|
83
96
|
## Progressive Patterns
|
|
84
97
|
|
|
85
98
|
Same helpers + `fx.emit` from a queue job to a fan-out and a browser feed:
|
|
@@ -347,6 +360,6 @@ export default defineConfig({
|
|
|
347
360
|
<Card
|
|
348
361
|
title="The Model"
|
|
349
362
|
description="Eight elements overview."
|
|
350
|
-
href="/docs/understand/the-
|
|
363
|
+
href="/docs/understand/the-architecture"
|
|
351
364
|
/>
|
|
352
365
|
</Cards>
|
|
@@ -342,9 +342,10 @@ import { z } from "zod";
|
|
|
342
342
|
import { orderStatus } from "@/signals/orders";
|
|
343
343
|
|
|
344
344
|
export const ship = on(
|
|
345
|
-
http.post(
|
|
346
|
-
flow({
|
|
345
|
+
http.post({
|
|
347
346
|
in: z.object({ id: z.string() }),
|
|
347
|
+
}),
|
|
348
|
+
flow({
|
|
348
349
|
do: async ({ id }, fx) => {
|
|
349
350
|
await fx.emit(orderStatus, { orderId: id, status: "shipped" });
|
|
350
351
|
return { id, status: "shipped" };
|
|
@@ -311,9 +311,10 @@ import { z } from "zod";
|
|
|
311
311
|
import { orderPlaced } from "@/signals/orders";
|
|
312
312
|
|
|
313
313
|
export const create = on(
|
|
314
|
-
http.post(
|
|
315
|
-
flow({
|
|
314
|
+
http.post({
|
|
316
315
|
in: z.object({ userId: z.string(), amount: z.number() }),
|
|
316
|
+
}),
|
|
317
|
+
flow({
|
|
317
318
|
do: async ({ userId, amount }, fx) => {
|
|
318
319
|
const orderId = fx.id();
|
|
319
320
|
await fx.emit(orderPlaced, { orderId, amount, userId }, { key: userId });
|
|
@@ -38,13 +38,14 @@ import { z } from "zod";
|
|
|
38
38
|
import { uploads } from "@/core";
|
|
39
39
|
|
|
40
40
|
export const create = on(
|
|
41
|
-
http.post(
|
|
42
|
-
flow({
|
|
41
|
+
http.post({
|
|
43
42
|
in: z.object({
|
|
44
43
|
name: z.string().min(1),
|
|
45
44
|
bytes: z.string(), // base64 for the demo
|
|
46
45
|
}),
|
|
47
46
|
out: z.object({ key: z.string() }),
|
|
47
|
+
}),
|
|
48
|
+
flow({
|
|
48
49
|
do: async ({ name, bytes }, fx) => {
|
|
49
50
|
const key = `uploads/${fx.id()}-${name}`;
|
|
50
51
|
const data = Uint8Array.from(atob(bytes), (c) => c.charCodeAt(0));
|
|
@@ -123,9 +124,10 @@ import { z } from "zod";
|
|
|
123
124
|
import { uploads } from "@/core";
|
|
124
125
|
|
|
125
126
|
export const cleanup = on(
|
|
126
|
-
http.delete(
|
|
127
|
-
flow({
|
|
127
|
+
http.delete({
|
|
128
128
|
in: z.object({ prefix: z.string().default("avatars/") }),
|
|
129
|
+
}),
|
|
130
|
+
flow({
|
|
129
131
|
do: async ({ prefix }, fx) => {
|
|
130
132
|
const keys = await fx.store(uploads).list(prefix);
|
|
131
133
|
let removed = 0;
|
|
@@ -154,9 +156,10 @@ import { z } from "zod";
|
|
|
154
156
|
import { uploads } from "@/core";
|
|
155
157
|
|
|
156
158
|
export const create = on(
|
|
157
|
-
http.post(
|
|
158
|
-
flow({
|
|
159
|
+
http.post({
|
|
159
160
|
in: z.object({ bytes: z.string() }),
|
|
161
|
+
}),
|
|
162
|
+
flow({
|
|
160
163
|
do: async ({ bytes }, fx) => {
|
|
161
164
|
const key = `photos/${fx.id()}.jpg`;
|
|
162
165
|
const data = Uint8Array.from(atob(bytes), (c) => c.charCodeAt(0));
|
|
@@ -195,9 +198,10 @@ import { z } from "zod";
|
|
|
195
198
|
import { uploads } from "@/core";
|
|
196
199
|
|
|
197
200
|
export const card = on(
|
|
198
|
-
http.post(
|
|
199
|
-
flow({
|
|
201
|
+
http.post({
|
|
200
202
|
in: z.object({ sourceKey: z.string() }),
|
|
203
|
+
}),
|
|
204
|
+
flow({
|
|
201
205
|
do: async ({ sourceKey }, fx) => {
|
|
202
206
|
const outKey = sourceKey.replace(/\.[^.]+$/, ".card.webp");
|
|
203
207
|
await fx
|
|
@@ -282,13 +286,14 @@ import { z } from "zod";
|
|
|
282
286
|
import { uploads } from "@/core";
|
|
283
287
|
|
|
284
288
|
export const create = on(
|
|
285
|
-
http.post(
|
|
286
|
-
flow({
|
|
289
|
+
http.post({
|
|
287
290
|
in: z.object({
|
|
288
291
|
id: z.string(),
|
|
289
292
|
name: z.string().min(1),
|
|
290
293
|
bytes: z.string(),
|
|
291
294
|
}),
|
|
295
|
+
}),
|
|
296
|
+
flow({
|
|
292
297
|
do: async ({ id, name, bytes }, fx) => {
|
|
293
298
|
const key = `notes/${id}/${name}`;
|
|
294
299
|
const data = Uint8Array.from(atob(bytes), (c) => c.charCodeAt(0));
|
|
@@ -311,10 +316,11 @@ import { z } from "zod";
|
|
|
311
316
|
import { uploads } from "@/core";
|
|
312
317
|
|
|
313
318
|
export const get = on(
|
|
314
|
-
http.get(
|
|
315
|
-
flow({
|
|
319
|
+
http.get({
|
|
316
320
|
in: z.object({ id: z.string(), name: z.string() }),
|
|
317
321
|
errors: { NotFound: z.object({ key: z.string() }) },
|
|
322
|
+
}),
|
|
323
|
+
flow({
|
|
318
324
|
do: async ({ id, name }, fx) => {
|
|
319
325
|
const key = `notes/${id}/${name}`;
|
|
320
326
|
const bytes = await fx.store(uploads).get(key);
|
|
@@ -337,9 +343,10 @@ import { z } from "zod";
|
|
|
337
343
|
import { uploads } from "@/core";
|
|
338
344
|
|
|
339
345
|
export const remove = on(
|
|
340
|
-
http.delete(
|
|
341
|
-
flow({
|
|
346
|
+
http.delete({
|
|
342
347
|
in: z.object({ id: z.string(), name: z.string() }),
|
|
348
|
+
}),
|
|
349
|
+
flow({
|
|
343
350
|
do: async ({ id, name }, fx) => {
|
|
344
351
|
const key = `notes/${id}/${name}`;
|
|
345
352
|
const removed = await fx.store(uploads).delete(key);
|
|
@@ -362,9 +369,10 @@ import { z } from "zod";
|
|
|
362
369
|
import { uploads } from "@/core";
|
|
363
370
|
|
|
364
371
|
export const attachments = on(
|
|
365
|
-
http.get(
|
|
366
|
-
flow({
|
|
372
|
+
http.get({
|
|
367
373
|
in: z.object({ id: z.string() }),
|
|
374
|
+
}),
|
|
375
|
+
flow({
|
|
368
376
|
do: async ({ id }, fx) => {
|
|
369
377
|
const keys = await fx.store(uploads).list(`notes/${id}/`);
|
|
370
378
|
return { keys };
|
|
@@ -47,9 +47,10 @@ import { z } from "zod";
|
|
|
47
47
|
import { db, notes } from "@/schema";
|
|
48
48
|
|
|
49
49
|
export const create = on(
|
|
50
|
-
http.post(
|
|
51
|
-
flow({
|
|
50
|
+
http.post({
|
|
52
51
|
in: z.object({ title: z.string().min(1) }),
|
|
52
|
+
}),
|
|
53
|
+
flow({
|
|
53
54
|
do: async ({ title }, fx) => {
|
|
54
55
|
const id = fx.id();
|
|
55
56
|
await fx.store(db).insert(notes).values({ id, title });
|
|
@@ -106,10 +107,11 @@ import { eq } from "drizzle-orm";
|
|
|
106
107
|
import { db, notes } from "@/schema";
|
|
107
108
|
|
|
108
109
|
export const get = on(
|
|
109
|
-
http.get(
|
|
110
|
-
flow({
|
|
110
|
+
http.get({
|
|
111
111
|
in: z.object({ id: z.string() }),
|
|
112
112
|
errors: { NotFound: z.object({ id: z.string() }) },
|
|
113
|
+
}),
|
|
114
|
+
flow({
|
|
113
115
|
do: async ({ id }, fx) => {
|
|
114
116
|
const [note] = await fx.store(db).select().from(notes).where(eq(notes.id, id));
|
|
115
117
|
if (!note) return fx.fail("NotFound", { id });
|
|
@@ -128,11 +130,11 @@ Prefer [`store.resource`](/docs/elements/store/sql#resources) when you want five
|
|
|
128
130
|
Pass the **declaration** into `fx.store` — there is no `fx.store.kv` namespace. TTL is a duration string:
|
|
129
131
|
|
|
130
132
|
```typescript title="src/flows/sessions/put.ts"
|
|
131
|
-
import {
|
|
133
|
+
import { call } from "okengine";
|
|
132
134
|
import { z } from "zod";
|
|
133
135
|
import { sessions } from "@/core";
|
|
134
136
|
|
|
135
|
-
export const putSession =
|
|
137
|
+
export const putSession = call("sessions.put", {
|
|
136
138
|
in: z.object({ token: z.string(), userId: z.string() }),
|
|
137
139
|
do: async ({ token, userId }, fx) => {
|
|
138
140
|
await fx.store(sessions).set(`session:${token}`, userId, "1h");
|
|
@@ -159,9 +161,10 @@ import { z } from "zod";
|
|
|
159
161
|
import { uploads } from "@/core";
|
|
160
162
|
|
|
161
163
|
export const create = on(
|
|
162
|
-
http.post(
|
|
163
|
-
flow({
|
|
164
|
+
http.post({
|
|
164
165
|
in: z.object({ bytes: z.string() }),
|
|
166
|
+
}),
|
|
167
|
+
flow({
|
|
165
168
|
do: async ({ bytes }, fx) => {
|
|
166
169
|
const key = `avatars/${fx.id()}.png`;
|
|
167
170
|
const data = Uint8Array.from(atob(bytes), (c) => c.charCodeAt(0));
|
|
@@ -39,9 +39,10 @@ import { z } from "zod";
|
|
|
39
39
|
import { sessions } from "@/core";
|
|
40
40
|
|
|
41
41
|
export const create = on(
|
|
42
|
-
http.post(
|
|
43
|
-
flow({
|
|
42
|
+
http.post({
|
|
44
43
|
in: z.object({ token: z.string(), userId: z.string() }),
|
|
44
|
+
}),
|
|
45
|
+
flow({
|
|
45
46
|
do: async ({ token, userId }, fx) => {
|
|
46
47
|
await fx.store(sessions).set(`session:${token}`, userId, "1h");
|
|
47
48
|
const cached = await fx.store(sessions).get(`session:${token}`);
|
|
@@ -97,9 +98,10 @@ import { z } from "zod";
|
|
|
97
98
|
import { sessions } from "@/core";
|
|
98
99
|
|
|
99
100
|
export const get = on(
|
|
100
|
-
http.get(
|
|
101
|
-
flow({
|
|
101
|
+
http.get({
|
|
102
102
|
in: z.object({ userId: z.string() }),
|
|
103
|
+
}),
|
|
104
|
+
flow({
|
|
103
105
|
do: async ({ userId }, fx) => {
|
|
104
106
|
const prefs = await fx.store(sessions).get(`user:${userId}:prefs`);
|
|
105
107
|
return { prefs: prefs ?? null };
|
|
@@ -122,9 +124,10 @@ import { z } from "zod";
|
|
|
122
124
|
import { sessions } from "@/core";
|
|
123
125
|
|
|
124
126
|
export const create = on(
|
|
125
|
-
http.post(
|
|
126
|
-
flow({
|
|
127
|
+
http.post({
|
|
127
128
|
in: z.object({ userId: z.string() }),
|
|
129
|
+
}),
|
|
130
|
+
flow({
|
|
128
131
|
do: async ({ userId }, fx) => {
|
|
129
132
|
const code = "482193";
|
|
130
133
|
await fx.store(sessions).set(`otp:${userId}`, code, "5m");
|
|
@@ -251,10 +254,11 @@ import { z } from "zod";
|
|
|
251
254
|
import { sessions } from "@/core";
|
|
252
255
|
|
|
253
256
|
export const get = on(
|
|
254
|
-
http.get(
|
|
255
|
-
flow({
|
|
257
|
+
http.get({
|
|
256
258
|
in: z.object({ token: z.string() }),
|
|
257
259
|
errors: { NotFound: z.object({ token: z.string() }) },
|
|
260
|
+
}),
|
|
261
|
+
flow({
|
|
258
262
|
do: async ({ token }, fx) => {
|
|
259
263
|
const userId = await fx.store(sessions).get(`session:${token}`);
|
|
260
264
|
if (userId === undefined || userId === null) {
|
|
@@ -278,9 +282,10 @@ import { z } from "zod";
|
|
|
278
282
|
import { sessions } from "@/core";
|
|
279
283
|
|
|
280
284
|
export const create = on(
|
|
281
|
-
http.post(
|
|
282
|
-
flow({
|
|
285
|
+
http.post({
|
|
283
286
|
in: z.object({ token: z.string(), userId: z.string() }),
|
|
287
|
+
}),
|
|
288
|
+
flow({
|
|
284
289
|
do: async ({ token, userId }, fx) => {
|
|
285
290
|
await fx.store(sessions).set(`session:${token}`, userId, "1h");
|
|
286
291
|
return { ok: true as const };
|
|
@@ -304,9 +309,10 @@ import { z } from "zod";
|
|
|
304
309
|
import { sessions } from "@/core";
|
|
305
310
|
|
|
306
311
|
export const remove = on(
|
|
307
|
-
http.delete(
|
|
308
|
-
flow({
|
|
312
|
+
http.delete({
|
|
309
313
|
in: z.object({ token: z.string() }),
|
|
314
|
+
}),
|
|
315
|
+
flow({
|
|
310
316
|
do: async ({ token }, fx) => {
|
|
311
317
|
const removed = await fx.store(sessions).delete(`session:${token}`);
|
|
312
318
|
return { removed };
|
|
@@ -358,9 +364,10 @@ import { z } from "zod";
|
|
|
358
364
|
import { sessions } from "@/core";
|
|
359
365
|
|
|
360
366
|
export const ttl = on(
|
|
361
|
-
http.get(
|
|
362
|
-
flow({
|
|
367
|
+
http.get({
|
|
363
368
|
in: z.object({ userId: z.string() }),
|
|
369
|
+
}),
|
|
370
|
+
flow({
|
|
364
371
|
do: async ({ userId }, fx) => {
|
|
365
372
|
const ttlMs = await fx.store(sessions).ttlMs(`otp:${userId}`);
|
|
366
373
|
return { ttlMs };
|
|
@@ -409,13 +416,14 @@ import { z } from "zod";
|
|
|
409
416
|
import { drafts } from "@/core";
|
|
410
417
|
|
|
411
418
|
export const save = on(
|
|
412
|
-
http.put("/drafts/:id"
|
|
413
|
-
flow({
|
|
419
|
+
http.put("/drafts/:id", {
|
|
414
420
|
in: z.object({
|
|
415
421
|
id: z.string().min(1),
|
|
416
422
|
title: z.string().min(1),
|
|
417
423
|
body: z.string().optional(),
|
|
418
424
|
}),
|
|
425
|
+
}),
|
|
426
|
+
flow({
|
|
419
427
|
do: async ({ id, title, body }, fx) => {
|
|
420
428
|
await fx.store(drafts).set(id, { title, body: body ?? "" }, "7d");
|
|
421
429
|
return { id };
|
|
@@ -42,9 +42,10 @@ import { z } from "zod";
|
|
|
42
42
|
import { articles, db } from "@/schema";
|
|
43
43
|
|
|
44
44
|
export const search = on(
|
|
45
|
-
http.get(
|
|
46
|
-
flow({
|
|
45
|
+
http.get({
|
|
47
46
|
in: z.object({ q: z.string() }),
|
|
47
|
+
}),
|
|
48
|
+
flow({
|
|
48
49
|
do: async ({ q }, fx) => {
|
|
49
50
|
const result = await fx.store(db).search(articles, {
|
|
50
51
|
query: q,
|
|
@@ -399,9 +400,10 @@ import { z } from "zod";
|
|
|
399
400
|
import { articles, db } from "@/schema";
|
|
400
401
|
|
|
401
402
|
export const search = on(
|
|
402
|
-
http.get(
|
|
403
|
-
flow({
|
|
403
|
+
http.get({
|
|
404
404
|
in: z.object({ q: z.string().min(1) }),
|
|
405
|
+
}),
|
|
406
|
+
flow({
|
|
405
407
|
do: async ({ q }, fx) => {
|
|
406
408
|
const result = await fx.store(db).search(articles, {
|
|
407
409
|
query: q,
|
|
@@ -430,12 +432,13 @@ import { z } from "zod";
|
|
|
430
432
|
import { articles, db } from "@/schema";
|
|
431
433
|
|
|
432
434
|
export const search = on(
|
|
433
|
-
http.get(
|
|
434
|
-
flow({
|
|
435
|
+
http.get({
|
|
435
436
|
in: z.object({
|
|
436
437
|
q: z.string().min(1),
|
|
437
438
|
status: z.string().optional(),
|
|
438
439
|
}),
|
|
440
|
+
}),
|
|
441
|
+
flow({
|
|
439
442
|
do: async ({ q, status }, fx) => {
|
|
440
443
|
const result = await fx.store(db).search(articles, {
|
|
441
444
|
query: q,
|
|
@@ -539,7 +542,7 @@ intentional. Do not poll `fx.embed` from the writer to “close” it.
|
|
|
539
542
|
| ------------------------------------- | ---------------------------------------------------------- |
|
|
540
543
|
| Generated `tsvector` + GIN | BM25 candidate retrieval (`plainto_tsquery('english', …)`) |
|
|
541
544
|
| `real[]` embedding column | Stored vector per `.embed()` field |
|
|
542
|
-
| `bigint` LSH column + B-tree |
|
|
545
|
+
| `bigint` LSH column + B-tree | Stored SimHash pack (query ranks by Hamming, not equality) |
|
|
543
546
|
| Corpus stats / DF / hyperplane tables | IDF, average length, stable LSH planes |
|
|
544
547
|
|
|
545
548
|
Hyperplanes insert once (`ON CONFLICT DO NOTHING`) and are **never** regenerated.
|
|
@@ -619,8 +622,9 @@ await fx.store(db).search(articles, {
|
|
|
619
622
|
BM25F uses Robertson–Zaragoza saturation: **k1 = 1.2**, **b = 0.75**. Field
|
|
620
623
|
`weight` multiplies term frequency *before* that saturation.
|
|
621
624
|
|
|
622
|
-
LSH uses **64** hyperplanes
|
|
623
|
-
|
|
625
|
+
LSH uses **64** hyperplanes packed into a `bigint`. Query-time retrieval ranks
|
|
626
|
+
rows by Hamming distance (`bit_count` of XOR) and keeps the oversampled nearest
|
|
627
|
+
(`max(limit × 5, 50)`, cap 500), then cosine-reranks in process.
|
|
624
628
|
|
|
625
629
|
</Accordion>
|
|
626
630
|
|
|
@@ -800,13 +804,13 @@ Headline numbers from the live-Postgres G17 gate (`OKE_TEST_POSTGRES=1`, Bun 1.4
|
|
|
800
804
|
|
|
801
805
|
| Corpus size | BM25 (text) p50 | LSH/hybrid p50 | LSH precision@10 vs exact cosine | Guidance |
|
|
802
806
|
| ----------- | --------------- | -------------- | -------------------------------- | ---------------------------------------------------------------------------------------- |
|
|
803
|
-
| ≤10k | ~1–
|
|
804
|
-
| ~100k | ~
|
|
805
|
-
| ~1M | ~0.
|
|
807
|
+
| ≤10k | ~1–5 ms | ~1–9 ms | 0.10–0.17 vector | Built-in hybrid is fine for ranking UX; LSH is not HNSW |
|
|
808
|
+
| ~100k | ~21 ms | ~31–34 ms | 0.017 vector | Expect tens of ms; re-`EXPLAIN` after `ANALYZE` |
|
|
809
|
+
| ~1M | ~0.3 s | ~0.4 s | 0.017 vector / 0 hybrid | Prefer external `store.index` (pgvector / Meilisearch) for semantic recall at this scale |
|
|
806
810
|
|
|
807
|
-
**Honest LSH note:**
|
|
811
|
+
**Honest LSH note:** after the Hamming-rank fix (2026-09-11), vector precision@10 vs exact cosine is 0.17 at 1k, 0.10 at 10k, ~0.02 at 100k–1M on the G17 hash-bag corpus — a smooth drop, not the v0.19.0 zero collapse. Still **not** HNSW. Prefer BM25-only or `store.index` when semantic recall matters.
|
|
808
812
|
|
|
809
|
-
**Query plan:** at N=100k, `EXPLAIN (ANALYZE, BUFFERS)`
|
|
813
|
+
**Query plan:** at N=100k, `EXPLAIN (ANALYZE, BUFFERS)` is a UNION of GIN **Bitmap Index Scan** and a **Parallel Seq Scan** + top-N heapsort on Hamming distance (~15 ms). Hamming-rank cannot use the LSH B-tree. Capture your own plan on production data.
|
|
810
814
|
|
|
811
815
|
**Backfill:** `oke db search-backfill` is interrupt-safe to re-run (G17 killed at 2k/50k embeds, resumed to completion in ~29 s on that table).
|
|
812
816
|
|
|
@@ -291,11 +291,12 @@ import { eq } from "drizzle-orm";
|
|
|
291
291
|
import { db, notes } from "@/schema";
|
|
292
292
|
|
|
293
293
|
export const get = on(
|
|
294
|
-
http.get(
|
|
295
|
-
flow({
|
|
294
|
+
http.get({
|
|
296
295
|
in: z.object({ id: z.string() }),
|
|
297
296
|
out: z.object({ id: z.string(), title: z.string() }),
|
|
298
297
|
errors: { NotFound: z.object({ id: z.string() }) },
|
|
298
|
+
}),
|
|
299
|
+
flow({
|
|
299
300
|
do: async ({ id }, fx) => {
|
|
300
301
|
const [note] = await fx.store(db).select().from(notes).where(eq(notes.id, id));
|
|
301
302
|
if (!note) return fx.fail("NotFound", { id });
|
|
@@ -317,10 +318,11 @@ import { z } from "zod";
|
|
|
317
318
|
import { db, notes } from "@/schema";
|
|
318
319
|
|
|
319
320
|
export const create = on(
|
|
320
|
-
http.post(
|
|
321
|
-
flow({
|
|
321
|
+
http.post({
|
|
322
322
|
in: z.object({ title: z.string().min(1) }),
|
|
323
323
|
out: z.object({ id: z.string(), title: z.string() }),
|
|
324
|
+
}),
|
|
325
|
+
flow({
|
|
324
326
|
do: async ({ title }, fx) => {
|
|
325
327
|
const id = fx.id();
|
|
326
328
|
const [row] = await fx.store(db).insert(notes).values({ id, title }).returning();
|
|
@@ -344,13 +346,14 @@ import { eq } from "drizzle-orm";
|
|
|
344
346
|
import { db, notes } from "@/schema";
|
|
345
347
|
|
|
346
348
|
export const update = on(
|
|
347
|
-
http.patch(
|
|
348
|
-
flow({
|
|
349
|
+
http.patch({
|
|
349
350
|
in: z.object({
|
|
350
351
|
id: z.string(),
|
|
351
352
|
title: z.string().min(1).optional(),
|
|
352
353
|
}),
|
|
353
354
|
errors: { NotFound: z.object({ id: z.string() }) },
|
|
355
|
+
}),
|
|
356
|
+
flow({
|
|
354
357
|
do: async ({ id, title }, fx) => {
|
|
355
358
|
if (title !== undefined) {
|
|
356
359
|
await fx.store(db).update(notes).set({ title }).where(eq(notes.id, id));
|
|
@@ -376,9 +379,10 @@ import { z } from "zod";
|
|
|
376
379
|
import { db, notes } from "@/schema";
|
|
377
380
|
|
|
378
381
|
export const remove = on(
|
|
379
|
-
http.delete(
|
|
380
|
-
flow({
|
|
382
|
+
http.delete({
|
|
381
383
|
in: z.object({ id: z.string() }),
|
|
384
|
+
}),
|
|
385
|
+
flow({
|
|
382
386
|
do: async ({ id }, fx) => {
|
|
383
387
|
await fx.store(db).delete(notes, id);
|
|
384
388
|
return fx.json.empty();
|
|
@@ -422,11 +426,11 @@ prefer them for live feeds. Offset is fine for admin tables.
|
|
|
422
426
|
Default is insert-once. Pass `{ onExisting: "update" }` to overwrite a match:
|
|
423
427
|
|
|
424
428
|
```typescript title="src/flows/notes/ensure.ts"
|
|
425
|
-
import {
|
|
429
|
+
import { call } from "okengine";
|
|
426
430
|
import { z } from "zod";
|
|
427
431
|
import { db, notes } from "@/schema";
|
|
428
432
|
|
|
429
|
-
export const ensureWelcome =
|
|
433
|
+
export const ensureWelcome = call("notes.ensureWelcome", {
|
|
430
434
|
in: z.object({ title: z.string() }),
|
|
431
435
|
do: async ({ title }, fx) => {
|
|
432
436
|
const result = await fx.store(db).upsert(
|
|
@@ -803,8 +807,8 @@ Default is insert-once; pass `{ onExisting: "update" }` to overwrite.
|
|
|
803
807
|
</Callout>
|
|
804
808
|
|
|
805
809
|
`db.table(orders).changed(column?)` builds a CDC trigger for `on(…)`. Input is
|
|
806
|
-
|
|
807
|
-
Manifest — it is not an op filter.
|
|
810
|
+
`{ before, after }` plus `table` / `action` / `id`. `changed("status")` stamps a
|
|
811
|
+
**column** name on the Manifest — it is not an op filter.
|
|
808
812
|
|
|
809
813
|
```typescript title="src/flows/orders/on-status.ts"
|
|
810
814
|
import { on, flow } from "okengine";
|
|
@@ -48,9 +48,12 @@ import { member } from "@/core/gate";
|
|
|
48
48
|
import { publicAppUrl, noteCreatedMail } from "@/core";
|
|
49
49
|
|
|
50
50
|
export const create = on(
|
|
51
|
-
http
|
|
51
|
+
http
|
|
52
|
+
.post({
|
|
53
|
+
in: z.object({ email: z.string().email() }),
|
|
54
|
+
})
|
|
55
|
+
.gate(member),
|
|
52
56
|
flow({
|
|
53
|
-
in: z.object({ email: z.string().email() }),
|
|
54
57
|
do: async ({ email }, fx) => {
|
|
55
58
|
const origin = await fx.vault.get(publicAppUrl);
|
|
56
59
|
await fx.send(noteCreatedMail, {
|
|
@@ -93,10 +93,13 @@ import { gate } from "okengine";
|
|
|
93
93
|
const operator = gate.policy("operator", ({ operator: op }) => !!op);
|
|
94
94
|
|
|
95
95
|
export const rotateStripe = on(
|
|
96
|
-
http
|
|
96
|
+
http
|
|
97
|
+
.post({
|
|
98
|
+
in: z.object({ value: z.string().min(1) }),
|
|
99
|
+
})
|
|
100
|
+
.gate(operator),
|
|
97
101
|
flow({
|
|
98
102
|
plane: "operator",
|
|
99
|
-
in: z.object({ value: z.string().min(1) }),
|
|
100
103
|
effects: { secrets: ["STRIPE_KEY"] },
|
|
101
104
|
do: async ({ value }, fx) => {
|
|
102
105
|
const result = await fx.vault.rotate("STRIPE_KEY", value);
|
|
@@ -51,9 +51,12 @@ import { member } from "@/core/gate";
|
|
|
51
51
|
import { stripeKey } from "@/core/vault";
|
|
52
52
|
|
|
53
53
|
export const charge = on(
|
|
54
|
-
http
|
|
54
|
+
http
|
|
55
|
+
.post({
|
|
56
|
+
in: z.object({ amount: z.number().int().positive() }),
|
|
57
|
+
})
|
|
58
|
+
.gate(member),
|
|
55
59
|
flow({
|
|
56
|
-
in: z.object({ amount: z.number().int().positive() }),
|
|
57
60
|
do: async ({ amount }, fx) => {
|
|
58
61
|
const key = await fx.vault.get(stripeKey);
|
|
59
62
|
const stripe = new Stripe(key.reveal());
|
|
@@ -23,29 +23,14 @@ Master the mental model before exploring features:
|
|
|
23
23
|
|
|
24
24
|
<Cards>
|
|
25
25
|
<Card
|
|
26
|
-
title="The
|
|
27
|
-
description="
|
|
28
|
-
href="/docs/understand/the-
|
|
29
|
-
/>
|
|
30
|
-
<Card
|
|
31
|
-
title="The Model"
|
|
32
|
-
description="The one rule that removes the disagreement between systems — stated plainly, in two parts."
|
|
33
|
-
href="/docs/understand/the-model"
|
|
34
|
-
/>
|
|
35
|
-
<Card
|
|
36
|
-
title="The Vocabulary"
|
|
37
|
-
description="The eight things the door recognizes — what each one replaces, and why nothing else made the cut."
|
|
38
|
-
href="/docs/understand/the-vocabulary"
|
|
39
|
-
/>
|
|
40
|
-
<Card
|
|
41
|
-
title="The Anatomy"
|
|
42
|
-
description="The five pieces behind on(trigger, flow) — on, trigger, flow, do, and fx — explained one at a time."
|
|
43
|
-
href="/docs/understand/the-anatomy"
|
|
26
|
+
title="The Architecture"
|
|
27
|
+
description="Why backends drift apart, the one rule that stops it, and the five pieces behind every Flow."
|
|
28
|
+
href="/docs/understand/the-architecture"
|
|
44
29
|
/>
|
|
45
30
|
<Card
|
|
46
31
|
title="Try It"
|
|
47
32
|
description="From an empty folder to a Flow running in the Console — one sitting, minimal detour."
|
|
48
|
-
href="/docs/
|
|
33
|
+
href="/docs/understand/try-it"
|
|
49
34
|
/>
|
|
50
35
|
</Cards>
|
|
51
36
|
|
|
@@ -38,7 +38,7 @@ export const app = oke({
|
|
|
38
38
|
|
|
39
39
|
```typescript
|
|
40
40
|
const { data } = await api.auth.signInAnonymous();
|
|
41
|
-
// data.userId is a fresh
|
|
41
|
+
// data.userId is a fresh OKID; store tokens like any other session
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
`POST /auth/sign-in/anonymous` — no body.
|
|
@@ -71,7 +71,7 @@ const origins = configSource({
|
|
|
71
71
|
db: { store: db },
|
|
72
72
|
kv: cache,
|
|
73
73
|
});
|
|
74
|
-
const corsSyncClock = clock("cors.sync",
|
|
74
|
+
const corsSyncClock = clock.every("cors.sync", "30s");
|
|
75
75
|
on(corsSyncClock, origins.sync());
|
|
76
76
|
export const app = oke({ name: "shop", env: "dev" }).plug(cors(origins));
|
|
77
77
|
```
|
|
@@ -75,7 +75,7 @@ const rules = configSource({
|
|
|
75
75
|
db: { store: db },
|
|
76
76
|
kv: cache,
|
|
77
77
|
});
|
|
78
|
-
const csrfSyncClock = clock("csrf.sync",
|
|
78
|
+
const csrfSyncClock = clock.every("csrf.sync", "30s");
|
|
79
79
|
on(csrfSyncClock, rules.sync());
|
|
80
80
|
export const app = oke({ name: "shop", env: "dev" }).plug(csrf(rules));
|
|
81
81
|
```
|
|
@@ -103,7 +103,7 @@ const headerConfig = configSource({
|
|
|
103
103
|
db: { store: db },
|
|
104
104
|
kv: cache,
|
|
105
105
|
});
|
|
106
|
-
const headerSyncClock = clock("headers.sync",
|
|
106
|
+
const headerSyncClock = clock.every("headers.sync", "30s");
|
|
107
107
|
on(headerSyncClock, headerConfig.sync());
|
|
108
108
|
export const app = oke({ name: "shop", env: "dev" }).plug(headers(headerConfig));
|
|
109
109
|
```
|
|
@@ -69,7 +69,7 @@ const rules = configSource({
|
|
|
69
69
|
db: { store: db },
|
|
70
70
|
kv: cache,
|
|
71
71
|
});
|
|
72
|
-
const ipRulesSyncClock = clock("ip-allowlist.sync",
|
|
72
|
+
const ipRulesSyncClock = clock.every("ip-allowlist.sync", "30s");
|
|
73
73
|
on(ipRulesSyncClock, rules.sync());
|
|
74
74
|
export const app = oke({ name: "shop", env: "dev" }).plug(ipAllowlist(rules));
|
|
75
75
|
```
|