@cosmicdrift/kumiko-dev-server 0.394.0 → 0.395.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.
@@ -1,4 +1,5 @@
1
1
  /// <reference types="bun-types" preserve="true" />
2
+ import type { RedisSseBroker } from "@cosmicdrift/kumiko-framework/api";
2
3
  import type { TestStack } from "@cosmicdrift/kumiko-testing/stack";
3
4
  import type { DevDbMode, DevJobRunners } from "./create-kumiko-server-stack.ts";
4
5
  import { type BunServer } from "./dev-server-runtime.ts";
@@ -9,6 +10,7 @@ export type DevServerStopDeps = {
9
10
  readonly stack: TestStack;
10
11
  readonly devJobRunners: DevJobRunners;
11
12
  readonly dbMode: DevDbMode;
13
+ readonly ownedRedisSseBroker: RedisSseBroker | undefined;
12
14
  };
13
15
  export declare function createDevServerStop(deps: DevServerStopDeps): () => Promise<void>;
14
16
  export declare function installDevSignalHandlers(stop: () => Promise<void>): void;
@@ -23,7 +23,7 @@ async function deleteEphemeralJobQueues(raw, jobQueueNamePrefix) {
23
23
  }
24
24
  }
25
25
  export function createDevServerStop(deps) {
26
- const { watcherAbort, killTailwind, server, stack, devJobRunners, dbMode } = deps;
26
+ const { watcherAbort, killTailwind, server, stack, devJobRunners, dbMode, ownedRedisSseBroker } = deps;
27
27
  return async () => {
28
28
  // Stop the watcher first so no onChange callback runs during teardown;
29
29
  // otherwise removing the temporary directory could trigger process.exit(75).
@@ -37,6 +37,7 @@ export function createDevServerStop(deps) {
37
37
  await stack.eventDispatcher.stop();
38
38
  }
39
39
  await devJobRunners.stop();
40
+ await ownedRedisSseBroker?.close();
40
41
  const rawRedis = dbMode.persistentDb
41
42
  ? undefined
42
43
  : stack.redis.redis.duplicate({ keyPrefix: "" });
@@ -1,3 +1,4 @@
1
+ import { type RedisSseBroker } from "@cosmicdrift/kumiko-framework/api";
1
2
  import { startDevJobRunners } from "@cosmicdrift/kumiko-server-runtime/boot/job-run-logger";
2
3
  import { type TestStack } from "@cosmicdrift/kumiko-testing/stack";
3
4
  import type { CreateKumikoServerOptions } from "./create-kumiko-server.ts";
@@ -5,8 +6,13 @@ export type DevDbMode = {
5
6
  readonly devDbName: string | undefined;
6
7
  readonly persistentDb: boolean;
7
8
  readonly jobQueueNamePrefix: string;
9
+ readonly sseChannelPrefix: string;
8
10
  };
9
11
  export type DevJobRunners = Awaited<ReturnType<typeof startDevJobRunners>>;
10
12
  export declare function resolveDevDbMode(processEnv: Record<string, string | undefined>): DevDbMode;
11
- export declare function bootDevStack(options: CreateKumikoServerOptions, dbMode: DevDbMode): Promise<TestStack>;
13
+ export type DevStackBoot = {
14
+ readonly stack: TestStack;
15
+ readonly ownedRedisSseBroker: RedisSseBroker | undefined;
16
+ };
17
+ export declare function bootDevStack(options: CreateKumikoServerOptions, dbMode: DevDbMode): Promise<DevStackBoot>;
12
18
  export declare function startDevBackgroundRunners(stack: TestStack, jobQueueNamePrefix: string): Promise<DevJobRunners>;
@@ -1,4 +1,5 @@
1
1
  import { resolveAnonymousAccessFromRegistry } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
2
+ import { createDefaultSseBroker, } from "@cosmicdrift/kumiko-framework/api";
2
3
  import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
3
4
  import { createMetricsForFactory } from "@cosmicdrift/kumiko-framework/observability";
4
5
  import { startDevJobRunners } from "@cosmicdrift/kumiko-server-runtime/boot/job-run-logger";
@@ -22,11 +23,15 @@ export function resolveDevDbMode(processEnv) {
22
23
  const jobQueueNamePrefix = persistentDb
23
24
  ? `kumiko-dev-${devDbName}`
24
25
  : `kumiko-dev-${crypto.randomUUID()}`;
25
- return { devDbName, persistentDb, jobQueueNamePrefix };
26
+ // Pub/Sub ignores the DB index, so the SSE channels need the same per-boot
27
+ // isolation as the queues; an explicit env value wins so a worker can be pointed at it.
28
+ const sseChannelPrefix = processEnv["KUMIKO_REDIS_CHANNEL_PREFIX"]?.trim() || jobQueueNamePrefix;
29
+ return { devDbName, persistentDb, jobQueueNamePrefix, sseChannelPrefix };
26
30
  }
27
- function buildDevStackOptions(options, dbMode) {
31
+ function buildDevStackOptions(options, dbMode, sseBroker) {
28
32
  return {
29
33
  features: options.features,
34
+ sseBroker,
30
35
  ...(dbMode.persistentDb && { dbName: dbMode.devDbName, persistentDb: true }),
31
36
  ...(options.auth !== undefined && { authConfig: options.auth }),
32
37
  ...(options.extraContext !== undefined && { extraContext: options.extraContext }),
@@ -48,9 +53,23 @@ function buildDevStackOptions(options, dbMode) {
48
53
  }
49
54
  export async function bootDevStack(options, dbMode) {
50
55
  logInfo(`[kumiko-server] booting Kumiko stack${dbMode.persistentDb ? ` — persistent DB "${dbMode.devDbName}"` : " — ephemeral test DB"}…`);
51
- const stack = await setupTestStack(buildDevStackOptions(options, dbMode));
52
- await createEventsTable(stack.unscopedDb);
53
- await pushEntityProjectionTables(stack, stack.registry);
56
+ // setupTestStack defaults to an in-memory broker, which would make this
57
+ // process ignore Redis: live signals published by a separate worker process
58
+ // would then never reach the browsers connected here.
59
+ const { sseBroker, ownedRedisSseBroker } = createDefaultSseBroker({
60
+ ...process.env,
61
+ KUMIKO_REDIS_CHANNEL_PREFIX: dbMode.sseChannelPrefix,
62
+ });
63
+ let stack;
64
+ try {
65
+ stack = await setupTestStack(buildDevStackOptions(options, dbMode, sseBroker));
66
+ await createEventsTable(stack.unscopedDb);
67
+ await pushEntityProjectionTables(stack, stack.registry);
68
+ }
69
+ catch (err) {
70
+ await ownedRedisSseBroker?.close();
71
+ throw err;
72
+ }
54
73
  // `extraRoutes` is already mounted by setupTestStack's buildServer();
55
74
  // `wire` is for co-running components that need the system-write
56
75
  // dispatcher but don't declare a route.
@@ -73,7 +92,7 @@ export async function bootDevStack(options, dbMode) {
73
92
  if (options.onAfterSetup !== undefined) {
74
93
  await options.onAfterSetup(stack);
75
94
  }
76
- return stack;
95
+ return { stack, ownedRedisSseBroker };
77
96
  }
78
97
  // setupTestStack configures but does not start the event dispatcher so
79
98
  // integration tests can drain it deterministically with runOnce(). The dev
@@ -203,5 +203,8 @@ export type KumikoServerHandle = {
203
203
  /** BullMQ queue-name prefix of this boot: per-boot in ephemeral mode, so
204
204
  * its `bull:<prefix>-*` keys belong to this server alone on a shared Redis. */
205
205
  readonly jobQueueNamePrefix: string;
206
+ /** KUMIKO_REDIS_CHANNEL_PREFIX value of this boot's SSE broker; a separate worker
207
+ * must set the same value for its live signals to reach this server. */
208
+ readonly sseChannelPrefix: string;
206
209
  };
207
210
  export declare function createKumikoServer(options: CreateKumikoServerOptions): Promise<KumikoServerHandle>;
@@ -47,7 +47,7 @@ export async function createKumikoServer(options) {
47
47
  const stylesheet = await startDevStylesheet(options);
48
48
  const templates = await loadHtmlTemplates(entries, options.htmlPath);
49
49
  const dbMode = resolveDevDbMode(process.env);
50
- const stack = await bootDevStack(options, dbMode);
50
+ const { stack, ownedRedisSseBroker } = await bootDevStack(options, dbMode);
51
51
  const devJobRunners = await startDevBackgroundRunners(stack, dbMode.jobQueueNamePrefix);
52
52
  // Built once purely to surface authoring warnings (settings-hub placement
53
53
  // etc.) at the same point prod would; clients fetch the schema themselves
@@ -75,6 +75,7 @@ export async function createKumikoServer(options) {
75
75
  stack,
76
76
  devJobRunners,
77
77
  dbMode,
78
+ ownedRedisSseBroker,
78
79
  });
79
80
  startDevFileWatchers({
80
81
  entries,
@@ -94,5 +95,12 @@ export async function createKumikoServer(options) {
94
95
  ? ` (hot reload on ${entries.length === 1 ? "client entry" : `${entries.length} entries`})`
95
96
  : ""));
96
97
  }
97
- return { fetch: handleFetch, server, stack, stop, jobQueueNamePrefix: dbMode.jobQueueNamePrefix };
98
+ return {
99
+ fetch: handleFetch,
100
+ server,
101
+ stack,
102
+ stop,
103
+ jobQueueNamePrefix: dbMode.jobQueueNamePrefix,
104
+ sseChannelPrefix: dbMode.sseChannelPrefix,
105
+ };
98
106
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-dev-server",
3
- "version": "0.394.0",
3
+ "version": "0.395.0",
4
4
  "description": "Dev-tooling for Kumiko apps: local dev-server bootstrap (runDevApp), scaffolding. Its compose-stacks/env-schema subpaths are consumed at prod boot too — see @cosmicdrift/kumiko-server-runtime for the prod runner itself.",
5
5
  "license": "MIT",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -77,15 +77,15 @@
77
77
  "kumiko-upgrade": "./bin/kumiko-upgrade.ts"
78
78
  },
79
79
  "dependencies": {
80
- "@cosmicdrift/kumiko-bundled-features": "0.394.0",
81
- "@cosmicdrift/kumiko-feature-ast": "0.394.0",
82
- "@cosmicdrift/kumiko-framework": "0.394.0",
83
- "@cosmicdrift/kumiko-renderer": "0.394.0",
84
- "@cosmicdrift/kumiko-renderer-mail-html": "0.394.0",
85
- "@cosmicdrift/kumiko-guards": "0.394.0",
86
- "@cosmicdrift/kumiko-server-runtime": "0.394.0",
87
- "@cosmicdrift/kumiko-testing": "0.394.0",
88
- "@cosmicdrift/kumiko-types": "0.394.0",
80
+ "@cosmicdrift/kumiko-bundled-features": "0.395.0",
81
+ "@cosmicdrift/kumiko-feature-ast": "0.395.0",
82
+ "@cosmicdrift/kumiko-framework": "0.395.0",
83
+ "@cosmicdrift/kumiko-renderer": "0.395.0",
84
+ "@cosmicdrift/kumiko-renderer-mail-html": "0.395.0",
85
+ "@cosmicdrift/kumiko-guards": "0.395.0",
86
+ "@cosmicdrift/kumiko-server-runtime": "0.395.0",
87
+ "@cosmicdrift/kumiko-testing": "0.395.0",
88
+ "@cosmicdrift/kumiko-types": "0.395.0",
89
89
  "ts-morph": "^28.0.0",
90
90
  "zod": "^4.6.7"
91
91
  },
package/src/changes.json CHANGED
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.395.0",
4
+ "type": "fix",
5
+ "title": "Live updates from a separate worker process reach the dev-server browser",
6
+ "detail": "The dev server always used an in-memory SSE broker, so it never subscribed to Redis. When a separate worker process won the shared event cursor it published live signals to Redis, and they never reached the browser. The dev server now uses the Redis-backed broker when REDIS_URL is set, and setupTestStack accepts an optional sseBroker. Its channels are prefixed per boot (KUMIKO_REDIS_CHANNEL_PREFIX if set, otherwise derived from the dev DB mode), so parallel dev servers on one Redis stay apart; a separate worker must set the same KUMIKO_REDIS_CHANNEL_PREFIX (the handle exposes it as sseChannelPrefix)."
7
+ },
2
8
  {
3
9
  "version": "0.394.0",
4
10
  "type": "improvement",