okengine 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -2
- package/site/content/docs/deployment/docker-swarm.mdx +228 -0
- package/site/content/docs/deployment/docker.mdx +212 -0
- package/site/content/docs/deployment/index.mdx +83 -0
- package/site/content/docs/deployment/kubernetes.mdx +176 -0
- package/site/content/docs/deployment/meta.json +5 -0
- package/site/content/docs/deployment/reverse-proxy.mdx +216 -0
- package/site/content/docs/elements/channel.mdx +25 -12
- package/site/content/docs/elements/clock.mdx +17 -15
- package/site/content/docs/elements/flow.mdx +6 -2
- package/site/content/docs/elements/signal.mdx +10 -8
- package/site/content/docs/elements/store.mdx +165 -0
- package/site/content/docs/get-started/index.mdx +5 -0
- package/site/content/docs/get-started/installation.mdx +18 -16
- package/site/content/docs/index.mdx +5 -0
- package/site/content/docs/meta.json +10 -1
- package/site/content/docs/plugins/index.mdx +1 -2
- package/site/content/docs/plugins/magic-link.mdx +44 -2
- package/site/content/docs/plugins/meta.json +1 -2
- package/site/content/docs/plugins/otp.mdx +202 -0
- package/site/content/docs/plugins/two-factor.mdx +2 -1
- package/site/content/docs/reference/cli.md +5 -2
- package/site/content/docs/reference/configuration.mdx +21 -3
- package/site/content/docs/reference/environment-variables.mdx +21 -8
- package/site/content/docs/reference/plugins.mdx +1 -1
- package/src/auth/auth.test.ts +36 -0
- package/src/auth/bindings.ts +3 -12
- package/src/auth/identity.ts +33 -0
- package/src/auth/index.ts +5 -0
- package/src/auth/otp-capability.ts +119 -0
- package/src/auth/otp-seal.test.ts +61 -0
- package/src/auth/otp-seal.ts +84 -0
- package/src/auth/schema.ts +3 -0
- package/src/auth/sessions.ts +26 -27
- package/src/auth/tables.ts +4 -0
- package/src/auth/verification.ts +61 -1
- package/src/cli/db-seed.ts +359 -0
- package/src/cli/db.test.ts +341 -3
- package/src/cli/db.ts +75 -8
- package/src/cli/dev-app-runner.ts +4 -0
- package/src/cli/docker.ts +4 -1
- package/src/cli/load-config.images.test.ts +26 -0
- package/src/cli/load-config.ts +10 -2
- package/src/cli/registry.ts +38 -2
- package/src/compiler/effects-infer.ts +1 -0
- package/src/config/index.ts +4 -0
- package/src/console/server/operator-db.ts +34 -9
- package/src/docker/compose.ts +162 -6
- package/src/docker/derive.ts +60 -3
- package/src/docker/docker.test.ts +374 -1
- package/src/docker/helpers.ts +2 -0
- package/src/docker/index.ts +11 -0
- package/src/docker/recipes/caddy.ts +51 -0
- package/src/docker/recipes/dragonfly.ts +31 -0
- package/src/docker/recipes/index.ts +25 -2
- package/src/docker/recipes/pgdog.ts +84 -0
- package/src/docker/recipes/redis.ts +6 -3
- package/src/docker/recipes/traefik.ts +83 -0
- package/src/docker/recipes/valkey.ts +30 -0
- package/src/docker/stack-id.ts +5 -0
- package/src/docker/types.ts +18 -0
- package/src/drivers/channel-sently.test.ts +8 -0
- package/src/drivers/channel-taqnyat-mail.ts +34 -0
- package/src/drivers/channel-taqnyat-whatsapp.ts +94 -0
- package/src/drivers/channel-types.ts +72 -0
- package/src/drivers/clock-postgres.test.ts +258 -0
- package/src/drivers/clock-postgres.ts +410 -0
- package/src/drivers/index.ts +18 -0
- package/src/drivers/journal-postgres.test.ts +175 -0
- package/src/drivers/journal-postgres.ts +492 -0
- package/src/elements/channel/otp-delivery.test.ts +76 -0
- package/src/elements/channel/otp-delivery.ts +291 -0
- package/src/elements/channel/runtime.ts +203 -114
- package/src/elements/channel.test.ts +71 -0
- package/src/elements/channel.ts +12 -2
- package/src/elements/clock/chaos-child.ts +280 -41
- package/src/elements/clock/durable.ts +7 -0
- package/src/elements/clock/reconcile.ts +2 -2
- package/src/elements/clock/runtime.ts +5 -3
- package/src/elements/clock.ts +1 -1
- package/src/elements/store/seed.test.ts +27 -0
- package/src/elements/store/seed.ts +68 -0
- package/src/elements/store/sql-session.test.ts +39 -0
- package/src/elements/store/sql-session.ts +55 -0
- package/src/elements/store/upsert-app.test.ts +103 -0
- package/src/elements/store.ts +5 -0
- package/src/index.ts +18 -0
- package/src/kernel/app.ts +221 -14
- package/src/kernel/boot-bind/channel.test.ts +16 -0
- package/src/kernel/boot-bind/channel.ts +64 -0
- package/src/kernel/boot-bind/clock.ts +17 -6
- package/src/kernel/boot-bind/gate.ts +14 -19
- package/src/kernel/boot-bind/honor-config.test.ts +123 -4
- package/src/kernel/boot-bind/journal.ts +89 -0
- package/src/kernel/boot-bind/signal.ts +20 -0
- package/src/kernel/boot-bind/store.test.ts +82 -0
- package/src/kernel/boot-bind/store.ts +22 -0
- package/src/kernel/boot.test.ts +6 -4
- package/src/kernel/boot.ts +53 -13
- package/src/kernel/concurrency.ts +1 -1
- package/src/kernel/fx.test.ts +9 -0
- package/src/kernel/fx.ts +175 -5
- package/src/kernel/graceful-shutdown.test.ts +76 -0
- package/src/kernel/graceful-shutdown.ts +106 -0
- package/src/kernel/horizontal-child.ts +257 -0
- package/src/kernel/horizontal.integration.test.ts +229 -0
- package/src/kernel/index.ts +14 -0
- package/src/kernel/journal-boot.test.ts +397 -0
- package/src/kernel/journal-suspend.ts +35 -0
- package/src/kernel/journal.test.ts +142 -0
- package/src/kernel/journal.ts +202 -27
- package/src/kernel/ready.test.ts +76 -0
- package/src/plugins/auth-delivery.mailpit.integration.test.ts +5 -5
- package/src/plugins/auth-methods.security.test.ts +20 -27
- package/src/plugins/auth-methods.test.ts +7 -6
- package/src/plugins/index.ts +12 -8
- package/src/plugins/magic-link.ts +1 -23
- package/src/plugins/otp.test.ts +236 -0
- package/src/plugins/otp.ts +570 -0
- package/src/plugins/taqnyat.live.test.ts +172 -0
- package/src/release/official-plugins.ts +1 -2
- package/site/content/docs/plugins/email-otp.mdx +0 -117
- package/site/content/docs/plugins/phone-number.mdx +0 -111
- package/src/plugins/email-otp.ts +0 -214
- package/src/plugins/phone-number.ts +0 -149
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type ClockDecl,
|
|
14
14
|
type ClockRuntime,
|
|
15
15
|
} from "../../elements/clock.ts";
|
|
16
|
+
import { createPostgresCronStore } from "../../drivers/clock-postgres.ts";
|
|
16
17
|
import { resolveDriverId, type ConfigEnv } from "../../config/index.ts";
|
|
17
18
|
import type { BootOptions } from "../boot.ts";
|
|
18
19
|
|
|
@@ -40,8 +41,7 @@ export function resolveClockDriverId(options: BootOptions, env: ConfigEnv): stri
|
|
|
40
41
|
/**
|
|
41
42
|
* Construct / adopt a Clock runtime, register decls, reconcile.
|
|
42
43
|
*
|
|
43
|
-
* Supported ids: `frozen` · `memory` · `file
|
|
44
|
-
* fail loud — there is no postgres CronStore.
|
|
44
|
+
* Supported ids: `frozen` · `memory` · `file` · `postgres`.
|
|
45
45
|
*
|
|
46
46
|
* @param options - Boot options
|
|
47
47
|
* @param env - Active environment
|
|
@@ -76,12 +76,23 @@ export async function bindClock(
|
|
|
76
76
|
store: createFileCronStore(path),
|
|
77
77
|
});
|
|
78
78
|
} else if (clockDriver === "postgres") {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
const url = process.env.DATABASE_URL ?? process.env.OKE_STORE_SQL_URL ?? undefined;
|
|
80
|
+
if (!url) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
env === "docker"
|
|
83
|
+
? 'oke boot: clock driver "postgres" needs DATABASE_URL (did `oke dev -d` write docker/.env.docker?)'
|
|
84
|
+
: 'oke boot: clock driver "postgres" needs DATABASE_URL',
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
const store = await createPostgresCronStore({ url });
|
|
88
|
+
clock = createClockRuntime({
|
|
89
|
+
instanceId: options.instanceId,
|
|
90
|
+
now,
|
|
91
|
+
store,
|
|
92
|
+
});
|
|
82
93
|
} else {
|
|
83
94
|
throw new Error(
|
|
84
|
-
`oke boot: unknown clock driver "${clockDriver}" (expected memory · file · frozen)`,
|
|
95
|
+
`oke boot: unknown clock driver "${clockDriver}" (expected memory · file · postgres · frozen)`,
|
|
85
96
|
);
|
|
86
97
|
}
|
|
87
98
|
|
|
@@ -33,15 +33,14 @@ export function resolveGateKvDriverId(
|
|
|
33
33
|
return docker ? "redis" : "memory";
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
function kvUrlFor(docker: boolean): string
|
|
36
|
+
function kvUrlFor(docker: boolean): string {
|
|
37
37
|
const url = process.env.REDIS_URL ?? process.env.OKE_STORE_KV_URL ?? undefined;
|
|
38
38
|
if (!url) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
"oke boot: gate redis kv needs REDIS_URL (did `oke dev -d` write docker/.env.docker?)"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return undefined;
|
|
39
|
+
throw new Error(
|
|
40
|
+
docker
|
|
41
|
+
? "oke boot: gate redis kv needs REDIS_URL (did `oke dev -d` write docker/.env.docker?)"
|
|
42
|
+
: "oke boot: gate redis kv needs REDIS_URL — declare drivers.store.kv memory for local, or set REDIS_URL",
|
|
43
|
+
);
|
|
45
44
|
}
|
|
46
45
|
return url;
|
|
47
46
|
}
|
|
@@ -65,19 +64,15 @@ export async function bindGate(
|
|
|
65
64
|
|
|
66
65
|
let kvNs;
|
|
67
66
|
if (kvId === "redis") {
|
|
67
|
+
// Fail loud when redis is configured without URL/inject — never soft-fallback
|
|
68
|
+
// to memory (that silently doubles rate budgets under horizontal scale).
|
|
68
69
|
const url = injected ? undefined : kvUrlFor(docker);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
name: GATE_KV_NAMESPACE,
|
|
76
|
-
...(url !== undefined ? { url } : {}),
|
|
77
|
-
...(injected !== undefined ? { client: injected } : {}),
|
|
78
|
-
nowMs: now,
|
|
79
|
-
});
|
|
80
|
-
}
|
|
70
|
+
kvNs = await redisDriver.open({
|
|
71
|
+
name: GATE_KV_NAMESPACE,
|
|
72
|
+
...(url !== undefined ? { url } : {}),
|
|
73
|
+
...(injected !== undefined ? { client: injected } : {}),
|
|
74
|
+
nowMs: now,
|
|
75
|
+
});
|
|
81
76
|
} else {
|
|
82
77
|
kvNs = await memoryDrivers.kv.open({ name: GATE_KV_NAMESPACE, nowMs: now });
|
|
83
78
|
}
|
|
@@ -15,6 +15,7 @@ import { gate } from "../../elements/gate.ts";
|
|
|
15
15
|
import { signal } from "../../elements/signal.ts";
|
|
16
16
|
import { vault } from "../../elements/vault.ts";
|
|
17
17
|
import { buildVaultBootChain } from "../../elements/vault/boot-chain.ts";
|
|
18
|
+
import { flow } from "../flow.ts";
|
|
18
19
|
import { bootApplication } from "../boot.ts";
|
|
19
20
|
|
|
20
21
|
describe("boot binders honour drivers.* config", () => {
|
|
@@ -126,19 +127,119 @@ describe("boot binders honour drivers.* config", () => {
|
|
|
126
127
|
}
|
|
127
128
|
});
|
|
128
129
|
|
|
129
|
-
test("clock: drivers.clock postgres fails
|
|
130
|
+
test("clock: drivers.clock postgres fails without DATABASE_URL", async () => {
|
|
131
|
+
const prevDb = process.env.DATABASE_URL;
|
|
132
|
+
const prevStore = process.env.OKE_STORE_SQL_URL;
|
|
133
|
+
delete process.env.DATABASE_URL;
|
|
134
|
+
delete process.env.OKE_STORE_SQL_URL;
|
|
135
|
+
try {
|
|
136
|
+
await expect(
|
|
137
|
+
bootApplication({
|
|
138
|
+
env: "local",
|
|
139
|
+
startScheduler: false,
|
|
140
|
+
clocks: [clock("tick", { every: "1h" })],
|
|
141
|
+
config: {
|
|
142
|
+
drivers: {
|
|
143
|
+
clock: { local: "postgres" },
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
}),
|
|
147
|
+
).rejects.toThrow(/clock driver "postgres" needs DATABASE_URL/);
|
|
148
|
+
} finally {
|
|
149
|
+
if (prevDb !== undefined) process.env.DATABASE_URL = prevDb;
|
|
150
|
+
else delete process.env.DATABASE_URL;
|
|
151
|
+
if (prevStore !== undefined) process.env.OKE_STORE_SQL_URL = prevStore;
|
|
152
|
+
else delete process.env.OKE_STORE_SQL_URL;
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("journal: durable flow binds memory journal by default", async () => {
|
|
157
|
+
const result = await bootApplication({
|
|
158
|
+
env: "local",
|
|
159
|
+
startScheduler: false,
|
|
160
|
+
flows: [flow({ name: "charge", durable: true, do: () => ({ ok: true }) })],
|
|
161
|
+
});
|
|
162
|
+
try {
|
|
163
|
+
expect(result.journal?.driverId).toBe("memory");
|
|
164
|
+
expect(typeof result.journal?.instanceId).toBe("string");
|
|
165
|
+
expect(result.journal?.leaseMs).toBeGreaterThan(0);
|
|
166
|
+
} finally {
|
|
167
|
+
await result.close();
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("journal: no durable flow → no journal runtime", async () => {
|
|
172
|
+
const result = await bootApplication({
|
|
173
|
+
env: "local",
|
|
174
|
+
startScheduler: false,
|
|
175
|
+
flows: [flow({ name: "plain", do: () => ({ ok: true }) })],
|
|
176
|
+
});
|
|
177
|
+
try {
|
|
178
|
+
expect(result.journal).toBeUndefined();
|
|
179
|
+
} finally {
|
|
180
|
+
await result.close();
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("journal: drivers.journal file binds file store", async () => {
|
|
185
|
+
tmp = await mkdtemp(join(tmpdir(), "oke-journal-file-"));
|
|
186
|
+
process.chdir(tmp);
|
|
187
|
+
const result = await bootApplication({
|
|
188
|
+
env: "local",
|
|
189
|
+
startScheduler: false,
|
|
190
|
+
flows: [flow({ name: "charge", durable: true, do: () => ({ ok: true }) })],
|
|
191
|
+
config: {
|
|
192
|
+
drivers: {
|
|
193
|
+
journal: { local: "file", docker: "postgres", test: "memory", prod: "postgres" },
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
try {
|
|
198
|
+
expect(result.journal?.driverId).toBe("file");
|
|
199
|
+
} finally {
|
|
200
|
+
await result.close();
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("journal: drivers.journal postgres fails without DATABASE_URL", async () => {
|
|
205
|
+
const prevDb = process.env.DATABASE_URL;
|
|
206
|
+
const prevStore = process.env.OKE_STORE_SQL_URL;
|
|
207
|
+
delete process.env.DATABASE_URL;
|
|
208
|
+
delete process.env.OKE_STORE_SQL_URL;
|
|
209
|
+
try {
|
|
210
|
+
await expect(
|
|
211
|
+
bootApplication({
|
|
212
|
+
env: "local",
|
|
213
|
+
startScheduler: false,
|
|
214
|
+
flows: [flow({ name: "charge", durable: true, do: () => ({ ok: true }) })],
|
|
215
|
+
config: {
|
|
216
|
+
drivers: {
|
|
217
|
+
journal: { local: "postgres" },
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
}),
|
|
221
|
+
).rejects.toThrow(/journal driver "postgres" needs DATABASE_URL/);
|
|
222
|
+
} finally {
|
|
223
|
+
if (prevDb !== undefined) process.env.DATABASE_URL = prevDb;
|
|
224
|
+
else delete process.env.DATABASE_URL;
|
|
225
|
+
if (prevStore !== undefined) process.env.OKE_STORE_SQL_URL = prevStore;
|
|
226
|
+
else delete process.env.OKE_STORE_SQL_URL;
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("journal: unknown driver fails loud", async () => {
|
|
130
231
|
await expect(
|
|
131
232
|
bootApplication({
|
|
132
233
|
env: "local",
|
|
133
234
|
startScheduler: false,
|
|
134
|
-
|
|
235
|
+
flows: [flow({ name: "charge", durable: true, do: () => ({ ok: true }) })],
|
|
135
236
|
config: {
|
|
136
237
|
drivers: {
|
|
137
|
-
|
|
238
|
+
journal: { local: "neon" },
|
|
138
239
|
},
|
|
139
240
|
},
|
|
140
241
|
}),
|
|
141
|
-
).rejects.toThrow(/
|
|
242
|
+
).rejects.toThrow(/unknown journal driver "neon"/);
|
|
142
243
|
});
|
|
143
244
|
|
|
144
245
|
test("gate: drivers.store.kv redis opens redis-backed oke:gates", async () => {
|
|
@@ -162,6 +263,24 @@ describe("boot binders honour drivers.* config", () => {
|
|
|
162
263
|
}
|
|
163
264
|
});
|
|
164
265
|
|
|
266
|
+
test("gate: drivers.store.kv redis without REDIS_URL fails loud (never soft memory)", async () => {
|
|
267
|
+
delete process.env.REDIS_URL;
|
|
268
|
+
delete process.env.OKE_STORE_KV_URL;
|
|
269
|
+
await expect(
|
|
270
|
+
bootApplication({
|
|
271
|
+
env: "local",
|
|
272
|
+
gates: [gate.rate({ max: 10, per: "1m" })],
|
|
273
|
+
config: {
|
|
274
|
+
drivers: {
|
|
275
|
+
store: {
|
|
276
|
+
kv: { local: "redis", docker: "redis", prod: "redis" },
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
}),
|
|
281
|
+
).rejects.toThrow(/gate redis kv needs REDIS_URL/);
|
|
282
|
+
});
|
|
283
|
+
|
|
165
284
|
test("gate: drivers.store.kv memory keeps memory oke:gates", async () => {
|
|
166
285
|
const result = await bootApplication({
|
|
167
286
|
env: "local",
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy journal binder — loaded only when a flow declares `durable: true`.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { mkdirSync } from "node:fs";
|
|
6
|
+
import { dirname, resolve } from "node:path";
|
|
7
|
+
import {
|
|
8
|
+
createFileJournalStore,
|
|
9
|
+
createMemoryJournalStore,
|
|
10
|
+
JOURNAL_DEFAULT_LEASE_MS,
|
|
11
|
+
type JournalStore,
|
|
12
|
+
} from "../journal.ts";
|
|
13
|
+
import { createPostgresJournalStore } from "../../drivers/journal-postgres.ts";
|
|
14
|
+
import { resolveDriverId, type ConfigEnv } from "../../config/index.ts";
|
|
15
|
+
import type { BootOptions } from "../boot.ts";
|
|
16
|
+
|
|
17
|
+
/** Bound journal runtime — store + this instance's lease identity. */
|
|
18
|
+
export interface JournalRuntime {
|
|
19
|
+
readonly store: JournalStore;
|
|
20
|
+
readonly instanceId: string;
|
|
21
|
+
readonly leaseMs: number;
|
|
22
|
+
readonly driverId: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Result of binding a journal runtime. */
|
|
26
|
+
export interface BindJournalResult {
|
|
27
|
+
readonly journal: JournalRuntime;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Default on-disk journal path (single-host file driver). */
|
|
31
|
+
export const DEFAULT_FILE_JOURNAL_PATH = ".oke/journal.json";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Resolve `drivers.journal` for the active env (default `memory`).
|
|
35
|
+
*
|
|
36
|
+
* @param options - Boot options
|
|
37
|
+
* @param env - Active environment
|
|
38
|
+
*/
|
|
39
|
+
export function resolveJournalDriverId(options: BootOptions, env: ConfigEnv): string {
|
|
40
|
+
const resolved = resolveDriverId(options.config?.drivers?.journal, env);
|
|
41
|
+
if (resolved) return resolved;
|
|
42
|
+
return "memory";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Construct a journal store. Supported ids: `memory` · `file` · `postgres`.
|
|
47
|
+
*
|
|
48
|
+
* @param options - Boot options
|
|
49
|
+
* @param env - Active environment
|
|
50
|
+
*/
|
|
51
|
+
export async function bindJournal(
|
|
52
|
+
options: BootOptions,
|
|
53
|
+
env: ConfigEnv,
|
|
54
|
+
): Promise<BindJournalResult> {
|
|
55
|
+
const driver = resolveJournalDriverId(options, env);
|
|
56
|
+
const instanceId = options.instanceId ?? `inst-${crypto.randomUUID()}`;
|
|
57
|
+
|
|
58
|
+
let store: JournalStore;
|
|
59
|
+
if (driver === "memory") {
|
|
60
|
+
store = createMemoryJournalStore();
|
|
61
|
+
} else if (driver === "file") {
|
|
62
|
+
const path = resolve(process.cwd(), DEFAULT_FILE_JOURNAL_PATH);
|
|
63
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
64
|
+
store = createFileJournalStore(path);
|
|
65
|
+
} else if (driver === "postgres") {
|
|
66
|
+
const url = process.env.DATABASE_URL ?? process.env.OKE_STORE_SQL_URL ?? undefined;
|
|
67
|
+
if (!url) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
env === "docker"
|
|
70
|
+
? 'oke boot: journal driver "postgres" needs DATABASE_URL (did `oke dev -d` write docker/.env.docker?)'
|
|
71
|
+
: 'oke boot: journal driver "postgres" needs DATABASE_URL',
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
store = await createPostgresJournalStore({ url });
|
|
75
|
+
} else {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`oke boot: unknown journal driver "${driver}" (expected memory · file · postgres)`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
journal: {
|
|
83
|
+
store,
|
|
84
|
+
instanceId,
|
|
85
|
+
leaseMs: options.journalLeaseMs ?? JOURNAL_DEFAULT_LEASE_MS,
|
|
86
|
+
driverId: driver,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -29,6 +29,25 @@ function redisUrlFor(docker: boolean): string | undefined {
|
|
|
29
29
|
return url;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
let signalRedisWarned = false;
|
|
33
|
+
|
|
34
|
+
/** One-shot: redis Signal is emit-relay + process-local outbox consume today. */
|
|
35
|
+
function warnSignalRedisProcessLocal(): void {
|
|
36
|
+
if (signalRedisWarned) return;
|
|
37
|
+
signalRedisWarned = true;
|
|
38
|
+
console.warn(
|
|
39
|
+
"oke boot: drivers.signal redis — emit relays to Redis, but consume/live/drain use a " +
|
|
40
|
+
"process-local outbox (not multi-instance competing consumers). Prefer a shared durable " +
|
|
41
|
+
"outbox path for multi-process tests, or a single consumer instance, until Redis Streams " +
|
|
42
|
+
"consume ships.",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Test helper — reset the one-shot redis Signal warn. */
|
|
47
|
+
export function resetSignalRedisWarnForTests(): void {
|
|
48
|
+
signalRedisWarned = false;
|
|
49
|
+
}
|
|
50
|
+
|
|
32
51
|
/**
|
|
33
52
|
* Construct a Signal runtime, register decls / binding names, start the bus.
|
|
34
53
|
*
|
|
@@ -59,6 +78,7 @@ export async function bindSignal(
|
|
|
59
78
|
break;
|
|
60
79
|
case "redis": {
|
|
61
80
|
const redis = injectedRedis ?? createBunSignalRedisClient(redisUrlFor(docker));
|
|
81
|
+
warnSignalRedisProcessLocal();
|
|
62
82
|
signal = createSignalRuntime({
|
|
63
83
|
driver: redisSignalDriver,
|
|
64
84
|
now,
|
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
6
|
+
import { files } from "../../elements/store.ts";
|
|
6
7
|
import {
|
|
8
|
+
bindStore,
|
|
7
9
|
indexDriverFor,
|
|
10
|
+
resetFilesFsWarnForTests,
|
|
8
11
|
resolveFilesDriverId,
|
|
9
12
|
resolveIndexDriverId,
|
|
10
13
|
resolveKvDriverId,
|
|
@@ -77,6 +80,85 @@ describe("bindStore driver resolution", () => {
|
|
|
77
80
|
});
|
|
78
81
|
});
|
|
79
82
|
|
|
83
|
+
describe("bindStore files fs multi-instance warn", () => {
|
|
84
|
+
afterEach(() => {
|
|
85
|
+
resetFilesFsWarnForTests();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("warns once when files driver is fs", () => {
|
|
89
|
+
const warnings: string[] = [];
|
|
90
|
+
const prev = console.warn;
|
|
91
|
+
console.warn = (...args: unknown[]) => {
|
|
92
|
+
warnings.push(args.map(String).join(" "));
|
|
93
|
+
};
|
|
94
|
+
try {
|
|
95
|
+
const uploads = files("uploads");
|
|
96
|
+
bindStore(
|
|
97
|
+
{
|
|
98
|
+
stores: [uploads],
|
|
99
|
+
config: {
|
|
100
|
+
drivers: {
|
|
101
|
+
store: {
|
|
102
|
+
files: { local: "fs", docker: "s3", prod: "s3" },
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
"local",
|
|
108
|
+
() => Date.now(),
|
|
109
|
+
false,
|
|
110
|
+
);
|
|
111
|
+
bindStore(
|
|
112
|
+
{
|
|
113
|
+
stores: [uploads],
|
|
114
|
+
config: {
|
|
115
|
+
drivers: {
|
|
116
|
+
store: {
|
|
117
|
+
files: { local: "fs", docker: "s3", prod: "s3" },
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
"local",
|
|
123
|
+
() => Date.now(),
|
|
124
|
+
false,
|
|
125
|
+
);
|
|
126
|
+
expect(warnings.some((w) => w.includes('drivers.store.files "fs"'))).toBe(true);
|
|
127
|
+
expect(warnings.filter((w) => w.includes('drivers.store.files "fs"'))).toHaveLength(1);
|
|
128
|
+
} finally {
|
|
129
|
+
console.warn = prev;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("does not warn for memory or s3 files", () => {
|
|
134
|
+
const warnings: string[] = [];
|
|
135
|
+
const prev = console.warn;
|
|
136
|
+
console.warn = (...args: unknown[]) => {
|
|
137
|
+
warnings.push(args.map(String).join(" "));
|
|
138
|
+
};
|
|
139
|
+
try {
|
|
140
|
+
bindStore(
|
|
141
|
+
{
|
|
142
|
+
stores: [files("uploads")],
|
|
143
|
+
config: {
|
|
144
|
+
drivers: {
|
|
145
|
+
store: {
|
|
146
|
+
files: { local: "memory", docker: "s3", prod: "s3" },
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
"local",
|
|
152
|
+
() => Date.now(),
|
|
153
|
+
false,
|
|
154
|
+
);
|
|
155
|
+
expect(warnings.some((w) => w.includes('drivers.store.files "fs"'))).toBe(false);
|
|
156
|
+
} finally {
|
|
157
|
+
console.warn = prev;
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
80
162
|
describe("bindStore index driver resolution", () => {
|
|
81
163
|
const prevIndex = process.env.OKE_INDEX_DRIVER;
|
|
82
164
|
|
|
@@ -29,6 +29,27 @@ import type { BootOptions } from "../boot.ts"; // type-only — no cycle at runt
|
|
|
29
29
|
* @param now - Clock
|
|
30
30
|
* @param docker - Prefer compose URLs when opening postgres/redis
|
|
31
31
|
*/
|
|
32
|
+
let filesFsWarned = false;
|
|
33
|
+
|
|
34
|
+
/** Test helper — reset the one-shot `fs` multi-instance warn. */
|
|
35
|
+
export function resetFilesFsWarnForTests(): void {
|
|
36
|
+
filesFsWarned = false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Warn once when `drivers.store.files` is `fs` — per-process (or per-pod)
|
|
41
|
+
* filesystem visibility under horizontal scale.
|
|
42
|
+
*/
|
|
43
|
+
function warnFilesFsMultiInstance(): void {
|
|
44
|
+
if (filesFsWarned) return;
|
|
45
|
+
filesFsWarned = true;
|
|
46
|
+
console.warn(
|
|
47
|
+
'oke boot: drivers.store.files "fs" is single-host — each instance sees its own ' +
|
|
48
|
+
"filesystem (temp root when unbound). For horizontal scale use drivers.store.files s3 " +
|
|
49
|
+
"(or an explicitly shared volume root, still single-host semantics).",
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
32
53
|
export function bindStore(
|
|
33
54
|
options: BootOptions,
|
|
34
55
|
env: ConfigEnv,
|
|
@@ -39,6 +60,7 @@ export function bindStore(
|
|
|
39
60
|
const kvId = resolveKvDriverId(options, env, docker);
|
|
40
61
|
const filesId = resolveFilesDriverId(options, env, docker);
|
|
41
62
|
const indexId = resolveIndexDriverId(options, env, docker);
|
|
63
|
+
if (filesId === "fs") warnFilesFsMultiInstance();
|
|
42
64
|
const sqlUrl = sqlUrlFor(sqlId, docker);
|
|
43
65
|
const kvUrl = kvUrlFor(kvId, docker);
|
|
44
66
|
const filesRoot = filesRootFor(filesId);
|
package/src/kernel/boot.test.ts
CHANGED
|
@@ -79,7 +79,7 @@ describe("boot — lazy element needs", () => {
|
|
|
79
79
|
expect(needs.signal).toBe(false);
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
-
test("oke() Store-only graph stays under the prior
|
|
82
|
+
test("oke() Store-only graph stays under the prior 53 kB baseline", async () => {
|
|
83
83
|
const dir = await mkdtemp(join(tmpdir(), "oke-store-only-"));
|
|
84
84
|
const entry = join(dir, "entry.ts");
|
|
85
85
|
const appPath = join(import.meta.dir, "app.ts");
|
|
@@ -116,9 +116,11 @@ describe("boot — lazy element needs", () => {
|
|
|
116
116
|
if (raw.byteLength === 0) continue;
|
|
117
117
|
total += Bun.gzipSync(new Uint8Array(raw)).byteLength;
|
|
118
118
|
}
|
|
119
|
-
// Rebased after
|
|
120
|
-
//
|
|
121
|
-
|
|
119
|
+
// Rebased after fx.sendOtp/verifyOtp on the shared fx surface (~50.1 kB
|
|
120
|
+
// gzip), durable-journal lease surface (~51.2 kB), then fx.deliverOtp +
|
|
121
|
+
// unified otp() plugin wiring on the shared surface (~50.9 kB → 53 kB
|
|
122
|
+
// cap). Clock/channel/journal drivers stay lazy-bound; far below eager bind.
|
|
123
|
+
expect(total).toBeLessThan(53_000);
|
|
122
124
|
} finally {
|
|
123
125
|
await rm(dir, { recursive: true, force: true });
|
|
124
126
|
}
|