@cosmicdrift/kumiko-server-runtime 0.193.1 → 0.195.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-server-runtime",
3
- "version": "0.193.1",
3
+ "version": "0.195.0",
4
4
  "description": "Production server-boot runtime for Kumiko apps: connections, schema-drift-gate, seeds, lifecycle, graceful shutdown. Symmetric to kumiko-dev-server's runDevApp, without dev/scaffold/codegen tooling.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -76,8 +76,8 @@
76
76
  }
77
77
  },
78
78
  "dependencies": {
79
- "@cosmicdrift/kumiko-bundled-features": "0.193.1",
80
- "@cosmicdrift/kumiko-framework": "0.193.1",
79
+ "@cosmicdrift/kumiko-bundled-features": "0.195.0",
80
+ "@cosmicdrift/kumiko-framework": "0.195.0",
81
81
  "temporal-polyfill": "^0.3.2"
82
82
  },
83
83
  "publishConfig": {
@@ -1,8 +1,8 @@
1
- // injectSchema teilt sich dev-server (every HTML response) und prod-
2
- // server (static-fallback index.html) Pfad. Bug hier wäre stiller
3
- // Production-Fail: createKumikoApp findet `window.__KUMIKO_SCHEMA__`
4
- // nicht und mountet leer. Tests pinnen die Idempotenz + die zwei
5
- // Insertion-Punkte (vor /client.js-Tag oder vor </body>).
1
+ // injectSchema is shared by the dev-server (every HTML response) and
2
+ // prod-server (static-fallback index.html) paths. A bug here would be a
3
+ // silent production failure: createKumikoApp wouldn't find
4
+ // `window.__KUMIKO_SCHEMA__` and would mount empty. Tests pin idempotency
5
+ // plus the two insertion points (before the /client.js tag or before </body>).
6
6
 
7
7
  import { describe, expect, test } from "bun:test";
8
8
  import { injectSchema } from "../inject-schema";
@@ -15,11 +15,20 @@ describe("injectSchema", () => {
15
15
  const html = '<html><body><script src="/client.js" defer></script></body></html>';
16
16
  const out = injectSchema(html, SCHEMA);
17
17
  expect(out).toContain(TAG);
18
- // Schema MUSS vor dem Client-Bundle stehensonst läuft
19
- // createKumikoApp() bevor window.__KUMIKO_SCHEMA__ gesetzt ist.
18
+ // Schema MUST come before the client bundle otherwise createKumikoApp()
19
+ // runs before window.__KUMIKO_SCHEMA__ is set.
20
20
  expect(out.indexOf(TAG)).toBeLessThan(out.indexOf('<script src="/client.js"'));
21
21
  });
22
22
 
23
+ test("HTML mit module-form /client.js-Tag: Schema-Tag wird DAVOR eingefügt", () => {
24
+ const html = '<html><body><script type="module" src="/client.js"></script></body></html>';
25
+ const out = injectSchema(html, SCHEMA);
26
+ expect(out).toContain(TAG);
27
+ // Dev templates use `type="module"`, not the classic form — insertion
28
+ // must match this tag pattern too.
29
+ expect(out.indexOf(TAG)).toBeLessThan(out.indexOf('<script type="module" src="/client.js"'));
30
+ });
31
+
23
32
  test("HTML ohne /client.js-Tag: Schema-Tag wird vor </body> eingefügt", () => {
24
33
  const html = "<html><body><div id=root></div></body></html>";
25
34
  const out = injectSchema(html, SCHEMA);
@@ -36,8 +45,8 @@ describe("injectSchema", () => {
36
45
  test("Idempotent: bei bereits injectem Schema kein zweiter Tag", () => {
37
46
  const html = `<html><body>${TAG}</body></html>`;
38
47
  const out = injectSchema(html, '{"features":[{"differentSchema":true}]}');
39
- // Original-Tag bleibt, kein zweiter Tag hinzugefügtder Marker-
40
- // Check verhindert sonst stacking-Tags bei repeated reads.
48
+ // Original tag stays, no second tag addedthe marker check otherwise
49
+ // prevents stacking tags on repeated reads.
41
50
  expect(out).toBe(html);
42
51
  });
43
52
 
@@ -53,10 +62,39 @@ describe("injectSchema", () => {
53
62
  });
54
63
  const html = '<html><body><script src="/client.js"></script></body></html>';
55
64
  const out = injectSchema(html, complex);
56
- // Sanity: das injected Skript muss valid JS sein (Object-Literal-
57
- // Syntax, keine HTML-Reserved-Chars im JSON die den <script>-Block
58
- // brechen würden — JSON.stringify entkommt /, < usw. nicht, aber
59
- // die Standard-Chars die wir hier nutzen sind unkritisch).
65
+ // Sanity: the injected script must be valid JS (object-literal syntax,
66
+ // no HTML-reserved chars in the JSON that would break the <script>
67
+ // block — JSON.stringify doesn't escape /, < etc., but the standard
68
+ // chars used here are harmless).
60
69
  expect(out).toContain(`window.__KUMIKO_SCHEMA__=${complex}`);
61
70
  });
71
+
72
+ test("Schema mit $-Replacement-Patterns bleibt byte-identisch (kein HTML-Splicing)", () => {
73
+ // String.prototype.replace() interprets $$, $&, $`, $' specially in the
74
+ // REPLACEMENT argument. The injected schema JSON string lands there — an
75
+ // i18n label containing "$'" or similar would otherwise splice parts of
76
+ // `html` into the script tag. A replacer-function argument sidesteps that.
77
+ const dollarSchema = '{"label":"$\' tail $$ amp $& end"}';
78
+ const dollarTag = `<script>window.__KUMIKO_SCHEMA__=${dollarSchema};</script>`;
79
+ const html = '<html><body><script src="/client.js"></script></body></html>';
80
+ const out = injectSchema(html, dollarSchema);
81
+ // Insertion point is right before the /client.js tag, not the start of
82
+ // the document — built via indexOf/slice, not .replace(), so the
83
+ // expected value doesn't itself run through the same $-pattern footgun.
84
+ const clientScriptIdx = html.indexOf('<script src="/client.js"');
85
+ const expected = html.slice(0, clientScriptIdx) + dollarTag + html.slice(clientScriptIdx);
86
+ expect(out).toBe(expected);
87
+ });
88
+
89
+ test("Schema mit $-Replacement-Patterns, Insertion vor </body>", () => {
90
+ const dollarSchema = '{"label":"$\' tail $$ amp $& end"}';
91
+ const dollarTag = `<script>window.__KUMIKO_SCHEMA__=${dollarSchema};</script>`;
92
+ const html = "<html><body><div id=root></div></body></html>";
93
+ const out = injectSchema(html, dollarSchema);
94
+ // Built via slice/concat, not .replace() — the expected value must not
95
+ // itself run through the same $-pattern footgun the fix removes.
96
+ const bodyIdx = html.indexOf("</body>");
97
+ const expected = html.slice(0, bodyIdx) + dollarTag + html.slice(bodyIdx);
98
+ expect(out).toBe(expected);
99
+ });
62
100
  });
@@ -0,0 +1,77 @@
1
+ // Regression guard for startDevJobRunners' lane default: a job registered
2
+ // WITHOUT an explicit runIn used to get no consumer at all in the dev server
3
+ // (the old `lanes` filter dropped `undefined` lanes), so it sat in the queue
4
+ // forever. `runners.length` alone can't catch that — a lane with no consumer
5
+ // still produces a runner object. This dispatches a real job and waits for
6
+ // its side effect to prove the lane is actually being consumed.
7
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
8
+ import {
9
+ createJobsFeature,
10
+ jobRunLogsTable,
11
+ jobRunsTable,
12
+ } from "@cosmicdrift/kumiko-bundled-features/jobs";
13
+ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
14
+ import { createRegistry, defineFeature } from "@cosmicdrift/kumiko-framework/engine";
15
+ import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
16
+ import {
17
+ createTestDb,
18
+ createTestRedis,
19
+ type TestDb,
20
+ type TestRedis,
21
+ unsafePushTables,
22
+ } from "@cosmicdrift/kumiko-framework/stack";
23
+ import { waitFor } from "@cosmicdrift/kumiko-framework/testing";
24
+ import { startDevJobRunners } from "../job-run-logger";
25
+
26
+ let testDb: TestDb;
27
+ let testRedis: TestRedis;
28
+ let db: DbConnection;
29
+
30
+ const jobRuns: Array<{ readonly note: string }> = [];
31
+
32
+ // No `runIn` — the exact case the lane-default regression hits.
33
+ const probeFeature = defineFeature("probe", (r) => {
34
+ r.job("noteIt", { trigger: { manual: true } }, async (payload) => {
35
+ jobRuns.push({ note: (payload as { note: string }).note });
36
+ });
37
+ });
38
+
39
+ beforeAll(async () => {
40
+ testDb = await createTestDb();
41
+ testRedis = await createTestRedis();
42
+ db = testDb.db;
43
+ await unsafePushTables(db, { jobRunsTable, jobRunLogsTable });
44
+ await createEventsTable(db);
45
+ });
46
+
47
+ afterAll(async () => {
48
+ await testDb.cleanup();
49
+ await testRedis.cleanup();
50
+ });
51
+
52
+ describe("startDevJobRunners", () => {
53
+ test("job without explicit runIn gets a consumer and actually executes", async () => {
54
+ const registry = createRegistry([probeFeature, createJobsFeature()]);
55
+
56
+ const { runners, stop } = await startDevJobRunners({
57
+ registry,
58
+ db,
59
+ context: {},
60
+ redisUrl: testRedis.redisUrl,
61
+ });
62
+
63
+ try {
64
+ expect(runners.length).toBe(1);
65
+ const runner = runners[0];
66
+ if (runner === undefined) throw new Error("expected a runner");
67
+
68
+ await runner.dispatch("probe:job:note-it", { note: "hello-from-dev-runner" });
69
+
70
+ await waitFor(() => {
71
+ expect(jobRuns.some((r) => r.note === "hello-from-dev-runner")).toBe(true);
72
+ });
73
+ } finally {
74
+ await stop();
75
+ }
76
+ });
77
+ });
@@ -11,7 +11,7 @@
11
11
  // wenn nur clientEntry da ist und kein eigenes CSS)
12
12
  // public/ → rsync 1:1 (kein Hash — User-bewusste URLs)
13
13
  // public/index.html | index.html → Template, Placeholder-Tags ersetzt:
14
- // <script src="/client.js"> → /assets/client-<hash>.js
14
+ // <script type="module" src="/client.js"> → /assets/client-<hash>.js
15
15
  // <link href="/styles.css"> → /assets/styles-<hash>.css
16
16
  // (kein HTML, vanilla) → Default-HTML ohne Asset-Tags
17
17
  //
@@ -569,8 +569,8 @@ function buildMissingTemplateError(manifest: BuildManifest, entry: ClientEntry):
569
569
  //
570
570
  // Convention: das HTML-Template MUSS Placeholder-Tags für jedes Asset
571
571
  // dieses Entries enthalten:
572
- // - `<script src="/client.js">` für single-mode entry "client"
573
- // - `<script src="/client-<name>.js">` für multi-mode entry "<name>"
572
+ // - `<script type="module" src="/client.js">` für single-mode entry "client"
573
+ // - `<script type="module" src="/client-<name>.js">` für multi-mode entry "<name>"
574
574
  // - `<link href="/styles.css">` für styles (gemeinsam über alle entries)
575
575
  // Der Build ersetzt sie durch die gehashten URLs.
576
576
  //
@@ -20,11 +20,14 @@ function scriptSafeJsonHtml(json: string): string {
20
20
  return json.replace(/</g, "\\u003c");
21
21
  }
22
22
 
23
+ const CLIENT_SCRIPT_TAG_RE = /<script\b[^>]*\ssrc="\/client\.js"/;
24
+
23
25
  export function injectSchema(html: string, schemaJson: string): string {
24
26
  if (html.includes("__KUMIKO_SCHEMA__")) return html;
25
27
  const tag = `<script>window.__KUMIKO_SCHEMA__=${scriptSafeJsonHtml(schemaJson)};</script>`;
26
- if (html.includes('<script src="/client.js"')) {
27
- return html.replace('<script src="/client.js"', `${tag}<script src="/client.js"`);
28
+ const clientScriptMatch = html.match(CLIENT_SCRIPT_TAG_RE);
29
+ if (clientScriptMatch) {
30
+ return html.replace(CLIENT_SCRIPT_TAG_RE, (m) => `${tag}${m}`);
28
31
  }
29
- return html.includes("</body>") ? html.replace("</body>", `${tag}</body>`) : html + tag;
32
+ return html.includes("</body>") ? html.replace("</body>", () => `${tag}</body>`) : html + tag;
30
33
  }