1tube 0.1.9 → 0.1.10
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 +1 -1
- package/src/boot-progress.ts +69 -78
- package/src/server.ts +17 -22
package/package.json
CHANGED
package/src/boot-progress.ts
CHANGED
|
@@ -1,101 +1,98 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Append-only boot progress reporter for function discovery.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* Why not a live in-place spinner? With many functions importing in
|
|
5
|
+
* parallel, their top-level `console.log()` calls hit the same TTY as our
|
|
6
|
+
* renderer. Any cursor-rewrite scheme (\r + clear-line, ANSI cursor
|
|
7
|
+
* positioning, alt-screen, even Ink) corrupts the moment a foreign writer
|
|
8
|
+
* lands a line — you get flicker, half-erased frames, or text glued to
|
|
9
|
+
* the spinner. Below 200 ms of redraw cadence the eye also reads it as
|
|
10
|
+
* blinking, which the operator perceives as "broken UI".
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* So we don't redraw. We print one normal newline-terminated line every
|
|
13
|
+
* `pulseMs` saying "still working, X/Y done, in progress: …". It's not
|
|
14
|
+
* animated, but it's:
|
|
15
|
+
* - race-free with foreign stdout writers,
|
|
16
|
+
* - zero-cost (one syscall every couple of seconds),
|
|
17
|
+
* - safe to pipe to a file or capture in CI,
|
|
18
|
+
* - and identical between TTY and non-TTY paths (no branching).
|
|
19
|
+
*
|
|
20
|
+
* Pulses self-suppress when no work has changed since the previous tick,
|
|
21
|
+
* so a wedged import won't spam the log either.
|
|
15
22
|
*/
|
|
16
23
|
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const MAX_LINE = 100;
|
|
24
|
+
const DEFAULT_PULSE_MS = 2000;
|
|
25
|
+
/** Cap inflight names rendered inline so a long list doesn't wrap. */
|
|
26
|
+
const MAX_INFLIGHT_RENDERED = 4;
|
|
21
27
|
|
|
22
28
|
export interface BootProgress {
|
|
23
|
-
/**
|
|
29
|
+
/** Reset and arm the heartbeat for a new batch of `total` items. */
|
|
24
30
|
start(total: number): void;
|
|
25
31
|
/** An import has begun. */
|
|
26
32
|
onStart(name: string): void;
|
|
27
|
-
/**
|
|
33
|
+
/** Print a completion line and update internal counters. */
|
|
28
34
|
onFinish(line: string, name: string): void;
|
|
29
|
-
/** Stop
|
|
35
|
+
/** Stop the heartbeat. Safe to call repeatedly. */
|
|
30
36
|
stop(): void;
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
interface Sink {
|
|
34
|
-
isTerminal(): boolean;
|
|
35
40
|
writeSync(chunk: Uint8Array): number;
|
|
41
|
+
// Kept in the interface for future use / fakes; renderer ignores it.
|
|
42
|
+
isTerminal?(): boolean;
|
|
36
43
|
}
|
|
37
44
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
export interface BootProgressOptions {
|
|
46
|
+
/** Heartbeat cadence in ms. Default 2000. Set ≤0 to disable heartbeats. */
|
|
47
|
+
pulseMs?: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function createBootProgress(
|
|
51
|
+
sink: Sink = Deno.stdout,
|
|
52
|
+
opts: BootProgressOptions = {},
|
|
53
|
+
): BootProgress {
|
|
43
54
|
const enc = new TextEncoder();
|
|
44
|
-
const
|
|
45
|
-
try {
|
|
46
|
-
return sink.isTerminal();
|
|
47
|
-
} catch {
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
50
|
-
})();
|
|
55
|
+
const pulseMs = opts.pulseMs ?? DEFAULT_PULSE_MS;
|
|
51
56
|
|
|
52
57
|
const active = new Set<string>();
|
|
53
58
|
let completed = 0;
|
|
54
59
|
let total = 0;
|
|
55
60
|
let startedAt = 0;
|
|
56
61
|
let timer: number | undefined;
|
|
57
|
-
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
let
|
|
62
|
+
// Snapshot used to suppress a heartbeat when nothing has moved since the
|
|
63
|
+
// last pulse — important when a single slow import blocks the worker
|
|
64
|
+
// pool and we'd otherwise emit identical lines forever.
|
|
65
|
+
let lastSnapshot = "";
|
|
61
66
|
|
|
62
67
|
const write = (s: string) => {
|
|
63
68
|
try {
|
|
64
69
|
sink.writeSync(enc.encode(s));
|
|
65
70
|
} catch {
|
|
66
|
-
//
|
|
71
|
+
// Closed pipe — never crash boot over a broken stdout.
|
|
67
72
|
}
|
|
68
73
|
};
|
|
69
74
|
|
|
70
|
-
const
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
const renderInflight = (): string => {
|
|
76
|
+
if (active.size === 0) return "(idle)";
|
|
77
|
+
const names = [...active];
|
|
78
|
+
if (names.length <= MAX_INFLIGHT_RENDERED) return names.join(", ");
|
|
79
|
+
return (
|
|
80
|
+
names.slice(0, MAX_INFLIGHT_RENDERED).join(", ") +
|
|
81
|
+
` +${names.length - MAX_INFLIGHT_RENDERED} more`
|
|
82
|
+
);
|
|
75
83
|
};
|
|
76
84
|
|
|
77
|
-
const
|
|
78
|
-
if (
|
|
85
|
+
const pulse = () => {
|
|
86
|
+
if (total === 0) return;
|
|
87
|
+
if (completed >= total) return;
|
|
88
|
+
const snapshot = `${completed}|${[...active].sort().join(",")}`;
|
|
89
|
+
if (snapshot === lastSnapshot) return;
|
|
90
|
+
lastSnapshot = snapshot;
|
|
79
91
|
const elapsed = ((performance.now() - startedAt) / 1000).toFixed(1);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
inflight = "waiting…";
|
|
85
|
-
} else if (names.length <= 4) {
|
|
86
|
-
inflight = names.join(", ");
|
|
87
|
-
} else {
|
|
88
|
-
inflight = names.slice(0, 4).join(", ") + ` +${names.length - 4} more`;
|
|
89
|
-
}
|
|
90
|
-
let line =
|
|
91
|
-
`\x1b[36m${spin}\x1b[0m \x1b[1m${completed}/${total}\x1b[0m loaded, ` +
|
|
92
|
-
`${active.size} in progress: \x1b[2m${inflight}\x1b[0m \x1b[2m(${elapsed}s)\x1b[0m`;
|
|
93
|
-
if (line.length > MAX_LINE) {
|
|
94
|
-
line = line.slice(0, MAX_LINE - 1) + "…";
|
|
95
|
-
}
|
|
96
|
-
eraseLive();
|
|
97
|
-
write(line);
|
|
98
|
-
lastDrawn = line;
|
|
92
|
+
write(
|
|
93
|
+
`[1tube] \x1b[2m… still working: ${completed}/${total} loaded, ` +
|
|
94
|
+
`${active.size} in progress (${renderInflight()}) (${elapsed}s)\x1b[0m\n`,
|
|
95
|
+
);
|
|
99
96
|
};
|
|
100
97
|
|
|
101
98
|
return {
|
|
@@ -104,37 +101,31 @@ export function createBootProgress(sink: Sink = Deno.stdout): BootProgress {
|
|
|
104
101
|
completed = 0;
|
|
105
102
|
active.clear();
|
|
106
103
|
startedAt = performance.now();
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
timer =
|
|
111
|
-
|
|
112
|
-
|
|
104
|
+
lastSnapshot = "";
|
|
105
|
+
if (timer !== undefined) {
|
|
106
|
+
clearInterval(timer);
|
|
107
|
+
timer = undefined;
|
|
108
|
+
}
|
|
109
|
+
if (pulseMs > 0 && t > 0) {
|
|
110
|
+
timer = setInterval(pulse, pulseMs) as unknown as number;
|
|
113
111
|
}
|
|
114
112
|
},
|
|
115
113
|
onStart(name) {
|
|
116
114
|
active.add(name);
|
|
117
|
-
// Non-TTY: announce starts inline so logs still show progress between
|
|
118
|
-
// completions. We rate-limit to one announcement per batch by only
|
|
119
|
-
// emitting when nothing else is in-flight yet.
|
|
120
|
-
if (!isTty && active.size <= 4) {
|
|
121
|
-
write(`[1tube] \x1b[2m→ compiling ${name}\x1b[0m\n`);
|
|
122
|
-
}
|
|
123
|
-
draw();
|
|
124
115
|
},
|
|
125
116
|
onFinish(line, name) {
|
|
126
117
|
active.delete(name);
|
|
127
118
|
completed++;
|
|
128
|
-
eraseLive();
|
|
129
119
|
write(line + "\n");
|
|
130
|
-
|
|
120
|
+
// Reset the snapshot so the next pulse always reflects the new state
|
|
121
|
+
// immediately rather than waiting another `pulseMs`.
|
|
122
|
+
lastSnapshot = "";
|
|
131
123
|
},
|
|
132
124
|
stop() {
|
|
133
125
|
if (timer !== undefined) {
|
|
134
126
|
clearInterval(timer);
|
|
135
127
|
timer = undefined;
|
|
136
128
|
}
|
|
137
|
-
eraseLive();
|
|
138
129
|
},
|
|
139
130
|
};
|
|
140
131
|
}
|
package/src/server.ts
CHANGED
|
@@ -303,37 +303,32 @@ async function reloadFunctions(
|
|
|
303
303
|
// very next request — even when boot-time discovery was lazy.
|
|
304
304
|
const useLazy = opts.lazy && isFullReload;
|
|
305
305
|
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
306
|
+
// Append-only "still working" heartbeat. Does not redraw, does not race
|
|
307
|
+
// with foreign console.log() from imported modules — see
|
|
308
|
+
// src/boot-progress.ts for the design rationale. Heartbeat is silent for
|
|
309
|
+
// tiny HMR reloads (≤2 functions) where the per-completion line is
|
|
310
|
+
// already enough signal.
|
|
311
|
+
const isTinyReload = changedFunctionNames instanceof Set &&
|
|
312
|
+
changedFunctionNames.size <= 2;
|
|
313
|
+
const progress = createBootProgress(Deno.stdout, {
|
|
314
|
+
pulseMs: isTinyReload ? 0 : 2000,
|
|
315
|
+
});
|
|
316
316
|
|
|
317
317
|
let result;
|
|
318
318
|
try {
|
|
319
|
-
if (useSpinner) progress.start(0); // total filled in once we see the first onStart
|
|
320
319
|
result = await discoverAndLoad(
|
|
321
320
|
opts.functionsPath,
|
|
322
321
|
registry,
|
|
323
322
|
{
|
|
324
323
|
cacheBust,
|
|
325
324
|
only: isFullReload ? undefined : changedFunctionNames,
|
|
326
|
-
onStart:
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
: undefined,
|
|
334
|
-
onProgress: useSpinner
|
|
335
|
-
? (p) => progress.onFinish(progressLine(p), p.name)
|
|
336
|
-
: (p) => console.log(progressLine(p)),
|
|
325
|
+
onStart: (p) => {
|
|
326
|
+
// Late-bind the total on the first start event — by then we
|
|
327
|
+
// know the real work-list size after `only`/exists filtering.
|
|
328
|
+
if (p.index === 1) progress.start(p.total);
|
|
329
|
+
progress.onStart(p.name);
|
|
330
|
+
},
|
|
331
|
+
onProgress: (p) => progress.onFinish(progressLine(p), p.name),
|
|
337
332
|
lazy: useLazy,
|
|
338
333
|
},
|
|
339
334
|
);
|