@cosmicdrift/kumiko-dev-server 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-dev-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.195.0",
|
|
4
4
|
"description": "Dev-tooling for Kumiko apps: local dev-server bootstrap (runDevApp), scaffolding, codegen. Not shipped into production node_modules — see @cosmicdrift/kumiko-server-runtime for the prod boot path.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -54,9 +54,9 @@
|
|
|
54
54
|
"kumiko-schema-check": "./bin/kumiko-schema-check.ts"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@cosmicdrift/kumiko-bundled-features": "0.
|
|
58
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
59
|
-
"@cosmicdrift/kumiko-server-runtime": "0.
|
|
57
|
+
"@cosmicdrift/kumiko-bundled-features": "0.195.0",
|
|
58
|
+
"@cosmicdrift/kumiko-framework": "0.195.0",
|
|
59
|
+
"@cosmicdrift/kumiko-server-runtime": "0.195.0",
|
|
60
60
|
"ts-morph": "^28.0.0"
|
|
61
61
|
},
|
|
62
62
|
"publishConfig": {
|
|
@@ -47,21 +47,40 @@ afterAll(() => {
|
|
|
47
47
|
* implicitly assume "this many ms is enough", which is brittle on
|
|
48
48
|
* loaded CI runners. The polling form converges as fast as the system
|
|
49
49
|
* allows AND fails loudly with a useful message if the event never lands.
|
|
50
|
+
*
|
|
51
|
+
* `retry`/`retryIntervalMs`: for predicates gated on a native `fs.watch`
|
|
52
|
+
* event, a single write only gets one chance at delivery — under macOS
|
|
53
|
+
* FSEvents backlog (hundreds of concurrent recursive watches in a full
|
|
54
|
+
* `bun test` run), events aren't just delayed, they're sometimes dropped
|
|
55
|
+
* entirely, and Node doesn't surface a rescan signal. `retry` re-fires the
|
|
56
|
+
* triggering action on a cadence so a dropped event costs one interval,
|
|
57
|
+
* not the whole timeout. `retryIntervalMs` must stay well above the
|
|
58
|
+
* watcher's `debounceMs` — a retry that lands mid-debounce just resets
|
|
59
|
+
* the timer and can starve `fire()` forever.
|
|
50
60
|
*/
|
|
51
61
|
async function waitFor(
|
|
52
62
|
predicate: () => boolean,
|
|
53
|
-
opts: {
|
|
63
|
+
opts: {
|
|
64
|
+
timeout?: number;
|
|
65
|
+
interval?: number;
|
|
66
|
+
label?: string;
|
|
67
|
+
retry?: () => void;
|
|
68
|
+
retryIntervalMs?: number;
|
|
69
|
+
} = {},
|
|
54
70
|
): Promise<void> {
|
|
55
|
-
// Default 5000ms — fchokidar-FS-watch events take >2s under CI load on
|
|
56
|
-
// the cdgs-runner (Memory feedback_watch_test_flaky, observed 3× in
|
|
57
|
-
// a row on PR #80). 5s gives headroom without slowing the happy-path.
|
|
58
71
|
const timeout = opts.timeout ?? 5000;
|
|
59
72
|
const interval = opts.interval ?? 25;
|
|
73
|
+
const retryIntervalMs = opts.retryIntervalMs ?? 250;
|
|
60
74
|
const deadline = Date.now() + timeout;
|
|
75
|
+
let nextRetryAt = Date.now() + retryIntervalMs;
|
|
61
76
|
while (!predicate()) {
|
|
62
77
|
if (Date.now() > deadline) {
|
|
63
78
|
throw new Error(`waitFor: ${opts.label ?? "predicate"} not satisfied within ${timeout}ms`);
|
|
64
79
|
}
|
|
80
|
+
if (opts.retry && Date.now() >= nextRetryAt) {
|
|
81
|
+
opts.retry();
|
|
82
|
+
nextRetryAt = Date.now() + retryIntervalMs;
|
|
83
|
+
}
|
|
65
84
|
await new Promise((r) => setTimeout(r, interval));
|
|
66
85
|
}
|
|
67
86
|
}
|
|
@@ -105,11 +124,11 @@ describe("watchAndRegenerate", () => {
|
|
|
105
124
|
expect(results).toHaveLength(1);
|
|
106
125
|
expect(results[0]?.eventCount).toBe(1);
|
|
107
126
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
127
|
+
const rewrite = () =>
|
|
128
|
+
writeFile(
|
|
129
|
+
appRoot,
|
|
130
|
+
"src/feature.ts",
|
|
131
|
+
`
|
|
113
132
|
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
114
133
|
import { z } from "zod";
|
|
115
134
|
|
|
@@ -118,20 +137,29 @@ export default defineFeature("orders", (r) => {
|
|
|
118
137
|
r.defineEvent("second", z.object({ tag: z.string() }));
|
|
119
138
|
});
|
|
120
139
|
`,
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
// Poll until the watcher's debounced re-run has landed. fs.watch
|
|
124
|
-
// events on macOS arrive in 1-5ms; CI runners can stretch that —
|
|
125
|
-
// polling adapts to the actual schedule instead of guessing a fixed
|
|
126
|
-
// sleep.
|
|
127
|
-
await waitFor(() => results.length >= 2, {
|
|
128
|
-
timeout: 5000,
|
|
129
|
-
label: "second codegen result",
|
|
130
|
-
});
|
|
140
|
+
);
|
|
131
141
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
142
|
+
// Add a second event-definition by rewriting the feature.
|
|
143
|
+
rewrite();
|
|
144
|
+
|
|
145
|
+
// Poll until the watcher's debounced re-run has landed, re-touching
|
|
146
|
+
// the file every 250ms in case the triggering fs.watch event was
|
|
147
|
+
// dropped rather than merely delayed (see waitFor's `retry` doc).
|
|
148
|
+
// Waits on the expected *state* (eventCount 2), not just a length
|
|
149
|
+
// bump — writeFileSync truncates then writes, so an event fired
|
|
150
|
+
// mid-write would satisfy a length-only predicate with a stale
|
|
151
|
+
// (0 or 1) eventCount, especially with retries widening that window.
|
|
152
|
+
try {
|
|
153
|
+
await waitFor(() => results.some((r) => r.eventCount === 2), {
|
|
154
|
+
timeout: 12000,
|
|
155
|
+
label: "second codegen result",
|
|
156
|
+
retry: rewrite,
|
|
157
|
+
});
|
|
158
|
+
expect(results.at(-1)?.eventCount).toBe(2);
|
|
159
|
+
} finally {
|
|
160
|
+
handle.close();
|
|
161
|
+
}
|
|
162
|
+
}, 15000);
|
|
135
163
|
|
|
136
164
|
test("close() is idempotent", () => {
|
|
137
165
|
const appRoot = makeAppDir();
|
|
@@ -174,15 +202,20 @@ export default defineFeature("orders", (r) => {
|
|
|
174
202
|
|
|
175
203
|
// Positive control: a .ts change MUST trigger. waitFor exits as
|
|
176
204
|
// soon as the new result lands, confirming the watcher is alive.
|
|
177
|
-
|
|
205
|
+
// Re-touched on retry in case the triggering event is dropped
|
|
206
|
+
// rather than merely delayed under a loaded fs.watch backlog.
|
|
207
|
+
const triggerRewrite = () =>
|
|
208
|
+
writeFile(appRoot, "src/feature.ts", FEATURE_TEMPLATE("ignore-css", "after"));
|
|
209
|
+
triggerRewrite();
|
|
178
210
|
await waitFor(() => results.length > afterNonTs, {
|
|
179
|
-
timeout:
|
|
211
|
+
timeout: 12000,
|
|
180
212
|
label: "ts-change result after non-ts noise",
|
|
213
|
+
retry: triggerRewrite,
|
|
181
214
|
});
|
|
182
215
|
|
|
183
216
|
// The non-ts writes should not have advanced the count past the
|
|
184
217
|
// baseline. If they did, the watcher's filter is broken.
|
|
185
218
|
expect(afterNonTs).toBe(baseline);
|
|
186
219
|
handle.close();
|
|
187
|
-
});
|
|
220
|
+
}, 15000);
|
|
188
221
|
});
|
|
@@ -153,9 +153,9 @@ export type CreateKumikoServerOptions = {
|
|
|
153
153
|
* `stylesheet: false` → CSS-Pipeline explizit deaktivieren. */
|
|
154
154
|
readonly stylesheet?: string | false;
|
|
155
155
|
/** Optional HTML template served at `GET /`. The dev-server injects
|
|
156
|
-
* a `<script src="/client.js">` and a reload-listener
|
|
157
|
-
* `</body>` if those aren't already there. Defaults to a
|
|
158
|
-
* empty-body document — enough to boot the client. */
|
|
156
|
+
* a `<script type="module" src="/client.js">` and a reload-listener
|
|
157
|
+
* snippet into `</body>` if those aren't already there. Defaults to a
|
|
158
|
+
* minimal empty-body document — enough to boot the client. */
|
|
159
159
|
readonly htmlPath?: string;
|
|
160
160
|
/** Port to listen on. Default 4173. Overridable via `PORT` env. */
|
|
161
161
|
readonly port?: number;
|