@cplieger/web-terminal-engine 3.2.0 → 3.3.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/README.md +39 -1
- package/jsr.json +3 -2
- package/package.json +7 -5
- package/src/connection.ts +54 -16
- package/src/status-stream.ts +27 -1
- package/src/types.ts +5 -3
- package/src/wire-manifest.ts +77 -0
- package/wire-compatibility.json +9 -0
package/README.md
CHANGED
|
@@ -84,12 +84,50 @@ document.addEventListener("keydown", (ev) => {
|
|
|
84
84
|
- **`scroll`** — Auto-follow tracker for the scroll container. `init`, `stickToBottom`, `scrollToBottom`, `isUserScrolledUp`.
|
|
85
85
|
- **`modes`** — DEC private mode state (synced from server's `ModesMessage`). `setModes`, `isBracketedPaste`, `isApplicationCursor`, `getMouseMode`, `isMouseSGR`, `isFocusReporting`, `isApplicationKeypad`, `isReverseVideo`.
|
|
86
86
|
- **`decodeWireBinary(buf)`**: Top-level decoder for the binary WebSocket frames. Returns a `ServerMessage` or `null` for invalid/truncated frames.
|
|
87
|
-
- **Wire compatibility metadata**: `WIRE_PROTOCOL_VERSION`, `MIN_SUPPORTED_SERVER_WIRE_VERSION`, `WIRE_INCOMPATIBLE_CLOSE_CODE`, and `WIRE_COMPATIBILITY` publish this client release's directional contract.
|
|
87
|
+
- **Wire compatibility metadata**: `WIRE_PROTOCOL_VERSION`, `MIN_SUPPORTED_SERVER_WIRE_VERSION`, `WIRE_INCOMPATIBLE_CLOSE_CODE`, and `WIRE_COMPATIBILITY` publish this client release's directional contract. The same values ship as a language-neutral JSON artifact for non-TypeScript consumers — see [Wire compatibility manifest](#wire-compatibility-manifest).
|
|
88
88
|
- **`connection`**: Client → server WebSocket lifecycle: owns the socket, exponential-backoff reconnect, and the resume/inputAck reliability layer (outbox + server-restart detection). `init(callbacks)`, `connect`, `sendBinary(bytes)`, `sendResize`, `reconnectNow`. The callbacks expose `onMessage(ServerMessage)`, `onOpen`/`onClose`/`onConnecting`/`onOutboxFull`/`onServerRestart`, `onWireVersionMismatch`, and the definitive `onWireIncompatible`; a `computeSize()` provider; and an optional `wsPath` (defaults to `"/ws"`). An explicit below-floor server revision or close code 4002 stops automatic reconnects until `disconnect()` or a page reload. Version-silent and future-revision servers remain tolerated. The module decodes frames internally and applies `modes.setModes`, so a consumer only needs to dispatch screen/scroll to `render`. Prefer this over wiring `WebSocket` + `decodeWireBinary` by hand unless you need full control.
|
|
89
89
|
- **`controlFrame(msg)` / `wsURL(proto, host, path?)`** — Low-level helpers for the client → server protocol (0x00-prefixed JSON control frames, WebSocket URL building). Used internally by `connection`; exported for advanced consumers.
|
|
90
90
|
|
|
91
91
|
Wire types (`WireRun`, `ScreenMessage`, `ScrollMessage`, `ModesMessage`, `TitleMessage`, `ResumeAckMessage`, `ServerMessage`, `ControlMessage`) are re-exported from the package root and match the Go server's wire format byte-for-byte.
|
|
92
92
|
|
|
93
|
+
## Wire compatibility manifest
|
|
94
|
+
|
|
95
|
+
The same compatibility numbers are also published as a language-neutral JSON artifact, so a consumer that cannot import TypeScript — a Dockerfile, a shell release gate, a CI script in any language — can read them without scraping source:
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
# npm consumer
|
|
99
|
+
jq -r .wireCompatibility.protocolVersion \
|
|
100
|
+
node_modules/@cplieger/web-terminal-engine/wire-compatibility.json
|
|
101
|
+
|
|
102
|
+
# JSR consumer (published as an included file; JSR exports are modules only)
|
|
103
|
+
curl -fsSL https://jsr.io/@cplieger/web-terminal-engine/<version>/wire-compatibility.json
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"schemaVersion": 1,
|
|
109
|
+
"generatedBy": "web/src/wire-manifest.ts",
|
|
110
|
+
"wireCompatibility": {
|
|
111
|
+
"protocolVersion": 4,
|
|
112
|
+
"minimumServerProtocolVersion": 3,
|
|
113
|
+
"incompatibleCloseCode": 4002
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
It is generated from `WIRE_COMPATIBILITY` (there is no second copy of the numbers), checked into the repo because the publish pipeline runs no build step, and guarded by a regenerate-and-diff test so it cannot go stale. Its values are pinned to both the TypeScript constants and the Go `terminal` constants, so the three surfaces cannot diverge.
|
|
119
|
+
|
|
120
|
+
**What you may rely on** (semver-governed public artifact, versioned by the package version):
|
|
121
|
+
|
|
122
|
+
- The file is present at the package root of every npm tarball and JSR publish, at path `wire-compatibility.json`, and is importable from npm as `@cplieger/web-terminal-engine/wire-compatibility.json`.
|
|
123
|
+
- `schemaVersion` is an integer describing this file's LAYOUT, not the wire protocol. Read it first and reject a value you do not understand.
|
|
124
|
+
- Within a `schemaVersion`, the fields under `wireCompatibility` keep their names, types and meanings, and equal the correspondingly named `WIRE_COMPATIBILITY` members of the same release.
|
|
125
|
+
- New fields may be added under either object in a MINOR release, so parse permissively (ignore unknown keys).
|
|
126
|
+
|
|
127
|
+
**What is not part of the contract**: `generatedBy` is an informational provenance note. The manifest deliberately carries no package version — the publish step injects that into `package.json` / `jsr.json` after checkout, so a version here would ship frozen at the repo placeholder; read the package version from those files.
|
|
128
|
+
|
|
129
|
+
**Breaking changes to the manifest** (MAJOR, and always release-noted): removing or renaming a field, changing a field's type or meaning, moving the file, or bumping `schemaVersion`. A change to the wire revision NUMBERS is not a manifest break — reporting them is what the file is for.
|
|
130
|
+
|
|
93
131
|
## Browser-only
|
|
94
132
|
|
|
95
133
|
This package depends on `document`, `HTMLElement`, `MessageChannel`, and other DOM APIs, so it only runs in browser-like environments. The companion Go server runs anywhere Go does.
|
package/jsr.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/web-terminal-engine",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"exports": {
|
|
5
5
|
".": "./src/index.ts"
|
|
6
6
|
},
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"include": [
|
|
9
9
|
"LICENSE",
|
|
10
10
|
"README.md",
|
|
11
|
-
"src/**/*.ts"
|
|
11
|
+
"src/**/*.ts",
|
|
12
|
+
"wire-compatibility.json"
|
|
12
13
|
],
|
|
13
14
|
"exclude": [
|
|
14
15
|
"src/**/*.test.ts",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/web-terminal-engine",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"typecheck": "tsc -p tsconfig.json",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"devDependencies": {
|
|
12
12
|
"@cplieger/web-terminal-engine-v3": "npm:@cplieger/web-terminal-engine@2.8.0",
|
|
13
13
|
"@eslint/js": "10.0.1",
|
|
14
|
-
"@playwright/test": "1.
|
|
14
|
+
"@playwright/test": "1.62.0",
|
|
15
15
|
"@stryker-mutator/core": "9.6.1",
|
|
16
16
|
"@stryker-mutator/typescript-checker": "9.6.1",
|
|
17
17
|
"@stryker-mutator/vitest-runner": "9.6.1",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"@typescript/native": "npm:typescript@7.0.2",
|
|
20
20
|
"@vitest/coverage-v8": "4.1.10",
|
|
21
21
|
"esbuild": "0.28.1",
|
|
22
|
-
"eslint": "10.
|
|
22
|
+
"eslint": "10.8.0",
|
|
23
23
|
"fast-check": "4.9.0",
|
|
24
24
|
"happy-dom": "20.11.1",
|
|
25
25
|
"pngjs": "7.0.0",
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
"description": "Browser terminal engine: VT500 screen buffer, DOM renderer, and binary wire protocol",
|
|
35
35
|
"license": "GPL-3.0-or-later",
|
|
36
36
|
"exports": {
|
|
37
|
-
".": "./src/index.ts"
|
|
37
|
+
".": "./src/index.ts",
|
|
38
|
+
"./wire-compatibility.json": "./wire-compatibility.json"
|
|
38
39
|
},
|
|
39
40
|
"types": "./src/index.ts",
|
|
40
41
|
"publishConfig": {
|
|
@@ -50,6 +51,7 @@
|
|
|
50
51
|
"!src/**/*.test.ts",
|
|
51
52
|
"!src/**/test-helpers",
|
|
52
53
|
"!src/**/fc-strict-setup.ts",
|
|
53
|
-
"jsr.json"
|
|
54
|
+
"jsr.json",
|
|
55
|
+
"wire-compatibility.json"
|
|
54
56
|
]
|
|
55
57
|
}
|
package/src/connection.ts
CHANGED
|
@@ -278,12 +278,16 @@ export interface Callbacks {
|
|
|
278
278
|
* transient treatment (onClose + backoff reconnect), so existing
|
|
279
279
|
* consumers are unaffected until they opt in. */
|
|
280
280
|
onProcessExit?(): void;
|
|
281
|
-
/** Fired when the
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
281
|
+
/** Fired when the client's queued input can no longer be trusted, which
|
|
282
|
+
* happens two ways: the server's boot-epoch in resumeAck differs from the
|
|
283
|
+
* one observed on a previous connection (the server restarted), or the
|
|
284
|
+
* server no longer holds this session's input ledger AND the outbox still
|
|
285
|
+
* held unacked bytes. A forgotten ledger whose every byte was already acked
|
|
286
|
+
* does NOT fire this — nothing was lost, and reporting it made a phone
|
|
287
|
+
* waking from sleep announce a restart that never happened. By the time this
|
|
288
|
+
* fires, the connection module has already reset bytesSent/bytesAcked/outbox
|
|
289
|
+
* so subsequent input starts from zero. UI should surface a banner so the
|
|
290
|
+
* user knows recent input may have been lost. */
|
|
287
291
|
onServerRestart?(): void;
|
|
288
292
|
/** Fired when a resumeAck carries an explicit server revision outside
|
|
289
293
|
* [MIN_SUPPORTED_SERVER_WIRE_VERSION, WIRE_PROTOCOL_VERSION]. A newer
|
|
@@ -415,18 +419,51 @@ export function sendResize(): void {
|
|
|
415
419
|
sendControl({ type: "resize", cols, rows });
|
|
416
420
|
}
|
|
417
421
|
|
|
418
|
-
//
|
|
419
|
-
//
|
|
420
|
-
// the
|
|
421
|
-
//
|
|
422
|
-
|
|
422
|
+
// resetLedger drops a session's reliable-input accounting. Called when the
|
|
423
|
+
// server's boot epoch changes (restart) or when the server no longer recognizes
|
|
424
|
+
// the session -- both invalidate the local bytesSent/bytesAcked/outbox state.
|
|
425
|
+
// The three entry points below differ only in whether the UI hears about it.
|
|
426
|
+
// resetLedger drops a session's reliable-input accounting. Called whenever the
|
|
427
|
+
// server no longer holds the ledger these counters were keyed to (a restart, an
|
|
428
|
+
// idle-GC'd resume key, a session the server does not recognize): its
|
|
429
|
+
// replacement counts from zero, so keeping non-zero counters would make every
|
|
430
|
+
// later ack read as stale in applyAck and the outbox would never trim again.
|
|
431
|
+
function resetLedger(st: ResumeState): void {
|
|
423
432
|
st.bytesSent = 0;
|
|
424
433
|
st.bytesAcked = 0;
|
|
425
434
|
st.outbox.length = 0;
|
|
426
435
|
st.outboxBytes = 0;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// resetSessionAfterRestart resets the ledger and tells the UI the server
|
|
439
|
+
// restarted. Reserved for a boot-epoch change, which is the one cause where
|
|
440
|
+
// that sentence is literally true.
|
|
441
|
+
function resetSessionAfterRestart(st: ResumeState): void {
|
|
442
|
+
resetLedger(st);
|
|
427
443
|
cb?.onServerRestart?.();
|
|
428
444
|
}
|
|
429
445
|
|
|
446
|
+
// resetForgottenLedger handles a ledger the server no longer holds (the
|
|
447
|
+
// explicit ledgerLost flag, or the old-server received=0 heuristic). The server
|
|
448
|
+
// forgetting a ledger is not a restart and is not by itself a loss: only the
|
|
449
|
+
// UNACKED remainder was ever at risk. So the UI is notified only when there was
|
|
450
|
+
// one, and a session whose every sent byte was already acked resets silently —
|
|
451
|
+
// nothing can be replayed and nothing was lost.
|
|
452
|
+
//
|
|
453
|
+
// The silent case is the common one, and reporting it was a real defect: an
|
|
454
|
+
// iPad whose screen slept for half an hour woke, reconnected, and announced a
|
|
455
|
+
// server restart that never happened. Server-side, the ledger it claimed had
|
|
456
|
+
// received EXACTLY as many bytes as the client claimed to have sent (10531 ==
|
|
457
|
+
// 10531 in the incident logs), i.e. an empty outbox and a fully-applied
|
|
458
|
+
// session, and the banner still fired.
|
|
459
|
+
function resetForgottenLedger(st: ResumeState): void {
|
|
460
|
+
const lostUnacked = st.bytesSent > st.bytesAcked;
|
|
461
|
+
resetLedger(st);
|
|
462
|
+
if (lostUnacked) {
|
|
463
|
+
cb?.onServerRestart?.();
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
430
467
|
// applyAck drops chunks from the front of the session's outbox until the
|
|
431
468
|
// running total of unacked bytes matches (bytesSent - newAck). Runs in
|
|
432
469
|
// O(chunks_dropped) by tracking outboxBytes incrementally rather than
|
|
@@ -901,11 +938,12 @@ export function connect(): void {
|
|
|
901
938
|
// tail): the resume key missed the server's registry while we claimed
|
|
902
939
|
// sent bytes — our ledger was reclaimed (idle GC / cap eviction). The
|
|
903
940
|
// server cannot vouch for ANY previously sent input, so replaying the
|
|
904
|
-
// outbox risks duplicate execution. Deterministic drop
|
|
941
|
+
// outbox risks duplicate execution. Deterministic drop, plus a notify
|
|
942
|
+
// only when unacked bytes were actually dropped (resetForgottenLedger),
|
|
905
943
|
// covering the bytesAcked === 0 case the heuristic below cannot see
|
|
906
944
|
// (acks that never reached us before the disconnect).
|
|
907
945
|
if (msg.ledgerLost) {
|
|
908
|
-
|
|
946
|
+
resetForgottenLedger(st);
|
|
909
947
|
return;
|
|
910
948
|
}
|
|
911
949
|
// Server-doesn't-recognize-this-session safeguard (old-server
|
|
@@ -914,12 +952,12 @@ export function connect(): void {
|
|
|
914
952
|
// our session (idle GC kicked in, or sessionId persistence failed and
|
|
915
953
|
// a reload created a new one). Replaying the outbox would deliver
|
|
916
954
|
// every queued chunk again, causing the iOS tab-suspend
|
|
917
|
-
// duplicate-resend bug. Drop the outbox and notify
|
|
918
|
-
//
|
|
955
|
+
// duplicate-resend bug. Drop the outbox, and notify only if it held
|
|
956
|
+
// unacked bytes — input since the last successful ack is
|
|
919
957
|
// irrecoverable but at least not duplicated. Skip this branch when
|
|
920
958
|
// bytesSent = 0 (genuine first-connect; received=0 is correct).
|
|
921
959
|
if (msg.received === 0 && st.bytesAcked > 0) {
|
|
922
|
-
|
|
960
|
+
resetForgottenLedger(st);
|
|
923
961
|
return;
|
|
924
962
|
}
|
|
925
963
|
applyAck(st, msg.received);
|
package/src/status-stream.ts
CHANGED
|
@@ -19,7 +19,21 @@ import { nextBackoffDelay } from "./reconnect.js";
|
|
|
19
19
|
* remember. */
|
|
20
20
|
export interface SessionInfo {
|
|
21
21
|
readonly id: string;
|
|
22
|
-
|
|
22
|
+
/** the session's current status. "working"/"idle"/"exited"/"crashed" are
|
|
23
|
+
* derived by the server; "input"/"done" are latched from a classified OSC 9
|
|
24
|
+
* notification; "failed" is OSC 9;4 progress state 2 and "warning" is state 4
|
|
25
|
+
* (iTerm2 semantics), both of which PERSIST until the program changes the
|
|
26
|
+
* progress state. "exited" and "crashed" split the session's end: "exited" is
|
|
27
|
+
* an ordinary one (status 0, or any exit the server itself caused — a closed
|
|
28
|
+
* session, the idle reaper, a server shutdown), "crashed" is a non-zero exit
|
|
29
|
+
* status or a terminating signal the program was not asked for, so a routine
|
|
30
|
+
* restart never renders as a failure. Neither is a progress state: they
|
|
31
|
+
* outrank everything, and nothing clears them. A consumer must tolerate an
|
|
32
|
+
* UNKNOWN status string from a newer server: the stream is parsed, not
|
|
33
|
+
* validated, and an unrecognised value is forwarded rather than dropped
|
|
34
|
+
* (forward compatibility across the wire floor). */
|
|
35
|
+
readonly status:
|
|
36
|
+
"working" | "idle" | "input" | "done" | "exited" | "crashed" | "failed" | "warning";
|
|
23
37
|
readonly title: string;
|
|
24
38
|
/** the raw client-derived automatic title (before the precedence baked into
|
|
25
39
|
* `title`); a consumer that treats the program's OSC window title as
|
|
@@ -46,6 +60,18 @@ export interface SessionInfo {
|
|
|
46
60
|
export interface SessionStatus extends SessionInfo {
|
|
47
61
|
/** true when the session is gone (closed or reaped); the consumer drops it. */
|
|
48
62
|
readonly removed?: boolean;
|
|
63
|
+
/** the OSC 9;4 percentage: -1 when absent or unknown (no sequence seen, or a
|
|
64
|
+
* state that carries none — clear/indeterminate), else 0-100. Paired with
|
|
65
|
+
* `status` to render a determinate bar. */
|
|
66
|
+
readonly progressValue?: number;
|
|
67
|
+
/** the OSC 9 notification message, delivered to EVERY subscriber whether or
|
|
68
|
+
* not the server has a status classifier installed. A notification is an
|
|
69
|
+
* EVENT, not a state: it does not latch a status, so a consumer that wants a
|
|
70
|
+
* status out of it decides that itself. */
|
|
71
|
+
readonly notification?: string;
|
|
72
|
+
/** the notification's sequence number (monotonic per session), so a repeated
|
|
73
|
+
* message is still recognised as a new event. */
|
|
74
|
+
readonly notificationSeq?: number;
|
|
49
75
|
}
|
|
50
76
|
|
|
51
77
|
/** StatusStreamCallbacks are the consumer's hooks. Declared as function-typed
|
package/src/types.ts
CHANGED
|
@@ -123,9 +123,11 @@ export interface ResumeAckMessage {
|
|
|
123
123
|
* claimed sent bytes: the input ledger was reclaimed (idle GC / cap
|
|
124
124
|
* eviction), so the server cannot vouch for any previously sent input.
|
|
125
125
|
* The connection module responds deterministically with its designed loss
|
|
126
|
-
* semantic — drop the outbox
|
|
127
|
-
*
|
|
128
|
-
*
|
|
126
|
+
* semantic — drop the outbox rather than replay possibly-duplicate input —
|
|
127
|
+
* and notifies (`onServerRestart`) only when that outbox actually held
|
|
128
|
+
* unacked bytes; a fully-acked session lost nothing and resets silently.
|
|
129
|
+
* Absent on older servers (the `received === 0 && bytesAcked > 0` heuristic
|
|
130
|
+
* remains as their fallback).
|
|
129
131
|
*/
|
|
130
132
|
ledgerLost?: boolean;
|
|
131
133
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Generator for the published, language-neutral wire-compatibility manifest
|
|
2
|
+
// (`web/wire-compatibility.json`).
|
|
3
|
+
//
|
|
4
|
+
// Why it exists: the wire revision and directional floor are published as
|
|
5
|
+
// TypeScript constants (wire-compatibility.ts) and as Go constants
|
|
6
|
+
// (terminal.WireProtocolVersion / MinSupportedClientWireVersion). A consumer
|
|
7
|
+
// that is neither — a Dockerfile, a shell release gate, a Python CI script —
|
|
8
|
+
// had no machine-readable surface to read them from and resorted to scraping
|
|
9
|
+
// the TypeScript source with `sed`, which breaks silently on any reformat.
|
|
10
|
+
//
|
|
11
|
+
// This module renders that surface FROM the existing WIRE_COMPATIBILITY export,
|
|
12
|
+
// so the numbers keep exactly one source of truth in the TypeScript half. The
|
|
13
|
+
// rendered text is checked into the repo (see the drift guard in
|
|
14
|
+
// wire-manifest.test.ts) because the publish pipeline runs no build step: npm
|
|
15
|
+
// and JSR publish the working tree as-is, so a manifest generated at publish
|
|
16
|
+
// time would never exist in the tarball.
|
|
17
|
+
//
|
|
18
|
+
// The manifest deliberately does NOT carry the package version. `npm pkg set
|
|
19
|
+
// version` injects that at publish time, after checkout, with no regeneration
|
|
20
|
+
// step — an embedded version would be frozen at the repo's placeholder (0.1.0)
|
|
21
|
+
// in every published artifact. Consumers read the package version from
|
|
22
|
+
// package.json / jsr.json, which the pipeline does rewrite.
|
|
23
|
+
//
|
|
24
|
+
// Public-artifact contract: see the "Wire compatibility manifest" section of
|
|
25
|
+
// web/README.md for what a consumer may rely on and what counts as a breaking
|
|
26
|
+
// change.
|
|
27
|
+
|
|
28
|
+
import { WIRE_COMPATIBILITY, type WireCompatibility } from "./wire-compatibility.js";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Schema revision of the manifest FILE, independent of the wire protocol
|
|
32
|
+
* revision it reports. A consumer must reject a `schemaVersion` it does not
|
|
33
|
+
* understand rather than guess at the shape. Bumped only for a breaking
|
|
34
|
+
* change to the manifest's own layout (a removed or repurposed field).
|
|
35
|
+
*/
|
|
36
|
+
export const WIRE_MANIFEST_SCHEMA_VERSION = 1;
|
|
37
|
+
|
|
38
|
+
/** Path of the checked-in artifact this module renders, relative to `web/`. */
|
|
39
|
+
export const WIRE_MANIFEST_PATH = "wire-compatibility.json";
|
|
40
|
+
|
|
41
|
+
/** Shape of the generated wire-compatibility manifest. */
|
|
42
|
+
export interface WireManifest {
|
|
43
|
+
/** Schema revision of this file's layout (see WIRE_MANIFEST_SCHEMA_VERSION). */
|
|
44
|
+
readonly schemaVersion: number;
|
|
45
|
+
/** Informational provenance note; not part of the parsed contract. */
|
|
46
|
+
readonly generatedBy: string;
|
|
47
|
+
/** The same values the TypeScript `WIRE_COMPATIBILITY` export carries. */
|
|
48
|
+
readonly wireCompatibility: WireCompatibility;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Builds the manifest object from the live TypeScript constants. Nesting the
|
|
53
|
+
* protocol values under `wireCompatibility` keeps file-level metadata
|
|
54
|
+
* (`schemaVersion`) in a separate namespace from the payload, so a future
|
|
55
|
+
* metadata field can never collide with a protocol field name.
|
|
56
|
+
*/
|
|
57
|
+
export function buildWireManifest(): WireManifest {
|
|
58
|
+
return {
|
|
59
|
+
schemaVersion: WIRE_MANIFEST_SCHEMA_VERSION,
|
|
60
|
+
generatedBy: "web/src/wire-manifest.ts",
|
|
61
|
+
wireCompatibility: {
|
|
62
|
+
protocolVersion: WIRE_COMPATIBILITY.protocolVersion,
|
|
63
|
+
minimumServerProtocolVersion: WIRE_COMPATIBILITY.minimumServerProtocolVersion,
|
|
64
|
+
incompatibleCloseCode: WIRE_COMPATIBILITY.incompatibleCloseCode,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Renders the exact bytes of the checked-in manifest: two-space indentation and
|
|
71
|
+
* a trailing newline, which is what `prettier --check` requires of a JSON file
|
|
72
|
+
* in this repo. Comparing rendered text (not parsed objects) is what makes the
|
|
73
|
+
* drift guard able to fail on a hand edit that happens to parse the same.
|
|
74
|
+
*/
|
|
75
|
+
export function renderWireManifest(): string {
|
|
76
|
+
return `${JSON.stringify(buildWireManifest(), null, 2)}\n`;
|
|
77
|
+
}
|