@dimina-kit/devkit 0.1.2-dev.20260612025610 → 0.1.2-dev.20260615070430
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/README.md +5 -3
- package/dist/compile-log.d.ts +16 -0
- package/dist/compile-log.d.ts.map +1 -0
- package/dist/compile-log.js +42 -0
- package/dist/compile-log.test.d.ts +2 -0
- package/dist/compile-log.test.d.ts.map +1 -0
- package/dist/compile-log.test.js +134 -0
- package/dist/compile-worker-entry.d.ts +47 -0
- package/dist/compile-worker-entry.d.ts.map +1 -0
- package/dist/compile-worker-entry.js +117 -0
- package/dist/compile-worker-entry.test.d.ts +2 -0
- package/dist/compile-worker-entry.test.d.ts.map +1 -0
- package/dist/compile-worker-entry.test.js +247 -0
- package/dist/compile-worker-leak.test.d.ts +2 -0
- package/dist/compile-worker-leak.test.d.ts.map +1 -0
- package/dist/compile-worker-leak.test.js +284 -0
- package/dist/compile-worker.d.ts +33 -0
- package/dist/compile-worker.d.ts.map +1 -0
- package/dist/compile-worker.js +213 -0
- package/dist/compile-worker.test.d.ts +2 -0
- package/dist/compile-worker.test.d.ts.map +1 -0
- package/dist/compile-worker.test.js +791 -0
- package/dist/index.d.ts +15 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +100 -31
- package/dist/open-project-cleanup.test.d.ts +2 -0
- package/dist/open-project-cleanup.test.d.ts.map +1 -0
- package/dist/open-project-cleanup.test.js +176 -0
- package/dist/open-project-compile-log.test.d.ts +2 -0
- package/dist/open-project-compile-log.test.d.ts.map +1 -0
- package/dist/open-project-compile-log.test.js +174 -0
- package/dist/rebuild-scheduler.d.ts +24 -0
- package/dist/rebuild-scheduler.d.ts.map +1 -0
- package/dist/rebuild-scheduler.js +58 -0
- package/dist/rebuild-scheduler.test.d.ts +2 -0
- package/dist/rebuild-scheduler.test.d.ts.map +1 -0
- package/dist/rebuild-scheduler.test.js +201 -0
- package/dist/watch-rebuild.testutil.d.ts +21 -0
- package/dist/watch-rebuild.testutil.d.ts.map +1 -0
- package/dist/watch-rebuild.testutil.js +81 -0
- package/package.json +3 -3
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serialize rebuild runs with a dirty-flag + trailing rerun, replacing the old
|
|
3
|
+
* `if (isBuilding) return` early-exit in `openProject`'s rebuild loop, which
|
|
4
|
+
* silently DROPPED any watcher event that landed while a build was in flight
|
|
5
|
+
* (a save made during the ~1-2s compile window never produced a rebuild — the
|
|
6
|
+
* simulator stayed stale until the user saved again).
|
|
7
|
+
*
|
|
8
|
+
* Semantics:
|
|
9
|
+
* - `schedule()` while idle starts `run` immediately.
|
|
10
|
+
* - `schedule()` while `run` is in flight never starts a concurrent run
|
|
11
|
+
* (`build()` chdir()s into the project — concurrency would corrupt cwd);
|
|
12
|
+
* it marks the state dirty instead.
|
|
13
|
+
* - When the in-flight run settles and the state is dirty, exactly one
|
|
14
|
+
* trailing run starts: N saves during one build coalesce into 1 rerun.
|
|
15
|
+
* The trailing run is itself schedulable-against, recursively.
|
|
16
|
+
* - A rejecting run neither wedges the scheduler nor drops a pending dirty
|
|
17
|
+
* flag — `run` is expected to do its own error reporting (`onBuildError`).
|
|
18
|
+
*/
|
|
19
|
+
export function createRebuildScheduler(run) {
|
|
20
|
+
let running = false;
|
|
21
|
+
let dirty = false;
|
|
22
|
+
function start() {
|
|
23
|
+
running = true;
|
|
24
|
+
// `run` stays synchronously invoked (idle schedule() starts the build
|
|
25
|
+
// in the same tick), but a SYNCHRONOUS throw must funnel into the same
|
|
26
|
+
// swallow-and-continue path as a rejection — a bare `run()` call would
|
|
27
|
+
// let the throw escape before `.catch` attaches, leaving `running`
|
|
28
|
+
// stuck at true and wedging every future schedule().
|
|
29
|
+
let settled;
|
|
30
|
+
try {
|
|
31
|
+
settled = run();
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
settled = Promise.reject();
|
|
35
|
+
}
|
|
36
|
+
settled
|
|
37
|
+
.catch(() => {
|
|
38
|
+
// Failures are `run`'s responsibility to report; the scheduler
|
|
39
|
+
// only guarantees liveness (no wedge, no lost dirty flag).
|
|
40
|
+
})
|
|
41
|
+
.then(() => {
|
|
42
|
+
running = false;
|
|
43
|
+
if (dirty) {
|
|
44
|
+
dirty = false;
|
|
45
|
+
start();
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
schedule() {
|
|
51
|
+
if (running) {
|
|
52
|
+
dirty = true;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
start();
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rebuild-scheduler.test.d.ts","sourceRoot":"","sources":["../src/rebuild-scheduler.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import * as devkit from './index.js';
|
|
3
|
+
function getCreateRebuildScheduler() {
|
|
4
|
+
const factory = devkit.createRebuildScheduler;
|
|
5
|
+
expect(typeof factory, 'devkit must export createRebuildScheduler(run) — the dirty-flag + trailing-rerun replacement for the "if (isBuilding) return" drop in openProject\'s rebuild()').toBe('function');
|
|
6
|
+
return factory;
|
|
7
|
+
}
|
|
8
|
+
/** A `run` whose completion the test controls call-by-call. */
|
|
9
|
+
function makeDeferredRun() {
|
|
10
|
+
const pending = [];
|
|
11
|
+
const run = vi.fn(() => new Promise((resolve, reject) => {
|
|
12
|
+
pending.push({ resolve, reject });
|
|
13
|
+
}));
|
|
14
|
+
return {
|
|
15
|
+
run,
|
|
16
|
+
/** Settle the i-th started run (0-based). */
|
|
17
|
+
finish: (i) => pending[i].resolve(),
|
|
18
|
+
fail: (i, err) => pending[i].reject(err),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** Let queued microtasks/then-chains drain. */
|
|
22
|
+
async function settle(ms = 25) {
|
|
23
|
+
await new Promise(resolve => setTimeout(resolve, ms));
|
|
24
|
+
}
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
vi.restoreAllMocks();
|
|
27
|
+
});
|
|
28
|
+
describe('createRebuildScheduler — saves during an in-flight build must not be dropped', () => {
|
|
29
|
+
it('runs the build immediately when idle', async () => {
|
|
30
|
+
const createRebuildScheduler = getCreateRebuildScheduler();
|
|
31
|
+
const { run } = makeDeferredRun();
|
|
32
|
+
const scheduler = createRebuildScheduler(run);
|
|
33
|
+
scheduler.schedule();
|
|
34
|
+
await vi.waitFor(() => expect(run).toHaveBeenCalledTimes(1));
|
|
35
|
+
});
|
|
36
|
+
it('never starts a concurrent build while one is in flight', async () => {
|
|
37
|
+
const createRebuildScheduler = getCreateRebuildScheduler();
|
|
38
|
+
const { run } = makeDeferredRun();
|
|
39
|
+
const scheduler = createRebuildScheduler(run);
|
|
40
|
+
scheduler.schedule();
|
|
41
|
+
await vi.waitFor(() => expect(run).toHaveBeenCalledTimes(1));
|
|
42
|
+
// Saves landing mid-build must not spawn parallel compiler runs
|
|
43
|
+
// (build() chdir()s into the project — concurrency would corrupt cwd).
|
|
44
|
+
scheduler.schedule();
|
|
45
|
+
scheduler.schedule();
|
|
46
|
+
await settle();
|
|
47
|
+
expect(run).toHaveBeenCalledTimes(1);
|
|
48
|
+
});
|
|
49
|
+
it('coalesces N saves during one build into EXACTLY ONE trailing rebuild', async () => {
|
|
50
|
+
const createRebuildScheduler = getCreateRebuildScheduler();
|
|
51
|
+
const { run, finish } = makeDeferredRun();
|
|
52
|
+
const scheduler = createRebuildScheduler(run);
|
|
53
|
+
scheduler.schedule();
|
|
54
|
+
await vi.waitFor(() => expect(run).toHaveBeenCalledTimes(1));
|
|
55
|
+
// Three rapid saves while build #0 is still compiling.
|
|
56
|
+
scheduler.schedule();
|
|
57
|
+
scheduler.schedule();
|
|
58
|
+
scheduler.schedule();
|
|
59
|
+
await settle();
|
|
60
|
+
expect(run, 'no concurrent run may start').toHaveBeenCalledTimes(1);
|
|
61
|
+
// Build #0 finishes → the dirty flag triggers ONE trailing rerun.
|
|
62
|
+
finish(0);
|
|
63
|
+
await vi.waitFor(() => expect(run).toHaveBeenCalledTimes(2));
|
|
64
|
+
// The trailing run finishes with nothing else dirty → quiescent.
|
|
65
|
+
finish(1);
|
|
66
|
+
await settle();
|
|
67
|
+
expect(run, '3 saves during one build = 1 trailing rebuild, not 3').toHaveBeenCalledTimes(2);
|
|
68
|
+
});
|
|
69
|
+
it('does NOT rerun when nothing was scheduled during the build', async () => {
|
|
70
|
+
const createRebuildScheduler = getCreateRebuildScheduler();
|
|
71
|
+
const { run, finish } = makeDeferredRun();
|
|
72
|
+
const scheduler = createRebuildScheduler(run);
|
|
73
|
+
scheduler.schedule();
|
|
74
|
+
await vi.waitFor(() => expect(run).toHaveBeenCalledTimes(1));
|
|
75
|
+
finish(0);
|
|
76
|
+
await settle();
|
|
77
|
+
expect(run, 'a clean build with no mid-flight saves must not loop').toHaveBeenCalledTimes(1);
|
|
78
|
+
});
|
|
79
|
+
it('saves during the trailing run coalesce again (chained trailing runs)', async () => {
|
|
80
|
+
const createRebuildScheduler = getCreateRebuildScheduler();
|
|
81
|
+
const { run, finish } = makeDeferredRun();
|
|
82
|
+
const scheduler = createRebuildScheduler(run);
|
|
83
|
+
scheduler.schedule();
|
|
84
|
+
await vi.waitFor(() => expect(run).toHaveBeenCalledTimes(1));
|
|
85
|
+
scheduler.schedule(); // dirty during run #0
|
|
86
|
+
finish(0);
|
|
87
|
+
await vi.waitFor(() => expect(run).toHaveBeenCalledTimes(2));
|
|
88
|
+
scheduler.schedule(); // dirty during trailing run #1
|
|
89
|
+
scheduler.schedule();
|
|
90
|
+
finish(1);
|
|
91
|
+
await vi.waitFor(() => expect(run).toHaveBeenCalledTimes(3));
|
|
92
|
+
finish(2);
|
|
93
|
+
await settle();
|
|
94
|
+
expect(run).toHaveBeenCalledTimes(3);
|
|
95
|
+
});
|
|
96
|
+
it('stays usable after the build rejects', async () => {
|
|
97
|
+
const createRebuildScheduler = getCreateRebuildScheduler();
|
|
98
|
+
const { run, fail } = makeDeferredRun();
|
|
99
|
+
const scheduler = createRebuildScheduler(run);
|
|
100
|
+
scheduler.schedule();
|
|
101
|
+
await vi.waitFor(() => expect(run).toHaveBeenCalledTimes(1));
|
|
102
|
+
fail(0, new Error('compile exploded'));
|
|
103
|
+
await settle();
|
|
104
|
+
// A later save must still trigger a build — the scheduler must not wedge.
|
|
105
|
+
scheduler.schedule();
|
|
106
|
+
await vi.waitFor(() => expect(run).toHaveBeenCalledTimes(2));
|
|
107
|
+
});
|
|
108
|
+
it('a save during a FAILING build still gets its trailing rebuild', async () => {
|
|
109
|
+
const createRebuildScheduler = getCreateRebuildScheduler();
|
|
110
|
+
const { run, finish, fail } = makeDeferredRun();
|
|
111
|
+
const scheduler = createRebuildScheduler(run);
|
|
112
|
+
scheduler.schedule();
|
|
113
|
+
await vi.waitFor(() => expect(run).toHaveBeenCalledTimes(1));
|
|
114
|
+
scheduler.schedule(); // the save that fixes the broken code
|
|
115
|
+
fail(0, new Error('syntax error mid-edit'));
|
|
116
|
+
// The dirty flag set during the failing build must survive the failure.
|
|
117
|
+
await vi.waitFor(() => expect(run).toHaveBeenCalledTimes(2));
|
|
118
|
+
finish(1);
|
|
119
|
+
await settle();
|
|
120
|
+
expect(run).toHaveBeenCalledTimes(2);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
// ─── ROUND 2 — `run` throwing SYNCHRONOUSLY (not rejecting) ──────────────────
|
|
124
|
+
//
|
|
125
|
+
// Regression tests for the wedge found in review: `start()` sets
|
|
126
|
+
// `running = true` and then calls `run()` bare. If `run` throws synchronously
|
|
127
|
+
// (e.g. a config/IO error raised before the build's first await — no promise
|
|
128
|
+
// ever exists), the `.catch().then()` settle chain is never attached, so:
|
|
129
|
+
// - the exception escapes through `schedule()` to the watcher callsite,
|
|
130
|
+
// - `running` stays true FOREVER,
|
|
131
|
+
// - every later `schedule()` only sets `dirty` and is silently swallowed —
|
|
132
|
+
// the project never rebuilds again until the process restarts.
|
|
133
|
+
//
|
|
134
|
+
// The documented contract ("a failing run neither wedges the scheduler nor
|
|
135
|
+
// drops a pending dirty flag", with failures being `run`'s responsibility to
|
|
136
|
+
// report) must hold regardless of HOW the run fails: rejection and synchronous
|
|
137
|
+
// throw are the same event to the scheduler.
|
|
138
|
+
describe('ROUND 2 — createRebuildScheduler: run() throwing synchronously must not wedge the scheduler', () => {
|
|
139
|
+
/** A `run` that throws synchronously on the i-th call(s), resolves otherwise. */
|
|
140
|
+
function makeSyncThrowingRun(throwOnCalls) {
|
|
141
|
+
const run = vi.fn(() => {
|
|
142
|
+
if (throwOnCalls.includes(run.mock.calls.length)) {
|
|
143
|
+
throw new Error('sync failure before any promise exists');
|
|
144
|
+
}
|
|
145
|
+
return Promise.resolve();
|
|
146
|
+
});
|
|
147
|
+
return run;
|
|
148
|
+
}
|
|
149
|
+
it('schedule() does not propagate a synchronous throw from run (same swallow semantics as a rejection)', () => {
|
|
150
|
+
const createRebuildScheduler = getCreateRebuildScheduler();
|
|
151
|
+
const run = makeSyncThrowingRun([1]);
|
|
152
|
+
const scheduler = createRebuildScheduler(run);
|
|
153
|
+
// BUG CAUGHT: today the throw escapes start() → schedule() → the watcher
|
|
154
|
+
// callsite, which never expected schedule() to throw.
|
|
155
|
+
expect(() => scheduler.schedule()).not.toThrow();
|
|
156
|
+
expect(run).toHaveBeenCalledTimes(1);
|
|
157
|
+
});
|
|
158
|
+
it('a later save still triggers a build after run threw synchronously (no permanent running=true wedge)', async () => {
|
|
159
|
+
const createRebuildScheduler = getCreateRebuildScheduler();
|
|
160
|
+
const run = makeSyncThrowingRun([1]);
|
|
161
|
+
const scheduler = createRebuildScheduler(run);
|
|
162
|
+
// Tolerate the current buggy escape so this test isolates the WEDGE, not
|
|
163
|
+
// the throw itself (the throw is pinned by the test above).
|
|
164
|
+
try {
|
|
165
|
+
scheduler.schedule();
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// current buggy behavior — swallowed so we can probe liveness
|
|
169
|
+
}
|
|
170
|
+
expect(run).toHaveBeenCalledTimes(1);
|
|
171
|
+
await settle();
|
|
172
|
+
// BUG CAUGHT: `running` was never reset, so this schedule() only sets
|
|
173
|
+
// `dirty` and the rebuild is silently dropped forever.
|
|
174
|
+
scheduler.schedule();
|
|
175
|
+
await vi.waitFor(() => expect(run).toHaveBeenCalledTimes(2));
|
|
176
|
+
});
|
|
177
|
+
it('a dirty flag set during a synchronously-throwing run still gets its trailing rebuild', async () => {
|
|
178
|
+
const createRebuildScheduler = getCreateRebuildScheduler();
|
|
179
|
+
// Run #1: a watcher event lands re-entrantly while the build is starting
|
|
180
|
+
// (running=true → marks dirty), THEN the build throws synchronously.
|
|
181
|
+
const run = vi.fn(() => {
|
|
182
|
+
if (run.mock.calls.length === 1) {
|
|
183
|
+
scheduler.schedule(); // the save that fixes the broken state
|
|
184
|
+
throw new Error('sync failure before any promise exists');
|
|
185
|
+
}
|
|
186
|
+
return Promise.resolve();
|
|
187
|
+
});
|
|
188
|
+
const scheduler = createRebuildScheduler(run);
|
|
189
|
+
try {
|
|
190
|
+
scheduler.schedule();
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
// current buggy behavior — swallowed so we can probe the dirty flag
|
|
194
|
+
}
|
|
195
|
+
// BUG CAUGHT: the dirty flag set during run #1 is never consumed because
|
|
196
|
+
// the settle chain (which performs the trailing rerun) was never attached.
|
|
197
|
+
await vi.waitFor(() => expect(run).toHaveBeenCalledTimes(2));
|
|
198
|
+
await settle();
|
|
199
|
+
expect(run, 'exactly one trailing rebuild — no loop').toHaveBeenCalledTimes(2);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write `file` with `contentFor(attempt)`; if `settled` (e.g. the onRebuild
|
|
3
|
+
* promise) does not resolve within ~400ms, re-write with the next attempt's
|
|
4
|
+
* content (guaranteed size/mtime change → fresh inotify event) and keep
|
|
5
|
+
* retrying until `settled` resolves or the test's own vitest timeout fires.
|
|
6
|
+
*
|
|
7
|
+
* `contentFor` MUST embed `attempt` so successive writes differ in length —
|
|
8
|
+
* an identical-bytes rewrite can be coalesced by the OS into no new event.
|
|
9
|
+
*/
|
|
10
|
+
export declare function writeUntilSettled(settled: Promise<unknown>, file: string, contentFor: (attempt: number) => string): Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Like `writeUntilSettled` but waits on a polled PREDICATE instead of a
|
|
13
|
+
* promise — for the `vi.waitFor(buildSends === N)` / `logEntries.length > 0`
|
|
14
|
+
* style waiters where the thing being awaited is observable state, not a
|
|
15
|
+
* one-shot resolve. Re-writes the file every ~400ms until `predicate()` is
|
|
16
|
+
* true. Same coalescing-safety contract: predicate must be count-agnostic /
|
|
17
|
+
* monotone-reached (a `>=`, a "saw output", or a stable equality that extra
|
|
18
|
+
* coalesced rebuilds cannot overshoot).
|
|
19
|
+
*/
|
|
20
|
+
export declare function writeUntilPredicate(predicate: () => boolean, file: string, contentFor: (attempt: number) => string): Promise<void>;
|
|
21
|
+
//# sourceMappingURL=watch-rebuild.testutil.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watch-rebuild.testutil.d.ts","sourceRoot":"","sources":["../src/watch-rebuild.testutil.ts"],"names":[],"mappings":"AA+BA;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CACtC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,EACzB,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GACrC,OAAO,CAAC,IAAI,CAAC,CAoBf;AAED;;;;;;;;GAQG;AACH,wBAAsB,mBAAmB,CACxC,SAAS,EAAE,MAAM,OAAO,EACxB,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GACrC,OAAO,CAAC,IAAI,CAAC,CAYf"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
/**
|
|
3
|
+
* FLAKE-HARDENING HELPER (not a test file — name avoids the `*.test.ts` glob).
|
|
4
|
+
*
|
|
5
|
+
* Why this exists
|
|
6
|
+
* ───────────────
|
|
7
|
+
* The watcher-driven rebuild tests in compile-worker.test.ts /
|
|
8
|
+
* open-project-compile-log.test.ts / compile-worker-leak.test.ts exercise the
|
|
9
|
+
* ONE genuinely-real moving part left after fork is mocked: chokidar's inotify
|
|
10
|
+
* watch. A single `fs.writeFileSync` produces a single inotify `change` event,
|
|
11
|
+
* and under CI load (Linux inotify + a concurrent REAL dmcc compile in
|
|
12
|
+
* open-project-compile-log.test.ts pegging the CPU) chokidar can DROP that lone
|
|
13
|
+
* event — the rebuild never fires and the test waits forever (PR #44's 30s
|
|
14
|
+
* timeout at compile-worker.test.ts:297).
|
|
15
|
+
*
|
|
16
|
+
* The fix is NOT a longer timeout (the event is lost, not slow): re-issue the
|
|
17
|
+
* filesystem write — with micro-varied content so size+mtime change and a fresh
|
|
18
|
+
* inotify event is guaranteed — until the rebuild we're waiting on actually
|
|
19
|
+
* lands. The rebuild scheduler (rebuild-scheduler.ts) coalesces every extra
|
|
20
|
+
* write into exactly ONE trailing build, so re-writing is safe for the
|
|
21
|
+
* count-agnostic / `>=` assertions these helpers are applied to.
|
|
22
|
+
*
|
|
23
|
+
* ⚠️ This is flake hardening, NOT goalpost-moving: no assertion is touched.
|
|
24
|
+
* Only apply these helpers where the assertion is count-agnostic or `>=`
|
|
25
|
+
* (re-triggering an extra coalesced rebuild cannot change the outcome). Tests
|
|
26
|
+
* that pin an EXACT rebuild/build-send count must not use them.
|
|
27
|
+
*/
|
|
28
|
+
const REWRITE_INTERVAL_MS = 400;
|
|
29
|
+
/**
|
|
30
|
+
* Write `file` with `contentFor(attempt)`; if `settled` (e.g. the onRebuild
|
|
31
|
+
* promise) does not resolve within ~400ms, re-write with the next attempt's
|
|
32
|
+
* content (guaranteed size/mtime change → fresh inotify event) and keep
|
|
33
|
+
* retrying until `settled` resolves or the test's own vitest timeout fires.
|
|
34
|
+
*
|
|
35
|
+
* `contentFor` MUST embed `attempt` so successive writes differ in length —
|
|
36
|
+
* an identical-bytes rewrite can be coalesced by the OS into no new event.
|
|
37
|
+
*/
|
|
38
|
+
export async function writeUntilSettled(settled, file, contentFor) {
|
|
39
|
+
let done = false;
|
|
40
|
+
const guard = settled.then(() => { done = true; }, () => { done = true; });
|
|
41
|
+
let attempt = 0;
|
|
42
|
+
// First write happens immediately; subsequent re-writes only if still unsettled.
|
|
43
|
+
for (;;) {
|
|
44
|
+
fs.writeFileSync(file, contentFor(attempt));
|
|
45
|
+
attempt += 1;
|
|
46
|
+
const tick = await Promise.race([
|
|
47
|
+
guard.then(() => 'settled'),
|
|
48
|
+
sleep(REWRITE_INTERVAL_MS).then(() => 'retry'),
|
|
49
|
+
]);
|
|
50
|
+
if (tick === 'settled' || done)
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
await settled;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Like `writeUntilSettled` but waits on a polled PREDICATE instead of a
|
|
57
|
+
* promise — for the `vi.waitFor(buildSends === N)` / `logEntries.length > 0`
|
|
58
|
+
* style waiters where the thing being awaited is observable state, not a
|
|
59
|
+
* one-shot resolve. Re-writes the file every ~400ms until `predicate()` is
|
|
60
|
+
* true. Same coalescing-safety contract: predicate must be count-agnostic /
|
|
61
|
+
* monotone-reached (a `>=`, a "saw output", or a stable equality that extra
|
|
62
|
+
* coalesced rebuilds cannot overshoot).
|
|
63
|
+
*/
|
|
64
|
+
export async function writeUntilPredicate(predicate, file, contentFor) {
|
|
65
|
+
let attempt = 0;
|
|
66
|
+
for (;;) {
|
|
67
|
+
fs.writeFileSync(file, contentFor(attempt));
|
|
68
|
+
attempt += 1;
|
|
69
|
+
const start = Date.now();
|
|
70
|
+
while (Date.now() - start < REWRITE_INTERVAL_MS) {
|
|
71
|
+
if (predicate())
|
|
72
|
+
return;
|
|
73
|
+
await sleep(25);
|
|
74
|
+
}
|
|
75
|
+
if (predicate())
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function sleep(ms) {
|
|
80
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
81
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dimina-kit/devkit",
|
|
3
|
-
"version": "0.1.2-dev.
|
|
3
|
+
"version": "0.1.2-dev.20260615070430",
|
|
4
4
|
"description": "Development toolkit for Dimina mini-apps with H5 container preview",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dimina",
|
|
@@ -52,8 +52,8 @@
|
|
|
52
52
|
"eslint": "^10.2.1",
|
|
53
53
|
"typescript": "5.9.2",
|
|
54
54
|
"vitest": "^4.1.4",
|
|
55
|
-
"@dimina-kit/
|
|
56
|
-
"@dimina-kit/
|
|
55
|
+
"@dimina-kit/typescript-config": "0.1.0",
|
|
56
|
+
"@dimina-kit/eslint-config": "0.1.0"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
59
|
"@dimina/compiler": "1.0.16",
|