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.
- package/AGENTS.md +5 -3
- package/README.md +14 -10
- package/manifest.v1.schema.json +61 -2
- package/package.json +19 -5
- package/site/content/docs/ai/index.mdx +1 -1
- package/site/content/docs/ai/mcp.mdx +10 -1
- package/site/content/docs/ai/skills.mdx +9 -9
- package/site/content/docs/elements/ai.mdx +1 -1
- package/site/content/docs/elements/clock.mdx +1 -1
- package/site/content/docs/elements/flow.mdx +25 -1
- package/site/content/docs/elements/gate.mdx +3 -2
- package/site/content/docs/elements/store.mdx +289 -341
- package/site/content/docs/elements/vault.mdx +5 -5
- package/site/content/docs/get-started/basic-usage.mdx +3 -10
- package/site/content/docs/get-started/index.mdx +1 -1
- package/site/content/docs/get-started/installation.mdx +2 -3
- package/site/content/docs/get-started/introduction.mdx +58 -121
- package/site/content/docs/get-started/meta.json +9 -1
- package/site/content/docs/get-started/project-structure.mdx +4 -11
- package/site/content/docs/get-started/testing.mdx +328 -0
- package/site/content/docs/get-started/why.mdx +93 -71
- package/site/content/docs/index.mdx +44 -11
- package/site/content/docs/meta.json +8 -5
- package/site/content/docs/plugins/apple.mdx +151 -0
- package/site/content/docs/plugins/discord.mdx +139 -0
- package/site/content/docs/plugins/facebook.mdx +134 -0
- package/site/content/docs/plugins/figma.mdx +138 -0
- package/site/content/docs/plugins/github.mdx +138 -0
- package/site/content/docs/plugins/google.mdx +153 -0
- package/site/content/docs/plugins/index.mdx +47 -1
- package/site/content/docs/plugins/meta.json +10 -0
- package/site/content/docs/plugins/microsoft.mdx +151 -0
- package/site/content/docs/plugins/oauth.mdx +188 -0
- package/site/content/docs/plugins/x.mdx +125 -0
- package/site/content/docs/providers/index.mdx +2 -0
- package/site/content/docs/recipes/index.mdx +2 -0
- package/site/content/docs/reference/cli.md +3 -2
- package/site/content/docs/reference/client.mdx +58 -1
- package/site/content/docs/reference/configuration.mdx +2 -4
- package/site/content/docs/reference/fx.mdx +3 -1
- package/site/content/docs/reference/index.mdx +0 -5
- package/site/content/docs/reference/meta.json +2 -2
- package/site/content/docs/reference/okid.mdx +137 -0
- package/src/auth/bindings.ts +1 -1
- package/src/auth/config.ts +9 -0
- package/src/auth/identity-sql.ts +314 -0
- package/src/auth/identity.ts +140 -2
- package/src/auth/index.ts +17 -1
- package/src/auth/method-context.ts +3 -0
- package/src/auth/oauth-as/cimd.ts +132 -0
- package/src/auth/oauth-as/crypto.test.ts +101 -0
- package/src/auth/oauth-as/crypto.ts +393 -0
- package/src/auth/oauth-as/errors.ts +68 -0
- package/src/auth/oauth-as/http.test.ts +419 -0
- package/src/auth/oauth-as/http.ts +842 -0
- package/src/auth/oauth-as/stores.ts +61 -0
- package/src/auth/oauth-as/tables.ts +142 -0
- package/src/auth/tables.ts +0 -11
- package/src/bench/README.md +83 -0
- package/src/bench/REPORT.md +176 -0
- package/src/bench/g01-rls-stamp.bench.ts +194 -0
- package/src/bench/g02-clock-per-tenant.bench.ts +158 -0
- package/src/bench/g03-signal-once.bench.ts +157 -0
- package/src/bench/g03-signal-reconnect.bench.ts +254 -0
- package/src/bench/g03-signal-sse-memory.bench.ts +191 -0
- package/src/bench/g04-auth-vault-hotpath.bench.ts +170 -0
- package/src/bench/g05-sustained-full.bench.ts +265 -0
- package/src/bench/g06-mixed-load.bench.ts +260 -0
- package/src/bench/g07-vault-crypto.bench.ts +100 -0
- package/src/bench/g07-vault-rotate-under-read.bench.ts +285 -0
- package/src/bench/g08-conn-oversubscribe.bench.ts +194 -0
- package/src/bench/g08-store-kv-durable.bench.ts +133 -0
- package/src/bench/g08-store-sql.bench.ts +178 -0
- package/src/bench/g09-journal-sustained.bench.ts +203 -0
- package/src/bench/g10-observability-contention.bench.ts +246 -0
- package/src/bench/g11-cold-start-cycle.bench.ts +164 -0
- package/src/bench/g13-elements.bench.ts +427 -0
- package/src/bench/g14-graceful-shutdown.bench.ts +244 -0
- package/src/bench/g15-postgres-degradation.bench.ts +264 -0
- package/src/bench/g16-live-query-fanout.bench.ts +206 -0
- package/src/bench/lib/event-loop-lag.ts +26 -0
- package/src/bench/lib/infra.ts +60 -0
- package/src/bench/lib/report.ts +52 -0
- package/src/bench/lib/rss-sampler.ts +61 -0
- package/src/bench/lib/signal-pg.ts +88 -0
- package/src/bench/load-app.ts +337 -0
- package/src/bench/load-child.ts +108 -0
- package/src/bench/smoke.bench.ts +43 -0
- package/src/cli/competitor-mention-removal.test.ts +28 -0
- package/src/cli/doctor-fd.ts +117 -0
- package/src/cli/doctor.test.ts +192 -0
- package/src/cli/doctor.ts +129 -1
- package/src/client/create.ts +95 -1
- package/src/client/index.ts +9 -2
- package/src/client/transport.ts +11 -4
- package/src/client/use-live-query.ts +154 -0
- package/src/client-react/index.ts +15 -1
- package/src/client-react/live-resource.ts +246 -0
- package/src/client-react/use-live-query.test.ts +475 -0
- package/src/client-react/use-live-query.ts +530 -0
- package/src/compiler/extract.test.ts +518 -0
- package/src/compiler/extract.ts +386 -19
- package/src/console/server/invoke-user-flow.ts +2 -1
- package/src/console/ui-next/dist/assets/{access-page-DnWbnGzq.js → access-page-De7Lc2JC.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-BiZ4-6yQ.js → flows-page-RGy7VEA_.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-C8NRK2R-.js → index-_rgpdVzo.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-CrB6vd1T.js → observability-page-Ds6pcnh-.js} +1 -1
- package/src/console/ui-next/dist/assets/{store-page-CS5-aETQ.js → store-page-02xOiqIK.js} +3 -3
- package/src/console/ui-next/dist/assets/{units-page-CjtdlW8l.js → units-page-4rHOePuE.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-C6Xxm9SA.js → vault-page-DISPgxLM.js} +1 -1
- package/src/console/ui-next/dist/index.html +1 -1
- package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +36 -2
- package/src/drivers/cdc-outbox.ts +389 -0
- package/src/drivers/memory.ts +20 -0
- package/src/drivers/oauth-apple.ts +156 -0
- package/src/drivers/oauth-discord.ts +79 -0
- package/src/drivers/oauth-facebook.ts +80 -0
- package/src/drivers/oauth-figma.ts +116 -0
- package/src/drivers/oauth-github.ts +92 -0
- package/src/drivers/oauth-google.ts +142 -0
- package/src/drivers/oauth-microsoft.ts +174 -0
- package/src/drivers/oauth-oidc.ts +293 -0
- package/src/drivers/oauth-shared.ts +326 -0
- package/src/drivers/oauth-types.ts +159 -0
- package/src/drivers/oauth-x.ts +77 -0
- package/src/drivers/oauth2-common.ts +95 -0
- package/src/drivers/oauth2-token.ts +61 -0
- package/src/drivers/pg-rls-row-passes.ts +251 -0
- package/src/drivers/pg-rls.ts +2 -0
- package/src/drivers/postgres.ts +45 -2
- package/src/drivers/signal-postgres.ts +2 -1
- package/src/elements/channel/runtime.ts +29 -2
- package/src/elements/channel.test.ts +52 -0
- package/src/elements/gate/boot.ts +29 -2
- package/src/elements/store/emit-drizzle.ts +147 -14
- package/src/elements/store/field-ddl.test.ts +118 -0
- package/src/elements/store/field-types.test.ts +455 -0
- package/src/elements/store/list-query.golden.json +777 -0
- package/src/elements/store/list-query.parity.test.ts +396 -0
- package/src/elements/store/list-query.ts +792 -0
- package/src/elements/store/live-default.test.ts +136 -0
- package/src/elements/store/live-http.test.ts +160 -0
- package/src/elements/store/live-isolation.test.ts +291 -0
- package/src/elements/store/live-query-runtime.test.ts +323 -0
- package/src/elements/store/live-query-runtime.ts +403 -0
- package/src/elements/store/live-query-server.test.ts +377 -0
- package/src/elements/store/live-query-server.ts +102 -0
- package/src/elements/store/live-query.ts +97 -0
- package/src/elements/store/resource.ts +189 -680
- package/src/elements/store/rls-row-passes-policies.parity.test.ts +665 -0
- package/src/elements/store/schema-decl.ts +539 -41
- package/src/elements/store/sql-rls-stamp.test.ts +27 -0
- package/src/elements/store/sql-session.ts +297 -35
- package/src/elements/store/table.ts +102 -21
- package/src/elements/store.test.ts +3 -1
- package/src/elements/store.ts +12 -1
- package/src/elements/vault/chaos-child.ts +74 -1
- package/src/elements/vault/chaos.test.ts +4 -2
- package/src/elements/vault/storage.ts +4 -2
- package/src/index.ts +5 -2
- package/src/kernel/app-auth.ts +1 -0
- package/src/kernel/app.ts +116 -2
- package/src/kernel/auth-sharing.test.ts +196 -0
- package/src/kernel/boot.test.ts +3 -3
- package/src/kernel/errors.ts +8 -0
- package/src/kernel/fx.test.ts +1 -0
- package/src/kernel/fx.ts +14 -2
- package/src/kernel/horizontal-child.ts +2 -1
- package/src/kernel/http-resource.ts +33 -7
- package/src/kernel/identity-host-persist.test.ts +119 -0
- package/src/kernel/instance-id.ts +4 -2
- package/src/kernel/journal.ts +2 -1
- package/src/kernel/mcp-tool.test.ts +95 -0
- package/src/kernel/on.ts +9 -0
- package/src/kernel/realtime-bind.ts +326 -0
- package/src/kernel/resource-live.ts +117 -0
- package/src/kernel/triggers.ts +86 -4
- package/src/manifest/diff.ts +37 -0
- package/src/manifest/types.ts +64 -2
- package/src/okid.bench.test.ts +64 -0
- package/src/okid.test.ts +338 -0
- package/src/okid.ts +245 -0
- package/src/plugins/anonymous.ts +19 -1
- package/src/plugins/auth/shared.ts +15 -0
- package/src/plugins/index.ts +2 -0
- package/src/plugins/magic-link.ts +10 -8
- package/src/plugins/mcp-oauth.ts +208 -0
- package/src/plugins/oauth/flow-store.ts +117 -0
- package/src/plugins/oauth/link.ts +69 -0
- package/src/plugins/oauth/shared.ts +108 -0
- package/src/plugins/oauth/token-vault.ts +100 -0
- package/src/plugins/oauth.security.test.ts +535 -0
- package/src/plugins/oauth.ts +532 -0
- package/src/plugins/otp.ts +48 -6
- package/src/plugins/passkey.ts +20 -1
- package/src/plugins/two-factor.ts +11 -0
- package/src/plugins/username.ts +40 -7
- package/src/release/build-lib.ts +7 -1
- package/src/release/measure.ts +1 -0
- package/src/release/official-plugins.ts +4 -1
- package/src/runs/collect.ts +2 -1
- package/src/runs/drivers/files.ts +2 -1
- package/src/test/create-test-app.ts +114 -5
- package/src/test/export-bundle.test.ts +33 -0
- package/src/test/live-signals.test.ts +83 -0
- package/src/test/tenant-isolation.test.ts +175 -0
- package/src/testing.ts +26 -0
- package/src/upgrade/codemods.ts +1 -1
- package/site/content/docs/reference/migrating-environments.mdx +0 -158
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* G16 — Live query fan-out (subscribers × stamped EXISTS).
|
|
3
|
+
*
|
|
4
|
+
* Characterizes the Realtime capability's honest cost model:
|
|
5
|
+
* cost_per_event ≈ active_subscriptions × (stamp_prelude + point_lookup)
|
|
6
|
+
*
|
|
7
|
+
* Sweeps subscriber counts on one shared PGlite connection (serialized stamp
|
|
8
|
+
* queue — G1 physics). Each CDC event fans out a stamped EXISTS probe per
|
|
9
|
+
* subscriber; wall-clock and per-event p50/p99 are recorded.
|
|
10
|
+
*
|
|
11
|
+
* Run: bun test src/bench/g16-live-query-fanout.bench.ts --timeout 120000
|
|
12
|
+
* Env: OKE_BENCH=1 (PGlite). OKE_BENCH_CAL=1 shrinks N for a calibration pass.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
16
|
+
import { connectPglite } from "../drivers/pglite.ts";
|
|
17
|
+
import { installOkeRlsHelpers, type RlsIdentity } from "../drivers/pg-rls.ts";
|
|
18
|
+
import type { SqlConnection } from "../drivers/types.ts";
|
|
19
|
+
import {
|
|
20
|
+
LiveQueryRuntime,
|
|
21
|
+
type LiveQueryEvent,
|
|
22
|
+
type LiveSubscription,
|
|
23
|
+
} from "../elements/store/live-query-runtime.ts";
|
|
24
|
+
import { liveQueryRuntimeFromConn } from "../elements/store/live-query-server.ts";
|
|
25
|
+
import { DISCLAIMER, HARDWARE, percentile, writeArtifact } from "./lib/report.ts";
|
|
26
|
+
|
|
27
|
+
const CAL = process.env.OKE_BENCH_CAL === "1";
|
|
28
|
+
/** Subscriber counts per sweep point. */
|
|
29
|
+
const SUBSCRIBERS = CAL ? [10, 50] : [10, 50, 100, 200];
|
|
30
|
+
/** CDC events fired per point (each fans out to every subscriber). */
|
|
31
|
+
const EVENTS = CAL ? 5 : 20;
|
|
32
|
+
/** Settle wait after the last event (ms). */
|
|
33
|
+
const SETTLE_MS = 2_000;
|
|
34
|
+
|
|
35
|
+
const NOTES_DDL = `CREATE TABLE notes (
|
|
36
|
+
id text PRIMARY KEY,
|
|
37
|
+
owner text NOT NULL,
|
|
38
|
+
body text NOT NULL
|
|
39
|
+
)`;
|
|
40
|
+
|
|
41
|
+
const OWNER_POLICY = `CREATE POLICY owner_select ON notes
|
|
42
|
+
AS PERMISSIVE FOR SELECT TO public
|
|
43
|
+
USING (owner = oke.user())`;
|
|
44
|
+
|
|
45
|
+
let conn: SqlConnection;
|
|
46
|
+
let runtime: LiveQueryRuntime;
|
|
47
|
+
|
|
48
|
+
beforeAll(async () => {
|
|
49
|
+
conn = await connectPglite({ url: "memory://bench-g16-live-fanout", role: "primary" });
|
|
50
|
+
await conn.exec(NOTES_DDL);
|
|
51
|
+
await conn.exec(`INSERT INTO notes VALUES ('n1', 'alice', 'hi'), ('n2', 'bob', 'yo')`);
|
|
52
|
+
await conn.exec(`ALTER TABLE notes ENABLE ROW LEVEL SECURITY`);
|
|
53
|
+
await installOkeRlsHelpers((sql) => conn.exec(sql));
|
|
54
|
+
await conn.exec(OWNER_POLICY);
|
|
55
|
+
runtime = liveQueryRuntimeFromConn(conn);
|
|
56
|
+
}, 15_000);
|
|
57
|
+
|
|
58
|
+
afterAll(async () => {
|
|
59
|
+
await conn.close();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
interface PointResult {
|
|
63
|
+
readonly subscribers: number;
|
|
64
|
+
readonly events: number;
|
|
65
|
+
readonly delivered: number;
|
|
66
|
+
readonly wallMs: number;
|
|
67
|
+
readonly p50MsPerEvent: number;
|
|
68
|
+
readonly p99MsPerEvent: number;
|
|
69
|
+
readonly eventsPerSec: number;
|
|
70
|
+
readonly shed: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function aliceIdentity(_i: number): RlsIdentity {
|
|
74
|
+
return { gate: "member", userId: "alice", scopes: ["member"] };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function waitFor(pred: () => boolean, timeoutMs: number): Promise<boolean> {
|
|
78
|
+
const start = Date.now();
|
|
79
|
+
return new Promise((resolve) => {
|
|
80
|
+
const tick = (): void => {
|
|
81
|
+
if (pred()) {
|
|
82
|
+
resolve(true);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (Date.now() - start > timeoutMs) {
|
|
86
|
+
resolve(false);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
setTimeout(tick, 5);
|
|
90
|
+
};
|
|
91
|
+
tick();
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
describe("G16 live query fan-out", () => {
|
|
96
|
+
test("subscribers × stamped EXISTS scales without unbounded queue growth", async () => {
|
|
97
|
+
if (process.env.OKE_BENCH !== "1") {
|
|
98
|
+
console.log("skip: set OKE_BENCH=1 to run G16");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const points: PointResult[] = [];
|
|
103
|
+
const issues: string[] = [];
|
|
104
|
+
|
|
105
|
+
for (const n of SUBSCRIBERS) {
|
|
106
|
+
const delivered: number[] = [];
|
|
107
|
+
const unsubs: Array<() => void> = [];
|
|
108
|
+
let totalDelivered = 0;
|
|
109
|
+
|
|
110
|
+
for (let i = 0; i < n; i += 1) {
|
|
111
|
+
delivered.push(0);
|
|
112
|
+
const idx = i;
|
|
113
|
+
const sub: LiveSubscription = {
|
|
114
|
+
id: `sub-${n}-${i}`,
|
|
115
|
+
ref: "sql:notes",
|
|
116
|
+
table: "notes",
|
|
117
|
+
pkColumn: "id",
|
|
118
|
+
identity: aliceIdentity(i),
|
|
119
|
+
deliver(_event: LiveQueryEvent) {
|
|
120
|
+
delivered[idx] = (delivered[idx] ?? 0) + 1;
|
|
121
|
+
totalDelivered += 1;
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
unsubs.push(runtime.subscribe(sub));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const shedBefore = runtime.metrics.eventsShed;
|
|
128
|
+
const eventLatencies: number[] = [];
|
|
129
|
+
const t0 = performance.now();
|
|
130
|
+
for (let e = 0; e < EVENTS; e += 1) {
|
|
131
|
+
const before = totalDelivered;
|
|
132
|
+
const te0 = performance.now();
|
|
133
|
+
runtime.onCdc({
|
|
134
|
+
tableName: "notes",
|
|
135
|
+
op: "update",
|
|
136
|
+
before: { id: "n1", owner: "alice", body: "hi" },
|
|
137
|
+
after: { id: "n1", owner: "alice", body: `hi-${e}` },
|
|
138
|
+
seq: e + 1,
|
|
139
|
+
});
|
|
140
|
+
// Wait until every subscriber got this event (or settle timeout).
|
|
141
|
+
const expected = before + n;
|
|
142
|
+
const ok = await waitFor(() => totalDelivered >= expected, SETTLE_MS);
|
|
143
|
+
eventLatencies.push(performance.now() - te0);
|
|
144
|
+
if (!ok) {
|
|
145
|
+
issues.push(
|
|
146
|
+
`N=${n} event ${e}: delivered ${totalDelivered}/${expected} within ${SETTLE_MS}ms`,
|
|
147
|
+
);
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const wallMs = performance.now() - t0;
|
|
152
|
+
const shed = runtime.metrics.eventsShed - shedBefore;
|
|
153
|
+
|
|
154
|
+
for (const u of unsubs) u();
|
|
155
|
+
|
|
156
|
+
const point: PointResult = {
|
|
157
|
+
subscribers: n,
|
|
158
|
+
events: eventLatencies.length,
|
|
159
|
+
delivered: totalDelivered,
|
|
160
|
+
wallMs: Number(wallMs.toFixed(1)),
|
|
161
|
+
p50MsPerEvent: Number(percentile(eventLatencies, 50).toFixed(2)),
|
|
162
|
+
p99MsPerEvent: Number(percentile(eventLatencies, 99).toFixed(2)),
|
|
163
|
+
eventsPerSec: Number(((eventLatencies.length / wallMs) * 1000).toFixed(1)),
|
|
164
|
+
shed,
|
|
165
|
+
};
|
|
166
|
+
points.push(point);
|
|
167
|
+
|
|
168
|
+
// Shed must stay at 0 under these N×EVENTS sizes — queue is 10k.
|
|
169
|
+
expect(shed).toBe(0);
|
|
170
|
+
// Rough linear growth: p50 at max N should not be > 20× p50 at min N
|
|
171
|
+
// (serialized PGlite is slow by design; collapse would be >>20×).
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (points.length >= 2) {
|
|
175
|
+
const first = points[0]!;
|
|
176
|
+
const last = points[points.length - 1]!;
|
|
177
|
+
const ratio = last.p50MsPerEvent / Math.max(0.001, first.p50MsPerEvent);
|
|
178
|
+
if (ratio > 40) {
|
|
179
|
+
issues.push(
|
|
180
|
+
`super-linear fan-out: p50 ${first.subscribers}→${last.subscribers} grew ${ratio.toFixed(1)}×`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const path = await writeArtifact({
|
|
186
|
+
group: "g16-live-query-fanout",
|
|
187
|
+
hardware: HARDWARE,
|
|
188
|
+
disclaimer: DISCLAIMER,
|
|
189
|
+
command: "bun test src/bench/g16-live-query-fanout.bench.ts",
|
|
190
|
+
metrics: Object.fromEntries(
|
|
191
|
+
points.flatMap((p) => [
|
|
192
|
+
[`n${p.subscribers}_p50Ms`, p.p50MsPerEvent],
|
|
193
|
+
[`n${p.subscribers}_p99Ms`, p.p99MsPerEvent],
|
|
194
|
+
[`n${p.subscribers}_eventsPerSec`, p.eventsPerSec],
|
|
195
|
+
[`n${p.subscribers}_wallMs`, p.wallMs],
|
|
196
|
+
]),
|
|
197
|
+
),
|
|
198
|
+
issues,
|
|
199
|
+
fixes: [],
|
|
200
|
+
remeasured: null,
|
|
201
|
+
});
|
|
202
|
+
console.log(`G16 artifact → ${path}`);
|
|
203
|
+
console.table(points);
|
|
204
|
+
expect(points.length).toBe(SUBSCRIBERS.length);
|
|
205
|
+
}, 120_000);
|
|
206
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event-loop lag probe — setInterval drift under load.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Measure setInterval drift under load. Returns lag samples in ms. */
|
|
6
|
+
export function measureEventLoopLag(expectedIntervalMs = 100): {
|
|
7
|
+
lags: () => number[];
|
|
8
|
+
stop: () => void;
|
|
9
|
+
} {
|
|
10
|
+
const lags: number[] = [];
|
|
11
|
+
let last = performance.now();
|
|
12
|
+
let alive = true;
|
|
13
|
+
const id = setInterval(() => {
|
|
14
|
+
if (!alive) return;
|
|
15
|
+
const now = performance.now();
|
|
16
|
+
lags.push(Number((now - last - expectedIntervalMs).toFixed(2)));
|
|
17
|
+
last = now;
|
|
18
|
+
}, expectedIntervalMs);
|
|
19
|
+
return {
|
|
20
|
+
lags: () => lags,
|
|
21
|
+
stop: () => {
|
|
22
|
+
alive = false;
|
|
23
|
+
clearInterval(id);
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bench infra helpers — live-env resolution, file waits, jsonl loading.
|
|
3
|
+
*
|
|
4
|
+
* Env gating mirrors `src/kernel/horizontal.integration.test.ts`:
|
|
5
|
+
* OKE_TEST_POSTGRES_URL (or OKE_TEST_POSTGRES=1 + DATABASE_URL /
|
|
6
|
+
* OKE_STORE_SQL_URL) AND OKE_TEST_REDIS_URL (or REDIS_URL). Credentials are
|
|
7
|
+
* resolved at runtime from the environment only — never printed or logged.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const LIVE_PG =
|
|
11
|
+
process.env.OKE_TEST_POSTGRES_URL?.trim() ||
|
|
12
|
+
(process.env.OKE_TEST_POSTGRES === "1"
|
|
13
|
+
? (process.env.DATABASE_URL ?? process.env.OKE_STORE_SQL_URL)?.trim()
|
|
14
|
+
: undefined);
|
|
15
|
+
|
|
16
|
+
export const LIVE_REDIS =
|
|
17
|
+
process.env.OKE_TEST_REDIS_URL?.trim() || process.env.REDIS_URL?.trim() || undefined;
|
|
18
|
+
|
|
19
|
+
export function resolveLivePg(): string | undefined {
|
|
20
|
+
return LIVE_PG;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function resolveLiveRedis(): string | undefined {
|
|
24
|
+
return LIVE_REDIS;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Both live Postgres and Redis, or undefined — the bench gate. */
|
|
28
|
+
export function liveInfra(): { pg: string; redis: string } | undefined {
|
|
29
|
+
return LIVE_PG && LIVE_REDIS ? { pg: LIVE_PG, redis: LIVE_REDIS } : undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Poll until `path` exists (or timeout). Mirrors the horizontal test helper. */
|
|
33
|
+
export async function waitForFile(path: string, timeoutMs = 20_000): Promise<boolean> {
|
|
34
|
+
const deadline = Date.now() + timeoutMs;
|
|
35
|
+
while (Date.now() < deadline) {
|
|
36
|
+
if (await Bun.file(path).exists()) return true;
|
|
37
|
+
await Bun.sleep(20);
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Poll until `cond()` returns true (or timeout). */
|
|
43
|
+
export async function waitFor(cond: () => Promise<boolean>, timeoutMs = 20_000): Promise<boolean> {
|
|
44
|
+
const deadline = Date.now() + timeoutMs;
|
|
45
|
+
while (Date.now() < deadline) {
|
|
46
|
+
if (await cond()) return true;
|
|
47
|
+
await Bun.sleep(20);
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Read a JSONL file into parsed records; missing file → empty array. */
|
|
53
|
+
export async function loadJsonl<T = Record<string, unknown>>(path: string): Promise<T[]> {
|
|
54
|
+
if (!(await Bun.file(path).exists())) return [];
|
|
55
|
+
return (await Bun.file(path).text())
|
|
56
|
+
.trim()
|
|
57
|
+
.split("\n")
|
|
58
|
+
.filter(Boolean)
|
|
59
|
+
.map((l) => JSON.parse(l) as T);
|
|
60
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bench artifact writer — every benchmark emits a uniform, disclaimer-stamped
|
|
3
|
+
* JSON artifact into `src/bench/results/` (gitignored).
|
|
4
|
+
*
|
|
5
|
+
* These numbers are for regression trend analysis on this hardware — not
|
|
6
|
+
* production SLA targets. macOS/Bun/JSC behavior differs from Linux prod.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface BenchArtifact {
|
|
10
|
+
readonly group: string;
|
|
11
|
+
readonly hardware: string;
|
|
12
|
+
readonly disclaimer: string;
|
|
13
|
+
readonly command: string;
|
|
14
|
+
readonly metrics: Record<string, number>;
|
|
15
|
+
readonly issues: string[];
|
|
16
|
+
readonly fixes: string[];
|
|
17
|
+
readonly remeasured: null | { metrics: Record<string, number>; rerunScope?: string };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const HARDWARE = "Apple M4, 24GB";
|
|
21
|
+
|
|
22
|
+
export const DISCLAIMER = "trend-analysis-only; not SLA; macOS M4 not Linux prod";
|
|
23
|
+
|
|
24
|
+
/** Percentile of an array of samples (ms or ops). */
|
|
25
|
+
export function percentile(samples: number[], p: number): number {
|
|
26
|
+
if (samples.length === 0) return NaN;
|
|
27
|
+
const sorted = [...samples].sort((a, b) => a - b);
|
|
28
|
+
const idx = Math.min(sorted.length - 1, Math.ceil((p / 100) * sorted.length) - 1);
|
|
29
|
+
return sorted[idx]!;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function summarize(samples: number[]): {
|
|
33
|
+
p50Ms: number;
|
|
34
|
+
p99Ms: number;
|
|
35
|
+
opsPerSec: number;
|
|
36
|
+
} {
|
|
37
|
+
const total = samples.reduce((a, b) => a + b, 0);
|
|
38
|
+
return {
|
|
39
|
+
p50Ms: Number(percentile(samples, 50).toFixed(3)),
|
|
40
|
+
p99Ms: Number(percentile(samples, 99).toFixed(3)),
|
|
41
|
+
opsPerSec: total > 0 ? Number((samples.length / (total / 1000)).toFixed(1)) : 0,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Write `src/bench/results/<group>-<timestamp>.json`. */
|
|
46
|
+
export async function writeArtifact(a: BenchArtifact): Promise<string> {
|
|
47
|
+
const dir = new URL("../results/", import.meta.url).pathname;
|
|
48
|
+
await Bun.$`mkdir -p ${dir}`.quiet();
|
|
49
|
+
const path = `${dir}${a.group}-${Date.now()}.json`;
|
|
50
|
+
await Bun.write(path, `${JSON.stringify(a, null, 2)}\n`);
|
|
51
|
+
return path;
|
|
52
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background RSS / open-fd sampler for a foreign PID (the served app child).
|
|
3
|
+
*
|
|
4
|
+
* Samples via `ps` / `lsof` best-effort; lsof failure yields fds=0.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface RssSample {
|
|
8
|
+
tMs: number;
|
|
9
|
+
rssMb: number;
|
|
10
|
+
fds: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Sample RSS every intervalMs until stopped. Open-fd count via
|
|
15
|
+
* `lsof -p <pid> -Fn | wc -l` best-effort (0 if lsof fails).
|
|
16
|
+
*/
|
|
17
|
+
export function startSampler(
|
|
18
|
+
pid: number,
|
|
19
|
+
intervalMs = 5000,
|
|
20
|
+
): { samples: () => readonly RssSample[]; stop: () => Promise<void> } {
|
|
21
|
+
const samples: RssSample[] = [];
|
|
22
|
+
let alive = true;
|
|
23
|
+
const t0 = performance.now();
|
|
24
|
+
const tick = async () => {
|
|
25
|
+
while (alive) {
|
|
26
|
+
let rssMb = 0;
|
|
27
|
+
let fds = 0;
|
|
28
|
+
try {
|
|
29
|
+
const ps = Bun.spawnSync(["ps", "-o", "rss=", "-p", String(pid)]);
|
|
30
|
+
rssMb = Number(ps.stdout.toString().trim()) / 1024;
|
|
31
|
+
const lf = Bun.spawnSync(["sh", "-c", `lsof -p ${pid} -Fn 2>/dev/null | wc -l`]);
|
|
32
|
+
fds = Number(lf.stdout.toString().trim());
|
|
33
|
+
} catch {
|
|
34
|
+
/* best-effort */
|
|
35
|
+
}
|
|
36
|
+
samples.push({ tMs: Math.round(performance.now() - t0), rssMb, fds });
|
|
37
|
+
await Bun.sleep(intervalMs);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
void tick();
|
|
41
|
+
return {
|
|
42
|
+
samples: () => samples,
|
|
43
|
+
stop: async () => {
|
|
44
|
+
alive = false;
|
|
45
|
+
await Bun.sleep(intervalMs);
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Linear slope MB/min over samples (least squares on last N points). */
|
|
51
|
+
export function rssSlopeMbPerMin(samples: readonly RssSample[], windowLast = 12): number {
|
|
52
|
+
const pts = samples.slice(-windowLast).filter((s) => s.rssMb > 0);
|
|
53
|
+
if (pts.length < 2) return 0;
|
|
54
|
+
const n = pts.length;
|
|
55
|
+
const sx = pts.reduce((a, s) => a + s.tMs, 0);
|
|
56
|
+
const sy = pts.reduce((a, s) => a + s.rssMb, 0);
|
|
57
|
+
const sxy = pts.reduce((a, s) => a + s.tMs * s.rssMb, 0);
|
|
58
|
+
const sxx = pts.reduce((a, s) => a + s.tMs * s.tMs, 0);
|
|
59
|
+
const slopePerMs = (n * sxy - sx * sy) / (n * sxx - sx * sx || 1);
|
|
60
|
+
return Number((slopePerMs * 60_000).toFixed(2));
|
|
61
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live-Postgres SQL surface for `openPostgresSignal` (`src/drivers/signal-postgres.ts`).
|
|
3
|
+
*
|
|
4
|
+
* Bun.SQL has no LISTEN/NOTIFY API (see `src/drivers/bun-native-completeness.test.ts`),
|
|
5
|
+
* so this adapter:
|
|
6
|
+
* - runs the real transactional outbox (INSERT + FOR UPDATE SKIP LOCKED claims)
|
|
7
|
+
* against live Postgres via `Bun.SQL`;
|
|
8
|
+
* - fans `notify()` out to in-process `listen()` callbacks so wakeups work
|
|
9
|
+
* within one process, and additionally issues a best-effort real
|
|
10
|
+
* `pg_notify` for cross-process visibility.
|
|
11
|
+
*
|
|
12
|
+
* Cross-process LISTEN delivery is NOT covered — bench artifacts using this
|
|
13
|
+
* adapter must say so.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { toPostgresParams, withPinnedPostgres } from "../../drivers/postgres.ts";
|
|
17
|
+
import type { PostgresSignalSql } from "../../drivers/signal-postgres.ts";
|
|
18
|
+
|
|
19
|
+
interface UnsafeClient {
|
|
20
|
+
unsafe(
|
|
21
|
+
sql: string,
|
|
22
|
+
values?: readonly unknown[],
|
|
23
|
+
): PromiseLike<Record<string, unknown>[] | { length: number; changes?: number }>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function wrap(
|
|
27
|
+
client: UnsafeClient,
|
|
28
|
+
listeners: Map<string, Set<(payload: string) => void>>,
|
|
29
|
+
): PostgresSignalSql {
|
|
30
|
+
return {
|
|
31
|
+
async query(sql, params = []) {
|
|
32
|
+
const result = await client.unsafe(toPostgresParams(sql, params), [...params]);
|
|
33
|
+
if (Array.isArray(result)) return result as Record<string, unknown>[];
|
|
34
|
+
return Array.from(result as ArrayLike<Record<string, unknown>>);
|
|
35
|
+
},
|
|
36
|
+
async exec(sql, params = []) {
|
|
37
|
+
const result = await client.unsafe(toPostgresParams(sql, params), [...params]);
|
|
38
|
+
if (
|
|
39
|
+
result &&
|
|
40
|
+
typeof result === "object" &&
|
|
41
|
+
"changes" in result &&
|
|
42
|
+
typeof (result as { changes: unknown }).changes === "number"
|
|
43
|
+
) {
|
|
44
|
+
return { changes: (result as { changes: number }).changes };
|
|
45
|
+
}
|
|
46
|
+
if (Array.isArray(result)) return { changes: result.length };
|
|
47
|
+
return { changes: 0 };
|
|
48
|
+
},
|
|
49
|
+
// withPinnedPostgres pins ONE pooled connection — required behind pgdog.
|
|
50
|
+
begin: (fn) =>
|
|
51
|
+
withPinnedPostgres(client as never, (tx) => fn(wrap(tx as UnsafeClient, listeners))),
|
|
52
|
+
async listen(channel, onNotify) {
|
|
53
|
+
const set = listeners.get(channel) ?? new Set<(payload: string) => void>();
|
|
54
|
+
set.add(onNotify);
|
|
55
|
+
listeners.set(channel, set);
|
|
56
|
+
return () => {
|
|
57
|
+
set.delete(onNotify);
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
async notify(channel, payload) {
|
|
61
|
+
// Real NOTIFY keeps cross-process semantics on the server; local fanout
|
|
62
|
+
// is what actually wakes this process's drain loop (no LISTEN API).
|
|
63
|
+
try {
|
|
64
|
+
await client.unsafe("SELECT pg_notify($1, $2)", [channel, payload]);
|
|
65
|
+
} catch {
|
|
66
|
+
/* best-effort */
|
|
67
|
+
}
|
|
68
|
+
for (const fn of listeners.get(channel) ?? []) fn(payload);
|
|
69
|
+
},
|
|
70
|
+
async close() {
|
|
71
|
+
/* per-transaction wrappers share the parent pool — nothing to close */
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function createBunSignalSql(url: string): PostgresSignalSql & { close(): Promise<void> } {
|
|
77
|
+
const client = new Bun.SQL(url, { max: 4 }) as unknown as UnsafeClient & {
|
|
78
|
+
close?: (options?: { timeout?: number }) => Promise<void>;
|
|
79
|
+
};
|
|
80
|
+
const listeners = new Map<string, Set<(payload: string) => void>>();
|
|
81
|
+
const api = wrap(client, listeners);
|
|
82
|
+
return {
|
|
83
|
+
...api,
|
|
84
|
+
close: async () => {
|
|
85
|
+
await client.close?.();
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|