@muspellheim/shared 0.11.0 → 0.12.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/shared.d.ts CHANGED
@@ -125,6 +125,11 @@ export declare class ConfigurableResponses<T = unknown> {
125
125
  next(): T;
126
126
  }
127
127
 
128
+ export declare interface ConsoleMessage {
129
+ level: "log" | "error" | "warn" | "info" | "debug" | "trace";
130
+ message: unknown[];
131
+ }
132
+
128
133
  /**
129
134
  * A stub for the console interface.
130
135
  *
@@ -140,7 +145,7 @@ export declare class ConsoleStub extends EventTarget {
140
145
  /**
141
146
  * Track the console messages.
142
147
  */
143
- trackMessages(): OutputTracker<unknown>;
148
+ trackMessages(): OutputTracker<ConsoleMessage>;
144
149
  }
145
150
 
146
151
  /**
@@ -153,6 +158,51 @@ export declare class ConsoleStub extends EventTarget {
153
158
  */
154
159
  export declare function createFetchStub(responses?: ResponseData | Error | (ResponseData | Error)[]): () => Promise<Response>;
155
160
 
161
+ /**
162
+ * Track events from an event target.
163
+ *
164
+ * Wait asynchronously for events. Useful in test code.
165
+ */
166
+ export declare class EventTracker<T extends Event> {
167
+ #private;
168
+ /**
169
+ * Create a tracker for a specific event of an event target.
170
+ *
171
+ * @param eventTarget The target to track.
172
+ * @param event The event name to track.
173
+ */
174
+ static create<T extends Event>(eventTarget: EventTarget, event: string): EventTracker<T>;
175
+ /**
176
+ * Create a tracker for a specific event of an event target.
177
+ *
178
+ * @param eventTarget The target to track.
179
+ * @param event The event name to track.
180
+ */
181
+ constructor(eventTarget: EventTarget, event: string);
182
+ /**
183
+ * Return the tracked events.
184
+ *
185
+ * @return The tracked events.
186
+ */
187
+ get events(): T[];
188
+ /**
189
+ * Clear the tracked events and return the cleared events.
190
+ *
191
+ * @return The cleared events.
192
+ */
193
+ clear(): T[];
194
+ /**
195
+ * Stop tracking.
196
+ */
197
+ stop(): void;
198
+ /**
199
+ * Wait asynchronously for a number of events.
200
+ *
201
+ * @param count number of events, default 1.
202
+ */
203
+ waitFor(count?: number): Promise<T[]>;
204
+ }
205
+
156
206
  /**
157
207
  * A failed status.
158
208
  */
package/dist/shared.js CHANGED
@@ -47,7 +47,82 @@ class c {
47
47
  return this.date().getTime();
48
48
  }
49
49
  }
50
- class d {
50
+ class p {
51
+ /**
52
+ * Create a tracker for a specific event of an event target.
53
+ *
54
+ * @param eventTarget The target to track.
55
+ * @param event The event name to track.
56
+ */
57
+ static create(t, e) {
58
+ return new p(t, e);
59
+ }
60
+ #e;
61
+ #t;
62
+ #s;
63
+ #r;
64
+ /**
65
+ * Create a tracker for a specific event of an event target.
66
+ *
67
+ * @param eventTarget The target to track.
68
+ * @param event The event name to track.
69
+ */
70
+ constructor(t, e) {
71
+ this.#e = t, this.#t = e, this.#s = [], this.#r = (s) => this.#s.push(s), this.#e.addEventListener(this.#t, this.#r);
72
+ }
73
+ /**
74
+ * Return the tracked events.
75
+ *
76
+ * @return The tracked events.
77
+ */
78
+ get events() {
79
+ return this.#s;
80
+ }
81
+ /**
82
+ * Clear the tracked events and return the cleared events.
83
+ *
84
+ * @return The cleared events.
85
+ */
86
+ clear() {
87
+ const t = [...this.#s];
88
+ return this.#s.length = 0, t;
89
+ }
90
+ /**
91
+ * Stop tracking.
92
+ */
93
+ stop() {
94
+ this.#e.removeEventListener(this.#t, this.#r);
95
+ }
96
+ /**
97
+ * Wait asynchronously for a number of events.
98
+ *
99
+ * @param count number of events, default 1.
100
+ */
101
+ async waitFor(t = 1) {
102
+ return new Promise((e) => {
103
+ const s = () => {
104
+ this.#s.length >= t && (this.#e.removeEventListener(this.#t, s), e(this.events));
105
+ };
106
+ this.#e.addEventListener(this.#t, s), s();
107
+ });
108
+ }
109
+ }
110
+ class f {
111
+ isSuccess = !0;
112
+ }
113
+ class S {
114
+ isSuccess = !1;
115
+ errorMessage;
116
+ /**
117
+ * Creates a failed status.
118
+ *
119
+ * @param errorMessage
120
+ */
121
+ constructor(t) {
122
+ this.errorMessage = t;
123
+ }
124
+ }
125
+ class u {
51
126
  /**
52
127
  * Create a list of responses (by providing an array), or a single repeating
53
128
  * response (by providing any other type). 'Name' is optional and used in
@@ -57,7 +132,7 @@ class d {
57
132
  * @param name An optional name for the responses.
58
133
  */
59
134
  static create(t, e) {
60
- return new d(t, e);
135
+ return new u(t, e);
61
136
  }
62
137
  /**
63
138
  * Convert all properties in an object into ConfigurableResponse instances.
@@ -70,7 +145,7 @@ class d {
70
145
  static mapObject(t, e) {
71
146
  const r = Object.entries(t).map(([h, n]) => {
72
147
  const l = e === void 0 ? void 0 : `${e}: ${h}`;
73
- return [h, d.create(n, l)];
148
+ return [h, u.create(n, l)];
74
149
  });
75
150
  return Object.fromEntries(r);
76
151
  }
@@ -100,7 +175,7 @@ class d {
100
175
  return t;
101
176
  }
102
177
  }
103
- class u {
178
+ class d {
104
179
  /**
105
180
  * Create a tracker for a specific event of an event target.
106
181
  *
@@ -108,7 +183,7 @@ class u {
108
183
  * @param event The event name to track.
109
184
  */
110
185
  static create(t, e) {
111
- return new u(t, e);
186
+ return new d(t, e);
112
187
  }
113
188
  #e;
114
189
  #t;
@@ -147,23 +222,8 @@ class u {
147
222
  this.#e.removeEventListener(this.#t, this.#r);
148
223
  }
149
224
  }
150
- class m {
151
- isSuccess = !0;
152
- }
153
- class f {
154
- isSuccess = !1;
155
- errorMessage;
156
- /**
157
- * Creates a failed status.
158
- *
159
- * @param errorMessage
160
- */
161
- constructor(t) {
162
- this.errorMessage = t;
163
- }
164
- }
165
225
  const i = "message";
166
- class S extends EventTarget {
226
+ class b extends EventTarget {
167
227
  log(...t) {
168
228
  this.dispatchEvent(
169
229
  new CustomEvent(i, {
@@ -210,19 +270,19 @@ class S extends EventTarget {
210
270
  * Track the console messages.
211
271
  */
212
272
  trackMessages() {
213
- return new u(this, i);
273
+ return new d(this, i);
214
274
  }
215
275
  }
216
- function b(a) {
217
- const t = d.create(a);
276
+ function N(a) {
277
+ const t = u.create(a);
218
278
  return async function() {
219
279
  const e = t.next();
220
280
  if (e instanceof Error)
221
281
  throw e;
222
- return new p(e);
282
+ return new g(e);
223
283
  };
224
284
  }
225
- class p {
285
+ class g {
226
286
  #e;
227
287
  #t;
228
288
  #s;
@@ -371,7 +431,7 @@ class o extends EventTarget {
371
431
  this.readyState = o.CLOSED;
372
432
  }
373
433
  }
374
- const g = "heartbeat", w = "message-sent";
434
+ const y = "heartbeat", w = "message-sent";
375
435
  class v extends EventTarget {
376
436
  /**
377
437
  * Create a WebSocket client.
@@ -395,7 +455,7 @@ class v extends EventTarget {
395
455
  return new v(
396
456
  t,
397
457
  e,
398
- y
458
+ m
399
459
  );
400
460
  }
401
461
  #e;
@@ -421,7 +481,7 @@ class v extends EventTarget {
421
481
  }
422
482
  try {
423
483
  this.#r = new this.#s(t), this.#r.addEventListener("open", (r) => {
424
- this.#u(r), e();
484
+ this.#d(r), e();
425
485
  }), this.#r.addEventListener(
426
486
  "message",
427
487
  (r) => this.#a(r)
@@ -444,7 +504,7 @@ class v extends EventTarget {
444
504
  * @return A new output tracker.
445
505
  */
446
506
  trackMessageSent() {
447
- return u.create(this, w);
507
+ return d.create(this, w);
448
508
  }
449
509
  /**
450
510
  * Close the connection.
@@ -475,7 +535,7 @@ class v extends EventTarget {
475
535
  * Simulate a heartbeat.
476
536
  */
477
537
  simulateHeartbeat() {
478
- this.#d();
538
+ this.#u();
479
539
  }
480
540
  /**
481
541
  * Simulate a close event.
@@ -492,7 +552,7 @@ class v extends EventTarget {
492
552
  simulateError() {
493
553
  this.#r?.close(), this.#o(new Event("error"));
494
554
  }
495
- #u(t) {
555
+ #d(t) {
496
556
  this.dispatchEvent(new Event(t.type, t)), this.#E();
497
557
  }
498
558
  #a(t) {
@@ -517,18 +577,18 @@ class v extends EventTarget {
517
577
  }
518
578
  #E() {
519
579
  this.#e <= 0 || (this.#n = setInterval(
520
- () => this.#d(),
580
+ () => this.#u(),
521
581
  this.#e
522
582
  ));
523
583
  }
524
584
  #v() {
525
585
  clearInterval(this.#n), this.#n = void 0;
526
586
  }
527
- #d() {
528
- this.#n != null && this.send(g);
587
+ #u() {
588
+ this.#n != null && this.send(y);
529
589
  }
530
590
  }
531
- class y extends EventTarget {
591
+ class m extends EventTarget {
532
592
  url;
533
593
  readyState = WebSocket.CONNECTING;
534
594
  constructor(t) {
@@ -544,14 +604,15 @@ class y extends EventTarget {
544
604
  }
545
605
  export {
546
606
  c as Clock,
547
- d as ConfigurableResponses,
548
- S as ConsoleStub,
549
- f as Failure,
550
- g as HEARTBEAT_TYPE,
551
- u as OutputTracker,
607
+ u as ConfigurableResponses,
608
+ b as ConsoleStub,
609
+ p as EventTracker,
610
+ S as Failure,
611
+ y as HEARTBEAT_TYPE,
612
+ d as OutputTracker,
552
613
  E as SseClient,
553
- m as Success,
614
+ f as Success,
554
615
  v as WebSocketClient,
555
- b as createFetchStub
616
+ N as createFetchStub
556
617
  };
557
618
  //# sourceMappingURL=shared.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"shared.js","sources":["../src/common/clock.ts","../src/common/configurable_responses.ts","../src/common/output_tracker.ts","../src/domain/messages.ts","../src/infrastructure/console_stub.ts","../src/infrastructure/fetch_stub.ts","../src/infrastructure/sse_client.ts","../src/infrastructure/web_socket_client.ts"],"sourcesContent":["// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.\n\n/**\n * A clock provides access to the current timestamp.\n */\nexport class Clock {\n /**\n * Create a clock using system the clock.\n *\n * @return A clock that uses the system clock.\n */\n static system(): Clock {\n return new Clock();\n }\n\n /**\n * Create a clock using a fixed date.\n *\n * @param date The fixed date of the clock.\n * @return A clock that always returns a fixed date.\n */\n static fixed(date: Date | string | number): Clock {\n return new Clock(new Date(date));\n }\n\n /**\n * Create a clock that returns a fixed offset from the given clock.\n *\n * @param clock The clock to offset from.\n * @param offsetMillis The offset in milliseconds.\n * @return A clock that returns a fixed offset from the given clock.\n */\n static offset(clock: Clock, offsetMillis: number): Clock {\n return new Clock(new Date(clock.millis() + offsetMillis));\n }\n\n readonly #date?: Date;\n\n private constructor(date?: Date) {\n this.#date = date;\n }\n\n /**\n * Return the current timestamp of the clock.\n *\n * @return The current timestamp.\n */\n date(): Date {\n return this.#date ? new Date(this.#date) : new Date();\n }\n\n /**\n * Return the current timestamp of the clock in milliseconds.\n *\n * @return The current timestamp in milliseconds.\n */\n millis(): number {\n return this.date().getTime();\n }\n}\n","// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.\n// Copyright 2023 Titanium I.T. LLC. MIT License.\n\n/**\n * Handle returning pre-configured responses.\n *\n * This is one of the nullability patterns from James Shore's article on\n * [testing without mocks](https://www.jamesshore.com/v2/projects/nullables/testing-without-mocks#configurable-responses).\n *\n * Example usage for stubbing `fetch` function:\n *\n * ```javascript\n * function createFetchStub(responses) {\n * const configurableResponses = ConfigurableResponses.create(responses);\n * return async function () {\n * const response = configurableResponses.next();\n * return {\n * status: response.status,\n * json: async () => response.body,\n * };\n * };\n * }\n * ```\n */\nexport class ConfigurableResponses<T = unknown> {\n /**\n * Create a list of responses (by providing an array), or a single repeating\n * response (by providing any other type). 'Name' is optional and used in\n * error messages.\n *\n * @param responses A single response or an array of responses.\n * @param name An optional name for the responses.\n */\n static create<T>(responses?: T | T[], name?: string) {\n return new ConfigurableResponses<T>(responses, name);\n }\n\n /**\n * Convert all properties in an object into ConfigurableResponse instances.\n * For example, { a: 1 } becomes { a: ConfigurableResponses.create(1) }.\n * 'Name' is optional and used in error messages.\n *\n * @param responseObject An object with single response or an array of responses.\n * @param name An optional name for the responses.\n */\n static mapObject<T extends Record<string, unknown>>(\n responseObject: T,\n name?: string,\n ) {\n const entries = Object.entries(responseObject);\n const translatedEntries = entries.map(([key, value]) => {\n const translatedName = name === undefined ? undefined : `${name}: ${key}`;\n return [key, ConfigurableResponses.create(value, translatedName)];\n });\n return Object.fromEntries(translatedEntries);\n }\n\n readonly #description;\n readonly #responses;\n\n /**\n * Create a list of responses (by providing an array), or a single repeating\n * response (by providing any other type). 'Name' is optional and used in\n * error messages.\n *\n * @param responses A single response or an array of responses.\n * @param name An optional name for the responses.\n */\n constructor(responses?: T | T[], name?: string) {\n this.#description = name == null ? \"\" : ` in ${name}`;\n this.#responses = Array.isArray(responses) ? [...responses] : responses;\n }\n\n /**\n * Get the next configured response. Throws an error when configured with a list\n * of responses and no more responses remain.\n *\n * @return The next response.\n */\n next(): T {\n const response = Array.isArray(this.#responses)\n ? this.#responses.shift()\n : this.#responses;\n if (response === undefined) {\n throw new Error(`No more responses configured${this.#description}.`);\n }\n\n return response;\n }\n}\n","// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.\n// Copyright 2020-2022 Titanium I.T. LLC. MIT License.\n\n/**\n * Track output events.\n *\n * This is one of the nullability patterns from James Shore's article on\n * [testing without mocks](https://www.jamesshore.com/v2/projects/nullables/testing-without-mocks#output-tracking).\n *\n * Example implementation of an event store:\n *\n * ```javascript\n * async record(event) {\n * // ...\n * this.dispatchEvent(new CustomEvent(\"eventRecorded\", { detail: event }));\n * }\n *\n * trackEventsRecorded() {\n * return new OutputTracker(this, \"eventRecorded\");\n * }\n * ```\n *\n * Example usage:\n *\n * ```javascript\n * const eventsRecorded = eventStore.trackEventsRecorded();\n * // ...\n * const data = eventsRecorded.data(); // [event1, event2, ...]\n * ```\n */\nexport class OutputTracker<T = unknown> {\n /**\n * Create a tracker for a specific event of an event target.\n *\n * @param eventTarget The target to track.\n * @param event The event name to track.\n */\n static create<T>(eventTarget: EventTarget, event: string) {\n return new OutputTracker<T>(eventTarget, event);\n }\n\n readonly #eventTarget;\n readonly #event;\n readonly #data: T[];\n readonly #tracker;\n\n /**\n * Create a tracker for a specific event of an event target.\n *\n * @param eventTarget The target to track.\n * @param event The event name to track.\n */\n constructor(eventTarget: EventTarget, event: string) {\n this.#eventTarget = eventTarget;\n this.#event = event;\n this.#data = [];\n this.#tracker = (event: Event) =>\n this.#data.push((event as CustomEvent<T>).detail);\n\n this.#eventTarget.addEventListener(this.#event, this.#tracker);\n }\n\n /**\n * Return the tracked data.\n *\n * @return The tracked data.\n */\n get data(): T[] {\n return this.#data;\n }\n\n /**\n * Clear the tracked data and return the cleared data.\n *\n * @return The cleared data.\n */\n clear(): T[] {\n const result = [...this.#data];\n this.#data.length = 0;\n return result;\n }\n\n /**\n * Stop tracking.\n */\n stop() {\n this.#eventTarget.removeEventListener(this.#event, this.#tracker);\n }\n}\n","// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.\n\n/**\n * Provides CQNS features.\n *\n * The Command Query Notification Separation principle is a software design\n * principle that separates the concerns of commands, queries, and\n * notifications.\n *\n * Message hierarchy:\n *\n * - Message\n * - Incoming / outgoing\n * - Request (outgoing) -> response (incoming)\n * - Command -> command status\n * - Query -> query result\n * - Notification\n * - Incoming: notification -> commands\n * - Outgoing\n * - Event (internal)\n *\n * @see https://ralfw.de/command-query-notification-separation-cqns/\n * @module\n */\n\n/**\n * The status returned by a command handler.\n */\nexport type CommandStatus = Success | Failure;\n\n/**\n * A successful status.\n */\nexport class Success {\n readonly isSuccess = true;\n}\n\n/**\n * A failed status.\n */\nexport class Failure {\n readonly isSuccess = false;\n errorMessage: string;\n\n /**\n * Creates a failed status.\n *\n * @param errorMessage\n */\n constructor(errorMessage: string) {\n this.errorMessage = errorMessage;\n }\n}\n","// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.\n\nimport { OutputTracker } from \"../common/output_tracker\";\n\nconst MESSAGE_EVENT = \"message\";\n\n/**\n * A stub for the console interface.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Console_API\n */\nexport class ConsoleStub extends EventTarget {\n log(...data: unknown[]) {\n this.dispatchEvent(\n new CustomEvent(MESSAGE_EVENT, {\n detail: { level: \"log\", message: data },\n }),\n );\n }\n\n error(...data: unknown[]) {\n this.dispatchEvent(\n new CustomEvent(MESSAGE_EVENT, {\n detail: { level: \"error\", message: data },\n }),\n );\n }\n\n warn(...data: unknown[]) {\n this.dispatchEvent(\n new CustomEvent(MESSAGE_EVENT, {\n detail: { level: \"warn\", message: data },\n }),\n );\n }\n\n info(...data: unknown[]) {\n this.dispatchEvent(\n new CustomEvent(MESSAGE_EVENT, {\n detail: { level: \"info\", message: data },\n }),\n );\n }\n\n debug(...data: unknown[]) {\n this.dispatchEvent(\n new CustomEvent(MESSAGE_EVENT, {\n detail: { level: \"debug\", message: data },\n }),\n );\n }\n\n trace(...data: unknown[]) {\n this.dispatchEvent(\n new CustomEvent(MESSAGE_EVENT, {\n detail: { level: \"trace\", message: data },\n }),\n );\n }\n\n /**\n * Track the console messages.\n */\n trackMessages() {\n return new OutputTracker(this, MESSAGE_EVENT);\n }\n}\n","// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.\n\nimport { ConfigurableResponses } from \"../common/configurable_responses\";\n\n/**\n * This data object configures the response of a fetch stub call.\n */\nexport interface ResponseData {\n /** The HTTP status code. */\n status: number;\n\n /** The HTTP status text. */\n statusText: string;\n\n /** The optional response body. */\n body?: Blob | object | string | null;\n}\n\n/**\n * Create a fetch stub.\n *\n * The stub returns a response from the provided response data or throws an provided error.\n *\n * @param responses A single response or an array of responses.\n * @returns The fetch stub.\n */\nexport function createFetchStub(\n responses?: ResponseData | Error | (ResponseData | Error)[],\n): () => Promise<Response> {\n const configurableResponses = ConfigurableResponses.create(responses);\n return async function () {\n const response = configurableResponses.next();\n if (response instanceof Error) {\n throw response;\n }\n\n return new ResponseStub(response) as unknown as Response;\n };\n}\n\nclass ResponseStub {\n #status: number;\n #statusText: string;\n #body?: Blob | object | string | null;\n\n constructor({ status, statusText, body = null }: ResponseData) {\n this.#status = status;\n this.#statusText = statusText;\n this.#body = body;\n }\n\n get ok() {\n return this.status >= 200 && this.status < 300;\n }\n\n get status() {\n return this.#status;\n }\n\n get statusText() {\n return this.#statusText;\n }\n\n async blob() {\n if (this.#body == null) {\n return null;\n }\n\n if (this.#body instanceof Blob) {\n return this.#body;\n }\n\n throw new TypeError(\"Body is not a Blob.\");\n }\n\n async json() {\n const json =\n typeof this.#body === \"string\" ? this.#body : JSON.stringify(this.#body);\n return Promise.resolve(JSON.parse(json));\n }\n\n async text() {\n if (this.#body == null) {\n return \"\";\n }\n\n return String(this.#body);\n }\n}\n","// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.\n\nimport type { MessageClient } from \"./message_client\";\n\n/**\n * A client for the server-sent events protocol.\n */\nexport class SseClient extends EventTarget implements MessageClient {\n /**\n * Create an SSE client.\n *\n * @return A new SSE client.\n */\n static create(): SseClient {\n return new SseClient(EventSource);\n }\n\n /**\n * Create a nulled SSE client.\n *\n * @return A new SSE client.\n */\n static createNull(): SseClient {\n return new SseClient(EventSourceStub as typeof EventSource);\n }\n\n readonly #eventSourceConstructor: typeof EventSource;\n\n #eventSource?: EventSource;\n\n private constructor(eventSourceConstructor: typeof EventSource) {\n super();\n this.#eventSourceConstructor = eventSourceConstructor;\n }\n\n get isConnected(): boolean {\n return this.#eventSource?.readyState === this.#eventSourceConstructor.OPEN;\n }\n\n get url(): string | undefined {\n return this.#eventSource?.url;\n }\n\n async connect(\n url: string | URL,\n eventName = \"message\",\n ...otherEvents: string[]\n ): Promise<void> {\n await new Promise<void>((resolve, reject) => {\n if (this.isConnected) {\n reject(new Error(\"Already connected.\"));\n return;\n }\n\n try {\n this.#eventSource = new this.#eventSourceConstructor(url);\n this.#eventSource.addEventListener(\"open\", (e) => {\n this.#handleOpen(e);\n resolve();\n });\n this.#eventSource.addEventListener(eventName, (e) =>\n this.#handleMessage(e),\n );\n for (const otherEvent of otherEvents) {\n this.#eventSource.addEventListener(otherEvent, (e) =>\n this.#handleMessage(e),\n );\n }\n this.#eventSource.addEventListener(\"error\", (e) =>\n this.#handleError(e),\n );\n } catch (error) {\n reject(error);\n }\n });\n }\n\n send(_message: string, _type?: string): Promise<void> {\n throw new Error(\"Method not implemented.\");\n }\n\n async close(): Promise<void> {\n await new Promise<void>((resolve, reject) => {\n if (!this.isConnected) {\n resolve();\n return;\n }\n\n try {\n this.#eventSource!.close();\n resolve();\n } catch (error) {\n reject(error);\n }\n });\n }\n\n /**\n * Simulate a message event from the server.\n *\n * @param message The message to receive.\n * @param eventName The optional event type.\n * @param lastEventId The optional last event ID.\n */\n simulateMessage(\n message: string | number | boolean | object | null,\n eventName = \"message\",\n lastEventId?: string,\n ) {\n if (typeof message !== \"string\") {\n message = JSON.stringify(message);\n }\n this.#handleMessage(\n new MessageEvent(eventName, { data: message, lastEventId }),\n );\n }\n\n /**\n * Simulate an error event.\n */\n simulateError() {\n this.#handleError(new Event(\"error\"));\n }\n\n #handleOpen(event: Event) {\n this.dispatchEvent(new Event(event.type, event));\n }\n\n #handleMessage(event: MessageEvent) {\n this.dispatchEvent(\n new MessageEvent(event.type, event as unknown as MessageEventInit),\n );\n }\n\n #handleError(event: Event) {\n this.dispatchEvent(new Event(event.type, event));\n }\n}\n\nclass EventSourceStub extends EventTarget {\n // The constants have to be defined here because Node.js support is currently\n // experimental only.\n static CONNECTING = 0;\n static OPEN = 1;\n static CLOSED = 2;\n\n url: string;\n readyState = EventSourceStub.CONNECTING;\n\n constructor(url: string | URL) {\n super();\n this.url = url.toString();\n setTimeout(() => {\n this.readyState = EventSourceStub.OPEN;\n this.dispatchEvent(new Event(\"open\"));\n }, 0);\n }\n\n close() {\n this.readyState = EventSourceStub.CLOSED;\n }\n}\n","// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.\n\nimport { OutputTracker } from \"../common/output_tracker\";\nimport type { MessageClient } from \"./message_client\";\n\nexport const HEARTBEAT_TYPE = \"heartbeat\";\n\nconst MESSAGE_SENT_EVENT = \"message-sent\";\n\n/**\n * Options for the WebSocket client.\n */\nexport interface WebSocketOptions {\n /**\n * The heartbeat interval in milliseconds. A value <= 0 disables the\n * heartbeat.\n */\n heartbeat?: number;\n\n /**\n * The time in milliseconds to wait before retrying a connection after an\n * error. A value <= 0 disables automatic retries.\n */\n retry?: number;\n}\n\n/**\n * A client for the WebSocket protocol.\n */\nexport class WebSocketClient extends EventTarget implements MessageClient {\n /**\n * Create a WebSocket client.\n *\n * @param options The options for the WebSocket client.\n * @return A new WebSocket client.\n */\n static create({\n heartbeat = 30000,\n retry = 1000,\n }: WebSocketOptions = {}): WebSocketClient {\n return new WebSocketClient(heartbeat, retry, WebSocket);\n }\n\n /**\n * Create a nulled WebSocket client.\n *\n * @param options The options for the WebSocket client.\n * @return A new nulled WebSocket client.\n */\n static createNull({ heartbeat = 0, retry = 0 }: WebSocketOptions = {}) {\n return new WebSocketClient(\n heartbeat,\n retry,\n WebSocketStub as unknown as typeof WebSocket,\n );\n }\n\n readonly #heartbeat: number;\n readonly #retry: number;\n readonly #webSocketConstructor: typeof WebSocket;\n\n #webSocket?: WebSocket;\n #heartbeatId?: ReturnType<typeof setTimeout>;\n #retryId?: ReturnType<typeof setTimeout>;\n\n private constructor(\n heartbeat: number,\n retry: number,\n webSocketConstructor: typeof WebSocket,\n ) {\n super();\n this.#heartbeat = heartbeat;\n this.#retry = retry;\n this.#webSocketConstructor = webSocketConstructor;\n }\n\n get isConnected(): boolean {\n return this.#webSocket?.readyState === WebSocket.OPEN;\n }\n\n get url(): string | undefined {\n return this.#webSocket?.url;\n }\n\n async connect(url: string | URL): Promise<void> {\n await new Promise<void>((resolve, reject) => {\n this.#stopRetry();\n\n if (this.isConnected) {\n reject(new Error(\"Already connected.\"));\n return;\n }\n\n try {\n this.#webSocket = new this.#webSocketConstructor(url);\n this.#webSocket.addEventListener(\"open\", (e) => {\n this.#handleOpen(e);\n resolve();\n });\n this.#webSocket.addEventListener(\"message\", (e) =>\n this.#handleMessage(e),\n );\n this.#webSocket.addEventListener(\"close\", (e) => this.#handleClose(e));\n this.#webSocket.addEventListener(\"error\", (e) => this.#handleError(e));\n } catch (error) {\n reject(error);\n }\n });\n }\n\n async send(\n message: string | ArrayBuffer | Blob | ArrayBufferView,\n ): Promise<void> {\n if (!this.isConnected) {\n throw new Error(\"Not connected.\");\n }\n\n this.#webSocket!.send(message);\n this.dispatchEvent(\n new CustomEvent(MESSAGE_SENT_EVENT, { detail: message }),\n );\n await Promise.resolve();\n }\n\n /**\n * Return a tracker for messages sent.\n *\n * @return A new output tracker.\n */\n trackMessageSent(): OutputTracker<string> {\n return OutputTracker.create(this, MESSAGE_SENT_EVENT);\n }\n\n /**\n * Close the connection.\n *\n * If a code is provided, also a reason should be provided.\n *\n * @param code An optional code.\n * @param reason An optional reason.\n */\n async close(code?: number, reason?: string): Promise<void> {\n await new Promise<void>((resolve) => {\n this.#stopRetry();\n\n if (!this.isConnected) {\n resolve();\n return;\n }\n\n this.#webSocket!.addEventListener(\"close\", () => resolve());\n this.#webSocket!.close(code, reason);\n });\n }\n\n /**\n * Simulate a message event from the server.\n *\n * @param message The message to receive.\n */\n simulateMessage(message: string | number | boolean | object | null) {\n if (typeof message !== \"string\") {\n message = JSON.stringify(message);\n }\n this.#handleMessage(new MessageEvent(\"message\", { data: message }));\n }\n\n /**\n * Simulate a heartbeat.\n */\n simulateHeartbeat() {\n this.#sendHeartbeat();\n }\n\n /**\n * Simulate a close event.\n *\n * @param code An optional code.\n * @param reason An optional reason.\n */\n simulateClose(code?: number, reason?: string) {\n this.#handleClose(new CloseEvent(\"close\", { code, reason }));\n }\n\n /**\n * Simulate an error event.\n */\n simulateError() {\n this.#webSocket?.close();\n this.#handleError(new Event(\"error\"));\n }\n\n #handleOpen(event: Event) {\n this.dispatchEvent(new Event(event.type, event));\n this.#startHeartbeat();\n }\n\n #handleMessage(event: MessageEvent) {\n this.dispatchEvent(\n new MessageEvent(event.type, event as unknown as MessageEventInit),\n );\n }\n\n #handleClose(event: CloseEvent) {\n this.#stopHeartbeat();\n this.dispatchEvent(new CloseEvent(event.type, event));\n }\n\n #handleError(event: Event) {\n this.dispatchEvent(new Event(event.type, event));\n this.#startRetry();\n }\n\n #startRetry() {\n if (this.#retry <= 0) {\n return;\n }\n this.#retryId = setInterval(\n () => this.connect(this.#webSocket!.url),\n this.#retry,\n );\n }\n\n #stopRetry() {\n clearInterval(this.#retryId);\n this.#retryId = undefined;\n }\n\n #startHeartbeat() {\n if (this.#heartbeat <= 0) {\n return;\n }\n\n this.#heartbeatId = setInterval(\n () => this.#sendHeartbeat(),\n this.#heartbeat,\n );\n }\n\n #stopHeartbeat() {\n clearInterval(this.#heartbeatId);\n this.#heartbeatId = undefined;\n }\n\n #sendHeartbeat() {\n if (this.#heartbeatId == null) {\n return;\n }\n\n void this.send(HEARTBEAT_TYPE);\n }\n}\n\nclass WebSocketStub extends EventTarget {\n url: string;\n readyState: number = WebSocket.CONNECTING;\n\n constructor(url: string | URL) {\n super();\n this.url = url.toString();\n setTimeout(() => {\n this.readyState = WebSocket.OPEN;\n this.dispatchEvent(new Event(\"open\"));\n }, 0);\n }\n\n send() {}\n\n close() {\n this.readyState = WebSocket.CLOSED;\n this.dispatchEvent(new Event(\"close\"));\n }\n}\n"],"names":["Clock","date","clock","offsetMillis","#date","ConfigurableResponses","responses","name","responseObject","translatedEntries","key","value","translatedName","#description","#responses","response","OutputTracker","eventTarget","event","#eventTarget","#event","#data","#tracker","result","Success","Failure","errorMessage","MESSAGE_EVENT","ConsoleStub","data","createFetchStub","configurableResponses","ResponseStub","#status","#statusText","#body","status","statusText","body","json","SseClient","EventSourceStub","#eventSourceConstructor","#eventSource","eventSourceConstructor","url","eventName","otherEvents","resolve","reject","e","#handleOpen","#handleMessage","otherEvent","#handleError","error","_message","_type","message","lastEventId","HEARTBEAT_TYPE","MESSAGE_SENT_EVENT","WebSocketClient","heartbeat","retry","WebSocketStub","#heartbeat","#retry","#webSocketConstructor","#webSocket","#heartbeatId","#retryId","webSocketConstructor","#stopRetry","#handleClose","code","reason","#sendHeartbeat","#startHeartbeat","#stopHeartbeat","#startRetry"],"mappings":"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"}
1
+ {"version":3,"file":"shared.js","sources":["../src/common/clock.ts","../src/common/event_tracker.ts","../src/domain/messages.ts","../src/infrastructure/configurable_responses.ts","../src/infrastructure/output_tracker.ts","../src/infrastructure/console_stub.ts","../src/infrastructure/fetch_stub.ts","../src/infrastructure/sse_client.ts","../src/infrastructure/web_socket_client.ts"],"sourcesContent":["// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.\n\n/**\n * A clock provides access to the current timestamp.\n */\nexport class Clock {\n /**\n * Create a clock using system the clock.\n *\n * @return A clock that uses the system clock.\n */\n static system(): Clock {\n return new Clock();\n }\n\n /**\n * Create a clock using a fixed date.\n *\n * @param date The fixed date of the clock.\n * @return A clock that always returns a fixed date.\n */\n static fixed(date: Date | string | number): Clock {\n return new Clock(new Date(date));\n }\n\n /**\n * Create a clock that returns a fixed offset from the given clock.\n *\n * @param clock The clock to offset from.\n * @param offsetMillis The offset in milliseconds.\n * @return A clock that returns a fixed offset from the given clock.\n */\n static offset(clock: Clock, offsetMillis: number): Clock {\n return new Clock(new Date(clock.millis() + offsetMillis));\n }\n\n readonly #date?: Date;\n\n private constructor(date?: Date) {\n this.#date = date;\n }\n\n /**\n * Return the current timestamp of the clock.\n *\n * @return The current timestamp.\n */\n date(): Date {\n return this.#date ? new Date(this.#date) : new Date();\n }\n\n /**\n * Return the current timestamp of the clock in milliseconds.\n *\n * @return The current timestamp in milliseconds.\n */\n millis(): number {\n return this.date().getTime();\n }\n}\n","// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.\n\n/**\n * Track events from an event target.\n *\n * Wait asynchronously for events. Useful in test code.\n */\nexport class EventTracker<T extends Event> {\n /**\n * Create a tracker for a specific event of an event target.\n *\n * @param eventTarget The target to track.\n * @param event The event name to track.\n */\n static create<T extends Event>(eventTarget: EventTarget, event: string) {\n return new EventTracker<T>(eventTarget, event);\n }\n\n readonly #eventTarget;\n readonly #event;\n readonly #events: T[];\n readonly #tracker;\n\n /**\n * Create a tracker for a specific event of an event target.\n *\n * @param eventTarget The target to track.\n * @param event The event name to track.\n */\n constructor(eventTarget: EventTarget, event: string) {\n this.#eventTarget = eventTarget;\n this.#event = event;\n this.#events = [];\n this.#tracker = (event: Event) => this.#events.push(event as T);\n\n this.#eventTarget.addEventListener(this.#event, this.#tracker);\n }\n\n /**\n * Return the tracked events.\n *\n * @return The tracked events.\n */\n get events(): T[] {\n return this.#events;\n }\n\n /**\n * Clear the tracked events and return the cleared events.\n *\n * @return The cleared events.\n */\n clear(): T[] {\n const result = [...this.#events];\n this.#events.length = 0;\n return result;\n }\n\n /**\n * Stop tracking.\n */\n stop() {\n this.#eventTarget.removeEventListener(this.#event, this.#tracker);\n }\n\n /**\n * Wait asynchronously for a number of events.\n *\n * @param count number of events, default 1.\n */\n async waitFor(count = 1) {\n return new Promise<T[]>((resolve) => {\n const checkEvents = () => {\n if (this.#events.length >= count) {\n this.#eventTarget.removeEventListener(this.#event, checkEvents);\n resolve(this.events);\n }\n };\n\n this.#eventTarget.addEventListener(this.#event, checkEvents);\n checkEvents();\n });\n }\n}\n","// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.\n\n/**\n * Provides CQNS features.\n *\n * The Command Query Notification Separation principle is a software design\n * principle that separates the concerns of commands, queries, and\n * notifications.\n *\n * Message hierarchy:\n *\n * - Message\n * - Incoming / outgoing\n * - Request (outgoing) -> response (incoming)\n * - Command -> command status\n * - Query -> query result\n * - Notification\n * - Incoming: notification -> commands\n * - Outgoing\n * - Event (internal)\n *\n * @see https://ralfw.de/command-query-notification-separation-cqns/\n * @module\n */\n\n/**\n * The status returned by a command handler.\n */\nexport type CommandStatus = Success | Failure;\n\n/**\n * A successful status.\n */\nexport class Success {\n readonly isSuccess = true;\n}\n\n/**\n * A failed status.\n */\nexport class Failure {\n readonly isSuccess = false;\n errorMessage: string;\n\n /**\n * Creates a failed status.\n *\n * @param errorMessage\n */\n constructor(errorMessage: string) {\n this.errorMessage = errorMessage;\n }\n}\n","// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.\n// Copyright 2023 Titanium I.T. LLC. MIT License.\n\n/**\n * Handle returning pre-configured responses.\n *\n * This is one of the nullability patterns from James Shore's article on\n * [testing without mocks](https://www.jamesshore.com/v2/projects/nullables/testing-without-mocks#configurable-responses).\n *\n * Example usage for stubbing `fetch` function:\n *\n * ```javascript\n * function createFetchStub(responses) {\n * const configurableResponses = ConfigurableResponses.create(responses);\n * return async function () {\n * const response = configurableResponses.next();\n * return {\n * status: response.status,\n * json: async () => response.body,\n * };\n * };\n * }\n * ```\n */\nexport class ConfigurableResponses<T = unknown> {\n /**\n * Create a list of responses (by providing an array), or a single repeating\n * response (by providing any other type). 'Name' is optional and used in\n * error messages.\n *\n * @param responses A single response or an array of responses.\n * @param name An optional name for the responses.\n */\n static create<T>(responses?: T | T[], name?: string) {\n return new ConfigurableResponses<T>(responses, name);\n }\n\n /**\n * Convert all properties in an object into ConfigurableResponse instances.\n * For example, { a: 1 } becomes { a: ConfigurableResponses.create(1) }.\n * 'Name' is optional and used in error messages.\n *\n * @param responseObject An object with single response or an array of responses.\n * @param name An optional name for the responses.\n */\n static mapObject<T extends Record<string, unknown>>(\n responseObject: T,\n name?: string,\n ) {\n const entries = Object.entries(responseObject);\n const translatedEntries = entries.map(([key, value]) => {\n const translatedName = name === undefined ? undefined : `${name}: ${key}`;\n return [key, ConfigurableResponses.create(value, translatedName)];\n });\n return Object.fromEntries(translatedEntries);\n }\n\n readonly #description;\n readonly #responses;\n\n /**\n * Create a list of responses (by providing an array), or a single repeating\n * response (by providing any other type). 'Name' is optional and used in\n * error messages.\n *\n * @param responses A single response or an array of responses.\n * @param name An optional name for the responses.\n */\n constructor(responses?: T | T[], name?: string) {\n this.#description = name == null ? \"\" : ` in ${name}`;\n this.#responses = Array.isArray(responses) ? [...responses] : responses;\n }\n\n /**\n * Get the next configured response. Throws an error when configured with a list\n * of responses and no more responses remain.\n *\n * @return The next response.\n */\n next(): T {\n const response = Array.isArray(this.#responses)\n ? this.#responses.shift()\n : this.#responses;\n if (response === undefined) {\n throw new Error(`No more responses configured${this.#description}.`);\n }\n\n return response;\n }\n}\n","// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.\n// Copyright 2020-2022 Titanium I.T. LLC. MIT License.\n\n/**\n * Track output events.\n *\n * This is one of the nullability patterns from James Shore's article on\n * [testing without mocks](https://www.jamesshore.com/v2/projects/nullables/testing-without-mocks#output-tracking).\n *\n * Example implementation of an event store:\n *\n * ```javascript\n * async record(event) {\n * // ...\n * this.dispatchEvent(new CustomEvent(\"eventRecorded\", { detail: event }));\n * }\n *\n * trackEventsRecorded() {\n * return new OutputTracker(this, \"eventRecorded\");\n * }\n * ```\n *\n * Example usage:\n *\n * ```javascript\n * const eventsRecorded = eventStore.trackEventsRecorded();\n * // ...\n * const data = eventsRecorded.data(); // [event1, event2, ...]\n * ```\n */\nexport class OutputTracker<T = unknown> {\n /**\n * Create a tracker for a specific event of an event target.\n *\n * @param eventTarget The target to track.\n * @param event The event name to track.\n */\n static create<T>(eventTarget: EventTarget, event: string) {\n return new OutputTracker<T>(eventTarget, event);\n }\n\n readonly #eventTarget;\n readonly #event;\n readonly #data: T[];\n readonly #tracker;\n\n /**\n * Create a tracker for a specific event of an event target.\n *\n * @param eventTarget The target to track.\n * @param event The event name to track.\n */\n constructor(eventTarget: EventTarget, event: string) {\n this.#eventTarget = eventTarget;\n this.#event = event;\n this.#data = [];\n this.#tracker = (event: Event) =>\n this.#data.push((event as CustomEvent<T>).detail);\n\n this.#eventTarget.addEventListener(this.#event, this.#tracker);\n }\n\n /**\n * Return the tracked data.\n *\n * @return The tracked data.\n */\n get data(): T[] {\n return this.#data;\n }\n\n /**\n * Clear the tracked data and return the cleared data.\n *\n * @return The cleared data.\n */\n clear(): T[] {\n const result = [...this.#data];\n this.#data.length = 0;\n return result;\n }\n\n /**\n * Stop tracking.\n */\n stop() {\n this.#eventTarget.removeEventListener(this.#event, this.#tracker);\n }\n}\n","// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.\n\nimport { OutputTracker } from \"./output_tracker\";\n\nconst MESSAGE_EVENT = \"message\";\n\nexport interface ConsoleMessage {\n level: \"log\" | \"error\" | \"warn\" | \"info\" | \"debug\" | \"trace\";\n message: unknown[];\n}\n\n/**\n * A stub for the console interface.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Console_API\n */\nexport class ConsoleStub extends EventTarget {\n log(...data: unknown[]) {\n this.dispatchEvent(\n new CustomEvent(MESSAGE_EVENT, {\n detail: { level: \"log\", message: data },\n }),\n );\n }\n\n error(...data: unknown[]) {\n this.dispatchEvent(\n new CustomEvent(MESSAGE_EVENT, {\n detail: { level: \"error\", message: data },\n }),\n );\n }\n\n warn(...data: unknown[]) {\n this.dispatchEvent(\n new CustomEvent(MESSAGE_EVENT, {\n detail: { level: \"warn\", message: data },\n }),\n );\n }\n\n info(...data: unknown[]) {\n this.dispatchEvent(\n new CustomEvent(MESSAGE_EVENT, {\n detail: { level: \"info\", message: data },\n }),\n );\n }\n\n debug(...data: unknown[]) {\n this.dispatchEvent(\n new CustomEvent(MESSAGE_EVENT, {\n detail: { level: \"debug\", message: data },\n }),\n );\n }\n\n trace(...data: unknown[]) {\n this.dispatchEvent(\n new CustomEvent(MESSAGE_EVENT, {\n detail: { level: \"trace\", message: data },\n }),\n );\n }\n\n /**\n * Track the console messages.\n */\n trackMessages() {\n return new OutputTracker<ConsoleMessage>(this, MESSAGE_EVENT);\n }\n}\n","// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.\n\nimport { ConfigurableResponses } from \"./configurable_responses\";\n\n/**\n * This data object configures the response of a fetch stub call.\n */\nexport interface ResponseData {\n /** The HTTP status code. */\n status: number;\n\n /** The HTTP status text. */\n statusText: string;\n\n /** The optional response body. */\n body?: Blob | object | string | null;\n}\n\n/**\n * Create a fetch stub.\n *\n * The stub returns a response from the provided response data or throws an provided error.\n *\n * @param responses A single response or an array of responses.\n * @returns The fetch stub.\n */\nexport function createFetchStub(\n responses?: ResponseData | Error | (ResponseData | Error)[],\n): () => Promise<Response> {\n const configurableResponses = ConfigurableResponses.create(responses);\n return async function () {\n const response = configurableResponses.next();\n if (response instanceof Error) {\n throw response;\n }\n\n return new ResponseStub(response) as unknown as Response;\n };\n}\n\nclass ResponseStub {\n #status: number;\n #statusText: string;\n #body?: Blob | object | string | null;\n\n constructor({ status, statusText, body = null }: ResponseData) {\n this.#status = status;\n this.#statusText = statusText;\n this.#body = body;\n }\n\n get ok() {\n return this.status >= 200 && this.status < 300;\n }\n\n get status() {\n return this.#status;\n }\n\n get statusText() {\n return this.#statusText;\n }\n\n async blob() {\n if (this.#body == null) {\n return null;\n }\n\n if (this.#body instanceof Blob) {\n return this.#body;\n }\n\n throw new TypeError(\"Body is not a Blob.\");\n }\n\n async json() {\n const json =\n typeof this.#body === \"string\" ? this.#body : JSON.stringify(this.#body);\n return Promise.resolve(JSON.parse(json));\n }\n\n async text() {\n if (this.#body == null) {\n return \"\";\n }\n\n return String(this.#body);\n }\n}\n","// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.\n\nimport type { MessageClient } from \"./message_client\";\n\n/**\n * A client for the server-sent events protocol.\n */\nexport class SseClient extends EventTarget implements MessageClient {\n /**\n * Create an SSE client.\n *\n * @return A new SSE client.\n */\n static create(): SseClient {\n return new SseClient(EventSource);\n }\n\n /**\n * Create a nulled SSE client.\n *\n * @return A new SSE client.\n */\n static createNull(): SseClient {\n return new SseClient(EventSourceStub as typeof EventSource);\n }\n\n readonly #eventSourceConstructor: typeof EventSource;\n\n #eventSource?: EventSource;\n\n private constructor(eventSourceConstructor: typeof EventSource) {\n super();\n this.#eventSourceConstructor = eventSourceConstructor;\n }\n\n get isConnected(): boolean {\n return this.#eventSource?.readyState === this.#eventSourceConstructor.OPEN;\n }\n\n get url(): string | undefined {\n return this.#eventSource?.url;\n }\n\n async connect(\n url: string | URL,\n eventName = \"message\",\n ...otherEvents: string[]\n ): Promise<void> {\n await new Promise<void>((resolve, reject) => {\n if (this.isConnected) {\n reject(new Error(\"Already connected.\"));\n return;\n }\n\n try {\n this.#eventSource = new this.#eventSourceConstructor(url);\n this.#eventSource.addEventListener(\"open\", (e) => {\n this.#handleOpen(e);\n resolve();\n });\n this.#eventSource.addEventListener(eventName, (e) =>\n this.#handleMessage(e),\n );\n for (const otherEvent of otherEvents) {\n this.#eventSource.addEventListener(otherEvent, (e) =>\n this.#handleMessage(e),\n );\n }\n this.#eventSource.addEventListener(\"error\", (e) =>\n this.#handleError(e),\n );\n } catch (error) {\n reject(error);\n }\n });\n }\n\n send(_message: string, _type?: string): Promise<void> {\n throw new Error(\"Method not implemented.\");\n }\n\n async close(): Promise<void> {\n await new Promise<void>((resolve, reject) => {\n if (!this.isConnected) {\n resolve();\n return;\n }\n\n try {\n this.#eventSource!.close();\n resolve();\n } catch (error) {\n reject(error);\n }\n });\n }\n\n /**\n * Simulate a message event from the server.\n *\n * @param message The message to receive.\n * @param eventName The optional event type.\n * @param lastEventId The optional last event ID.\n */\n simulateMessage(\n message: string | number | boolean | object | null,\n eventName = \"message\",\n lastEventId?: string,\n ) {\n if (typeof message !== \"string\") {\n message = JSON.stringify(message);\n }\n this.#handleMessage(\n new MessageEvent(eventName, { data: message, lastEventId }),\n );\n }\n\n /**\n * Simulate an error event.\n */\n simulateError() {\n this.#handleError(new Event(\"error\"));\n }\n\n #handleOpen(event: Event) {\n this.dispatchEvent(new Event(event.type, event));\n }\n\n #handleMessage(event: MessageEvent) {\n this.dispatchEvent(\n new MessageEvent(event.type, event as unknown as MessageEventInit),\n );\n }\n\n #handleError(event: Event) {\n this.dispatchEvent(new Event(event.type, event));\n }\n}\n\nclass EventSourceStub extends EventTarget {\n // The constants have to be defined here because Node.js support is currently\n // experimental only.\n static CONNECTING = 0;\n static OPEN = 1;\n static CLOSED = 2;\n\n url: string;\n readyState = EventSourceStub.CONNECTING;\n\n constructor(url: string | URL) {\n super();\n this.url = url.toString();\n setTimeout(() => {\n this.readyState = EventSourceStub.OPEN;\n this.dispatchEvent(new Event(\"open\"));\n }, 0);\n }\n\n close() {\n this.readyState = EventSourceStub.CLOSED;\n }\n}\n","// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.\n\nimport { OutputTracker } from \"./output_tracker\";\nimport type { MessageClient } from \"./message_client\";\n\nexport const HEARTBEAT_TYPE = \"heartbeat\";\n\nconst MESSAGE_SENT_EVENT = \"message-sent\";\n\n/**\n * Options for the WebSocket client.\n */\nexport interface WebSocketOptions {\n /**\n * The heartbeat interval in milliseconds. A value <= 0 disables the\n * heartbeat.\n */\n heartbeat?: number;\n\n /**\n * The time in milliseconds to wait before retrying a connection after an\n * error. A value <= 0 disables automatic retries.\n */\n retry?: number;\n}\n\n/**\n * A client for the WebSocket protocol.\n */\nexport class WebSocketClient extends EventTarget implements MessageClient {\n /**\n * Create a WebSocket client.\n *\n * @param options The options for the WebSocket client.\n * @return A new WebSocket client.\n */\n static create({\n heartbeat = 30000,\n retry = 1000,\n }: WebSocketOptions = {}): WebSocketClient {\n return new WebSocketClient(heartbeat, retry, WebSocket);\n }\n\n /**\n * Create a nulled WebSocket client.\n *\n * @param options The options for the WebSocket client.\n * @return A new nulled WebSocket client.\n */\n static createNull({ heartbeat = 0, retry = 0 }: WebSocketOptions = {}) {\n return new WebSocketClient(\n heartbeat,\n retry,\n WebSocketStub as unknown as typeof WebSocket,\n );\n }\n\n readonly #heartbeat: number;\n readonly #retry: number;\n readonly #webSocketConstructor: typeof WebSocket;\n\n #webSocket?: WebSocket;\n #heartbeatId?: ReturnType<typeof setTimeout>;\n #retryId?: ReturnType<typeof setTimeout>;\n\n private constructor(\n heartbeat: number,\n retry: number,\n webSocketConstructor: typeof WebSocket,\n ) {\n super();\n this.#heartbeat = heartbeat;\n this.#retry = retry;\n this.#webSocketConstructor = webSocketConstructor;\n }\n\n get isConnected(): boolean {\n return this.#webSocket?.readyState === WebSocket.OPEN;\n }\n\n get url(): string | undefined {\n return this.#webSocket?.url;\n }\n\n async connect(url: string | URL): Promise<void> {\n await new Promise<void>((resolve, reject) => {\n this.#stopRetry();\n\n if (this.isConnected) {\n reject(new Error(\"Already connected.\"));\n return;\n }\n\n try {\n this.#webSocket = new this.#webSocketConstructor(url);\n this.#webSocket.addEventListener(\"open\", (e) => {\n this.#handleOpen(e);\n resolve();\n });\n this.#webSocket.addEventListener(\"message\", (e) =>\n this.#handleMessage(e),\n );\n this.#webSocket.addEventListener(\"close\", (e) => this.#handleClose(e));\n this.#webSocket.addEventListener(\"error\", (e) => this.#handleError(e));\n } catch (error) {\n reject(error);\n }\n });\n }\n\n async send(\n message: string | ArrayBuffer | Blob | ArrayBufferView,\n ): Promise<void> {\n if (!this.isConnected) {\n throw new Error(\"Not connected.\");\n }\n\n this.#webSocket!.send(message);\n this.dispatchEvent(\n new CustomEvent(MESSAGE_SENT_EVENT, { detail: message }),\n );\n await Promise.resolve();\n }\n\n /**\n * Return a tracker for messages sent.\n *\n * @return A new output tracker.\n */\n trackMessageSent(): OutputTracker<string> {\n return OutputTracker.create(this, MESSAGE_SENT_EVENT);\n }\n\n /**\n * Close the connection.\n *\n * If a code is provided, also a reason should be provided.\n *\n * @param code An optional code.\n * @param reason An optional reason.\n */\n async close(code?: number, reason?: string): Promise<void> {\n await new Promise<void>((resolve) => {\n this.#stopRetry();\n\n if (!this.isConnected) {\n resolve();\n return;\n }\n\n this.#webSocket!.addEventListener(\"close\", () => resolve());\n this.#webSocket!.close(code, reason);\n });\n }\n\n /**\n * Simulate a message event from the server.\n *\n * @param message The message to receive.\n */\n simulateMessage(message: string | number | boolean | object | null) {\n if (typeof message !== \"string\") {\n message = JSON.stringify(message);\n }\n this.#handleMessage(new MessageEvent(\"message\", { data: message }));\n }\n\n /**\n * Simulate a heartbeat.\n */\n simulateHeartbeat() {\n this.#sendHeartbeat();\n }\n\n /**\n * Simulate a close event.\n *\n * @param code An optional code.\n * @param reason An optional reason.\n */\n simulateClose(code?: number, reason?: string) {\n this.#handleClose(new CloseEvent(\"close\", { code, reason }));\n }\n\n /**\n * Simulate an error event.\n */\n simulateError() {\n this.#webSocket?.close();\n this.#handleError(new Event(\"error\"));\n }\n\n #handleOpen(event: Event) {\n this.dispatchEvent(new Event(event.type, event));\n this.#startHeartbeat();\n }\n\n #handleMessage(event: MessageEvent) {\n this.dispatchEvent(\n new MessageEvent(event.type, event as unknown as MessageEventInit),\n );\n }\n\n #handleClose(event: CloseEvent) {\n this.#stopHeartbeat();\n this.dispatchEvent(new CloseEvent(event.type, event));\n }\n\n #handleError(event: Event) {\n this.dispatchEvent(new Event(event.type, event));\n this.#startRetry();\n }\n\n #startRetry() {\n if (this.#retry <= 0) {\n return;\n }\n this.#retryId = setInterval(\n () => this.connect(this.#webSocket!.url),\n this.#retry,\n );\n }\n\n #stopRetry() {\n clearInterval(this.#retryId);\n this.#retryId = undefined;\n }\n\n #startHeartbeat() {\n if (this.#heartbeat <= 0) {\n return;\n }\n\n this.#heartbeatId = setInterval(\n () => this.#sendHeartbeat(),\n this.#heartbeat,\n );\n }\n\n #stopHeartbeat() {\n clearInterval(this.#heartbeatId);\n this.#heartbeatId = undefined;\n }\n\n #sendHeartbeat() {\n if (this.#heartbeatId == null) {\n return;\n }\n\n void this.send(HEARTBEAT_TYPE);\n }\n}\n\nclass WebSocketStub extends EventTarget {\n url: string;\n readyState: number = WebSocket.CONNECTING;\n\n constructor(url: string | URL) {\n super();\n this.url = url.toString();\n setTimeout(() => {\n this.readyState = WebSocket.OPEN;\n this.dispatchEvent(new Event(\"open\"));\n }, 0);\n }\n\n send() {}\n\n close() {\n this.readyState = WebSocket.CLOSED;\n this.dispatchEvent(new Event(\"close\"));\n }\n}\n"],"names":["Clock","date","clock","offsetMillis","#date","EventTracker","eventTarget","event","#eventTarget","#event","#events","#tracker","result","count","resolve","checkEvents","Success","Failure","errorMessage","ConfigurableResponses","responses","name","responseObject","translatedEntries","key","value","translatedName","#description","#responses","response","OutputTracker","#data","MESSAGE_EVENT","ConsoleStub","data","createFetchStub","configurableResponses","ResponseStub","#status","#statusText","#body","status","statusText","body","json","SseClient","EventSourceStub","#eventSourceConstructor","#eventSource","eventSourceConstructor","url","eventName","otherEvents","reject","e","#handleOpen","#handleMessage","otherEvent","#handleError","error","_message","_type","message","lastEventId","HEARTBEAT_TYPE","MESSAGE_SENT_EVENT","WebSocketClient","heartbeat","retry","WebSocketStub","#heartbeat","#retry","#webSocketConstructor","#webSocket","#heartbeatId","#retryId","webSocketConstructor","#stopRetry","#handleClose","code","reason","#sendHeartbeat","#startHeartbeat","#stopHeartbeat","#startRetry"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- (function(n,a){typeof exports=="object"&&typeof module<"u"?a(exports):typeof define=="function"&&define.amd?define(["exports"],a):(n=typeof globalThis<"u"?globalThis:n||self,a(n.Shared={}))})(this,(function(n){"use strict";class a{static system(){return new a}static fixed(t){return new a(new Date(t))}static offset(t,e){return new a(new Date(t.millis()+e))}#e;constructor(t){this.#e=t}date(){return this.#e?new Date(this.#e):new Date}millis(){return this.date().getTime()}}class c{static create(t,e){return new c(t,e)}static mapObject(t,e){const r=Object.entries(t).map(([l,i])=>{const w=e===void 0?void 0:`${e}: ${l}`;return[l,c.create(i,w)]});return Object.fromEntries(r)}#e;#t;constructor(t,e){this.#e=e==null?"":` in ${e}`,this.#t=Array.isArray(t)?[...t]:t}next(){const t=Array.isArray(this.#t)?this.#t.shift():this.#t;if(t===void 0)throw new Error(`No more responses configured${this.#e}.`);return t}}class u{static create(t,e){return new u(t,e)}#e;#t;#s;#n;constructor(t,e){this.#e=t,this.#t=e,this.#s=[],this.#n=s=>this.#s.push(s.detail),this.#e.addEventListener(this.#t,this.#n)}get data(){return this.#s}clear(){const t=[...this.#s];return this.#s.length=0,t}stop(){this.#e.removeEventListener(this.#t,this.#n)}}class y{isSuccess=!0}class f{isSuccess=!1;errorMessage;constructor(t){this.errorMessage=t}}const h="message";class S extends EventTarget{log(...t){this.dispatchEvent(new CustomEvent(h,{detail:{level:"log",message:t}}))}error(...t){this.dispatchEvent(new CustomEvent(h,{detail:{level:"error",message:t}}))}warn(...t){this.dispatchEvent(new CustomEvent(h,{detail:{level:"warn",message:t}}))}info(...t){this.dispatchEvent(new CustomEvent(h,{detail:{level:"info",message:t}}))}debug(...t){this.dispatchEvent(new CustomEvent(h,{detail:{level:"debug",message:t}}))}trace(...t){this.dispatchEvent(new CustomEvent(h,{detail:{level:"trace",message:t}}))}trackMessages(){return new u(this,h)}}function m(o){const t=c.create(o);return async function(){const e=t.next();if(e instanceof Error)throw e;return new b(e)}}class b{#e;#t;#s;constructor({status:t,statusText:e,body:s=null}){this.#e=t,this.#t=e,this.#s=s}get ok(){return this.status>=200&&this.status<300}get status(){return this.#e}get statusText(){return this.#t}async blob(){if(this.#s==null)return null;if(this.#s instanceof Blob)return this.#s;throw new TypeError("Body is not a Blob.")}async json(){const t=typeof this.#s=="string"?this.#s:JSON.stringify(this.#s);return Promise.resolve(JSON.parse(t))}async text(){return this.#s==null?"":String(this.#s)}}class E extends EventTarget{static create(){return new E(EventSource)}static createNull(){return new E(d)}#e;#t;constructor(t){super(),this.#e=t}get isConnected(){return this.#t?.readyState===this.#e.OPEN}get url(){return this.#t?.url}async connect(t,e="message",...s){await new Promise((r,l)=>{if(this.isConnected){l(new Error("Already connected."));return}try{this.#t=new this.#e(t),this.#t.addEventListener("open",i=>{this.#s(i),r()}),this.#t.addEventListener(e,i=>this.#n(i));for(const i of s)this.#t.addEventListener(i,w=>this.#n(w));this.#t.addEventListener("error",i=>this.#r(i))}catch(i){l(i)}})}send(t,e){throw new Error("Method not implemented.")}async close(){await new Promise((t,e)=>{if(!this.isConnected){t();return}try{this.#t.close(),t()}catch(s){e(s)}})}simulateMessage(t,e="message",s){typeof t!="string"&&(t=JSON.stringify(t)),this.#n(new MessageEvent(e,{data:t,lastEventId:s}))}simulateError(){this.#r(new Event("error"))}#s(t){this.dispatchEvent(new Event(t.type,t))}#n(t){this.dispatchEvent(new MessageEvent(t.type,t))}#r(t){this.dispatchEvent(new Event(t.type,t))}}class d extends EventTarget{static CONNECTING=0;static OPEN=1;static CLOSED=2;url;readyState=d.CONNECTING;constructor(t){super(),this.url=t.toString(),setTimeout(()=>{this.readyState=d.OPEN,this.dispatchEvent(new Event("open"))},0)}close(){this.readyState=d.CLOSED}}const g="heartbeat",p="message-sent";class v extends EventTarget{static create({heartbeat:t=3e4,retry:e=1e3}={}){return new v(t,e,WebSocket)}static createNull({heartbeat:t=0,retry:e=0}={}){return new v(t,e,C)}#e;#t;#s;#n;#r;#i;constructor(t,e,s){super(),this.#e=t,this.#t=e,this.#s=s}get isConnected(){return this.#n?.readyState===WebSocket.OPEN}get url(){return this.#n?.url}async connect(t){await new Promise((e,s)=>{if(this.#c(),this.isConnected){s(new Error("Already connected."));return}try{this.#n=new this.#s(t),this.#n.addEventListener("open",r=>{this.#d(r),e()}),this.#n.addEventListener("message",r=>this.#a(r)),this.#n.addEventListener("close",r=>this.#h(r)),this.#n.addEventListener("error",r=>this.#o(r))}catch(r){s(r)}})}async send(t){if(!this.isConnected)throw new Error("Not connected.");this.#n.send(t),this.dispatchEvent(new CustomEvent(p,{detail:t})),await Promise.resolve()}trackMessageSent(){return u.create(this,p)}async close(t,e){await new Promise(s=>{if(this.#c(),!this.isConnected){s();return}this.#n.addEventListener("close",()=>s()),this.#n.close(t,e)})}simulateMessage(t){typeof t!="string"&&(t=JSON.stringify(t)),this.#a(new MessageEvent("message",{data:t}))}simulateHeartbeat(){this.#u()}simulateClose(t,e){this.#h(new CloseEvent("close",{code:t,reason:e}))}simulateError(){this.#n?.close(),this.#o(new Event("error"))}#d(t){this.dispatchEvent(new Event(t.type,t)),this.#E()}#a(t){this.dispatchEvent(new MessageEvent(t.type,t))}#h(t){this.#v(),this.dispatchEvent(new CloseEvent(t.type,t))}#o(t){this.dispatchEvent(new Event(t.type,t)),this.#l()}#l(){this.#t<=0||(this.#i=setInterval(()=>this.connect(this.#n.url),this.#t))}#c(){clearInterval(this.#i),this.#i=void 0}#E(){this.#e<=0||(this.#r=setInterval(()=>this.#u(),this.#e))}#v(){clearInterval(this.#r),this.#r=void 0}#u(){this.#r!=null&&this.send(g)}}class C extends EventTarget{url;readyState=WebSocket.CONNECTING;constructor(t){super(),this.url=t.toString(),setTimeout(()=>{this.readyState=WebSocket.OPEN,this.dispatchEvent(new Event("open"))},0)}send(){}close(){this.readyState=WebSocket.CLOSED,this.dispatchEvent(new Event("close"))}}n.Clock=a,n.ConfigurableResponses=c,n.ConsoleStub=S,n.Failure=f,n.HEARTBEAT_TYPE=g,n.OutputTracker=u,n.SseClient=E,n.Success=y,n.WebSocketClient=v,n.createFetchStub=m,Object.defineProperty(n,Symbol.toStringTag,{value:"Module"})}));
1
+ (function(n,a){typeof exports=="object"&&typeof module<"u"?a(exports):typeof define=="function"&&define.amd?define(["exports"],a):(n=typeof globalThis<"u"?globalThis:n||self,a(n.Shared={}))})(this,(function(n){"use strict";class a{static system(){return new a}static fixed(t){return new a(new Date(t))}static offset(t,e){return new a(new Date(t.millis()+e))}#e;constructor(t){this.#e=t}date(){return this.#e?new Date(this.#e):new Date}millis(){return this.date().getTime()}}class w{static create(t,e){return new w(t,e)}#e;#t;#s;#n;constructor(t,e){this.#e=t,this.#t=e,this.#s=[],this.#n=s=>this.#s.push(s),this.#e.addEventListener(this.#t,this.#n)}get events(){return this.#s}clear(){const t=[...this.#s];return this.#s.length=0,t}stop(){this.#e.removeEventListener(this.#t,this.#n)}async waitFor(t=1){return new Promise(e=>{const s=()=>{this.#s.length>=t&&(this.#e.removeEventListener(this.#t,s),e(this.events))};this.#e.addEventListener(this.#t,s),s()})}}class f{isSuccess=!0}class S{isSuccess=!1;errorMessage;constructor(t){this.errorMessage=t}}class o{static create(t,e){return new o(t,e)}static mapObject(t,e){const r=Object.entries(t).map(([l,i])=>{const g=e===void 0?void 0:`${e}: ${l}`;return[l,o.create(i,g)]});return Object.fromEntries(r)}#e;#t;constructor(t,e){this.#e=e==null?"":` in ${e}`,this.#t=Array.isArray(t)?[...t]:t}next(){const t=Array.isArray(this.#t)?this.#t.shift():this.#t;if(t===void 0)throw new Error(`No more responses configured${this.#e}.`);return t}}class u{static create(t,e){return new u(t,e)}#e;#t;#s;#n;constructor(t,e){this.#e=t,this.#t=e,this.#s=[],this.#n=s=>this.#s.push(s.detail),this.#e.addEventListener(this.#t,this.#n)}get data(){return this.#s}clear(){const t=[...this.#s];return this.#s.length=0,t}stop(){this.#e.removeEventListener(this.#t,this.#n)}}const h="message";class m extends EventTarget{log(...t){this.dispatchEvent(new CustomEvent(h,{detail:{level:"log",message:t}}))}error(...t){this.dispatchEvent(new CustomEvent(h,{detail:{level:"error",message:t}}))}warn(...t){this.dispatchEvent(new CustomEvent(h,{detail:{level:"warn",message:t}}))}info(...t){this.dispatchEvent(new CustomEvent(h,{detail:{level:"info",message:t}}))}debug(...t){this.dispatchEvent(new CustomEvent(h,{detail:{level:"debug",message:t}}))}trace(...t){this.dispatchEvent(new CustomEvent(h,{detail:{level:"trace",message:t}}))}trackMessages(){return new u(this,h)}}function b(c){const t=o.create(c);return async function(){const e=t.next();if(e instanceof Error)throw e;return new C(e)}}class C{#e;#t;#s;constructor({status:t,statusText:e,body:s=null}){this.#e=t,this.#t=e,this.#s=s}get ok(){return this.status>=200&&this.status<300}get status(){return this.#e}get statusText(){return this.#t}async blob(){if(this.#s==null)return null;if(this.#s instanceof Blob)return this.#s;throw new TypeError("Body is not a Blob.")}async json(){const t=typeof this.#s=="string"?this.#s:JSON.stringify(this.#s);return Promise.resolve(JSON.parse(t))}async text(){return this.#s==null?"":String(this.#s)}}class E extends EventTarget{static create(){return new E(EventSource)}static createNull(){return new E(d)}#e;#t;constructor(t){super(),this.#e=t}get isConnected(){return this.#t?.readyState===this.#e.OPEN}get url(){return this.#t?.url}async connect(t,e="message",...s){await new Promise((r,l)=>{if(this.isConnected){l(new Error("Already connected."));return}try{this.#t=new this.#e(t),this.#t.addEventListener("open",i=>{this.#s(i),r()}),this.#t.addEventListener(e,i=>this.#n(i));for(const i of s)this.#t.addEventListener(i,g=>this.#n(g));this.#t.addEventListener("error",i=>this.#r(i))}catch(i){l(i)}})}send(t,e){throw new Error("Method not implemented.")}async close(){await new Promise((t,e)=>{if(!this.isConnected){t();return}try{this.#t.close(),t()}catch(s){e(s)}})}simulateMessage(t,e="message",s){typeof t!="string"&&(t=JSON.stringify(t)),this.#n(new MessageEvent(e,{data:t,lastEventId:s}))}simulateError(){this.#r(new Event("error"))}#s(t){this.dispatchEvent(new Event(t.type,t))}#n(t){this.dispatchEvent(new MessageEvent(t.type,t))}#r(t){this.dispatchEvent(new Event(t.type,t))}}class d extends EventTarget{static CONNECTING=0;static OPEN=1;static CLOSED=2;url;readyState=d.CONNECTING;constructor(t){super(),this.url=t.toString(),setTimeout(()=>{this.readyState=d.OPEN,this.dispatchEvent(new Event("open"))},0)}close(){this.readyState=d.CLOSED}}const p="heartbeat",y="message-sent";class v extends EventTarget{static create({heartbeat:t=3e4,retry:e=1e3}={}){return new v(t,e,WebSocket)}static createNull({heartbeat:t=0,retry:e=0}={}){return new v(t,e,T)}#e;#t;#s;#n;#r;#i;constructor(t,e,s){super(),this.#e=t,this.#t=e,this.#s=s}get isConnected(){return this.#n?.readyState===WebSocket.OPEN}get url(){return this.#n?.url}async connect(t){await new Promise((e,s)=>{if(this.#o(),this.isConnected){s(new Error("Already connected."));return}try{this.#n=new this.#s(t),this.#n.addEventListener("open",r=>{this.#d(r),e()}),this.#n.addEventListener("message",r=>this.#a(r)),this.#n.addEventListener("close",r=>this.#h(r)),this.#n.addEventListener("error",r=>this.#c(r))}catch(r){s(r)}})}async send(t){if(!this.isConnected)throw new Error("Not connected.");this.#n.send(t),this.dispatchEvent(new CustomEvent(y,{detail:t})),await Promise.resolve()}trackMessageSent(){return u.create(this,y)}async close(t,e){await new Promise(s=>{if(this.#o(),!this.isConnected){s();return}this.#n.addEventListener("close",()=>s()),this.#n.close(t,e)})}simulateMessage(t){typeof t!="string"&&(t=JSON.stringify(t)),this.#a(new MessageEvent("message",{data:t}))}simulateHeartbeat(){this.#u()}simulateClose(t,e){this.#h(new CloseEvent("close",{code:t,reason:e}))}simulateError(){this.#n?.close(),this.#c(new Event("error"))}#d(t){this.dispatchEvent(new Event(t.type,t)),this.#E()}#a(t){this.dispatchEvent(new MessageEvent(t.type,t))}#h(t){this.#v(),this.dispatchEvent(new CloseEvent(t.type,t))}#c(t){this.dispatchEvent(new Event(t.type,t)),this.#l()}#l(){this.#t<=0||(this.#i=setInterval(()=>this.connect(this.#n.url),this.#t))}#o(){clearInterval(this.#i),this.#i=void 0}#E(){this.#e<=0||(this.#r=setInterval(()=>this.#u(),this.#e))}#v(){clearInterval(this.#r),this.#r=void 0}#u(){this.#r!=null&&this.send(p)}}class T extends EventTarget{url;readyState=WebSocket.CONNECTING;constructor(t){super(),this.url=t.toString(),setTimeout(()=>{this.readyState=WebSocket.OPEN,this.dispatchEvent(new Event("open"))},0)}send(){}close(){this.readyState=WebSocket.CLOSED,this.dispatchEvent(new Event("close"))}}n.Clock=a,n.ConfigurableResponses=o,n.ConsoleStub=m,n.EventTracker=w,n.Failure=S,n.HEARTBEAT_TYPE=p,n.OutputTracker=u,n.SseClient=E,n.Success=f,n.WebSocketClient=v,n.createFetchStub=b,Object.defineProperty(n,Symbol.toStringTag,{value:"Module"})}));
2
2
  //# sourceMappingURL=shared.umd.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"shared.umd.cjs","sources":["../src/common/clock.ts","../src/common/configurable_responses.ts","../src/common/output_tracker.ts","../src/domain/messages.ts","../src/infrastructure/console_stub.ts","../src/infrastructure/fetch_stub.ts","../src/infrastructure/sse_client.ts","../src/infrastructure/web_socket_client.ts"],"sourcesContent":["// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.\n\n/**\n * A clock provides access to the current timestamp.\n */\nexport class Clock {\n /**\n * Create a clock using system the clock.\n *\n * @return A clock that uses the system clock.\n */\n static system(): Clock {\n return new Clock();\n }\n\n /**\n * Create a clock using a fixed date.\n *\n * @param date The fixed date of the clock.\n * @return A clock that always returns a fixed date.\n */\n static fixed(date: Date | string | number): Clock {\n return new Clock(new Date(date));\n }\n\n /**\n * Create a clock that returns a fixed offset from the given clock.\n *\n * @param clock The clock to offset from.\n * @param offsetMillis The offset in milliseconds.\n * @return A clock that returns a fixed offset from the given clock.\n */\n static offset(clock: Clock, offsetMillis: number): Clock {\n return new Clock(new Date(clock.millis() + offsetMillis));\n }\n\n readonly #date?: Date;\n\n private constructor(date?: Date) {\n this.#date = date;\n }\n\n /**\n * Return the current timestamp of the clock.\n *\n * @return The current timestamp.\n */\n date(): Date {\n return this.#date ? new Date(this.#date) : new Date();\n }\n\n /**\n * Return the current timestamp of the clock in milliseconds.\n *\n * @return The current timestamp in milliseconds.\n */\n millis(): number {\n return this.date().getTime();\n }\n}\n","// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.\n// Copyright 2023 Titanium I.T. LLC. MIT License.\n\n/**\n * Handle returning pre-configured responses.\n *\n * This is one of the nullability patterns from James Shore's article on\n * [testing without mocks](https://www.jamesshore.com/v2/projects/nullables/testing-without-mocks#configurable-responses).\n *\n * Example usage for stubbing `fetch` function:\n *\n * ```javascript\n * function createFetchStub(responses) {\n * const configurableResponses = ConfigurableResponses.create(responses);\n * return async function () {\n * const response = configurableResponses.next();\n * return {\n * status: response.status,\n * json: async () => response.body,\n * };\n * };\n * }\n * ```\n */\nexport class ConfigurableResponses<T = unknown> {\n /**\n * Create a list of responses (by providing an array), or a single repeating\n * response (by providing any other type). 'Name' is optional and used in\n * error messages.\n *\n * @param responses A single response or an array of responses.\n * @param name An optional name for the responses.\n */\n static create<T>(responses?: T | T[], name?: string) {\n return new ConfigurableResponses<T>(responses, name);\n }\n\n /**\n * Convert all properties in an object into ConfigurableResponse instances.\n * For example, { a: 1 } becomes { a: ConfigurableResponses.create(1) }.\n * 'Name' is optional and used in error messages.\n *\n * @param responseObject An object with single response or an array of responses.\n * @param name An optional name for the responses.\n */\n static mapObject<T extends Record<string, unknown>>(\n responseObject: T,\n name?: string,\n ) {\n const entries = Object.entries(responseObject);\n const translatedEntries = entries.map(([key, value]) => {\n const translatedName = name === undefined ? undefined : `${name}: ${key}`;\n return [key, ConfigurableResponses.create(value, translatedName)];\n });\n return Object.fromEntries(translatedEntries);\n }\n\n readonly #description;\n readonly #responses;\n\n /**\n * Create a list of responses (by providing an array), or a single repeating\n * response (by providing any other type). 'Name' is optional and used in\n * error messages.\n *\n * @param responses A single response or an array of responses.\n * @param name An optional name for the responses.\n */\n constructor(responses?: T | T[], name?: string) {\n this.#description = name == null ? \"\" : ` in ${name}`;\n this.#responses = Array.isArray(responses) ? [...responses] : responses;\n }\n\n /**\n * Get the next configured response. Throws an error when configured with a list\n * of responses and no more responses remain.\n *\n * @return The next response.\n */\n next(): T {\n const response = Array.isArray(this.#responses)\n ? this.#responses.shift()\n : this.#responses;\n if (response === undefined) {\n throw new Error(`No more responses configured${this.#description}.`);\n }\n\n return response;\n }\n}\n","// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.\n// Copyright 2020-2022 Titanium I.T. LLC. MIT License.\n\n/**\n * Track output events.\n *\n * This is one of the nullability patterns from James Shore's article on\n * [testing without mocks](https://www.jamesshore.com/v2/projects/nullables/testing-without-mocks#output-tracking).\n *\n * Example implementation of an event store:\n *\n * ```javascript\n * async record(event) {\n * // ...\n * this.dispatchEvent(new CustomEvent(\"eventRecorded\", { detail: event }));\n * }\n *\n * trackEventsRecorded() {\n * return new OutputTracker(this, \"eventRecorded\");\n * }\n * ```\n *\n * Example usage:\n *\n * ```javascript\n * const eventsRecorded = eventStore.trackEventsRecorded();\n * // ...\n * const data = eventsRecorded.data(); // [event1, event2, ...]\n * ```\n */\nexport class OutputTracker<T = unknown> {\n /**\n * Create a tracker for a specific event of an event target.\n *\n * @param eventTarget The target to track.\n * @param event The event name to track.\n */\n static create<T>(eventTarget: EventTarget, event: string) {\n return new OutputTracker<T>(eventTarget, event);\n }\n\n readonly #eventTarget;\n readonly #event;\n readonly #data: T[];\n readonly #tracker;\n\n /**\n * Create a tracker for a specific event of an event target.\n *\n * @param eventTarget The target to track.\n * @param event The event name to track.\n */\n constructor(eventTarget: EventTarget, event: string) {\n this.#eventTarget = eventTarget;\n this.#event = event;\n this.#data = [];\n this.#tracker = (event: Event) =>\n this.#data.push((event as CustomEvent<T>).detail);\n\n this.#eventTarget.addEventListener(this.#event, this.#tracker);\n }\n\n /**\n * Return the tracked data.\n *\n * @return The tracked data.\n */\n get data(): T[] {\n return this.#data;\n }\n\n /**\n * Clear the tracked data and return the cleared data.\n *\n * @return The cleared data.\n */\n clear(): T[] {\n const result = [...this.#data];\n this.#data.length = 0;\n return result;\n }\n\n /**\n * Stop tracking.\n */\n stop() {\n this.#eventTarget.removeEventListener(this.#event, this.#tracker);\n }\n}\n","// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.\n\n/**\n * Provides CQNS features.\n *\n * The Command Query Notification Separation principle is a software design\n * principle that separates the concerns of commands, queries, and\n * notifications.\n *\n * Message hierarchy:\n *\n * - Message\n * - Incoming / outgoing\n * - Request (outgoing) -> response (incoming)\n * - Command -> command status\n * - Query -> query result\n * - Notification\n * - Incoming: notification -> commands\n * - Outgoing\n * - Event (internal)\n *\n * @see https://ralfw.de/command-query-notification-separation-cqns/\n * @module\n */\n\n/**\n * The status returned by a command handler.\n */\nexport type CommandStatus = Success | Failure;\n\n/**\n * A successful status.\n */\nexport class Success {\n readonly isSuccess = true;\n}\n\n/**\n * A failed status.\n */\nexport class Failure {\n readonly isSuccess = false;\n errorMessage: string;\n\n /**\n * Creates a failed status.\n *\n * @param errorMessage\n */\n constructor(errorMessage: string) {\n this.errorMessage = errorMessage;\n }\n}\n","// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.\n\nimport { OutputTracker } from \"../common/output_tracker\";\n\nconst MESSAGE_EVENT = \"message\";\n\n/**\n * A stub for the console interface.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Console_API\n */\nexport class ConsoleStub extends EventTarget {\n log(...data: unknown[]) {\n this.dispatchEvent(\n new CustomEvent(MESSAGE_EVENT, {\n detail: { level: \"log\", message: data },\n }),\n );\n }\n\n error(...data: unknown[]) {\n this.dispatchEvent(\n new CustomEvent(MESSAGE_EVENT, {\n detail: { level: \"error\", message: data },\n }),\n );\n }\n\n warn(...data: unknown[]) {\n this.dispatchEvent(\n new CustomEvent(MESSAGE_EVENT, {\n detail: { level: \"warn\", message: data },\n }),\n );\n }\n\n info(...data: unknown[]) {\n this.dispatchEvent(\n new CustomEvent(MESSAGE_EVENT, {\n detail: { level: \"info\", message: data },\n }),\n );\n }\n\n debug(...data: unknown[]) {\n this.dispatchEvent(\n new CustomEvent(MESSAGE_EVENT, {\n detail: { level: \"debug\", message: data },\n }),\n );\n }\n\n trace(...data: unknown[]) {\n this.dispatchEvent(\n new CustomEvent(MESSAGE_EVENT, {\n detail: { level: \"trace\", message: data },\n }),\n );\n }\n\n /**\n * Track the console messages.\n */\n trackMessages() {\n return new OutputTracker(this, MESSAGE_EVENT);\n }\n}\n","// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.\n\nimport { ConfigurableResponses } from \"../common/configurable_responses\";\n\n/**\n * This data object configures the response of a fetch stub call.\n */\nexport interface ResponseData {\n /** The HTTP status code. */\n status: number;\n\n /** The HTTP status text. */\n statusText: string;\n\n /** The optional response body. */\n body?: Blob | object | string | null;\n}\n\n/**\n * Create a fetch stub.\n *\n * The stub returns a response from the provided response data or throws an provided error.\n *\n * @param responses A single response or an array of responses.\n * @returns The fetch stub.\n */\nexport function createFetchStub(\n responses?: ResponseData | Error | (ResponseData | Error)[],\n): () => Promise<Response> {\n const configurableResponses = ConfigurableResponses.create(responses);\n return async function () {\n const response = configurableResponses.next();\n if (response instanceof Error) {\n throw response;\n }\n\n return new ResponseStub(response) as unknown as Response;\n };\n}\n\nclass ResponseStub {\n #status: number;\n #statusText: string;\n #body?: Blob | object | string | null;\n\n constructor({ status, statusText, body = null }: ResponseData) {\n this.#status = status;\n this.#statusText = statusText;\n this.#body = body;\n }\n\n get ok() {\n return this.status >= 200 && this.status < 300;\n }\n\n get status() {\n return this.#status;\n }\n\n get statusText() {\n return this.#statusText;\n }\n\n async blob() {\n if (this.#body == null) {\n return null;\n }\n\n if (this.#body instanceof Blob) {\n return this.#body;\n }\n\n throw new TypeError(\"Body is not a Blob.\");\n }\n\n async json() {\n const json =\n typeof this.#body === \"string\" ? this.#body : JSON.stringify(this.#body);\n return Promise.resolve(JSON.parse(json));\n }\n\n async text() {\n if (this.#body == null) {\n return \"\";\n }\n\n return String(this.#body);\n }\n}\n","// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.\n\nimport type { MessageClient } from \"./message_client\";\n\n/**\n * A client for the server-sent events protocol.\n */\nexport class SseClient extends EventTarget implements MessageClient {\n /**\n * Create an SSE client.\n *\n * @return A new SSE client.\n */\n static create(): SseClient {\n return new SseClient(EventSource);\n }\n\n /**\n * Create a nulled SSE client.\n *\n * @return A new SSE client.\n */\n static createNull(): SseClient {\n return new SseClient(EventSourceStub as typeof EventSource);\n }\n\n readonly #eventSourceConstructor: typeof EventSource;\n\n #eventSource?: EventSource;\n\n private constructor(eventSourceConstructor: typeof EventSource) {\n super();\n this.#eventSourceConstructor = eventSourceConstructor;\n }\n\n get isConnected(): boolean {\n return this.#eventSource?.readyState === this.#eventSourceConstructor.OPEN;\n }\n\n get url(): string | undefined {\n return this.#eventSource?.url;\n }\n\n async connect(\n url: string | URL,\n eventName = \"message\",\n ...otherEvents: string[]\n ): Promise<void> {\n await new Promise<void>((resolve, reject) => {\n if (this.isConnected) {\n reject(new Error(\"Already connected.\"));\n return;\n }\n\n try {\n this.#eventSource = new this.#eventSourceConstructor(url);\n this.#eventSource.addEventListener(\"open\", (e) => {\n this.#handleOpen(e);\n resolve();\n });\n this.#eventSource.addEventListener(eventName, (e) =>\n this.#handleMessage(e),\n );\n for (const otherEvent of otherEvents) {\n this.#eventSource.addEventListener(otherEvent, (e) =>\n this.#handleMessage(e),\n );\n }\n this.#eventSource.addEventListener(\"error\", (e) =>\n this.#handleError(e),\n );\n } catch (error) {\n reject(error);\n }\n });\n }\n\n send(_message: string, _type?: string): Promise<void> {\n throw new Error(\"Method not implemented.\");\n }\n\n async close(): Promise<void> {\n await new Promise<void>((resolve, reject) => {\n if (!this.isConnected) {\n resolve();\n return;\n }\n\n try {\n this.#eventSource!.close();\n resolve();\n } catch (error) {\n reject(error);\n }\n });\n }\n\n /**\n * Simulate a message event from the server.\n *\n * @param message The message to receive.\n * @param eventName The optional event type.\n * @param lastEventId The optional last event ID.\n */\n simulateMessage(\n message: string | number | boolean | object | null,\n eventName = \"message\",\n lastEventId?: string,\n ) {\n if (typeof message !== \"string\") {\n message = JSON.stringify(message);\n }\n this.#handleMessage(\n new MessageEvent(eventName, { data: message, lastEventId }),\n );\n }\n\n /**\n * Simulate an error event.\n */\n simulateError() {\n this.#handleError(new Event(\"error\"));\n }\n\n #handleOpen(event: Event) {\n this.dispatchEvent(new Event(event.type, event));\n }\n\n #handleMessage(event: MessageEvent) {\n this.dispatchEvent(\n new MessageEvent(event.type, event as unknown as MessageEventInit),\n );\n }\n\n #handleError(event: Event) {\n this.dispatchEvent(new Event(event.type, event));\n }\n}\n\nclass EventSourceStub extends EventTarget {\n // The constants have to be defined here because Node.js support is currently\n // experimental only.\n static CONNECTING = 0;\n static OPEN = 1;\n static CLOSED = 2;\n\n url: string;\n readyState = EventSourceStub.CONNECTING;\n\n constructor(url: string | URL) {\n super();\n this.url = url.toString();\n setTimeout(() => {\n this.readyState = EventSourceStub.OPEN;\n this.dispatchEvent(new Event(\"open\"));\n }, 0);\n }\n\n close() {\n this.readyState = EventSourceStub.CLOSED;\n }\n}\n","// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.\n\nimport { OutputTracker } from \"../common/output_tracker\";\nimport type { MessageClient } from \"./message_client\";\n\nexport const HEARTBEAT_TYPE = \"heartbeat\";\n\nconst MESSAGE_SENT_EVENT = \"message-sent\";\n\n/**\n * Options for the WebSocket client.\n */\nexport interface WebSocketOptions {\n /**\n * The heartbeat interval in milliseconds. A value <= 0 disables the\n * heartbeat.\n */\n heartbeat?: number;\n\n /**\n * The time in milliseconds to wait before retrying a connection after an\n * error. A value <= 0 disables automatic retries.\n */\n retry?: number;\n}\n\n/**\n * A client for the WebSocket protocol.\n */\nexport class WebSocketClient extends EventTarget implements MessageClient {\n /**\n * Create a WebSocket client.\n *\n * @param options The options for the WebSocket client.\n * @return A new WebSocket client.\n */\n static create({\n heartbeat = 30000,\n retry = 1000,\n }: WebSocketOptions = {}): WebSocketClient {\n return new WebSocketClient(heartbeat, retry, WebSocket);\n }\n\n /**\n * Create a nulled WebSocket client.\n *\n * @param options The options for the WebSocket client.\n * @return A new nulled WebSocket client.\n */\n static createNull({ heartbeat = 0, retry = 0 }: WebSocketOptions = {}) {\n return new WebSocketClient(\n heartbeat,\n retry,\n WebSocketStub as unknown as typeof WebSocket,\n );\n }\n\n readonly #heartbeat: number;\n readonly #retry: number;\n readonly #webSocketConstructor: typeof WebSocket;\n\n #webSocket?: WebSocket;\n #heartbeatId?: ReturnType<typeof setTimeout>;\n #retryId?: ReturnType<typeof setTimeout>;\n\n private constructor(\n heartbeat: number,\n retry: number,\n webSocketConstructor: typeof WebSocket,\n ) {\n super();\n this.#heartbeat = heartbeat;\n this.#retry = retry;\n this.#webSocketConstructor = webSocketConstructor;\n }\n\n get isConnected(): boolean {\n return this.#webSocket?.readyState === WebSocket.OPEN;\n }\n\n get url(): string | undefined {\n return this.#webSocket?.url;\n }\n\n async connect(url: string | URL): Promise<void> {\n await new Promise<void>((resolve, reject) => {\n this.#stopRetry();\n\n if (this.isConnected) {\n reject(new Error(\"Already connected.\"));\n return;\n }\n\n try {\n this.#webSocket = new this.#webSocketConstructor(url);\n this.#webSocket.addEventListener(\"open\", (e) => {\n this.#handleOpen(e);\n resolve();\n });\n this.#webSocket.addEventListener(\"message\", (e) =>\n this.#handleMessage(e),\n );\n this.#webSocket.addEventListener(\"close\", (e) => this.#handleClose(e));\n this.#webSocket.addEventListener(\"error\", (e) => this.#handleError(e));\n } catch (error) {\n reject(error);\n }\n });\n }\n\n async send(\n message: string | ArrayBuffer | Blob | ArrayBufferView,\n ): Promise<void> {\n if (!this.isConnected) {\n throw new Error(\"Not connected.\");\n }\n\n this.#webSocket!.send(message);\n this.dispatchEvent(\n new CustomEvent(MESSAGE_SENT_EVENT, { detail: message }),\n );\n await Promise.resolve();\n }\n\n /**\n * Return a tracker for messages sent.\n *\n * @return A new output tracker.\n */\n trackMessageSent(): OutputTracker<string> {\n return OutputTracker.create(this, MESSAGE_SENT_EVENT);\n }\n\n /**\n * Close the connection.\n *\n * If a code is provided, also a reason should be provided.\n *\n * @param code An optional code.\n * @param reason An optional reason.\n */\n async close(code?: number, reason?: string): Promise<void> {\n await new Promise<void>((resolve) => {\n this.#stopRetry();\n\n if (!this.isConnected) {\n resolve();\n return;\n }\n\n this.#webSocket!.addEventListener(\"close\", () => resolve());\n this.#webSocket!.close(code, reason);\n });\n }\n\n /**\n * Simulate a message event from the server.\n *\n * @param message The message to receive.\n */\n simulateMessage(message: string | number | boolean | object | null) {\n if (typeof message !== \"string\") {\n message = JSON.stringify(message);\n }\n this.#handleMessage(new MessageEvent(\"message\", { data: message }));\n }\n\n /**\n * Simulate a heartbeat.\n */\n simulateHeartbeat() {\n this.#sendHeartbeat();\n }\n\n /**\n * Simulate a close event.\n *\n * @param code An optional code.\n * @param reason An optional reason.\n */\n simulateClose(code?: number, reason?: string) {\n this.#handleClose(new CloseEvent(\"close\", { code, reason }));\n }\n\n /**\n * Simulate an error event.\n */\n simulateError() {\n this.#webSocket?.close();\n this.#handleError(new Event(\"error\"));\n }\n\n #handleOpen(event: Event) {\n this.dispatchEvent(new Event(event.type, event));\n this.#startHeartbeat();\n }\n\n #handleMessage(event: MessageEvent) {\n this.dispatchEvent(\n new MessageEvent(event.type, event as unknown as MessageEventInit),\n );\n }\n\n #handleClose(event: CloseEvent) {\n this.#stopHeartbeat();\n this.dispatchEvent(new CloseEvent(event.type, event));\n }\n\n #handleError(event: Event) {\n this.dispatchEvent(new Event(event.type, event));\n this.#startRetry();\n }\n\n #startRetry() {\n if (this.#retry <= 0) {\n return;\n }\n this.#retryId = setInterval(\n () => this.connect(this.#webSocket!.url),\n this.#retry,\n );\n }\n\n #stopRetry() {\n clearInterval(this.#retryId);\n this.#retryId = undefined;\n }\n\n #startHeartbeat() {\n if (this.#heartbeat <= 0) {\n return;\n }\n\n this.#heartbeatId = setInterval(\n () => this.#sendHeartbeat(),\n this.#heartbeat,\n );\n }\n\n #stopHeartbeat() {\n clearInterval(this.#heartbeatId);\n this.#heartbeatId = undefined;\n }\n\n #sendHeartbeat() {\n if (this.#heartbeatId == null) {\n return;\n }\n\n void this.send(HEARTBEAT_TYPE);\n }\n}\n\nclass WebSocketStub extends EventTarget {\n url: string;\n readyState: number = WebSocket.CONNECTING;\n\n constructor(url: string | URL) {\n super();\n this.url = url.toString();\n setTimeout(() => {\n this.readyState = WebSocket.OPEN;\n this.dispatchEvent(new Event(\"open\"));\n }, 0);\n }\n\n send() {}\n\n close() {\n this.readyState = WebSocket.CLOSED;\n this.dispatchEvent(new Event(\"close\"));\n }\n}\n"],"names":["Clock","date","clock","offsetMillis","#date","ConfigurableResponses","responses","name","responseObject","translatedEntries","key","value","translatedName","#description","#responses","response","OutputTracker","eventTarget","event","#eventTarget","#event","#data","#tracker","result","Success","Failure","errorMessage","MESSAGE_EVENT","ConsoleStub","data","createFetchStub","configurableResponses","ResponseStub","#status","#statusText","#body","status","statusText","body","json","SseClient","EventSourceStub","#eventSourceConstructor","#eventSource","eventSourceConstructor","url","eventName","otherEvents","resolve","reject","e","#handleOpen","#handleMessage","otherEvent","#handleError","error","_message","_type","message","lastEventId","HEARTBEAT_TYPE","MESSAGE_SENT_EVENT","WebSocketClient","heartbeat","retry","WebSocketStub","#heartbeat","#retry","#webSocketConstructor","#webSocket","#heartbeatId","#retryId","webSocketConstructor","#stopRetry","#handleClose","code","reason","#sendHeartbeat","#startHeartbeat","#stopHeartbeat","#startRetry"],"mappings":"+NAKO,MAAMA,CAAM,CAMjB,OAAO,QAAgB,CACrB,OAAO,IAAIA,CACb,CAQA,OAAO,MAAMC,EAAqC,CAChD,OAAO,IAAID,EAAM,IAAI,KAAKC,CAAI,CAAC,CACjC,CASA,OAAO,OAAOC,EAAcC,EAA6B,CACvD,OAAO,IAAIH,EAAM,IAAI,KAAKE,EAAM,OAAA,EAAWC,CAAY,CAAC,CAC1D,CAESC,GAED,YAAYH,EAAa,CAC/B,KAAKG,GAAQH,CACf,CAOA,MAAa,CACX,OAAO,KAAKG,GAAQ,IAAI,KAAK,KAAKA,EAAK,MAAQ,IACjD,CAOA,QAAiB,CACf,OAAO,KAAK,KAAA,EAAO,QAAA,CACrB,CACF,CCnCO,MAAMC,CAAmC,CAS9C,OAAO,OAAUC,EAAqBC,EAAe,CACnD,OAAO,IAAIF,EAAyBC,EAAWC,CAAI,CACrD,CAUA,OAAO,UACLC,EACAD,EACA,CAEA,MAAME,EADU,OAAO,QAAQD,CAAc,EACX,IAAI,CAAC,CAACE,EAAKC,CAAK,IAAM,CACtD,MAAMC,EAAiBL,IAAS,OAAY,OAAY,GAAGA,CAAI,KAAKG,CAAG,GACvE,MAAO,CAACA,EAAKL,EAAsB,OAAOM,EAAOC,CAAc,CAAC,CAClE,CAAC,EACD,OAAO,OAAO,YAAYH,CAAiB,CAC7C,CAESI,GACAC,GAUT,YAAYR,EAAqBC,EAAe,CAC9C,KAAKM,GAAeN,GAAQ,KAAO,GAAK,OAAOA,CAAI,GACnD,KAAKO,GAAa,MAAM,QAAQR,CAAS,EAAI,CAAC,GAAGA,CAAS,EAAIA,CAChE,CAQA,MAAU,CACR,MAAMS,EAAW,MAAM,QAAQ,KAAKD,EAAU,EAC1C,KAAKA,GAAW,MAAA,EAChB,KAAKA,GACT,GAAIC,IAAa,OACf,MAAM,IAAI,MAAM,+BAA+B,KAAKF,EAAY,GAAG,EAGrE,OAAOE,CACT,CACF,CC3DO,MAAMC,CAA2B,CAOtC,OAAO,OAAUC,EAA0BC,EAAe,CACxD,OAAO,IAAIF,EAAiBC,EAAaC,CAAK,CAChD,CAESC,GACAC,GACAC,GACAC,GAQT,YAAYL,EAA0BC,EAAe,CACnD,KAAKC,GAAeF,EACpB,KAAKG,GAASF,EACd,KAAKG,GAAQ,CAAA,EACb,KAAKC,GAAYJ,GACf,KAAKG,GAAM,KAAMH,EAAyB,MAAM,EAElD,KAAKC,GAAa,iBAAiB,KAAKC,GAAQ,KAAKE,EAAQ,CAC/D,CAOA,IAAI,MAAY,CACd,OAAO,KAAKD,EACd,CAOA,OAAa,CACX,MAAME,EAAS,CAAC,GAAG,KAAKF,EAAK,EAC7B,YAAKA,GAAM,OAAS,EACbE,CACT,CAKA,MAAO,CACL,KAAKJ,GAAa,oBAAoB,KAAKC,GAAQ,KAAKE,EAAQ,CAClE,CACF,CCvDO,MAAME,CAAQ,CACV,UAAY,EACvB,CAKO,MAAMC,CAAQ,CACV,UAAY,GACrB,aAOA,YAAYC,EAAsB,CAChC,KAAK,aAAeA,CACtB,CACF,CChDA,MAAMC,EAAgB,UAOf,MAAMC,UAAoB,WAAY,CAC3C,OAAOC,EAAiB,CACtB,KAAK,cACH,IAAI,YAAYF,EAAe,CAC7B,OAAQ,CAAE,MAAO,MAAO,QAASE,CAAA,CAAK,CACvC,CAAA,CAEL,CAEA,SAASA,EAAiB,CACxB,KAAK,cACH,IAAI,YAAYF,EAAe,CAC7B,OAAQ,CAAE,MAAO,QAAS,QAASE,CAAA,CAAK,CACzC,CAAA,CAEL,CAEA,QAAQA,EAAiB,CACvB,KAAK,cACH,IAAI,YAAYF,EAAe,CAC7B,OAAQ,CAAE,MAAO,OAAQ,QAASE,CAAA,CAAK,CACxC,CAAA,CAEL,CAEA,QAAQA,EAAiB,CACvB,KAAK,cACH,IAAI,YAAYF,EAAe,CAC7B,OAAQ,CAAE,MAAO,OAAQ,QAASE,CAAA,CAAK,CACxC,CAAA,CAEL,CAEA,SAASA,EAAiB,CACxB,KAAK,cACH,IAAI,YAAYF,EAAe,CAC7B,OAAQ,CAAE,MAAO,QAAS,QAASE,CAAA,CAAK,CACzC,CAAA,CAEL,CAEA,SAASA,EAAiB,CACxB,KAAK,cACH,IAAI,YAAYF,EAAe,CAC7B,OAAQ,CAAE,MAAO,QAAS,QAASE,CAAA,CAAK,CACzC,CAAA,CAEL,CAKA,eAAgB,CACd,OAAO,IAAIb,EAAc,KAAMW,CAAa,CAC9C,CACF,CCxCO,SAASG,EACdxB,EACyB,CACzB,MAAMyB,EAAwB1B,EAAsB,OAAOC,CAAS,EACpE,OAAO,gBAAkB,CACvB,MAAMS,EAAWgB,EAAsB,KAAA,EACvC,GAAIhB,aAAoB,MACtB,MAAMA,EAGR,OAAO,IAAIiB,EAAajB,CAAQ,CAClC,CACF,CAEA,MAAMiB,CAAa,CACjBC,GACAC,GACAC,GAEA,YAAY,CAAE,OAAAC,EAAQ,WAAAC,EAAY,KAAAC,EAAO,MAAsB,CAC7D,KAAKL,GAAUG,EACf,KAAKF,GAAcG,EACnB,KAAKF,GAAQG,CACf,CAEA,IAAI,IAAK,CACP,OAAO,KAAK,QAAU,KAAO,KAAK,OAAS,GAC7C,CAEA,IAAI,QAAS,CACX,OAAO,KAAKL,EACd,CAEA,IAAI,YAAa,CACf,OAAO,KAAKC,EACd,CAEA,MAAM,MAAO,CACX,GAAI,KAAKC,IAAS,KAChB,OAAO,KAGT,GAAI,KAAKA,cAAiB,KACxB,OAAO,KAAKA,GAGd,MAAM,IAAI,UAAU,qBAAqB,CAC3C,CAEA,MAAM,MAAO,CACX,MAAMI,EACJ,OAAO,KAAKJ,IAAU,SAAW,KAAKA,GAAQ,KAAK,UAAU,KAAKA,EAAK,EACzE,OAAO,QAAQ,QAAQ,KAAK,MAAMI,CAAI,CAAC,CACzC,CAEA,MAAM,MAAO,CACX,OAAI,KAAKJ,IAAS,KACT,GAGF,OAAO,KAAKA,EAAK,CAC1B,CACF,CCjFO,MAAMK,UAAkB,WAAqC,CAMlE,OAAO,QAAoB,CACzB,OAAO,IAAIA,EAAU,WAAW,CAClC,CAOA,OAAO,YAAwB,CAC7B,OAAO,IAAIA,EAAUC,CAAqC,CAC5D,CAESC,GAETC,GAEQ,YAAYC,EAA4C,CAC9D,MAAA,EACA,KAAKF,GAA0BE,CACjC,CAEA,IAAI,aAAuB,CACzB,OAAO,KAAKD,IAAc,aAAe,KAAKD,GAAwB,IACxE,CAEA,IAAI,KAA0B,CAC5B,OAAO,KAAKC,IAAc,GAC5B,CAEA,MAAM,QACJE,EACAC,EAAY,aACTC,EACY,CACf,MAAM,IAAI,QAAc,CAACC,EAASC,IAAW,CAC3C,GAAI,KAAK,YAAa,CACpBA,EAAO,IAAI,MAAM,oBAAoB,CAAC,EACtC,MACF,CAEA,GAAI,CACF,KAAKN,GAAe,IAAI,KAAKD,GAAwBG,CAAG,EACxD,KAAKF,GAAa,iBAAiB,OAASO,GAAM,CAChD,KAAKC,GAAYD,CAAC,EAClBF,EAAA,CACF,CAAC,EACD,KAAKL,GAAa,iBAAiBG,EAAYI,GAC7C,KAAKE,GAAeF,CAAC,CAAA,EAEvB,UAAWG,KAAcN,EACvB,KAAKJ,GAAa,iBAAiBU,EAAaH,GAC9C,KAAKE,GAAeF,CAAC,CAAA,EAGzB,KAAKP,GAAa,iBAAiB,QAAUO,GAC3C,KAAKI,GAAaJ,CAAC,CAAA,CAEvB,OAASK,EAAO,CACdN,EAAOM,CAAK,CACd,CACF,CAAC,CACH,CAEA,KAAKC,EAAkBC,EAA+B,CACpD,MAAM,IAAI,MAAM,yBAAyB,CAC3C,CAEA,MAAM,OAAuB,CAC3B,MAAM,IAAI,QAAc,CAACT,EAASC,IAAW,CAC3C,GAAI,CAAC,KAAK,YAAa,CACrBD,EAAA,EACA,MACF,CAEA,GAAI,CACF,KAAKL,GAAc,MAAA,EACnBK,EAAA,CACF,OAASO,EAAO,CACdN,EAAOM,CAAK,CACd,CACF,CAAC,CACH,CASA,gBACEG,EACAZ,EAAY,UACZa,EACA,CACI,OAAOD,GAAY,WACrBA,EAAU,KAAK,UAAUA,CAAO,GAElC,KAAKN,GACH,IAAI,aAAaN,EAAW,CAAE,KAAMY,EAAS,YAAAC,EAAa,CAAA,CAE9D,CAKA,eAAgB,CACd,KAAKL,GAAa,IAAI,MAAM,OAAO,CAAC,CACtC,CAEAH,GAAYjC,EAAc,CACxB,KAAK,cAAc,IAAI,MAAMA,EAAM,KAAMA,CAAK,CAAC,CACjD,CAEAkC,GAAelC,EAAqB,CAClC,KAAK,cACH,IAAI,aAAaA,EAAM,KAAMA,CAAoC,CAAA,CAErE,CAEAoC,GAAapC,EAAc,CACzB,KAAK,cAAc,IAAI,MAAMA,EAAM,KAAMA,CAAK,CAAC,CACjD,CACF,CAEA,MAAMuB,UAAwB,WAAY,CAGxC,OAAO,WAAa,EACpB,OAAO,KAAO,EACd,OAAO,OAAS,EAEhB,IACA,WAAaA,EAAgB,WAE7B,YAAYI,EAAmB,CAC7B,MAAA,EACA,KAAK,IAAMA,EAAI,SAAA,EACf,WAAW,IAAM,CACf,KAAK,WAAaJ,EAAgB,KAClC,KAAK,cAAc,IAAI,MAAM,MAAM,CAAC,CACtC,EAAG,CAAC,CACN,CAEA,OAAQ,CACN,KAAK,WAAaA,EAAgB,MACpC,CACF,CC5JO,MAAMmB,EAAiB,YAExBC,EAAqB,eAsBpB,MAAMC,UAAwB,WAAqC,CAOxE,OAAO,OAAO,CACZ,UAAAC,EAAY,IACZ,MAAAC,EAAQ,GAAA,EACY,GAAqB,CACzC,OAAO,IAAIF,EAAgBC,EAAWC,EAAO,SAAS,CACxD,CAQA,OAAO,WAAW,CAAE,UAAAD,EAAY,EAAG,MAAAC,EAAQ,CAAA,EAAwB,GAAI,CACrE,OAAO,IAAIF,EACTC,EACAC,EACAC,CAAA,CAEJ,CAESC,GACAC,GACAC,GAETC,GACAC,GACAC,GAEQ,YACNR,EACAC,EACAQ,EACA,CACA,MAAA,EACA,KAAKN,GAAaH,EAClB,KAAKI,GAASH,EACd,KAAKI,GAAwBI,CAC/B,CAEA,IAAI,aAAuB,CACzB,OAAO,KAAKH,IAAY,aAAe,UAAU,IACnD,CAEA,IAAI,KAA0B,CAC5B,OAAO,KAAKA,IAAY,GAC1B,CAEA,MAAM,QAAQxB,EAAkC,CAC9C,MAAM,IAAI,QAAc,CAACG,EAASC,IAAW,CAG3C,GAFA,KAAKwB,GAAA,EAED,KAAK,YAAa,CACpBxB,EAAO,IAAI,MAAM,oBAAoB,CAAC,EACtC,MACF,CAEA,GAAI,CACF,KAAKoB,GAAa,IAAI,KAAKD,GAAsBvB,CAAG,EACpD,KAAKwB,GAAW,iBAAiB,OAASnB,GAAM,CAC9C,KAAKC,GAAYD,CAAC,EAClBF,EAAA,CACF,CAAC,EACD,KAAKqB,GAAW,iBAAiB,UAAYnB,GAC3C,KAAKE,GAAeF,CAAC,CAAA,EAEvB,KAAKmB,GAAW,iBAAiB,QAAUnB,GAAM,KAAKwB,GAAaxB,CAAC,CAAC,EACrE,KAAKmB,GAAW,iBAAiB,QAAUnB,GAAM,KAAKI,GAAaJ,CAAC,CAAC,CACvE,OAASK,EAAO,CACdN,EAAOM,CAAK,CACd,CACF,CAAC,CACH,CAEA,MAAM,KACJG,EACe,CACf,GAAI,CAAC,KAAK,YACR,MAAM,IAAI,MAAM,gBAAgB,EAGlC,KAAKW,GAAY,KAAKX,CAAO,EAC7B,KAAK,cACH,IAAI,YAAYG,EAAoB,CAAE,OAAQH,EAAS,CAAA,EAEzD,MAAM,QAAQ,QAAA,CAChB,CAOA,kBAA0C,CACxC,OAAO1C,EAAc,OAAO,KAAM6C,CAAkB,CACtD,CAUA,MAAM,MAAMc,EAAeC,EAAgC,CACzD,MAAM,IAAI,QAAe5B,GAAY,CAGnC,GAFA,KAAKyB,GAAA,EAED,CAAC,KAAK,YAAa,CACrBzB,EAAA,EACA,MACF,CAEA,KAAKqB,GAAY,iBAAiB,QAAS,IAAMrB,GAAS,EAC1D,KAAKqB,GAAY,MAAMM,EAAMC,CAAM,CACrC,CAAC,CACH,CAOA,gBAAgBlB,EAAoD,CAC9D,OAAOA,GAAY,WACrBA,EAAU,KAAK,UAAUA,CAAO,GAElC,KAAKN,GAAe,IAAI,aAAa,UAAW,CAAE,KAAMM,CAAA,CAAS,CAAC,CACpE,CAKA,mBAAoB,CAClB,KAAKmB,GAAA,CACP,CAQA,cAAcF,EAAeC,EAAiB,CAC5C,KAAKF,GAAa,IAAI,WAAW,QAAS,CAAE,KAAAC,EAAM,OAAAC,CAAA,CAAQ,CAAC,CAC7D,CAKA,eAAgB,CACd,KAAKP,IAAY,MAAA,EACjB,KAAKf,GAAa,IAAI,MAAM,OAAO,CAAC,CACtC,CAEAH,GAAYjC,EAAc,CACxB,KAAK,cAAc,IAAI,MAAMA,EAAM,KAAMA,CAAK,CAAC,EAC/C,KAAK4D,GAAA,CACP,CAEA1B,GAAelC,EAAqB,CAClC,KAAK,cACH,IAAI,aAAaA,EAAM,KAAMA,CAAoC,CAAA,CAErE,CAEAwD,GAAaxD,EAAmB,CAC9B,KAAK6D,GAAA,EACL,KAAK,cAAc,IAAI,WAAW7D,EAAM,KAAMA,CAAK,CAAC,CACtD,CAEAoC,GAAapC,EAAc,CACzB,KAAK,cAAc,IAAI,MAAMA,EAAM,KAAMA,CAAK,CAAC,EAC/C,KAAK8D,GAAA,CACP,CAEAA,IAAc,CACR,KAAKb,IAAU,IAGnB,KAAKI,GAAW,YACd,IAAM,KAAK,QAAQ,KAAKF,GAAY,GAAG,EACvC,KAAKF,EAAA,EAET,CAEAM,IAAa,CACX,cAAc,KAAKF,EAAQ,EAC3B,KAAKA,GAAW,MAClB,CAEAO,IAAkB,CACZ,KAAKZ,IAAc,IAIvB,KAAKI,GAAe,YAClB,IAAM,KAAKO,GAAA,EACX,KAAKX,EAAA,EAET,CAEAa,IAAiB,CACf,cAAc,KAAKT,EAAY,EAC/B,KAAKA,GAAe,MACtB,CAEAO,IAAiB,CACX,KAAKP,IAAgB,MAIpB,KAAK,KAAKV,CAAc,CAC/B,CACF,CAEA,MAAMK,UAAsB,WAAY,CACtC,IACA,WAAqB,UAAU,WAE/B,YAAYpB,EAAmB,CAC7B,MAAA,EACA,KAAK,IAAMA,EAAI,SAAA,EACf,WAAW,IAAM,CACf,KAAK,WAAa,UAAU,KAC5B,KAAK,cAAc,IAAI,MAAM,MAAM,CAAC,CACtC,EAAG,CAAC,CACN,CAEA,MAAO,CAAC,CAER,OAAQ,CACN,KAAK,WAAa,UAAU,OAC5B,KAAK,cAAc,IAAI,MAAM,OAAO,CAAC,CACvC,CACF"}
1
+ {"version":3,"file":"shared.umd.cjs","sources":["../src/common/clock.ts","../src/common/event_tracker.ts","../src/domain/messages.ts","../src/infrastructure/configurable_responses.ts","../src/infrastructure/output_tracker.ts","../src/infrastructure/console_stub.ts","../src/infrastructure/fetch_stub.ts","../src/infrastructure/sse_client.ts","../src/infrastructure/web_socket_client.ts"],"sourcesContent":["// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.\n\n/**\n * A clock provides access to the current timestamp.\n */\nexport class Clock {\n /**\n * Create a clock using system the clock.\n *\n * @return A clock that uses the system clock.\n */\n static system(): Clock {\n return new Clock();\n }\n\n /**\n * Create a clock using a fixed date.\n *\n * @param date The fixed date of the clock.\n * @return A clock that always returns a fixed date.\n */\n static fixed(date: Date | string | number): Clock {\n return new Clock(new Date(date));\n }\n\n /**\n * Create a clock that returns a fixed offset from the given clock.\n *\n * @param clock The clock to offset from.\n * @param offsetMillis The offset in milliseconds.\n * @return A clock that returns a fixed offset from the given clock.\n */\n static offset(clock: Clock, offsetMillis: number): Clock {\n return new Clock(new Date(clock.millis() + offsetMillis));\n }\n\n readonly #date?: Date;\n\n private constructor(date?: Date) {\n this.#date = date;\n }\n\n /**\n * Return the current timestamp of the clock.\n *\n * @return The current timestamp.\n */\n date(): Date {\n return this.#date ? new Date(this.#date) : new Date();\n }\n\n /**\n * Return the current timestamp of the clock in milliseconds.\n *\n * @return The current timestamp in milliseconds.\n */\n millis(): number {\n return this.date().getTime();\n }\n}\n","// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.\n\n/**\n * Track events from an event target.\n *\n * Wait asynchronously for events. Useful in test code.\n */\nexport class EventTracker<T extends Event> {\n /**\n * Create a tracker for a specific event of an event target.\n *\n * @param eventTarget The target to track.\n * @param event The event name to track.\n */\n static create<T extends Event>(eventTarget: EventTarget, event: string) {\n return new EventTracker<T>(eventTarget, event);\n }\n\n readonly #eventTarget;\n readonly #event;\n readonly #events: T[];\n readonly #tracker;\n\n /**\n * Create a tracker for a specific event of an event target.\n *\n * @param eventTarget The target to track.\n * @param event The event name to track.\n */\n constructor(eventTarget: EventTarget, event: string) {\n this.#eventTarget = eventTarget;\n this.#event = event;\n this.#events = [];\n this.#tracker = (event: Event) => this.#events.push(event as T);\n\n this.#eventTarget.addEventListener(this.#event, this.#tracker);\n }\n\n /**\n * Return the tracked events.\n *\n * @return The tracked events.\n */\n get events(): T[] {\n return this.#events;\n }\n\n /**\n * Clear the tracked events and return the cleared events.\n *\n * @return The cleared events.\n */\n clear(): T[] {\n const result = [...this.#events];\n this.#events.length = 0;\n return result;\n }\n\n /**\n * Stop tracking.\n */\n stop() {\n this.#eventTarget.removeEventListener(this.#event, this.#tracker);\n }\n\n /**\n * Wait asynchronously for a number of events.\n *\n * @param count number of events, default 1.\n */\n async waitFor(count = 1) {\n return new Promise<T[]>((resolve) => {\n const checkEvents = () => {\n if (this.#events.length >= count) {\n this.#eventTarget.removeEventListener(this.#event, checkEvents);\n resolve(this.events);\n }\n };\n\n this.#eventTarget.addEventListener(this.#event, checkEvents);\n checkEvents();\n });\n }\n}\n","// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.\n\n/**\n * Provides CQNS features.\n *\n * The Command Query Notification Separation principle is a software design\n * principle that separates the concerns of commands, queries, and\n * notifications.\n *\n * Message hierarchy:\n *\n * - Message\n * - Incoming / outgoing\n * - Request (outgoing) -> response (incoming)\n * - Command -> command status\n * - Query -> query result\n * - Notification\n * - Incoming: notification -> commands\n * - Outgoing\n * - Event (internal)\n *\n * @see https://ralfw.de/command-query-notification-separation-cqns/\n * @module\n */\n\n/**\n * The status returned by a command handler.\n */\nexport type CommandStatus = Success | Failure;\n\n/**\n * A successful status.\n */\nexport class Success {\n readonly isSuccess = true;\n}\n\n/**\n * A failed status.\n */\nexport class Failure {\n readonly isSuccess = false;\n errorMessage: string;\n\n /**\n * Creates a failed status.\n *\n * @param errorMessage\n */\n constructor(errorMessage: string) {\n this.errorMessage = errorMessage;\n }\n}\n","// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.\n// Copyright 2023 Titanium I.T. LLC. MIT License.\n\n/**\n * Handle returning pre-configured responses.\n *\n * This is one of the nullability patterns from James Shore's article on\n * [testing without mocks](https://www.jamesshore.com/v2/projects/nullables/testing-without-mocks#configurable-responses).\n *\n * Example usage for stubbing `fetch` function:\n *\n * ```javascript\n * function createFetchStub(responses) {\n * const configurableResponses = ConfigurableResponses.create(responses);\n * return async function () {\n * const response = configurableResponses.next();\n * return {\n * status: response.status,\n * json: async () => response.body,\n * };\n * };\n * }\n * ```\n */\nexport class ConfigurableResponses<T = unknown> {\n /**\n * Create a list of responses (by providing an array), or a single repeating\n * response (by providing any other type). 'Name' is optional and used in\n * error messages.\n *\n * @param responses A single response or an array of responses.\n * @param name An optional name for the responses.\n */\n static create<T>(responses?: T | T[], name?: string) {\n return new ConfigurableResponses<T>(responses, name);\n }\n\n /**\n * Convert all properties in an object into ConfigurableResponse instances.\n * For example, { a: 1 } becomes { a: ConfigurableResponses.create(1) }.\n * 'Name' is optional and used in error messages.\n *\n * @param responseObject An object with single response or an array of responses.\n * @param name An optional name for the responses.\n */\n static mapObject<T extends Record<string, unknown>>(\n responseObject: T,\n name?: string,\n ) {\n const entries = Object.entries(responseObject);\n const translatedEntries = entries.map(([key, value]) => {\n const translatedName = name === undefined ? undefined : `${name}: ${key}`;\n return [key, ConfigurableResponses.create(value, translatedName)];\n });\n return Object.fromEntries(translatedEntries);\n }\n\n readonly #description;\n readonly #responses;\n\n /**\n * Create a list of responses (by providing an array), or a single repeating\n * response (by providing any other type). 'Name' is optional and used in\n * error messages.\n *\n * @param responses A single response or an array of responses.\n * @param name An optional name for the responses.\n */\n constructor(responses?: T | T[], name?: string) {\n this.#description = name == null ? \"\" : ` in ${name}`;\n this.#responses = Array.isArray(responses) ? [...responses] : responses;\n }\n\n /**\n * Get the next configured response. Throws an error when configured with a list\n * of responses and no more responses remain.\n *\n * @return The next response.\n */\n next(): T {\n const response = Array.isArray(this.#responses)\n ? this.#responses.shift()\n : this.#responses;\n if (response === undefined) {\n throw new Error(`No more responses configured${this.#description}.`);\n }\n\n return response;\n }\n}\n","// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.\n// Copyright 2020-2022 Titanium I.T. LLC. MIT License.\n\n/**\n * Track output events.\n *\n * This is one of the nullability patterns from James Shore's article on\n * [testing without mocks](https://www.jamesshore.com/v2/projects/nullables/testing-without-mocks#output-tracking).\n *\n * Example implementation of an event store:\n *\n * ```javascript\n * async record(event) {\n * // ...\n * this.dispatchEvent(new CustomEvent(\"eventRecorded\", { detail: event }));\n * }\n *\n * trackEventsRecorded() {\n * return new OutputTracker(this, \"eventRecorded\");\n * }\n * ```\n *\n * Example usage:\n *\n * ```javascript\n * const eventsRecorded = eventStore.trackEventsRecorded();\n * // ...\n * const data = eventsRecorded.data(); // [event1, event2, ...]\n * ```\n */\nexport class OutputTracker<T = unknown> {\n /**\n * Create a tracker for a specific event of an event target.\n *\n * @param eventTarget The target to track.\n * @param event The event name to track.\n */\n static create<T>(eventTarget: EventTarget, event: string) {\n return new OutputTracker<T>(eventTarget, event);\n }\n\n readonly #eventTarget;\n readonly #event;\n readonly #data: T[];\n readonly #tracker;\n\n /**\n * Create a tracker for a specific event of an event target.\n *\n * @param eventTarget The target to track.\n * @param event The event name to track.\n */\n constructor(eventTarget: EventTarget, event: string) {\n this.#eventTarget = eventTarget;\n this.#event = event;\n this.#data = [];\n this.#tracker = (event: Event) =>\n this.#data.push((event as CustomEvent<T>).detail);\n\n this.#eventTarget.addEventListener(this.#event, this.#tracker);\n }\n\n /**\n * Return the tracked data.\n *\n * @return The tracked data.\n */\n get data(): T[] {\n return this.#data;\n }\n\n /**\n * Clear the tracked data and return the cleared data.\n *\n * @return The cleared data.\n */\n clear(): T[] {\n const result = [...this.#data];\n this.#data.length = 0;\n return result;\n }\n\n /**\n * Stop tracking.\n */\n stop() {\n this.#eventTarget.removeEventListener(this.#event, this.#tracker);\n }\n}\n","// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.\n\nimport { OutputTracker } from \"./output_tracker\";\n\nconst MESSAGE_EVENT = \"message\";\n\nexport interface ConsoleMessage {\n level: \"log\" | \"error\" | \"warn\" | \"info\" | \"debug\" | \"trace\";\n message: unknown[];\n}\n\n/**\n * A stub for the console interface.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Console_API\n */\nexport class ConsoleStub extends EventTarget {\n log(...data: unknown[]) {\n this.dispatchEvent(\n new CustomEvent(MESSAGE_EVENT, {\n detail: { level: \"log\", message: data },\n }),\n );\n }\n\n error(...data: unknown[]) {\n this.dispatchEvent(\n new CustomEvent(MESSAGE_EVENT, {\n detail: { level: \"error\", message: data },\n }),\n );\n }\n\n warn(...data: unknown[]) {\n this.dispatchEvent(\n new CustomEvent(MESSAGE_EVENT, {\n detail: { level: \"warn\", message: data },\n }),\n );\n }\n\n info(...data: unknown[]) {\n this.dispatchEvent(\n new CustomEvent(MESSAGE_EVENT, {\n detail: { level: \"info\", message: data },\n }),\n );\n }\n\n debug(...data: unknown[]) {\n this.dispatchEvent(\n new CustomEvent(MESSAGE_EVENT, {\n detail: { level: \"debug\", message: data },\n }),\n );\n }\n\n trace(...data: unknown[]) {\n this.dispatchEvent(\n new CustomEvent(MESSAGE_EVENT, {\n detail: { level: \"trace\", message: data },\n }),\n );\n }\n\n /**\n * Track the console messages.\n */\n trackMessages() {\n return new OutputTracker<ConsoleMessage>(this, MESSAGE_EVENT);\n }\n}\n","// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.\n\nimport { ConfigurableResponses } from \"./configurable_responses\";\n\n/**\n * This data object configures the response of a fetch stub call.\n */\nexport interface ResponseData {\n /** The HTTP status code. */\n status: number;\n\n /** The HTTP status text. */\n statusText: string;\n\n /** The optional response body. */\n body?: Blob | object | string | null;\n}\n\n/**\n * Create a fetch stub.\n *\n * The stub returns a response from the provided response data or throws an provided error.\n *\n * @param responses A single response or an array of responses.\n * @returns The fetch stub.\n */\nexport function createFetchStub(\n responses?: ResponseData | Error | (ResponseData | Error)[],\n): () => Promise<Response> {\n const configurableResponses = ConfigurableResponses.create(responses);\n return async function () {\n const response = configurableResponses.next();\n if (response instanceof Error) {\n throw response;\n }\n\n return new ResponseStub(response) as unknown as Response;\n };\n}\n\nclass ResponseStub {\n #status: number;\n #statusText: string;\n #body?: Blob | object | string | null;\n\n constructor({ status, statusText, body = null }: ResponseData) {\n this.#status = status;\n this.#statusText = statusText;\n this.#body = body;\n }\n\n get ok() {\n return this.status >= 200 && this.status < 300;\n }\n\n get status() {\n return this.#status;\n }\n\n get statusText() {\n return this.#statusText;\n }\n\n async blob() {\n if (this.#body == null) {\n return null;\n }\n\n if (this.#body instanceof Blob) {\n return this.#body;\n }\n\n throw new TypeError(\"Body is not a Blob.\");\n }\n\n async json() {\n const json =\n typeof this.#body === \"string\" ? this.#body : JSON.stringify(this.#body);\n return Promise.resolve(JSON.parse(json));\n }\n\n async text() {\n if (this.#body == null) {\n return \"\";\n }\n\n return String(this.#body);\n }\n}\n","// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.\n\nimport type { MessageClient } from \"./message_client\";\n\n/**\n * A client for the server-sent events protocol.\n */\nexport class SseClient extends EventTarget implements MessageClient {\n /**\n * Create an SSE client.\n *\n * @return A new SSE client.\n */\n static create(): SseClient {\n return new SseClient(EventSource);\n }\n\n /**\n * Create a nulled SSE client.\n *\n * @return A new SSE client.\n */\n static createNull(): SseClient {\n return new SseClient(EventSourceStub as typeof EventSource);\n }\n\n readonly #eventSourceConstructor: typeof EventSource;\n\n #eventSource?: EventSource;\n\n private constructor(eventSourceConstructor: typeof EventSource) {\n super();\n this.#eventSourceConstructor = eventSourceConstructor;\n }\n\n get isConnected(): boolean {\n return this.#eventSource?.readyState === this.#eventSourceConstructor.OPEN;\n }\n\n get url(): string | undefined {\n return this.#eventSource?.url;\n }\n\n async connect(\n url: string | URL,\n eventName = \"message\",\n ...otherEvents: string[]\n ): Promise<void> {\n await new Promise<void>((resolve, reject) => {\n if (this.isConnected) {\n reject(new Error(\"Already connected.\"));\n return;\n }\n\n try {\n this.#eventSource = new this.#eventSourceConstructor(url);\n this.#eventSource.addEventListener(\"open\", (e) => {\n this.#handleOpen(e);\n resolve();\n });\n this.#eventSource.addEventListener(eventName, (e) =>\n this.#handleMessage(e),\n );\n for (const otherEvent of otherEvents) {\n this.#eventSource.addEventListener(otherEvent, (e) =>\n this.#handleMessage(e),\n );\n }\n this.#eventSource.addEventListener(\"error\", (e) =>\n this.#handleError(e),\n );\n } catch (error) {\n reject(error);\n }\n });\n }\n\n send(_message: string, _type?: string): Promise<void> {\n throw new Error(\"Method not implemented.\");\n }\n\n async close(): Promise<void> {\n await new Promise<void>((resolve, reject) => {\n if (!this.isConnected) {\n resolve();\n return;\n }\n\n try {\n this.#eventSource!.close();\n resolve();\n } catch (error) {\n reject(error);\n }\n });\n }\n\n /**\n * Simulate a message event from the server.\n *\n * @param message The message to receive.\n * @param eventName The optional event type.\n * @param lastEventId The optional last event ID.\n */\n simulateMessage(\n message: string | number | boolean | object | null,\n eventName = \"message\",\n lastEventId?: string,\n ) {\n if (typeof message !== \"string\") {\n message = JSON.stringify(message);\n }\n this.#handleMessage(\n new MessageEvent(eventName, { data: message, lastEventId }),\n );\n }\n\n /**\n * Simulate an error event.\n */\n simulateError() {\n this.#handleError(new Event(\"error\"));\n }\n\n #handleOpen(event: Event) {\n this.dispatchEvent(new Event(event.type, event));\n }\n\n #handleMessage(event: MessageEvent) {\n this.dispatchEvent(\n new MessageEvent(event.type, event as unknown as MessageEventInit),\n );\n }\n\n #handleError(event: Event) {\n this.dispatchEvent(new Event(event.type, event));\n }\n}\n\nclass EventSourceStub extends EventTarget {\n // The constants have to be defined here because Node.js support is currently\n // experimental only.\n static CONNECTING = 0;\n static OPEN = 1;\n static CLOSED = 2;\n\n url: string;\n readyState = EventSourceStub.CONNECTING;\n\n constructor(url: string | URL) {\n super();\n this.url = url.toString();\n setTimeout(() => {\n this.readyState = EventSourceStub.OPEN;\n this.dispatchEvent(new Event(\"open\"));\n }, 0);\n }\n\n close() {\n this.readyState = EventSourceStub.CLOSED;\n }\n}\n","// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.\n\nimport { OutputTracker } from \"./output_tracker\";\nimport type { MessageClient } from \"./message_client\";\n\nexport const HEARTBEAT_TYPE = \"heartbeat\";\n\nconst MESSAGE_SENT_EVENT = \"message-sent\";\n\n/**\n * Options for the WebSocket client.\n */\nexport interface WebSocketOptions {\n /**\n * The heartbeat interval in milliseconds. A value <= 0 disables the\n * heartbeat.\n */\n heartbeat?: number;\n\n /**\n * The time in milliseconds to wait before retrying a connection after an\n * error. A value <= 0 disables automatic retries.\n */\n retry?: number;\n}\n\n/**\n * A client for the WebSocket protocol.\n */\nexport class WebSocketClient extends EventTarget implements MessageClient {\n /**\n * Create a WebSocket client.\n *\n * @param options The options for the WebSocket client.\n * @return A new WebSocket client.\n */\n static create({\n heartbeat = 30000,\n retry = 1000,\n }: WebSocketOptions = {}): WebSocketClient {\n return new WebSocketClient(heartbeat, retry, WebSocket);\n }\n\n /**\n * Create a nulled WebSocket client.\n *\n * @param options The options for the WebSocket client.\n * @return A new nulled WebSocket client.\n */\n static createNull({ heartbeat = 0, retry = 0 }: WebSocketOptions = {}) {\n return new WebSocketClient(\n heartbeat,\n retry,\n WebSocketStub as unknown as typeof WebSocket,\n );\n }\n\n readonly #heartbeat: number;\n readonly #retry: number;\n readonly #webSocketConstructor: typeof WebSocket;\n\n #webSocket?: WebSocket;\n #heartbeatId?: ReturnType<typeof setTimeout>;\n #retryId?: ReturnType<typeof setTimeout>;\n\n private constructor(\n heartbeat: number,\n retry: number,\n webSocketConstructor: typeof WebSocket,\n ) {\n super();\n this.#heartbeat = heartbeat;\n this.#retry = retry;\n this.#webSocketConstructor = webSocketConstructor;\n }\n\n get isConnected(): boolean {\n return this.#webSocket?.readyState === WebSocket.OPEN;\n }\n\n get url(): string | undefined {\n return this.#webSocket?.url;\n }\n\n async connect(url: string | URL): Promise<void> {\n await new Promise<void>((resolve, reject) => {\n this.#stopRetry();\n\n if (this.isConnected) {\n reject(new Error(\"Already connected.\"));\n return;\n }\n\n try {\n this.#webSocket = new this.#webSocketConstructor(url);\n this.#webSocket.addEventListener(\"open\", (e) => {\n this.#handleOpen(e);\n resolve();\n });\n this.#webSocket.addEventListener(\"message\", (e) =>\n this.#handleMessage(e),\n );\n this.#webSocket.addEventListener(\"close\", (e) => this.#handleClose(e));\n this.#webSocket.addEventListener(\"error\", (e) => this.#handleError(e));\n } catch (error) {\n reject(error);\n }\n });\n }\n\n async send(\n message: string | ArrayBuffer | Blob | ArrayBufferView,\n ): Promise<void> {\n if (!this.isConnected) {\n throw new Error(\"Not connected.\");\n }\n\n this.#webSocket!.send(message);\n this.dispatchEvent(\n new CustomEvent(MESSAGE_SENT_EVENT, { detail: message }),\n );\n await Promise.resolve();\n }\n\n /**\n * Return a tracker for messages sent.\n *\n * @return A new output tracker.\n */\n trackMessageSent(): OutputTracker<string> {\n return OutputTracker.create(this, MESSAGE_SENT_EVENT);\n }\n\n /**\n * Close the connection.\n *\n * If a code is provided, also a reason should be provided.\n *\n * @param code An optional code.\n * @param reason An optional reason.\n */\n async close(code?: number, reason?: string): Promise<void> {\n await new Promise<void>((resolve) => {\n this.#stopRetry();\n\n if (!this.isConnected) {\n resolve();\n return;\n }\n\n this.#webSocket!.addEventListener(\"close\", () => resolve());\n this.#webSocket!.close(code, reason);\n });\n }\n\n /**\n * Simulate a message event from the server.\n *\n * @param message The message to receive.\n */\n simulateMessage(message: string | number | boolean | object | null) {\n if (typeof message !== \"string\") {\n message = JSON.stringify(message);\n }\n this.#handleMessage(new MessageEvent(\"message\", { data: message }));\n }\n\n /**\n * Simulate a heartbeat.\n */\n simulateHeartbeat() {\n this.#sendHeartbeat();\n }\n\n /**\n * Simulate a close event.\n *\n * @param code An optional code.\n * @param reason An optional reason.\n */\n simulateClose(code?: number, reason?: string) {\n this.#handleClose(new CloseEvent(\"close\", { code, reason }));\n }\n\n /**\n * Simulate an error event.\n */\n simulateError() {\n this.#webSocket?.close();\n this.#handleError(new Event(\"error\"));\n }\n\n #handleOpen(event: Event) {\n this.dispatchEvent(new Event(event.type, event));\n this.#startHeartbeat();\n }\n\n #handleMessage(event: MessageEvent) {\n this.dispatchEvent(\n new MessageEvent(event.type, event as unknown as MessageEventInit),\n );\n }\n\n #handleClose(event: CloseEvent) {\n this.#stopHeartbeat();\n this.dispatchEvent(new CloseEvent(event.type, event));\n }\n\n #handleError(event: Event) {\n this.dispatchEvent(new Event(event.type, event));\n this.#startRetry();\n }\n\n #startRetry() {\n if (this.#retry <= 0) {\n return;\n }\n this.#retryId = setInterval(\n () => this.connect(this.#webSocket!.url),\n this.#retry,\n );\n }\n\n #stopRetry() {\n clearInterval(this.#retryId);\n this.#retryId = undefined;\n }\n\n #startHeartbeat() {\n if (this.#heartbeat <= 0) {\n return;\n }\n\n this.#heartbeatId = setInterval(\n () => this.#sendHeartbeat(),\n this.#heartbeat,\n );\n }\n\n #stopHeartbeat() {\n clearInterval(this.#heartbeatId);\n this.#heartbeatId = undefined;\n }\n\n #sendHeartbeat() {\n if (this.#heartbeatId == null) {\n return;\n }\n\n void this.send(HEARTBEAT_TYPE);\n }\n}\n\nclass WebSocketStub extends EventTarget {\n url: string;\n readyState: number = WebSocket.CONNECTING;\n\n constructor(url: string | URL) {\n super();\n this.url = url.toString();\n setTimeout(() => {\n this.readyState = WebSocket.OPEN;\n this.dispatchEvent(new Event(\"open\"));\n }, 0);\n }\n\n send() {}\n\n close() {\n this.readyState = WebSocket.CLOSED;\n this.dispatchEvent(new Event(\"close\"));\n }\n}\n"],"names":["Clock","date","clock","offsetMillis","#date","EventTracker","eventTarget","event","#eventTarget","#event","#events","#tracker","result","count","resolve","checkEvents","Success","Failure","errorMessage","ConfigurableResponses","responses","name","responseObject","translatedEntries","key","value","translatedName","#description","#responses","response","OutputTracker","#data","MESSAGE_EVENT","ConsoleStub","data","createFetchStub","configurableResponses","ResponseStub","#status","#statusText","#body","status","statusText","body","json","SseClient","EventSourceStub","#eventSourceConstructor","#eventSource","eventSourceConstructor","url","eventName","otherEvents","reject","e","#handleOpen","#handleMessage","otherEvent","#handleError","error","_message","_type","message","lastEventId","HEARTBEAT_TYPE","MESSAGE_SENT_EVENT","WebSocketClient","heartbeat","retry","WebSocketStub","#heartbeat","#retry","#webSocketConstructor","#webSocket","#heartbeatId","#retryId","webSocketConstructor","#stopRetry","#handleClose","code","reason","#sendHeartbeat","#startHeartbeat","#stopHeartbeat","#startRetry"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@muspellheim/shared",
3
- "version": "0.11.0",
3
+ "version": "0.12.6",
4
4
  "author": "Falko Schumann",
5
5
  "license": "MIT",
6
6
  "engines": {
@@ -25,19 +25,19 @@
25
25
  "test": "vitest"
26
26
  },
27
27
  "devDependencies": {
28
- "@eslint/js": "9.36.0",
29
- "@types/node": "22.18.6",
30
- "@vitest/coverage-v8": "3.2.4",
31
- "eslint": "9.36.0",
28
+ "@eslint/js": "9.38.0",
29
+ "@types/node": "22.18.11",
30
+ "@vitest/coverage-v8": "4.0.1",
31
+ "eslint": "9.38.0",
32
32
  "eslint-plugin-headers": "1.3.3",
33
- "jsdom": "27.0.0",
33
+ "jsdom": "27.0.1",
34
34
  "globals": "16.4.0",
35
35
  "prettier": "3.6.2",
36
- "typedoc": "0.28.13",
37
- "typescript": "5.9.2",
38
- "typescript-eslint": "8.44.0",
39
- "vite": "7.1.6",
40
- "vitest": "3.2.4",
36
+ "typedoc": "0.28.14",
37
+ "typescript": "5.8.2",
38
+ "typescript-eslint": "8.46.2",
39
+ "vite": "7.1.11",
40
+ "vitest": "4.0.1",
41
41
  "vite-plugin-dts": "4.5.4"
42
42
  }
43
43
  }
@@ -0,0 +1,84 @@
1
+ // Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.
2
+
3
+ /**
4
+ * Track events from an event target.
5
+ *
6
+ * Wait asynchronously for events. Useful in test code.
7
+ */
8
+ export class EventTracker<T extends Event> {
9
+ /**
10
+ * Create a tracker for a specific event of an event target.
11
+ *
12
+ * @param eventTarget The target to track.
13
+ * @param event The event name to track.
14
+ */
15
+ static create<T extends Event>(eventTarget: EventTarget, event: string) {
16
+ return new EventTracker<T>(eventTarget, event);
17
+ }
18
+
19
+ readonly #eventTarget;
20
+ readonly #event;
21
+ readonly #events: T[];
22
+ readonly #tracker;
23
+
24
+ /**
25
+ * Create a tracker for a specific event of an event target.
26
+ *
27
+ * @param eventTarget The target to track.
28
+ * @param event The event name to track.
29
+ */
30
+ constructor(eventTarget: EventTarget, event: string) {
31
+ this.#eventTarget = eventTarget;
32
+ this.#event = event;
33
+ this.#events = [];
34
+ this.#tracker = (event: Event) => this.#events.push(event as T);
35
+
36
+ this.#eventTarget.addEventListener(this.#event, this.#tracker);
37
+ }
38
+
39
+ /**
40
+ * Return the tracked events.
41
+ *
42
+ * @return The tracked events.
43
+ */
44
+ get events(): T[] {
45
+ return this.#events;
46
+ }
47
+
48
+ /**
49
+ * Clear the tracked events and return the cleared events.
50
+ *
51
+ * @return The cleared events.
52
+ */
53
+ clear(): T[] {
54
+ const result = [...this.#events];
55
+ this.#events.length = 0;
56
+ return result;
57
+ }
58
+
59
+ /**
60
+ * Stop tracking.
61
+ */
62
+ stop() {
63
+ this.#eventTarget.removeEventListener(this.#event, this.#tracker);
64
+ }
65
+
66
+ /**
67
+ * Wait asynchronously for a number of events.
68
+ *
69
+ * @param count number of events, default 1.
70
+ */
71
+ async waitFor(count = 1) {
72
+ return new Promise<T[]>((resolve) => {
73
+ const checkEvents = () => {
74
+ if (this.#events.length >= count) {
75
+ this.#eventTarget.removeEventListener(this.#event, checkEvents);
76
+ resolve(this.events);
77
+ }
78
+ };
79
+
80
+ this.#eventTarget.addEventListener(this.#event, checkEvents);
81
+ checkEvents();
82
+ });
83
+ }
84
+ }
package/src/common/mod.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  // Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.
2
2
 
3
3
  export * from "./clock";
4
- export * from "./configurable_responses";
4
+ export * from "./event_tracker";
5
5
  export * from "./log";
6
- export * from "./output_tracker";
@@ -1,9 +1,14 @@
1
1
  // Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.
2
2
 
3
- import { OutputTracker } from "../common/output_tracker";
3
+ import { OutputTracker } from "./output_tracker";
4
4
 
5
5
  const MESSAGE_EVENT = "message";
6
6
 
7
+ export interface ConsoleMessage {
8
+ level: "log" | "error" | "warn" | "info" | "debug" | "trace";
9
+ message: unknown[];
10
+ }
11
+
7
12
  /**
8
13
  * A stub for the console interface.
9
14
  *
@@ -62,6 +67,6 @@ export class ConsoleStub extends EventTarget {
62
67
  * Track the console messages.
63
68
  */
64
69
  trackMessages() {
65
- return new OutputTracker(this, MESSAGE_EVENT);
70
+ return new OutputTracker<ConsoleMessage>(this, MESSAGE_EVENT);
66
71
  }
67
72
  }
@@ -1,6 +1,6 @@
1
1
  // Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.
2
2
 
3
- import { ConfigurableResponses } from "../common/configurable_responses";
3
+ import { ConfigurableResponses } from "./configurable_responses";
4
4
 
5
5
  /**
6
6
  * This data object configures the response of a fetch stub call.
@@ -1,7 +1,9 @@
1
1
  // Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.
2
2
 
3
+ export * from "./configurable_responses";
3
4
  export * from "./console_stub";
4
5
  export * from "./fetch_stub";
5
6
  export * from "./message_client";
7
+ export * from "./output_tracker";
6
8
  export * from "./sse_client";
7
9
  export * from "./web_socket_client";
@@ -1,6 +1,6 @@
1
1
  // Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.
2
2
 
3
- import { OutputTracker } from "../common/output_tracker";
3
+ import { OutputTracker } from "./output_tracker";
4
4
  import type { MessageClient } from "./message_client";
5
5
 
6
6
  export const HEARTBEAT_TYPE = "heartbeat";