@cosmicdrift/kumiko-framework 0.310.0 → 0.311.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 +4 -4
- package/src/entrypoint/__tests__/split-deploy.integration.test.ts +4 -11
- package/src/files/__tests__/write-stream.test.ts +10 -6
- package/src/jobs/__tests__/jobs.integration.test.ts +2 -0
- package/src/lifecycle/__tests__/signal-handlers.test.ts +1 -9
- package/src/pipeline/__tests__/event-dispatcher-db-outage.integration.test.ts +6 -1
- package/src/pipeline/__tests__/event-dispatcher-lifecycle.integration.test.ts +3 -14
- package/src/pipeline/__tests__/event-dispatcher-second-audit.integration.test.ts +5 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.311.0",
|
|
4
4
|
"description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -198,8 +198,8 @@
|
|
|
198
198
|
"./package.json": "./package.json"
|
|
199
199
|
},
|
|
200
200
|
"dependencies": {
|
|
201
|
-
"@cosmicdrift/kumiko-http": "0.
|
|
202
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
201
|
+
"@cosmicdrift/kumiko-http": "0.311.0",
|
|
202
|
+
"@cosmicdrift/kumiko-types": "0.311.0",
|
|
203
203
|
"bullmq": "^5.76.7",
|
|
204
204
|
"bun-types": "^1.3.13",
|
|
205
205
|
"hono": "^4.13.1",
|
|
@@ -215,7 +215,7 @@
|
|
|
215
215
|
"zod": "^4.4.3"
|
|
216
216
|
},
|
|
217
217
|
"devDependencies": {
|
|
218
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
218
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.311.0",
|
|
219
219
|
"bun-types": "^1.3.13",
|
|
220
220
|
"pino-pretty": "^13.1.3"
|
|
221
221
|
},
|
|
@@ -20,6 +20,7 @@ import { createRegistry, defineFeature } from "../../engine";
|
|
|
20
20
|
import { createArchivedStreamsTable } from "../../event-store";
|
|
21
21
|
import { createEventConsumerStateTable } from "../../pipeline";
|
|
22
22
|
import { createTestRedis, type TestRedis, TestUsers } from "../../stack";
|
|
23
|
+
import { waitFor } from "../../testing";
|
|
23
24
|
import { createAllInOneEntrypoint, createApiEntrypoint, createWorkerEntrypoint } from "../index";
|
|
24
25
|
|
|
25
26
|
const splitFeature = defineFeature("split", (r) => {
|
|
@@ -103,14 +104,6 @@ const fileJobFeature = defineFeature("fileJob", (r) => {
|
|
|
103
104
|
r.multiStreamProjection({ name: "noop", apply: { [requested.name]: async () => {} } });
|
|
104
105
|
});
|
|
105
106
|
|
|
106
|
-
async function waitForCondition(check: () => boolean, timeoutMs = 5000): Promise<void> {
|
|
107
|
-
const deadline = Date.now() + timeoutMs;
|
|
108
|
-
while (!check()) {
|
|
109
|
-
if (Date.now() > deadline) throw new Error("waitForCondition: timed out");
|
|
110
|
-
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
107
|
const JWT = "split-deploy-test-secret-must-be-32-chars!!";
|
|
115
108
|
|
|
116
109
|
// Per-test queue-name with a random suffix. Date.now() alone collided
|
|
@@ -195,7 +188,7 @@ describe("entrypoint factories", () => {
|
|
|
195
188
|
{ storageKey: "some/key.pdf" },
|
|
196
189
|
TestUsers.admin,
|
|
197
190
|
);
|
|
198
|
-
await
|
|
191
|
+
await waitFor(() => jobSawFilesRef.length > 0);
|
|
199
192
|
expect(jobSawFilesRef[0]).toBe("function");
|
|
200
193
|
} finally {
|
|
201
194
|
await worker.stop();
|
|
@@ -239,7 +232,7 @@ describe("entrypoint factories", () => {
|
|
|
239
232
|
// The worker's own eventDispatcher (started above) picks the event
|
|
240
233
|
// back up and runs the MSP — proves the afterCommit/MSP chain works
|
|
241
234
|
// inside the worker lane, not just that the write itself succeeded.
|
|
242
|
-
await
|
|
235
|
+
await waitFor(() => consumedNotes.includes("written from the worker"));
|
|
243
236
|
} finally {
|
|
244
237
|
await worker.stop();
|
|
245
238
|
}
|
|
@@ -264,7 +257,7 @@ describe("entrypoint factories", () => {
|
|
|
264
257
|
{ storageKey: "some/key.pdf" },
|
|
265
258
|
TestUsers.admin,
|
|
266
259
|
);
|
|
267
|
-
await
|
|
260
|
+
await waitFor(() => jobSawFilesRef.length > 0);
|
|
268
261
|
expect(jobSawFilesRef[0]).toBe("function");
|
|
269
262
|
} finally {
|
|
270
263
|
await entry.stop();
|
|
@@ -16,6 +16,7 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
|
16
16
|
import { mkdtemp, readdir, rm, stat } from "node:fs/promises";
|
|
17
17
|
import { tmpdir } from "node:os";
|
|
18
18
|
import { join } from "node:path";
|
|
19
|
+
import { waitFor } from "../../testing";
|
|
19
20
|
import { createInMemoryFileProvider } from "../in-memory-provider";
|
|
20
21
|
import { createLocalProvider } from "../local-provider";
|
|
21
22
|
|
|
@@ -215,12 +216,15 @@ describe("FileStorageProvider.writeStream — Streaming-Property", () => {
|
|
|
215
216
|
// Poll bis tmp existiert ODER final schon fertig (zu schneller CI).
|
|
216
217
|
let hasTmp = false;
|
|
217
218
|
let alreadyDone = false;
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
219
|
+
await waitFor(
|
|
220
|
+
async () => {
|
|
221
|
+
const dirContents = await readdir(basePath);
|
|
222
|
+
hasTmp = dirContents.some((f) => f.endsWith(".tmp"));
|
|
223
|
+
alreadyDone = dirContents.includes("streamed.bin");
|
|
224
|
+
return hasTmp || alreadyDone;
|
|
225
|
+
},
|
|
226
|
+
{ delays: Array(20).fill(10) },
|
|
227
|
+
);
|
|
224
228
|
if (alreadyDone && !hasTmp) {
|
|
225
229
|
throw new Error(
|
|
226
230
|
"slowChunks-delay zu kurz fuer CI: writeStream war fertig bevor poll start. " +
|
|
@@ -439,6 +439,7 @@ describe("concurrency: skip", () => {
|
|
|
439
439
|
|
|
440
440
|
// Try dispatching multiple times while first is running
|
|
441
441
|
let skippedCount = 0;
|
|
442
|
+
// @timeout-exception: #3118 paces repeated dispatch attempts, nothing to poll for
|
|
442
443
|
for (let i = 0; i < 5; i++) {
|
|
443
444
|
await sleep(50);
|
|
444
445
|
const id = await runner.dispatch("test:job:skip-job", { n: i + 2 });
|
|
@@ -1212,6 +1213,7 @@ describe("handleEvent maxPerTenant", () => {
|
|
|
1212
1213
|
// racing against the handler's own 500ms sleep, which could land
|
|
1213
1214
|
// outside the window on a starved runner and silently pass.
|
|
1214
1215
|
await runner.handleEvent("test:write:capped-event", { n: 3 }, user);
|
|
1216
|
+
// @timeout-exception: #3118 asserts a negative (stays capped) at each checkpoint
|
|
1215
1217
|
for (const delayMs of [50, 100, 200, 400]) {
|
|
1216
1218
|
await sleep(delayMs);
|
|
1217
1219
|
const startedNow = jobLog.filter((e) => e.name === "test:job:per-tenant-limited-event");
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, expect, mock, test } from "bun:test";
|
|
2
|
+
import { waitFor } from "../../testing";
|
|
2
3
|
import { createLifecycle } from "../lifecycle";
|
|
3
4
|
import { attachSignalHandlers } from "../signal-handlers";
|
|
4
5
|
import { createTestLifecycle } from "./create-test-lifecycle";
|
|
@@ -95,12 +96,3 @@ describe("attachSignalHandlers", () => {
|
|
|
95
96
|
expect(process.listenerCount("SIGTERM")).toBe(before);
|
|
96
97
|
});
|
|
97
98
|
});
|
|
98
|
-
|
|
99
|
-
async function waitFor(predicate: () => boolean, timeoutMs = 500): Promise<void> {
|
|
100
|
-
const deadline = Date.now() + timeoutMs;
|
|
101
|
-
while (Date.now() < deadline) {
|
|
102
|
-
if (predicate()) return;
|
|
103
|
-
await new Promise((r) => setTimeout(r, 5));
|
|
104
|
-
}
|
|
105
|
-
throw new Error("waitFor timed out");
|
|
106
|
-
}
|
|
@@ -186,7 +186,12 @@ describe("E: dispatcher survives a DB outage and logs the recovery", () => {
|
|
|
186
186
|
} finally {
|
|
187
187
|
await dispatcher.stop();
|
|
188
188
|
await listenClient.end({ timeout: 1 });
|
|
189
|
-
|
|
189
|
+
// Destroy instead of a graceful end(): when proxy2.stop() kills a socket
|
|
190
|
+
// mid-query, postgres.js keeps that query referenced on the dead
|
|
191
|
+
// connection, and a plain end() then waits for a resolver only
|
|
192
|
+
// terminate() fires, so it never returns. The pool is test-owned and
|
|
193
|
+
// every assertion has already run, so dropping in-flight work is safe.
|
|
194
|
+
await proxiedDb.client.end({ timeout: 0 });
|
|
190
195
|
}
|
|
191
196
|
});
|
|
192
197
|
});
|
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
TestUsers,
|
|
32
32
|
unsafeCreateEntityTable,
|
|
33
33
|
} from "../../stack";
|
|
34
|
-
import { sharedWidgetEntity, sharedWidgetTable } from "../../testing";
|
|
34
|
+
import { sharedWidgetEntity, sharedWidgetTable, waitFor } from "../../testing";
|
|
35
35
|
|
|
36
36
|
// --- Test fixtures ---
|
|
37
37
|
|
|
@@ -118,7 +118,7 @@ describe("E.1 — .start() lifecycle + slow handler", () => {
|
|
|
118
118
|
|
|
119
119
|
// pollIntervalMs in the test-stack is 50ms. Give the timer a few
|
|
120
120
|
// ticks to observe the event.
|
|
121
|
-
await waitFor(() => observations.length >= 1,
|
|
121
|
+
await waitFor(() => observations.length >= 1, { delays: Array(40).fill(50) });
|
|
122
122
|
expect(observations).toHaveLength(1);
|
|
123
123
|
expect(observations[0]?.event.payload["name"]).toBe("started-delivery");
|
|
124
124
|
} finally {
|
|
@@ -140,7 +140,7 @@ describe("E.1 — .start() lifecycle + slow handler", () => {
|
|
|
140
140
|
await appendWidget("slow-3");
|
|
141
141
|
|
|
142
142
|
// Wait until all 3 slow-observer invocations have completed.
|
|
143
|
-
await waitFor(() => slowHandlerInvocations.length >= 3,
|
|
143
|
+
await waitFor(() => slowHandlerInvocations.length >= 3, { delays: Array(50).fill(100) });
|
|
144
144
|
|
|
145
145
|
// Check: no invocation overlapped with the next — every pass
|
|
146
146
|
// finished before the following one started. The guard does
|
|
@@ -207,14 +207,3 @@ describe("E.1 — consumer-lag metric", () => {
|
|
|
207
207
|
});
|
|
208
208
|
|
|
209
209
|
// --- Helpers ---
|
|
210
|
-
|
|
211
|
-
async function waitFor(predicate: () => boolean, timeoutMs: number): Promise<void> {
|
|
212
|
-
const start = Date.now();
|
|
213
|
-
while (Date.now() - start < timeoutMs) {
|
|
214
|
-
if (predicate()) return;
|
|
215
|
-
await new Promise((r) => setTimeout(r, 20));
|
|
216
|
-
}
|
|
217
|
-
if (!predicate()) {
|
|
218
|
-
throw new Error(`waitFor: predicate never became true within ${timeoutMs}ms`);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
TestUsers,
|
|
34
34
|
unsafeCreateEntityTable,
|
|
35
35
|
} from "../../stack";
|
|
36
|
-
import { sharedWidgetEntity } from "../../testing";
|
|
36
|
+
import { sharedWidgetEntity, waitFor } from "../../testing";
|
|
37
37
|
import { generateId } from "../../utils";
|
|
38
38
|
|
|
39
39
|
// --- Fixture ---
|
|
@@ -247,7 +247,7 @@ describe("Second audit — LISTEN gauge", () => {
|
|
|
247
247
|
).length;
|
|
248
248
|
|
|
249
249
|
// Wait for the initial onlisten (gauge.set 1) to land.
|
|
250
|
-
await waitFor(() => connectsWithValue1() >= 1,
|
|
250
|
+
await waitFor(() => connectsWithValue1() >= 1, { delays: Array(40).fill(50) });
|
|
251
251
|
const initialConnects = connectsWithValue1();
|
|
252
252
|
expect(initialConnects).toBeGreaterThanOrEqual(1);
|
|
253
253
|
|
|
@@ -266,7 +266,9 @@ describe("Second audit — LISTEN gauge", () => {
|
|
|
266
266
|
|
|
267
267
|
// Wait for the SECOND gauge.set(1) — the reconnect. Generous timeout
|
|
268
268
|
// because postgres.js's reconnect loop includes backoff.
|
|
269
|
-
await waitFor(() => connectsWithValue1() > initialConnects,
|
|
269
|
+
await waitFor(() => connectsWithValue1() > initialConnects, {
|
|
270
|
+
delays: Array(50).fill(200),
|
|
271
|
+
});
|
|
270
272
|
expect(connectsWithValue1()).toBeGreaterThan(initialConnects);
|
|
271
273
|
} finally {
|
|
272
274
|
await recStack.eventDispatcher?.stop();
|
|
@@ -278,14 +280,3 @@ describe("Second audit — LISTEN gauge", () => {
|
|
|
278
280
|
});
|
|
279
281
|
|
|
280
282
|
// --- Helpers ---
|
|
281
|
-
|
|
282
|
-
async function waitFor(predicate: () => boolean, timeoutMs: number): Promise<void> {
|
|
283
|
-
const start = Date.now();
|
|
284
|
-
while (Date.now() - start < timeoutMs) {
|
|
285
|
-
if (predicate()) return;
|
|
286
|
-
await new Promise((r) => setTimeout(r, 25));
|
|
287
|
-
}
|
|
288
|
-
if (!predicate()) {
|
|
289
|
-
throw new Error(`waitFor: predicate never became true within ${timeoutMs}ms`);
|
|
290
|
-
}
|
|
291
|
-
}
|