@cosmicdrift/kumiko-server-runtime 0.231.0 → 0.232.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.
|
|
3
|
+
"version": "0.232.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>",
|
|
@@ -80,8 +80,8 @@
|
|
|
80
80
|
}
|
|
81
81
|
},
|
|
82
82
|
"dependencies": {
|
|
83
|
-
"@cosmicdrift/kumiko-bundled-features": "0.
|
|
84
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
83
|
+
"@cosmicdrift/kumiko-bundled-features": "0.232.0",
|
|
84
|
+
"@cosmicdrift/kumiko-framework": "0.232.0",
|
|
85
85
|
"temporal-polyfill": "^0.3.2"
|
|
86
86
|
},
|
|
87
87
|
"publishConfig": {
|
|
@@ -4,23 +4,38 @@
|
|
|
4
4
|
// forever. `runners.length` alone can't catch that — a lane with no consumer
|
|
5
5
|
// still produces a runner object. This dispatches a real job and waits for
|
|
6
6
|
// its side effect to prove the lane is actually being consumed.
|
|
7
|
-
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
7
|
+
import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
|
|
8
8
|
import {
|
|
9
9
|
createJobsFeature,
|
|
10
10
|
jobRunLogsTable,
|
|
11
11
|
jobRunsTable,
|
|
12
12
|
} from "@cosmicdrift/kumiko-bundled-features/jobs";
|
|
13
13
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
buildEntityTable,
|
|
16
|
+
createEventStoreExecutor,
|
|
17
|
+
selectMany,
|
|
18
|
+
} from "@cosmicdrift/kumiko-framework/db";
|
|
19
|
+
import {
|
|
20
|
+
createEntity,
|
|
21
|
+
createRegistry,
|
|
22
|
+
createTextField,
|
|
23
|
+
defineFeature,
|
|
24
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
15
25
|
import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
26
|
+
import { createDispatcher } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
16
27
|
import {
|
|
17
28
|
createTestDb,
|
|
18
29
|
createTestRedis,
|
|
30
|
+
setupTestStack,
|
|
19
31
|
type TestDb,
|
|
20
32
|
type TestRedis,
|
|
33
|
+
TestUsers,
|
|
34
|
+
unsafeCreateEntityTable,
|
|
21
35
|
unsafePushTables,
|
|
22
36
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
23
37
|
import { waitFor } from "@cosmicdrift/kumiko-framework/testing";
|
|
38
|
+
import { z } from "zod";
|
|
24
39
|
import { startDevJobRunners } from "../job-run-logger";
|
|
25
40
|
|
|
26
41
|
let testDb: TestDb;
|
|
@@ -58,6 +73,10 @@ describe("startDevJobRunners", () => {
|
|
|
58
73
|
db,
|
|
59
74
|
context: {},
|
|
60
75
|
redisUrl: testRedis.redisUrl,
|
|
76
|
+
// Construction-only — this job never calls ctx.write/ctx.queryAs, so a
|
|
77
|
+
// dispatcher built against a bare `{}` context is enough to satisfy
|
|
78
|
+
// the (now required) opts.dispatcher without wiring a full stack.
|
|
79
|
+
dispatcher: createDispatcher(registry, {}),
|
|
61
80
|
});
|
|
62
81
|
|
|
63
82
|
try {
|
|
@@ -75,3 +94,101 @@ describe("startDevJobRunners", () => {
|
|
|
75
94
|
}
|
|
76
95
|
});
|
|
77
96
|
});
|
|
97
|
+
|
|
98
|
+
// kumiko-framework#2553: startDevJobRunners built its per-lane JobRunners but
|
|
99
|
+
// never called attachDispatcher() on them (unlike createApiEntrypoint /
|
|
100
|
+
// createWorkerEntrypoint / createAllInOneEntrypoint, which all do this
|
|
101
|
+
// automatically) — so ctx.write/ctx.queryAs inside a dev-run job always hit
|
|
102
|
+
// the "dispatcher attached — call attachDispatcher() first" stub. This
|
|
103
|
+
// mirrors create-kumiko-server.ts's real wiring (setupTestStack + a separate
|
|
104
|
+
// startDevJobRunners as the lane consumer) and drives a job that calls both
|
|
105
|
+
// ctx.write and ctx.queryAs end-to-end.
|
|
106
|
+
const attachNoteEntity = createEntity({
|
|
107
|
+
table: "job_attach_notes",
|
|
108
|
+
fields: { text: createTextField({ required: true }) },
|
|
109
|
+
});
|
|
110
|
+
const attachNoteTable = buildEntityTable("note", attachNoteEntity);
|
|
111
|
+
|
|
112
|
+
const attachResults: Array<{ writeOk: boolean; rowCount: number }> = [];
|
|
113
|
+
const attachFailures: string[] = [];
|
|
114
|
+
|
|
115
|
+
const attachDispatcherFeature = defineFeature("jobattach", (r) => {
|
|
116
|
+
r.entity("note", attachNoteEntity);
|
|
117
|
+
|
|
118
|
+
r.writeHandler(
|
|
119
|
+
"create",
|
|
120
|
+
z.object({ text: z.string() }),
|
|
121
|
+
async (event, ctx) => {
|
|
122
|
+
const crud = createEventStoreExecutor(attachNoteTable, attachNoteEntity, {
|
|
123
|
+
entityName: "note",
|
|
124
|
+
});
|
|
125
|
+
return crud.create(event.payload, event.user, ctx.db);
|
|
126
|
+
},
|
|
127
|
+
{ access: { openToAll: true } },
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
r.queryHandler("list", z.object({}), async (_query, ctx) => selectMany(ctx.db, attachNoteTable), {
|
|
131
|
+
access: { openToAll: true },
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
r.job("record", { trigger: { manual: true }, retries: 0 }, async (payload, ctx) => {
|
|
135
|
+
try {
|
|
136
|
+
const text = (payload as { text: string }).text;
|
|
137
|
+
const writeResult = await ctx.write("jobattach:write:create", { text });
|
|
138
|
+
const rows = await ctx.queryAs(TestUsers.admin, "jobattach:query:list", {});
|
|
139
|
+
attachResults.push({
|
|
140
|
+
writeOk: writeResult.isSuccess,
|
|
141
|
+
rowCount: (rows as unknown[]).length,
|
|
142
|
+
});
|
|
143
|
+
} catch (error) {
|
|
144
|
+
attachFailures.push(error instanceof Error ? error.message : String(error));
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("startDevJobRunners attaches the dispatcher (kumiko-framework#2553)", () => {
|
|
151
|
+
afterEach(() => {
|
|
152
|
+
attachResults.length = 0;
|
|
153
|
+
attachFailures.length = 0;
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("a dev-run job's ctx.write and ctx.queryAs both succeed", async () => {
|
|
157
|
+
const stack = await setupTestStack({
|
|
158
|
+
features: [attachDispatcherFeature, createJobsFeature()],
|
|
159
|
+
// Enqueuer-only — startDevJobRunners below is the sole consumer,
|
|
160
|
+
// mirroring create-kumiko-server.ts.
|
|
161
|
+
jobs: {},
|
|
162
|
+
});
|
|
163
|
+
await unsafeCreateEntityTable(stack.db, attachNoteEntity);
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const { runners, stop } = await startDevJobRunners({
|
|
167
|
+
registry: stack.registry,
|
|
168
|
+
db: stack.db,
|
|
169
|
+
context: stack.context,
|
|
170
|
+
redisUrl: stack.redis.redisUrl,
|
|
171
|
+
dispatcher: stack.dispatcher,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const runner = runners[0];
|
|
176
|
+
if (runner === undefined) throw new Error("expected a runner");
|
|
177
|
+
|
|
178
|
+
await runner.dispatch("jobattach:job:record", { text: "hello-from-dev-runner" });
|
|
179
|
+
|
|
180
|
+
await waitFor(() => {
|
|
181
|
+
expect(attachResults.length + attachFailures.length).toBeGreaterThan(0);
|
|
182
|
+
});
|
|
183
|
+
if (attachFailures.length > 0) {
|
|
184
|
+
throw new Error(`job failed: ${attachFailures.join(", ")}`);
|
|
185
|
+
}
|
|
186
|
+
expect(attachResults[0]).toEqual({ writeOk: true, rowCount: 1 });
|
|
187
|
+
} finally {
|
|
188
|
+
await stop();
|
|
189
|
+
}
|
|
190
|
+
} finally {
|
|
191
|
+
await stack.cleanup();
|
|
192
|
+
}
|
|
193
|
+
}, 15000);
|
|
194
|
+
});
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { createJobRunLogger } from "@cosmicdrift/kumiko-bundled-features/jobs";
|
|
2
2
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
3
3
|
import type { Registry } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
AppContext,
|
|
6
|
+
DispatchWriteRef,
|
|
7
|
+
JobRunIn,
|
|
8
|
+
} from "@cosmicdrift/kumiko-framework/engine/types";
|
|
5
9
|
import { createJobRunner, type JobRunner } from "@cosmicdrift/kumiko-framework/jobs";
|
|
10
|
+
import type { Dispatcher } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
6
11
|
|
|
7
12
|
export function jobRunLoggerCallbacks(
|
|
8
13
|
registry: Registry,
|
|
@@ -12,12 +17,25 @@ export function jobRunLoggerCallbacks(
|
|
|
12
17
|
return createJobRunLogger({ db, registry });
|
|
13
18
|
}
|
|
14
19
|
|
|
20
|
+
// Same adapter as the prod entrypoints' dispatcherToWriteRef (entrypoint/
|
|
21
|
+
// index.ts) — DispatchWriteRef's (user, qn, payload) shape vs. Dispatcher's
|
|
22
|
+
// (type, payload, user). Duplicated here because the entrypoint's version
|
|
23
|
+
// isn't exported: dev boot builds its own dispatcher via setupTestStack
|
|
24
|
+
// rather than going through createApiEntrypoint/createWorkerEntrypoint.
|
|
25
|
+
function dispatcherToWriteRef(dispatcher: Dispatcher): DispatchWriteRef {
|
|
26
|
+
return {
|
|
27
|
+
write: (user, qn, payload) => dispatcher.write(qn, payload, user),
|
|
28
|
+
queryAs: (user, qn, payload) => dispatcher.query(qn, payload, user),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
15
32
|
/** Dev-server parity: consume api + worker lanes when jobs are registered. */
|
|
16
33
|
export async function startDevJobRunners(opts: {
|
|
17
34
|
readonly registry: Registry;
|
|
18
35
|
readonly db: DbConnection;
|
|
19
36
|
readonly context: AppContext;
|
|
20
37
|
readonly redisUrl: string;
|
|
38
|
+
readonly dispatcher: Dispatcher;
|
|
21
39
|
}): Promise<{ readonly runners: readonly JobRunner[]; readonly stop: () => Promise<void> }> {
|
|
22
40
|
const jobs = [...opts.registry.getAllJobs().values()];
|
|
23
41
|
if (opts.registry.getFeature("jobs") === undefined || jobs.length === 0) {
|
|
@@ -39,6 +57,12 @@ export async function startDevJobRunners(opts: {
|
|
|
39
57
|
consumerLane: lane,
|
|
40
58
|
...logger,
|
|
41
59
|
});
|
|
60
|
+
// Without this, ctx.write/ctx.queryAs inside a dev-run job throw
|
|
61
|
+
// "dispatcher attached — call attachDispatcher() first" on their first
|
|
62
|
+
// call — the prod entrypoints (createApiEntrypoint/createWorkerEntrypoint/
|
|
63
|
+
// createAllInOneEntrypoint) do this automatically, dev boot must too
|
|
64
|
+
// (kumiko-framework#2553).
|
|
65
|
+
jr.attachDispatcher(dispatcherToWriteRef(opts.dispatcher));
|
|
42
66
|
await jr.start();
|
|
43
67
|
runners.push(jr);
|
|
44
68
|
}
|