@rest-vir/define-service 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/LICENSE-CC0 +121 -0
  2. package/LICENSE-MIT +21 -0
  3. package/README.md +88 -0
  4. package/dist/augments/json.d.ts +8 -0
  5. package/dist/augments/json.js +13 -0
  6. package/dist/dev-port/find-dev-port.d.ts +132 -0
  7. package/dist/dev-port/find-dev-port.js +156 -0
  8. package/dist/endpoint/endpoint-path.d.ts +27 -0
  9. package/dist/endpoint/endpoint-path.js +14 -0
  10. package/dist/endpoint/endpoint.d.ts +198 -0
  11. package/dist/endpoint/endpoint.js +80 -0
  12. package/dist/frontend-connect/connect-web-socket.d.ts +95 -0
  13. package/dist/frontend-connect/connect-web-socket.js +64 -0
  14. package/dist/frontend-connect/fetch-endpoint.d.ts +199 -0
  15. package/dist/frontend-connect/fetch-endpoint.js +135 -0
  16. package/dist/frontend-connect/generate-api.d.ts +102 -0
  17. package/dist/frontend-connect/generate-api.js +83 -0
  18. package/dist/frontend-connect/mock-client-web-socket.d.ts +187 -0
  19. package/dist/frontend-connect/mock-client-web-socket.js +198 -0
  20. package/dist/frontend-connect/web-socket-protocol-parse.d.ts +10 -0
  21. package/dist/frontend-connect/web-socket-protocol-parse.js +111 -0
  22. package/dist/index.d.ts +23 -0
  23. package/dist/index.js +23 -0
  24. package/dist/service/define-service.d.ts +19 -0
  25. package/dist/service/define-service.js +133 -0
  26. package/dist/service/match-url.d.ts +30 -0
  27. package/dist/service/match-url.js +31 -0
  28. package/dist/service/minimal-service.d.ts +44 -0
  29. package/dist/service/minimal-service.js +1 -0
  30. package/dist/service/service-definition.d.ts +80 -0
  31. package/dist/service/service-definition.error.d.ts +35 -0
  32. package/dist/service/service-definition.error.js +34 -0
  33. package/dist/service/service-definition.js +1 -0
  34. package/dist/util/mock-fetch.d.ts +107 -0
  35. package/dist/util/mock-fetch.js +199 -0
  36. package/dist/util/no-param.d.ts +16 -0
  37. package/dist/util/no-param.js +8 -0
  38. package/dist/util/origin.d.ts +43 -0
  39. package/dist/util/origin.js +19 -0
  40. package/dist/util/path-to-regexp.d.ts +54 -0
  41. package/dist/util/path-to-regexp.js +307 -0
  42. package/dist/util/search-params.d.ts +9 -0
  43. package/dist/util/search-params.js +1 -0
  44. package/dist/web-socket/common-web-socket.d.ts +103 -0
  45. package/dist/web-socket/common-web-socket.js +28 -0
  46. package/dist/web-socket/overwrite-web-socket-methods.d.ts +276 -0
  47. package/dist/web-socket/overwrite-web-socket-methods.js +210 -0
  48. package/dist/web-socket/web-socket-definition.d.ts +170 -0
  49. package/dist/web-socket/web-socket-definition.js +78 -0
  50. package/package.json +68 -0
@@ -0,0 +1,276 @@
1
+ import { MaybePromise, Overwrite, SelectFrom } from '@augment-vir/common';
2
+ import { type AnyDuration } from 'date-vir';
3
+ import type { HasRequiredKeys } from 'type-fest';
4
+ import { NoParam } from '../util/no-param.js';
5
+ import { CommonWebSocket, CommonWebSocketEventMap } from './common-web-socket.js';
6
+ import { type WebSocketDefinition } from './web-socket-definition.js';
7
+ /**
8
+ * Location of the WebSocket in question: on a client connecting to a WebSocket host or on the host
9
+ * that's accepting WebSocket connections.
10
+ *
11
+ * @category Internal
12
+ * @category Package : @rest-vir/define-service
13
+ * @package [`@rest-vir/define-service`](https://www.npmjs.com/package/@rest-vir/define-service)
14
+ */
15
+ export declare enum WebSocketLocation {
16
+ OnHost = "on-host",
17
+ OnClient = "on-client"
18
+ }
19
+ /**
20
+ * Returns the inverse WebSocket location compared to the given WebSocket location. For example,
21
+ * passing in `WebSocketLocation.OnHost` here will give you `WebSocketLocation.OnClient`.
22
+ *
23
+ * @category Internal
24
+ * @category Package : @rest-vir/define-service
25
+ * @package [`@rest-vir/define-service`](https://www.npmjs.com/package/@rest-vir/define-service)
26
+ */
27
+ export declare function getOppositeWebSocketLocation(originalWebSocketLocation: WebSocketLocation): WebSocketLocation;
28
+ /**
29
+ * Returns the inverse WebSocket location compared to the given WebSocket location. For example,
30
+ * passing in `WebSocketLocation.OnHost` here will give you `WebSocketLocation.OnClient`.
31
+ *
32
+ * @category Internal
33
+ * @category Package : @rest-vir/define-service
34
+ * @package [`@rest-vir/define-service`](https://www.npmjs.com/package/@rest-vir/define-service)
35
+ */
36
+ export type FlipWebSocketLocation<Location extends WebSocketLocation> = Location extends WebSocketLocation.OnHost ? WebSocketLocation.OnClient : WebSocketLocation.OnHost;
37
+ /**
38
+ * Determines a message's type based on the WebSocketLocation of where that message came from.
39
+ *
40
+ * @category Internal
41
+ * @category Package : @rest-vir/define-service
42
+ * @package [`@rest-vir/define-service`](https://www.npmjs.com/package/@rest-vir/define-service)
43
+ */
44
+ export type GetWebSocketMessageTypeFromLocation<SpecificWebSocket extends Readonly<Pick<WebSocketDefinition, 'MessageFromClientType' | 'MessageFromHostType'>>, MessageFromSource extends WebSocketLocation> = MessageFromSource extends WebSocketLocation.OnClient ? SpecificWebSocket['MessageFromClientType'] : SpecificWebSocket['MessageFromHostType'];
45
+ /**
46
+ * Parameters for the `sendAndWaitForReply` method that gets attached to WebSockets.
47
+ *
48
+ * @category Internal
49
+ * @category Package : @rest-vir/define-service
50
+ * @package [`@rest-vir/define-service`](https://www.npmjs.com/package/@rest-vir/define-service)
51
+ */
52
+ export type SendAndWaitForReplyParams<Location extends WebSocketLocation, WebSocketToConnect extends Readonly<SelectFrom<WebSocketDefinition, {
53
+ MessageFromClientType: true;
54
+ MessageFromHostType: true;
55
+ }>> | NoParam = NoParam> = (WebSocketToConnect extends NoParam ? {
56
+ /** Generic message to send. */
57
+ message?: any;
58
+ } : GetWebSocketMessageTypeFromLocation<Exclude<WebSocketToConnect, NoParam>, Location> extends undefined ? {
59
+ /** The message data to send to the other side of the WebSocket connection. */
60
+ message?: GetWebSocketMessageTypeFromLocation<Exclude<WebSocketToConnect, NoParam>, Location>;
61
+ } : {
62
+ /** The message data to send to the other side of the WebSocket connection. */
63
+ message: GetWebSocketMessageTypeFromLocation<Exclude<WebSocketToConnect, NoParam>, Location>;
64
+ }) & {
65
+ /**
66
+ * The duration to wait for a reply message. If this duration is exceeded and a response still
67
+ * hasn't been received, an error is thrown.
68
+ *
69
+ * @default {seconds: 10}
70
+ */
71
+ timeout?: Readonly<AnyDuration> | undefined;
72
+ };
73
+ /**
74
+ * Collapsed version of {@link SendAndWaitForReplyParams} for the `sendAndWaitForReply` method that
75
+ * only _requires_ an object parameter if the parameters object has any required keys.
76
+ *
77
+ * @category Internal
78
+ * @category Package : @rest-vir/define-service
79
+ * @package [`@rest-vir/define-service`](https://www.npmjs.com/package/@rest-vir/define-service)
80
+ */
81
+ export type CollapsedSendAndWaitForReplyParams<Location extends WebSocketLocation, WebSocketToConnect extends Readonly<SelectFrom<WebSocketDefinition, {
82
+ MessageFromClientType: true;
83
+ MessageFromHostType: true;
84
+ }>> | NoParam = NoParam> = HasRequiredKeys<SendAndWaitForReplyParams<Location, WebSocketToConnect>> extends true ? [SendAndWaitForReplyParams<Location, WebSocketToConnect>] : [SendAndWaitForReplyParams<Location, WebSocketToConnect>?];
85
+ /**
86
+ * Takes any WebSocket class and overwrites it with some new rest vir methods and makes some
87
+ * existing WebSocket methods type safe.
88
+ *
89
+ * @category Internal
90
+ * @category Package : @rest-vir/define-service
91
+ * @package [`@rest-vir/define-service`](https://www.npmjs.com/package/@rest-vir/define-service)
92
+ */
93
+ export type OverwriteWebSocketMethods<WebSocketClass extends CommonWebSocket, Location extends WebSocketLocation, OriginalWebSocketDefinition extends Readonly<SelectFrom<WebSocketDefinition, {
94
+ MessageFromClientType: true;
95
+ MessageFromHostType: true;
96
+ SearchParamsType: true;
97
+ }>> | NoParam = NoParam> = Overwrite<WebSocketClass, {
98
+ /**
99
+ * Adds an event listener that's wrapped in assertions to verify that message events have
100
+ * the expected contents.
101
+ */
102
+ addEventListener<const EventName extends keyof CommonWebSocketEventMap>(eventName: EventName, listener: WebSocketListener<EventName, OriginalWebSocketDefinition, FlipWebSocketLocation<Location>, WebSocketClass>): void;
103
+ /**
104
+ * Sends a message to the other side of the WebSocket connection and waits that other side
105
+ * to send a message in response.
106
+ *
107
+ * This will catch messages that might not have been intended as a response for the original
108
+ * message as it will catch _any_ message sent from the other side.
109
+ */
110
+ sendAndWaitForReply(...params: CollapsedSendAndWaitForReplyParams<Location, OriginalWebSocketDefinition>): Promise<OriginalWebSocketDefinition extends NoParam ? any : GetWebSocketMessageTypeFromLocation<Exclude<OriginalWebSocketDefinition, NoParam>, FlipWebSocketLocation<Location>>>;
111
+ /**
112
+ * Sends data through the WebSocket to the other side of the connection. This rest-vir
113
+ * wrapper ensures that all sent messages match expected types from the WebSocket
114
+ * definition.
115
+ *
116
+ * See [MDN](https://developer.mozilla.org/docs/Web/API/WebSocket/send) for the original
117
+ * `WebSocket.send()` docs.
118
+ */
119
+ send(...args: OriginalWebSocketDefinition extends NoParam ? [message?: any] : GetWebSocketMessageTypeFromLocation<Exclude<OriginalWebSocketDefinition, NoParam>, Location> extends undefined ? [
120
+ message?: GetWebSocketMessageTypeFromLocation<Exclude<OriginalWebSocketDefinition, NoParam>, Location>
121
+ ] : [
122
+ message: GetWebSocketMessageTypeFromLocation<Exclude<OriginalWebSocketDefinition, NoParam>, Location>
123
+ ]): void;
124
+ }>;
125
+ /**
126
+ * A WebSocket instance used only in clients to connect to a host.
127
+ *
128
+ * @category Internal
129
+ * @category Package : @rest-vir/define-service
130
+ * @package [`@rest-vir/define-service`](https://www.npmjs.com/package/@rest-vir/define-service)
131
+ */
132
+ export type ClientWebSocket<WebSocketToConnect extends Readonly<SelectFrom<WebSocketDefinition, {
133
+ MessageFromClientType: true;
134
+ MessageFromHostType: true;
135
+ SearchParamsType: true;
136
+ }>> | NoParam = NoParam, WebSocketClass extends CommonWebSocket = CommonWebSocket> = OverwriteWebSocketMethods<WebSocketClass, WebSocketLocation.OnClient, WebSocketToConnect>;
137
+ /**
138
+ * Parameters for a type-safe WebSocket listener callback. Used in {@link WebSocketListener}.
139
+ *
140
+ * @category Internal
141
+ * @category Package : @rest-vir/define-service
142
+ * @package [`@rest-vir/define-service`](https://www.npmjs.com/package/@rest-vir/define-service)
143
+ */
144
+ export type WebSocketListenerParams<EventName extends keyof CommonWebSocketEventMap, WebSocketToConnect extends Readonly<SelectFrom<WebSocketDefinition, {
145
+ MessageFromClientType: true;
146
+ MessageFromHostType: true;
147
+ SearchParamsType: true;
148
+ }>> | NoParam, MessageSource extends WebSocketLocation, WebSocketClass extends CommonWebSocket> = {
149
+ webSocketDefinition: WebSocketToConnect extends NoParam ? Readonly<Overwrite<WebSocketDefinition,
150
+ /**
151
+ * This `Overwrite` is needed so that
152
+ * `CollapsedConnectWebSocketParams<ACTUAL_SOCKET>` can be assigned to
153
+ * `CollapsedConnectWebSocketParams<NoParam>`. Idk why.
154
+ */
155
+ {
156
+ path: any;
157
+ customProps: any;
158
+ }>> : Readonly<WebSocketToConnect>;
159
+ webSocket: ClientWebSocket<WebSocketToConnect, WebSocketClass>;
160
+ } & (EventName extends 'message' ? {
161
+ event: Overwrite<CommonWebSocketEventMap[EventName], {
162
+ data: WebSocketToConnect extends NoParam ? any : GetWebSocketMessageTypeFromLocation<Exclude<WebSocketToConnect, NoParam>, MessageSource>;
163
+ }>;
164
+ searchParams: WebSocketToConnect extends NoParam ? any : Exclude<WebSocketToConnect, NoParam>['SearchParamsType'];
165
+ message: WebSocketToConnect extends NoParam ? any : GetWebSocketMessageTypeFromLocation<Exclude<WebSocketToConnect, NoParam>, MessageSource>;
166
+ } : {
167
+ event: CommonWebSocketEventMap[EventName];
168
+ });
169
+ /**
170
+ * An object defining declaratively created listeners that will be attached to a rest-vir
171
+ * client-side WebSocket.
172
+ *
173
+ * @category Internal
174
+ * @category Package : @rest-vir/define-service
175
+ * @package [`@rest-vir/define-service`](https://www.npmjs.com/package/@rest-vir/define-service)
176
+ */
177
+ export type ConnectWebSocketListeners<WebSocketToConnect extends Readonly<SelectFrom<WebSocketDefinition, {
178
+ MessageFromClientType: true;
179
+ MessageFromHostType: true;
180
+ SearchParamsType: true;
181
+ }>> | NoParam, WebSocketClass extends CommonWebSocket> = Partial<{
182
+ [EventName in keyof CommonWebSocketEventMap]: WebSocketListener<EventName, WebSocketToConnect, WebSocketLocation.OnHost, WebSocketClass>;
183
+ }>;
184
+ /**
185
+ * A type-safe WebSocket listener callback.
186
+ *
187
+ * @category Internal
188
+ * @category Package : @rest-vir/define-service
189
+ * @package [`@rest-vir/define-service`](https://www.npmjs.com/package/@rest-vir/define-service)
190
+ */
191
+ export type WebSocketListener<EventName extends keyof CommonWebSocketEventMap, WebSocketToConnect extends Readonly<SelectFrom<WebSocketDefinition, {
192
+ MessageFromClientType: true;
193
+ MessageFromHostType: true;
194
+ SearchParamsType: true;
195
+ }>> | NoParam, MessageSource extends WebSocketLocation, WebSocketClass extends CommonWebSocket> = (params: WebSocketListenerParams<EventName, WebSocketToConnect, MessageSource, WebSocketClass>) => MaybePromise<void>;
196
+ /**
197
+ * Generic connection parameters for `connectWebSocket`.
198
+ *
199
+ * @category Internal
200
+ * @category Package : @rest-vir/define-service
201
+ * @package [`@rest-vir/define-service`](https://www.npmjs.com/package/@rest-vir/define-service)
202
+ */
203
+ export type GenericConnectWebSocketParams<WebSocketClass extends CommonWebSocket> = {
204
+ /** Parameters for WebSocket paths that need them, like `'/my-path/:param1/:param2'`. */
205
+ pathParams?: Record<string, string> | undefined;
206
+ /**
207
+ * A list of WebSocket protocols. This is the standard built-in argument for the `WebSocket`
208
+ * constructor.
209
+ *
210
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket#protocols
211
+ */
212
+ protocols?: string[];
213
+ /**
214
+ * Optional listeners that can be immediately attached to the WebSocket instance instead of
215
+ * requiring externally adding them.
216
+ */
217
+ listeners?: ConnectWebSocketListeners<NoParam, WebSocketClass>;
218
+ /**
219
+ * A custom `WebSocket` constructor. Useful for debugging or unit testing. This can safely be
220
+ * omitted to use the default JavaScript built-in global `WebSocket` class.
221
+ *
222
+ * @default globalThis.WebSocket
223
+ */
224
+ webSocketConstructor?: (new (url: string, protocols: string[] | undefined, webSocketDefinition: WebSocketDefinition) => WebSocketClass) | undefined;
225
+ searchParams?: unknown;
226
+ };
227
+ /**
228
+ * Overwrites WebSocket methods with their rest-vir, type-safe replacements.
229
+ *
230
+ * WARNING: this mutates the input WebSocket.
231
+ *
232
+ * @category Internal
233
+ * @category Package : @rest-vir/define-service
234
+ * @package [`@rest-vir/define-service`](https://www.npmjs.com/package/@rest-vir/define-service)
235
+ */
236
+ export declare function overwriteWebSocketMethods<const WebSocketToConnect extends WebSocketDefinition | NoParam, const WebSocketClass extends CommonWebSocket, const Location extends WebSocketLocation>(webSocketDefinition: WebSocketToConnect extends NoParam ? WebSocketDefinition : WebSocketToConnect, rawWebSocket: Readonly<WebSocketClass>, webSocketLocation: Location): OverwriteWebSocketMethods<WebSocketClass, Location, WebSocketToConnect>;
237
+ /**
238
+ * Waits for a WebSocket to reach to the open state.
239
+ *
240
+ * @category Internal
241
+ * @category Package : @rest-vir/define-service
242
+ * @package [`@rest-vir/define-service`](https://www.npmjs.com/package/@rest-vir/define-service)
243
+ */
244
+ export declare function waitForOpenWebSocket(webSocket: Readonly<Pick<CommonWebSocket, 'readyState' | 'addEventListener' | 'removeEventListener'>>): Promise<void>;
245
+ /**
246
+ * Overwrites WebSocket methods with the typed rest-vir replacements, attaches WebSocket listeners,
247
+ * and waits for the WebSocket to be opened.
248
+ *
249
+ * @category Internal
250
+ * @category Package : @rest-vir/define-service
251
+ * @package [`@rest-vir/define-service`](https://www.npmjs.com/package/@rest-vir/define-service)
252
+ */
253
+ export declare function finalizeWebSocket<const WebSocketToConnect extends Readonly<WebSocketDefinition> | NoParam, const WebSocketClass extends CommonWebSocket, const Location extends WebSocketLocation>(webSocketDefinition: WebSocketToConnect extends NoParam ? WebSocketDefinition : WebSocketToConnect,
254
+ /** An already-constructed WebSocket instance. */
255
+ webSocketInput: WebSocketClass, listeners: ConnectWebSocketListeners<NoParam, WebSocketClass> | undefined, location: Location): Promise<OverwriteWebSocketMethods<WebSocketClass, Location, WebSocketToConnect>>;
256
+ /**
257
+ * Verifies that the given WebSocket message matches the defined expectations for the given message
258
+ * source.
259
+ *
260
+ * @category Internal
261
+ * @category Package : @rest-vir/define-service
262
+ * @package [`@rest-vir/define-service`](https://www.npmjs.com/package/@rest-vir/define-service)
263
+ */
264
+ export declare function verifyWebSocketMessage<const SpecificWebSocket extends SelectFrom<WebSocketDefinition, {
265
+ MessageFromHostType: true;
266
+ messageFromHostShape: true;
267
+ messageFromClientShape: true;
268
+ path: true;
269
+ service: {
270
+ serviceName: true;
271
+ };
272
+ }>, Location extends WebSocketLocation>(webSocketDefinition: Readonly<SpecificWebSocket>,
273
+ /** The raw message data. */
274
+ message: any,
275
+ /** The location from which the message was sent. */
276
+ messageSentFrom: Location): SpecificWebSocket['MessageFromHostType'];
@@ -0,0 +1,210 @@
1
+ import { waitUntil } from '@augment-vir/assert';
2
+ import { DeferredPromise, ensureErrorAndPrependMessage, getOrSet, stringify, } from '@augment-vir/common';
3
+ import { convertDuration } from 'date-vir';
4
+ import { assertValidShape } from 'object-shape-tester';
5
+ import { parseJsonWithUndefined } from '../augments/json.js';
6
+ import { CommonWebSocketState, } from './common-web-socket.js';
7
+ /**
8
+ * Location of the WebSocket in question: on a client connecting to a WebSocket host or on the host
9
+ * that's accepting WebSocket connections.
10
+ *
11
+ * @category Internal
12
+ * @category Package : @rest-vir/define-service
13
+ * @package [`@rest-vir/define-service`](https://www.npmjs.com/package/@rest-vir/define-service)
14
+ */
15
+ export var WebSocketLocation;
16
+ (function (WebSocketLocation) {
17
+ WebSocketLocation["OnHost"] = "on-host";
18
+ WebSocketLocation["OnClient"] = "on-client";
19
+ })(WebSocketLocation || (WebSocketLocation = {}));
20
+ /**
21
+ * Returns the inverse WebSocket location compared to the given WebSocket location. For example,
22
+ * passing in `WebSocketLocation.OnHost` here will give you `WebSocketLocation.OnClient`.
23
+ *
24
+ * @category Internal
25
+ * @category Package : @rest-vir/define-service
26
+ * @package [`@rest-vir/define-service`](https://www.npmjs.com/package/@rest-vir/define-service)
27
+ */
28
+ export function getOppositeWebSocketLocation(originalWebSocketLocation) {
29
+ if (originalWebSocketLocation === WebSocketLocation.OnClient) {
30
+ return WebSocketLocation.OnHost;
31
+ }
32
+ else {
33
+ return WebSocketLocation.OnClient;
34
+ }
35
+ }
36
+ /**
37
+ * Overwrites WebSocket methods with their rest-vir, type-safe replacements.
38
+ *
39
+ * WARNING: this mutates the input WebSocket.
40
+ *
41
+ * @category Internal
42
+ * @category Package : @rest-vir/define-service
43
+ * @package [`@rest-vir/define-service`](https://www.npmjs.com/package/@rest-vir/define-service)
44
+ */
45
+ export function overwriteWebSocketMethods(webSocketDefinition, rawWebSocket, webSocketLocation) {
46
+ const originalSend = rawWebSocket.send;
47
+ const originalAddEventListener = rawWebSocket.addEventListener;
48
+ const originalRemoveEventListener = rawWebSocket.removeEventListener;
49
+ const webSocket = rawWebSocket;
50
+ const originalListenerMap = {};
51
+ Object.assign(webSocket, {
52
+ originalListenerMap,
53
+ addEventListener(eventName, listener) {
54
+ function newListener(event) {
55
+ const baseParams = {
56
+ event,
57
+ webSocket,
58
+ webSocketDefinition,
59
+ };
60
+ if (eventName === 'message') {
61
+ const message = verifyWebSocketMessage(webSocketDefinition, parseJsonWithUndefined(String(event.data)),
62
+ /**
63
+ * Flip the WebSocket location because messages on the client WebSocket will
64
+ * come from the host and messages on the host WebSocket will come from the
65
+ * client.
66
+ */
67
+ getOppositeWebSocketLocation(webSocketLocation));
68
+ return listener({
69
+ ...baseParams,
70
+ message,
71
+ });
72
+ }
73
+ else {
74
+ return listener(baseParams);
75
+ }
76
+ }
77
+ getOrSet(originalListenerMap, eventName, () => new WeakMap()).set(listener, newListener);
78
+ return originalAddEventListener.call(webSocket, eventName, newListener);
79
+ },
80
+ removeEventListener(eventName, listener) {
81
+ const existing = originalListenerMap[eventName]?.get(listener);
82
+ if (existing) {
83
+ originalListenerMap[eventName]?.delete(listener);
84
+ originalRemoveEventListener.call(webSocket, eventName, existing);
85
+ }
86
+ },
87
+ async sendAndWaitForReply({ message, timeout = { seconds: 10 }, } = {}) {
88
+ const deferredReply = new DeferredPromise();
89
+ function listener({ message, }) {
90
+ if (!deferredReply.isSettled) {
91
+ deferredReply.resolve(message);
92
+ }
93
+ }
94
+ setTimeout(() => {
95
+ if (!deferredReply.isSettled) {
96
+ deferredReply.reject('Timeout: got no reply from the host.');
97
+ }
98
+ }, convertDuration(timeout, { milliseconds: true }).milliseconds);
99
+ webSocket.addEventListener('message', listener);
100
+ webSocket.send(message);
101
+ try {
102
+ const reply = await deferredReply.promise;
103
+ return reply;
104
+ }
105
+ finally {
106
+ webSocket.removeEventListener('message', listener);
107
+ }
108
+ },
109
+ send(message) {
110
+ originalSend.call(webSocket,
111
+ /** The extra `String()` wrapper is to convert `undefined` into `'undefined'`. */
112
+ String(JSON.stringify(verifyWebSocketMessage(webSocketDefinition, message, webSocketLocation))));
113
+ },
114
+ });
115
+ return webSocket;
116
+ }
117
+ /**
118
+ * Waits for a WebSocket to reach to the open state.
119
+ *
120
+ * @category Internal
121
+ * @category Package : @rest-vir/define-service
122
+ * @package [`@rest-vir/define-service`](https://www.npmjs.com/package/@rest-vir/define-service)
123
+ */
124
+ export async function waitForOpenWebSocket(webSocket) {
125
+ const webSocketOpenedPromise = new DeferredPromise();
126
+ function errorListener(error) {
127
+ if (!webSocketOpenedPromise.isSettled) {
128
+ webSocketOpenedPromise.reject(ensureErrorAndPrependMessage(error, 'WebSocket connection failed.'));
129
+ }
130
+ }
131
+ webSocket.addEventListener('error', errorListener);
132
+ void waitUntil
133
+ .isTruthy(() => {
134
+ if (webSocketOpenedPromise.isSettled) {
135
+ return true;
136
+ }
137
+ if (webSocket.readyState === CommonWebSocketState.Closed) {
138
+ webSocketOpenedPromise.reject('WebSocket closed while waiting for it to open.');
139
+ return true;
140
+ }
141
+ else if (webSocket.readyState === CommonWebSocketState.Open) {
142
+ webSocketOpenedPromise.resolve();
143
+ return true;
144
+ }
145
+ else {
146
+ return false;
147
+ }
148
+ }, undefined, 'WebSocket never opened')
149
+ .catch((error) => {
150
+ if (!webSocketOpenedPromise.isSettled) {
151
+ webSocketOpenedPromise.reject(error);
152
+ }
153
+ });
154
+ await webSocketOpenedPromise.promise.finally(() => {
155
+ webSocket.removeEventListener('error', errorListener);
156
+ });
157
+ }
158
+ /**
159
+ * Overwrites WebSocket methods with the typed rest-vir replacements, attaches WebSocket listeners,
160
+ * and waits for the WebSocket to be opened.
161
+ *
162
+ * @category Internal
163
+ * @category Package : @rest-vir/define-service
164
+ * @package [`@rest-vir/define-service`](https://www.npmjs.com/package/@rest-vir/define-service)
165
+ */
166
+ export async function finalizeWebSocket(webSocketDefinition,
167
+ /** An already-constructed WebSocket instance. */
168
+ webSocketInput, listeners, location) {
169
+ const webSocket = overwriteWebSocketMethods(webSocketDefinition, webSocketInput, location);
170
+ if (listeners?.open) {
171
+ webSocket.addEventListener('open', listeners.open);
172
+ }
173
+ if (listeners?.error) {
174
+ webSocket.addEventListener('error', listeners.error);
175
+ }
176
+ if (listeners?.message) {
177
+ webSocket.addEventListener('message', listeners.message);
178
+ }
179
+ if (listeners?.close) {
180
+ webSocket.addEventListener('close', listeners.close);
181
+ }
182
+ await waitForOpenWebSocket(webSocketInput);
183
+ return webSocket;
184
+ }
185
+ /**
186
+ * Verifies that the given WebSocket message matches the defined expectations for the given message
187
+ * source.
188
+ *
189
+ * @category Internal
190
+ * @category Package : @rest-vir/define-service
191
+ * @package [`@rest-vir/define-service`](https://www.npmjs.com/package/@rest-vir/define-service)
192
+ */
193
+ export function verifyWebSocketMessage(webSocketDefinition,
194
+ /** The raw message data. */
195
+ message,
196
+ /** The location from which the message was sent. */
197
+ messageSentFrom) {
198
+ const shape = messageSentFrom === WebSocketLocation.OnClient
199
+ ? webSocketDefinition.messageFromClientShape
200
+ : webSocketDefinition.messageFromHostShape;
201
+ if (shape) {
202
+ assertValidShape(message, shape, {
203
+ allowExtraKeys: true,
204
+ });
205
+ }
206
+ else if (message) {
207
+ throw new TypeError(`WebSocket '${webSocketDefinition.path}' in service '${webSocketDefinition.service.serviceName}' does not expect any message data from the ${messageSentFrom === WebSocketLocation.OnClient ? 'client' : 'host'} but received it: ${stringify(message)}.`);
208
+ }
209
+ return message;
210
+ }
@@ -0,0 +1,170 @@
1
+ import { AnyObject, Overwrite, type SelectFrom } from '@augment-vir/common';
2
+ import { ShapeDefinition, ShapeToRuntimeType } from 'object-shape-tester';
3
+ import type { IsEqual } from 'type-fest';
4
+ import { EndpointPathBase } from '../endpoint/endpoint-path.js';
5
+ import { MinimalService } from '../service/minimal-service.js';
6
+ import { NoParam } from '../util/no-param.js';
7
+ import { OriginRequirement } from '../util/origin.js';
8
+ import type { BaseSearchParams } from '../util/search-params.js';
9
+ /**
10
+ * Initialization for a WebSocket within a service definition..
11
+ *
12
+ * @category Internal
13
+ * @category Package : @rest-vir/define-service
14
+ * @package [`@rest-vir/define-service`](https://www.npmjs.com/package/@rest-vir/define-service)
15
+ */
16
+ export type WebSocketInit<MessageFromClientShape = unknown, MessageFromServerShape = unknown> = {
17
+ messageFromClientShape: MessageFromClientShape;
18
+ messageFromHostShape: MessageFromServerShape;
19
+ /**
20
+ * Set a required client origin for this endpoint.
21
+ *
22
+ * - If this is omitted, the service's origin requirement is used instead.
23
+ * - If this is explicitly set to `undefined`, this endpoint allows any origins (regardless of the
24
+ * service's origin requirement).
25
+ * - Any other set value overrides the service's origin requirement (if it has any).
26
+ */
27
+ requiredClientOrigin?: OriginRequirement;
28
+ /**
29
+ * A shape (that is parsed by the
30
+ * [`object-shape-tester`](https://www.npmjs.com/package/object-shape-tester) package) that all
31
+ * protocols for this WebSocket are collectively tested against. Omit this or set it to
32
+ * `undefined` to allow a string array of any length.
33
+ *
34
+ * This shape will be tested against all protocols together in a single array, so this should be
35
+ * an array shape. Only string-compatible values inside the array will work. It is recommended
36
+ * to use `tupleShape` from the `object-shape-tester` package.
37
+ *
38
+ * @example
39
+ *
40
+ * ```ts
41
+ * import {tupleShape, exact} from 'object-shape-tester';
42
+ *
43
+ * const partialWebSocketInit = {
44
+ * protocolsShape: tupleShape('', exact('hi')),
45
+ * };
46
+ * ```
47
+ */
48
+ protocolsShape?: unknown;
49
+ /**
50
+ * A shape used to verify search params. This should match the entire search params object.
51
+ *
52
+ * Note the following:
53
+ *
54
+ * - Search param values will _always_ be in an array
55
+ * - Elements in search param value arrays will _always_ be strings
56
+ *
57
+ * @example
58
+ *
59
+ * ```ts
60
+ * import {exact, enumShape, tupleShape} from 'object-shape-tester';
61
+ *
62
+ * const partialWebSocketInit = {
63
+ * searchParamsShape: {
64
+ * // use `tupleShape` to ensure there's exactly one entry for this search param
65
+ * userId: tupleShape(enumShape(MyEnum)),
66
+ * date: tupleShape(exact('2')),
67
+ * // don't use `tupleShape` here so that there can be any number of entries
68
+ * colors: [''],
69
+ * },
70
+ * };
71
+ * ```
72
+ */
73
+ searchParamsShape?: unknown;
74
+ /** Attach any other properties that you want inside of here. */
75
+ customProps?: Record<PropertyKey, unknown> | undefined;
76
+ };
77
+ /**
78
+ * Adds final props to a {@link WebSocketInit}, converting it into a {@link WebSocketDefinition}.
79
+ *
80
+ * @category Internal
81
+ * @category Package : @rest-vir/define-service
82
+ * @package [`@rest-vir/define-service`](https://www.npmjs.com/package/@rest-vir/define-service)
83
+ */
84
+ export type WithFinalWebSocketProps<Init, WebSocketPath extends EndpointPathBase> = (Init extends AnyObject ? Overwrite<Init, {
85
+ messageFromClientShape: IsEqual<Init['messageFromClientShape'], NoParam> extends true ? any : Init['messageFromClientShape'] extends NoParam ? ShapeDefinition<any, true> | undefined : undefined extends Init['messageFromClientShape'] ? undefined : ShapeDefinition<Init['messageFromClientShape'], true>;
86
+ messageFromHostShape: IsEqual<Init['messageFromHostShape'], NoParam> extends true ? any : Init['messageFromHostShape'] extends NoParam ? ShapeDefinition<any, true> | undefined : undefined extends Init['messageFromHostShape'] ? undefined : ShapeDefinition<Init['messageFromHostShape'], true>;
87
+ MessageFromClientType: Init['messageFromClientShape'] extends NoParam ? any : undefined extends Init['messageFromClientShape'] ? undefined : ShapeToRuntimeType<ShapeDefinition<Init['messageFromClientShape'], true>, false, true>;
88
+ MessageFromHostType: Init['messageFromHostShape'] extends NoParam ? any : undefined extends Init['messageFromHostShape'] ? undefined : ShapeToRuntimeType<ShapeDefinition<Init['messageFromHostShape'], true>, false, true>;
89
+ protocolsShape: 'protocolsShape' extends keyof Init ? undefined extends Init['protocolsShape'] ? undefined : ShapeDefinition<Init['protocolsShape'], true> | undefined : undefined;
90
+ ProtocolsType: undefined extends Init['protocolsShape'] ? string[] : ShapeToRuntimeType<ShapeDefinition<Init['protocolsShape'], true>, false, true>;
91
+ searchParamsShape: 'searchParamsShape' extends keyof Init ? undefined extends Init['searchParamsShape'] ? undefined : ShapeDefinition<Init['searchParamsShape'], true> | undefined : undefined;
92
+ SearchParamsType: IsEqual<Init['searchParamsShape'], undefined> extends true ? BaseSearchParams : ShapeToRuntimeType<ShapeDefinition<Init['searchParamsShape'], true>, false, true>;
93
+ customProps: 'customProps' extends keyof Init ? Init['customProps'] : undefined;
94
+ }> : never) & {
95
+ path: WebSocketPath;
96
+ isWebSocket: true;
97
+ isEndpoint: false;
98
+ service: MinimalService;
99
+ };
100
+ /**
101
+ * A finished REST server WebSocket definition. This is generated from `defineService`.
102
+ *
103
+ * @category Internal
104
+ * @category Package : @rest-vir/define-service
105
+ * @package [`@rest-vir/define-service`](https://www.npmjs.com/package/@rest-vir/define-service)
106
+ */
107
+ export type WebSocketDefinition<MessageFromClientShape = NoParam, MessageFromServerShape = NoParam, WebSocketPath extends EndpointPathBase = EndpointPathBase> = WithFinalWebSocketProps<WebSocketInit<MessageFromClientShape, MessageFromServerShape>, WebSocketPath>;
108
+ /**
109
+ * A generic version of {@link WebSocketDefinition} that any WebSocketDefinition can be assigned to.
110
+ *
111
+ * @category Internal
112
+ * @category Package : @rest-vir/define-service
113
+ * @package [`@rest-vir/define-service`](https://www.npmjs.com/package/@rest-vir/define-service)
114
+ */
115
+ export type GenericWebSocketDefinition = Overwrite<WebSocketDefinition, {
116
+ protocolsShape: any;
117
+ ProtocolsType: any;
118
+ searchParamsShape: any;
119
+ SearchParamsType: any;
120
+ }>;
121
+ /**
122
+ * Shape definition for {@link WebSocketInit}.
123
+ *
124
+ * @category Internal
125
+ * @category Package : @rest-vir/define-service
126
+ * @package [`@rest-vir/define-service`](https://www.npmjs.com/package/@rest-vir/define-service)
127
+ */
128
+ export declare const webSocketInitShape: ShapeDefinition<{
129
+ messageFromClientShape: import("object-shape-tester").ShapeUnknown<[unknown]>;
130
+ messageFromHostShape: import("object-shape-tester").ShapeUnknown<[unknown]>;
131
+ searchParamsShape: import("object-shape-tester").ShapeUnknown<[unknown]>;
132
+ /**
133
+ * Set a required client origin for this WebSocket.
134
+ *
135
+ * - If this is omitted, the service's origin requirement is used instead.
136
+ * - If this is explicitly set to `undefined`, this endpoint allows any origins (regardless of the
137
+ * service's origin requirement).
138
+ * - Any other set value overrides the service's origin requirement (if it has any).
139
+ */
140
+ requiredClientOrigin: import("object-shape-tester").ShapeOr<[undefined, string, import("object-shape-tester").ShapeClass<[RegExpConstructor]>, () => void, import("object-shape-tester").ShapeOr<[string, import("object-shape-tester").ShapeClass<[RegExpConstructor]>, () => void]>[]]>;
141
+ customProps: import("object-shape-tester").ShapeOptional<import("object-shape-tester").ShapeOr<[undefined, import("object-shape-tester").ShapeIndexedKeys<[{
142
+ keys: import("object-shape-tester").ShapeUnknown<[unknown]>;
143
+ values: import("object-shape-tester").ShapeUnknown<[unknown]>;
144
+ required: false;
145
+ }]>]>>;
146
+ protocolsShape: import("object-shape-tester").ShapeUnknown<[unknown]>;
147
+ }, false>;
148
+ /**
149
+ * Attaches message type-only getters to a WebSocket definition.
150
+ *
151
+ * @category Internal
152
+ * @category Package : @rest-vir/define-service
153
+ * @package [`@rest-vir/define-service`](https://www.npmjs.com/package/@rest-vir/define-service)
154
+ */
155
+ export declare function attachWebSocketShapeTypeGetters<const T extends AnyObject>(webSocketDefinition: T): asserts webSocketDefinition is T & Pick<WebSocketDefinition, 'MessageFromClientType' | 'MessageFromHostType'>;
156
+ /**
157
+ * Asserts the the WebSocket definition is valid.
158
+ *
159
+ * @category Internal
160
+ * @category Package : @rest-vir/define-service
161
+ * @package [`@rest-vir/define-service`](https://www.npmjs.com/package/@rest-vir/define-service)
162
+ */
163
+ export declare function assertValidWebSocketDefinition(webSocketDefinition: Readonly<SelectFrom<WebSocketDefinition, {
164
+ isEndpoint: true;
165
+ isWebSocket: true;
166
+ path: true;
167
+ service: {
168
+ serviceName: true;
169
+ };
170
+ }>>): void;