nixamp 0.16.0 → 0.17.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.
Files changed (106) hide show
  1. package/README.md +139 -0
  2. package/dist/audio.d.ts +70 -1
  3. package/dist/audio.js +84 -5
  4. package/dist/channels.d.ts +107 -3
  5. package/dist/channels.js +151 -3
  6. package/dist/compression/analyze.d.ts +105 -0
  7. package/dist/compression/analyze.js +213 -0
  8. package/dist/compression/blocks.d.ts +35 -0
  9. package/dist/compression/blocks.js +73 -0
  10. package/dist/compression/cli.d.ts +1 -0
  11. package/dist/compression/cli.js +347 -0
  12. package/dist/compression/codec.d.ts +66 -0
  13. package/dist/compression/codec.js +178 -0
  14. package/dist/compression/envelope.d.ts +77 -0
  15. package/dist/compression/envelope.js +191 -0
  16. package/dist/compression/jobs.d.ts +59 -0
  17. package/dist/compression/jobs.js +120 -0
  18. package/dist/compression/metrics.d.ts +62 -0
  19. package/dist/compression/metrics.js +64 -0
  20. package/dist/compression/policy.d.ts +76 -0
  21. package/dist/compression/policy.js +148 -0
  22. package/dist/compression/receiver.d.ts +51 -0
  23. package/dist/compression/receiver.js +92 -0
  24. package/dist/compression/relay.d.ts +134 -0
  25. package/dist/compression/relay.js +430 -0
  26. package/dist/compression/routes.d.ts +25 -0
  27. package/dist/compression/routes.js +266 -0
  28. package/dist/compression/service.d.ts +162 -0
  29. package/dist/compression/service.js +488 -0
  30. package/dist/compression/static.d.ts +65 -0
  31. package/dist/compression/static.js +248 -0
  32. package/dist/compression/store.d.ts +36 -0
  33. package/dist/compression/store.js +119 -0
  34. package/dist/compression/ts-transform.d.ts +44 -0
  35. package/dist/compression/ts-transform.js +148 -0
  36. package/dist/hls.d.ts +36 -3
  37. package/dist/hls.js +85 -11
  38. package/dist/layouts.d.ts +10 -0
  39. package/dist/layouts.js +71 -1
  40. package/dist/live-api.d.ts +16 -2
  41. package/dist/live-api.js +152 -19
  42. package/dist/live-events.d.ts +57 -2
  43. package/dist/live-events.js +243 -11
  44. package/dist/main.js +47 -0
  45. package/dist/mcp.d.ts +40 -0
  46. package/dist/mcp.js +255 -0
  47. package/dist/oauth-api.d.ts +50 -0
  48. package/dist/oauth-api.js +381 -0
  49. package/dist/oauth-server.d.ts +174 -0
  50. package/dist/oauth-server.js +559 -0
  51. package/dist/party.d.ts +32 -0
  52. package/dist/party.js +196 -0
  53. package/dist/playlist.d.ts +11 -0
  54. package/dist/playlist.js +27 -6
  55. package/dist/server.d.ts +37 -0
  56. package/dist/server.js +277 -45
  57. package/dist/sources.d.ts +16 -8
  58. package/dist/sources.js +102 -0
  59. package/dist/tickets.d.ts +67 -0
  60. package/dist/tickets.js +156 -0
  61. package/dist/tokens.d.ts +27 -2
  62. package/dist/tokens.js +42 -1
  63. package/dist/watch-party.d.ts +127 -0
  64. package/dist/watch-party.js +301 -0
  65. package/package.json +1 -1
  66. package/src/audio.ts +119 -6
  67. package/src/channels.ts +177 -7
  68. package/src/compression/analyze.ts +270 -0
  69. package/src/compression/blocks.ts +93 -0
  70. package/src/compression/cli.ts +340 -0
  71. package/src/compression/codec.ts +202 -0
  72. package/src/compression/envelope.ts +237 -0
  73. package/src/compression/jobs.ts +157 -0
  74. package/src/compression/metrics.ts +109 -0
  75. package/src/compression/policy.ts +171 -0
  76. package/src/compression/receiver.ts +107 -0
  77. package/src/compression/relay.ts +462 -0
  78. package/src/compression/routes.ts +294 -0
  79. package/src/compression/service.ts +525 -0
  80. package/src/compression/static.ts +264 -0
  81. package/src/compression/store.ts +126 -0
  82. package/src/compression/ts-transform.ts +148 -0
  83. package/src/hls.ts +100 -10
  84. package/src/layouts.ts +78 -1
  85. package/src/live-api.ts +172 -17
  86. package/src/live-events.ts +269 -11
  87. package/src/main.ts +47 -0
  88. package/src/mcp.ts +282 -0
  89. package/src/oauth-api.ts +481 -0
  90. package/src/oauth-server.ts +653 -0
  91. package/src/party.ts +216 -0
  92. package/src/playlist.ts +28 -5
  93. package/src/server.ts +296 -44
  94. package/src/sources.ts +100 -0
  95. package/src/tickets.ts +195 -0
  96. package/src/tokens.ts +50 -3
  97. package/src/watch-party.ts +390 -0
  98. package/web/dist/assets/{hls-3VKVEQE3-CrILISPJ.js → hls-3VKVEQE3-eV54kXE3.js} +1 -1
  99. package/web/dist/assets/index-CurZFzlH.css +1 -0
  100. package/web/dist/assets/index-DDzutJ75.js +1 -0
  101. package/web/dist/assets/{mpegts-CWeN63eG.js → mpegts-Buc3Odv6.js} +1 -1
  102. package/web/dist/assets/{mpegts-LO6RVLD6-DaMgRvlO.js → mpegts-LO6RVLD6-DcDKPB4P.js} +1 -1
  103. package/web/dist/index.html +59 -6
  104. package/web/dist/sw.js +6 -6
  105. package/web/dist/assets/index-CQ_m5HqS.css +0 -1
  106. package/web/dist/assets/index-CTxPM5KS.js +0 -1
@@ -0,0 +1,148 @@
1
+ /**
2
+ * What an operator can set, and what a block has to achieve to be sent
3
+ * compressed.
4
+ *
5
+ * Three settings that must never be confused: whether to squeeze the bytes
6
+ * losslessly, how to package HLS, and whether to re-encode the picture.
7
+ * Turning one on never turns another on. The first is the only one this
8
+ * module measures; the other two are carried here so that one document
9
+ * describes a channel, and so that the API can refuse a change that mixes
10
+ * them up.
11
+ */
12
+ import { DEFAULT_MAX_FRAME_BYTES, CEILING_FRAME_BYTES, FRAME_HEADER_BYTES } from "./envelope.js";
13
+ import { MAX_ZSTD_LEVEL, MIN_ZSTD_LEVEL } from "./codec.js";
14
+ export const DEFAULT_LOSSLESS = {
15
+ mode: "off",
16
+ boundary: "channel",
17
+ zstdLevel: 1,
18
+ minSavingsPercent: 3,
19
+ minSavingsBytes: 512,
20
+ maxBlockBytes: DEFAULT_MAX_FRAME_BYTES,
21
+ maxHoldMs: 100,
22
+ resampleAfterMs: 60_000,
23
+ maxChannelQueueBytes: 8 * 1024 * 1024,
24
+ maxListenerQueueBytes: 1024 * 1024,
25
+ tsAware: false,
26
+ };
27
+ export const DEFAULT_POLICY = {
28
+ losslessCompression: DEFAULT_LOSSLESS,
29
+ hlsPackaging: "mpegts",
30
+ qualityProfile: "source",
31
+ version: 0,
32
+ };
33
+ /** Fresh copies, so nobody edits the defaults in place. */
34
+ export function defaultPolicy() {
35
+ return { ...DEFAULT_POLICY, losslessCompression: { ...DEFAULT_LOSSLESS } };
36
+ }
37
+ const RANGES = {
38
+ zstdLevel: [MIN_ZSTD_LEVEL, MAX_ZSTD_LEVEL],
39
+ minSavingsPercent: [0, 100],
40
+ minSavingsBytes: [0, CEILING_FRAME_BYTES],
41
+ maxBlockBytes: [1024, CEILING_FRAME_BYTES],
42
+ maxHoldMs: [1, 10_000],
43
+ resampleAfterMs: [1000, 24 * 3600 * 1000],
44
+ maxChannelQueueBytes: [64 * 1024, 1024 * 1024 * 1024],
45
+ maxListenerQueueBytes: [16 * 1024, 1024 * 1024 * 1024],
46
+ };
47
+ /**
48
+ * A change, applied over what is there, checked field by field.
49
+ *
50
+ * Unknown keys are errors rather than ignored: a typo that is silently
51
+ * dropped is a setting the operator believes is on and is not. The version
52
+ * is not settable here; the store bumps it when it accepts the result.
53
+ */
54
+ export function normalizePolicy(input, base) {
55
+ const errors = [];
56
+ if (typeof input !== "object" || input === null || Array.isArray(input))
57
+ return { ok: false, errors: ["a policy is an object"] };
58
+ const change = input;
59
+ const policy = { ...base, losslessCompression: { ...base.losslessCompression } };
60
+ for (const key of Object.keys(change)) {
61
+ if (!["losslessCompression", "hlsPackaging", "qualityProfile", "version"].includes(key))
62
+ errors.push(`unknown setting: ${key}`);
63
+ }
64
+ if ("hlsPackaging" in change) {
65
+ const value = change["hlsPackaging"];
66
+ if (value === "mpegts" || value === "fmp4")
67
+ policy.hlsPackaging = value;
68
+ else
69
+ errors.push("hlsPackaging must be mpegts or fmp4");
70
+ }
71
+ if ("qualityProfile" in change) {
72
+ if (change["qualityProfile"] === "source")
73
+ policy.qualityProfile = "source";
74
+ else
75
+ errors.push("qualityProfile: only source is available; lower-bitrate profiles are a separate, explicit setting that this version does not ship");
76
+ }
77
+ if ("losslessCompression" in change) {
78
+ const lc = change["losslessCompression"];
79
+ if (typeof lc !== "object" || lc === null || Array.isArray(lc)) {
80
+ errors.push("losslessCompression is an object");
81
+ }
82
+ else {
83
+ const fields = lc;
84
+ for (const [key, value] of Object.entries(fields)) {
85
+ if (key === "mode") {
86
+ if (value === "off" || value === "auto" || value === "zstd")
87
+ policy.losslessCompression.mode = value;
88
+ else
89
+ errors.push("losslessCompression.mode must be off, auto or zstd");
90
+ }
91
+ else if (key === "boundary") {
92
+ if (value === "channel" || value === "source")
93
+ policy.losslessCompression.boundary = value;
94
+ else
95
+ errors.push("losslessCompression.boundary must be channel or source");
96
+ }
97
+ else if (key === "tsAware") {
98
+ if (typeof value === "boolean")
99
+ policy.losslessCompression.tsAware = value;
100
+ else
101
+ errors.push("losslessCompression.tsAware must be true or false");
102
+ }
103
+ else if (key in RANGES) {
104
+ const [low, high] = RANGES[key];
105
+ if (typeof value === "number" && Number.isInteger(value) && value >= low && value <= high) {
106
+ policy.losslessCompression[key] = value;
107
+ }
108
+ else {
109
+ errors.push(`losslessCompression.${key} must be an integer from ${low} to ${high}`);
110
+ }
111
+ }
112
+ else {
113
+ errors.push(`unknown setting: losslessCompression.${key}`);
114
+ }
115
+ }
116
+ }
117
+ }
118
+ if (policy.losslessCompression.minSavingsBytes >= policy.losslessCompression.maxBlockBytes) {
119
+ errors.push("losslessCompression.minSavingsBytes must be smaller than maxBlockBytes, or nothing can ever qualify");
120
+ }
121
+ return errors.length > 0 ? { ok: false, errors } : { ok: true, policy };
122
+ }
123
+ /**
124
+ * Whether a compressed block earns its place.
125
+ *
126
+ * Both representations carry the same frame header, so the comparison of
127
+ * complete representations reduces to the payloads: the saving is what the
128
+ * original would have cost stored, less what the encoding costs. Both
129
+ * thresholds must hold. A tiny block can meet the percentage and still not
130
+ * be worth the decode; a huge one can save half a kilobyte and still be
131
+ * nothing.
132
+ */
133
+ export function eligible(originalLength, encodedLength, policy) {
134
+ if (originalLength <= 0)
135
+ return false;
136
+ const saved = originalLength - encodedLength;
137
+ if (saved < policy.minSavingsBytes)
138
+ return false;
139
+ return (saved * 100) / originalLength >= policy.minSavingsPercent;
140
+ }
141
+ /** What a block costs on the wire in each form, header included. */
142
+ export function wireBytes(payloadLength) {
143
+ return FRAME_HEADER_BYTES + payloadLength;
144
+ }
145
+ /** Whether two policies would produce the same bytes for the same input. */
146
+ export function variantOf(policy) {
147
+ return `${policy.mode}:${policy.boundary}:${policy.zstdLevel}:${policy.tsAware ? "ts" : "plain"}:${policy.maxBlockBytes}`;
148
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The receiving end of a relay, as a client: ask another nixamp for a
3
+ * channel in the envelope, decode it, hand the original bytes on.
4
+ *
5
+ * The negotiation is two headers. `Accept` names the envelope's media type,
6
+ * which is how the server knows this is a receiver and not a browser that
7
+ * followed a link; `X-Nixamp-Stream-Codecs` lists what this decoder can
8
+ * undo, and the server compresses with nothing outside that list. A server
9
+ * that will not or cannot relay answers with JSON and an ordinary status,
10
+ * which is surfaced as an error naming the reason, never as a stream of
11
+ * something else.
12
+ */
13
+ import { type Mode, type StreamHeader } from "./envelope.ts";
14
+ export declare const CODECS_HEADER = "x-nixamp-stream-codecs";
15
+ export declare const KIND_HEADER = "x-nixamp-kind";
16
+ export declare const KEY_HEADER = "x-nixamp-key";
17
+ export interface ReceiveOptions {
18
+ url: string;
19
+ key: string | null;
20
+ /** What this receiver can decode. Stored is always implied. */
21
+ modes?: Mode[];
22
+ maxFrameBytes?: number;
23
+ /** The server said yes: what it will compress with, and what the channel carries. Before any byte. */
24
+ onStart?: (accepted: {
25
+ codecs: string;
26
+ kind: "audio" | "video" | "";
27
+ }) => void;
28
+ /** The stream header arrived: the generation this is. */
29
+ onHeader?: (header: StreamHeader) => void;
30
+ onBytes: (bytes: Buffer) => void | Promise<void>;
31
+ signal?: AbortSignal;
32
+ /** Injected by tests. */
33
+ fetchImpl?: typeof fetch;
34
+ }
35
+ export interface ReceiveResult {
36
+ generation: number;
37
+ frames: number;
38
+ bytes: number;
39
+ }
40
+ /** The server said no. `status` is what it said it with. */
41
+ export declare class RelayRefused extends Error {
42
+ readonly status: number;
43
+ constructor(status: number, message: string);
44
+ }
45
+ /**
46
+ * Pull one generation of a relay to its clean end. Resolves when the end
47
+ * frame arrives; rejects with a RelayError for a stream that broke a rule
48
+ * or stopped short, and with RelayRefused for a server that would not
49
+ * start one.
50
+ */
51
+ export declare function receiveRelay(options: ReceiveOptions): Promise<ReceiveResult>;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * The receiving end of a relay, as a client: ask another nixamp for a
3
+ * channel in the envelope, decode it, hand the original bytes on.
4
+ *
5
+ * The negotiation is two headers. `Accept` names the envelope's media type,
6
+ * which is how the server knows this is a receiver and not a browser that
7
+ * followed a link; `X-Nixamp-Stream-Codecs` lists what this decoder can
8
+ * undo, and the server compresses with nothing outside that list. A server
9
+ * that will not or cannot relay answers with JSON and an ordinary status,
10
+ * which is surfaced as an error naming the reason, never as a stream of
11
+ * something else.
12
+ */
13
+ import { MEDIA_TYPE, RelayError } from "./envelope.js";
14
+ import { RelayDecoder } from "./relay.js";
15
+ export const CODECS_HEADER = "x-nixamp-stream-codecs";
16
+ export const KIND_HEADER = "x-nixamp-kind";
17
+ export const KEY_HEADER = "x-nixamp-key";
18
+ /** The server said no. `status` is what it said it with. */
19
+ export class RelayRefused extends Error {
20
+ status;
21
+ constructor(status, message) {
22
+ super(message);
23
+ this.status = status;
24
+ this.name = "RelayRefused";
25
+ }
26
+ }
27
+ /**
28
+ * Pull one generation of a relay to its clean end. Resolves when the end
29
+ * frame arrives; rejects with a RelayError for a stream that broke a rule
30
+ * or stopped short, and with RelayRefused for a server that would not
31
+ * start one.
32
+ */
33
+ export async function receiveRelay(options) {
34
+ const modes = new Set(["stored", ...(options.modes ?? ["zstd", "ts-zstd"])]);
35
+ const headers = {
36
+ accept: MEDIA_TYPE,
37
+ [CODECS_HEADER]: [...modes].join(","),
38
+ };
39
+ if (options.key)
40
+ headers[KEY_HEADER] = options.key;
41
+ const send = options.fetchImpl ?? fetch;
42
+ const response = await send(options.url, { headers, ...(options.signal ? { signal: options.signal } : {}) });
43
+ const type = response.headers.get("content-type") ?? "";
44
+ if (response.status !== 200 || !type.startsWith(MEDIA_TYPE)) {
45
+ let reason = `${response.status}`;
46
+ try {
47
+ const body = (await response.json());
48
+ if (typeof body.error === "string")
49
+ reason = body.error;
50
+ }
51
+ catch {
52
+ // Not JSON; the status is the message.
53
+ }
54
+ throw new RelayRefused(response.status, reason);
55
+ }
56
+ if (!response.body)
57
+ throw new RelayRefused(response.status, "no body");
58
+ const kindSaid = response.headers.get(KIND_HEADER);
59
+ options.onStart?.({
60
+ codecs: response.headers.get(CODECS_HEADER) ?? "",
61
+ kind: kindSaid === "audio" || kindSaid === "video" ? kindSaid : "",
62
+ });
63
+ const decoder = new RelayDecoder({
64
+ modes,
65
+ ...(options.maxFrameBytes !== undefined ? { maxFrameBytes: options.maxFrameBytes } : {}),
66
+ onBytes: options.onBytes,
67
+ });
68
+ let told = false;
69
+ const reader = response.body.getReader();
70
+ try {
71
+ for (;;) {
72
+ const { value, done } = await reader.read();
73
+ if (done)
74
+ break;
75
+ await decoder.feed(Buffer.from(value));
76
+ if (!told && decoder.header) {
77
+ told = true;
78
+ options.onHeader?.(decoder.header);
79
+ }
80
+ }
81
+ }
82
+ catch (error) {
83
+ if (options.signal?.aborted)
84
+ throw new RelayError("TRUNCATED", "cancelled");
85
+ throw error;
86
+ }
87
+ finally {
88
+ reader.releaseLock();
89
+ }
90
+ await decoder.end();
91
+ return { generation: decoder.header?.generation ?? 0, frames: decoder.frames, bytes: decoder.bytes };
92
+ }
@@ -0,0 +1,134 @@
1
+ import { Pool } from "./codec.ts";
2
+ import { type Boundary, type Mode, type StreamHeader } from "./envelope.ts";
3
+ import type { ChannelMetrics, FallbackReason } from "./metrics.ts";
4
+ import { type LosslessPolicy } from "./policy.ts";
5
+ /** Somewhere frames go: a response, in practice. `pending` is how far behind it is. */
6
+ export interface RelayListener {
7
+ write(chunk: Buffer): boolean;
8
+ end(): void;
9
+ pending?(): number;
10
+ }
11
+ export interface EncodedBlock {
12
+ /** Offset of this block's first byte since the encoder attached. */
13
+ start: number;
14
+ mode: Mode;
15
+ original: Buffer;
16
+ digest: Buffer;
17
+ payload: Buffer;
18
+ }
19
+ /** How many blocks in a row may fail to pay before `auto` stops trying, for a while. */
20
+ export declare const GIVE_UP_AFTER = 8;
21
+ export interface RelayEncoderOptions {
22
+ generation: number;
23
+ policy: LosslessPolicy;
24
+ pool: Pool;
25
+ metrics: ChannelMetrics;
26
+ boundary: Boundary;
27
+ /** The compressor fell too far behind, or the channel ended: this relay is over. */
28
+ onAbort: (reason: FallbackReason | "ended") => void;
29
+ /** The last session left. */
30
+ onIdle: () => void;
31
+ now?: () => number;
32
+ }
33
+ export declare class RelayEncoder {
34
+ private readonly options;
35
+ private attached;
36
+ private readonly blocker;
37
+ private chain;
38
+ private pushed;
39
+ private readonly sessions;
40
+ private ineligibleRun;
41
+ private bypassUntil;
42
+ private closed;
43
+ private readonly now;
44
+ readonly streamHeader: Buffer;
45
+ constructor(options: RelayEncoderOptions);
46
+ get generation(): number;
47
+ get sessionCount(): number;
48
+ /**
49
+ * Call once `Channel.listen` has returned. What it wrote before then was
50
+ * the opening bytes, which every session gets for itself as its preface;
51
+ * taking them here too would send them twice to the first session.
52
+ */
53
+ attach(): void;
54
+ /** The channel's listener face. */
55
+ write(chunk: Buffer): boolean;
56
+ /** The channel ended, or started over: finish every session cleanly. */
57
+ end(): void;
58
+ /**
59
+ * A new receiver. `preface` is what the channel would write first to an
60
+ * ordinary listener, snapshotted by the caller in the same tick as this
61
+ * call, so that the shared blocks from here on follow it exactly.
62
+ */
63
+ join(listener: RelayListener, preface: Buffer[]): RelaySession;
64
+ private onBlock;
65
+ /**
66
+ * The bytes in the mode the policy picks: compressed when that pays,
67
+ * stored when it does not or cannot be done in time.
68
+ */
69
+ encodeBytes(bytes: Buffer): Promise<{
70
+ mode: Mode;
71
+ payload: Buffer;
72
+ }>;
73
+ private abort;
74
+ }
75
+ /** One receiver's view of the relay: its own frame numbers, its own end marker. */
76
+ export declare class RelaySession {
77
+ private readonly listener;
78
+ private readonly encoder;
79
+ private readonly from;
80
+ private readonly maxQueueBytes;
81
+ private readonly onClose;
82
+ private seq;
83
+ private total;
84
+ private readonly digest;
85
+ private queue;
86
+ private queuedBytes;
87
+ private ready;
88
+ closed: boolean;
89
+ constructor(listener: RelayListener, encoder: RelayEncoder, from: number, maxQueueBytes: number, onClose: (session: RelaySession, reason: FallbackReason | null) => void);
90
+ start(preface: Buffer[]): Promise<void>;
91
+ private get maxBlockBytes();
92
+ deliver(block: EncodedBlock): void;
93
+ private send;
94
+ /** Clean end: the end frame, then the socket. */
95
+ finish(): void;
96
+ close(reason: FallbackReason): void;
97
+ /** The receiver hung up. Not a fault of anybody's; nothing is counted against it. */
98
+ leave(): void;
99
+ private put;
100
+ }
101
+ export interface DecoderOptions {
102
+ /** What this decoder can undo. Anything else in a frame is refused. */
103
+ modes: ReadonlySet<Mode>;
104
+ /** The most a stream may negotiate; a header asking for more is refused. */
105
+ maxFrameBytes?: number;
106
+ /** The boundary the caller expects, when it matters. */
107
+ boundary?: Boundary;
108
+ onBytes: (bytes: Buffer) => void | Promise<void>;
109
+ }
110
+ /**
111
+ * The receiving end: bytes in, original bytes out, every rule checked on
112
+ * the way. Frames are validated before their payload is read, payloads are
113
+ * decoded under the size the frame promised, and the result is checked
114
+ * against the length and the digest before it is handed on. A stream that
115
+ * stops without its end frame is reported as cut off by `end()`.
116
+ */
117
+ export declare class RelayDecoder {
118
+ private readonly options;
119
+ header: StreamHeader | null;
120
+ frames: number;
121
+ bytes: number;
122
+ ended: boolean;
123
+ private buffer;
124
+ private seq;
125
+ private pending;
126
+ private readonly digest;
127
+ private busy;
128
+ constructor(options: DecoderOptions);
129
+ /** Feed bytes as they arrive. Serialised, so callers need not await between chunks, though they may. */
130
+ feed(chunk: Buffer): Promise<void>;
131
+ private consume;
132
+ /** The connection closed. Fine after the end frame; a cut-off otherwise. */
133
+ end(): Promise<void>;
134
+ }