@shivaedev/test-story 0.11.1 → 0.12.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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.12.0 - 2026-10-18
4
+
5
+ ### Added
6
+
7
+ - 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.
8
+
3
9
  ## 0.11.1 - 2026-10-18
4
10
 
5
11
  ### Changed
package/README.md CHANGED
@@ -270,7 +270,7 @@ For timeout stories, return `undefined` from the `status` callback to accept an
270
270
 
271
271
  #### Faking an outbound HTTP API
272
272
 
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`.
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` (or a `Uint8Array`/Buffer sent as raw bytes), optional `status` (default 200), and optional response `headers`.
274
274
 
275
275
  ```ts
276
276
  import { fakeHttpService } from "@shivaedev/test-story/fakeHttpService.ts";
@@ -300,6 +300,26 @@ const user = yield* Fiber.join(pending);
300
300
 
301
301
  `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
302
 
303
+ 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.
304
+
305
+ ```ts
306
+ yield* api.replyOnce("download", {
307
+ body: Uint8Array.from([0, 255, 128]),
308
+ headers: { "content-type": "application/octet-stream" },
309
+ });
310
+ const stalled = yield* api.stall("download");
311
+ const pending = yield* Effect.forkChild(application.download());
312
+ yield* stalled.waiting;
313
+ yield* stalled.release;
314
+ const bytes = yield* Fiber.join(pending);
315
+ ```
316
+
317
+ `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.
318
+
319
+ `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.
320
+
321
+ 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.
322
+
303
323
  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
324
 
305
325
  ### 2. Once per feature: traits and verbs
@@ -577,7 +597,7 @@ Import each module with the prefix `@shivaedev/test-story/`; there is no root en
577
597
  | `domNavigate.ts` | `domNavigate(url?)` |
578
598
  | `genreTags.ts` | `genreTags(...kitNames)`, `GenreTag` |
579
599
  | `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` |
600
+ | `fakeHttpService.ts` | `fakeHttpService(options)`, `FakeHttpRoute`, `FakeHttpRequest`, `FakeHttpReply`, `FakeHttpFault`, `FakeHttpHold`, `FakeHttpServiceOptions` |
581
601
  | `forwardingProxy.ts` | `forwardingProxy(upstream, options?)`, `ForwardingProxyOptions` |
582
602
  | `otlpReceiver.ts` | `otlpReceiver(options?)`, `OtlpSpan`, `OtlpExport`, `OtlpReceiverOptions` |
583
603
 
@@ -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,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":"AAGA,MAAM,UAAU,kBAAkB,CAAC,QAAwB;IAC1D,MAAM,MAAM,GAAG,IAAI,OAAO,CAAY,CAAC,OAAO,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;IACrG,OAAO;QACN,MAAM;QACN,KAAK,EAAE,KAAK,EAAE,OAAkC,EAAE,EAAE;YACnD,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;gBAC3B,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;YACvC,CAAC;QACF,CAAC;KACD,CAAC;AACH,CAAC;AAED,KAAK,UAAU,UAAU,CAAC,QAAwB,EAAE,MAA4C,EAAE,MAA0B;IAC3H,MAAM,QAAQ,GAAG,MAAM,CAAC,aAAa,IAAI,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;IAC7G,IAAI,CAAC;QACJ,OAAO,CAAC,CAAC,QAAQ,CAAC,SAAS,IAAI,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC;YACxD,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC;YAC3D,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;gBACxB,MAAM;YACP,CAAC;YACD,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;QAC1H,CAAC;IACF,CAAC;YAAS,CAAC;QACV,KAAK,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IAC3E,CAAC;AACF,CAAC;AAED,SAAS,SAAS,CAAC,IAAa;IAC/B,IAAI,IAAI,YAAY,UAAU,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACtD,OAAO,IAAI,CAAC;IACb,CAAC;IACD,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;AAC7B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,QAAwB,EAAE,KAAoB,EAAE,KAA2B;IAC7G,IAAI,QAAQ,CAAC,SAAS,IAAI,QAAQ,CAAC,aAAa,EAAE,CAAC;QAClD,OAAO;IACR,CAAC;IACD,MAAM,UAAU,GAAG,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IAChD,MAAM,IAAI,GAAG,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACnC,QAAQ,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,IAAI,GAAG,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,GAAG,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;IAClG,IAAI,KAAK,EAAE,CAAC;QACX,QAAQ,CAAC,YAAY,EAAE,CAAC;QACxB,MAAM,UAAU,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC;IACjC,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;QAClB,MAAM,UAAU,CAAC,QAAQ,EAAE,KAAK,CAAC,MAAM,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;IAC7D,CAAC;IACD,IAAI,CAAC,CAAC,QAAQ,CAAC,SAAS,IAAI,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC;QACrD,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACpB,CAAC;AACF,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shivaedev/test-story",
3
- "version": "0.11.1",
3
+ "version": "0.12.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.0",
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,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
+ }