@ingenioz-it/caits 0.6.0 → 0.8.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 (57) hide show
  1. package/bin/caits.js +20 -7
  2. package/bin/init.js +16 -1
  3. package/package.json +8 -5
  4. package/quality/architecture/adapters/scss.mjs +2 -2
  5. package/quality/architecture/facts.mjs +39 -11
  6. package/quality/architecture/project.mjs +8 -6
  7. package/quality/dev.mjs +6 -3
  8. package/quality/mutation-summary.mjs +30 -20
  9. package/quality/mutation.mjs +170 -14
  10. package/src/adapter.js +94 -0
  11. package/src/di/Container.ts +27 -0
  12. package/src/di/RequestContext.ts +8 -2
  13. package/src/di/backend.ts +12 -0
  14. package/src/di/createBackend.ts +197 -25
  15. package/src/di/declareDependencies.js +25 -5
  16. package/src/di/discover.ts +19 -9
  17. package/src/di/discoverFrontends.ts +4 -8
  18. package/src/di/discovered.ts +2 -2
  19. package/src/di/discoveredFrontends.ts +2 -2
  20. package/src/messaging/BackendEventBus.ts +2 -3
  21. package/src/messaging/ContextEvents.ts +11 -0
  22. package/src/messaging/ContextMap.ts +3 -4
  23. package/src/messaging/Controller.ts +36 -75
  24. package/src/messaging/EventSubscriber.ts +1 -1
  25. package/src/messaging/InProcessMessageBus.ts +8 -9
  26. package/src/messaging/MessageBus.ts +4 -2
  27. package/src/messaging/Request.ts +116 -0
  28. package/src/messaging/RequestEvents.ts +8 -0
  29. package/src/messaging/StreamController.ts +261 -0
  30. package/src/messaging/TimerController.ts +25 -0
  31. package/src/messaging/sse.ts +23 -0
  32. package/src/server/entry.ts +37 -0
  33. package/src/server/kernel.ts +19 -0
  34. package/src/server/packages.d.ts +12 -0
  35. package/src/server/serve.js +121 -0
  36. package/src/state/createStore.ts +1 -2
  37. package/src/testing/backend/application.ts +1 -1
  38. package/src/testing/backend/applicationScenario.ts +6 -7
  39. package/src/testing/backend/applications.ts +4 -1
  40. package/src/testing/backend/context-map.ts +1 -1
  41. package/src/testing/backend/integration.ts +1 -1
  42. package/src/testing/backend/presentationApplication.ts +23 -3
  43. package/src/testing/backend/presentationScenario.ts +84 -7
  44. package/src/testing/frontend/applicationScenario.ts +2 -3
  45. package/src/testing/frontend/applications.ts +3 -3
  46. package/src/testing/frontend/frontends.ts +3 -4
  47. package/src/testing/shared/FakeMessageBus.ts +39 -5
  48. package/src/virtual-modules.d.ts +0 -2
  49. package/src/vite.js +2 -2
  50. package/src/vite.types.d.ts +7 -0
  51. package/template/caits/architecture.mjs +162 -113
  52. package/template/caits/imports.mjs +2 -2
  53. package/template/caits/stryker.mjs +11 -6
  54. package/template/caits/vite.ts +15 -5
  55. package/template/project/AGENTS +1 -1
  56. package/quality/mutation-suite.mjs +0 -82
  57. package/src/messaging/Input.ts +0 -4
@@ -1,6 +1,6 @@
1
1
  /// <reference path="../virtual-modules.d.ts" />
2
- import { bindings, dtos, handlers, controllers, maps } from "virtual:kernel/backend";
2
+ import { bindings, handlers, controllers, maps } from "virtual:kernel/backend";
3
3
  import { discover } from "./discover.js";
4
4
 
5
5
  /** Every context of the backend: its di.ts, its use cases, its controllers and its context map. The plugin of the kernel finds them. */
6
- export const discovered = discover({ bindings, dtos, handlers, controllers, maps });
6
+ export const discovered = discover({ bindings, handlers, controllers, maps });
@@ -1,6 +1,6 @@
1
1
  /// <reference path="../virtual-modules.d.ts" />
2
- import { bindings, dtos, handlers, maps, stores } from "virtual:kernel/frontend";
2
+ import { bindings, handlers, maps, stores } from "virtual:kernel/frontend";
3
3
  import { discoverFrontends } from "./discoverFrontends.js";
4
4
 
5
5
  /** Every context of the frontend: its di.ts, its use cases, its store and its context map. The plugin of the kernel finds them. */
6
- export const discoveredFrontends = discoverFrontends({ bindings, dtos, handlers, stores, maps });
6
+ export const discoveredFrontends = discoverFrontends({ bindings, handlers, stores, maps });
@@ -1,6 +1,5 @@
1
1
  import type { DomainEvent } from "../DomainEvent.js";
2
2
  import type { ContextMap, Envelope } from "./ContextMap.js";
3
- import type { Input } from "./Input.js";
4
3
  import type { ReportError } from "./ReportError.js";
5
4
 
6
5
  export type Published = { readonly event: DomainEvent; readonly envelope: Envelope };
@@ -18,7 +17,7 @@ export class BackendEventBus {
18
17
 
19
18
  constructor(private readonly options: {
20
19
  map: ContextMap;
21
- send: (input: Input<unknown>, envelope: Envelope) => Promise<unknown>;
20
+ send: (input: object, envelope: Envelope) => Promise<unknown>;
22
21
  nextEventId: () => string;
23
22
  reportError: ReportError;
24
23
  }) {}
@@ -48,7 +47,7 @@ export class BackendEventBus {
48
47
 
49
48
  private async deliver(event: DomainEvent, envelope: Envelope): Promise<void> {
50
49
  await nextTurn();
51
- let inputs: Input<unknown>[];
50
+ let inputs: object[];
52
51
  try {
53
52
  inputs = this.options.map.inputsFor(event, envelope);
54
53
  } catch (error) {
@@ -0,0 +1,11 @@
1
+ import type { DomainEvent } from "../DomainEvent.js";
2
+ import { Token } from "../Token.js";
3
+ import type { EventSubscriber } from "./EventSubscriber.js";
4
+
5
+ /**
6
+ * The events that the handlers of a context publish, in every request: a controller that streams a feed writes them. They end when the
7
+ * backend stops: then `signal` aborts, and a feed ends with them.
8
+ */
9
+ export type ContextEvents = EventSubscriber<DomainEvent> & { readonly signal: AbortSignal };
10
+
11
+ export const ContextEvents = new Token<ContextEvents>();
@@ -1,5 +1,4 @@
1
1
  import type { DomainEvent } from "../DomainEvent.js";
2
- import type { Input } from "./Input.js";
3
2
 
4
3
  /** What the shared bus adds to an event it carries: an id of its own, and the correlation id of the request that published it. */
5
4
  export type Envelope = { readonly eventId: string; readonly correlationId: string };
@@ -11,14 +10,14 @@ type EventClass<E> = abstract new (...args: never[]) => E;
11
10
  * input DTOs of the context, or an event of the context that it shares, which becomes events of the context shared.
12
11
  */
13
12
  export type Translation =
14
- | { readonly shares: false; readonly event: EventClass<DomainEvent>; readonly inputs: (event: never, envelope: Envelope) => Input<unknown>[] }
13
+ | { readonly shares: false; readonly event: EventClass<DomainEvent>; readonly inputs: (event: never, envelope: Envelope) => object[] }
15
14
  | { readonly shares: true; readonly event: EventClass<DomainEvent>; readonly events: (event: never) => DomainEvent[] };
16
15
 
17
16
  /**
18
17
  * A line that hears the context shared: when this event of shared comes on the shared bus, the handlers of the context execute these
19
18
  * input DTOs. A context map names only its own context and the context shared: two contexts never name each other.
20
19
  */
21
- export function when<E extends DomainEvent>(event: EventClass<E>, inputs: (event: E, envelope: Envelope) => Input<unknown>[]): Translation {
20
+ export function when<E extends DomainEvent>(event: EventClass<E>, inputs: (event: E, envelope: Envelope) => object[]): Translation {
22
21
  return { shares: false, event, inputs };
23
22
  }
24
23
 
@@ -35,7 +34,7 @@ export class ContextMap {
35
34
  constructor(private readonly translations: Translation[]) {}
36
35
 
37
36
  /** The input DTOs that the lines that hear this event of shared give, none when no line hears it. */
38
- inputsFor(event: DomainEvent, envelope: Envelope): Input<unknown>[] {
37
+ inputsFor(event: DomainEvent, envelope: Envelope): object[] {
39
38
  return this.translations.flatMap(translation => !translation.shares && event instanceof translation.event ? translation.inputs(event as never, envelope) : []);
40
39
  }
41
40
 
@@ -1,62 +1,9 @@
1
- import type { Input } from "./Input.js";
2
1
  import type { MessageBus } from "./MessageBus.js";
3
2
  import { reportToConsole, type ReportError } from "./ReportError.js";
3
+ import { BodyTooLargeError, CrossSiteRequestError, InvalidInputError, MalformedBodyError, Request, UnsupportedMediaTypeError } from "./Request.js";
4
4
 
5
- export class InvalidInputError extends Error {
6
- constructor(message: string, readonly details?: unknown) {
7
- super(message);
8
- this.name = "InvalidInputError";
9
- }
10
- }
11
-
12
- export class MalformedBodyError extends InvalidInputError {}
13
-
14
- export class UnsupportedMediaTypeError extends InvalidInputError {}
15
-
16
- export class BodyTooLargeError extends InvalidInputError {}
17
-
18
- /** The largest body that readBody reads by default, in bytes: 1 MB. */
19
- export const MAX_BODY_SIZE = 1024 * 1024;
20
-
21
- /**
22
- * A JSON media type, in any case: application/json, or a type with the suffix +json (application/merge-patch+json). Parameters can
23
- * follow, after a ";". A parameter is not the type: a browser sends `text/plain;x+json` from any site, as text/plain.
24
- */
25
- const JSON_MEDIA_TYPE = /^(?:application\/json|[^\s/]+\/[^\s/;]+\+json)\s*(?:;|$)/i;
26
-
27
- /**
28
- * The JSON of the body. The request must say that its body is JSON: content-type application/json, or a type with the suffix +json.
29
- * A browser sends a body of another type (text/plain, a form) from any site, without asking the server first: such a request cannot
30
- * change the data of the app. The body must not be larger than `maxSize` bytes (MAX_BODY_SIZE, 1 MB, by default): the rest of a
31
- * larger body is not read. The request stays readable.
32
- * @throws {UnsupportedMediaTypeError} when the request does not say that its body is JSON: the Controller answers 415.
33
- * @throws {BodyTooLargeError} when the body is larger than `maxSize`: the Controller answers 413.
34
- * @throws {MalformedBodyError} when the body is not JSON: the Controller answers 400.
35
- */
36
- export async function readBody(request: Request, maxSize = MAX_BODY_SIZE): Promise<unknown> {
37
- const contentType = request.headers.get("content-type");
38
- if (!contentType?.match(JSON_MEDIA_TYPE)) throw new UnsupportedMediaTypeError("The content type must be application/json");
39
- const text = await textOf(request.clone(), maxSize);
40
- try {
41
- return JSON.parse(text);
42
- } catch {
43
- throw new MalformedBodyError("Invalid JSON");
44
- }
45
- }
46
-
47
- /** The text of the body, read chunk by chunk: the reading stops at the first chunk after `maxSize` bytes. */
48
- async function textOf(request: Request, maxSize: number): Promise<string> {
49
- const decoder = new TextDecoder();
50
- let text = "";
51
- let size = 0;
52
- const reader = request.body?.getReader();
53
- for (let chunk = await reader?.read(); chunk && !chunk.done; chunk = await reader?.read()) {
54
- size += chunk.value.byteLength;
55
- if (size > maxSize) throw new BodyTooLargeError(`The body is larger than ${maxSize} bytes`);
56
- text += decoder.decode(chunk.value, { stream: true });
57
- }
58
- return text + decoder.decode();
59
- }
5
+ // The errors of a request that a controller reads, which its toErrorResponse answers.
6
+ export { BodyTooLargeError, CrossSiteRequestError, InvalidInputError, MalformedBodyError, MAX_BODY_SIZE, UnsupportedMediaTypeError } from "./Request.js";
60
7
 
61
8
  const problemType = (slug: string) => `urn:caits:problem:${slug}`;
62
9
 
@@ -67,43 +14,57 @@ export function problem(status: number, slug: string, title: string, detail?: st
67
14
  });
68
15
  }
69
16
 
70
- /** Turns a request into an input DTO, sends it to the message bus of the request, and turns the output or the error into a response. */
71
- export abstract class Controller<Dto extends Input<Output>, Output> {
72
- constructor(private bus: MessageBus, private reportError: ReportError = reportToConsole) {}
17
+ /**
18
+ * The response to an error of the request or of the application. A body that is neither JSON nor a form is a 415, a
19
+ * body too large a 413, a form that a browser sends from another site a 403, a body that is not JSON, or not the form that it says, a
20
+ * 400, a body that toDto refuses a 422 with its issues, and any other error is an internal server error, which is reported and tells
21
+ * nothing.
22
+ */
23
+ export function errorResponseOf(error: unknown, reportError: ReportError): Response {
24
+ if (error instanceof UnsupportedMediaTypeError) return problem(415, "unsupported-media-type", "Unsupported media type", error.message);
25
+ if (error instanceof BodyTooLargeError) return problem(413, "request-body-too-large", "Request body too large", error.message);
26
+ if (error instanceof CrossSiteRequestError) return problem(403, "cross-site-request", "Cross-site request", error.message);
27
+ if (error instanceof MalformedBodyError) return problem(400, "invalid-request-body", "Invalid request body", error.message);
28
+ if (error instanceof InvalidInputError) return problem(422, "invalid-request-body", "Invalid request body", error.message, { issues: error.details });
29
+ reportError(error);
30
+ return problem(500, "internal-server-error", "Internal server error");
31
+ }
32
+
33
+ /**
34
+ * Turns a request into an input DTO, sends it to the message bus of the request, and turns the output or the error into a response. It
35
+ * reads the request through the Request of CAITS, which gives the content of the body at each call, and gives the bus the signal of the
36
+ * request with the DTO: its handler sees its client leave.
37
+ */
38
+ export abstract class Controller<Dto extends object, Output> {
39
+ constructor(private bus: MessageBus, protected readonly reportError: ReportError = reportToConsole) {}
73
40
 
74
41
  protected abstract toDto(request: Request): Dto | Promise<Dto>;
75
42
 
76
43
  protected abstract toResponse(output: Output, request: Request): Response | Promise<Response>;
77
44
 
78
45
  /**
79
- * The response to an error of the request or of the application. A body that does not say it is JSON is a 415, a body too large a 413,
80
- * a body that is not JSON a 400, a body that toDto refuses a 422 with its issues, and any other error is an internal server error. A
81
- * controller answers its own errors first, then calls super.
46
+ * The response to an error of the request or of the application. A body that is neither JSON nor a form is a 415, a
47
+ * body too large a 413, a form that a browser sends from another site a 403, a body that is not JSON, or not the form that it says, a
48
+ * 400, a body that toDto refuses a 422 with its issues, and any other error is an internal server error. A controller answers its own
49
+ * errors first, then calls super.
82
50
  */
83
51
  protected toErrorResponse(error: unknown, _request: Request): Response | Promise<Response> {
84
- if (error instanceof UnsupportedMediaTypeError) return problem(415, "unsupported-media-type", "Unsupported media type", error.message);
85
- if (error instanceof BodyTooLargeError) return problem(413, "request-body-too-large", "Request body too large", error.message);
86
- if (error instanceof MalformedBodyError) return problem(400, "invalid-request-body", "Invalid request body", error.message);
87
- if (error instanceof InvalidInputError) return problem(422, "invalid-request-body", "Invalid request body", error.message, { issues: error.details });
88
- return this.internalServerError(error);
52
+ return errorResponseOf(error, this.reportError);
89
53
  }
90
54
 
91
- async handle(request: Request): Promise<Response> {
55
+ /** Answers a request of the web: toDto, toResponse and toErrorResponse read it through the Request of CAITS. */
56
+ async handle(received: globalThis.Request): Promise<Response> {
57
+ const request = new Request(received);
92
58
  try {
93
59
  let output: Output;
94
60
  try {
95
- output = await this.bus.send(await this.toDto(request));
61
+ output = await this.bus.send(await this.toDto(request), { signal: request.signal }) as Output;
96
62
  } catch (error) {
97
63
  return await this.toErrorResponse(error, request);
98
64
  }
99
65
  return await this.toResponse(output, request);
100
66
  } catch (failure) {
101
- return this.internalServerError(failure);
67
+ return errorResponseOf(failure, this.reportError);
102
68
  }
103
69
  }
104
-
105
- private internalServerError(error: unknown): Response {
106
- this.reportError(error);
107
- return problem(500, "internal-server-error", "Internal server error");
108
- }
109
70
  }
@@ -1,4 +1,4 @@
1
- /** Subscribes to the events of a frontend context: the store gets it. It cannot publish. */
1
+ /** Subscribes to the events of a context: in the frontend, the store gets it; in the backend, the controllers that stream. It cannot publish. */
2
2
  export interface EventSubscriber<E> {
3
3
  subscribe(listener: (event: E) => void): () => void;
4
4
  }
@@ -1,12 +1,10 @@
1
- import type { Input } from "./Input.js";
2
- import type { MessageBus } from "./MessageBus.js";
1
+ import type { MessageBus, Sending } from "./MessageBus.js";
3
2
 
4
- export type OutputOf<I> = I extends Input<infer Output> ? Output : never;
5
- type InputClass<I extends Input<unknown>> = abstract new (...args: never[]) => I;
3
+ type InputClass<I> = abstract new (...args: never[]) => I;
6
4
 
7
5
  export class UnknownInputError extends Error {
8
6
  constructor(readonly inputName: string) {
9
- super(`No handler subscribes to ${inputName}`);
7
+ super(`No handler takes ${inputName}: write one next to its DTO, in <use-case>.handler.ts, with execute(input: ${inputName})`);
10
8
  this.name = "UnknownInputError";
11
9
  }
12
10
  }
@@ -20,16 +18,17 @@ export class DuplicateHandlerError extends Error {
20
18
 
21
19
  /** The message bus of one context, in the process: each handler subscribes to the class of the input DTO it handles. */
22
20
  export class InProcessMessageBus implements MessageBus {
23
- private handlers = new Map<unknown, (input: never) => unknown>();
21
+ private handlers = new Map<unknown, (input: never, sending: Sending) => unknown>();
24
22
 
25
- subscribe<I extends Input<unknown>>(input: InputClass<I>, handler: (input: I) => OutputOf<I> | Promise<OutputOf<I>>): void {
23
+ /** The handler gets the input, and what goes with it: the signal of its request. */
24
+ subscribe<I extends object>(input: InputClass<I>, handler: (input: I, sending: Sending) => unknown): void {
26
25
  if (this.handlers.has(input)) throw new DuplicateHandlerError(input.name);
27
26
  this.handlers.set(input, handler);
28
27
  }
29
28
 
30
- async send<Output>(input: Input<Output>): Promise<Output> {
29
+ async send(input: object, sending: Sending = {}): Promise<unknown> {
31
30
  const handler = this.handlers.get(input.constructor);
32
31
  if (!handler) throw new UnknownInputError(input.constructor.name);
33
- return (await handler(input as never)) as Output;
32
+ return await handler(input as never, sending);
34
33
  }
35
34
  }
@@ -1,12 +1,14 @@
1
1
  import { Token } from "../Token.js";
2
- import type { Input } from "./Input.js";
2
+
3
+ /** What goes with an input DTO: the signal of its request, which aborts when its client leaves. Its handler reads it in its RequestContext. */
4
+ export type Sending = { readonly signal?: AbortSignal };
3
5
 
4
6
  /**
5
7
  * Where a controller or a store sends an input DTO: the message bus gives it to the handler of its class, and gives back the output of
6
8
  * the handler.
7
9
  */
8
10
  export interface MessageBus {
9
- send<Output>(input: Input<Output>): Promise<Output>;
11
+ send(input: object, sending?: Sending): Promise<unknown>;
10
12
  }
11
13
 
12
14
  export const MessageBus = new Token<MessageBus>();
@@ -0,0 +1,116 @@
1
+ export class InvalidInputError extends Error {
2
+ constructor(message: string, readonly details?: unknown) {
3
+ super(message);
4
+ this.name = "InvalidInputError";
5
+ }
6
+ }
7
+
8
+ export class MalformedBodyError extends InvalidInputError {}
9
+
10
+ export class UnsupportedMediaTypeError extends InvalidInputError {}
11
+
12
+ export class BodyTooLargeError extends InvalidInputError {}
13
+
14
+ export class CrossSiteRequestError extends InvalidInputError {}
15
+
16
+ /** The largest body that content reads by default, in bytes: 1 MB. */
17
+ export const MAX_BODY_SIZE = 1024 * 1024;
18
+
19
+ /**
20
+ * A JSON media type, in any case: application/json, or a type with the suffix +json (application/merge-patch+json). Parameters can
21
+ * follow, after a ";". A parameter is not the type: a browser sends `text/plain;x+json` from any site, as text/plain.
22
+ */
23
+ const JSON_MEDIA_TYPE = /^(?:application\/json|[^\s/]+\/[^\s/;]+\+json)\s*(?:;|$)/i;
24
+
25
+ /** The media types of a form, in any case, with parameters after a ";": the boundary of the parts of multipart/form-data. */
26
+ const FORM_MEDIA_TYPE = /^(?:multipart\/form-data|application\/x-www-form-urlencoded)\s*(?:;|$)/i;
27
+
28
+ /** The headers of a request, which a controller reads and does not change. */
29
+ export type RequestHeaders = Pick<Headers, "get" | "has" | "forEach" | "entries" | "keys" | "values" | typeof Symbol.iterator>;
30
+
31
+ /** What content reads: the largest body, in bytes. */
32
+ export type ContentOptions = { readonly maxSize?: number };
33
+
34
+ /**
35
+ * The request that a controller reads. It gives the address, the method, the headers and the signal of a client that leaves, as the
36
+ * request of the web gives them, and the content of the body, which it reads once, and gives again at each call: the request of the web
37
+ * reads its body once only. A controller reads this request, never the request of the web (rule controllers-read-the-request-of-caits).
38
+ */
39
+ export class Request {
40
+ readonly url: string;
41
+ readonly method: string;
42
+ readonly headers: RequestHeaders;
43
+ /** Aborts when the client leaves. */
44
+ readonly signal: AbortSignal;
45
+ readonly #request: globalThis.Request;
46
+ /** The bytes of the body, once read: the first call of content reads them, with its largest size. */
47
+ #body: Promise<Blob> | undefined;
48
+
49
+ constructor(request: globalThis.Request) {
50
+ this.#request = request;
51
+ this.url = request.url;
52
+ this.method = request.method;
53
+ this.headers = request.headers;
54
+ this.signal = request.signal;
55
+ }
56
+
57
+ /**
58
+ * The content of the body: the value of its JSON, or the fields of a form as an object, a text or a File for each name, a list for a
59
+ * name that comes more than once. The request must say what its body is: content-type application/json, or a type with the suffix
60
+ * +json, or a form, multipart/form-data or application/x-www-form-urlencoded. A browser sends a body of another type (text/plain) or a
61
+ * form from any site, without asking the server first, and says where the request comes from in its header Sec-Fetch-Site, which a
62
+ * client that is not a browser does not send: a form that a browser sends from another site is refused. The body must not be larger
63
+ * than `maxSize` bytes (MAX_BODY_SIZE, 1 MB, by default): the rest of a larger body is not read. The first call reads the body, with its
64
+ * `maxSize`; the next calls give its content again.
65
+ * @throws {UnsupportedMediaTypeError} when the request does not say that its body is JSON or a form: the Controller answers 415.
66
+ * @throws {CrossSiteRequestError} when a browser sends the form from another site: the Controller answers 403.
67
+ * @throws {BodyTooLargeError} when the body is larger than `maxSize`: the Controller answers 413.
68
+ * @throws {MalformedBodyError} when the body is not JSON, or not the form that it says: the Controller answers 400.
69
+ */
70
+ async content({ maxSize = MAX_BODY_SIZE }: ContentOptions = {}): Promise<unknown> {
71
+ const contentType = this.headers.get("content-type");
72
+ if (contentType?.match(JSON_MEDIA_TYPE)) {
73
+ const text = await (await this.#bytes(maxSize)).text();
74
+ try {
75
+ return JSON.parse(text);
76
+ } catch {
77
+ throw new MalformedBodyError("Invalid JSON");
78
+ }
79
+ }
80
+ if (!contentType?.match(FORM_MEDIA_TYPE)) throw new UnsupportedMediaTypeError("The content type must be application/json, multipart/form-data or application/x-www-form-urlencoded");
81
+ const site = this.headers.get("sec-fetch-site");
82
+ if (site === "cross-site" || site === "same-site") throw new CrossSiteRequestError("A browser sent this form from another site");
83
+ const bytes = await this.#bytes(maxSize);
84
+ let form: FormData;
85
+ try {
86
+ form = await new Response(bytes, { headers: { "content-type": contentType } }).formData();
87
+ } catch {
88
+ throw new MalformedBodyError("Invalid form");
89
+ }
90
+ const fields: Record<string, FormDataEntryValue | FormDataEntryValue[]> = {};
91
+ for (const [name, value] of form) {
92
+ // A field of its own, as JSON.parse makes it, also for a name such as __proto__.
93
+ const known = Object.hasOwn(fields, name) ? fields[name] : undefined;
94
+ Object.defineProperty(fields, name, { value: known === undefined ? value : [known, value].flat(), enumerable: true, writable: true, configurable: true });
95
+ }
96
+ return fields;
97
+ }
98
+
99
+ #bytes(maxSize: number): Promise<Blob> {
100
+ this.#body ??= bytesOf(this.#request, maxSize);
101
+ return this.#body;
102
+ }
103
+ }
104
+
105
+ /** The bytes of the body, read chunk by chunk: the reading stops at the first chunk after `maxSize` bytes. */
106
+ async function bytesOf(request: globalThis.Request, maxSize: number): Promise<Blob> {
107
+ const chunks: Uint8Array<ArrayBuffer>[] = [];
108
+ let size = 0;
109
+ const reader = request.body?.getReader();
110
+ for (let chunk = await reader?.read(); chunk && !chunk.done; chunk = await reader?.read()) {
111
+ size += chunk.value.byteLength;
112
+ if (size > maxSize) throw new BodyTooLargeError(`The body is larger than ${maxSize} bytes`);
113
+ chunks.push(chunk.value);
114
+ }
115
+ return new Blob(chunks);
116
+ }
@@ -0,0 +1,8 @@
1
+ import type { DomainEvent } from "../DomainEvent.js";
2
+ import { Token } from "../Token.js";
3
+ import type { EventSubscriber } from "./EventSubscriber.js";
4
+
5
+ /** The events that the handlers of a request publish, as they publish them: a controller that streams writes them in its response. */
6
+ export type RequestEvents = EventSubscriber<DomainEvent>;
7
+
8
+ export const RequestEvents = new Token<RequestEvents>();