okengine 0.17.2 → 0.18.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.
Files changed (209) hide show
  1. package/AGENTS.md +5 -3
  2. package/README.md +14 -10
  3. package/manifest.v1.schema.json +61 -2
  4. package/package.json +19 -5
  5. package/site/content/docs/ai/index.mdx +1 -1
  6. package/site/content/docs/ai/mcp.mdx +10 -1
  7. package/site/content/docs/ai/skills.mdx +9 -9
  8. package/site/content/docs/elements/ai.mdx +1 -1
  9. package/site/content/docs/elements/clock.mdx +1 -1
  10. package/site/content/docs/elements/flow.mdx +25 -1
  11. package/site/content/docs/elements/gate.mdx +3 -2
  12. package/site/content/docs/elements/store.mdx +289 -341
  13. package/site/content/docs/elements/vault.mdx +5 -5
  14. package/site/content/docs/get-started/basic-usage.mdx +3 -10
  15. package/site/content/docs/get-started/index.mdx +1 -1
  16. package/site/content/docs/get-started/installation.mdx +2 -3
  17. package/site/content/docs/get-started/introduction.mdx +58 -121
  18. package/site/content/docs/get-started/meta.json +9 -1
  19. package/site/content/docs/get-started/project-structure.mdx +4 -11
  20. package/site/content/docs/get-started/testing.mdx +328 -0
  21. package/site/content/docs/get-started/why.mdx +93 -71
  22. package/site/content/docs/index.mdx +44 -11
  23. package/site/content/docs/meta.json +8 -5
  24. package/site/content/docs/plugins/apple.mdx +151 -0
  25. package/site/content/docs/plugins/discord.mdx +139 -0
  26. package/site/content/docs/plugins/facebook.mdx +134 -0
  27. package/site/content/docs/plugins/figma.mdx +138 -0
  28. package/site/content/docs/plugins/github.mdx +138 -0
  29. package/site/content/docs/plugins/google.mdx +153 -0
  30. package/site/content/docs/plugins/index.mdx +47 -1
  31. package/site/content/docs/plugins/meta.json +10 -0
  32. package/site/content/docs/plugins/microsoft.mdx +151 -0
  33. package/site/content/docs/plugins/oauth.mdx +188 -0
  34. package/site/content/docs/plugins/x.mdx +125 -0
  35. package/site/content/docs/providers/index.mdx +2 -0
  36. package/site/content/docs/recipes/index.mdx +2 -0
  37. package/site/content/docs/reference/cli.md +3 -2
  38. package/site/content/docs/reference/client.mdx +58 -1
  39. package/site/content/docs/reference/configuration.mdx +2 -4
  40. package/site/content/docs/reference/fx.mdx +3 -1
  41. package/site/content/docs/reference/index.mdx +0 -5
  42. package/site/content/docs/reference/meta.json +2 -2
  43. package/site/content/docs/reference/okid.mdx +137 -0
  44. package/src/auth/bindings.ts +1 -1
  45. package/src/auth/config.ts +9 -0
  46. package/src/auth/identity-sql.ts +314 -0
  47. package/src/auth/identity.ts +140 -2
  48. package/src/auth/index.ts +17 -1
  49. package/src/auth/method-context.ts +3 -0
  50. package/src/auth/oauth-as/cimd.ts +132 -0
  51. package/src/auth/oauth-as/crypto.test.ts +101 -0
  52. package/src/auth/oauth-as/crypto.ts +393 -0
  53. package/src/auth/oauth-as/errors.ts +68 -0
  54. package/src/auth/oauth-as/http.test.ts +419 -0
  55. package/src/auth/oauth-as/http.ts +842 -0
  56. package/src/auth/oauth-as/stores.ts +61 -0
  57. package/src/auth/oauth-as/tables.ts +142 -0
  58. package/src/auth/tables.ts +0 -11
  59. package/src/bench/README.md +83 -0
  60. package/src/bench/REPORT.md +176 -0
  61. package/src/bench/g01-rls-stamp.bench.ts +194 -0
  62. package/src/bench/g02-clock-per-tenant.bench.ts +158 -0
  63. package/src/bench/g03-signal-once.bench.ts +157 -0
  64. package/src/bench/g03-signal-reconnect.bench.ts +254 -0
  65. package/src/bench/g03-signal-sse-memory.bench.ts +191 -0
  66. package/src/bench/g04-auth-vault-hotpath.bench.ts +170 -0
  67. package/src/bench/g05-sustained-full.bench.ts +265 -0
  68. package/src/bench/g06-mixed-load.bench.ts +260 -0
  69. package/src/bench/g07-vault-crypto.bench.ts +100 -0
  70. package/src/bench/g07-vault-rotate-under-read.bench.ts +285 -0
  71. package/src/bench/g08-conn-oversubscribe.bench.ts +194 -0
  72. package/src/bench/g08-store-kv-durable.bench.ts +133 -0
  73. package/src/bench/g08-store-sql.bench.ts +178 -0
  74. package/src/bench/g09-journal-sustained.bench.ts +203 -0
  75. package/src/bench/g10-observability-contention.bench.ts +246 -0
  76. package/src/bench/g11-cold-start-cycle.bench.ts +164 -0
  77. package/src/bench/g13-elements.bench.ts +427 -0
  78. package/src/bench/g14-graceful-shutdown.bench.ts +244 -0
  79. package/src/bench/g15-postgres-degradation.bench.ts +264 -0
  80. package/src/bench/g16-live-query-fanout.bench.ts +206 -0
  81. package/src/bench/lib/event-loop-lag.ts +26 -0
  82. package/src/bench/lib/infra.ts +60 -0
  83. package/src/bench/lib/report.ts +52 -0
  84. package/src/bench/lib/rss-sampler.ts +61 -0
  85. package/src/bench/lib/signal-pg.ts +88 -0
  86. package/src/bench/load-app.ts +337 -0
  87. package/src/bench/load-child.ts +108 -0
  88. package/src/bench/smoke.bench.ts +43 -0
  89. package/src/cli/competitor-mention-removal.test.ts +28 -0
  90. package/src/cli/doctor-fd.ts +117 -0
  91. package/src/cli/doctor.test.ts +192 -0
  92. package/src/cli/doctor.ts +129 -1
  93. package/src/client/create.ts +95 -1
  94. package/src/client/index.ts +9 -2
  95. package/src/client/transport.ts +11 -4
  96. package/src/client/use-live-query.ts +154 -0
  97. package/src/client-react/index.ts +15 -1
  98. package/src/client-react/live-resource.ts +246 -0
  99. package/src/client-react/use-live-query.test.ts +475 -0
  100. package/src/client-react/use-live-query.ts +530 -0
  101. package/src/compiler/extract.test.ts +518 -0
  102. package/src/compiler/extract.ts +386 -19
  103. package/src/console/server/invoke-user-flow.ts +2 -1
  104. package/src/console/ui-next/dist/assets/{access-page-DnWbnGzq.js → access-page-De7Lc2JC.js} +1 -1
  105. package/src/console/ui-next/dist/assets/{flows-page-BiZ4-6yQ.js → flows-page-RGy7VEA_.js} +1 -1
  106. package/src/console/ui-next/dist/assets/{index-C8NRK2R-.js → index-_rgpdVzo.js} +3 -3
  107. package/src/console/ui-next/dist/assets/{observability-page-CrB6vd1T.js → observability-page-Ds6pcnh-.js} +1 -1
  108. package/src/console/ui-next/dist/assets/{store-page-CS5-aETQ.js → store-page-02xOiqIK.js} +3 -3
  109. package/src/console/ui-next/dist/assets/{units-page-CjtdlW8l.js → units-page-4rHOePuE.js} +1 -1
  110. package/src/console/ui-next/dist/assets/{vault-page-C6Xxm9SA.js → vault-page-DISPgxLM.js} +1 -1
  111. package/src/console/ui-next/dist/index.html +1 -1
  112. package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +36 -2
  113. package/src/drivers/cdc-outbox.ts +389 -0
  114. package/src/drivers/memory.ts +20 -0
  115. package/src/drivers/oauth-apple.ts +156 -0
  116. package/src/drivers/oauth-discord.ts +79 -0
  117. package/src/drivers/oauth-facebook.ts +80 -0
  118. package/src/drivers/oauth-figma.ts +116 -0
  119. package/src/drivers/oauth-github.ts +92 -0
  120. package/src/drivers/oauth-google.ts +142 -0
  121. package/src/drivers/oauth-microsoft.ts +174 -0
  122. package/src/drivers/oauth-oidc.ts +293 -0
  123. package/src/drivers/oauth-shared.ts +326 -0
  124. package/src/drivers/oauth-types.ts +159 -0
  125. package/src/drivers/oauth-x.ts +77 -0
  126. package/src/drivers/oauth2-common.ts +95 -0
  127. package/src/drivers/oauth2-token.ts +61 -0
  128. package/src/drivers/pg-rls-row-passes.ts +251 -0
  129. package/src/drivers/pg-rls.ts +2 -0
  130. package/src/drivers/postgres.ts +45 -2
  131. package/src/drivers/signal-postgres.ts +2 -1
  132. package/src/elements/channel/runtime.ts +29 -2
  133. package/src/elements/channel.test.ts +52 -0
  134. package/src/elements/gate/boot.ts +29 -2
  135. package/src/elements/store/emit-drizzle.ts +147 -14
  136. package/src/elements/store/field-ddl.test.ts +118 -0
  137. package/src/elements/store/field-types.test.ts +455 -0
  138. package/src/elements/store/list-query.golden.json +777 -0
  139. package/src/elements/store/list-query.parity.test.ts +396 -0
  140. package/src/elements/store/list-query.ts +792 -0
  141. package/src/elements/store/live-default.test.ts +136 -0
  142. package/src/elements/store/live-http.test.ts +160 -0
  143. package/src/elements/store/live-isolation.test.ts +291 -0
  144. package/src/elements/store/live-query-runtime.test.ts +323 -0
  145. package/src/elements/store/live-query-runtime.ts +403 -0
  146. package/src/elements/store/live-query-server.test.ts +377 -0
  147. package/src/elements/store/live-query-server.ts +102 -0
  148. package/src/elements/store/live-query.ts +97 -0
  149. package/src/elements/store/resource.ts +189 -680
  150. package/src/elements/store/rls-row-passes-policies.parity.test.ts +665 -0
  151. package/src/elements/store/schema-decl.ts +539 -41
  152. package/src/elements/store/sql-rls-stamp.test.ts +27 -0
  153. package/src/elements/store/sql-session.ts +297 -35
  154. package/src/elements/store/table.ts +102 -21
  155. package/src/elements/store.test.ts +3 -1
  156. package/src/elements/store.ts +12 -1
  157. package/src/elements/vault/chaos-child.ts +74 -1
  158. package/src/elements/vault/chaos.test.ts +4 -2
  159. package/src/elements/vault/storage.ts +4 -2
  160. package/src/index.ts +5 -2
  161. package/src/kernel/app-auth.ts +1 -0
  162. package/src/kernel/app.ts +116 -2
  163. package/src/kernel/auth-sharing.test.ts +196 -0
  164. package/src/kernel/boot.test.ts +3 -3
  165. package/src/kernel/errors.ts +8 -0
  166. package/src/kernel/fx.test.ts +1 -0
  167. package/src/kernel/fx.ts +14 -2
  168. package/src/kernel/horizontal-child.ts +2 -1
  169. package/src/kernel/http-resource.ts +33 -7
  170. package/src/kernel/identity-host-persist.test.ts +119 -0
  171. package/src/kernel/instance-id.ts +4 -2
  172. package/src/kernel/journal.ts +2 -1
  173. package/src/kernel/mcp-tool.test.ts +95 -0
  174. package/src/kernel/on.ts +9 -0
  175. package/src/kernel/realtime-bind.ts +326 -0
  176. package/src/kernel/resource-live.ts +117 -0
  177. package/src/kernel/triggers.ts +86 -4
  178. package/src/manifest/diff.ts +37 -0
  179. package/src/manifest/types.ts +64 -2
  180. package/src/okid.bench.test.ts +64 -0
  181. package/src/okid.test.ts +338 -0
  182. package/src/okid.ts +245 -0
  183. package/src/plugins/anonymous.ts +19 -1
  184. package/src/plugins/auth/shared.ts +15 -0
  185. package/src/plugins/index.ts +2 -0
  186. package/src/plugins/magic-link.ts +10 -8
  187. package/src/plugins/mcp-oauth.ts +208 -0
  188. package/src/plugins/oauth/flow-store.ts +117 -0
  189. package/src/plugins/oauth/link.ts +69 -0
  190. package/src/plugins/oauth/shared.ts +108 -0
  191. package/src/plugins/oauth/token-vault.ts +100 -0
  192. package/src/plugins/oauth.security.test.ts +535 -0
  193. package/src/plugins/oauth.ts +532 -0
  194. package/src/plugins/otp.ts +48 -6
  195. package/src/plugins/passkey.ts +20 -1
  196. package/src/plugins/two-factor.ts +11 -0
  197. package/src/plugins/username.ts +40 -7
  198. package/src/release/build-lib.ts +7 -1
  199. package/src/release/measure.ts +1 -0
  200. package/src/release/official-plugins.ts +4 -1
  201. package/src/runs/collect.ts +2 -1
  202. package/src/runs/drivers/files.ts +2 -1
  203. package/src/test/create-test-app.ts +114 -5
  204. package/src/test/export-bundle.test.ts +33 -0
  205. package/src/test/live-signals.test.ts +83 -0
  206. package/src/test/tenant-isolation.test.ts +175 -0
  207. package/src/testing.ts +26 -0
  208. package/src/upgrade/codemods.ts +1 -1
  209. package/site/content/docs/reference/migrating-environments.mdx +0 -158
@@ -0,0 +1,164 @@
1
+ /**
2
+ * G11 — cold start cycle: 100 consecutive fresh `Bun.spawn(["bun","-e",probe])`
3
+ * cycles following `measureColdStartMedianMsOnce`
4
+ * (`src/release/measure.ts`) — every cycle is a brand-new Bun process, so no
5
+ * module cache is shared across cycles.
6
+ *
7
+ * Reported:
8
+ * - two windows: cycles 1–10 (cold-ish) vs 91–100 (OS-warm) medians;
9
+ * - parent-process RSS slope over all cycles (leak watch);
10
+ * - interpretation rules baked into the artifact:
11
+ * rising 91–100 slope → suspected leak (issue);
12
+ * drop across windows → expected OS warmup, not a win.
13
+ *
14
+ * Run: OKE_BENCH=1 bun test ./src/bench/g11-cold-start-cycle.bench.ts --timeout 300000
15
+ */
16
+
17
+ import { describe, expect, test } from "bun:test";
18
+ import { LIVE_PG } from "./lib/infra.ts";
19
+ import {
20
+ DISCLAIMER,
21
+ HARDWARE,
22
+ percentile,
23
+ writeArtifact,
24
+ type BenchArtifact,
25
+ } from "./lib/report.ts";
26
+
27
+ const ROOT = `${import.meta.dir}/../..`;
28
+ const CYCLES = Math.max(20, Number(process.env.OKE_BENCH_G11_CYCLES ?? "100"));
29
+
30
+ /** Identical probe body to measureColdStartMedianMsOnce (src/release/measure.ts). */
31
+ const PROBE = `
32
+ const t0 = performance.now();
33
+ const { createBunRuntime, oke, on, flow, http } = await import(${JSON.stringify(`${ROOT}/src/http.ts`)});
34
+ const { resetBindings } = await import(${JSON.stringify(`${ROOT}/src/kernel/on.ts`)});
35
+ resetBindings();
36
+ on(http.get("/ping").public(), flow("ping", { do: () => ({ ok: true }) }));
37
+ const app = oke({ name: "cold-start" });
38
+ const rt = createBunRuntime();
39
+ const server = rt.serve(app, { port: 0, hostname: "127.0.0.1" });
40
+ const ms = performance.now() - t0;
41
+ server.stop(true);
42
+ process.stdout.write(String(ms));
43
+ `;
44
+
45
+ interface CycleSample {
46
+ readonly i: number;
47
+ readonly coldMs: number;
48
+ readonly parentRssMb: number;
49
+ }
50
+
51
+ function leastSquaresSlope(xs: readonly number[], ys: readonly number[]): number {
52
+ const n = xs.length;
53
+ if (n < 2) return 0;
54
+ const sx = xs.reduce((a, b) => a + b, 0);
55
+ const sy = ys.reduce((a, b) => a + b, 0);
56
+ const sxy = xs.reduce((a, x, i) => a + x * ys[i]!, 0);
57
+ const sxx = xs.reduce((a, x) => a + x * x, 0);
58
+ return (n * sxy - sx * sy) / (n * sxx - sx * sx || 1);
59
+ }
60
+
61
+ describe.skipIf(!process.env.OKE_BENCH)("G11 — cold start cycle", () => {
62
+ test(`${CYCLES} fresh Bun.spawn cycles — two-window medians + parent RSS slope`, async () => {
63
+ const samples: CycleSample[] = [];
64
+ for (let i = 1; i <= CYCLES; i++) {
65
+ const proc = Bun.spawn(["bun", "-e", PROBE], {
66
+ cwd: ROOT,
67
+ stdout: "pipe",
68
+ stderr: "pipe",
69
+ });
70
+ const [stdout, stderr, exitCode] = await Promise.all([
71
+ new Response(proc.stdout).text(),
72
+ new Response(proc.stderr).text(),
73
+ proc.exited,
74
+ ]);
75
+ if (exitCode !== 0) throw new Error(`cycle ${i} probe failed: ${stderr || stdout}`);
76
+ const ms = Number(stdout.trim());
77
+ if (!Number.isFinite(ms) || ms <= 0) {
78
+ throw new Error(`cycle ${i} returned non-numeric: ${stdout}`);
79
+ }
80
+ samples.push({
81
+ i,
82
+ coldMs: ms,
83
+ parentRssMb: process.memoryUsage.rss() / 1024 / 1024,
84
+ });
85
+ if (i % 25 === 0) console.log(`[G11] cycle ${i}/${CYCLES}: ${ms.toFixed(1)}ms`);
86
+ }
87
+
88
+ const all = samples.map((s) => s.coldMs);
89
+ const w1 = samples.slice(0, 10).map((s) => s.coldMs);
90
+ const w2 = samples.slice(-10).map((s) => s.coldMs);
91
+
92
+ // Slopes: ms per cycle within the last window, and MB per cycle for RSS.
93
+ const tail = samples.slice(-10);
94
+ const tailCycleSlopePerCycle = leastSquaresSlope(
95
+ tail.map((s) => s.i),
96
+ tail.map((s) => s.coldMs),
97
+ );
98
+ const rssSlopePerCycleAll = leastSquaresSlope(
99
+ samples.map((s) => s.i),
100
+ samples.map((s) => s.parentRssMb),
101
+ );
102
+
103
+ const metrics: Record<string, number> = {
104
+ cycles: CYCLES,
105
+ window1_p50Ms: Number(percentile(w1, 50).toFixed(3)),
106
+ window1_p99Ms: Number(percentile(w1, 99).toFixed(3)),
107
+ window2_p50Ms: Number(percentile(w2, 50).toFixed(3)),
108
+ window2_p99Ms: Number(percentile(w2, 99).toFixed(3)),
109
+ overallP50Ms: Number(percentile(all, 50).toFixed(3)),
110
+ overallP99Ms: Number(percentile(all, 99).toFixed(3)),
111
+ overallMinMs: Number(Math.min(...all).toFixed(3)),
112
+ overallMaxMs: Number(Math.max(...all).toFixed(3)),
113
+ window2MinusWindow1P50Ms: Number((percentile(w2, 50) - percentile(w1, 50)).toFixed(3)),
114
+ window2CycleSlopeMsPerCycle: Number(tailCycleSlopePerCycle.toFixed(4)),
115
+ parentRssStartMb: Number(samples[0]!.parentRssMb.toFixed(1)),
116
+ parentRssEndMb: Number(samples.at(-1)!.parentRssMb.toFixed(1)),
117
+ parentRssSlopeMbPerCycle: Number(rssSlopePerCycleAll.toFixed(4)),
118
+ parentRssGrowthMb: Number((samples.at(-1)!.parentRssMb - samples[0]!.parentRssMb).toFixed(1)),
119
+ };
120
+
121
+ // Interpretation rules baked into the artifact.
122
+ const issues: string[] = [];
123
+ const notes: string[] = [];
124
+ if (metrics.window2CycleSlopeMsPerCycle! > 2 && percentile(w2, 50) > percentile(w1, 50)) {
125
+ issues.push(
126
+ `suspected leak: cycles 91-100 trend rising at ${metrics.window2CycleSlopeMsPerCycle}ms/cycle ` +
127
+ `and warm window p50 (${metrics.window2_p50Ms}ms) exceeds cold window p50 ` +
128
+ `(${metrics.window1_p50Ms}ms)`,
129
+ );
130
+ }
131
+ if (metrics.parentRssSlopeMbPerCycle! > 0.5) {
132
+ issues.push(
133
+ `parent RSS climbing at ${metrics.parentRssSlopeMbPerCycle}MB/cycle ` +
134
+ `(+${metrics.parentRssGrowthMb}MB over run)`,
135
+ );
136
+ }
137
+ if (issues.length === 0 && metrics.window2MinusWindow1P50Ms! < 0) {
138
+ notes.push(
139
+ "warm-window p50 below cold-window p50 — expected OS/FS cache warmup across 100 spawns, not a real improvement",
140
+ );
141
+ }
142
+
143
+ const artifact: BenchArtifact = {
144
+ group: "G11-cold-start-cycle",
145
+ hardware: HARDWARE,
146
+ disclaimer: DISCLAIMER,
147
+ command: "OKE_BENCH=1 bun test ./src/bench/g11-cold-start-cycle.bench.ts --timeout 300000",
148
+ metrics,
149
+ issues: [...notes.map((n) => `note: ${n}`), ...issues],
150
+ fixes: [],
151
+ remeasured: null,
152
+ };
153
+ console.log("[G11] metrics:", JSON.stringify(metrics));
154
+ if (artifact.issues.length > 0) console.log("[G11] issues:", artifact.issues);
155
+ const path = await writeArtifact(artifact);
156
+ console.log(`[G11] artifact: ${path}`);
157
+
158
+ expect(CYCLES).toBeGreaterThanOrEqual(100);
159
+ expect(metrics.overallP50Ms!).toBeGreaterThan(0);
160
+ }, 300_000);
161
+ });
162
+
163
+ // G11 is pure subprocess work — live infra import kept only for gate symmetry.
164
+ void LIVE_PG;
@@ -0,0 +1,427 @@
1
+ /**
2
+ * G13 — remaining elements (one file, four sub-benchmarks):
3
+ *
4
+ * a. Gate.rate `takeRate` at high RPS against live Redis (EVAL Lua).
5
+ * b. Cumulative `gated()` cost — capability assert + effect ledger per call
6
+ * through the stub-store path, swept 1k → 10k → 50k ops.
7
+ * c. Concurrent `fx.json.stream` through `encodeSseStream` (mock AI driver).
8
+ * d. Channel bulk send via RetryTransport → Mailpit SMTP
9
+ * (skips loudly when Mailpit is absent).
10
+ *
11
+ * Run: OKE_BENCH=1 bun test ./src/bench/g13-elements.bench.ts --timeout 300000
12
+ */
13
+
14
+ import { afterAll, describe, expect, test } from "bun:test";
15
+ import { openSmtpChannel } from "../drivers/channel-smtp.ts";
16
+ import { openRedisKv } from "../drivers/redis.ts";
17
+ import type { KvNamespace } from "../drivers/types.ts";
18
+ import { channel } from "../elements/channel.ts";
19
+ import { createChannelRuntime, type TemplateCatalog } from "../elements/channel/runtime.ts";
20
+ import { takeRate } from "../elements/gate/strategies.ts";
21
+ import { ai } from "../elements/ai.ts";
22
+ import type { AiDriver } from "../drivers/ai-types.ts";
23
+ import { oke } from "../kernel/app.ts";
24
+ import { flow, resetFlowSeq, type AnyFlowDef } from "../kernel/flow.ts";
25
+ import { on, resetBindings, type Binding } from "../kernel/on.ts";
26
+ import { http } from "../kernel/triggers.ts";
27
+ import { LIVE_REDIS } from "./lib/infra.ts";
28
+ import { DISCLAIMER, HARDWARE, percentile, writeArtifact } from "./lib/report.ts";
29
+
30
+ const CAL = process.env.OKE_BENCH_CAL === "1";
31
+
32
+ // --- Mailpit probe ------------------------------------------------------------
33
+ const MAILPIT_HOST = process.env.OKE_BENCH_MAILPIT_HOST?.trim() || "127.0.0.1";
34
+ const MAILPIT_SMTP_PORT = Number(process.env.OKE_BENCH_MAILPIT_PORT ?? "20850");
35
+ const MAILPIT_UI = process.env.OKE_BENCH_MAILPIT_UI?.trim() || "http://127.0.0.1:21850";
36
+
37
+ async function probeMailpit(): Promise<boolean> {
38
+ // Bun.connect needs a socket handler; the Mailpit UI is the reliable
39
+ // liveness probe (SMTP + UI ship together).
40
+ try {
41
+ const res = await fetch(`${MAILPIT_UI}/api/v1/messages?limit=1`, {
42
+ signal: AbortSignal.timeout(2_000),
43
+ });
44
+ return res.ok;
45
+ } catch {
46
+ return false;
47
+ }
48
+ }
49
+
50
+ describe.skipIf(!process.env.OKE_BENCH)("G13 — remaining elements", () => {
51
+ // ---------------------------------------------------------------- a. takeRate
52
+ const HAVE_REDIS = Boolean(LIVE_REDIS);
53
+ test.skipIf(!HAVE_REDIS)(
54
+ "gate takeRate at high RPS (live redis EVAL)",
55
+ async () => {
56
+ const kv: KvNamespace & { eval?: unknown } = await openRedisKv({
57
+ name: "bench-g13-gate",
58
+ url: LIVE_REDIS!,
59
+ });
60
+ // Deterministic window so takes never fight refill noise.
61
+ const nowMs = 1_700_000_000_000;
62
+ const subjects = Array.from({ length: CAL ? 5 : 50 }, (_, i) => `s${i}`);
63
+ const TAKES = CAL ? 500 : 10_000;
64
+ const takesPerSubject = TAKES / subjects.length;
65
+ // Half the per-subject takes fit the bucket — exercises allow AND deny.
66
+ const MAX = Math.max(1, Math.floor(takesPerSubject / 2));
67
+
68
+ const latencies: number[] = [];
69
+ let allowed = 0;
70
+ const t0 = performance.now();
71
+ await Promise.all(
72
+ subjects.map(async (subject) => {
73
+ for (let i = 0; i < takesPerSubject; i++) {
74
+ const s = performance.now();
75
+ const r = await takeRate(kv, {
76
+ strategy: "token-bucket",
77
+ max: MAX,
78
+ windowMs: 60_000,
79
+ subject: `g13:${subject}`,
80
+ nowMs,
81
+ });
82
+ latencies.push(performance.now() - s);
83
+ if (r.allowed) allowed += 1;
84
+ }
85
+ }),
86
+ );
87
+ const wallS = (performance.now() - t0) / 1000;
88
+ const metrics: Record<string, number> = {
89
+ takes: latencies.length,
90
+ takesPerSec: Number((latencies.length / wallS).toFixed(1)),
91
+ p50Ms: Number(percentile(latencies, 50).toFixed(3)),
92
+ p99Ms: Number(percentile(latencies, 99).toFixed(3)),
93
+ bucketMax: MAX,
94
+ allowedWithinBucket: allowed,
95
+ deniedAfterBucketExhausted: latencies.length - allowed,
96
+ };
97
+ console.log("[G13a] metrics:", JSON.stringify(metrics));
98
+
99
+ // Correctness: deterministic clock + max per subject → exactly MAX
100
+ // allows per subject, everything after denied.
101
+ expect(allowed).toBe(MAX * subjects.length);
102
+
103
+ const issues: string[] = [];
104
+ if (metrics.takesPerSec! < 200)
105
+ issues.push(`takeRate throughput low: ${metrics.takesPerSec}/s`);
106
+
107
+ const path = await writeArtifact({
108
+ group: "G13a",
109
+ hardware: HARDWARE,
110
+ disclaimer: DISCLAIMER,
111
+ command:
112
+ "OKE_BENCH=1 OKE_TEST_REDIS_URL=$OKE_TEST_REDIS_URL bun test ./src/bench/g13-elements.bench.ts --timeout 300000",
113
+ metrics,
114
+ issues,
115
+ fixes: [],
116
+ remeasured: null,
117
+ });
118
+ console.log(`[G13a] artifact: ${path}`);
119
+ expect(issues.length).toBe(0);
120
+ },
121
+ CAL ? 60_000 : 180_000,
122
+ );
123
+
124
+ // ------------------------------------------------------------- b. gated() cost
125
+ test(
126
+ "gated() cumulative cost — 1k/10k/50k capability+ledger calls",
127
+ async () => {
128
+ resetBindings();
129
+ resetFlowSeq();
130
+
131
+ let reportedUs = 0;
132
+ // Every stub-store op routes through gated("read"/"write"):
133
+ // capability.assert + effect ledger per call.
134
+ const gatedBench = flow("g13.gated", {
135
+ do: async (input, fx) => {
136
+ const n = Math.max(1, Math.min(100_000, Number((input as { n?: number }).n ?? 1)));
137
+ const store = fx.store(`kv:g13cost` as never) as unknown as {
138
+ set(key: string, value: number): Promise<void>;
139
+ get(key: string): Promise<unknown>;
140
+ };
141
+ const t0 = performance.now();
142
+ for (let i = 0; i < n; i++) {
143
+ await store.set(`k${i}`, i);
144
+ }
145
+ for (let i = 0; i < n; i++) {
146
+ await store.get(`k${i}`);
147
+ }
148
+ reportedUs = Number(((performance.now() - t0) * 1000).toFixed(3));
149
+ return { us: reportedUs, n };
150
+ },
151
+ });
152
+
153
+ const bindings: Binding[] = [
154
+ { trigger: http.post("/g13/gated").public(), flow: gatedBench as AnyFlowDef },
155
+ ];
156
+ const app13b = oke({
157
+ name: "g13-gated-cost",
158
+ env: "test",
159
+ startScheduler: false,
160
+ gate: { unguardedHttp: "allow" },
161
+ bindings,
162
+ });
163
+
164
+ const metrics: Record<string, number> = {};
165
+ for (const n of CAL ? [1_000] : [1_000, 10_000, 50_000]) {
166
+ const res = await app13b.fetch(
167
+ new Request("http://localhost/g13/gated", {
168
+ method: "POST",
169
+ body: JSON.stringify({ n }),
170
+ headers: { "content-type": "application/json" },
171
+ }),
172
+ );
173
+ if (!res.ok) throw new Error(`g13 gated HTTP ${res.status}`);
174
+ const wallUs = reportedUs;
175
+ const perOpUs = Number((wallUs / (2 * n)).toFixed(4));
176
+ metrics[`n${n}.wallMs`] = Number((wallUs / 1000).toFixed(3));
177
+ metrics[`n${n}.perOpUs`] = perOpUs;
178
+ console.log(`[G13b] n=${n}: ${metrics[`n${n}.wallMs`]}ms total, ${perOpUs}µs/gated-op`);
179
+ }
180
+ console.log("[G13b] metrics:", JSON.stringify(metrics));
181
+
182
+ const issues: string[] = [];
183
+ if ((metrics["n50000.perOpUs"] ?? 0) > (metrics["n1000.perOpUs"] ?? 0) * 5) {
184
+ issues.push("gated() per-op cost grows super-linearly with cumulative calls");
185
+ }
186
+
187
+ const path = await writeArtifact({
188
+ group: "G13b",
189
+ hardware: HARDWARE,
190
+ disclaimer: DISCLAIMER,
191
+ command: "OKE_BENCH=1 bun test ./src/bench/g13-elements.bench.ts --timeout 300000",
192
+ metrics,
193
+ issues,
194
+ fixes: [],
195
+ remeasured: null,
196
+ });
197
+ console.log(`[G13b] artifact: ${path}`);
198
+ expect(issues.length).toBe(0);
199
+ },
200
+ CAL ? 60_000 : 240_000,
201
+ );
202
+
203
+ // --------------------------------------------------------- c. fx.json.stream
204
+ test(
205
+ "concurrent fx.json.stream via encodeSseStream",
206
+ async () => {
207
+ resetBindings();
208
+ resetFlowSeq();
209
+ const CHUNKS = CAL ? 8 : 32;
210
+
211
+ function tokenDriver(chunks: number): AiDriver {
212
+ return {
213
+ id: "mock",
214
+ async open() {
215
+ return {
216
+ driverId: "mock" as const,
217
+ model: "smart",
218
+ async complete() {
219
+ return { text: "", model: "smart", driverId: "mock" as const };
220
+ },
221
+ async *stream() {
222
+ for (let i = 0; i < chunks; i++) yield { text: `t${i}` };
223
+ },
224
+ };
225
+ },
226
+ };
227
+ }
228
+
229
+ const smart = ai.model("smart");
230
+ on(
231
+ http.get("/chat").public(),
232
+ flow("g13.chat", {
233
+ do: (_input, fx) => fx.json.stream(fx.stream(smart, { prompt: "hi" })),
234
+ }),
235
+ );
236
+ const app13c = oke({
237
+ name: "g13-stream",
238
+ env: "test",
239
+ startScheduler: false,
240
+ gate: { unguardedHttp: "allow" },
241
+ ai: { models: [smart], defaultDriver: tokenDriver(CHUNKS) },
242
+ });
243
+
244
+ const STREAMS = CAL ? 20 : 200;
245
+ const CONCURRENCY = CAL ? 5 : 25;
246
+ const latencies: number[] = [];
247
+ let framesTotal = 0;
248
+ let nextId = 0;
249
+
250
+ const t0 = performance.now();
251
+ await Promise.all(
252
+ Array.from({ length: CONCURRENCY }, async () => {
253
+ for (;;) {
254
+ const id = ++nextId;
255
+ if (id > STREAMS) return;
256
+ const s = performance.now();
257
+ const res = await app13c.fetch(new Request("http://localhost/chat"));
258
+ if (!res.headers.get("content-type")?.includes("text/event-stream")) {
259
+ throw new Error("g13 stream: not SSE");
260
+ }
261
+ const body = await res.text();
262
+ framesTotal += body.split("\n").filter((l) => l.startsWith("data:")).length;
263
+ latencies.push(performance.now() - s);
264
+ }
265
+ }),
266
+ );
267
+ const wallS = (performance.now() - t0) / 1000;
268
+ const metrics: Record<string, number> = {
269
+ streams: latencies.length,
270
+ streamsPerSec: Number((latencies.length / wallS).toFixed(1)),
271
+ sseFramesPerSec: Number((framesTotal / wallS).toFixed(1)),
272
+ framesPerStream: CHUNKS + 1,
273
+ streamP50Ms: Number(percentile(latencies, 50).toFixed(3)),
274
+ streamP99Ms: Number(percentile(latencies, 99).toFixed(3)),
275
+ concurrency: CONCURRENCY,
276
+ };
277
+ console.log("[G13c] metrics:", JSON.stringify(metrics));
278
+
279
+ const issues: string[] = [];
280
+ if (!latencies.every((ms) => ms < 30_000)) issues.push("stream latency outlier > 30s");
281
+
282
+ const path = await writeArtifact({
283
+ group: "G13c",
284
+ hardware: HARDWARE,
285
+ disclaimer: DISCLAIMER,
286
+ command: "OKE_BENCH=1 bun test ./src/bench/g13-elements.bench.ts --timeout 300000",
287
+ metrics,
288
+ issues,
289
+ fixes: [],
290
+ remeasured: null,
291
+ });
292
+ console.log(`[G13c] artifact: ${path}`);
293
+ expect(latencies.length).toBe(STREAMS);
294
+ },
295
+ CAL ? 60_000 : 180_000,
296
+ );
297
+
298
+ // ------------------------------------------------------- d. Channel bulk send
299
+ test(
300
+ "channel bulk send via RetryTransport → Mailpit",
301
+ async () => {
302
+ const up = await probeMailpit();
303
+ if (!up) {
304
+ console.warn(
305
+ `[G13d] SKIPPED LOUDLY: no Mailpit SMTP on ${MAILPIT_HOST}:${MAILPIT_SMTP_PORT}`,
306
+ );
307
+ return;
308
+ }
309
+
310
+ let totalBefore = -1;
311
+ try {
312
+ const r = await fetch(`${MAILPIT_UI}/api/v1/messages?limit=1`);
313
+ totalBefore = ((await r.json()) as { total: number }).total;
314
+ } catch {
315
+ /* UI count is advisory */
316
+ }
317
+
318
+ const WELCOME = channel.email().template("g13-bench-welcome");
319
+ const catalog: TemplateCatalog = {
320
+ "g13-bench-welcome": { en: { subject: "G13 bench", text: "Hello {{i}}" } },
321
+ };
322
+ const rt = createChannelRuntime({
323
+ templates: [WELCOME],
324
+ drivers: [openSmtpChannel({ host: MAILPIT_HOST, port: MAILPIT_SMTP_PORT })],
325
+ catalog,
326
+ retry: true, // wraps transports in sently RetryTransport
327
+ now: () => Date.now(),
328
+ });
329
+
330
+ const SENDS = CAL ? 20 : 150;
331
+ const CONCURRENCY = CAL ? 5 : 15;
332
+ const latencies: number[] = [];
333
+ let sent = 0;
334
+ let failed = 0;
335
+ let idx = 0;
336
+
337
+ const t0 = performance.now();
338
+ await Promise.all(
339
+ Array.from({ length: CONCURRENCY }, async () => {
340
+ for (;;) {
341
+ const i = ++idx;
342
+ if (i > SENDS) return;
343
+ const s = performance.now();
344
+ try {
345
+ const r = await rt.send(WELCOME.name, {
346
+ to: `g13-bench-${i}@example.test`,
347
+ data: { i },
348
+ });
349
+ if (r.ok) sent++;
350
+ else failed++;
351
+ } catch {
352
+ failed++;
353
+ }
354
+ latencies.push(performance.now() - s);
355
+ }
356
+ }),
357
+ );
358
+ const wallS = (performance.now() - t0) / 1000;
359
+
360
+ let totalAfter = -1;
361
+ try {
362
+ const r = await fetch(`${MAILPIT_UI}/api/v1/messages?limit=1`);
363
+ totalAfter = ((await r.json()) as { total: number }).total;
364
+ } catch {
365
+ /* advisory */
366
+ }
367
+
368
+ const metrics: Record<string, number> = {
369
+ sends: SENDS,
370
+ sendsOk: sent,
371
+ sendsFailed: failed,
372
+ sendsPerSec: Number((latencies.length / wallS).toFixed(2)),
373
+ p50Ms: Number(percentile(latencies, 50).toFixed(3)),
374
+ p99Ms: Number(percentile(latencies, 99).toFixed(3)),
375
+ mailpitTotalBefore: totalBefore,
376
+ mailpitTotalAfter: totalAfter,
377
+ mailpitDelta: totalAfter >= 0 && totalBefore >= 0 ? totalAfter - totalBefore : -1,
378
+ };
379
+ console.log("[G13d] metrics:", JSON.stringify(metrics));
380
+
381
+ const issues: string[] = [
382
+ "calibration run 1 (pre-fix): 20/20 sends failed — channel runtime default sender " +
383
+ "'oke@localhost' is dotless and rejected by sently SMTP address validation, so every " +
384
+ "out-of-the-box email burned a ~3s RetryTransport cycle then failed against Mailpit",
385
+ "calibration run 2 (post default-from fix): concurrent sends through ONE shared transport " +
386
+ "interleave SMTP wire data — 'Unexpected SMTP response for DATA end', all sends failed after retries",
387
+ ...(failed > 0 ? [`channel sends still failing through RetryTransport: ${failed}`] : []),
388
+ ];
389
+
390
+ const path = await writeArtifact({
391
+ group: "G13d",
392
+ hardware: HARDWARE,
393
+ disclaimer: DISCLAIMER,
394
+ command: "OKE_BENCH=1 bun test ./src/bench/g13-elements.bench.ts --timeout 300000",
395
+ metrics,
396
+ issues,
397
+ fixes: [
398
+ "channel: default sender 'oke@localhost' → 'oke@localhost.test' in src/elements/channel/runtime.ts — " +
399
+ "sently validates the From header (dotless domains rejected), so the old default could never deliver. " +
400
+ "Regression test added: channel.test.ts 'default sender passes SMTP address validation'",
401
+ "channel: per-transport send serialization in createChannelRuntime — sently transports are not safe " +
402
+ "for concurrent send() on one shared socket; concurrent flows now queue instead of corrupting the " +
403
+ "connection. Regression test added: channel.test.ts 'concurrent sends serialize per transport'. " +
404
+ "Note: bulk email throughput is now bounded by one serialized wire conversation per transport instance",
405
+ ],
406
+ remeasured: {
407
+ // Post-fix full run (this run).
408
+ metrics: {
409
+ sendsOk: metrics.sendsOk!,
410
+ sendsFailed: metrics.sendsFailed!,
411
+ sendsPerSec: metrics.sendsPerSec!,
412
+ mailpitDelta: metrics.mailpitDelta!,
413
+ },
414
+ rerunScope: "full send sweep from t=0 after default-from + serialization fixes",
415
+ },
416
+ });
417
+ console.log(`[G13d] artifact: ${path}`);
418
+ expect(failed).toBe(0);
419
+ },
420
+ CAL ? 60_000 : 180_000,
421
+ );
422
+ });
423
+
424
+ afterAll(() => {
425
+ resetBindings();
426
+ resetFlowSeq();
427
+ });