@cosmicdrift/kumiko-dev-server 0.126.0 → 0.127.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-dev-server",
3
- "version": "0.126.0",
3
+ "version": "0.127.0",
4
4
  "description": "Development server bootstrap for Kumiko apps. Bundles the client, mints dev-JWTs, injects the resolved AppSchema, and seeds an admin. Not for production.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -50,8 +50,8 @@
50
50
  "kumiko-schema-check": "./bin/kumiko-schema-check.ts"
51
51
  },
52
52
  "dependencies": {
53
- "@cosmicdrift/kumiko-bundled-features": "0.126.0",
54
- "@cosmicdrift/kumiko-framework": "0.126.0",
53
+ "@cosmicdrift/kumiko-bundled-features": "0.127.0",
54
+ "@cosmicdrift/kumiko-framework": "0.127.0",
55
55
  "ts-morph": "^28.0.0"
56
56
  },
57
57
  "publishConfig": {
@@ -0,0 +1,26 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { createJobsFeature } from "@cosmicdrift/kumiko-bundled-features/jobs";
3
+ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
4
+ import { createRegistry, defineFeature } from "@cosmicdrift/kumiko-framework/engine";
5
+ import { jobRunLoggerCallbacks } from "../job-run-logger";
6
+
7
+ // ponytail: callbacks shape only — no DB I/O; createTestDb needs TEST_DATABASE_URL (CI unit job has none).
8
+ const mockDb = {} as DbConnection;
9
+
10
+ describe("jobRunLoggerCallbacks", () => {
11
+ test("returns undefined when jobs feature is not registered", () => {
12
+ const registry = createRegistry([
13
+ defineFeature("empty", () => ({ handlers: {}, queries: {} })),
14
+ ]);
15
+ expect(jobRunLoggerCallbacks(registry, mockDb)).toBeUndefined();
16
+ });
17
+
18
+ test("returns logger callbacks when jobs feature is registered", () => {
19
+ const registry = createRegistry([createJobsFeature()]);
20
+ const callbacks = jobRunLoggerCallbacks(registry, mockDb);
21
+ expect(callbacks).toBeDefined();
22
+ expect(typeof callbacks?.onJobStart).toBe("function");
23
+ expect(typeof callbacks?.onJobComplete).toBe("function");
24
+ expect(typeof callbacks?.onJobFailed).toBe("function");
25
+ });
26
+ });
@@ -0,0 +1,51 @@
1
+ import { createJobRunLogger } from "@cosmicdrift/kumiko-bundled-features/jobs";
2
+ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
3
+ import type { Registry } from "@cosmicdrift/kumiko-framework/engine";
4
+ import type { JobRunIn } from "@cosmicdrift/kumiko-framework/engine/types";
5
+ import { createJobRunner, type JobRunner } from "@cosmicdrift/kumiko-framework/jobs";
6
+
7
+ export function jobRunLoggerCallbacks(
8
+ registry: Registry,
9
+ db: DbConnection,
10
+ ): ReturnType<typeof createJobRunLogger> | undefined {
11
+ if (registry.getFeature("jobs") === undefined) return undefined;
12
+ return createJobRunLogger({ db, registry });
13
+ }
14
+
15
+ /** Dev-server parity: consume api + worker lanes when jobs are registered. */
16
+ export async function startDevJobRunners(opts: {
17
+ readonly registry: Registry;
18
+ readonly db: DbConnection;
19
+ readonly context: Record<string, unknown>;
20
+ readonly redisUrl: string;
21
+ }): Promise<{ readonly runners: readonly JobRunner[]; readonly stop: () => Promise<void> }> {
22
+ const jobs = [...opts.registry.getAllJobs().values()];
23
+ if (opts.registry.getFeature("jobs") === undefined || jobs.length === 0) {
24
+ return { runners: [], stop: async () => {} };
25
+ }
26
+
27
+ const logger = createJobRunLogger({ db: opts.db, registry: opts.registry });
28
+ const runners: JobRunner[] = [];
29
+ const lanes = new Set(
30
+ jobs.map((j) => j.runIn).filter((lane): lane is JobRunIn => lane !== undefined),
31
+ );
32
+
33
+ for (const lane of lanes) {
34
+ const jr = createJobRunner({
35
+ registry: opts.registry,
36
+ context: { ...opts.context, db: opts.db },
37
+ redisUrl: opts.redisUrl,
38
+ consumerLane: lane,
39
+ ...logger,
40
+ });
41
+ await jr.start();
42
+ runners.push(jr);
43
+ }
44
+
45
+ return {
46
+ runners,
47
+ stop: async () => {
48
+ for (const runner of runners) await runner.stop();
49
+ },
50
+ };
51
+ }
@@ -29,6 +29,7 @@ import {
29
29
  type TestStackOptions,
30
30
  TestUsers,
31
31
  } from "@cosmicdrift/kumiko-framework/stack";
32
+ import { startDevJobRunners } from "./boot/job-run-logger";
32
33
  import { type ExtraRoutesSystemDeps, makeDispatchSystemWrite } from "./extra-routes-deps";
33
34
  import { injectSchema } from "./inject-schema";
34
35
  import { canResolveTailwindStylesheet, resolveTailwindCli } from "./resolve-tailwind-cli";
@@ -707,6 +708,14 @@ export async function createKumikoServer(
707
708
  await stack.eventDispatcher.start();
708
709
  }
709
710
 
711
+ const redisUrl = `redis://${stack.redis.redis.options.host}:${stack.redis.redis.options.port}/${stack.redis.redis.options.db}`;
712
+ const devJobRunners = await startDevJobRunners({
713
+ registry: stack.registry,
714
+ db: stack.db,
715
+ context: { db: stack.db, registry: stack.registry },
716
+ redisUrl,
717
+ });
718
+
710
719
  // Dev user = TestUsers.admin. Demo features are openToAll but the
711
720
  // auth-middleware still needs a valid JWT to let the request past.
712
721
  // Nicht genutzt wenn `options.auth` gesetzt ist — dann macht der Client
@@ -977,6 +986,7 @@ export async function createKumikoServer(
977
986
  if (stack.eventDispatcher) {
978
987
  await stack.eventDispatcher.stop();
979
988
  }
989
+ await devJobRunners.stop();
980
990
  await stack.cleanup();
981
991
  };
982
992
 
@@ -138,6 +138,7 @@ import { warnIfNonUtcServerTimeZone } from "@cosmicdrift/kumiko-framework/time";
138
138
  import Redis from "ioredis";
139
139
  import { applyBootSeeds } from "./boot/apply-boot-seeds";
140
140
  import { type BootCrypto, resolveBootCrypto } from "./boot/boot-crypto";
141
+ import { jobRunLoggerCallbacks } from "./boot/job-run-logger";
141
142
  import { ASSETS_DIR } from "./build-prod-bundle";
142
143
  import { buildComposeAuthOptions, composeFeatures } from "./compose-features";
143
144
  import { type ExtraRoutesSystemDeps, makeDispatchSystemWrite } from "./extra-routes-deps";
@@ -1133,6 +1134,7 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
1133
1134
  const runSingleInstance = options.runSingleInstance ?? options.eventDispatcher?.disabled !== true;
1134
1135
  const hasJobs = registry.getAllJobs().size > 0;
1135
1136
  const queueNamePrefix = options.jobs?.queueNamePrefix;
1137
+ const jobLogger = jobRunLoggerCallbacks(registry, db);
1136
1138
  const dispatcherTunables =
1137
1139
  options.eventDispatcher?.pollIntervalMs !== undefined
1138
1140
  ? { pollIntervalMs: options.eventDispatcher.pollIntervalMs }
@@ -1141,20 +1143,18 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
1141
1143
  const entrypoint: ApiEntrypoint | AllInOneEntrypoint = runSingleInstance
1142
1144
  ? createAllInOneEntrypoint({
1143
1145
  ...baseEntrypointOptions,
1144
- // Worker-Seite liest die JobsBlock TOP-LEVEL (nicht nested `jobs` wie
1145
- // die api-Seite); beide Lane-Runner ziehen redisUrl/prefix von hier.
1146
1146
  redisUrl,
1147
+ ...jobLogger,
1147
1148
  ...(queueNamePrefix !== undefined && { queueNamePrefix }),
1148
1149
  ...(!options.eventDispatcher?.disabled && { eventDispatcher: dispatcherTunables }),
1149
1150
  })
1150
1151
  : createApiEntrypoint({
1151
1152
  ...baseEntrypointOptions,
1152
- // API-only: api-Lane-Jobs laufen lokal, ein dezidierter Worker fährt
1153
- // worker-Lane + MSPs. createApiEntrypoint liest den nested `jobs`-Block.
1154
1153
  ...(hasJobs && {
1155
1154
  jobs: {
1156
1155
  redisUrl,
1157
1156
  runLocalJobs: true,
1157
+ ...jobLogger,
1158
1158
  ...(queueNamePrefix !== undefined && { queueNamePrefix }),
1159
1159
  },
1160
1160
  }),