@shivaedev/test-story 0.11.1 → 0.13.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 (32) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +27 -3
  3. package/dist/server/fakeHttpService.d.ts +11 -0
  4. package/dist/server/fakeHttpService.d.ts.map +1 -1
  5. package/dist/server/fakeHttpService.js +8 -1
  6. package/dist/server/fakeHttpService.js.map +1 -1
  7. package/dist/server/forwardingProxy.d.ts +3 -1
  8. package/dist/server/forwardingProxy.d.ts.map +1 -1
  9. package/dist/server/forwardingProxy.js +9 -1
  10. package/dist/server/forwardingProxy.js.map +1 -1
  11. package/dist/server/internal/fakeHttpControls.d.ts +11 -5
  12. package/dist/server/internal/fakeHttpControls.d.ts.map +1 -1
  13. package/dist/server/internal/fakeHttpControls.js +56 -34
  14. package/dist/server/internal/fakeHttpControls.js.map +1 -1
  15. package/dist/server/internal/fakeHttpGates.d.ts +10 -0
  16. package/dist/server/internal/fakeHttpGates.d.ts.map +1 -0
  17. package/dist/server/internal/fakeHttpGates.js +38 -0
  18. package/dist/server/internal/fakeHttpGates.js.map +1 -0
  19. package/dist/server/internal/fakeHttpReceive.d.ts.map +1 -1
  20. package/dist/server/internal/fakeHttpReceive.js +12 -13
  21. package/dist/server/internal/fakeHttpReceive.js.map +1 -1
  22. package/dist/server/internal/fakeHttpSend.d.ts +8 -0
  23. package/dist/server/internal/fakeHttpSend.d.ts.map +1 -0
  24. package/dist/server/internal/fakeHttpSend.js +51 -0
  25. package/dist/server/internal/fakeHttpSend.js.map +1 -0
  26. package/package.json +2 -2
  27. package/src/server/fakeHttpService.ts +12 -1
  28. package/src/server/forwardingProxy.ts +13 -1
  29. package/src/server/internal/fakeHttpControls.ts +63 -35
  30. package/src/server/internal/fakeHttpGates.ts +39 -0
  31. package/src/server/internal/fakeHttpReceive.ts +12 -14
  32. package/src/server/internal/fakeHttpSend.ts +55 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.13.0 - 2026-10-19
4
+
5
+ ### Added
6
+
7
+ - `ForwardingProxyOptions.localAddress` selects the proxy's source socket address and rejects unavailable addresses during acquisition. `ForwardingProxyOptions.forwardedHops` appends visitor and edge hops after the client's `x-forwarded-for` chain instead of the caller address; fixed `headers` still override the generated chain.
8
+
9
+ ## 0.12.0 - 2026-10-18
10
+
11
+ ### Added
12
+
13
+ - Fake HTTP services expose `stall`, `replyOnce`, `releaseOnArrival` and `fault` controls for body stalls, raw byte and iterable chunk replies, automatic hold release on route arrival, and faults injected into arrived requests or replies in flight. `FakeHttpReply.chunks` accepts string or byte iterables, and the exported `FakeHttpFault` describes status, pre-header drop and mid-body reset faults. Controls validate route names, narrate through the story and clean up with its scope.
14
+
3
15
  ## 0.11.1 - 2026-10-18
4
16
 
5
17
  ### Changed
package/README.md CHANGED
@@ -241,6 +241,8 @@ Use `forwardingProxy` from `@shivaedev/test-story/forwardingProxy.ts` in the kit
241
241
  import { forwardingProxy } from "@shivaedev/test-story/forwardingProxy.ts";
242
242
 
243
243
  const proxy = yield* forwardingProxy(upstream, {
244
+ localAddress: "::1",
245
+ forwardedHops: ["203.0.113.9", "2001:db8::1"],
244
246
  headers: {
245
247
  host: "internal.test",
246
248
  "x-forwarded-host": "app.example",
@@ -249,7 +251,9 @@ const proxy = yield* forwardingProxy(upstream, {
249
251
  });
250
252
  ```
251
253
 
252
- Point story requests at `proxy.url`. The proxy listens on IPv4 and IPv6 loopback on the same allocated port, forwards the request's method, path, query, body and headers to `upstream`, and returns the host's status, headers and body. It appends the connecting caller's address to `x-forwarded-for`, preserving any existing chain. The optional `headers` map overrides incoming and generated headers, including `x-forwarded-for` when the story needs a fixed chain. The proxy returns 502 if it cannot connect to the host. Both listeners close with the story's Scope.
254
+ Point story requests at `proxy.url`. The proxy listens on IPv4 and IPv6 loopback on the same allocated port, forwards the request's method, path, query, body and headers to `upstream`, and returns the host's status, headers and body. It appends the connecting caller's address to `x-forwarded-for`, preserving any existing chain. Set `forwardedHops` to append those addresses in order instead of the caller, so a client chain of `spoofed` with hops `visitor` and `edge` becomes `spoofed, visitor, edge`. The optional `headers` map overrides incoming and generated headers, including `x-forwarded-for` when the story needs a fixed chain.
255
+
256
+ Set `localAddress` to choose the source socket address of the proxy's connection to the host. The upstream must accept that address family; for a hostname, the proxy resolves the matching family. Use `127.0.0.1` or `::1` for portable loopback stories. An unavailable local address fails acquisition with a message naming it. The proxy returns 502 if it cannot connect to the host. Both listeners close with the story's Scope.
253
257
 
254
258
  #### Reading OTLP exports
255
259
 
@@ -270,7 +274,7 @@ For timeout stories, return `undefined` from the `status` callback to accept an
270
274
 
271
275
  #### Faking an outbound HTTP API
272
276
 
273
- Use `fakeHttpService` from `@shivaedev/test-story/fakeHttpService.ts` in a kit's scoped `create` hook. Like the OTLP receiver, it listens on an allocated loopback port and closes with the scope. Routes have names for controls, a method (default `GET`), and a path whose `:segments` become decoded `request.params`. A reply receives the request and returns a JSON `body`, optional `status` (default 200), and optional response `headers`.
277
+ Use `fakeHttpService` from `@shivaedev/test-story/fakeHttpService.ts` in a kit's scoped `create` hook. Like the OTLP receiver, it listens on an allocated loopback port and closes with the scope. Routes have names for controls, a method (default `GET`), and a path whose `:segments` become decoded `request.params`. A reply receives the request and returns a JSON `body` (or a `Uint8Array`/Buffer sent as raw bytes), optional `status` (default 200), and optional response `headers`.
274
278
 
275
279
  ```ts
276
280
  import { fakeHttpService } from "@shivaedev/test-story/fakeHttpService.ts";
@@ -300,6 +304,26 @@ const user = yield* Fiber.join(pending);
300
304
 
301
305
  `application.loadUser` is the application's real client call. `failOnce` affects the next authorized matching request to that named route, then the route answers normally. `hold` pauses that route until `release`; `waiting` completes when a request reaches the hold, so a story needs no polling to know when to release it. Other routes still answer. Effects that wait for HTTP should use a live-clock kit.
302
306
 
307
+ Use the controls through kit verbs after acquiring the service. `replyOnce(route, reply)` scripts the next authorized reply, including raw bytes or `chunks`, a synchronous or asynchronous iterable of strings and byte arrays. Set `headers["content-type"]` to the story's media type; the default is JSON. Chunks are written in iteration order as they become available, so an async iterable can wait between pieces.
308
+
309
+ ```ts
310
+ yield* api.replyOnce("download", {
311
+ body: Uint8Array.from([0, 255, 128]),
312
+ headers: { "content-type": "application/octet-stream" },
313
+ });
314
+ const stalled = yield* api.stall("download");
315
+ const pending = yield* Effect.forkChild(application.download());
316
+ yield* stalled.waiting;
317
+ yield* stalled.release;
318
+ const bytes = yield* Fiber.join(pending);
319
+ ```
320
+
321
+ `stall(route)` sends the reply's status and headers, then pauses its body. Its `waiting` effect completes at that pause. Its `release` lets the body continue; a client that gives up also stops waiting on the server. Like a hold, a stall remains installed for matching requests until released. Holds pause before headers. `releaseOnArrival(heldRoute, arrivingRoute)` releases the current hold when an authorized request reaches the named route; script it before that arrival. Rejected requests do not trigger release.
322
+
323
+ `fault(route, fault)` acts on an arrived request in the required phase. `{ kind: "status", reply: { status: 503, body: { error: "unavailable" } } }` replaces a held reply before headers. `{ kind: "drop" }` drops a connection before headers. `{ kind: "reset" }` terminates a reply after headers, including after body chunks have arrived. Wait for the hold, stall or client observation before injecting the fault. A fault fails with guidance when no request is in the required phase; it does not queue a failure for a future call. `failOnce` continues to queue a status reply for the next request.
324
+
325
+ All controls validate route names and narrate through the service's story sink. Closing the scope closes active connections and releases holds, stalls and arrival scripts.
326
+
303
327
  A `pages` route accepts a one-based `?page=` query and returns `{ items, next }`, with `next: null` on the last page. No query means page 1; a page past the end returns an empty list; a non-positive or non-integer page gets 400. `api.requests` reads a snapshot of the request log, including rejected and held requests, with `method`, `path` (including query), raw `body` Buffer, `headers`, `params` and `query`.
304
328
 
305
329
  ### 2. Once per feature: traits and verbs
@@ -577,7 +601,7 @@ Import each module with the prefix `@shivaedev/test-story/`; there is no root en
577
601
  | `domNavigate.ts` | `domNavigate(url?)` |
578
602
  | `genreTags.ts` | `genreTags(...kitNames)`, `GenreTag` |
579
603
  | `domEnvironment.config.ts` | Default Vitest environment preserving Node host globals and happy-dom document transport |
580
- | `fakeHttpService.ts` | `fakeHttpService(options)`, `FakeHttpRoute`, `FakeHttpRequest`, `FakeHttpReply`, `FakeHttpHold`, `FakeHttpServiceOptions` |
604
+ | `fakeHttpService.ts` | `fakeHttpService(options)`, `FakeHttpRoute`, `FakeHttpRequest`, `FakeHttpReply`, `FakeHttpFault`, `FakeHttpHold`, `FakeHttpServiceOptions` |
581
605
  | `forwardingProxy.ts` | `forwardingProxy(upstream, options?)`, `ForwardingProxyOptions` |
582
606
  | `otlpReceiver.ts` | `otlpReceiver(options?)`, `OtlpSpan`, `OtlpExport`, `OtlpReceiverOptions` |
583
607
 
@@ -10,9 +10,16 @@ export interface FakeHttpRequest {
10
10
  }
11
11
  export interface FakeHttpReply {
12
12
  readonly body?: unknown;
13
+ readonly chunks?: Iterable<string | Uint8Array> | AsyncIterable<string | Uint8Array>;
13
14
  readonly headers?: Readonly<Record<string, string>>;
14
15
  readonly status?: number;
15
16
  }
17
+ export type FakeHttpFault = {
18
+ readonly kind: "status";
19
+ readonly reply: FakeHttpReply;
20
+ } | {
21
+ readonly kind: "drop" | "reset";
22
+ };
16
23
  export type FakeHttpRoute = {
17
24
  readonly method?: string;
18
25
  readonly path: string;
@@ -34,9 +41,13 @@ export interface FakeHttpHold {
34
41
  }
35
42
  export declare const fakeHttpService: (options: FakeHttpServiceOptions) => Effect.Effect<{
36
43
  failOnce: (route: string, reply: FakeHttpReply) => Effect.Effect<void, never, never>;
44
+ fault: (route: string, fault: FakeHttpFault) => Effect.Effect<void, never, never>;
37
45
  hold: (route: string) => Effect.Effect<FakeHttpHold, never, never>;
38
46
  narrate: (sink: (line: string) => void) => void;
47
+ releaseOnArrival: (route: string, arrival: string) => Effect.Effect<void, never, never>;
48
+ replyOnce: (route: string, reply: FakeHttpReply) => Effect.Effect<void, never, never>;
39
49
  readonly requests: readonly FakeHttpRequest[];
50
+ stall: (route: string) => Effect.Effect<FakeHttpHold, never, never>;
40
51
  url: string;
41
52
  }, import("effect/Cause").UnknownError, import("effect/Scope").Scope>;
42
53
  //# sourceMappingURL=fakeHttpService.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"fakeHttpService.d.ts","sourceRoot":"","sources":["../../src/server/fakeHttpService.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,mBAAmB,EAAE,MAAM,WAAW,CAAC;AACnE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAKhC,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,mBAAmB,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAClD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;CAChC;AAED,MAAM,WAAW,aAAa;IAC7B,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACpD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,MAAM,aAAa,GAAG;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB,GAAG,CACD;IAAE,QAAQ,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,aAAa,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CAAE,GACvF;IAAE,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,SAAS,OAAO,EAAE,CAAC,EAAE,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CAAE,CAC7E,CAAC;AAEF,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,OAAO,CAAC;IACvD,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC;IACzD,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACvC;AAED,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACtC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;CACtC;AAED,eAAO,MAAM,eAAe;sBAgBR,MAAM,SAAS,aAAa;kBAChC,MAAM;oBACJ,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI;uBAGtB,SAAS,eAAe,EAAE;;qEAK1C,CAAC"}
1
+ {"version":3,"file":"fakeHttpService.d.ts","sourceRoot":"","sources":["../../src/server/fakeHttpService.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,mBAAmB,EAAE,MAAM,WAAW,CAAC;AACnE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAKhC,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,mBAAmB,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAClD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;CAChC;AAED,MAAM,WAAW,aAAa;IAC7B,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,GAAG,UAAU,CAAC,GAAG,aAAa,CAAC,MAAM,GAAG,UAAU,CAAC,CAAC;IACrF,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACpD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,MAAM,aAAa,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,CAAC;AAE7H,MAAM,MAAM,aAAa,GAAG;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB,GAAG,CACD;IAAE,QAAQ,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,aAAa,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CAAE,GACvF;IAAE,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,SAAS,OAAO,EAAE,CAAC,EAAE,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CAAE,CAC7E,CAAC;AAEF,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,OAAO,CAAC;IACvD,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC;IACzD,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACvC;AAED,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACtC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;CACtC;AAED,eAAO,MAAM,eAAe;sBAgBR,MAAM,SAAS,aAAa;mBAC/B,MAAM,SAAS,aAAa;kBAC7B,MAAM;oBACJ,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI;8BAGZ,MAAM,WAAW,MAAM;uBAK9B,MAAM,SAAS,aAAa;uBAC/B,SAAS,eAAe,EAAE;mBAG3B,MAAM;;qEAGrB,CAAC"}
@@ -19,14 +19,21 @@ export const fakeHttpService = Effect.fnUntraced(function* (options) {
19
19
  }
20
20
  }
21
21
  return {
22
- failOnce: (route, reply) => Effect.sync(() => checkRoute(route)).pipe(Effect.andThen(controls.failOnce(route, reply))),
22
+ failOnce: (route, reply) => Effect.sync(() => checkRoute(route)).pipe(Effect.andThen(controls.queue(route, reply))),
23
+ fault: (route, fault) => Effect.sync(() => checkRoute(route)).pipe(Effect.andThen(controls.fault(route, fault))),
23
24
  hold: (route) => Effect.sync(() => checkRoute(route)).pipe(Effect.andThen(controls.hold(route))),
24
25
  narrate: (sink) => {
25
26
  narrate = sink;
26
27
  },
28
+ releaseOnArrival: (route, arrival) => Effect.sync(() => {
29
+ checkRoute(route);
30
+ checkRoute(arrival);
31
+ }).pipe(Effect.andThen(controls.releaseOnArrival(route, arrival))),
32
+ replyOnce: (route, reply) => Effect.sync(() => checkRoute(route)).pipe(Effect.andThen(controls.queue(route, reply))),
27
33
  get requests() {
28
34
  return requests.slice();
29
35
  },
36
+ stall: (route) => Effect.sync(() => checkRoute(route)).pipe(Effect.andThen(controls.stall(route))),
30
37
  url,
31
38
  };
32
39
  });
@@ -1 +1 @@
1
- {"version":3,"file":"fakeHttpService.js","sourceRoot":"","sources":["../../src/server/fakeHttpService.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAA4B,MAAM,WAAW,CAAC;AACnE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAoCvD,MAAM,CAAC,MAAM,eAAe,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,OAA+B;IAC1F,MAAM,QAAQ,GAAsB,EAAE,CAAC;IACvC,IAAI,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAC3B,SAAS,IAAI,CAAC,IAAY;QACzB,OAAO,EAAE,CAAC,IAAI,CAAC,CAAC;IACjB,CAAC;IACD,MAAM,QAAQ,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;IACxC,MAAM,MAAM,GAAG,YAAY,CAAC,CAAC,OAAO,EAAE,QAAQ,EAAE,EAAE,CAAC,eAAe,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC;IAC1H,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;IAC1C,KAAK,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;IAC9D,SAAS,UAAU,CAAC,KAAa;QAChC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC;YAC3C,MAAM,IAAI,KAAK,CAAC,4BAA4B,KAAK,oFAAoF,CAAC,CAAC;QACxI,CAAC;IACF,CAAC;IACD,OAAO;QACN,QAAQ,EAAE,CAAC,KAAa,EAAE,KAAoB,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;QAC7I,IAAI,EAAE,CAAC,KAAa,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;QACxG,OAAO,EAAE,CAAC,IAA4B,EAAE,EAAE;YACzC,OAAO,GAAG,IAAI,CAAC;QAChB,CAAC;QACD,IAAI,QAAQ;YACX,OAAO,QAAQ,CAAC,KAAK,EAAE,CAAC;QACzB,CAAC;QACD,GAAG;KACH,CAAC;AACH,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"fakeHttpService.js","sourceRoot":"","sources":["../../src/server/fakeHttpService.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,10 @@
1
1
  import { Effect } from "effect";
2
2
  export interface ForwardingProxyOptions {
3
+ readonly forwardedHops?: readonly string[];
3
4
  readonly headers?: Readonly<Record<string, string>>;
5
+ readonly localAddress?: string;
4
6
  }
5
7
  export declare const forwardingProxy: (upstream: string, options?: ForwardingProxyOptions | undefined) => Effect.Effect<{
6
8
  url: string;
7
- }, import("effect/Cause").UnknownError, import("effect/Scope").Scope>;
9
+ }, Error, import("effect/Scope").Scope>;
8
10
  //# sourceMappingURL=forwardingProxy.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"forwardingProxy.d.ts","sourceRoot":"","sources":["../../src/server/forwardingProxy.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAGhC,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACpD;AAED,eAAO,MAAM,eAAe;;qEA8B1B,CAAC"}
1
+ {"version":3,"file":"forwardingProxy.d.ts","sourceRoot":"","sources":["../../src/server/forwardingProxy.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAGhC,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACpD,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,eAAO,MAAM,eAAe;;uCAuC1B,CAAC"}
@@ -1,14 +1,22 @@
1
1
  import { createServer, request } from "node:http";
2
+ import { isIP } from "node:net";
2
3
  import { Effect } from "effect";
3
4
  import { loopbackServer } from "#internal/loopback.ts";
4
5
  export const forwardingProxy = Effect.fnUntraced(function* (upstream, options = {}) {
6
+ if (options.localAddress !== undefined) {
7
+ yield* Effect.scoped(loopbackServer(createServer(), { host: options.localAddress })).pipe(Effect.mapError((cause) => new Error(`Cannot acquire forwarding proxy with local address ${options.localAddress}`, { cause })));
8
+ }
5
9
  const forward = (incoming, response) => {
6
10
  const outgoing = request(`${upstream}${incoming.url}`, {
11
+ family: options.localAddress === undefined ? undefined : isIP(options.localAddress),
7
12
  headers: {
8
13
  ...incoming.headers,
9
- "x-forwarded-for": [incoming.headers["x-forwarded-for"], incoming.socket.remoteAddress].filter(Boolean).join(", "),
14
+ "x-forwarded-for": [incoming.headers["x-forwarded-for"], ...(options.forwardedHops ?? [incoming.socket.remoteAddress])]
15
+ .filter(Boolean)
16
+ .join(", "),
10
17
  ...options.headers,
11
18
  },
19
+ localAddress: options.localAddress,
12
20
  method: incoming.method,
13
21
  }, (result) => {
14
22
  response.writeHead(result.statusCode ?? 502, result.headers);
@@ -1 +1 @@
1
- {"version":3,"file":"forwardingProxy.js","sourceRoot":"","sources":["../../src/server/forwardingProxy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAwB,OAAO,EAAE,MAAM,WAAW,CAAC;AACxE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAMvD,MAAM,CAAC,MAAM,eAAe,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,QAAgB,EAAE,OAAO,GAA2B,EAAE;IACjH,MAAM,OAAO,GAAoB,CAAC,QAAQ,EAAE,QAAQ,EAAE,EAAE;QACvD,MAAM,QAAQ,GAAG,OAAO,CACvB,GAAG,QAAQ,GAAG,QAAQ,CAAC,GAAG,EAAE,EAC5B;YACC,OAAO,EAAE;gBACR,GAAG,QAAQ,CAAC,OAAO;gBACnB,iBAAiB,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,iBAAiB,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;gBAClH,GAAG,OAAO,CAAC,OAAO;aAClB;YACD,MAAM,EAAE,QAAQ,CAAC,MAAM;SACvB,EACD,CAAC,MAAM,EAAE,EAAE;YACV,QAAQ,CAAC,SAAS,CAAC,MAAM,CAAC,UAAU,IAAI,GAAG,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;YAC7D,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACvB,CAAC,CACD,CAAC;QACF,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;YAC9B,IAAI,QAAQ,CAAC,WAAW,EAAE,CAAC;gBAC1B,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YACzB,CAAC;iBAAM,CAAC;gBACP,QAAQ,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;YAC/B,CAAC;QACF,CAAC,CAAC,CAAC;QACH,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;QAC/C,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACzB,CAAC,CAAC;IACF,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,cAAc,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC;IACzD,KAAK,CAAC,CAAC,cAAc,CAAC,YAAY,CAAC,OAAO,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC/F,OAAO,EAAE,GAAG,EAAE,CAAC;AAChB,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"forwardingProxy.js","sourceRoot":"","sources":["../../src/server/forwardingProxy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAwB,OAAO,EAAE,MAAM,WAAW,CAAC;AACxE,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAChC,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAQvD,MAAM,CAAC,MAAM,eAAe,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,QAAgB,EAAE,OAAO,GAA2B,EAAE;IACjH,IAAI,OAAO,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;QACxC,KAAK,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,cAAc,CAAC,YAAY,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,IAAI,CACxF,MAAM,CAAC,QAAQ,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,KAAK,CAAC,sDAAsD,OAAO,CAAC,YAAY,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,CAC9H,CAAC;IACH,CAAC;IACD,MAAM,OAAO,GAAoB,CAAC,QAAQ,EAAE,QAAQ,EAAE,EAAE;QACvD,MAAM,QAAQ,GAAG,OAAO,CACvB,GAAG,QAAQ,GAAG,QAAQ,CAAC,GAAG,EAAE,EAC5B;YACC,MAAM,EAAE,OAAO,CAAC,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC;YACnF,OAAO,EAAE;gBACR,GAAG,QAAQ,CAAC,OAAO;gBACnB,iBAAiB,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,iBAAiB,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC,aAAa,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC;qBACrH,MAAM,CAAC,OAAO,CAAC;qBACf,IAAI,CAAC,IAAI,CAAC;gBACZ,GAAG,OAAO,CAAC,OAAO;aAClB;YACD,YAAY,EAAE,OAAO,CAAC,YAAY;YAClC,MAAM,EAAE,QAAQ,CAAC,MAAM;SACvB,EACD,CAAC,MAAM,EAAE,EAAE;YACV,QAAQ,CAAC,SAAS,CAAC,MAAM,CAAC,UAAU,IAAI,GAAG,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;YAC7D,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACvB,CAAC,CACD,CAAC;QACF,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;YAC9B,IAAI,QAAQ,CAAC,WAAW,EAAE,CAAC;gBAC1B,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YACzB,CAAC;iBAAM,CAAC;gBACP,QAAQ,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;YAC/B,CAAC;QACF,CAAC,CAAC,CAAC;QACH,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;QAC/C,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACzB,CAAC,CAAC;IACF,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,cAAc,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC;IACzD,KAAK,CAAC,CAAC,cAAc,CAAC,YAAY,CAAC,OAAO,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC/F,OAAO,EAAE,GAAG,EAAE,CAAC;AAChB,CAAC,CAAC,CAAC"}
@@ -1,10 +1,16 @@
1
+ import type { ServerResponse } from "node:http";
1
2
  import { Effect } from "effect";
2
- import type { FakeHttpHold, FakeHttpReply } from "#fakeHttpService.ts";
3
+ import type { FakeHttpFault, FakeHttpReply } from "#fakeHttpService.ts";
3
4
  export declare function fakeHttpControls(tell: (line: string) => void): {
5
+ arrive: (route: string, response: ServerResponse) => void;
4
6
  close: () => void;
5
- failOnce: (route: string, reply: FakeHttpReply) => Effect.Effect<void, never, never>;
6
- hold: (route: string) => Effect.Effect<FakeHttpHold>;
7
- pause: (route: string) => Promise<void>;
8
- takeFailure: (route: string) => FakeHttpReply | undefined;
7
+ fault: (route: string, fault: FakeHttpFault) => Effect.Effect<void, never, never>;
8
+ hold: (route: string) => Effect.Effect<import("#fakeHttpService.ts").FakeHttpHold, never, never>;
9
+ pause: (route: string, response: ServerResponse) => Promise<boolean>;
10
+ queue: (route: string, reply: FakeHttpReply) => Effect.Effect<void, never, never>;
11
+ releaseOnArrival: (route: string, arrival: string) => Effect.Effect<void, never, never>;
12
+ stall: (route: string) => Effect.Effect<import("#fakeHttpService.ts").FakeHttpHold, never, never>;
13
+ stallBody: (route: string) => (() => Promise<void>) | undefined;
14
+ takeReply: (route: string) => FakeHttpReply | undefined;
9
15
  };
10
16
  //# sourceMappingURL=fakeHttpControls.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"fakeHttpControls.d.ts","sourceRoot":"","sources":["../../../src/server/internal/fakeHttpControls.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC1C,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEvE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI;IAI3D,KAAK;IAKL,QAAQ,UAAU,MAAM,SAAS,aAAa;IAO9C,IAAI,UAAU,MAAM,KAAG,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC;IA0BlD,KAAK,UAAgB,MAAM;IAG3B,WAAW,UAAU,MAAM;EAE5B"}
1
+ {"version":3,"file":"fakeHttpControls.d.ts","sourceRoot":"","sources":["../../../src/server/internal/fakeHttpControls.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAIxE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI;IAO3D,MAAM,UAAU,MAAM,YAAY,cAAc;IAQhD,KAAK;IASL,KAAK,UAAU,MAAM,SAAS,aAAa;IAkB3C,IAAI,UAAU,MAAM;IACpB,KAAK,UAAgB,MAAM,YAAY,cAAc;IAIrD,KAAK,UAAU,MAAM,SAAS,aAAa;IAO3C,gBAAgB,UAAU,MAAM,WAAW,MAAM;IAWjD,KAAK,UAAU,MAAM;IACrB,SAAS,UAAU,MAAM;IAIzB,SAAS,UAAU,MAAM;EAE1B"}
@@ -1,48 +1,70 @@
1
- import { Deferred, Effect } from "effect";
1
+ import { Effect } from "effect";
2
+ import { fakeHttpGates } from "#internal/fakeHttpGates.ts";
3
+ import { fakeHttpConnection, fakeHttpSend } from "#internal/fakeHttpSend.ts";
2
4
  export function fakeHttpControls(tell) {
3
- const failures = new Map();
4
- const holds = new Map();
5
+ const replies = new Map();
6
+ const holds = fakeHttpGates(tell, "hold");
7
+ const stalls = fakeHttpGates(tell, "stall");
8
+ const arrivals = new Map();
9
+ const active = new Map();
5
10
  return {
11
+ arrive: (route, response) => {
12
+ active.set(response, route);
13
+ response.once("close", () => active.delete(response));
14
+ for (const release of arrivals.get(route) ?? []) {
15
+ release();
16
+ }
17
+ arrivals.delete(route);
18
+ },
6
19
  close: () => {
7
- for (const hold of holds.values()) {
8
- hold.release();
20
+ for (const response of active.keys()) {
21
+ response.destroy();
22
+ }
23
+ holds.close();
24
+ stalls.close();
25
+ arrivals.clear();
26
+ replies.clear();
27
+ },
28
+ fault: (route, fault) => Effect.promise(async () => {
29
+ const response = [...active].find(([candidate, name]) => name === route && !(candidate.destroyed || candidate.writableEnded) && candidate.headersSent === (fault.kind === "reset"))?.[0];
30
+ if (!response) {
31
+ throw new Error(`Fake HTTP route "${route}" has no request ready for a ${fault.kind} fault.\nhelp: Wait for a hold before a status or drop fault, or for response headers before a reset fault.`);
9
32
  }
33
+ tell(`fake HTTP ${route} fault ${fault.kind}`);
34
+ if (fault.kind === "status") {
35
+ await fakeHttpSend(response, fault.reply);
36
+ }
37
+ else {
38
+ response.destroy();
39
+ }
40
+ }),
41
+ hold: (route) => Effect.sync(() => holds.make(route)),
42
+ pause: async (route, response) => {
43
+ await fakeHttpConnection(response).pause(holds.get(route)?.enter());
44
+ return !(response.destroyed || response.writableEnded || response.headersSent);
10
45
  },
11
- failOnce: (route, reply) => Effect.sync(() => {
12
- const queue = failures.get(route) ?? [];
46
+ queue: (route, reply) => Effect.sync(() => {
47
+ const queue = replies.get(route) ?? [];
13
48
  queue.push(reply);
14
- failures.set(route, queue);
49
+ replies.set(route, queue);
15
50
  tell(`fake HTTP ${route} will answer once with ${reply.status ?? 200}`);
16
51
  }),
17
- hold: (route) => Effect.sync(() => {
18
- if (holds.has(route)) {
19
- throw new Error(`Fake HTTP route "${route}" is already held.\nhelp: Release its current hold before holding the route again.`);
52
+ releaseOnArrival: (route, arrival) => Effect.sync(() => {
53
+ const hold = holds.get(route);
54
+ if (!hold) {
55
+ throw new Error(`Fake HTTP route "${route}" has no hold.\nhelp: Hold the route before scripting its release on arrival.`);
20
56
  }
21
- const waiting = Deferred.makeUnsafe();
22
- const released = Promise.withResolvers();
23
- const hold = {
24
- enter: () => {
25
- tell(`fake HTTP ${route} has a waiting request`);
26
- Deferred.doneUnsafe(waiting, Effect.void);
27
- return released.promise;
28
- },
29
- release: () => {
30
- if (holds.get(route) !== hold) {
31
- return;
32
- }
33
- holds.delete(route);
34
- released.resolve();
35
- tell(`fake HTTP ${route} released`);
36
- },
37
- };
38
- holds.set(route, hold);
39
- tell(`fake HTTP ${route} held`);
40
- return { release: Effect.sync(hold.release), waiting: Deferred.await(waiting) };
57
+ const releases = arrivals.get(arrival) ?? new Set();
58
+ releases.add(hold.release);
59
+ arrivals.set(arrival, releases);
60
+ tell(`fake HTTP ${route} will release when ${arrival} arrives`);
41
61
  }),
42
- pause: async (route) => {
43
- await holds.get(route)?.enter();
62
+ stall: (route) => Effect.sync(() => stalls.make(route)),
63
+ stallBody: (route) => {
64
+ const stall = stalls.get(route);
65
+ return stall ? () => stall.enter() : undefined;
44
66
  },
45
- takeFailure: (route) => failures.get(route)?.shift(),
67
+ takeReply: (route) => replies.get(route)?.shift(),
46
68
  };
47
69
  }
48
70
  //# sourceMappingURL=fakeHttpControls.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"fakeHttpControls.js","sourceRoot":"","sources":["../../../src/server/internal/fakeHttpControls.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAG1C,MAAM,UAAU,gBAAgB,CAAC,IAA4B;IAC5D,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA2B,CAAC;IACpD,MAAM,KAAK,GAAG,IAAI,GAAG,EAAiF,CAAC;IACvG,OAAO;QACN,KAAK,EAAE,GAAG,EAAE;YACX,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;gBACnC,IAAI,CAAC,OAAO,EAAE,CAAC;YAChB,CAAC;QACF,CAAC;QACD,QAAQ,EAAE,CAAC,KAAa,EAAE,KAAoB,EAAE,EAAE,CACjD,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE;YAChB,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;YACxC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAClB,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;YAC3B,IAAI,CAAC,aAAa,KAAK,0BAA0B,KAAK,CAAC,MAAM,IAAI,GAAG,EAAE,CAAC,CAAC;QACzE,CAAC,CAAC;QACH,IAAI,EAAE,CAAC,KAAa,EAA+B,EAAE,CACpD,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE;YAChB,IAAI,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;gBACtB,MAAM,IAAI,KAAK,CAAC,oBAAoB,KAAK,oFAAoF,CAAC,CAAC;YAChI,CAAC;YACD,MAAM,OAAO,GAAG,QAAQ,CAAC,UAAU,EAAQ,CAAC;YAC5C,MAAM,QAAQ,GAAG,OAAO,CAAC,aAAa,EAAQ,CAAC;YAC/C,MAAM,IAAI,GAAG;gBACZ,KAAK,EAAE,GAAG,EAAE;oBACX,IAAI,CAAC,aAAa,KAAK,wBAAwB,CAAC,CAAC;oBACjD,QAAQ,CAAC,UAAU,CAAC,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;oBAC1C,OAAO,QAAQ,CAAC,OAAO,CAAC;gBACzB,CAAC;gBACD,OAAO,EAAE,GAAG,EAAE;oBACb,IAAI,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC;wBAC/B,OAAO;oBACR,CAAC;oBACD,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;oBACpB,QAAQ,CAAC,OAAO,EAAE,CAAC;oBACnB,IAAI,CAAC,aAAa,KAAK,WAAW,CAAC,CAAC;gBACrC,CAAC;aACD,CAAC;YACF,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;YACvB,IAAI,CAAC,aAAa,KAAK,OAAO,CAAC,CAAC;YAChC,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;QACjF,CAAC,CAAC;QACH,KAAK,EAAE,KAAK,EAAE,KAAa,EAAE,EAAE;YAC9B,MAAM,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,CAAC;QACjC,CAAC;QACD,WAAW,EAAE,CAAC,KAAa,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE;KAC5D,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"fakeHttpControls.js","sourceRoot":"","sources":["../../../src/server/internal/fakeHttpControls.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import type { FakeHttpHold } from "#fakeHttpService.ts";
2
+ export declare function fakeHttpGates(tell: (line: string) => void, kind: "hold" | "stall"): {
3
+ close: () => void;
4
+ get: (route: string) => {
5
+ enter: () => Promise<void>;
6
+ release: () => void;
7
+ } | undefined;
8
+ make: (route: string) => FakeHttpHold;
9
+ };
10
+ //# sourceMappingURL=fakeHttpGates.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fakeHttpGates.d.ts","sourceRoot":"","sources":["../../../src/server/internal/fakeHttpGates.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD,wBAAgB,aAAa,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO;IAGhF,KAAK;IAKL,GAAG,UAAU,MAAM;eAPmB,MAAM,OAAO,CAAC,IAAI,CAAC;iBAAW,MAAM,IAAI;;IAQ9E,IAAI,UAAU,MAAM,KAAG,YAAY;EA0BpC"}
@@ -0,0 +1,38 @@
1
+ import { Deferred, Effect } from "effect";
2
+ export function fakeHttpGates(tell, kind) {
3
+ const gates = new Map();
4
+ return {
5
+ close: () => {
6
+ for (const gate of gates.values()) {
7
+ gate.release();
8
+ }
9
+ },
10
+ get: (route) => gates.get(route),
11
+ make: (route) => {
12
+ if (gates.has(route)) {
13
+ throw new Error(`Fake HTTP route "${route}" already has a ${kind}.\nhelp: Release its current ${kind} before scripting another.`);
14
+ }
15
+ const waiting = Deferred.makeUnsafe();
16
+ const released = Promise.withResolvers();
17
+ const gate = {
18
+ enter: () => {
19
+ tell(kind === "hold" ? `fake HTTP ${route} has a waiting request` : `fake HTTP ${route} body stalled`);
20
+ Deferred.doneUnsafe(waiting, Effect.void);
21
+ return released.promise;
22
+ },
23
+ release: () => {
24
+ if (gates.get(route) !== gate) {
25
+ return;
26
+ }
27
+ gates.delete(route);
28
+ released.resolve();
29
+ tell(kind === "hold" ? `fake HTTP ${route} released` : `fake HTTP ${route} body released`);
30
+ },
31
+ };
32
+ gates.set(route, gate);
33
+ tell(kind === "hold" ? `fake HTTP ${route} held` : `fake HTTP ${route} body will stall`);
34
+ return { release: Effect.sync(gate.release), waiting: Deferred.await(waiting) };
35
+ },
36
+ };
37
+ }
38
+ //# sourceMappingURL=fakeHttpGates.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fakeHttpGates.js","sourceRoot":"","sources":["../../../src/server/internal/fakeHttpGates.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAG1C,MAAM,UAAU,aAAa,CAAC,IAA4B,EAAE,IAAsB;IACjF,MAAM,KAAK,GAAG,IAAI,GAAG,EAA+D,CAAC;IACrF,OAAO;QACN,KAAK,EAAE,GAAG,EAAE;YACX,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;gBACnC,IAAI,CAAC,OAAO,EAAE,CAAC;YAChB,CAAC;QACF,CAAC;QACD,GAAG,EAAE,CAAC,KAAa,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC;QACxC,IAAI,EAAE,CAAC,KAAa,EAAgB,EAAE;YACrC,IAAI,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;gBACtB,MAAM,IAAI,KAAK,CAAC,oBAAoB,KAAK,mBAAmB,IAAI,gCAAgC,IAAI,4BAA4B,CAAC,CAAC;YACnI,CAAC;YACD,MAAM,OAAO,GAAG,QAAQ,CAAC,UAAU,EAAQ,CAAC;YAC5C,MAAM,QAAQ,GAAG,OAAO,CAAC,aAAa,EAAQ,CAAC;YAC/C,MAAM,IAAI,GAAG;gBACZ,KAAK,EAAE,GAAG,EAAE;oBACX,IAAI,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,aAAa,KAAK,wBAAwB,CAAC,CAAC,CAAC,aAAa,KAAK,eAAe,CAAC,CAAC;oBACvG,QAAQ,CAAC,UAAU,CAAC,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;oBAC1C,OAAO,QAAQ,CAAC,OAAO,CAAC;gBACzB,CAAC;gBACD,OAAO,EAAE,GAAG,EAAE;oBACb,IAAI,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC;wBAC/B,OAAO;oBACR,CAAC;oBACD,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;oBACpB,QAAQ,CAAC,OAAO,EAAE,CAAC;oBACnB,IAAI,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,aAAa,KAAK,WAAW,CAAC,CAAC,CAAC,aAAa,KAAK,gBAAgB,CAAC,CAAC;gBAC5F,CAAC;aACD,CAAC;YACF,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;YACvB,IAAI,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,aAAa,KAAK,OAAO,CAAC,CAAC,CAAC,aAAa,KAAK,kBAAkB,CAAC,CAAC;YACzF,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;QACjF,CAAC;KACD,CAAC;AACH,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"fakeHttpReceive.d.ts","sourceRoot":"","sources":["../../../src/server/internal/fakeHttpReceive.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACjE,OAAO,KAAK,EAAiB,eAAe,EAAiB,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AACjH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAmDtE,wBAAsB,eAAe,CACpC,QAAQ,EAAE,eAAe,EACzB,QAAQ,EAAE,cAAc,EACxB,OAAO,EAAE,sBAAsB,EAC/B,QAAQ,EAAE,eAAe,EAAE,EAC3B,QAAQ,EAAE,UAAU,CAAC,OAAO,gBAAgB,CAAC,EAC7C,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,iBAoD5B"}
1
+ {"version":3,"file":"fakeHttpReceive.d.ts","sourceRoot":"","sources":["../../../src/server/internal/fakeHttpReceive.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACjE,OAAO,KAAK,EAAiB,eAAe,EAAiB,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AACjH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AA8CtE,wBAAsB,eAAe,CACpC,QAAQ,EAAE,eAAe,EACzB,QAAQ,EAAE,cAAc,EACxB,OAAO,EAAE,sBAAsB,EAC/B,QAAQ,EAAE,eAAe,EAAE,EAC3B,QAAQ,EAAE,UAAU,CAAC,OAAO,gBAAgB,CAAC,EAC7C,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,iBAuD5B"}
@@ -1,3 +1,4 @@
1
+ import { fakeHttpSend } from "#internal/fakeHttpSend.ts";
1
2
  function match(route, method, pathname) {
2
3
  if ((route.method ?? "GET").toUpperCase() !== method) {
3
4
  return;
@@ -40,11 +41,6 @@ function pageReply(route, request) {
40
41
  }
41
42
  return { body: { items: route.pages[page - 1] ?? [], next: page < route.pages.length ? page + 1 : null } };
42
43
  }
43
- function send(response, reply) {
44
- const body = reply.body === undefined ? undefined : JSON.stringify(reply.body);
45
- response.writeHead(reply.status ?? 200, { "content-type": "application/json", ...reply.headers });
46
- response.end(body);
47
- }
48
44
  export async function fakeHttpReceive(incoming, response, options, requests, controls, tell) {
49
45
  try {
50
46
  const path = incoming.url ?? "/";
@@ -71,31 +67,34 @@ export async function fakeHttpReceive(incoming, response, options, requests, con
71
67
  tell(`fake HTTP received ${method} ${path}`);
72
68
  if (!decodeParams(params)) {
73
69
  tell(`fake HTTP rejected ${method} ${path} with 400`);
74
- send(response, { status: 400 });
70
+ await fakeHttpSend(response, { status: 400 });
75
71
  return;
76
72
  }
77
73
  if (options.guard && !options.guard(request)) {
78
74
  tell(`fake HTTP rejected ${method} ${path} with 401`);
79
- send(response, { status: 401 });
75
+ await fakeHttpSend(response, { status: 401 });
80
76
  return;
81
77
  }
82
78
  if (!found) {
83
79
  tell(`fake HTTP answered ${method} ${path} with 404`);
84
- send(response, { status: 404 });
80
+ await fakeHttpSend(response, { status: 404 });
81
+ return;
82
+ }
83
+ controls.arrive(found.name, response);
84
+ if (!(await controls.pause(found.name, response))) {
85
85
  return;
86
86
  }
87
- await controls.pause(found.name);
88
- const reply = controls.takeFailure(found.name) ?? pageReply(found.route, request);
87
+ const reply = controls.takeReply(found.name) ?? pageReply(found.route, request);
89
88
  tell(`fake HTTP answered ${method} ${path} with ${reply.status ?? 200}`);
90
- send(response, reply);
89
+ await fakeHttpSend(response, reply, controls.stallBody(found.name));
91
90
  }
92
91
  catch (error) {
93
92
  tell(`fake HTTP could not answer: ${String(error)}`);
94
93
  if (response.headersSent) {
95
- response.end();
94
+ response.destroy();
96
95
  }
97
96
  else {
98
- send(response, { status: 500 });
97
+ await fakeHttpSend(response, { status: 500 });
99
98
  }
100
99
  }
101
100
  }
@@ -1 +1 @@
1
- {"version":3,"file":"fakeHttpReceive.js","sourceRoot":"","sources":["../../../src/server/internal/fakeHttpReceive.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"fakeHttpReceive.js","sourceRoot":"","sources":["../../../src/server/internal/fakeHttpReceive.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import type { ServerResponse } from "node:http";
2
+ import type { FakeHttpReply } from "#fakeHttpService.ts";
3
+ export declare function fakeHttpConnection(response: ServerResponse): {
4
+ closed: Promise<undefined>;
5
+ pause: (pending: Promise<void> | undefined) => Promise<void>;
6
+ };
7
+ export declare function fakeHttpSend(response: ServerResponse, reply: FakeHttpReply, pause?: () => Promise<void>): Promise<void>;
8
+ //# sourceMappingURL=fakeHttpSend.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fakeHttpSend.d.ts","sourceRoot":"","sources":["../../../src/server/internal/fakeHttpSend.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,cAAc;;qBAIlC,OAAO,CAAC,IAAI,CAAC,GAAG,SAAS;EAMjD;AAwBD,wBAAsB,YAAY,CAAC,QAAQ,EAAE,cAAc,EAAE,KAAK,EAAE,aAAa,EAAE,KAAK,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,iBAiB7G"}
@@ -0,0 +1,51 @@
1
+ export function fakeHttpConnection(response) {
2
+ const closed = new Promise((resolve) => response.once("close", () => resolve(undefined)));
3
+ return {
4
+ closed,
5
+ pause: async (pending) => {
6
+ if (pending !== undefined) {
7
+ await Promise.race([pending, closed]);
8
+ }
9
+ },
10
+ };
11
+ }
12
+ async function sendChunks(response, chunks, closed) {
13
+ const iterator = Symbol.asyncIterator in chunks ? chunks[Symbol.asyncIterator]() : chunks[Symbol.iterator]();
14
+ try {
15
+ while (!(response.destroyed || response.writableEnded)) {
16
+ const next = await Promise.race([iterator.next(), closed]);
17
+ if (!next || next.done) {
18
+ break;
19
+ }
20
+ await new Promise((resolve, reject) => response.write(next.value, (error) => (error ? reject(error) : resolve())));
21
+ }
22
+ }
23
+ finally {
24
+ void Promise.resolve(iterator.return?.()).catch(() => response.destroy());
25
+ }
26
+ }
27
+ function replyBody(body) {
28
+ if (body instanceof Uint8Array || body === undefined) {
29
+ return body;
30
+ }
31
+ return JSON.stringify(body);
32
+ }
33
+ export async function fakeHttpSend(response, reply, pause) {
34
+ if (response.destroyed || response.writableEnded) {
35
+ return;
36
+ }
37
+ const connection = fakeHttpConnection(response);
38
+ const body = replyBody(reply.body);
39
+ response.writeHead(reply.status ?? 200, { "content-type": "application/json", ...reply.headers });
40
+ if (pause) {
41
+ response.flushHeaders();
42
+ await connection.pause(pause());
43
+ }
44
+ if (reply.chunks) {
45
+ await sendChunks(response, reply.chunks, connection.closed);
46
+ }
47
+ if (!(response.destroyed || response.writableEnded)) {
48
+ response.end(body);
49
+ }
50
+ }
51
+ //# sourceMappingURL=fakeHttpSend.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fakeHttpSend.js","sourceRoot":"","sources":["../../../src/server/internal/fakeHttpSend.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shivaedev/test-story",
3
- "version": "0.11.1",
3
+ "version": "0.13.0",
4
4
  "description": "Tests that read like stories over the real engine: a story kit per engine with its own it, traits per feature, and a failure that prints the story, where it stopped and the engine's state",
5
5
  "homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/test-story#readme",
6
6
  "bugs": {
@@ -46,7 +46,7 @@
46
46
  },
47
47
  "devDependencies": {
48
48
  "@effect/vitest": "4.0.4",
49
- "@shivaedev/quality": "0.27.1",
49
+ "@shivaedev/quality": "0.28.1",
50
50
  "@types/node": "24.19.2",
51
51
  "effect": "4.0.4",
52
52
  "happy-dom": "20.14.6",
@@ -15,10 +15,13 @@ export interface FakeHttpRequest {
15
15
 
16
16
  export interface FakeHttpReply {
17
17
  readonly body?: unknown;
18
+ readonly chunks?: Iterable<string | Uint8Array> | AsyncIterable<string | Uint8Array>;
18
19
  readonly headers?: Readonly<Record<string, string>>;
19
20
  readonly status?: number;
20
21
  }
21
22
 
23
+ export type FakeHttpFault = { readonly kind: "status"; readonly reply: FakeHttpReply } | { readonly kind: "drop" | "reset" };
24
+
22
25
  export type FakeHttpRoute = {
23
26
  readonly method?: string;
24
27
  readonly path: string;
@@ -54,14 +57,22 @@ export const fakeHttpService = Effect.fnUntraced(function* (options: FakeHttpSer
54
57
  }
55
58
  }
56
59
  return {
57
- failOnce: (route: string, reply: FakeHttpReply) => Effect.sync(() => checkRoute(route)).pipe(Effect.andThen(controls.failOnce(route, reply))),
60
+ failOnce: (route: string, reply: FakeHttpReply) => Effect.sync(() => checkRoute(route)).pipe(Effect.andThen(controls.queue(route, reply))),
61
+ fault: (route: string, fault: FakeHttpFault) => Effect.sync(() => checkRoute(route)).pipe(Effect.andThen(controls.fault(route, fault))),
58
62
  hold: (route: string) => Effect.sync(() => checkRoute(route)).pipe(Effect.andThen(controls.hold(route))),
59
63
  narrate: (sink: (line: string) => void) => {
60
64
  narrate = sink;
61
65
  },
66
+ releaseOnArrival: (route: string, arrival: string) =>
67
+ Effect.sync(() => {
68
+ checkRoute(route);
69
+ checkRoute(arrival);
70
+ }).pipe(Effect.andThen(controls.releaseOnArrival(route, arrival))),
71
+ replyOnce: (route: string, reply: FakeHttpReply) => Effect.sync(() => checkRoute(route)).pipe(Effect.andThen(controls.queue(route, reply))),
62
72
  get requests(): readonly FakeHttpRequest[] {
63
73
  return requests.slice();
64
74
  },
75
+ stall: (route: string) => Effect.sync(() => checkRoute(route)).pipe(Effect.andThen(controls.stall(route))),
65
76
  url,
66
77
  };
67
78
  });
@@ -1,21 +1,33 @@
1
1
  import { createServer, type RequestListener, request } from "node:http";
2
+ import { isIP } from "node:net";
2
3
  import { Effect } from "effect";
3
4
  import { loopbackServer } from "#internal/loopback.ts";
4
5
 
5
6
  export interface ForwardingProxyOptions {
7
+ readonly forwardedHops?: readonly string[];
6
8
  readonly headers?: Readonly<Record<string, string>>;
9
+ readonly localAddress?: string;
7
10
  }
8
11
 
9
12
  export const forwardingProxy = Effect.fnUntraced(function* (upstream: string, options: ForwardingProxyOptions = {}) {
13
+ if (options.localAddress !== undefined) {
14
+ yield* Effect.scoped(loopbackServer(createServer(), { host: options.localAddress })).pipe(
15
+ Effect.mapError((cause) => new Error(`Cannot acquire forwarding proxy with local address ${options.localAddress}`, { cause })),
16
+ );
17
+ }
10
18
  const forward: RequestListener = (incoming, response) => {
11
19
  const outgoing = request(
12
20
  `${upstream}${incoming.url}`,
13
21
  {
22
+ family: options.localAddress === undefined ? undefined : isIP(options.localAddress),
14
23
  headers: {
15
24
  ...incoming.headers,
16
- "x-forwarded-for": [incoming.headers["x-forwarded-for"], incoming.socket.remoteAddress].filter(Boolean).join(", "),
25
+ "x-forwarded-for": [incoming.headers["x-forwarded-for"], ...(options.forwardedHops ?? [incoming.socket.remoteAddress])]
26
+ .filter(Boolean)
27
+ .join(", "),
17
28
  ...options.headers,
18
29
  },
30
+ localAddress: options.localAddress,
19
31
  method: incoming.method,
20
32
  },
21
33
  (result) => {
@@ -1,51 +1,79 @@
1
- import { Deferred, Effect } from "effect";
2
- import type { FakeHttpHold, FakeHttpReply } from "#fakeHttpService.ts";
1
+ import type { ServerResponse } from "node:http";
2
+ import { Effect } from "effect";
3
+ import type { FakeHttpFault, FakeHttpReply } from "#fakeHttpService.ts";
4
+ import { fakeHttpGates } from "#internal/fakeHttpGates.ts";
5
+ import { fakeHttpConnection, fakeHttpSend } from "#internal/fakeHttpSend.ts";
3
6
 
4
7
  export function fakeHttpControls(tell: (line: string) => void) {
5
- const failures = new Map<string, FakeHttpReply[]>();
6
- const holds = new Map<string, { readonly enter: () => Promise<void>; readonly release: () => void }>();
8
+ const replies = new Map<string, FakeHttpReply[]>();
9
+ const holds = fakeHttpGates(tell, "hold");
10
+ const stalls = fakeHttpGates(tell, "stall");
11
+ const arrivals = new Map<string, Set<() => void>>();
12
+ const active = new Map<ServerResponse, string>();
7
13
  return {
14
+ arrive: (route: string, response: ServerResponse) => {
15
+ active.set(response, route);
16
+ response.once("close", () => active.delete(response));
17
+ for (const release of arrivals.get(route) ?? []) {
18
+ release();
19
+ }
20
+ arrivals.delete(route);
21
+ },
8
22
  close: () => {
9
- for (const hold of holds.values()) {
10
- hold.release();
23
+ for (const response of active.keys()) {
24
+ response.destroy();
11
25
  }
26
+ holds.close();
27
+ stalls.close();
28
+ arrivals.clear();
29
+ replies.clear();
30
+ },
31
+ fault: (route: string, fault: FakeHttpFault) =>
32
+ Effect.promise(async () => {
33
+ const response = [...active].find(
34
+ ([candidate, name]) =>
35
+ name === route && !(candidate.destroyed || candidate.writableEnded) && candidate.headersSent === (fault.kind === "reset"),
36
+ )?.[0];
37
+ if (!response) {
38
+ throw new Error(
39
+ `Fake HTTP route "${route}" has no request ready for a ${fault.kind} fault.\nhelp: Wait for a hold before a status or drop fault, or for response headers before a reset fault.`,
40
+ );
41
+ }
42
+ tell(`fake HTTP ${route} fault ${fault.kind}`);
43
+ if (fault.kind === "status") {
44
+ await fakeHttpSend(response, fault.reply);
45
+ } else {
46
+ response.destroy();
47
+ }
48
+ }),
49
+ hold: (route: string) => Effect.sync(() => holds.make(route)),
50
+ pause: async (route: string, response: ServerResponse) => {
51
+ await fakeHttpConnection(response).pause(holds.get(route)?.enter());
52
+ return !(response.destroyed || response.writableEnded || response.headersSent);
12
53
  },
13
- failOnce: (route: string, reply: FakeHttpReply) =>
54
+ queue: (route: string, reply: FakeHttpReply) =>
14
55
  Effect.sync(() => {
15
- const queue = failures.get(route) ?? [];
56
+ const queue = replies.get(route) ?? [];
16
57
  queue.push(reply);
17
- failures.set(route, queue);
58
+ replies.set(route, queue);
18
59
  tell(`fake HTTP ${route} will answer once with ${reply.status ?? 200}`);
19
60
  }),
20
- hold: (route: string): Effect.Effect<FakeHttpHold> =>
61
+ releaseOnArrival: (route: string, arrival: string) =>
21
62
  Effect.sync(() => {
22
- if (holds.has(route)) {
23
- throw new Error(`Fake HTTP route "${route}" is already held.\nhelp: Release its current hold before holding the route again.`);
63
+ const hold = holds.get(route);
64
+ if (!hold) {
65
+ throw new Error(`Fake HTTP route "${route}" has no hold.\nhelp: Hold the route before scripting its release on arrival.`);
24
66
  }
25
- const waiting = Deferred.makeUnsafe<void>();
26
- const released = Promise.withResolvers<void>();
27
- const hold = {
28
- enter: () => {
29
- tell(`fake HTTP ${route} has a waiting request`);
30
- Deferred.doneUnsafe(waiting, Effect.void);
31
- return released.promise;
32
- },
33
- release: () => {
34
- if (holds.get(route) !== hold) {
35
- return;
36
- }
37
- holds.delete(route);
38
- released.resolve();
39
- tell(`fake HTTP ${route} released`);
40
- },
41
- };
42
- holds.set(route, hold);
43
- tell(`fake HTTP ${route} held`);
44
- return { release: Effect.sync(hold.release), waiting: Deferred.await(waiting) };
67
+ const releases = arrivals.get(arrival) ?? new Set<() => void>();
68
+ releases.add(hold.release);
69
+ arrivals.set(arrival, releases);
70
+ tell(`fake HTTP ${route} will release when ${arrival} arrives`);
45
71
  }),
46
- pause: async (route: string) => {
47
- await holds.get(route)?.enter();
72
+ stall: (route: string) => Effect.sync(() => stalls.make(route)),
73
+ stallBody: (route: string) => {
74
+ const stall = stalls.get(route);
75
+ return stall ? () => stall.enter() : undefined;
48
76
  },
49
- takeFailure: (route: string) => failures.get(route)?.shift(),
77
+ takeReply: (route: string) => replies.get(route)?.shift(),
50
78
  };
51
79
  }
@@ -0,0 +1,39 @@
1
+ import { Deferred, Effect } from "effect";
2
+ import type { FakeHttpHold } from "#fakeHttpService.ts";
3
+
4
+ export function fakeHttpGates(tell: (line: string) => void, kind: "hold" | "stall") {
5
+ const gates = new Map<string, { enter: () => Promise<void>; release: () => void }>();
6
+ return {
7
+ close: () => {
8
+ for (const gate of gates.values()) {
9
+ gate.release();
10
+ }
11
+ },
12
+ get: (route: string) => gates.get(route),
13
+ make: (route: string): FakeHttpHold => {
14
+ if (gates.has(route)) {
15
+ throw new Error(`Fake HTTP route "${route}" already has a ${kind}.\nhelp: Release its current ${kind} before scripting another.`);
16
+ }
17
+ const waiting = Deferred.makeUnsafe<void>();
18
+ const released = Promise.withResolvers<void>();
19
+ const gate = {
20
+ enter: () => {
21
+ tell(kind === "hold" ? `fake HTTP ${route} has a waiting request` : `fake HTTP ${route} body stalled`);
22
+ Deferred.doneUnsafe(waiting, Effect.void);
23
+ return released.promise;
24
+ },
25
+ release: () => {
26
+ if (gates.get(route) !== gate) {
27
+ return;
28
+ }
29
+ gates.delete(route);
30
+ released.resolve();
31
+ tell(kind === "hold" ? `fake HTTP ${route} released` : `fake HTTP ${route} body released`);
32
+ },
33
+ };
34
+ gates.set(route, gate);
35
+ tell(kind === "hold" ? `fake HTTP ${route} held` : `fake HTTP ${route} body will stall`);
36
+ return { release: Effect.sync(gate.release), waiting: Deferred.await(waiting) };
37
+ },
38
+ };
39
+ }
@@ -1,6 +1,7 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import type { FakeHttpReply, FakeHttpRequest, FakeHttpRoute, FakeHttpServiceOptions } from "#fakeHttpService.ts";
3
3
  import type { fakeHttpControls } from "#internal/fakeHttpControls.ts";
4
+ import { fakeHttpSend } from "#internal/fakeHttpSend.ts";
4
5
 
5
6
  function match(route: FakeHttpRoute, method: string, pathname: string) {
6
7
  if ((route.method ?? "GET").toUpperCase() !== method) {
@@ -45,12 +46,6 @@ function pageReply(route: FakeHttpRoute, request: FakeHttpRequest): FakeHttpRepl
45
46
  return { body: { items: route.pages[page - 1] ?? [], next: page < route.pages.length ? page + 1 : null } };
46
47
  }
47
48
 
48
- function send(response: ServerResponse, reply: FakeHttpReply) {
49
- const body = reply.body === undefined ? undefined : JSON.stringify(reply.body);
50
- response.writeHead(reply.status ?? 200, { "content-type": "application/json", ...reply.headers });
51
- response.end(body);
52
- }
53
-
54
49
  export async function fakeHttpReceive(
55
50
  incoming: IncomingMessage,
56
51
  response: ServerResponse,
@@ -84,29 +79,32 @@ export async function fakeHttpReceive(
84
79
  tell(`fake HTTP received ${method} ${path}`);
85
80
  if (!decodeParams(params)) {
86
81
  tell(`fake HTTP rejected ${method} ${path} with 400`);
87
- send(response, { status: 400 });
82
+ await fakeHttpSend(response, { status: 400 });
88
83
  return;
89
84
  }
90
85
  if (options.guard && !options.guard(request)) {
91
86
  tell(`fake HTTP rejected ${method} ${path} with 401`);
92
- send(response, { status: 401 });
87
+ await fakeHttpSend(response, { status: 401 });
93
88
  return;
94
89
  }
95
90
  if (!found) {
96
91
  tell(`fake HTTP answered ${method} ${path} with 404`);
97
- send(response, { status: 404 });
92
+ await fakeHttpSend(response, { status: 404 });
93
+ return;
94
+ }
95
+ controls.arrive(found.name, response);
96
+ if (!(await controls.pause(found.name, response))) {
98
97
  return;
99
98
  }
100
- await controls.pause(found.name);
101
- const reply = controls.takeFailure(found.name) ?? pageReply(found.route, request);
99
+ const reply = controls.takeReply(found.name) ?? pageReply(found.route, request);
102
100
  tell(`fake HTTP answered ${method} ${path} with ${reply.status ?? 200}`);
103
- send(response, reply);
101
+ await fakeHttpSend(response, reply, controls.stallBody(found.name));
104
102
  } catch (error) {
105
103
  tell(`fake HTTP could not answer: ${String(error)}`);
106
104
  if (response.headersSent) {
107
- response.end();
105
+ response.destroy();
108
106
  } else {
109
- send(response, { status: 500 });
107
+ await fakeHttpSend(response, { status: 500 });
110
108
  }
111
109
  }
112
110
  }
@@ -0,0 +1,55 @@
1
+ import type { ServerResponse } from "node:http";
2
+ import type { FakeHttpReply } from "#fakeHttpService.ts";
3
+
4
+ export function fakeHttpConnection(response: ServerResponse) {
5
+ const closed = new Promise<undefined>((resolve) => response.once("close", () => resolve(undefined)));
6
+ return {
7
+ closed,
8
+ pause: async (pending: Promise<void> | undefined) => {
9
+ if (pending !== undefined) {
10
+ await Promise.race([pending, closed]);
11
+ }
12
+ },
13
+ };
14
+ }
15
+
16
+ async function sendChunks(response: ServerResponse, chunks: NonNullable<FakeHttpReply["chunks"]>, closed: Promise<undefined>) {
17
+ const iterator = Symbol.asyncIterator in chunks ? chunks[Symbol.asyncIterator]() : chunks[Symbol.iterator]();
18
+ try {
19
+ while (!(response.destroyed || response.writableEnded)) {
20
+ const next = await Promise.race([iterator.next(), closed]);
21
+ if (!next || next.done) {
22
+ break;
23
+ }
24
+ await new Promise<void>((resolve, reject) => response.write(next.value, (error) => (error ? reject(error) : resolve())));
25
+ }
26
+ } finally {
27
+ void Promise.resolve(iterator.return?.()).catch(() => response.destroy());
28
+ }
29
+ }
30
+
31
+ function replyBody(body: unknown) {
32
+ if (body instanceof Uint8Array || body === undefined) {
33
+ return body;
34
+ }
35
+ return JSON.stringify(body);
36
+ }
37
+
38
+ export async function fakeHttpSend(response: ServerResponse, reply: FakeHttpReply, pause?: () => Promise<void>) {
39
+ if (response.destroyed || response.writableEnded) {
40
+ return;
41
+ }
42
+ const connection = fakeHttpConnection(response);
43
+ const body = replyBody(reply.body);
44
+ response.writeHead(reply.status ?? 200, { "content-type": "application/json", ...reply.headers });
45
+ if (pause) {
46
+ response.flushHeaders();
47
+ await connection.pause(pause());
48
+ }
49
+ if (reply.chunks) {
50
+ await sendChunks(response, reply.chunks, connection.closed);
51
+ }
52
+ if (!(response.destroyed || response.writableEnded)) {
53
+ response.end(body);
54
+ }
55
+ }