@ingenioz-it/caits 0.5.1 → 0.7.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 (53) hide show
  1. package/bin/caits.js +17 -12
  2. package/bin/init.js +15 -6
  3. package/package.json +1 -1
  4. package/quality/architecture/adapters/marko.mjs +7 -4
  5. package/quality/architecture/adapters/scss.mjs +29 -8
  6. package/quality/architecture/adapters/typescript.mjs +2 -2
  7. package/quality/architecture/facts.mjs +158 -31
  8. package/quality/architecture/project.mjs +121 -27
  9. package/quality/architecture.mjs +68 -22
  10. package/quality/check.mjs +20 -14
  11. package/quality/dev.mjs +3 -2
  12. package/quality/mutation-suite.mjs +56 -5
  13. package/quality/mutation-summary.mjs +28 -11
  14. package/quality/mutation.mjs +5 -6
  15. package/quality/terminal.mjs +30 -0
  16. package/quality/test-docs/test-docs.mjs +1 -2
  17. package/src/DomainEvent.ts +4 -1
  18. package/src/di/Container.ts +5 -1
  19. package/src/di/createBackend.ts +60 -15
  20. package/src/di/createFrontend.ts +7 -4
  21. package/src/di/declareDependencies.js +25 -5
  22. package/src/di/discover.ts +52 -13
  23. package/src/di/discoverFrontends.ts +30 -11
  24. package/src/di/discovered.ts +3 -3
  25. package/src/di/discoveredFrontends.ts +3 -3
  26. package/src/di/frontend.ts +46 -4
  27. package/src/messaging/BackendEventBus.ts +6 -6
  28. package/src/messaging/ContextMap.ts +30 -7
  29. package/src/messaging/Controller.ts +2 -3
  30. package/src/messaging/EventPublisher.ts +2 -2
  31. package/src/messaging/FrontendContextMap.ts +33 -0
  32. package/src/messaging/InProcessMessageBus.ts +5 -7
  33. package/src/messaging/MessageBus.ts +1 -2
  34. package/src/state/createStore.ts +1 -2
  35. package/src/testing/backend/application.ts +1 -1
  36. package/src/testing/backend/applicationScenario.ts +6 -7
  37. package/src/testing/backend/applications.ts +7 -6
  38. package/src/testing/backend/context-map.ts +5 -2
  39. package/src/testing/backend/integration.ts +1 -1
  40. package/src/testing/frontend/applicationScenario.ts +2 -3
  41. package/src/testing/frontend/applications.ts +3 -3
  42. package/src/testing/frontend/context-map.ts +6 -0
  43. package/src/testing/frontend/frontends.ts +17 -11
  44. package/src/virtual-modules.d.ts +3 -3
  45. package/src/vite.js +18 -13
  46. package/template/caits/architecture.mjs +1040 -187
  47. package/template/caits/eslint.js +18 -0
  48. package/template/caits/imports.mjs +14 -0
  49. package/template/caits/tsconfig.json +1 -1
  50. package/template/caits/vite.ts +7 -3
  51. package/template/project/AGENTS +9 -5
  52. package/template/project/quality/imports.mjs +10 -0
  53. package/src/messaging/Input.ts +0 -4
@@ -1,3 +1,5 @@
1
+ import type { EventPublisher } from "../messaging/EventPublisher.js";
2
+ import { eventsFor, type Line } from "../messaging/FrontendContextMap.js";
1
3
  import { createFrontend } from "./createFrontend.js";
2
4
  import { discoveredFrontends } from "./discoveredFrontends.js";
3
5
  import type { Fetcher } from "./Fetcher.js";
@@ -6,7 +8,6 @@ import type { Fetcher } from "./Fetcher.js";
6
8
  * The frontends of the contexts, by name. Each context adds its own, next to its type:
7
9
  * `declare module "@ingenioz-it/caits/di/frontend" { interface Frontends { counter: CounterFrontend } }`.
8
10
  */
9
- // eslint-disable-next-line @typescript-eslint/no-empty-object-type -- the contexts fill it
10
11
  export interface Frontends {}
11
12
 
12
13
  const kept = Symbol("frontends");
@@ -17,17 +18,58 @@ export function frontendsOn($global: object): Map<string, unknown> {
17
18
  return ($global as Global)[kept] ??= new Map<string, unknown>();
18
19
  }
19
20
 
21
+ /** The context whose bus is the shared bus: the context maps share its events there, and hear them. */
22
+ const SHARED = "shared";
23
+
20
24
  /**
21
25
  * The frontend of a context, for the views of one $global: the server has one $global per request, the browser one per page.
22
26
  * It is built the first time a view asks. On the server, it reaches the API with the fetch of the request (Marko Run, in the
23
- * same process); in the browser, with the fetch of the browser.
27
+ * same process); in the browser, with the fetch of the browser. The bus of the context shared is the shared bus of the $global: the
28
+ * context map of a context shares there the events of shared that the events of its context give, and each of them goes to the
29
+ * contexts whose context map hears it.
24
30
  */
25
31
  export function frontendOf<Name extends keyof Frontends>($global: object, name: Name): Frontends[Name] {
32
+ return frontendNamed($global, name) as Frontends[Name];
33
+ }
34
+
35
+ /** The frontend of the context of this name on a $global, built the first time that something asks for it. */
36
+ function frontendNamed($global: object, name: string): unknown {
26
37
  const frontends = frontendsOn($global);
27
38
  if (!frontends.has(name)) {
28
39
  const context = discoveredFrontends[name];
29
40
  if (!context) throw new Error(`No frontend context is named ${name}`);
30
- frontends.set(name, createFrontend(context, ($global as Global).fetch ?? ((input, init) => fetch(input, init))));
41
+ const frontend = createFrontend(context, ($global as Global).fetch ?? ((input, init) => fetch(input, init)));
42
+ frontends.set(name, frontend);
43
+ if (name === SHARED) frontend.events.subscribe(event => { hear($global, event as object); });
44
+ frontend.events.subscribe(event => { tell($global, context.map, event as object); });
45
+ }
46
+ return frontends.get(name);
47
+ }
48
+
49
+ /** The lines of a map that hear the context shared, or those that share with it. */
50
+ const lines = (map: readonly Line[], shares: boolean) => map.filter(line => line.shares === shares);
51
+
52
+ /**
53
+ * An event of a context, on its bus: the events of shared that the lines of its map that share give go to the shared bus of the $global.
54
+ * The frontend of shared is built when a context first shares an event.
55
+ */
56
+ function tell($global: object, map: readonly Line[], event: object): void {
57
+ const told = eventsFor(lines(map, true), event);
58
+ if (told.length === 0) return;
59
+ const { events: shared } = frontendNamed($global, SHARED) as { events: EventPublisher<object> };
60
+ for (const sharedEvent of told) shared.publish(sharedEvent);
61
+ }
62
+
63
+ /**
64
+ * An event of the context shared, on the shared bus of a $global: each context whose context map hears it publishes the events that
65
+ * the lines that hear it give on its own bus, its frontend built if no view asked for it yet. A context that does not hear it is not
66
+ * built.
67
+ */
68
+ function hear($global: object, event: object): void {
69
+ for (const [name, context] of Object.entries(discoveredFrontends)) {
70
+ const events = eventsFor(lines(context.map, false), event);
71
+ if (events.length === 0) continue;
72
+ const { events: bus } = frontendNamed($global, name) as { events: EventPublisher<object> };
73
+ for (const own of events) bus.publish(own);
31
74
  }
32
- return frontends.get(name) as Frontends[Name];
33
75
  }
@@ -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 };
@@ -8,8 +7,9 @@ export type Published = { readonly event: DomainEvent; readonly envelope: Envelo
8
7
  const nextTurn = () => new Promise(resolve => { setTimeout(resolve, 0); });
9
8
 
10
9
  /**
11
- * Carries the events of the contexts. The context map turns each event into input DTOs, and each input goes to the context that
12
- * handles it. The publisher never waits for the reactions; every failure is reported.
10
+ * The shared bus: carries the events of shared that the context maps share. The context map of each context turns the events of shared
11
+ * that it hears into input DTOs of the context, and each input goes to it. The publisher never waits for the reactions; every failure is
12
+ * reported.
13
13
  */
14
14
  export class BackendEventBus {
15
15
  private inFlight = new Set<Promise<void>>();
@@ -17,7 +17,7 @@ export class BackendEventBus {
17
17
 
18
18
  constructor(private readonly options: {
19
19
  map: ContextMap;
20
- send: (input: Input<unknown>, envelope: Envelope) => Promise<unknown>;
20
+ send: (input: object, envelope: Envelope) => Promise<unknown>;
21
21
  nextEventId: () => string;
22
22
  reportError: ReportError;
23
23
  }) {}
@@ -34,7 +34,7 @@ export class BackendEventBus {
34
34
  void delivery.then(() => { this.inFlight.delete(delivery); });
35
35
  }
36
36
 
37
- /** Watches every event the contexts publish. */
37
+ /** Watches every event on the shared bus. */
38
38
  subscribe(listener: (published: Published) => void): () => void {
39
39
  this.listeners.add(listener);
40
40
  return () => { this.listeners.delete(listener); };
@@ -47,7 +47,7 @@ export class BackendEventBus {
47
47
 
48
48
  private async deliver(event: DomainEvent, envelope: Envelope): Promise<void> {
49
49
  await nextTurn();
50
- let inputs: Input<unknown>[];
50
+ let inputs: object[];
51
51
  try {
52
52
  inputs = this.options.map.inputsFor(event, envelope);
53
53
  } catch (error) {
@@ -1,22 +1,45 @@
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 };
6
5
 
7
6
  type EventClass<E> = abstract new (...args: never[]) => E;
8
7
 
9
- export type Translation = { readonly event: EventClass<DomainEvent>; readonly inputs: (event: never, envelope: Envelope) => Input<unknown>[] };
8
+ /**
9
+ * One line of the context map of a backend context (its context-map.ts): an event of the context shared that it hears, which becomes
10
+ * input DTOs of the context, or an event of the context that it shares, which becomes events of the context shared.
11
+ */
12
+ export type Translation =
13
+ | { readonly shares: false; readonly event: EventClass<DomainEvent>; readonly inputs: (event: never, envelope: Envelope) => object[] }
14
+ | { readonly shares: true; readonly event: EventClass<DomainEvent>; readonly events: (event: never) => DomainEvent[] };
10
15
 
11
- /** One line of the context map: an event of a context becomes input DTOs, for whichever contexts handle them. */
12
- export function when<E extends DomainEvent>(event: EventClass<E>, inputs: (event: E, envelope: Envelope) => Input<unknown>[]): Translation {
13
- return { event, inputs };
16
+ /**
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
18
+ * input DTOs. A context map names only its own context and the context shared: two contexts never name each other.
19
+ */
20
+ export function when<E extends DomainEvent>(event: EventClass<E>, inputs: (event: E, envelope: Envelope) => object[]): Translation {
21
+ return { shares: false, event, inputs };
22
+ }
23
+
24
+ /**
25
+ * A line that tells the other contexts: when a handler of the context publishes this event of its own, the context publishes these
26
+ * events of shared on the shared bus, and the context map of each context that hears them turns them into inputs of its own. The event
27
+ * of the context holds what the events of shared need.
28
+ */
29
+ export function share<E extends DomainEvent>(event: EventClass<E>, events: (event: E) => DomainEvent[]): Translation {
30
+ return { shares: true, event, events };
14
31
  }
15
32
 
16
33
  export class ContextMap {
17
34
  constructor(private readonly translations: Translation[]) {}
18
35
 
19
- inputsFor(event: DomainEvent, envelope: Envelope): Input<unknown>[] {
20
- return this.translations.filter(translation => event instanceof translation.event).flatMap(translation => translation.inputs(event as never, envelope));
36
+ /** The input DTOs that the lines that hear this event of shared give, none when no line hears it. */
37
+ inputsFor(event: DomainEvent, envelope: Envelope): object[] {
38
+ return this.translations.flatMap(translation => !translation.shares && event instanceof translation.event ? translation.inputs(event as never, envelope) : []);
39
+ }
40
+
41
+ /** The events of shared that the lines that share this event of the context give, none when no line shares it. */
42
+ eventsFor(event: DomainEvent): DomainEvent[] {
43
+ return this.translations.flatMap(translation => translation.shares && event instanceof translation.event ? translation.events(event as never) : []);
21
44
  }
22
45
  }
@@ -1,4 +1,3 @@
1
- import type { Input } from "./Input.js";
2
1
  import type { MessageBus } from "./MessageBus.js";
3
2
  import { reportToConsole, type ReportError } from "./ReportError.js";
4
3
 
@@ -68,7 +67,7 @@ export function problem(status: number, slug: string, title: string, detail?: st
68
67
  }
69
68
 
70
69
  /** 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> {
70
+ export abstract class Controller<Dto extends object, Output> {
72
71
  constructor(private bus: MessageBus, private reportError: ReportError = reportToConsole) {}
73
72
 
74
73
  protected abstract toDto(request: Request): Dto | Promise<Dto>;
@@ -92,7 +91,7 @@ export abstract class Controller<Dto extends Input<Output>, Output> {
92
91
  try {
93
92
  let output: Output;
94
93
  try {
95
- output = await this.bus.send(await this.toDto(request));
94
+ output = await this.bus.send(await this.toDto(request)) as Output;
96
95
  } catch (error) {
97
96
  return await this.toErrorResponse(error, request);
98
97
  }
@@ -2,8 +2,8 @@ import { Token } from "../Token.js";
2
2
 
3
3
  /**
4
4
  * Publishes the events of a context: the handlers get it, and the views of the frontend. It cannot subscribe, and it gives nothing back.
5
- * In the backend, the events go later to the other contexts, through the context map. In the frontend, they go at once to the store of
6
- * the same context.
5
+ * In the backend, the context map of the context shares them as events of shared, which go later to the other contexts. In the frontend,
6
+ * they go at once to the store of the same context, and its context map shares them.
7
7
  */
8
8
  export interface EventPublisher<E> {
9
9
  publish(event: E): void;
@@ -0,0 +1,33 @@
1
+ type EventClass<E> = abstract new (...args: never[]) => E;
2
+
3
+ /**
4
+ * One line of the context map of a frontend context: an event of the context shared that it hears, which becomes events of the context,
5
+ * or an event of the context that it shares, which becomes events of the context shared.
6
+ */
7
+ export type Line = { readonly shares: boolean; readonly event: EventClass<object>; readonly events: (event: never) => object[] };
8
+
9
+ /**
10
+ * A line that hears the context shared, in the context map of a frontend context (its context-map.ts): when this event of shared comes
11
+ * on the shared bus, the context publishes these events of its own on its own bus, and its store reads them. A context map names only
12
+ * its own context and the context shared: two contexts never name each other.
13
+ */
14
+ export function when<E extends object>(event: EventClass<E>, events: (event: E) => object[]): Line {
15
+ return { shares: false, event, events };
16
+ }
17
+
18
+ /**
19
+ * A line that tells the other contexts: when a view, a component or a handler of the context publishes this event of its own on its
20
+ * bus, the context publishes these events of shared on the shared bus, and the context map of each context that hears them turns them
21
+ * into events of its own. The event of the context holds what the events of shared need.
22
+ */
23
+ export function share<E extends object>(event: EventClass<E>, events: (event: E) => object[]): Line {
24
+ return { shares: true, event, events };
25
+ }
26
+
27
+ /**
28
+ * The events that a map gives for an event, line by line, none when no line reads it: for an event of shared that it hears, events of
29
+ * the context; for an event of the context that it shares, events of shared.
30
+ */
31
+ export function eventsFor(map: readonly Line[], event: object): object[] {
32
+ return map.filter(line => event instanceof line.event).flatMap(line => line.events(event as never));
33
+ }
@@ -1,12 +1,10 @@
1
- import type { Input } from "./Input.js";
2
1
  import type { MessageBus } 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
  }
@@ -22,14 +20,14 @@ export class DuplicateHandlerError extends Error {
22
20
  export class InProcessMessageBus implements MessageBus {
23
21
  private handlers = new Map<unknown, (input: never) => unknown>();
24
22
 
25
- subscribe<I extends Input<unknown>>(input: InputClass<I>, handler: (input: I) => OutputOf<I> | Promise<OutputOf<I>>): void {
23
+ subscribe<I extends object>(input: InputClass<I>, handler: (input: I) => unknown): void {
26
24
  if (this.handlers.has(input)) throw new DuplicateHandlerError(input.name);
27
25
  this.handlers.set(input, handler);
28
26
  }
29
27
 
30
- async send<Output>(input: Input<Output>): Promise<Output> {
28
+ async send(input: object): Promise<unknown> {
31
29
  const handler = this.handlers.get(input.constructor);
32
30
  if (!handler) throw new UnknownInputError(input.constructor.name);
33
- return (await handler(input as never)) as Output;
31
+ return await handler(input as never);
34
32
  }
35
33
  }
@@ -1,12 +1,11 @@
1
1
  import { Token } from "../Token.js";
2
- import type { Input } from "./Input.js";
3
2
 
4
3
  /**
5
4
  * 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
5
  * the handler.
7
6
  */
8
7
  export interface MessageBus {
9
- send<Output>(input: Input<Output>): Promise<Output>;
8
+ send(input: object): Promise<unknown>;
10
9
  }
11
10
 
12
11
  export const MessageBus = new Token<MessageBus>();
@@ -1,5 +1,4 @@
1
1
  import type { EventSubscriber } from "../messaging/EventSubscriber.js";
2
- import type { Input } from "../messaging/Input.js";
3
2
  import type { MessageBus } from "../messaging/MessageBus.js";
4
3
  import { reportToConsole, type ReportError } from "../messaging/ReportError.js";
5
4
 
@@ -9,7 +8,7 @@ export interface StoreDefinition<S, E> {
9
8
  /** The state after the event. */
10
9
  reduce(state: S, event: E): S;
11
10
  /** The input DTO that the event asks the store to send to the message bus, or undefined. It reads the state before the event. */
12
- send(state: S, event: E): Input<unknown> | undefined;
11
+ send(state: S, event: E): object | undefined;
13
12
  }
14
13
 
15
14
  export interface Store<S> {
@@ -2,7 +2,7 @@ import { test as base } from "vitest";
2
2
  import { createTestApplication, type TestApplication } from "./applications.js";
3
3
 
4
4
  /** Everything an application test of the backend imports: the test of a use case, written as a scenario. */
5
- export { afterAll, afterEach, beforeAll, beforeEach, describe, expect, vi } from "vitest";
5
+ export { afterAll, afterEach, assert, beforeAll, beforeEach, describe, expect, vi } from "vitest";
6
6
  export { applicationScenario } from "./applicationScenario.js";
7
7
  export type { TestApplication } from "./applications.js";
8
8
 
@@ -1,19 +1,18 @@
1
1
  import { expect } from "vitest";
2
2
  import type { DomainEvent } from "../../DomainEvent.js";
3
- import type { Input } from "../../messaging/Input.js";
4
3
  import type { TestApplication } from "./applications.js";
5
4
 
6
- type Result<Output> = { output: Output } | { error: unknown };
5
+ type Result = { output: unknown } | { error: unknown };
7
6
 
8
7
  /**
9
8
  * An application test, written as a presentation test: the input DTO, the events the handler publishes, then the output DTO or the error
10
9
  * that the message bus gives. The events are checked only when the test says which ones it expects.
11
10
  */
12
- export class ApplicationScenario<Output> {
11
+ export class ApplicationScenario {
13
12
  private readonly expected: DomainEvent[] = [];
14
13
  private checksEvents = false;
15
14
 
16
- constructor(private readonly app: TestApplication, private readonly dto: Input<Output>) {}
15
+ constructor(private readonly app: TestApplication, private readonly dto: object) {}
17
16
 
18
17
  expectItToPublish(event: DomainEvent): this {
19
18
  this.expected.push(event);
@@ -26,7 +25,7 @@ export class ApplicationScenario<Output> {
26
25
  return this;
27
26
  }
28
27
 
29
- async expectTheOutputToBe(output: Output): Promise<void> {
28
+ async expectTheOutputToBe(output: object): Promise<void> {
30
29
  const from = this.app.published.length;
31
30
  const result = await this.run();
32
31
  if ("error" in result) throw result.error;
@@ -42,7 +41,7 @@ export class ApplicationScenario<Output> {
42
41
  expect(result.error).toStrictEqual(error);
43
42
  }
44
43
 
45
- private async run(): Promise<Result<Output>> {
44
+ private async run(): Promise<Result> {
46
45
  try {
47
46
  return { output: await this.app.send(this.dto) };
48
47
  } catch (error) {
@@ -56,5 +55,5 @@ export class ApplicationScenario<Output> {
56
55
  }
57
56
 
58
57
  export const applicationScenario = (app: TestApplication) => ({
59
- givenTheDto: <Output>(dto: Input<Output>) => new ApplicationScenario(app, dto)
58
+ givenTheDto: (dto: object) => new ApplicationScenario(app, dto)
60
59
  });
@@ -14,7 +14,7 @@ const testBindings: Record<string, readonly Binding[]> = Object.fromEntries(
14
14
 
15
15
  type EventType<E extends DomainEvent> = abstract new (...args: never[]) => E;
16
16
 
17
- /** The whole backend, built from the bindings of its contexts with their test.di.ts over them, and predictable ids. For the tests of the context map. */
17
+ /** The whole backend, built from the bindings of its contexts with their test.di.ts over them, and predictable ids. For the tests of the context maps. */
18
18
  export function createTestBackendApplication() {
19
19
  const reportedErrors: unknown[] = [];
20
20
  const published: Published[] = [];
@@ -29,7 +29,7 @@ export function createTestBackendApplication() {
29
29
  return {
30
30
  ...backend,
31
31
  reportedErrors,
32
- /** Every event the contexts published, in its envelope, in the order they were published. */
32
+ /** Every event of shared that the context maps shared, in its envelope, in the order they were published. */
33
33
  published,
34
34
  publishedNames: () => published.map(({ event }) => event.constructor.name),
35
35
  publishedOf: <E extends DomainEvent>(type: EventType<E>): E[] => published.flatMap(({ event }) => event instanceof type ? [event] : [])
@@ -37,13 +37,14 @@ export function createTestBackendApplication() {
37
37
  }
38
38
 
39
39
  /**
40
- * Every context, built from its di.ts with its test.di.ts over it, but alone: no event reaches another context. A test sends input DTOs
41
- * and reads the events the handlers published.
40
+ * Every context, built from its di.ts with its test.di.ts over it, but alone: no context map hears or shares an event. A test sends
41
+ * input DTOs and reads the events of their context that the handlers published.
42
42
  */
43
43
  export function createTestApplication() {
44
44
  const published: DomainEvent[] = [];
45
- const backend = createBackend({ ...discovered, map: [] }, { replace: testBindings, reportError: () => undefined });
46
- backend.subscribe(({ event }) => { published.push(event); });
45
+ const alone = Object.fromEntries(Object.entries(discovered.contexts).map(([name, context]) => [name, { ...context, map: [] }]));
46
+ const backend = createBackend({ ...discovered, contexts: alone }, { replace: testBindings, reportError: () => undefined });
47
+ backend.subscribeToContexts(event => { published.push(event); });
47
48
 
48
49
  return {
49
50
  /** Sends an input DTO to the message bus of the context that handles it. */
@@ -1,8 +1,11 @@
1
1
  import { test as base } from "vitest";
2
2
  import { createTestBackendApplication, type TestBackendApplication } from "./applications.js";
3
3
 
4
- /** Everything a test of the context map imports. `test` checks the Input DTOs that the `ContextMap` gives for an Event. */
5
- export { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
4
+ /**
5
+ * Everything a test of the context map imports. `test` checks what the `ContextMap` gives: the input DTOs for an event of shared
6
+ * (`inputsFor`), the events of shared for an event of the context (`eventsFor`).
7
+ */
8
+ export { afterAll, afterEach, assert, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
6
9
  export { ContextMap } from "../../messaging/ContextMap.js";
7
10
  export type { TestBackendApplication } from "./applications.js";
8
11
 
@@ -2,7 +2,7 @@ import { createBackend } from "../../di/createBackend.js";
2
2
  import { discovered } from "../../di/discovered.js";
3
3
 
4
4
  /** Everything an integration test imports: flows of the context map again, with the real infra implementations. */
5
- export { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
5
+ export { afterAll, afterEach, assert, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
6
6
 
7
7
  /** The whole backend, built from the di.ts of the contexts only: the real infra implementations, as in production. */
8
8
  export const productionBackend = () => createBackend(discovered);
@@ -1,5 +1,4 @@
1
1
  import { expect } from "vitest";
2
- import type { Input } from "../../messaging/Input.js";
3
2
  import type { TestApplication } from "./applications.js";
4
3
 
5
4
  /**
@@ -10,7 +9,7 @@ import type { TestApplication } from "./applications.js";
10
9
  export class ApplicationScenario implements PromiseLike<void> {
11
10
  private readonly expected: object[] = [];
12
11
 
13
- constructor(private readonly app: TestApplication, private readonly dto: Input<void>) {}
12
+ constructor(private readonly app: TestApplication, private readonly dto: object) {}
14
13
 
15
14
  expectItToPublish(event: object): this {
16
15
  this.expected.push(event);
@@ -32,5 +31,5 @@ export class ApplicationScenario implements PromiseLike<void> {
32
31
  }
33
32
 
34
33
  export const applicationScenario = (app: TestApplication) => ({
35
- givenTheDto: (dto: Input<void>) => new ApplicationScenario(app, dto)
34
+ givenTheDto: (dto: object) => new ApplicationScenario(app, dto)
36
35
  });
@@ -4,7 +4,7 @@ import type { Binding } from "../../di/Container.js";
4
4
  import { createFrontendWithBus } from "../../di/createFrontend.js";
5
5
  import { discoveredFrontends } from "../../di/discoveredFrontends.js";
6
6
  import type { Fetcher } from "../../di/Fetcher.js";
7
- import type { Input } from "../../messaging/Input.js";
7
+ import { UnknownInputError } from "../../messaging/InProcessMessageBus.js";
8
8
  import type { Token } from "../../Token.js";
9
9
 
10
10
  /** The test.di.ts of each context that has one: the bindings that replace, in tests, those that reach outside the browser. */
@@ -37,7 +37,7 @@ export function createTestApplication() {
37
37
 
38
38
  const ownerOf = (input: object): string => {
39
39
  const name = Object.keys(discoveredFrontends).find(context => discoveredFrontends[context].useCases.some(({ input: type }) => type === input.constructor));
40
- if (name === undefined) throw new Error(`No context handles ${input.constructor.name}`);
40
+ if (name === undefined) throw new UnknownInputError(input.constructor.name);
41
41
  return name;
42
42
  };
43
43
 
@@ -50,7 +50,7 @@ export function createTestApplication() {
50
50
 
51
51
  return {
52
52
  /** Sends an input DTO to the message bus of the context that handles it. */
53
- send: <Output>(input: Input<Output>): Promise<Output> => frontend(ownerOf(input)).bus.send(input),
53
+ send: (input: object): Promise<unknown> => frontend(ownerOf(input)).bus.send(input),
54
54
  /** The dependency with this token, built now if no handler needed it before. */
55
55
  get: <T>(token: Token<T>): T => {
56
56
  const found = holders(token);
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Everything a test of the context map of a frontend context imports: `eventsFor` gives the events of the context that an event of the
3
+ * context shared gives, and the events of shared that an event of the context gives.
4
+ */
5
+ export { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
6
+ export { eventsFor } from "../../messaging/FrontendContextMap.js";
@@ -1,23 +1,29 @@
1
1
  import { FrontendEventBus } from "../../messaging/FrontendEventBus.js";
2
- import type { Input } from "../../messaging/Input.js";
3
2
  import { createStore, type StoreDefinition } from "../../state/createStore.js";
4
- import type { Frontend } from "../../state/request.js";
3
+ import type { EventPublisher } from "../../messaging/EventPublisher.js";
5
4
  import { discoveredFrontends } from "../../di/discoveredFrontends.js";
6
5
  import { frontendsOn, type Frontends } from "../../di/frontend.js";
7
6
 
8
- type StateOf<F> = F extends Frontend<infer S, unknown> ? S : never;
9
- type EventOf<F> = F extends Frontend<unknown, infer E> ? E : never;
7
+ type EventOf<F> = F extends { events: EventPublisher<infer E> } ? E : never;
8
+ type StoreOf<F> = F extends { store: infer S } ? S : undefined;
9
+
10
+ /** The presentation of a context in a test: its frontend, the DTOs that its store sent, and the events that it published. */
11
+ export type TestPresentation<F> = { events: EventPublisher<EventOf<F>>; store: StoreOf<F>; sent: object[]; published: EventOf<F>[] };
10
12
 
11
13
  /**
12
- * The presentation of a context, as its views get it: its EventPublisher and its store. The message bus of the store is a Fake: it
13
- * records the DTOs it receives, and answers nothing. A test publishes the events of the handlers itself.
14
+ * The presentation of a context, as its views and its components get it: its EventPublisher and its store (none, for a context
15
+ * without a store). The message bus of the store is a Fake: it records the DTOs it receives in `sent`, and answers nothing. A test
16
+ * publishes the events of the handlers itself. `published` records every event of the bus, in their order: those that the views and
17
+ * the components publish. The context map is left aside: its own test reads what it shares.
14
18
  */
15
- export function testPresentation<Name extends keyof Frontends>(name: Name): Frontend<StateOf<Frontends[Name]>, EventOf<Frontends[Name]>> & { sent: Input<unknown>[] } {
19
+ export function testPresentation<Name extends keyof Frontends>(name: Name): TestPresentation<Frontends[Name]> {
16
20
  const events = new FrontendEventBus<EventOf<Frontends[Name]>>();
17
- const sent: Input<unknown>[] = [];
18
- const definition = discoveredFrontends[name].store as StoreDefinition<StateOf<Frontends[Name]>, EventOf<Frontends[Name]>>;
19
- const store = createStore(definition, { events, bus: { send: <Output>(input: Input<Output>) => { sent.push(input); return Promise.resolve(undefined as Output); } } });
20
- return { events, store, sent };
21
+ const sent: object[] = [];
22
+ const published: EventOf<Frontends[Name]>[] = [];
23
+ events.subscribe(event => { published.push(event); });
24
+ const definition = discoveredFrontends[name].store as StoreDefinition<unknown, EventOf<Frontends[Name]>> | undefined;
25
+ const store = definition && createStore(definition, { events, bus: { send: (input: object) => { sent.push(input); return Promise.resolve(undefined); } } });
26
+ return { events, store, sent, published } as unknown as TestPresentation<Frontends[Name]>;
21
27
  }
22
28
 
23
29
  /** A $global as Marko gives it to the views: the address of the page, and frontends already built. */
@@ -4,10 +4,9 @@ declare module "virtual:kernel/backend" {
4
4
  import type { Translation } from "@ingenioz-it/caits/messaging/ContextMap";
5
5
  type Modules = Record<string, Record<string, unknown>>;
6
6
  export const bindings: Record<string, readonly Binding[]>;
7
- export const dtos: Modules;
8
7
  export const handlers: Modules;
9
8
  export const controllers: Modules;
10
- export const map: Record<string, Translation[]>;
9
+ export const maps: Record<string, Translation[]>;
11
10
  }
12
11
 
13
12
  declare module "virtual:kernel/backend-test" {
@@ -17,12 +16,13 @@ declare module "virtual:kernel/backend-test" {
17
16
 
18
17
  declare module "virtual:kernel/frontend" {
19
18
  import type { Binding } from "@ingenioz-it/caits/di/Container";
19
+ import type { Line } from "@ingenioz-it/caits/messaging/FrontendContextMap";
20
20
  import type { StoreDefinition } from "@ingenioz-it/caits/state/createStore";
21
21
  type Modules = Record<string, Record<string, unknown>>;
22
22
  export const bindings: Record<string, readonly Binding[]>;
23
- export const dtos: Modules;
24
23
  export const handlers: Modules;
25
24
  export const stores: Record<string, StoreDefinition<unknown, unknown>>;
25
+ export const maps: Record<string, readonly Line[]>;
26
26
  }
27
27
 
28
28
  declare module "virtual:kernel/frontend-test" {
package/src/vite.js CHANGED
@@ -58,17 +58,16 @@ function dependencies(source) {
58
58
  const CONTEXT_MODULES = {
59
59
  "virtual:kernel/backend": [
60
60
  ["bindings", "backend/*/di.ts", true],
61
- ["dtos", "backend/*/application/*/*/*.dto.ts"],
62
- ["handlers", "backend/*/application/*/*/*.handler.ts"],
63
- ["controllers", "backend/*/presentation/*/*.controller.ts"],
64
- ["map", "backend/context-map.ts", true]
61
+ ["handlers", "backend/*/{command,query}/*/*.handler.ts"],
62
+ ["controllers", "backend/*/{command,query}/*/*.controller.ts"],
63
+ ["maps", "backend/*/context-map.ts", true]
65
64
  ],
66
65
  "virtual:kernel/backend-test": [["bindings", "backend/*/test.di.ts", true]],
67
66
  "virtual:kernel/frontend": [
68
67
  ["bindings", "frontend/*/di.ts", true],
69
- ["dtos", "frontend/*/application/*/*/*.dto.ts"],
70
- ["handlers", "frontend/*/application/*/*/*.handler.ts"],
71
- ["stores", "frontend/*/presentation/store/*Store.ts", true]
68
+ ["handlers", "frontend/*/{command,query}/*/*.handler.ts"],
69
+ ["stores", "frontend/*/presentation/store/*Store.ts", true],
70
+ ["maps", "frontend/*/context-map.ts", true]
72
71
  ],
73
72
  "virtual:kernel/frontend-test": [["bindings", "frontend/*/test.di.ts", true]]
74
73
  };
@@ -86,9 +85,9 @@ function contexts(source) {
86
85
  /** @type {(pattern: string, onlyDefault?: boolean) => string} */
87
86
  const glob = (pattern, onlyDefault = false) =>
88
87
  `import.meta.glob(${JSON.stringify(`${source}/${pattern}`)}, { eager: true${onlyDefault ? `, import: "default"` : ""} })`;
89
- /** A pattern as a regular expression: `*` is a part of a name, without "/". */
90
- const matcherOf = (/** @type {string} */ pattern) =>
91
- new RegExp(`^${`${source}/${pattern}`.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replaceAll("*", "[^/]*")}$`);
88
+ /** A pattern as a regular expression: `*` is a part of a name, without "/", and `{command,query}` one of these names. */
89
+ const matcherOf = (/** @type {string} */ pattern) => new RegExp(`^${`${source}/${pattern}`.replace(/[.+?^$()|[\]\\]/g, "\\$&").replaceAll("*", "[^/]*")
90
+ .replace(/\{([^}]*)\}/g, (/** @type {string} */ _, /** @type {string} */ names) => `(?:${names.split(",").join("|")})`)}$`);
92
91
  const modules = Object.entries(CONTEXT_MODULES).map(([id, exports]) => ({
93
92
  id,
94
93
  code: exports.map(([name, pattern, onlyDefault]) => `export const ${name} = ${glob(pattern, onlyDefault)};`).join("\n"),
@@ -102,9 +101,15 @@ function contexts(source) {
102
101
  // Vite processes the package as the code of the project, also when it is installed: it holds TypeScript and imports these modules.
103
102
  ssr: { noExternal: [KERNEL] },
104
103
  optimizeDeps: { exclude: [KERNEL] },
105
- // The shortcut #shared leads to the backend context shared from every file (the tsconfig.json of the project says it to TypeScript).
106
- // Vite reads an alias before the "imports" of the package.json of a context (#context), which cannot lead out of the context.
107
- resolve: { alias: [{ find: /^#shared\//, replacement: `${source}/backend/shared/` }] }
104
+ // The shortcut #shared leads to the backend context shared from every file, and #frontend-shared to the frontend context shared
105
+ // (the tsconfig.json of the project says it to TypeScript). Vite reads an alias before the "imports" of the package.json of a
106
+ // context (#context), which cannot lead out of the context.
107
+ resolve: {
108
+ alias: [
109
+ { find: /^#shared\//, replacement: `${source}/backend/shared/` },
110
+ { find: /^#frontend-shared\//, replacement: `${source}/frontend/shared/` }
111
+ ]
112
+ }
108
113
  }),
109
114
  configResolved: config => { root = config.root; },
110
115
  configureServer: server => {