@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-framework",
3
- "version": "0.310.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.310.0",
202
- "@cosmicdrift/kumiko-types": "0.310.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.310.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 waitForCondition(() => jobSawFilesRef.length > 0);
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 waitForCondition(() => consumedNotes.includes("written from the worker"));
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 waitForCondition(() => jobSawFilesRef.length > 0);
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
- for (let i = 0; i < 20 && !hasTmp && !alreadyDone; i++) {
219
- await new Promise((r) => setTimeout(r, 10));
220
- const dirContents = await readdir(basePath);
221
- hasTmp = dirContents.some((f) => f.endsWith(".tmp"));
222
- alreadyDone = dirContents.includes("streamed.bin");
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
- await proxiedDb.close();
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, 2000);
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, 5000);
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, 2000);
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, 10000);
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
- }