swarpc 0.6.1 → 0.7.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/src/server.ts ADDED
@@ -0,0 +1,193 @@
1
+ import { type } from "arktype"
2
+ import { createLogger, type LogLevel } from "./log.js"
3
+ import {
4
+ ImplementationsMap,
5
+ Payload,
6
+ PayloadCore,
7
+ PayloadHeaderSchema,
8
+ PayloadSchema,
9
+ zImplementations,
10
+ zProcedures,
11
+ type ProceduresMap,
12
+ type SwarpcServer,
13
+ } from "./types.js"
14
+ import { findTransferables } from "./utils.js"
15
+
16
+ export type { SwarpcServer } from "./types.js"
17
+
18
+ const abortControllers = new Map<string, AbortController>()
19
+ const abortedRequests = new Set<string>()
20
+
21
+ /**
22
+ * Creates a sw&rpc server instance.
23
+ * @param procedures procedures the server will implement
24
+ * @param options various options
25
+ * @param options.worker if provided, the server will use this worker to post messages, instead of sending it to all clients
26
+ * @returns a SwarpcServer instance. Each property of the procedures map will be a method, that accepts a function implementing the procedure. There is also .start(), to be called after implementing all procedures.
27
+ */
28
+ export function Server<Procedures extends ProceduresMap>(
29
+ procedures: Procedures,
30
+ { worker, loglevel = "debug" }: { worker?: Worker; loglevel?: LogLevel } = {}
31
+ ): SwarpcServer<Procedures> {
32
+ const l = createLogger("server", loglevel)
33
+
34
+ // Initialize the instance.
35
+ // Procedures and implementations are stored on properties with symbol keys,
36
+ // to avoid any conflicts with procedure names, and also discourage direct access to them.
37
+ const instance = {
38
+ [zProcedures]: procedures,
39
+ [zImplementations]: {} as ImplementationsMap<Procedures>,
40
+ start: (self: Window) => {},
41
+ } as SwarpcServer<Procedures>
42
+
43
+ // Set all implementation-setter methods
44
+ for (const functionName in procedures) {
45
+ instance[functionName] = ((implementation) => {
46
+ if (!instance[zProcedures][functionName]) {
47
+ throw new Error(`No procedure found for function name: ${functionName}`)
48
+ }
49
+ instance[zImplementations][functionName] = (
50
+ input,
51
+ onProgress,
52
+ abortSignal
53
+ ) => {
54
+ abortSignal?.throwIfAborted()
55
+ return new Promise((resolve, reject) => {
56
+ abortSignal?.addEventListener("abort", () => {
57
+ let { requestId, reason } = abortSignal?.reason
58
+ l.debug(requestId, `Aborted ${functionName} request: ${reason}`)
59
+ reject({ aborted: reason })
60
+ })
61
+
62
+ implementation(input, onProgress, abortSignal)
63
+ .then(resolve)
64
+ .catch(reject)
65
+ })
66
+ }
67
+ }) as SwarpcServer<Procedures>[typeof functionName]
68
+ }
69
+
70
+ instance.start = (self: Window) => {
71
+ // Used to post messages back to the client
72
+ const postMessage = async (
73
+ autotransfer: boolean,
74
+ data: Payload<Procedures>
75
+ ) => {
76
+ const transfer = autotransfer ? [] : findTransferables(data)
77
+
78
+ if (worker) {
79
+ self.postMessage(data, { transfer })
80
+ } else {
81
+ await (self as any).clients.matchAll().then((clients: any[]) => {
82
+ clients.forEach((client) => client.postMessage(data, { transfer }))
83
+ })
84
+ }
85
+ }
86
+
87
+ // Listen for messages from the client
88
+ self.addEventListener("message", async (event: MessageEvent) => {
89
+ // Decode the payload
90
+ const { requestId, functionName } = PayloadHeaderSchema(
91
+ type.enumerated(...Object.keys(procedures))
92
+ ).assert(event.data)
93
+
94
+ l.debug(requestId, `Received request for ${functionName}`, event.data)
95
+
96
+ // Get autotransfer preference from the procedure definition
97
+ const { autotransfer = "output-only", ...schemas } =
98
+ instance[zProcedures][functionName]
99
+
100
+ // Shorthand function with functionName, requestId, etc. set
101
+ const postMsg = async (
102
+ data: PayloadCore<Procedures, typeof functionName>
103
+ ) => {
104
+ if (abortedRequests.has(requestId)) return
105
+ await postMessage(autotransfer !== "never", {
106
+ by: "sw&rpc",
107
+ functionName,
108
+ requestId,
109
+ ...data,
110
+ })
111
+ }
112
+
113
+ // Prepare a function to post errors back to the client
114
+ const postError = async (error: any) =>
115
+ postMsg({
116
+ error: {
117
+ message: "message" in error ? error.message : String(error),
118
+ },
119
+ })
120
+
121
+ // Retrieve the implementation for the requested function
122
+ const implementation = instance[zImplementations][functionName]
123
+ if (!implementation) {
124
+ await postError("No implementation found")
125
+ return
126
+ }
127
+
128
+ // Define payload schema for incoming messages
129
+ const payload = PayloadSchema(
130
+ type(`"${functionName}"`),
131
+ schemas.input,
132
+ schemas.progress,
133
+ schemas.success
134
+ ).assert(event.data)
135
+
136
+ // Handle abortion requests (pro-choice ftw!!)
137
+ if (payload.abort) {
138
+ const controller = abortControllers.get(requestId)
139
+
140
+ if (!controller)
141
+ await postError("No abort controller found for request")
142
+
143
+ controller?.abort(payload.abort.reason)
144
+ return
145
+ }
146
+
147
+ // Set up the abort controller for this request
148
+ abortControllers.set(requestId, new AbortController())
149
+
150
+ if (!payload.input) {
151
+ await postError("No input provided")
152
+ return
153
+ }
154
+
155
+ // Call the implementation with the input and a progress callback
156
+ await implementation(
157
+ payload.input,
158
+ async (progress: any) => {
159
+ l.debug(requestId, `Progress for ${functionName}`, progress)
160
+ await postMsg({ progress })
161
+ },
162
+ abortControllers.get(requestId)?.signal
163
+ )
164
+ // Send errors
165
+ .catch(async (error: any) => {
166
+ // Handle errors caused by abortions
167
+ if ("aborted" in error) {
168
+ l.debug(
169
+ requestId,
170
+ `Received abort error for ${functionName}`,
171
+ error.aborted
172
+ )
173
+ abortedRequests.add(requestId)
174
+ abortControllers.delete(requestId)
175
+ return
176
+ }
177
+
178
+ l.error(requestId, `Error in ${functionName}`, error)
179
+ await postError(error)
180
+ })
181
+ // Send results
182
+ .then(async (result: any) => {
183
+ l.debug(requestId, `Result for ${functionName}`, result)
184
+ await postMsg({ result })
185
+ })
186
+ .finally(() => {
187
+ abortedRequests.delete(requestId)
188
+ })
189
+ })
190
+ }
191
+
192
+ return instance
193
+ }
package/src/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Type } from "arktype"
1
+ import { type, type Type } from "arktype"
2
2
 
3
3
  /**
4
4
  * A procedure declaration
@@ -30,16 +30,35 @@ export type Procedure<I extends Type, P extends Type, S extends Type> = {
30
30
  autotransfer?: "always" | "never" | "output-only"
31
31
  }
32
32
 
33
+ /**
34
+ * A promise that you can cancel by calling `.cancel(reason)` on it:
35
+ *
36
+ * ```js
37
+ * const { request, cancel } = client.runProcedure.cancelable(input, onProgress)
38
+ * setTimeout(() => cancel("Cancelled by user"), 1000)
39
+ * const result = await request
40
+ * ```
41
+ */
42
+ export type CancelablePromise<T> = {
43
+ request: Promise<T>
44
+ /**
45
+ * Abort the request.
46
+ * @param reason The reason for cancelling the request.
47
+ */
48
+ cancel: (reason: string) => Promise<void>
49
+ }
50
+
33
51
  /**
34
52
  * An implementation of a procedure
35
53
  */
36
54
  export type ProcedureImplementation<
37
55
  I extends Type,
38
56
  P extends Type,
39
- S extends Type
57
+ S extends Type,
40
58
  > = (
41
59
  input: I["inferOut"],
42
- onProgress: (progress: P["inferIn"]) => void
60
+ onProgress: (progress: P["inferIn"]) => void,
61
+ abortSignal?: AbortSignal
43
62
  ) => Promise<S["inferIn"]>
44
63
 
45
64
  /**
@@ -85,19 +104,32 @@ export type Hooks<Procedures extends ProceduresMap> = {
85
104
  ) => void
86
105
  }
87
106
 
107
+ export const PayloadHeaderSchema = type("<Name extends string>", {
108
+ by: '"sw&rpc"',
109
+ functionName: "Name",
110
+ requestId: "string >= 1",
111
+ })
112
+
88
113
  export type PayloadHeader<
89
114
  PM extends ProceduresMap,
90
- Name extends keyof PM = keyof PM
115
+ Name extends keyof PM = keyof PM,
91
116
  > = {
92
117
  by: "sw&rpc"
93
118
  functionName: Name & string
94
119
  requestId: string
95
- autotransfer: PM[Name]["autotransfer"]
96
120
  }
97
121
 
122
+ export const PayloadCoreSchema = type("<I, P, S>", {
123
+ "input?": "I",
124
+ "progress?": "P",
125
+ "result?": "S",
126
+ "abort?": { reason: "string" },
127
+ "error?": { message: "string" },
128
+ })
129
+
98
130
  export type PayloadCore<
99
131
  PM extends ProceduresMap,
100
- Name extends keyof PM = keyof PM
132
+ Name extends keyof PM = keyof PM,
101
133
  > =
102
134
  | {
103
135
  input: PM[Name]["input"]["inferOut"]
@@ -108,25 +140,45 @@ export type PayloadCore<
108
140
  | {
109
141
  result: PM[Name]["success"]["inferOut"]
110
142
  }
143
+ | {
144
+ abort: { reason: string }
145
+ }
111
146
  | {
112
147
  error: { message: string }
113
148
  }
114
149
 
150
+ export const PayloadSchema = type
151
+ .scope({ PayloadCoreSchema, PayloadHeaderSchema })
152
+ .type("<Name extends string, I, P, S>", [
153
+ "PayloadHeaderSchema<Name>",
154
+ "&",
155
+ "PayloadCoreSchema<I, P, S>",
156
+ ])
157
+
115
158
  /**
116
159
  * The effective payload as sent by the server to the client
117
160
  */
118
161
  export type Payload<
119
162
  PM extends ProceduresMap,
120
- Name extends keyof PM = keyof PM
163
+ Name extends keyof PM = keyof PM,
121
164
  > = PayloadHeader<PM, Name> & PayloadCore<PM, Name>
122
165
 
123
166
  /**
124
- * A procedure's corresponding method on the client instance -- used to call the procedure
167
+ * A procedure's corresponding method on the client instance -- used to call the procedure. If you want to be able to cancel the request, you can use the `cancelable` method instead of running the procedure directly.
125
168
  */
126
- export type ClientMethod<P extends Procedure<Type, Type, Type>> = (
169
+ export type ClientMethod<P extends Procedure<Type, Type, Type>> = ((
127
170
  input: P["input"]["inferIn"],
128
171
  onProgress?: (progress: P["progress"]["inferOut"]) => void
129
- ) => Promise<P["success"]["inferOut"]>
172
+ ) => Promise<P["success"]["inferOut"]>) & {
173
+ /**
174
+ * A method that returns a `CancelablePromise`. Cancel it by calling `.cancel(reason)` on it, and wait for the request to resolve by awaiting the `request` property on the returned object.
175
+ */
176
+ cancelable: (
177
+ input: P["input"]["inferIn"],
178
+ onProgress?: (progress: P["progress"]["inferOut"]) => void,
179
+ requestId?: string
180
+ ) => CancelablePromise<P["success"]["inferOut"]>
181
+ }
130
182
 
131
183
  /**
132
184
  * Symbol used as the key for the procedures map on the server instance
@@ -138,7 +190,9 @@ export const zImplementations = Symbol("SWARPC implementations")
138
190
  export const zProcedures = Symbol("SWARPC procedures")
139
191
 
140
192
  /**
141
- * The sw&rpc client instance, which provides methods to call procedures
193
+ * The sw&rpc client instance, which provides methods to call procedures.
194
+ * Each property of the procedures map will be a method, that accepts an input, an optional onProgress callback and an optional request ID.
195
+ * If you want to be able to cancel the request, you can set the request's ID yourself, and call `.abort(requestId, reason)` on the client instance to cancel it.
142
196
  */
143
197
  export type SwarpcClient<Procedures extends ProceduresMap> = {
144
198
  [zProcedures]: Procedures
@@ -153,7 +207,7 @@ export type SwarpcClient<Procedures extends ProceduresMap> = {
153
207
  export type SwarpcServer<Procedures extends ProceduresMap> = {
154
208
  [zProcedures]: Procedures
155
209
  [zImplementations]: ImplementationsMap<Procedures>
156
- start(self: Window): void
210
+ start(self: Window | Worker): void
157
211
  } & {
158
212
  [F in keyof Procedures]: (
159
213
  impl: ProcedureImplementation<
package/dist/swarpc.d.ts DELETED
@@ -1,25 +0,0 @@
1
- import { Hooks, type ProceduresMap, type SwarpcClient, type SwarpcServer } from "./types.js";
2
- export type { ProceduresMap, SwarpcClient, SwarpcServer } from "./types.js";
3
- /**
4
- * Creates a sw&rpc server instance.
5
- * @param procedures procedures the server will implement
6
- * @param param1 various options
7
- * @param param1.worker if provided, the server will use this worker to post messages, instead of sending it to all clients
8
- * @returns a SwarpcServer instance. Each property of the procedures map will be a method, that accepts a function implementing the procedure. There is also .start(), to be called after implementing all procedures.
9
- */
10
- export declare function Server<Procedures extends ProceduresMap>(procedures: Procedures, { worker }?: {
11
- worker?: Worker;
12
- }): SwarpcServer<Procedures>;
13
- /**
14
- *
15
- * @param procedures procedures the client will be able to call
16
- * @param param1 various options
17
- * @param param1.worker if provided, the client will use this worker to post messages.
18
- * @param param1.hooks hooks to run on messages received from the server
19
- * @returns a sw&rpc client instance. Each property of the procedures map will be a method, that accepts an input and an optional onProgress callback.
20
- */
21
- export declare function Client<Procedures extends ProceduresMap>(procedures: Procedures, { worker, hooks }?: {
22
- worker?: Worker;
23
- hooks?: Hooks<Procedures>;
24
- }): SwarpcClient<Procedures>;
25
- //# sourceMappingURL=swarpc.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"swarpc.d.ts","sourceRoot":"","sources":["../src/swarpc.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,EAML,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,YAAY,EAClB,MAAM,YAAY,CAAA;AAGnB,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE3E;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,UAAU,SAAS,aAAa,EACrD,UAAU,EAAE,UAAU,EACtB,EAAE,MAAM,EAAE,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,GACnC,YAAY,CAAC,UAAU,CAAC,CAuG1B;AA+FD;;;;;;;GAOG;AACH,wBAAgB,MAAM,CAAC,UAAU,SAAS,aAAa,EACrD,UAAU,EAAE,UAAU,EACtB,EAAE,MAAM,EAAE,KAAU,EAAE,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,KAAK,CAAC,UAAU,CAAC,CAAA;CAAO,GAC1E,YAAY,CAAC,UAAU,CAAC,CA0D1B"}
package/dist/swarpc.js DELETED
@@ -1,264 +0,0 @@
1
- import { type } from "arktype";
2
- import { zImplementations, zProcedures, } from "./types.js";
3
- import { findTransferables } from "./utils.js";
4
- /**
5
- * Creates a sw&rpc server instance.
6
- * @param procedures procedures the server will implement
7
- * @param param1 various options
8
- * @param param1.worker if provided, the server will use this worker to post messages, instead of sending it to all clients
9
- * @returns a SwarpcServer instance. Each property of the procedures map will be a method, that accepts a function implementing the procedure. There is also .start(), to be called after implementing all procedures.
10
- */
11
- export function Server(procedures, { worker } = {}) {
12
- // Initialize the instance.
13
- // Procedures and implementations are stored on properties with symbol keys,
14
- // to avoid any conflicts with procedure names, and also discourage direct access to them.
15
- const instance = {
16
- [zProcedures]: procedures,
17
- [zImplementations]: {},
18
- start: (self) => { },
19
- };
20
- // Set all implementation-setter methods
21
- for (const functionName in procedures) {
22
- instance[functionName] = ((implementation) => {
23
- if (!instance[zProcedures][functionName]) {
24
- throw new Error(`No procedure found for function name: ${functionName}`);
25
- }
26
- instance[zImplementations][functionName] = implementation;
27
- });
28
- }
29
- // Define payload schema for incoming messages
30
- const PayloadSchema = type.or(...Object.entries(procedures).map(([functionName, { input }]) => ({
31
- functionName: type(`"${functionName}"`),
32
- requestId: type("string >= 1"),
33
- input,
34
- })));
35
- instance.start = (self) => {
36
- // Used to post messages back to the client
37
- const postMessage = async (data) => {
38
- const transfer = data.autotransfer === "never" ? [] : findTransferables(data);
39
- if (worker) {
40
- self.postMessage(data, { transfer });
41
- }
42
- else {
43
- await self.clients.matchAll().then((clients) => {
44
- clients.forEach((client) => client.postMessage(data, { transfer }));
45
- });
46
- }
47
- };
48
- // Listen for messages from the client
49
- self.addEventListener("message", async (event) => {
50
- // Decode the payload
51
- const { functionName, requestId, input } = PayloadSchema.assert(event.data);
52
- l.server.debug(requestId, `Received request for ${functionName}`, input);
53
- // Get autotransfer preference from the procedure definition
54
- const { autotransfer = "output-only" } = instance[zProcedures][functionName];
55
- // Shorthand function with functionName, requestId, etc. set
56
- const postMsg = async (data) => postMessage({
57
- by: "sw&rpc",
58
- functionName,
59
- requestId,
60
- autotransfer,
61
- ...data,
62
- });
63
- // Prepare a function to post errors back to the client
64
- const postError = async (error) => postMsg({
65
- error: {
66
- message: "message" in error ? error.message : String(error),
67
- },
68
- });
69
- // Retrieve the implementation for the requested function
70
- const implementation = instance[zImplementations][functionName];
71
- if (!implementation) {
72
- await postError("No implementation found");
73
- return;
74
- }
75
- // Call the implementation with the input and a progress callback
76
- await implementation(input, async (progress) => {
77
- l.server.debug(requestId, `Progress for ${functionName}`, progress);
78
- await postMsg({ progress });
79
- })
80
- // Send errors
81
- .catch(async (error) => {
82
- l.server.error(requestId, `Error in ${functionName}`, error);
83
- await postError(error);
84
- })
85
- // Send results
86
- .then(async (result) => {
87
- l.server.debug(requestId, `Result for ${functionName}`, result);
88
- await postMsg({ result });
89
- });
90
- });
91
- };
92
- return instance;
93
- }
94
- /**
95
- * Generate a random request ID, used to identify requests between client and server.
96
- * @returns a 6-character hexadecimal string
97
- */
98
- function generateRequestId() {
99
- return Math.random().toString(16).substring(2, 8).toUpperCase();
100
- }
101
- /**
102
- * Pending requests are stored in a map, where the key is the request ID.
103
- * Each request has a set of handlers: resolve, reject, and onProgress.
104
- * This allows having a single listener for the client, and having multiple in-flight calls to the same procedure.
105
- */
106
- const pendingRequests = new Map();
107
- // Have we started the client listener?
108
- let _clientListenerStarted = false;
109
- /**
110
- * Starts the client listener, which listens for messages from the sw&rpc server.
111
- * @param worker if provided, the client will use this worker to listen for messages, instead of using the service worker
112
- * @returns
113
- */
114
- async function startClientListener(worker, hooks = {}) {
115
- if (_clientListenerStarted)
116
- return;
117
- // Get service worker registration if no worker is provided
118
- if (!worker) {
119
- const sw = await navigator.serviceWorker.ready;
120
- if (!sw?.active) {
121
- throw new Error("[SWARPC Client] Service Worker is not active");
122
- }
123
- if (!navigator.serviceWorker.controller) {
124
- l.client.warn("", "Service Worker is not controlling the page");
125
- }
126
- }
127
- const w = worker ?? navigator.serviceWorker;
128
- // Start listening for messages
129
- l.client.debug("", "Starting client listener on", w);
130
- w.addEventListener("message", (event) => {
131
- // Get the data from the event
132
- const eventData = event.data || {};
133
- // Ignore other messages that aren't for us
134
- if (eventData?.by !== "sw&rpc")
135
- return;
136
- // We don't use a arktype schema here, we trust the server to send valid data
137
- const { functionName, requestId, ...data } = eventData;
138
- // Sanity check in case we somehow receive a message without requestId
139
- if (!requestId) {
140
- throw new Error("[SWARPC Client] Message received without requestId");
141
- }
142
- // Get the associated pending request handlers
143
- const handlers = pendingRequests.get(requestId);
144
- if (!handlers) {
145
- throw new Error(`[SWARPC Client] ${requestId} has no active request handlers`);
146
- }
147
- // React to the data received: call hook, call handler,
148
- // and remove the request from pendingRequests (unless it's a progress update)
149
- if ("error" in data) {
150
- hooks.error?.(functionName, new Error(data.error.message));
151
- handlers.reject(new Error(data.error.message));
152
- pendingRequests.delete(requestId);
153
- }
154
- else if ("progress" in data) {
155
- hooks.progress?.(functionName, data.progress);
156
- handlers.onProgress(data.progress);
157
- }
158
- else if ("result" in data) {
159
- hooks.success?.(functionName, data.result);
160
- handlers.resolve(data.result);
161
- pendingRequests.delete(requestId);
162
- }
163
- });
164
- _clientListenerStarted = true;
165
- }
166
- /**
167
- *
168
- * @param procedures procedures the client will be able to call
169
- * @param param1 various options
170
- * @param param1.worker if provided, the client will use this worker to post messages.
171
- * @param param1.hooks hooks to run on messages received from the server
172
- * @returns a sw&rpc client instance. Each property of the procedures map will be a method, that accepts an input and an optional onProgress callback.
173
- */
174
- export function Client(procedures, { worker, hooks = {} } = {}) {
175
- // Store procedures on a symbol key, to avoid conflicts with procedure names
176
- const instance = { [zProcedures]: procedures };
177
- for (const functionName of Object.keys(procedures)) {
178
- if (typeof functionName !== "string") {
179
- throw new Error(`[SWARPC Client] Invalid function name, don't use symbols`);
180
- }
181
- // Set the method on the instance
182
- // @ts-expect-error
183
- instance[functionName] = (async (input, onProgress = () => { }) => {
184
- // Validate the input against the procedure's input schema
185
- procedures[functionName].input.assert(input);
186
- // Ensure that we're listening for messages from the server
187
- await startClientListener(worker, hooks);
188
- // If no worker is provided, we use the service worker
189
- const w = worker ?? (await navigator.serviceWorker.ready.then((r) => r.active));
190
- if (!w) {
191
- throw new Error("[SWARPC Client] No active service worker found");
192
- }
193
- return new Promise((resolve, reject) => {
194
- if (!worker && !navigator.serviceWorker.controller)
195
- l.client.warn("", "Service Worker is not controlling the page");
196
- const requestId = generateRequestId();
197
- // Store promise handlers (as well as progress updates handler)
198
- // so the client listener can resolve/reject the promise (and react to progress updates)
199
- // when the server sends messages back
200
- pendingRequests.set(requestId, { resolve, onProgress, reject });
201
- // Post the message to the server
202
- l.client.debug(requestId, `Requesting ${functionName} with`, input);
203
- w.postMessage({ functionName, input, requestId }, {
204
- transfer: procedures[functionName].autotransfer === "always"
205
- ? findTransferables(input)
206
- : [],
207
- });
208
- });
209
- });
210
- }
211
- return instance;
212
- }
213
- /**
214
- * Convenience shortcuts for logging.
215
- */
216
- const l = {
217
- server: {
218
- debug: logger("debug", "server"),
219
- info: logger("info", "server"),
220
- warn: logger("warn", "server"),
221
- error: logger("error", "server"),
222
- },
223
- client: {
224
- debug: logger("debug", "client"),
225
- info: logger("info", "client"),
226
- warn: logger("warn", "client"),
227
- error: logger("error", "client"),
228
- },
229
- };
230
- /**
231
- * Creates partially-applied logging functions given the first 2 args
232
- * @param severity
233
- * @param side
234
- * @returns
235
- */
236
- function logger(severity, side) {
237
- return (rqid, message, ...args) => log(severity, side, rqid, message, ...args);
238
- }
239
- /**
240
- * Send log messages to the console, with a helpful prefix.
241
- * @param severity
242
- * @param side
243
- * @param rqid request ID
244
- * @param message
245
- * @param args passed to console methods directly
246
- */
247
- function log(severity, side, rqid, message, ...args) {
248
- const prefix = "[" +
249
- ["SWARPC", side, rqid ? `%c${rqid}%c` : ""].filter(Boolean).join(" ") +
250
- "]";
251
- const prefixStyles = rqid ? ["color: cyan;", "color: inherit;"] : [];
252
- if (severity === "debug") {
253
- console.debug(prefix, ...prefixStyles, message, ...args);
254
- }
255
- else if (severity === "info") {
256
- console.info(prefix, ...prefixStyles, message, ...args);
257
- }
258
- else if (severity === "warn") {
259
- console.warn(prefix, ...prefixStyles, message, ...args);
260
- }
261
- else if (severity === "error") {
262
- console.error(prefix, ...prefixStyles, message, ...args);
263
- }
264
- }