@statewalker/webrun-http-streams 0.2.5 → 0.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/dist/fetch.d.ts.map +1 -1
- package/dist/http-data.d.ts +15 -0
- package/dist/http-data.d.ts.map +1 -1
- package/dist/index.js +44 -5
- package/package.json +1 -1
- package/src/fetch.ts +8 -29
- package/src/http-data.ts +67 -4
package/dist/fetch.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../src/fetch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,6BAA6B,CAAC;AAE1D,OAAO,EAAE,KAAK,eAAe,EAAwB,MAAM,gBAAgB,CAAC;AAsF5E;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,QAAQ,CAAC,
|
|
1
|
+
{"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../src/fetch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,6BAA6B,CAAC;AAE1D,OAAO,EAAE,KAAK,eAAe,EAAwB,MAAM,gBAAgB,CAAC;AAsF5E;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,QAAQ,CAAC,CAiDnB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,EAChD,OAAO,GAAE,eAAoB,GAC5B,MAAM,CAqDR"}
|
package/dist/http-data.d.ts
CHANGED
|
@@ -9,10 +9,25 @@ export type HttpDataOptions = {
|
|
|
9
9
|
export interface HttpFetchResult {
|
|
10
10
|
envelope: ResponseEnvelope;
|
|
11
11
|
body: AsyncIterable<Uint8Array>;
|
|
12
|
+
/**
|
|
13
|
+
* Abandons the call: the peer's producer is released (its `finally` runs).
|
|
14
|
+
* Leaving `body` early — `break`, `.return()`, a cancelled stream — does
|
|
15
|
+
* the same; this is for a caller that never starts reading it, such as a
|
|
16
|
+
* bodyless response. Optional, so that results built elsewhere (mocks,
|
|
17
|
+
* custom transports) still type-check; `httpFetch` always sets it.
|
|
18
|
+
*/
|
|
19
|
+
release?(): Promise<void>;
|
|
12
20
|
}
|
|
13
21
|
export interface HttpDataHandlerResult {
|
|
14
22
|
envelope: ResponseEnvelope;
|
|
15
23
|
body?: ByteSource;
|
|
24
|
+
/**
|
|
25
|
+
* Called when the caller abandons the response before its end. A body the
|
|
26
|
+
* codec never started pulling cannot be released through the iterator
|
|
27
|
+
* protocol (`.return()` on a generator in suspended start does nothing), so
|
|
28
|
+
* a producer that holds resources says here how to let them go.
|
|
29
|
+
*/
|
|
30
|
+
release?: () => unknown;
|
|
16
31
|
}
|
|
17
32
|
/**
|
|
18
33
|
* Low-level HTTP-over-Duplex handler. Takes the request envelope plus body
|
package/dist/http-data.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http-data.d.ts","sourceRoot":"","sources":["../src/http-data.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,6BAA6B,CAAC;AAK1D,OAAO,KAAK,EACV,UAAU,EAEV,YAAY,EACZ,eAAe,EACf,gBAAgB,EACjB,MAAM,cAAc,CAAC;AAEtB,gEAAgE;AAChE,eAAO,MAAM,iBAAiB,mBAAmB,CAAC;AAElD,MAAM,MAAM,eAAe,GAAG;IAC5B,gFAAgF;IAChF,KAAK,CAAC,EAAE,YAAY,CAAC;CACtB,CAAC;AAEF,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,IAAI,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"http-data.d.ts","sourceRoot":"","sources":["../src/http-data.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,6BAA6B,CAAC;AAK1D,OAAO,KAAK,EACV,UAAU,EAEV,YAAY,EACZ,eAAe,EACf,gBAAgB,EACjB,MAAM,cAAc,CAAC;AAEtB,gEAAgE;AAChE,eAAO,MAAM,iBAAiB,mBAAmB,CAAC;AAElD,MAAM,MAAM,eAAe,GAAG;IAC5B,gFAAgF;IAChF,KAAK,CAAC,EAAE,YAAY,CAAC;CACtB,CAAC;AAEF,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,IAAI,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;IAChC;;;;;;OAMG;IACH,OAAO,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC;CACzB;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,eAAe,GAAG,CAC5B,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,aAAa,CAAC,UAAU,CAAC,KAC5B,OAAO,CAAC,qBAAqB,CAAC,CAAC;AAgGpC;;;;;;;GAOG;AACH,wBAAsB,SAAS,CAC7B,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,eAAe,EACpB,IAAI,CAAC,EAAE,UAAU,EACjB,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,eAAe,CAAC,CAkB1B;AA0BD;;;GAGG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,eAAe,EAAE,OAAO,GAAE,eAAoB,GAAG,MAAM,CAoDzF"}
|
package/dist/index.js
CHANGED
|
@@ -855,11 +855,41 @@ async function throwIfPeerError(result, output) {
|
|
|
855
855
|
async function httpFetch(call, env, body, options = {}) {
|
|
856
856
|
const codec = options.codec ?? defaultCodec;
|
|
857
857
|
const output = call(codec.encodeRequest(env, body));
|
|
858
|
-
const
|
|
858
|
+
const decoded = await codec.decodeResponse(output, { method: env.method });
|
|
859
|
+
const release = async () => {
|
|
860
|
+
try {
|
|
861
|
+
await output.return?.(void 0);
|
|
862
|
+
} catch {}
|
|
863
|
+
};
|
|
864
|
+
const result = {
|
|
865
|
+
envelope: decoded.envelope,
|
|
866
|
+
body: releasingOnExit(decoded.body, release),
|
|
867
|
+
release
|
|
868
|
+
};
|
|
859
869
|
await throwIfPeerError(result, output);
|
|
860
870
|
return result;
|
|
861
871
|
}
|
|
862
872
|
/**
|
|
873
|
+
* `body`, but leaving it before its end releases the call.
|
|
874
|
+
*
|
|
875
|
+
* The body the codec hands back is a generator of its own over a `ByteReader`,
|
|
876
|
+
* and `.return()` on it never reaches the call's `output` (see
|
|
877
|
+
* `throwIfPeerError` for why the codec must not do that itself). So a caller
|
|
878
|
+
* that stopped reading — an aborted `fetch`, a cancelled response stream —
|
|
879
|
+
* left the peer's producer parked forever, holding its handler open. Safe here
|
|
880
|
+
* for the same reason it is safe in `throwIfPeerError`: `output` is this
|
|
881
|
+
* call's own response-only generator, and the request was fully sent.
|
|
882
|
+
*/
|
|
883
|
+
async function* releasingOnExit(body, release) {
|
|
884
|
+
let completed = false;
|
|
885
|
+
try {
|
|
886
|
+
yield* body;
|
|
887
|
+
completed = true;
|
|
888
|
+
} finally {
|
|
889
|
+
if (!completed) await release();
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
863
893
|
* Wrap an HTTP handler as a `Duplex` so it can be registered with any
|
|
864
894
|
* `webrun-streams-*` adapter's `serve`.
|
|
865
895
|
*/
|
|
@@ -882,7 +912,15 @@ function httpServe(handler, options = {}) {
|
|
|
882
912
|
yield* encodeErrorResponse(replyCodec, error, decoded.envelope.method);
|
|
883
913
|
return;
|
|
884
914
|
}
|
|
885
|
-
|
|
915
|
+
let completed = false;
|
|
916
|
+
try {
|
|
917
|
+
yield* replyCodec.encodeResponse(result.envelope, result.body, { method: decoded.envelope.method });
|
|
918
|
+
completed = true;
|
|
919
|
+
} finally {
|
|
920
|
+
if (!completed) try {
|
|
921
|
+
await result.release?.();
|
|
922
|
+
} catch {}
|
|
923
|
+
}
|
|
886
924
|
};
|
|
887
925
|
}
|
|
888
926
|
//#endregion
|
|
@@ -1140,14 +1178,14 @@ async function fetchOverDuplex(call, request, options = {}) {
|
|
|
1140
1178
|
const buffered = new Uint8Array(await request.arrayBuffer());
|
|
1141
1179
|
if (buffered.byteLength > 0) body = [buffered];
|
|
1142
1180
|
}
|
|
1143
|
-
const { envelope, body: respBody } = await httpFetch(call, env, body, options);
|
|
1181
|
+
const { envelope, body: respBody, release } = await httpFetch(call, env, body, options);
|
|
1144
1182
|
const responseInit = {
|
|
1145
1183
|
status: envelope.status,
|
|
1146
1184
|
statusText: envelope.statusText,
|
|
1147
1185
|
headers: forwardableHeaders(envelope.headers)
|
|
1148
1186
|
};
|
|
1149
1187
|
if (request.method === "HEAD" || request.method === "OPTIONS" || NULL_BODY_STATUSES.has(envelope.status)) {
|
|
1150
|
-
await
|
|
1188
|
+
await release?.();
|
|
1151
1189
|
return new Response(null, responseInit);
|
|
1152
1190
|
}
|
|
1153
1191
|
return new Response(asyncIterableToReadable(withAbort(respBody, request.signal)), responseInit);
|
|
@@ -1183,7 +1221,8 @@ function serveFetchOverDuplex(handler, options = {}) {
|
|
|
1183
1221
|
}
|
|
1184
1222
|
return {
|
|
1185
1223
|
envelope: respEnv,
|
|
1186
|
-
body: response.body ? readableToAsyncIterable(response.body) : void 0
|
|
1224
|
+
body: response.body ? readableToAsyncIterable(response.body) : void 0,
|
|
1225
|
+
release: () => response.body?.cancel()
|
|
1187
1226
|
};
|
|
1188
1227
|
}, options);
|
|
1189
1228
|
}
|
package/package.json
CHANGED
package/src/fetch.ts
CHANGED
|
@@ -126,46 +126,21 @@ export async function fetchOverDuplex(
|
|
|
126
126
|
const buffered = new Uint8Array(await request.arrayBuffer());
|
|
127
127
|
if (buffered.byteLength > 0) body = [buffered];
|
|
128
128
|
}
|
|
129
|
-
const { envelope, body: respBody } = await httpFetch(call, env, body, options);
|
|
129
|
+
const { envelope, body: respBody, release } = await httpFetch(call, env, body, options);
|
|
130
130
|
const responseInit: ResponseInit = {
|
|
131
131
|
status: envelope.status,
|
|
132
132
|
statusText: envelope.statusText,
|
|
133
133
|
headers: forwardableHeaders(envelope.headers),
|
|
134
134
|
};
|
|
135
135
|
// `new Response(body, { status })` throws for the null-body-status set, and
|
|
136
|
-
// HEAD/OPTIONS carry no body either, so return a bodyless Response and
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
// KNOWN GAP — this `.return()` does not actually release the producer, and
|
|
140
|
-
// `discard` would not fix it either. Do not read it as a working drain.
|
|
141
|
-
//
|
|
142
|
-
// `respBody` is *not* the generator the codec pulled from. `decodeResponse`
|
|
143
|
-
// pulls from its `input` (the `output` generator returned by `call(...)`) to
|
|
144
|
-
// read the head, and hands back a freshly constructed one —
|
|
145
|
-
// `convertBodyErrors(readBody(...))` in `http1/decode.ts`. Nobody has pulled
|
|
146
|
-
// from that, so it is in suspended start and `.return()` on it is a no-op,
|
|
147
|
-
// exactly as in the two sites `http-stubs.ts` fixed. Measured: the producer's
|
|
148
|
-
// `finally` does not run for 204, 205, 304, HEAD+200 or OPTIONS+200.
|
|
149
|
-
//
|
|
150
|
-
// `discard` does not help because the release has to reach further down.
|
|
151
|
-
// `ByteReader` never propagates `.return()` to its own `#iter`, so only
|
|
152
|
-
// `await output.return()` frees the underlying call — and `fetchOverDuplex`
|
|
153
|
-
// has no handle on `output`, since `httpFetch` returns only `{envelope,
|
|
154
|
-
// body}`. Closing this needs `httpFetch` to expose the call for cancellation;
|
|
155
|
-
// that is a design change, not a patch here.
|
|
156
|
-
//
|
|
157
|
-
// Worth knowing which shapes leak a *populated* body rather than an empty
|
|
158
|
-
// one: `decodeResponse`'s own bodyless test covers status < 200, 204, 304 and
|
|
159
|
-
// HEAD, so for 205 and for OPTIONS the decoder frames a real body that is
|
|
160
|
-
// then abandoned. `http-data.ts` (see `throwIfPeerError`) spells out the cost
|
|
161
|
-
// on a mux transport — one stream-table slot per call, until `maxStreams`.
|
|
136
|
+
// HEAD/OPTIONS carry no body either, so return a bodyless Response and
|
|
137
|
+
// release the call: nobody will read the body, so leaving it cannot.
|
|
162
138
|
if (
|
|
163
139
|
request.method === "HEAD" ||
|
|
164
140
|
request.method === "OPTIONS" ||
|
|
165
141
|
NULL_BODY_STATUSES.has(envelope.status)
|
|
166
142
|
) {
|
|
167
|
-
|
|
168
|
-
await returnable.return?.();
|
|
143
|
+
await release?.();
|
|
169
144
|
return new Response(null, responseInit);
|
|
170
145
|
}
|
|
171
146
|
return new Response(asyncIterableToReadable(withAbort(respBody, request.signal)), responseInit);
|
|
@@ -225,6 +200,10 @@ export function serveFetchOverDuplex(
|
|
|
225
200
|
return {
|
|
226
201
|
envelope: respEnv,
|
|
227
202
|
body: response.body ? readableToAsyncIterable(response.body) : undefined,
|
|
203
|
+
// A body the codec never started reading has no reader to cancel
|
|
204
|
+
// through; cancel the stream itself. Once read, it is locked and this
|
|
205
|
+
// rejects, harmlessly: the reader's own cancellation has run.
|
|
206
|
+
release: () => response.body?.cancel(),
|
|
228
207
|
};
|
|
229
208
|
}, options);
|
|
230
209
|
}
|
package/src/http-data.ts
CHANGED
|
@@ -22,11 +22,26 @@ export type HttpDataOptions = {
|
|
|
22
22
|
export interface HttpFetchResult {
|
|
23
23
|
envelope: ResponseEnvelope;
|
|
24
24
|
body: AsyncIterable<Uint8Array>;
|
|
25
|
+
/**
|
|
26
|
+
* Abandons the call: the peer's producer is released (its `finally` runs).
|
|
27
|
+
* Leaving `body` early — `break`, `.return()`, a cancelled stream — does
|
|
28
|
+
* the same; this is for a caller that never starts reading it, such as a
|
|
29
|
+
* bodyless response. Optional, so that results built elsewhere (mocks,
|
|
30
|
+
* custom transports) still type-check; `httpFetch` always sets it.
|
|
31
|
+
*/
|
|
32
|
+
release?(): Promise<void>;
|
|
25
33
|
}
|
|
26
34
|
|
|
27
35
|
export interface HttpDataHandlerResult {
|
|
28
36
|
envelope: ResponseEnvelope;
|
|
29
37
|
body?: ByteSource;
|
|
38
|
+
/**
|
|
39
|
+
* Called when the caller abandons the response before its end. A body the
|
|
40
|
+
* codec never started pulling cannot be released through the iterator
|
|
41
|
+
* protocol (`.return()` on a generator in suspended start does nothing), so
|
|
42
|
+
* a producer that holds resources says here how to let them go.
|
|
43
|
+
*/
|
|
44
|
+
release?: () => unknown;
|
|
30
45
|
}
|
|
31
46
|
|
|
32
47
|
/**
|
|
@@ -152,11 +167,47 @@ export async function httpFetch(
|
|
|
152
167
|
): Promise<HttpFetchResult> {
|
|
153
168
|
const codec = options.codec ?? defaultCodec;
|
|
154
169
|
const output = call(codec.encodeRequest(env, body));
|
|
155
|
-
const
|
|
170
|
+
const decoded = await codec.decodeResponse(output, { method: env.method });
|
|
171
|
+
const release = async () => {
|
|
172
|
+
try {
|
|
173
|
+
await output.return?.(undefined);
|
|
174
|
+
} catch {
|
|
175
|
+
/* the call is being abandoned; nothing to report */
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
const result: HttpFetchResult = {
|
|
179
|
+
envelope: decoded.envelope,
|
|
180
|
+
body: releasingOnExit(decoded.body, release),
|
|
181
|
+
release,
|
|
182
|
+
};
|
|
156
183
|
await throwIfPeerError(result, output);
|
|
157
184
|
return result;
|
|
158
185
|
}
|
|
159
186
|
|
|
187
|
+
/**
|
|
188
|
+
* `body`, but leaving it before its end releases the call.
|
|
189
|
+
*
|
|
190
|
+
* The body the codec hands back is a generator of its own over a `ByteReader`,
|
|
191
|
+
* and `.return()` on it never reaches the call's `output` (see
|
|
192
|
+
* `throwIfPeerError` for why the codec must not do that itself). So a caller
|
|
193
|
+
* that stopped reading — an aborted `fetch`, a cancelled response stream —
|
|
194
|
+
* left the peer's producer parked forever, holding its handler open. Safe here
|
|
195
|
+
* for the same reason it is safe in `throwIfPeerError`: `output` is this
|
|
196
|
+
* call's own response-only generator, and the request was fully sent.
|
|
197
|
+
*/
|
|
198
|
+
async function* releasingOnExit(
|
|
199
|
+
body: AsyncIterable<Uint8Array>,
|
|
200
|
+
release: () => Promise<void>,
|
|
201
|
+
): AsyncGenerator<Uint8Array> {
|
|
202
|
+
let completed = false;
|
|
203
|
+
try {
|
|
204
|
+
yield* body;
|
|
205
|
+
completed = true;
|
|
206
|
+
} finally {
|
|
207
|
+
if (!completed) await release();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
160
211
|
/**
|
|
161
212
|
* Wrap an HTTP handler as a `Duplex` so it can be registered with any
|
|
162
213
|
* `webrun-streams-*` adapter's `serve`.
|
|
@@ -197,8 +248,20 @@ export function httpServe(handler: HttpDataHandler, options: HttpDataOptions = {
|
|
|
197
248
|
yield* encodeErrorResponse(replyCodec, error, decoded.envelope.method);
|
|
198
249
|
return;
|
|
199
250
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
251
|
+
let completed = false;
|
|
252
|
+
try {
|
|
253
|
+
yield* replyCodec.encodeResponse(result.envelope, result.body, {
|
|
254
|
+
method: decoded.envelope.method,
|
|
255
|
+
});
|
|
256
|
+
completed = true;
|
|
257
|
+
} finally {
|
|
258
|
+
if (!completed) {
|
|
259
|
+
try {
|
|
260
|
+
await result.release?.();
|
|
261
|
+
} catch {
|
|
262
|
+
/* already released through the body, or nothing to release */
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
203
266
|
};
|
|
204
267
|
}
|