@zerotal/devtools 1.6.1 → 1.6.3
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/CHANGELOG.md +22 -0
- package/package.json +3 -3
- package/src/DevtoolsInjectionMiddleware.ts +43 -4
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,28 @@ follows the Zerotal monorepo's unified versioning.
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [1.6.2] — 2026-08-15
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **The panel's event stream was abandoned rather than closed on shutdown**, so the browser
|
|
16
|
+
was left holding a truncated chunked response and logged
|
|
17
|
+
`GET /__zerotal/devtools/sse net::ERR_INCOMPLETE_CHUNKED_ENCODING` — under `serve --dev`,
|
|
18
|
+
on every save. Nothing was broken by it (`EventSource` reconnects on its own), but a
|
|
19
|
+
devtools panel that fills the console with network errors is working against its own
|
|
20
|
+
purpose. Shutdown now closes each open stream, which writes the terminating chunk and
|
|
21
|
+
ends the request the way a reader expects.
|
|
22
|
+
|
|
23
|
+
On Windows the fix needed [`@zerotal/core`](../core/CHANGELOG.md)'s side too — a worker
|
|
24
|
+
that is terminated outright never reaches this code.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **A heartbeat on the event stream.** A comment frame every 25 seconds, which readers
|
|
29
|
+
ignore. Without one an idle stream can be dropped by an intermediary with nothing written
|
|
30
|
+
for either end to notice it by, and the panel goes on reporting a connection it no longer
|
|
31
|
+
has. The timer is unref'd, so it never holds a process open.
|
|
32
|
+
|
|
11
33
|
## [1.5.1] — 2026-08-15
|
|
12
34
|
|
|
13
35
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zerotal/devtools",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.3",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"maturity": "stable",
|
|
6
6
|
"private": false,
|
|
@@ -30,11 +30,11 @@
|
|
|
30
30
|
"typecheck": "tsc --noEmit"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@zerotal/core": "1.6.
|
|
33
|
+
"@zerotal/core": "1.6.3"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"typescript": "^5.8.0",
|
|
37
|
-
"@zerotal/orm": "1.6.
|
|
37
|
+
"@zerotal/orm": "1.6.3"
|
|
38
38
|
},
|
|
39
39
|
"description": "In-browser developer tools for Zerotal — request traces, an inspector panel, and an extensible tab registry.",
|
|
40
40
|
"keywords": [
|
|
@@ -49,9 +49,9 @@ export interface DevtoolsInjectionOptions {
|
|
|
49
49
|
const _sseClients = new Set<ReadableStreamDefaultController<Uint8Array>>();
|
|
50
50
|
const _enc = new TextEncoder();
|
|
51
51
|
|
|
52
|
-
function
|
|
52
|
+
function _ssePublishRaw(frame: string): void {
|
|
53
53
|
if (_sseClients.size === 0) return;
|
|
54
|
-
const chunk = _enc.encode(
|
|
54
|
+
const chunk = _enc.encode(frame);
|
|
55
55
|
for (const ctrl of _sseClients) {
|
|
56
56
|
try {
|
|
57
57
|
ctrl.enqueue(chunk);
|
|
@@ -61,22 +61,61 @@ function _ssePublish(data: unknown): void {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
function _ssePublish(data: unknown): void {
|
|
65
|
+
_ssePublishRaw(`data: ${JSON.stringify(data)}\n\n`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* End every open stream, in an order the browser recognises as an ending.
|
|
70
|
+
*
|
|
71
|
+
* Dropping the set is not enough: the response body is a chunked stream, so a
|
|
72
|
+
* process that exits with one half-written leaves the browser holding a
|
|
73
|
+
* truncated body and logging `ERR_INCOMPLETE_CHUNKED_ENCODING`. Under
|
|
74
|
+
* `serve --dev` that is every save. Closing the controller writes the
|
|
75
|
+
* terminating chunk, and `EventSource` then reconnects on its own timer without
|
|
76
|
+
* an error in the console.
|
|
77
|
+
*/
|
|
78
|
+
function _sseCloseAll(): void {
|
|
79
|
+
for (const ctrl of _sseClients) {
|
|
80
|
+
try {
|
|
81
|
+
ctrl.close();
|
|
82
|
+
} catch {
|
|
83
|
+
// Already closed or errored — the stream is over either way.
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
_sseClients.clear();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** How often to write a comment frame keeping an idle stream alive. */
|
|
90
|
+
const SSE_HEARTBEAT_MS = 25_000;
|
|
91
|
+
|
|
64
92
|
/**
|
|
65
93
|
* Bridge the trace store to connected panels. Called by {@link DevtoolsProvider}
|
|
66
94
|
* on boot rather than at module scope — subscribing on import would tie the
|
|
67
95
|
* stream to whichever store existed at import time, before the provider has
|
|
68
96
|
* built the one the app's config asks for.
|
|
69
97
|
*
|
|
70
|
-
* @returns A disposer that unsubscribes and
|
|
98
|
+
* @returns A disposer that unsubscribes and closes connected clients.
|
|
71
99
|
*/
|
|
72
100
|
export function startDevtoolsStream(): () => void {
|
|
73
101
|
const unsubscribe = traceStore().subscribe((trace) => {
|
|
74
102
|
if (trace === null) _ssePublish({ type: "clear" });
|
|
75
103
|
else _ssePublish({ type: "trace", data: trace });
|
|
76
104
|
});
|
|
105
|
+
|
|
106
|
+
// A comment frame, which SSE readers ignore. Without it a stream that goes
|
|
107
|
+
// quiet can be dropped by an intermediary with nothing written to notice it
|
|
108
|
+
// by, and the panel keeps reporting a connection it no longer has. Unref'd so
|
|
109
|
+
// it never holds the process open.
|
|
110
|
+
const heartbeat = setInterval(() => {
|
|
111
|
+
_ssePublishRaw(":\n\n");
|
|
112
|
+
}, SSE_HEARTBEAT_MS);
|
|
113
|
+
heartbeat.unref?.();
|
|
114
|
+
|
|
77
115
|
return () => {
|
|
78
116
|
unsubscribe();
|
|
79
|
-
|
|
117
|
+
clearInterval(heartbeat);
|
|
118
|
+
_sseCloseAll();
|
|
80
119
|
};
|
|
81
120
|
}
|
|
82
121
|
|