@muspellheim/shared 0.18.1 → 0.19.1
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/lib.cjs +4 -0
- package/dist/lib.cjs.map +18 -0
- package/dist/lib.js +4 -0
- package/dist/lib.js.map +18 -0
- package/package.json +30 -15
- package/src/common/clock.ts +60 -0
- package/src/common/event_tracker.ts +92 -0
- package/src/common/log.ts +27 -0
- package/src/common/mod.ts +5 -0
- package/src/domain/messages.ts +58 -0
- package/src/domain/mod.ts +3 -0
- package/src/infrastructure/configurable_responses.ts +90 -0
- package/src/infrastructure/console_log.ts +160 -0
- package/src/infrastructure/fetch_stub.ts +51 -0
- package/src/infrastructure/message_client.ts +50 -0
- package/src/infrastructure/mod.ts +9 -0
- package/src/infrastructure/output_tracker.ts +89 -0
- package/src/infrastructure/sse_client.ts +161 -0
- package/src/infrastructure/web_socket_client.ts +279 -0
- package/src/lib.ts +5 -0
- package/dist/mod.cjs +0 -556
- package/dist/mod.js +0 -524
- /package/dist/types/{mod.d.ts → lib.d.ts} +0 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.
|
|
2
|
+
// Copyright 2023 Titanium I.T. LLC. MIT License.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Handle returning pre-configured responses.
|
|
6
|
+
*
|
|
7
|
+
* This is one of the nullability patterns from James Shore's article on
|
|
8
|
+
* [testing without mocks](https://www.jamesshore.com/v2/projects/nullables/testing-without-mocks#configurable-responses).
|
|
9
|
+
*
|
|
10
|
+
* Example usage for stubbing `fetch` function:
|
|
11
|
+
*
|
|
12
|
+
* ```javascript
|
|
13
|
+
* function createFetchStub(responses) {
|
|
14
|
+
* const configurableResponses = ConfigurableResponses.create(responses);
|
|
15
|
+
* return async function () {
|
|
16
|
+
* const response = configurableResponses.next();
|
|
17
|
+
* return {
|
|
18
|
+
* status: response.status,
|
|
19
|
+
* json: async () => response.body,
|
|
20
|
+
* };
|
|
21
|
+
* };
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export class ConfigurableResponses<T = unknown> {
|
|
26
|
+
/**
|
|
27
|
+
* Create a list of responses (by providing an array), or a single repeating
|
|
28
|
+
* response (by providing any other type). 'Name' is optional and used in
|
|
29
|
+
* error messages.
|
|
30
|
+
*
|
|
31
|
+
* @param responses A single response or an array of responses.
|
|
32
|
+
* @param name An optional name for the responses.
|
|
33
|
+
*/
|
|
34
|
+
static create<T>(responses?: T | T[], name?: string) {
|
|
35
|
+
return new ConfigurableResponses<T>(responses, name);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Convert all properties in an object into ConfigurableResponse instances.
|
|
40
|
+
* For example, { a: 1 } becomes { a: ConfigurableResponses.create(1) }.
|
|
41
|
+
* 'Name' is optional and used in error messages.
|
|
42
|
+
*
|
|
43
|
+
* @param responseObject An object with single response or an array of responses.
|
|
44
|
+
* @param name An optional name for the responses.
|
|
45
|
+
*/
|
|
46
|
+
static mapObject<T extends Record<string, unknown>>(
|
|
47
|
+
responseObject: T,
|
|
48
|
+
name?: string,
|
|
49
|
+
): Record<string, ConfigurableResponses> {
|
|
50
|
+
const entries = Object.entries(responseObject);
|
|
51
|
+
const translatedEntries = entries.map(([key, value]) => {
|
|
52
|
+
const translatedName = name === undefined ? undefined : `${name}: ${key}`;
|
|
53
|
+
return [key, ConfigurableResponses.create(value, translatedName)];
|
|
54
|
+
});
|
|
55
|
+
return Object.fromEntries(translatedEntries);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
readonly #description;
|
|
59
|
+
readonly #responses;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create a list of responses (by providing an array), or a single repeating
|
|
63
|
+
* response (by providing any other type). 'Name' is optional and used in
|
|
64
|
+
* error messages.
|
|
65
|
+
*
|
|
66
|
+
* @param responses A single response or an array of responses.
|
|
67
|
+
* @param name An optional name for the responses.
|
|
68
|
+
*/
|
|
69
|
+
constructor(responses?: T | T[], name?: string) {
|
|
70
|
+
this.#description = name == null ? "" : ` in ${name}`;
|
|
71
|
+
this.#responses = Array.isArray(responses) ? [...responses] : responses;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get the next configured response. Throws an error when configured with a list
|
|
76
|
+
* of responses and no more responses remain.
|
|
77
|
+
*
|
|
78
|
+
* @return The next response.
|
|
79
|
+
*/
|
|
80
|
+
next(): T {
|
|
81
|
+
const response = Array.isArray(this.#responses)
|
|
82
|
+
? this.#responses.shift()
|
|
83
|
+
: this.#responses;
|
|
84
|
+
if (response === undefined) {
|
|
85
|
+
throw new Error(`No more responses configured${this.#description}.`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return response;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Copyright (c) 2026 Falko Schumann. All rights reserved. MIT license.
|
|
2
|
+
|
|
3
|
+
import type { Log, LogLevel } from "../common/mod";
|
|
4
|
+
import { OutputTracker } from "./output_tracker";
|
|
5
|
+
|
|
6
|
+
const MESSAGE_EVENT = "message";
|
|
7
|
+
|
|
8
|
+
export interface ConsoleMessage {
|
|
9
|
+
level: LogLevel;
|
|
10
|
+
message: unknown[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Wraps the console interface and allow setting the log level.
|
|
15
|
+
*
|
|
16
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/Console_API
|
|
17
|
+
*/
|
|
18
|
+
export class ConsoleLog extends EventTarget implements Log {
|
|
19
|
+
static create({ name }: { name?: string } = {}) {
|
|
20
|
+
return new ConsoleLog(globalThis.console, name);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static createNull({ name }: { name?: string } = {}) {
|
|
24
|
+
return new ConsoleLog(new ConsoleStub(), name);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
name?: string;
|
|
28
|
+
level: LogLevel = "info";
|
|
29
|
+
|
|
30
|
+
#console;
|
|
31
|
+
|
|
32
|
+
private constructor(console: Log, name?: string) {
|
|
33
|
+
super();
|
|
34
|
+
this.name = name;
|
|
35
|
+
this.#console = console;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
log(...data: unknown[]) {
|
|
39
|
+
if (!this.isLoggable("log")) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
data = this.#applyName(data);
|
|
44
|
+
this.#console.log(...data);
|
|
45
|
+
this.dispatchEvent(
|
|
46
|
+
new CustomEvent(MESSAGE_EVENT, {
|
|
47
|
+
detail: { level: "log", message: data },
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
error(...data: unknown[]) {
|
|
53
|
+
if (!this.isLoggable("error")) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
data = this.#applyName(data);
|
|
58
|
+
this.#console.error(...data);
|
|
59
|
+
this.dispatchEvent(
|
|
60
|
+
new CustomEvent(MESSAGE_EVENT, {
|
|
61
|
+
detail: { level: "error", message: data },
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
warn(...data: unknown[]) {
|
|
67
|
+
if (!this.isLoggable("warn")) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
data = this.#applyName(data);
|
|
72
|
+
this.#console.warn(...data);
|
|
73
|
+
this.dispatchEvent(
|
|
74
|
+
new CustomEvent(MESSAGE_EVENT, {
|
|
75
|
+
detail: { level: "warn", message: data },
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
info(...data: unknown[]) {
|
|
81
|
+
if (!this.isLoggable("info")) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
data = this.#applyName(data);
|
|
86
|
+
this.#console.info(...data);
|
|
87
|
+
this.dispatchEvent(
|
|
88
|
+
new CustomEvent(MESSAGE_EVENT, {
|
|
89
|
+
detail: { level: "info", message: data },
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
debug(...data: unknown[]) {
|
|
95
|
+
if (!this.isLoggable("debug")) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
data = this.#applyName(data);
|
|
100
|
+
this.#console.debug(...data);
|
|
101
|
+
this.dispatchEvent(
|
|
102
|
+
new CustomEvent(MESSAGE_EVENT, {
|
|
103
|
+
detail: { level: "debug", message: data },
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
trace(...data: unknown[]) {
|
|
109
|
+
if (!this.isLoggable("trace")) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
data = this.#applyName(data);
|
|
114
|
+
this.#console.trace(...data);
|
|
115
|
+
this.dispatchEvent(
|
|
116
|
+
new CustomEvent(MESSAGE_EVENT, {
|
|
117
|
+
detail: { level: "trace", message: data },
|
|
118
|
+
}),
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Track the console messages.
|
|
124
|
+
*/
|
|
125
|
+
trackMessages() {
|
|
126
|
+
return new OutputTracker<ConsoleMessage>(this, MESSAGE_EVENT);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
isLoggable(level: LogLevel) {
|
|
130
|
+
const normalize = (level: LogLevel) => (level === "log" ? "info" : level);
|
|
131
|
+
const levels: LogLevel[] = [
|
|
132
|
+
"off",
|
|
133
|
+
"error",
|
|
134
|
+
"warn",
|
|
135
|
+
"info",
|
|
136
|
+
"debug",
|
|
137
|
+
"trace",
|
|
138
|
+
];
|
|
139
|
+
const currentLevelIndex = levels.indexOf(normalize(this.level));
|
|
140
|
+
const messageLevelIndex = levels.indexOf(normalize(level));
|
|
141
|
+
return messageLevelIndex <= currentLevelIndex;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
#applyName(data: unknown[]) {
|
|
145
|
+
if (this.name == null) {
|
|
146
|
+
return data;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return [`${this.name}`, ...data];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
class ConsoleStub implements Log {
|
|
154
|
+
log(..._data: unknown[]) {}
|
|
155
|
+
error(..._data: unknown[]) {}
|
|
156
|
+
warn(..._data: unknown[]) {}
|
|
157
|
+
info(..._data: unknown[]) {}
|
|
158
|
+
debug(..._data: unknown[]) {}
|
|
159
|
+
trace(..._data: unknown[]) {}
|
|
160
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.
|
|
2
|
+
|
|
3
|
+
import { ConfigurableResponses } from "./configurable_responses";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* This data object configures the response of a fetch stub call.
|
|
7
|
+
*/
|
|
8
|
+
export interface ResponseData {
|
|
9
|
+
/** The HTTP status code. */
|
|
10
|
+
status: number;
|
|
11
|
+
|
|
12
|
+
/** The HTTP status text. */
|
|
13
|
+
statusText: string;
|
|
14
|
+
|
|
15
|
+
/** The optional response body. */
|
|
16
|
+
body?: BodyInit | object;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Create a fetch stub.
|
|
21
|
+
*
|
|
22
|
+
* The stub returns a response from the provided response data or throws an provided error.
|
|
23
|
+
*
|
|
24
|
+
* @param responses A single response or an array of responses.
|
|
25
|
+
* @returns The fetch stub.
|
|
26
|
+
*/
|
|
27
|
+
export function createFetchStub(
|
|
28
|
+
responses?: ResponseData | Error | (ResponseData | Error)[],
|
|
29
|
+
): () => Promise<Response> {
|
|
30
|
+
const configurableResponses = ConfigurableResponses.create(responses);
|
|
31
|
+
return async function () {
|
|
32
|
+
const response = configurableResponses.next();
|
|
33
|
+
if (response instanceof Error) {
|
|
34
|
+
throw response;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let body = response?.body;
|
|
38
|
+
if (
|
|
39
|
+
body != null &&
|
|
40
|
+
!(body instanceof Blob) &&
|
|
41
|
+
!(typeof body === "string")
|
|
42
|
+
) {
|
|
43
|
+
// If the body is an object, we convert it to a JSON string.
|
|
44
|
+
body = JSON.stringify(body);
|
|
45
|
+
}
|
|
46
|
+
return new Response(body, {
|
|
47
|
+
status: response.status,
|
|
48
|
+
statusText: response.statusText,
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* An interface for a streaming message client.
|
|
5
|
+
*
|
|
6
|
+
* Emits the following events:
|
|
7
|
+
*
|
|
8
|
+
* - open, {@link Event}
|
|
9
|
+
* - message, {@link MessageEvent}
|
|
10
|
+
* - error, {@link Event}
|
|
11
|
+
* - close, optional {@link CloseEvent}
|
|
12
|
+
*
|
|
13
|
+
* It is used for wrappers around {@link EventSource} and {@link WebSocket}.
|
|
14
|
+
*
|
|
15
|
+
* @see {@link SseClient}
|
|
16
|
+
* @see {@link WebSocketClient}
|
|
17
|
+
*/
|
|
18
|
+
export interface MessageClient extends EventTarget {
|
|
19
|
+
/**
|
|
20
|
+
* Return whether the client is connected.
|
|
21
|
+
*/
|
|
22
|
+
get isConnected(): boolean;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Return the server URL.
|
|
26
|
+
*/
|
|
27
|
+
get url(): string | undefined;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Connect to the server.
|
|
31
|
+
*
|
|
32
|
+
* @param url The server URL to connect to.
|
|
33
|
+
*/
|
|
34
|
+
connect(url: string | URL): Promise<void>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Send a message to the server.
|
|
38
|
+
*
|
|
39
|
+
* This is an optional method for streams with bidirectional communication.
|
|
40
|
+
*
|
|
41
|
+
* @param message The message to send.
|
|
42
|
+
* @param type The optional message type.
|
|
43
|
+
*/
|
|
44
|
+
send(message: string, type?: string): Promise<void>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Close the connection.
|
|
48
|
+
*/
|
|
49
|
+
close(): Promise<void>;
|
|
50
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.
|
|
2
|
+
|
|
3
|
+
export * from "./configurable_responses";
|
|
4
|
+
export * from "./console_log";
|
|
5
|
+
export * from "./fetch_stub";
|
|
6
|
+
export * from "./message_client";
|
|
7
|
+
export * from "./output_tracker";
|
|
8
|
+
export * from "./sse_client";
|
|
9
|
+
export * from "./web_socket_client";
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.
|
|
2
|
+
// Copyright 2020-2022 Titanium I.T. LLC. MIT License.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Track output events.
|
|
6
|
+
*
|
|
7
|
+
* This is one of the nullability patterns from James Shore's article on
|
|
8
|
+
* [testing without mocks](https://www.jamesshore.com/v2/projects/nullables/testing-without-mocks#output-tracking).
|
|
9
|
+
*
|
|
10
|
+
* Example implementation of an event store:
|
|
11
|
+
*
|
|
12
|
+
* ```javascript
|
|
13
|
+
* async record(event) {
|
|
14
|
+
* // ...
|
|
15
|
+
* this.dispatchEvent(new CustomEvent("eventRecorded", { detail: event }));
|
|
16
|
+
* }
|
|
17
|
+
*
|
|
18
|
+
* trackEventsRecorded() {
|
|
19
|
+
* return new OutputTracker(this, "eventRecorded");
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* Example usage:
|
|
24
|
+
*
|
|
25
|
+
* ```javascript
|
|
26
|
+
* const eventsRecorded = eventStore.trackEventsRecorded();
|
|
27
|
+
* // ...
|
|
28
|
+
* const data = eventsRecorded.data(); // [event1, event2, ...]
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export class OutputTracker<T = unknown> {
|
|
32
|
+
/**
|
|
33
|
+
* Create a tracker for a specific event of an event target.
|
|
34
|
+
*
|
|
35
|
+
* @param eventTarget The target to track.
|
|
36
|
+
* @param event The event name to track.
|
|
37
|
+
*/
|
|
38
|
+
static create<T>(eventTarget: EventTarget, event: string) {
|
|
39
|
+
return new OutputTracker<T>(eventTarget, event);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
readonly #eventTarget;
|
|
43
|
+
readonly #event;
|
|
44
|
+
readonly #data: T[];
|
|
45
|
+
readonly #tracker;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Create a tracker for a specific event of an event target.
|
|
49
|
+
*
|
|
50
|
+
* @param eventTarget The target to track.
|
|
51
|
+
* @param event The event name to track.
|
|
52
|
+
*/
|
|
53
|
+
constructor(eventTarget: EventTarget, event: string) {
|
|
54
|
+
this.#eventTarget = eventTarget;
|
|
55
|
+
this.#event = event;
|
|
56
|
+
this.#data = [];
|
|
57
|
+
this.#tracker = (event: Event) =>
|
|
58
|
+
this.#data.push((event as CustomEvent<T>).detail);
|
|
59
|
+
|
|
60
|
+
this.#eventTarget.addEventListener(this.#event, this.#tracker);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Return the tracked data.
|
|
65
|
+
*
|
|
66
|
+
* @return The tracked data.
|
|
67
|
+
*/
|
|
68
|
+
get data(): T[] {
|
|
69
|
+
return this.#data;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Clear the tracked data and return the cleared data.
|
|
74
|
+
*
|
|
75
|
+
* @return The cleared data.
|
|
76
|
+
*/
|
|
77
|
+
clear(): T[] {
|
|
78
|
+
const result = [...this.#data];
|
|
79
|
+
this.#data.length = 0;
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Stop tracking.
|
|
85
|
+
*/
|
|
86
|
+
stop() {
|
|
87
|
+
this.#eventTarget.removeEventListener(this.#event, this.#tracker);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// Copyright (c) 2025 Falko Schumann. All rights reserved. MIT license.
|
|
2
|
+
|
|
3
|
+
import type { MessageClient } from "./message_client";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A client for the server-sent events protocol.
|
|
7
|
+
*/
|
|
8
|
+
export class SseClient extends EventTarget implements MessageClient {
|
|
9
|
+
/**
|
|
10
|
+
* Create an SSE client.
|
|
11
|
+
*
|
|
12
|
+
* @return A new SSE client.
|
|
13
|
+
*/
|
|
14
|
+
static create(): SseClient {
|
|
15
|
+
return new SseClient(EventSource);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Create a nulled SSE client.
|
|
20
|
+
*
|
|
21
|
+
* @return A new SSE client.
|
|
22
|
+
*/
|
|
23
|
+
static createNull(): SseClient {
|
|
24
|
+
return new SseClient(EventSourceStub as typeof EventSource);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
readonly #eventSourceConstructor: typeof EventSource;
|
|
28
|
+
|
|
29
|
+
#eventSource?: EventSource;
|
|
30
|
+
|
|
31
|
+
private constructor(eventSourceConstructor: typeof EventSource) {
|
|
32
|
+
super();
|
|
33
|
+
this.#eventSourceConstructor = eventSourceConstructor;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get isConnected(): boolean {
|
|
37
|
+
return this.#eventSource?.readyState === this.#eventSourceConstructor.OPEN;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get url(): string | undefined {
|
|
41
|
+
return this.#eventSource?.url;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async connect(
|
|
45
|
+
url: string | URL,
|
|
46
|
+
eventName = "message",
|
|
47
|
+
...otherEvents: string[]
|
|
48
|
+
): Promise<void> {
|
|
49
|
+
await new Promise<void>((resolve, reject) => {
|
|
50
|
+
if (this.isConnected) {
|
|
51
|
+
reject(new Error("Already connected."));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
this.#eventSource = new this.#eventSourceConstructor(url);
|
|
57
|
+
this.#eventSource.addEventListener("open", (e) => {
|
|
58
|
+
this.#handleOpen(e);
|
|
59
|
+
resolve();
|
|
60
|
+
});
|
|
61
|
+
this.#eventSource.addEventListener(eventName, (e) =>
|
|
62
|
+
this.#handleMessage(e),
|
|
63
|
+
);
|
|
64
|
+
for (const otherEvent of otherEvents) {
|
|
65
|
+
this.#eventSource.addEventListener(otherEvent, (e) =>
|
|
66
|
+
this.#handleMessage(e),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
this.#eventSource.addEventListener("error", (e) =>
|
|
70
|
+
this.#handleError(e),
|
|
71
|
+
);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
reject(error);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async send(_message: string, _type?: string): Promise<void> {
|
|
79
|
+
throw new Error("unsupported method");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async close(): Promise<void> {
|
|
83
|
+
await new Promise<void>((resolve, reject) => {
|
|
84
|
+
if (!this.isConnected) {
|
|
85
|
+
resolve();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
this.#eventSource!.close();
|
|
91
|
+
resolve();
|
|
92
|
+
} catch (error) {
|
|
93
|
+
reject(error);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Simulate a message event from the server.
|
|
100
|
+
*
|
|
101
|
+
* @param message The message to receive.
|
|
102
|
+
* @param eventName The optional event type.
|
|
103
|
+
* @param lastEventId The optional last event ID.
|
|
104
|
+
*/
|
|
105
|
+
simulateMessage(
|
|
106
|
+
message: string | number | boolean | object | null,
|
|
107
|
+
eventName = "message",
|
|
108
|
+
lastEventId?: string,
|
|
109
|
+
) {
|
|
110
|
+
if (typeof message !== "string") {
|
|
111
|
+
// If the message is not a string, we convert it to a JSON string.
|
|
112
|
+
message = JSON.stringify(message);
|
|
113
|
+
}
|
|
114
|
+
this.#handleMessage(
|
|
115
|
+
new MessageEvent(eventName, { data: message, lastEventId }),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Simulate an error event.
|
|
121
|
+
*/
|
|
122
|
+
simulateError() {
|
|
123
|
+
this.#handleError(new Event("error"));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
#handleOpen(event: Event) {
|
|
127
|
+
this.dispatchEvent(new Event(event.type, event));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#handleMessage(event: MessageEvent) {
|
|
131
|
+
this.dispatchEvent(
|
|
132
|
+
new MessageEvent(event.type, event as unknown as MessageEventInit),
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
#handleError(event: Event) {
|
|
137
|
+
this.dispatchEvent(new Event(event.type, event));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
class EventSourceStub extends EventTarget {
|
|
142
|
+
static CONNECTING = 0;
|
|
143
|
+
static OPEN = 1;
|
|
144
|
+
static CLOSED = 2;
|
|
145
|
+
|
|
146
|
+
url: string;
|
|
147
|
+
readyState = EventSourceStub.CONNECTING;
|
|
148
|
+
|
|
149
|
+
constructor(url: string | URL) {
|
|
150
|
+
super();
|
|
151
|
+
this.url = url.toString();
|
|
152
|
+
setTimeout(() => {
|
|
153
|
+
this.readyState = EventSourceStub.OPEN;
|
|
154
|
+
this.dispatchEvent(new Event("open"));
|
|
155
|
+
}, 0);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
close() {
|
|
159
|
+
this.readyState = EventSourceStub.CLOSED;
|
|
160
|
+
}
|
|
161
|
+
}
|