@zhin.js/host-http 1.1.17 → 1.2.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 +60 -21
- package/lib/access-control.d.ts +127 -0
- package/lib/access-control.js +148 -0
- package/lib/addresses.d.ts +9 -0
- package/lib/addresses.js +9 -0
- package/lib/bridge/bridged-host.d.ts +43 -0
- package/lib/bridge/bridged-host.js +497 -0
- package/lib/bridge/control-channel.d.ts +59 -0
- package/lib/bridge/control-channel.js +131 -0
- package/lib/bridge/flow.d.ts +58 -0
- package/lib/bridge/flow.js +188 -0
- package/lib/bridge/index.d.ts +6 -0
- package/lib/bridge/index.js +6 -0
- package/lib/bridge/instance-transport.d.ts +112 -0
- package/lib/bridge/instance-transport.js +532 -0
- package/lib/bridge/protocol.d.ts +242 -0
- package/lib/bridge/protocol.js +369 -0
- package/lib/console-events.d.ts +5 -3
- package/lib/console-events.js +53 -27
- package/lib/console-plugin-rpc.d.ts +5 -1
- package/lib/console-plugin-rpc.js +5 -40
- package/lib/console-rest-pages-helpers.d.ts +4 -5
- package/lib/console-rest-pages-helpers.js +62 -79
- package/lib/console-rest-pages.js +80 -65
- package/lib/console-rpc-extended/dispatcher.js +1 -6
- package/lib/console-rpc-handlers.d.ts +0 -2
- package/lib/console-rpc-handlers.js +2 -40
- package/lib/console-rpc.d.ts +6 -14
- package/lib/console-rpc.js +6 -6
- package/lib/host-installer.d.ts +6 -3
- package/lib/host-installer.js +10 -40
- package/lib/host-options.d.ts +47 -0
- package/lib/host-options.js +1 -0
- package/lib/http-host.d.ts +124 -48
- package/lib/http-host.js +319 -332
- package/lib/index.d.ts +10 -5
- package/lib/index.js +9 -5
- package/lib/json-body.d.ts +1 -2
- package/lib/json-body.js +1 -2
- package/lib/marketplace-registry.d.ts +17 -0
- package/lib/marketplace-registry.js +52 -0
- package/lib/node/index.d.ts +10 -0
- package/lib/node/index.js +10 -0
- package/lib/{listener-group.d.ts → node/listener-group.d.ts} +2 -3
- package/lib/{listener-group.js → node/listener-group.js} +6 -10
- package/lib/node/listener.d.ts +26 -0
- package/lib/node/listener.js +231 -0
- package/lib/node/node-http.d.ts +38 -0
- package/lib/node/node-http.js +272 -0
- package/lib/node/server.d.ts +13 -0
- package/lib/node/server.js +64 -0
- package/lib/protocol-auth.d.ts +24 -0
- package/lib/protocol-auth.js +48 -0
- package/lib/public-api.d.ts +1 -1
- package/lib/token-registry.d.ts +54 -36
- package/lib/token-registry.js +133 -126
- package/lib/trust-proxy.d.ts +22 -0
- package/lib/trust-proxy.js +168 -0
- package/package.json +14 -6
- package/src/access-control.ts +255 -0
- package/src/addresses.ts +16 -0
- package/src/bridge/bridged-host.ts +548 -0
- package/src/bridge/control-channel.ts +173 -0
- package/src/bridge/flow.ts +208 -0
- package/src/bridge/index.ts +31 -0
- package/src/bridge/instance-transport.ts +627 -0
- package/src/bridge/protocol.ts +574 -0
- package/src/console-events.ts +59 -28
- package/src/console-plugin-rpc.ts +5 -51
- package/src/console-rest-pages-helpers.ts +60 -82
- package/src/console-rest-pages.ts +87 -67
- package/src/console-rpc-extended/dispatcher.ts +1 -6
- package/src/console-rpc-handlers.ts +2 -31
- package/src/console-rpc.ts +10 -19
- package/src/host-installer.ts +12 -44
- package/src/host-options.ts +47 -0
- package/src/http-host.ts +467 -421
- package/src/index.ts +55 -13
- package/src/json-body.ts +7 -2
- package/src/marketplace-registry.ts +62 -0
- package/src/node/index.ts +21 -0
- package/src/{listener-group.ts → node/listener-group.ts} +13 -24
- package/src/node/listener.ts +265 -0
- package/src/node/node-http.ts +263 -0
- package/src/node/server.ts +82 -0
- package/src/protocol-auth.ts +52 -0
- package/src/public-api.ts +2 -0
- package/src/token-registry.ts +143 -146
- package/src/trust-proxy.ts +178 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Instance side of the gateway control channel (ADR 0054 §3).
|
|
3
|
+
*
|
|
4
|
+
* Wraps the child-process IPC port (`process.send` / `process.on('message')`). The channel is
|
|
5
|
+
* unref'd so an idle IPC pipe never keeps a stopped instance alive.
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
BridgeProtocolMismatchError,
|
|
9
|
+
INSTANCE_BRIDGE_PROTOCOL_VERSION,
|
|
10
|
+
INSTANCE_CHILD_ENV,
|
|
11
|
+
isBridgeProtocolCompatible,
|
|
12
|
+
parseGatewayMessage,
|
|
13
|
+
type BridgeRouteTable,
|
|
14
|
+
type GatewayShutdownPayload,
|
|
15
|
+
type GatewayToInstanceMessage,
|
|
16
|
+
type InstanceToGatewayMessage,
|
|
17
|
+
} from './protocol.js';
|
|
18
|
+
|
|
19
|
+
/** The subset of `process` the control channel needs; tests pass a fake. */
|
|
20
|
+
export interface InstanceIpcPort {
|
|
21
|
+
send?(message: unknown, callback?: (error: Error | null) => void): boolean;
|
|
22
|
+
on(event: 'message', listener: (message: unknown) => void): unknown;
|
|
23
|
+
on(event: 'disconnect', listener: () => void): unknown;
|
|
24
|
+
off(event: 'message', listener: (message: unknown) => void): unknown;
|
|
25
|
+
off(event: 'disconnect', listener: () => void): unknown;
|
|
26
|
+
readonly connected?: boolean;
|
|
27
|
+
readonly channel?: { unref?(): void; ref?(): void } | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface InstanceReadyDetails {
|
|
31
|
+
readonly generation?: number;
|
|
32
|
+
/** Route table at ready time (ADR 0054 §3 `ready { routes }`). */
|
|
33
|
+
readonly routes?: BridgeRouteTable;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface InstanceControlChannel {
|
|
37
|
+
/** Announces that startup completed (carries the protocol version). */
|
|
38
|
+
ready(details?: InstanceReadyDetails): void;
|
|
39
|
+
/** Asks the gateway for a whole-process restart; replaces the former exit code 75. */
|
|
40
|
+
requestRestart(reason: string): void;
|
|
41
|
+
onShutdown(listener: (payload: GatewayShutdownPayload) => void): () => void;
|
|
42
|
+
/** Every validated gateway message (bridge streams, addresses, shutdown). */
|
|
43
|
+
onMessage(listener: (message: GatewayToInstanceMessage) => void): () => void;
|
|
44
|
+
/** Sends one bridge message; dropped once the channel is gone. */
|
|
45
|
+
send(message: InstanceToGatewayMessage): void;
|
|
46
|
+
/**
|
|
47
|
+
* Keeps the process alive through the channel (as a listening server would) until the
|
|
48
|
+
* returned release runs. Holds are counted; the channel is unref'd again at zero.
|
|
49
|
+
*/
|
|
50
|
+
hold(): () => void;
|
|
51
|
+
/** Protocol version the gateway announced through {@link INSTANCE_CHILD_ENV}, when numeric. */
|
|
52
|
+
readonly gatewayVersion?: number;
|
|
53
|
+
/** The gateway went away (the IPC channel closed). */
|
|
54
|
+
onDisconnect(listener: () => void): () => void;
|
|
55
|
+
readonly connected: boolean;
|
|
56
|
+
close(): void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Opens the control channel when the gateway started this process: the IPC channel must be
|
|
61
|
+
* present and {@link INSTANCE_CHILD_ENV} set. Returns `undefined` otherwise (not supervised).
|
|
62
|
+
*
|
|
63
|
+
* The marker matters: other parents also open an IPC channel (test runners' fork workers,
|
|
64
|
+
* process managers). Treating their channel as the gateway would send control messages to
|
|
65
|
+
* them and exit the process on a restart request.
|
|
66
|
+
*
|
|
67
|
+
* Throws {@link BridgeProtocolMismatchError} when the marker names a protocol version this build
|
|
68
|
+
* cannot speak: the instance must not start against an incompatible gateway.
|
|
69
|
+
*/
|
|
70
|
+
export function openInstanceControlChannel(
|
|
71
|
+
port: InstanceIpcPort = process as unknown as InstanceIpcPort,
|
|
72
|
+
pid: number = process.pid,
|
|
73
|
+
environment: Readonly<Record<string, string | undefined>> = process.env,
|
|
74
|
+
): InstanceControlChannel | undefined {
|
|
75
|
+
const marker = environment[INSTANCE_CHILD_ENV];
|
|
76
|
+
if (!marker) return undefined;
|
|
77
|
+
if (typeof port.send !== 'function' || port.connected === false) return undefined;
|
|
78
|
+
const gatewayVersion = /^\d+$/u.test(marker) ? Number(marker) : undefined;
|
|
79
|
+
if (gatewayVersion !== undefined && !isBridgeProtocolCompatible(gatewayVersion)) {
|
|
80
|
+
throw new BridgeProtocolMismatchError(gatewayVersion, 'gateway');
|
|
81
|
+
}
|
|
82
|
+
port.channel?.unref?.();
|
|
83
|
+
const shutdownListeners = new Set<(payload: GatewayShutdownPayload) => void>();
|
|
84
|
+
const messageListeners = new Set<(message: GatewayToInstanceMessage) => void>();
|
|
85
|
+
const disconnectListeners = new Set<() => void>();
|
|
86
|
+
let connected = true;
|
|
87
|
+
let closed = false;
|
|
88
|
+
let holds = 0;
|
|
89
|
+
const releaseHold = (): void => {
|
|
90
|
+
holds -= 1;
|
|
91
|
+
if (holds === 0) port.channel?.unref?.();
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const onMessage = (raw: unknown): void => {
|
|
95
|
+
const message = parseGatewayMessage(raw);
|
|
96
|
+
if (!message) return;
|
|
97
|
+
for (const listener of [...messageListeners]) {
|
|
98
|
+
try { listener(message); } catch { /* one consumer never breaks the channel */ }
|
|
99
|
+
}
|
|
100
|
+
if (message.type !== 'shutdown') return;
|
|
101
|
+
for (const listener of [...shutdownListeners]) listener(message.payload);
|
|
102
|
+
};
|
|
103
|
+
const onDisconnect = (): void => {
|
|
104
|
+
connected = false;
|
|
105
|
+
for (const listener of [...disconnectListeners]) listener();
|
|
106
|
+
};
|
|
107
|
+
port.on('message', onMessage);
|
|
108
|
+
port.on('disconnect', onDisconnect);
|
|
109
|
+
|
|
110
|
+
const send = (message: InstanceToGatewayMessage): void => {
|
|
111
|
+
if (!connected || closed) return;
|
|
112
|
+
try {
|
|
113
|
+
// The callback swallows EPIPE when the gateway is already gone.
|
|
114
|
+
port.send?.(message, () => undefined);
|
|
115
|
+
} catch { /* channel closed */ }
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
ready(details = {}) {
|
|
120
|
+
send({
|
|
121
|
+
type: 'ready',
|
|
122
|
+
payload: {
|
|
123
|
+
version: INSTANCE_BRIDGE_PROTOCOL_VERSION,
|
|
124
|
+
pid,
|
|
125
|
+
...(details.generation !== undefined ? { generation: details.generation } : {}),
|
|
126
|
+
...(details.routes ? { routes: details.routes } : {}),
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
send,
|
|
131
|
+
hold() {
|
|
132
|
+
if (closed) return () => undefined;
|
|
133
|
+
holds += 1;
|
|
134
|
+
if (holds === 1) port.channel?.ref?.();
|
|
135
|
+
let released = false;
|
|
136
|
+
return () => {
|
|
137
|
+
if (released) return;
|
|
138
|
+
released = true;
|
|
139
|
+
releaseHold();
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
onMessage(listener) {
|
|
143
|
+
messageListeners.add(listener);
|
|
144
|
+
return () => { messageListeners.delete(listener); };
|
|
145
|
+
},
|
|
146
|
+
...(gatewayVersion !== undefined ? { gatewayVersion } : {}),
|
|
147
|
+
requestRestart(reason) {
|
|
148
|
+
send({ type: 'restart-request', payload: { reason } });
|
|
149
|
+
},
|
|
150
|
+
onShutdown(listener) {
|
|
151
|
+
shutdownListeners.add(listener);
|
|
152
|
+
return () => { shutdownListeners.delete(listener); };
|
|
153
|
+
},
|
|
154
|
+
onDisconnect(listener) {
|
|
155
|
+
disconnectListeners.add(listener);
|
|
156
|
+
return () => { disconnectListeners.delete(listener); };
|
|
157
|
+
},
|
|
158
|
+
get connected() { return connected && !closed; },
|
|
159
|
+
close() {
|
|
160
|
+
if (closed) return;
|
|
161
|
+
closed = true;
|
|
162
|
+
if (holds > 0) {
|
|
163
|
+
holds = 0;
|
|
164
|
+
port.channel?.unref?.();
|
|
165
|
+
}
|
|
166
|
+
port.off('message', onMessage);
|
|
167
|
+
port.off('disconnect', onDisconnect);
|
|
168
|
+
shutdownListeners.clear();
|
|
169
|
+
messageListeners.clear();
|
|
170
|
+
disconnectListeners.clear();
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credit-based body streaming shared by both ends of the bridge (ADR 0054 §3).
|
|
3
|
+
*
|
|
4
|
+
* The sender ({@link pumpBody}) never has more than its credit in flight; the receiver
|
|
5
|
+
* ({@link InboundBody}) buffers at most the window and grants credit as its consumer reads.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A copy that owns its whole `ArrayBuffer`. Structured clone serializes a view's entire backing
|
|
10
|
+
* buffer, so a pooled `Buffer` or a `subarray` would otherwise ship unrelated bytes.
|
|
11
|
+
*/
|
|
12
|
+
export function ownBytes(chunk: Uint8Array): Uint8Array {
|
|
13
|
+
if (chunk.byteOffset === 0 && chunk.byteLength === chunk.buffer.byteLength && !(chunk.buffer instanceof SharedArrayBuffer)) {
|
|
14
|
+
return Object.getPrototypeOf(chunk) === Uint8Array.prototype ? chunk : new Uint8Array(chunk.buffer, 0, chunk.byteLength);
|
|
15
|
+
}
|
|
16
|
+
return new Uint8Array(chunk);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class BridgeStreamClosedError extends Error {
|
|
20
|
+
constructor(message = 'Bridge stream closed') {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = 'BridgeStreamClosedError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Sender-side credit: {@link take} waits while the receiver has not granted more. */
|
|
27
|
+
export class CreditGate {
|
|
28
|
+
#credit: number;
|
|
29
|
+
#waiter: (() => void) | undefined;
|
|
30
|
+
#closed: Error | undefined;
|
|
31
|
+
|
|
32
|
+
constructor(initial: number) {
|
|
33
|
+
this.#credit = initial;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get available(): number { return this.#credit; }
|
|
37
|
+
|
|
38
|
+
add(bytes: number): void {
|
|
39
|
+
this.#credit += bytes;
|
|
40
|
+
this.#wake();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Resolves with up to `max` bytes of credit (at least 1); rejects once closed. */
|
|
44
|
+
async take(max: number): Promise<number> {
|
|
45
|
+
while (this.#credit <= 0) {
|
|
46
|
+
if (this.#closed) throw this.#closed;
|
|
47
|
+
await new Promise<void>((resolve) => { this.#waiter = resolve; });
|
|
48
|
+
}
|
|
49
|
+
if (this.#closed) throw this.#closed;
|
|
50
|
+
const granted = Math.min(max, this.#credit);
|
|
51
|
+
this.#credit -= granted;
|
|
52
|
+
return granted;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
close(error: Error = new BridgeStreamClosedError()): void {
|
|
56
|
+
this.#closed ??= error;
|
|
57
|
+
this.#wake();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#wake(): void {
|
|
61
|
+
const waiter = this.#waiter;
|
|
62
|
+
this.#waiter = undefined;
|
|
63
|
+
waiter?.();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface PumpBodyOptions {
|
|
68
|
+
readonly gate: CreditGate;
|
|
69
|
+
readonly maxChunkBytes: number;
|
|
70
|
+
send(chunk: Uint8Array): void;
|
|
71
|
+
/** Aborting stops the pump and cancels the source. */
|
|
72
|
+
readonly signal?: AbortSignal;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Reads `source` to the end and sends it in chunks that fit both the frame limit and the
|
|
77
|
+
* available credit. Rejects when the source errors, the gate closes or `signal` aborts; the
|
|
78
|
+
* source is cancelled in every failure case.
|
|
79
|
+
*/
|
|
80
|
+
export async function pumpBody(source: ReadableStream<Uint8Array>, options: PumpBodyOptions): Promise<void> {
|
|
81
|
+
const reader = source.getReader();
|
|
82
|
+
const onAbort = (): void => {
|
|
83
|
+
options.gate.close(new BridgeStreamClosedError('Bridge stream aborted'));
|
|
84
|
+
void reader.cancel(options.signal?.reason).catch(() => undefined);
|
|
85
|
+
};
|
|
86
|
+
if (options.signal?.aborted) onAbort();
|
|
87
|
+
else options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
88
|
+
try {
|
|
89
|
+
for (;;) {
|
|
90
|
+
const next = await reader.read();
|
|
91
|
+
if (next.done) return;
|
|
92
|
+
const value: unknown = next.value;
|
|
93
|
+
const bytes = value instanceof Uint8Array
|
|
94
|
+
? value
|
|
95
|
+
: typeof value === 'string' ? new TextEncoder().encode(value) : undefined;
|
|
96
|
+
if (!bytes) throw new TypeError('Body chunks must be Uint8Array');
|
|
97
|
+
let offset = 0;
|
|
98
|
+
while (offset < bytes.byteLength) {
|
|
99
|
+
const size = await options.gate.take(Math.min(options.maxChunkBytes, bytes.byteLength - offset));
|
|
100
|
+
options.send(ownBytes(bytes.subarray(offset, offset + size)));
|
|
101
|
+
offset += size;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
} catch (error) {
|
|
105
|
+
await reader.cancel(error).catch(() => undefined);
|
|
106
|
+
throw error;
|
|
107
|
+
} finally {
|
|
108
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
109
|
+
reader.releaseLock();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface InboundBodyOptions {
|
|
114
|
+
/** Credit the sender started with; also the most this body buffers. */
|
|
115
|
+
readonly window: number;
|
|
116
|
+
/** Grants `bytes` more credit to the sender. */
|
|
117
|
+
grant(bytes: number): void;
|
|
118
|
+
/** The consumer cancelled the body; tell the sender to stop. */
|
|
119
|
+
cancel(reason: unknown): void;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Receiver side: a pull-driven `ReadableStream` that grants credit as its consumer reads. */
|
|
123
|
+
export class InboundBody {
|
|
124
|
+
readonly stream: ReadableStream<Uint8Array>;
|
|
125
|
+
readonly #options: InboundBodyOptions;
|
|
126
|
+
readonly #queue: Uint8Array[] = [];
|
|
127
|
+
#buffered = 0;
|
|
128
|
+
#ungranted = 0;
|
|
129
|
+
#ended = false;
|
|
130
|
+
#error: unknown;
|
|
131
|
+
#failed = false;
|
|
132
|
+
#cancelled = false;
|
|
133
|
+
#waiter: (() => void) | undefined;
|
|
134
|
+
|
|
135
|
+
constructor(options: InboundBodyOptions) {
|
|
136
|
+
this.#options = options;
|
|
137
|
+
this.stream = new ReadableStream<Uint8Array>({
|
|
138
|
+
pull: async (controller) => {
|
|
139
|
+
while (this.#queue.length === 0 && !this.#ended && !this.#failed) {
|
|
140
|
+
await new Promise<void>((resolve) => { this.#waiter = resolve; });
|
|
141
|
+
}
|
|
142
|
+
const chunk = this.#queue.shift();
|
|
143
|
+
if (chunk) {
|
|
144
|
+
this.#buffered -= chunk.byteLength;
|
|
145
|
+
this.#consumed(chunk.byteLength);
|
|
146
|
+
controller.enqueue(chunk);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (this.#failed) controller.error(this.#error);
|
|
150
|
+
else controller.close();
|
|
151
|
+
},
|
|
152
|
+
cancel: (reason) => {
|
|
153
|
+
this.#cancelled = true;
|
|
154
|
+
this.#queue.length = 0;
|
|
155
|
+
this.#buffered = 0;
|
|
156
|
+
if (!this.#ended && !this.#failed) this.#options.cancel(reason);
|
|
157
|
+
this.#ended = true;
|
|
158
|
+
},
|
|
159
|
+
}, { highWaterMark: 0 });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Whether the stream reached a terminal state (end, error, cancel). */
|
|
163
|
+
get settled(): boolean { return this.#ended || this.#failed; }
|
|
164
|
+
|
|
165
|
+
get bufferedBytes(): number { return this.#buffered; }
|
|
166
|
+
|
|
167
|
+
/** Queues a chunk; false when the sender exceeded its credit (protocol violation). */
|
|
168
|
+
push(chunk: Uint8Array): boolean {
|
|
169
|
+
if (this.#cancelled || this.settled) return true;
|
|
170
|
+
if (this.#buffered + this.#ungranted + chunk.byteLength > this.#options.window) return false;
|
|
171
|
+
this.#queue.push(chunk);
|
|
172
|
+
this.#buffered += chunk.byteLength;
|
|
173
|
+
this.#wake();
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
end(): void {
|
|
178
|
+
if (this.settled) return;
|
|
179
|
+
this.#ended = true;
|
|
180
|
+
this.#wake();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
fail(error: unknown): void {
|
|
184
|
+
if (this.settled) return;
|
|
185
|
+
this.#failed = true;
|
|
186
|
+
this.#error = error;
|
|
187
|
+
this.#queue.length = 0;
|
|
188
|
+
this.#buffered = 0;
|
|
189
|
+
this.#wake();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
#consumed(bytes: number): void {
|
|
193
|
+
this.#ungranted += bytes;
|
|
194
|
+
// Grant in batches. The sender stalls only with a full window in flight; once the queue is
|
|
195
|
+
// drained all of it is ungranted, so any threshold up to the window can never deadlock.
|
|
196
|
+
if (this.#ungranted >= Math.max(1, Math.floor(this.#options.window / 4))) {
|
|
197
|
+
const granted = this.#ungranted;
|
|
198
|
+
this.#ungranted = 0;
|
|
199
|
+
if (!this.#ended && !this.#failed) this.#options.grant(granted);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
#wake(): void {
|
|
204
|
+
const waiter = this.#waiter;
|
|
205
|
+
this.#waiter = undefined;
|
|
206
|
+
waiter?.();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** `@zhin.js/host-http/bridge` — gateway ↔ instance control channel and HTTP bridge (ADR 0054 §3). */
|
|
2
|
+
export * from './protocol.js';
|
|
3
|
+
export * from './control-channel.js';
|
|
4
|
+
export {
|
|
5
|
+
BridgeStreamClosedError,
|
|
6
|
+
CreditGate,
|
|
7
|
+
InboundBody,
|
|
8
|
+
ownBytes,
|
|
9
|
+
pumpBody,
|
|
10
|
+
type InboundBodyOptions,
|
|
11
|
+
type PumpBodyOptions,
|
|
12
|
+
} from './flow.js';
|
|
13
|
+
export {
|
|
14
|
+
createBridgedHttpHost,
|
|
15
|
+
PRINCIPAL_REVOKED_CLOSE_CODE,
|
|
16
|
+
sendableCloseCode,
|
|
17
|
+
type BridgeChannel,
|
|
18
|
+
type BridgedHttpHost,
|
|
19
|
+
type BridgedHttpHostOptions,
|
|
20
|
+
} from './bridged-host.js';
|
|
21
|
+
export {
|
|
22
|
+
InstanceTransportError,
|
|
23
|
+
IpcInstanceTransport,
|
|
24
|
+
type GatewayInstancePort,
|
|
25
|
+
type InstanceChildProcessLike,
|
|
26
|
+
type InstanceRequestContext,
|
|
27
|
+
type InstanceTransport,
|
|
28
|
+
type InstanceTransportErrorCode,
|
|
29
|
+
type InstanceWsOpenInfo,
|
|
30
|
+
type IpcInstanceTransportOptions,
|
|
31
|
+
} from './instance-transport.js';
|