okengine 0.17.2 → 0.18.3

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 (197) hide show
  1. package/README.md +9 -5
  2. package/manifest.v1.schema.json +61 -2
  3. package/package.json +18 -4
  4. package/site/content/docs/elements/clock.mdx +1 -1
  5. package/site/content/docs/elements/flow.mdx +25 -1
  6. package/site/content/docs/elements/store.mdx +287 -341
  7. package/site/content/docs/elements/vault.mdx +5 -5
  8. package/site/content/docs/get-started/installation.mdx +1 -2
  9. package/site/content/docs/get-started/introduction.mdx +54 -110
  10. package/site/content/docs/get-started/meta.json +9 -1
  11. package/site/content/docs/get-started/testing.mdx +328 -0
  12. package/site/content/docs/get-started/why.mdx +94 -70
  13. package/site/content/docs/index.mdx +1 -1
  14. package/site/content/docs/plugins/apple.mdx +151 -0
  15. package/site/content/docs/plugins/discord.mdx +139 -0
  16. package/site/content/docs/plugins/facebook.mdx +134 -0
  17. package/site/content/docs/plugins/figma.mdx +138 -0
  18. package/site/content/docs/plugins/github.mdx +138 -0
  19. package/site/content/docs/plugins/google.mdx +153 -0
  20. package/site/content/docs/plugins/index.mdx +47 -1
  21. package/site/content/docs/plugins/meta.json +10 -0
  22. package/site/content/docs/plugins/microsoft.mdx +151 -0
  23. package/site/content/docs/plugins/oauth.mdx +188 -0
  24. package/site/content/docs/plugins/x.mdx +125 -0
  25. package/site/content/docs/reference/cli.md +3 -2
  26. package/site/content/docs/reference/client.mdx +58 -1
  27. package/site/content/docs/reference/configuration.mdx +2 -4
  28. package/site/content/docs/reference/fx.mdx +3 -1
  29. package/site/content/docs/reference/index.mdx +0 -5
  30. package/site/content/docs/reference/meta.json +2 -2
  31. package/site/content/docs/reference/okid.mdx +137 -0
  32. package/src/auth/bindings.ts +1 -1
  33. package/src/auth/config.ts +9 -0
  34. package/src/auth/identity-sql.ts +314 -0
  35. package/src/auth/identity.ts +140 -2
  36. package/src/auth/index.ts +17 -1
  37. package/src/auth/method-context.ts +3 -0
  38. package/src/auth/oauth-as/cimd.ts +132 -0
  39. package/src/auth/oauth-as/crypto.test.ts +101 -0
  40. package/src/auth/oauth-as/crypto.ts +393 -0
  41. package/src/auth/oauth-as/errors.ts +68 -0
  42. package/src/auth/oauth-as/http.test.ts +419 -0
  43. package/src/auth/oauth-as/http.ts +842 -0
  44. package/src/auth/oauth-as/stores.ts +61 -0
  45. package/src/auth/oauth-as/tables.ts +142 -0
  46. package/src/auth/tables.ts +0 -11
  47. package/src/bench/README.md +83 -0
  48. package/src/bench/REPORT.md +176 -0
  49. package/src/bench/g01-rls-stamp.bench.ts +194 -0
  50. package/src/bench/g02-clock-per-tenant.bench.ts +158 -0
  51. package/src/bench/g03-signal-once.bench.ts +157 -0
  52. package/src/bench/g03-signal-reconnect.bench.ts +254 -0
  53. package/src/bench/g03-signal-sse-memory.bench.ts +191 -0
  54. package/src/bench/g04-auth-vault-hotpath.bench.ts +170 -0
  55. package/src/bench/g05-sustained-full.bench.ts +265 -0
  56. package/src/bench/g06-mixed-load.bench.ts +260 -0
  57. package/src/bench/g07-vault-crypto.bench.ts +100 -0
  58. package/src/bench/g07-vault-rotate-under-read.bench.ts +285 -0
  59. package/src/bench/g08-conn-oversubscribe.bench.ts +194 -0
  60. package/src/bench/g08-store-kv-durable.bench.ts +133 -0
  61. package/src/bench/g08-store-sql.bench.ts +178 -0
  62. package/src/bench/g09-journal-sustained.bench.ts +203 -0
  63. package/src/bench/g10-observability-contention.bench.ts +246 -0
  64. package/src/bench/g11-cold-start-cycle.bench.ts +164 -0
  65. package/src/bench/g13-elements.bench.ts +427 -0
  66. package/src/bench/g14-graceful-shutdown.bench.ts +244 -0
  67. package/src/bench/g15-postgres-degradation.bench.ts +264 -0
  68. package/src/bench/g16-live-query-fanout.bench.ts +206 -0
  69. package/src/bench/lib/event-loop-lag.ts +26 -0
  70. package/src/bench/lib/infra.ts +60 -0
  71. package/src/bench/lib/report.ts +52 -0
  72. package/src/bench/lib/rss-sampler.ts +61 -0
  73. package/src/bench/lib/signal-pg.ts +88 -0
  74. package/src/bench/load-app.ts +337 -0
  75. package/src/bench/load-child.ts +108 -0
  76. package/src/bench/smoke.bench.ts +43 -0
  77. package/src/cli/competitor-mention-removal.test.ts +28 -0
  78. package/src/cli/doctor-fd.ts +117 -0
  79. package/src/cli/doctor.test.ts +192 -0
  80. package/src/cli/doctor.ts +129 -1
  81. package/src/client/create.ts +95 -1
  82. package/src/client/index.ts +9 -2
  83. package/src/client/transport.ts +11 -4
  84. package/src/client/use-live-query.ts +154 -0
  85. package/src/client-react/index.ts +15 -1
  86. package/src/client-react/live-resource.ts +246 -0
  87. package/src/client-react/use-live-query.test.ts +475 -0
  88. package/src/client-react/use-live-query.ts +530 -0
  89. package/src/compiler/extract.test.ts +518 -0
  90. package/src/compiler/extract.ts +386 -19
  91. package/src/console/server/invoke-user-flow.ts +2 -1
  92. package/src/console/ui-next/dist/assets/{access-page-DnWbnGzq.js → access-page-DY4N6nnk.js} +1 -1
  93. package/src/console/ui-next/dist/assets/{flows-page-BiZ4-6yQ.js → flows-page-CsPDMrVM.js} +1 -1
  94. package/src/console/ui-next/dist/assets/{index-C8NRK2R-.js → index-CcTDXHuz.js} +3 -3
  95. package/src/console/ui-next/dist/assets/{observability-page-CrB6vd1T.js → observability-page-CKR595wP.js} +1 -1
  96. package/src/console/ui-next/dist/assets/{store-page-CS5-aETQ.js → store-page-02xOiqIK.js} +3 -3
  97. package/src/console/ui-next/dist/assets/{units-page-CjtdlW8l.js → units-page-CpPFFKyE.js} +1 -1
  98. package/src/console/ui-next/dist/assets/{vault-page-C6Xxm9SA.js → vault-page-BsMf-9_W.js} +1 -1
  99. package/src/console/ui-next/dist/index.html +1 -1
  100. package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +36 -2
  101. package/src/drivers/cdc-outbox.ts +389 -0
  102. package/src/drivers/memory.ts +20 -0
  103. package/src/drivers/oauth-apple.ts +156 -0
  104. package/src/drivers/oauth-discord.ts +79 -0
  105. package/src/drivers/oauth-facebook.ts +80 -0
  106. package/src/drivers/oauth-figma.ts +116 -0
  107. package/src/drivers/oauth-github.ts +92 -0
  108. package/src/drivers/oauth-google.ts +142 -0
  109. package/src/drivers/oauth-microsoft.ts +174 -0
  110. package/src/drivers/oauth-oidc.ts +293 -0
  111. package/src/drivers/oauth-shared.ts +326 -0
  112. package/src/drivers/oauth-types.ts +159 -0
  113. package/src/drivers/oauth-x.ts +77 -0
  114. package/src/drivers/oauth2-common.ts +95 -0
  115. package/src/drivers/oauth2-token.ts +61 -0
  116. package/src/drivers/pg-rls-row-passes.ts +251 -0
  117. package/src/drivers/pg-rls.ts +2 -0
  118. package/src/drivers/postgres.ts +45 -2
  119. package/src/drivers/signal-postgres.ts +2 -1
  120. package/src/elements/channel/runtime.ts +29 -2
  121. package/src/elements/channel.test.ts +52 -0
  122. package/src/elements/gate/boot.ts +29 -2
  123. package/src/elements/store/emit-drizzle.ts +147 -14
  124. package/src/elements/store/field-ddl.test.ts +118 -0
  125. package/src/elements/store/field-types.test.ts +455 -0
  126. package/src/elements/store/list-query.golden.json +777 -0
  127. package/src/elements/store/list-query.parity.test.ts +396 -0
  128. package/src/elements/store/list-query.ts +792 -0
  129. package/src/elements/store/live-default.test.ts +136 -0
  130. package/src/elements/store/live-http.test.ts +160 -0
  131. package/src/elements/store/live-isolation.test.ts +291 -0
  132. package/src/elements/store/live-query-runtime.test.ts +323 -0
  133. package/src/elements/store/live-query-runtime.ts +403 -0
  134. package/src/elements/store/live-query-server.test.ts +377 -0
  135. package/src/elements/store/live-query-server.ts +102 -0
  136. package/src/elements/store/live-query.ts +97 -0
  137. package/src/elements/store/resource.ts +189 -680
  138. package/src/elements/store/rls-row-passes-policies.parity.test.ts +665 -0
  139. package/src/elements/store/schema-decl.ts +539 -41
  140. package/src/elements/store/sql-rls-stamp.test.ts +27 -0
  141. package/src/elements/store/sql-session.ts +297 -35
  142. package/src/elements/store/table.ts +102 -21
  143. package/src/elements/store.test.ts +3 -1
  144. package/src/elements/store.ts +12 -1
  145. package/src/elements/vault/chaos-child.ts +74 -1
  146. package/src/elements/vault/chaos.test.ts +4 -2
  147. package/src/elements/vault/storage.ts +4 -2
  148. package/src/index.ts +4 -1
  149. package/src/kernel/app-auth.ts +1 -0
  150. package/src/kernel/app.ts +116 -2
  151. package/src/kernel/auth-sharing.test.ts +196 -0
  152. package/src/kernel/boot.test.ts +3 -3
  153. package/src/kernel/errors.ts +8 -0
  154. package/src/kernel/fx.test.ts +1 -0
  155. package/src/kernel/fx.ts +14 -2
  156. package/src/kernel/horizontal-child.ts +2 -1
  157. package/src/kernel/http-resource.ts +33 -7
  158. package/src/kernel/identity-host-persist.test.ts +119 -0
  159. package/src/kernel/instance-id.ts +4 -2
  160. package/src/kernel/journal.ts +2 -1
  161. package/src/kernel/mcp-tool.test.ts +95 -0
  162. package/src/kernel/on.ts +9 -0
  163. package/src/kernel/realtime-bind.ts +326 -0
  164. package/src/kernel/resource-live.ts +117 -0
  165. package/src/kernel/triggers.ts +86 -4
  166. package/src/manifest/diff.ts +37 -0
  167. package/src/manifest/types.ts +64 -2
  168. package/src/okid.bench.test.ts +64 -0
  169. package/src/okid.test.ts +338 -0
  170. package/src/okid.ts +245 -0
  171. package/src/plugins/anonymous.ts +19 -1
  172. package/src/plugins/auth/shared.ts +15 -0
  173. package/src/plugins/index.ts +2 -0
  174. package/src/plugins/magic-link.ts +10 -8
  175. package/src/plugins/mcp-oauth.ts +208 -0
  176. package/src/plugins/oauth/flow-store.ts +117 -0
  177. package/src/plugins/oauth/link.ts +69 -0
  178. package/src/plugins/oauth/shared.ts +108 -0
  179. package/src/plugins/oauth/token-vault.ts +100 -0
  180. package/src/plugins/oauth.security.test.ts +535 -0
  181. package/src/plugins/oauth.ts +532 -0
  182. package/src/plugins/otp.ts +48 -6
  183. package/src/plugins/passkey.ts +20 -1
  184. package/src/plugins/two-factor.ts +11 -0
  185. package/src/plugins/username.ts +40 -7
  186. package/src/release/build-lib.ts +7 -1
  187. package/src/release/measure.ts +1 -0
  188. package/src/release/official-plugins.ts +4 -1
  189. package/src/runs/collect.ts +2 -1
  190. package/src/runs/drivers/files.ts +2 -1
  191. package/src/test/create-test-app.ts +114 -5
  192. package/src/test/export-bundle.test.ts +33 -0
  193. package/src/test/live-signals.test.ts +83 -0
  194. package/src/test/tenant-isolation.test.ts +175 -0
  195. package/src/testing.ts +26 -0
  196. package/src/upgrade/codemods.ts +1 -1
  197. package/site/content/docs/reference/migrating-environments.mdx +0 -158
@@ -0,0 +1,265 @@
1
+ /**
2
+ * G5 — sustained full-app baseline + memory (≥5 min).
3
+ *
4
+ * Boots `load-child serve`, then drives fixed concurrency 50 closed-loop:
5
+ * 30 workers on `/ping`, 20 workers round-robining mixed element endpoints
6
+ * (vault read, json stream, rate gate, store insert). RSS/fd sampled every
7
+ * 10 s (`lib/rss-sampler.ts`). Duration `OKE_BENCH_DURATION_S` (default 300);
8
+ * auto-extends to 600 s ONLY if the trailing RSS slope is still rising.
9
+ * p99 spikes are annotated as likely-JSC-GC unless correlated with errors.
10
+ *
11
+ * Leak protocol: if a leak appears mid-run, STOP, fix, restart from t=0 —
12
+ * never resume. (Enforced by failing the run when errors spike.)
13
+ *
14
+ * Run: OKE_BENCH=1 OKE_TEST_POSTGRES=1 DATABASE_URL=… OKE_TEST_REDIS_URL=… bun test ./src/bench/g05-sustained-full.bench.ts --timeout 600000
15
+ */
16
+
17
+ import { describe, expect, test } from "bun:test";
18
+ import { LIVE_PG } from "./lib/infra.ts";
19
+ import { rssSlopeMbPerMin, startSampler } from "./lib/rss-sampler.ts";
20
+ import { DISCLAIMER, HARDWARE, percentile, writeArtifact } from "./lib/report.ts";
21
+
22
+ const CAL = process.env.OKE_BENCH_CAL === "1";
23
+ const PORT = 6675;
24
+ const BASE = `http://127.0.0.1:${PORT}`;
25
+ const DURATION_S = CAL ? 20 : Math.max(60, Number(process.env.OKE_BENCH_DURATION_S ?? "300"));
26
+ const EXTEND_S = CAL ? 0 : 300;
27
+ const CONCURRENCY = 50;
28
+ const PING_WORKERS = 30;
29
+ const MIXED = ["/_/bench/vault-read", "/_/bench/stream", "/rate", "/_/bench/rls"] as const;
30
+
31
+ interface Sample {
32
+ tMs: number;
33
+ rssMb: number;
34
+ fds: number;
35
+ }
36
+
37
+ type Server = Bun.Subprocess<"ignore", "pipe", "ignore">;
38
+
39
+ function spawnServe(): Server {
40
+ return Bun.spawn(["bun", "run", "src/bench/load-child.ts", "serve", String(PORT)], {
41
+ stdout: "pipe",
42
+ stderr: "ignore",
43
+ env: process.env,
44
+ });
45
+ }
46
+
47
+ async function readReadyPid(proc: Server): Promise<number> {
48
+ const reader = proc.stdout.getReader();
49
+ const dec = new TextDecoder();
50
+ let buf = "";
51
+ for (;;) {
52
+ const { done, value } = await reader.read();
53
+ if (done) throw new Error("server died before ready");
54
+ buf += dec.decode(value, { stream: true });
55
+ const nl = buf.indexOf("\n");
56
+ if (nl >= 0) {
57
+ const line = buf.slice(0, nl).trim();
58
+ if (line.startsWith("{")) {
59
+ void (async () => {
60
+ try {
61
+ for (;;) {
62
+ const r = await reader.read();
63
+ if (r.done) break;
64
+ }
65
+ } catch {
66
+ /* closed */
67
+ }
68
+ })();
69
+ return (JSON.parse(line) as { pid: number }).pid;
70
+ }
71
+ }
72
+ }
73
+ }
74
+
75
+ describe.skipIf(!process.env.OKE_BENCH || !LIVE_PG)("G5 — sustained baseline", () => {
76
+ test(
77
+ `${CONCURRENCY} closed-loop × ${DURATION_S}s (+${EXTEND_S}s if rising) — rps, RSS slope, GC spikes`,
78
+ async () => {
79
+ const server = spawnServe();
80
+ try {
81
+ const pid = await readReadyPid(server);
82
+ // Warm-up so boot/JIT isn't counted.
83
+ await Bun.sleep(CAL ? 2_000 : 5_000);
84
+
85
+ const sampler = startSampler(pid, 10_000);
86
+ const t0 = performance.now();
87
+ const deadlineMs = DURATION_S * 1000;
88
+
89
+ const latencies: Record<string, number[]> = { ping: [], mixed: [] };
90
+ // Thin latency samples beyond 200k so percentile sorting and memory
91
+ // stay bounded on long runs; request counts remain exact.
92
+ const pushLat = (arr: number[], ms: number): void => {
93
+ if (arr.length < 200_000 || arr.length % 8 === 0) arr.push(ms);
94
+ };
95
+ let requests = 0;
96
+ let errors = 0;
97
+ let stop = false;
98
+ const classCount = { ping: 0, mixed: 0 };
99
+
100
+ const pingWorker = async (): Promise<void> => {
101
+ while (!stop) {
102
+ const s = performance.now();
103
+ try {
104
+ const res = await fetch(`${BASE}/ping`);
105
+ await res.arrayBuffer();
106
+ if (!res.ok) errors++;
107
+ } catch {
108
+ errors++;
109
+ continue;
110
+ }
111
+ pushLat(latencies.ping!, performance.now() - s);
112
+ requests++;
113
+ classCount.ping++;
114
+ }
115
+ };
116
+
117
+ let mixedIdx = 0;
118
+ let gated429 = 0;
119
+ const mixedWorker = async (): Promise<void> => {
120
+ while (!stop) {
121
+ const path = MIXED[mixedIdx++ % MIXED.length]!;
122
+ const s = performance.now();
123
+ try {
124
+ const res =
125
+ path === "/rate"
126
+ ? await fetch(`${BASE}${path}`)
127
+ : await fetch(`${BASE}${path}`, {
128
+ method: "POST",
129
+ headers: { "content-type": "application/json" },
130
+ body: JSON.stringify(path === "/_/bench/stream" ? { chunks: 8 } : { n: 1 }),
131
+ });
132
+ await res.arrayBuffer();
133
+ if (res.status === 429 && path === "/rate") {
134
+ // Expected physics: the bench deliberately overruns the
135
+ // 1000/min gate — count separately, not as an error.
136
+ gated429++;
137
+ } else if (!res.ok) {
138
+ errors++;
139
+ }
140
+ } catch {
141
+ errors++;
142
+ continue;
143
+ }
144
+ pushLat(latencies.mixed!, performance.now() - s);
145
+ requests++;
146
+ classCount.mixed++;
147
+ }
148
+ };
149
+
150
+ const running = Promise.all([
151
+ ...Array.from({ length: PING_WORKERS }, () => pingWorker()),
152
+ ...Array.from({ length: CONCURRENCY - PING_WORKERS }, () => mixedWorker()),
153
+ ]);
154
+
155
+ // Phase 1: baseline duration; extend only while RSS slope still rises.
156
+ let phase1Samples: readonly Sample[] = [];
157
+ for (;;) {
158
+ await Bun.sleep(CAL ? 5_000 : 30_000);
159
+ phase1Samples = [...sampler.samples()];
160
+ const elapsed = performance.now() - t0;
161
+ if (stop) break;
162
+ if (elapsed >= deadlineMs) {
163
+ const tailSlope = rssSlopeMbPerMin(phase1Samples, 6);
164
+ if (!CAL && tailSlope > 2 && elapsed < deadlineMs + EXTEND_S * 1000 - 60_000) {
165
+ console.log(
166
+ `[G5] RSS slope still rising (${tailSlope} MB/min) at ${DURATION_S}s — extending to ${DURATION_S + EXTEND_S}s`,
167
+ );
168
+ continue;
169
+ }
170
+ stop = true;
171
+ }
172
+ }
173
+ await running;
174
+ const wallS = (performance.now() - t0) / 1000;
175
+ await sampler.stop();
176
+
177
+ const all = [...phase1Samples];
178
+ const pingLats = latencies.ping!;
179
+ const mixedLats = latencies.mixed!;
180
+ const flat = [...pingLats, ...mixedLats];
181
+
182
+ // Windowed p99 (per ~30 s slice) → spikes annotated as likely JSC GC
183
+ // unless correlated with request errors in the same window.
184
+ const windowP99s: number[] = [];
185
+ const buckets = new Map<number, number[]>();
186
+ const sliceCount = Math.max(1, Math.floor(wallS / 30));
187
+ for (let i = 0; i < flat.length; i++) {
188
+ const slice = Math.min(sliceCount - 1, Math.floor((i / flat.length) * sliceCount));
189
+ const arr = buckets.get(slice) ?? [];
190
+ arr.push(flat[i]!);
191
+ buckets.set(slice, arr);
192
+ }
193
+ for (const arr of buckets.values()) windowP99s.push(percentile(arr, 99));
194
+ const medianWindowP99 = percentile(windowP99s, 50) || 0;
195
+ const gcSpikeWindows = windowP99s.filter(
196
+ (p) => medianWindowP99 > 0 && p > medianWindowP99 * 3,
197
+ ).length;
198
+
199
+ const rssSlopeFull = rssSlopeMbPerMin(all);
200
+ const rssSlopeTail = rssSlopeMbPerMin(all, 6);
201
+ // Spread-into-Math.max overflows the call stack at millions of
202
+ // samples — always reduce instead.
203
+ const maxOf = (arr: readonly number[]): number => arr.reduce((a, b) => (b > a ? b : a), 0);
204
+ const issues: string[] = [];
205
+ if (errors > 0) {
206
+ issues.push(
207
+ `${errors} request errors under sustained load — investigate before trusting p99`,
208
+ );
209
+ }
210
+ if (rssSlopeFull > 5 && rssSlopeTail > 2) {
211
+ issues.push(
212
+ `RSS slope ${rssSlopeFull} MB/min full-run and still ${rssSlopeTail} MB/min over the last minute — suspected leak; re-run from t=0 after any fix`,
213
+ );
214
+ } else if (rssSlopeFull > 5) {
215
+ issues.push(
216
+ `RSS slope ${rssSlopeFull} MB/min full-run but tail is flat (${rssSlopeTail} MB/min last minute) — consistent with JSC heap expansion ramp, not an unbounded leak`,
217
+ );
218
+ }
219
+
220
+ const metrics: Record<string, number> = {
221
+ durationS: Number(wallS.toFixed(1)),
222
+ concurrency: CONCURRENCY,
223
+ totalRequests: requests,
224
+ reqPerSec: Number((requests / wallS).toFixed(1)),
225
+ pingReqPerSec: Number((classCount.ping / wallS).toFixed(1)),
226
+ mixedReqPerSec: Number((classCount.mixed / wallS).toFixed(1)),
227
+ pingP50Ms: Number(percentile(pingLats, 50).toFixed(2)),
228
+ pingP99Ms: Number(percentile(pingLats, 99).toFixed(2)),
229
+ mixedP50Ms: Number(percentile(mixedLats, 50).toFixed(2)),
230
+ mixedP99Ms: Number(percentile(mixedLats, 99).toFixed(2)),
231
+ overallMaxMs: flat.length ? Number(maxOf(flat).toFixed(2)) : 0,
232
+ gcSpikeWindows,
233
+ windowP99MedianMs: Number(medianWindowP99.toFixed(2)),
234
+ rssSlopeMbPerMin: rssSlopeFull,
235
+ rssSlopeLastMinute: rssSlopeTail,
236
+ rssMaxMb: all.length ? Number(maxOf(all.map((x) => x.rssMb)).toFixed(1)) : -1,
237
+ fdsFinal: all.length ? all[all.length - 1]!.fds : -1,
238
+ gateLimited429: gated429,
239
+ errors,
240
+ };
241
+ console.log("[G5] metrics:", JSON.stringify(metrics));
242
+
243
+ const path = await writeArtifact({
244
+ group: "G5-sustained-full",
245
+ hardware: HARDWARE,
246
+ disclaimer: DISCLAIMER,
247
+ command:
248
+ "OKE_BENCH=1 OKE_TEST_POSTGRES=1 DATABASE_URL=$DATABASE_URL OKE_TEST_REDIS_URL=$OKE_TEST_REDIS_URL bun test ./src/bench/g05-sustained-full.bench.ts --timeout 600000",
249
+ metrics,
250
+ issues,
251
+ fixes: [],
252
+ remeasured: null,
253
+ });
254
+ console.log(`[G5] artifact: ${path}`);
255
+
256
+ expect(errors / Math.max(1, requests)).toBeLessThan(0.001);
257
+ expect(requests).toBeGreaterThan(0);
258
+ } finally {
259
+ server.kill();
260
+ await server.exited.catch(() => {});
261
+ }
262
+ },
263
+ CAL ? 120_000 : 700_000,
264
+ );
265
+ });
@@ -0,0 +1,260 @@
1
+ /**
2
+ * G6 — mixed load + event-loop lag on a single child.
3
+ *
4
+ * One `load-child serve` process, driven concurrently by:
5
+ * - HTTP flood (20 closed-loop workers on `/ping`)
6
+ * - once-signal emit (8 workers on `/_/bench/emit`) + periodic drain
7
+ * - live-signal emits (8 workers on `/_/bench/emit-live`) fanned out to
8
+ * ≥50 in-process SSE subscribers whose received frames are counted
9
+ * - the child's internal clock tick loop (20 ms) under all of the above
10
+ *
11
+ * Reports per-subsystem throughput compared against the newest G5 artifact
12
+ * (cross-contention delta) plus harness-side `measureEventLoopLag()` p99.
13
+ *
14
+ * Run: OKE_BENCH=1 OKE_TEST_POSTGRES=1 DATABASE_URL=… OKE_TEST_REDIS_URL=… bun test ./src/bench/g06-mixed-load.bench.ts --timeout 300000
15
+ */
16
+
17
+ import { describe, expect, test } from "bun:test";
18
+ import { LIVE_PG } from "./lib/infra.ts";
19
+ import { measureEventLoopLag } from "./lib/event-loop-lag.ts";
20
+ import { DISCLAIMER, HARDWARE, percentile, writeArtifact } from "./lib/report.ts";
21
+
22
+ const CAL = process.env.OKE_BENCH_CAL === "1";
23
+ const PORT = 6676;
24
+ const BASE = `http://127.0.0.1:${PORT}`;
25
+ const DURATION_S = CAL ? 15 : Math.max(30, Number(process.env.OKE_BENCH_G6_S ?? "60"));
26
+ const SSE_SUBSCRIBERS = 50;
27
+ const PING_WORKERS = 20;
28
+ const EMIT_WORKERS = 8;
29
+ const LIVE_WORKERS = 8;
30
+
31
+ type Server = Bun.Subprocess<"ignore", "pipe", "ignore">;
32
+
33
+ function spawnServe(): Server {
34
+ return Bun.spawn(["bun", "run", "src/bench/load-child.ts", "serve", String(PORT)], {
35
+ stdout: "pipe",
36
+ stderr: "ignore",
37
+ env: process.env,
38
+ });
39
+ }
40
+
41
+ async function readReadyPid(proc: Server): Promise<number> {
42
+ const reader = proc.stdout.getReader();
43
+ const dec = new TextDecoder();
44
+ let buf = "";
45
+ for (;;) {
46
+ const { done, value } = await reader.read();
47
+ if (done) throw new Error("server died before ready");
48
+ buf += dec.decode(value, { stream: true });
49
+ const nl = buf.indexOf("\n");
50
+ if (nl >= 0) {
51
+ const line = buf.slice(0, nl).trim();
52
+ if (line.startsWith("{")) {
53
+ void (async () => {
54
+ try {
55
+ for (;;) {
56
+ const r = await reader.read();
57
+ if (r.done) break;
58
+ }
59
+ } catch {
60
+ /* closed */
61
+ }
62
+ })();
63
+ return (JSON.parse(line) as { pid: number }).pid;
64
+ }
65
+ }
66
+ }
67
+ }
68
+
69
+ /** SSE subscriber that counts received data frames until aborted. */
70
+ async function sseSubscriber(
71
+ target: string,
72
+ ac: AbortController,
73
+ onFrame: () => void,
74
+ ): Promise<void> {
75
+ while (!ac.signal.aborted) {
76
+ try {
77
+ const res = await fetch(target, {
78
+ signal: ac.signal,
79
+ headers: { accept: "text/event-stream" },
80
+ });
81
+ if (!res.body) return;
82
+ const reader = res.body.getReader();
83
+ const dec = new TextDecoder();
84
+ let buf = "";
85
+ for (;;) {
86
+ const { done, value } = await reader.read();
87
+ if (done) break;
88
+ buf += dec.decode(value, { stream: true });
89
+ let sep = buf.indexOf("\n\n");
90
+ while (sep >= 0) {
91
+ if (buf.slice(0, sep).includes("data:")) onFrame();
92
+ buf = buf.slice(sep + 2);
93
+ sep = buf.indexOf("\n\n");
94
+ }
95
+ }
96
+ } catch {
97
+ if (ac.signal.aborted) return;
98
+ }
99
+ await Bun.sleep(100); // reconnect briefly if dropped
100
+ }
101
+ }
102
+
103
+ /** Newest G5 artifact metrics for cross-contention comparison. */
104
+ async function latestG5(): Promise<Record<string, number> | null> {
105
+ try {
106
+ const proc = Bun.spawnSync([
107
+ "sh",
108
+ "-c",
109
+ "ls -t src/bench/results/G5-*.json 2>/dev/null | head -1",
110
+ ]);
111
+ const file = proc.stdout.toString().trim();
112
+ if (!file) return null;
113
+ return JSON.parse(await Bun.file(file).text()).metrics as Record<string, number>;
114
+ } catch {
115
+ return null;
116
+ }
117
+ }
118
+
119
+ describe.skipIf(!process.env.OKE_BENCH || !LIVE_PG)("G6 — mixed load contention", () => {
120
+ test(
121
+ `HTTP flood + emit/drain + live SSE(${SSE_SUBSCRIBERS}) + clock tick × ${DURATION_S}s`,
122
+ async () => {
123
+ const server = spawnServe();
124
+ try {
125
+ await readReadyPid(server);
126
+ await Bun.sleep(CAL ? 2_000 : 4_000); // warm-up
127
+
128
+ const lag = measureEventLoopLag(100);
129
+ const target = `${BASE}/_oke/live/bench-live`;
130
+ let sseFrames = 0;
131
+ const sseCtrl = new AbortController();
132
+ const subscribers = Array.from({ length: SSE_SUBSCRIBERS }, () =>
133
+ sseSubscriber(target, sseCtrl, () => sseFrames++),
134
+ );
135
+ // Let every subscriber connect before load starts.
136
+ await Bun.sleep(3_000);
137
+
138
+ const t0 = performance.now();
139
+ let stop = false;
140
+ const counters = { ping: 0, emit: 0, live: 0, drain: 0, errors: 0 };
141
+ const pingLats: number[] = [];
142
+ const emitLats: number[] = [];
143
+ const liveEmitLats: number[] = [];
144
+
145
+ const loop = async (
146
+ path: string,
147
+ counter: keyof typeof counters,
148
+ lats?: number[],
149
+ ): Promise<void> => {
150
+ while (!stop) {
151
+ const s = performance.now();
152
+ try {
153
+ const res =
154
+ path === "/ping"
155
+ ? await fetch(`${BASE}${path}`)
156
+ : await fetch(`${BASE}${path}`, { method: "POST" });
157
+ await res.arrayBuffer();
158
+ if (!res.ok) counters.errors++;
159
+ } catch {
160
+ counters.errors++;
161
+ continue;
162
+ }
163
+ lats?.push(performance.now() - s);
164
+ counters[counter]++;
165
+ }
166
+ };
167
+
168
+ const pingWorkers = Array.from({ length: PING_WORKERS }, () =>
169
+ loop("/ping", "ping", pingLats),
170
+ );
171
+ const emitWorkers = Array.from({ length: EMIT_WORKERS }, () =>
172
+ loop("/_/bench/emit", "emit", emitLats),
173
+ );
174
+ const liveWorkers = Array.from({ length: LIVE_WORKERS }, () =>
175
+ loop("/_/bench/emit-live", "live", liveEmitLats),
176
+ );
177
+
178
+ // Periodic explicit drain calls alongside the child's internal drain.
179
+ const drainLoop = (async () => {
180
+ while (!stop) {
181
+ try {
182
+ const res = await fetch(`${BASE}/_/bench/drain`, { method: "POST" });
183
+ await res.arrayBuffer();
184
+ if (res.ok) counters.drain++;
185
+ } catch {
186
+ counters.errors++;
187
+ }
188
+ await Bun.sleep(50);
189
+ }
190
+ })();
191
+
192
+ await Bun.sleep(DURATION_S * 1000);
193
+ stop = true;
194
+ await Promise.all([...pingWorkers, ...emitWorkers, ...liveWorkers, drainLoop]);
195
+ sseCtrl.abort();
196
+ await Promise.race([Promise.all(subscribers), Bun.sleep(5_000)]);
197
+ lag.stop();
198
+
199
+ const wallS = (performance.now() - t0) / 1000;
200
+ const g5 = await latestG5();
201
+
202
+ const lags = lag.lags();
203
+ const metrics: Record<string, number> = {
204
+ durationS: Number(wallS.toFixed(1)),
205
+ pingReqPerSec: Number((counters.ping / wallS).toFixed(1)),
206
+ emitPerSec: Number((counters.emit / wallS).toFixed(1)),
207
+ liveEmitPerSec: Number((counters.live / wallS).toFixed(1)),
208
+ drainsPerSec: Number((counters.drain / wallS).toFixed(1)),
209
+ sseFramesReceived: sseFrames,
210
+ sseDeliveredPerSec: Number((sseFrames / wallS).toFixed(1)),
211
+ errors: counters.errors,
212
+ lagP50Ms: Number(percentile(lags, 50).toFixed(2)),
213
+ lagP99Ms: Number(percentile(lags, 99).toFixed(2)),
214
+ lagMaxMs: lags.length ? Number(Math.max(...lags).toFixed(2)) : 0,
215
+ };
216
+ if (g5) {
217
+ const g5Ping = g5.pingReqPerSec ?? -1;
218
+ metrics.g5BaselinePingRps = g5Ping;
219
+ metrics.pingDeltaVsG5Pct =
220
+ g5Ping > 0
221
+ ? Number(((((metrics.pingReqPerSec ?? 0) - g5Ping) / g5Ping) * 100).toFixed(1))
222
+ : -1;
223
+ }
224
+
225
+ const issues: string[] = [];
226
+ if ((metrics.errors ?? 0) > 0) {
227
+ issues.push(`${metrics.errors} request errors under mixed load`);
228
+ }
229
+ if (g5 && metrics.pingDeltaVsG5Pct! < -40) {
230
+ issues.push(
231
+ `ping throughput fell ${Math.abs(metrics.pingDeltaVsG5Pct!)}% vs G5 baseline under cross-subsystem contention`,
232
+ );
233
+ }
234
+
235
+ console.log("[G6] metrics:", JSON.stringify(metrics));
236
+ const path = await writeArtifact({
237
+ group: "G6-mixed-load",
238
+ hardware: HARDWARE,
239
+ disclaimer: DISCLAIMER,
240
+ command:
241
+ "OKE_BENCH=1 OKE_TEST_POSTGRES=1 DATABASE_URL=$DATABASE_URL OKE_TEST_REDIS_URL=$OKE_TEST_REDIS_URL bun test ./src/bench/g06-mixed-load.bench.ts --timeout 300000",
242
+ metrics,
243
+ issues,
244
+ fixes: [],
245
+ remeasured: null,
246
+ });
247
+ console.log(`[G6] artifact: ${path}`);
248
+
249
+ expect(
250
+ (metrics.errors ?? 0) / Math.max(1, counters.ping + counters.emit + counters.live),
251
+ ).toBeLessThan(0.001);
252
+ expect(metrics.sseFramesReceived).toBeGreaterThan(0);
253
+ } finally {
254
+ server.kill();
255
+ await server.exited.catch(() => {});
256
+ }
257
+ },
258
+ CAL ? 120_000 : 240_000,
259
+ );
260
+ });
@@ -0,0 +1,100 @@
1
+ /**
2
+ * G7a — Vault raw crypto throughput (pure CPU).
3
+ *
4
+ * AES-256-GCM round-trips via `src/elements/vault/crypto.ts` — encrypt /
5
+ * decrypt / full round-trip ops-per-second at 1 KiB and 64 KiB payloads,
6
+ * with AAD bound exactly as production secrets are.
7
+ *
8
+ * Run: bun test ./src/bench/g07-vault-crypto.bench.ts
9
+ */
10
+
11
+ import { describe, expect, test } from "bun:test";
12
+ import {
13
+ buildAad,
14
+ decryptSecret,
15
+ encryptSecret,
16
+ generateDek,
17
+ importAesKey,
18
+ } from "../elements/vault/crypto.ts";
19
+ import { DISCLAIMER, HARDWARE, summarize, writeArtifact } from "./lib/report.ts";
20
+
21
+ const CAL = process.env.OKE_BENCH_CAL === "1";
22
+ const ITERS = CAL ? 1_000 : 10_000;
23
+ const SIZES = [
24
+ { label: "1KiB", bytes: 1024 },
25
+ { label: "64KiB", bytes: 64 * 1024 },
26
+ ] as const;
27
+
28
+ describe.skipIf(!process.env.OKE_BENCH)("G7a — vault crypto", () => {
29
+ test(
30
+ "AES-GCM encrypt/decrypt/round-trip at 1 KiB and 64 KiB",
31
+ async () => {
32
+ const dek = await importAesKey(generateDek());
33
+ const metrics: Record<string, number> = {};
34
+
35
+ for (const size of SIZES) {
36
+ const plaintext = "x".repeat(size.bytes);
37
+ const aad = buildAad("bench/crypto", 1, "aes-256-gcm", 1);
38
+
39
+ // Encrypt.
40
+ const encSamples: number[] = [];
41
+ let sealed;
42
+ for (let i = 0; i < ITERS; i += 1) {
43
+ const s = performance.now();
44
+ sealed = await encryptSecret(dek, plaintext, aad);
45
+ encSamples.push(performance.now() - s);
46
+ }
47
+
48
+ // Decrypt (of the last sealed blob — same shape every time).
49
+ const decSamples: number[] = [];
50
+ for (let i = 0; i < ITERS; i += 1) {
51
+ const s = performance.now();
52
+ const out = await decryptSecret(dek, sealed!, aad);
53
+ if (out !== plaintext) throw new Error("decrypt mismatch");
54
+ decSamples.push(performance.now() - s);
55
+ }
56
+
57
+ // Full round-trip.
58
+ const rtSamples: number[] = [];
59
+ for (let i = 0; i < ITERS; i += 1) {
60
+ const s = performance.now();
61
+ const blob = await encryptSecret(dek, plaintext, aad);
62
+ const out = await decryptSecret(dek, blob, aad);
63
+ if (out !== plaintext) throw new Error("round-trip mismatch");
64
+ rtSamples.push(performance.now() - s);
65
+ }
66
+
67
+ const e = summarize(encSamples);
68
+ const d = summarize(decSamples);
69
+ const r = summarize(rtSamples);
70
+ metrics[`${size.label}.encrypt.p50Ms`] = e.p50Ms;
71
+ metrics[`${size.label}.encrypt.p99Ms`] = e.p99Ms;
72
+ metrics[`${size.label}.encrypt.opsPerSec`] = e.opsPerSec;
73
+ metrics[`${size.label}.decrypt.p50Ms`] = d.p50Ms;
74
+ metrics[`${size.label}.decrypt.p99Ms`] = d.p99Ms;
75
+ metrics[`${size.label}.decrypt.opsPerSec`] = d.opsPerSec;
76
+ metrics[`${size.label}.roundTrip.p50Ms`] = r.p50Ms;
77
+ metrics[`${size.label}.roundTrip.p99Ms`] = r.p99Ms;
78
+ metrics[`${size.label}.roundTrip.opsPerSec`] = r.opsPerSec;
79
+
80
+ console.log(
81
+ `[G7a] ${size.label}: enc=${e.opsPerSec}/s dec=${d.opsPerSec}/s rt=${r.opsPerSec}/s`,
82
+ );
83
+ }
84
+
85
+ const path = await writeArtifact({
86
+ group: "G7a-crypto",
87
+ hardware: HARDWARE,
88
+ disclaimer: DISCLAIMER,
89
+ command: "OKE_BENCH=1 bun test ./src/bench/g07-vault-crypto.bench.ts",
90
+ metrics,
91
+ issues: [],
92
+ fixes: [],
93
+ remeasured: null,
94
+ });
95
+ console.log(`[G7a] artifact: ${path}`);
96
+ expect(metrics["1KiB.roundTrip.opsPerSec"]).toBeGreaterThan(0);
97
+ },
98
+ CAL ? 60_000 : 300_000,
99
+ );
100
+ });