swarpc 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/swarpc.d.ts +14 -0
- package/dist/swarpc.d.ts.map +1 -1
- package/dist/swarpc.js +140 -12
- package/dist/types.d.ts +50 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +6 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +32 -0
- package/package.json +2 -1
- package/src/swarpc.ts +179 -18
- package/src/types.ts +51 -2
- package/src/utils.ts +40 -0
package/dist/swarpc.d.ts
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
import { type ProceduresMap, type SwarpcClient, type SwarpcServer } from "./types.js";
|
|
2
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
|
+
*/
|
|
3
10
|
export declare function Server<Procedures extends ProceduresMap>(procedures: Procedures, { worker }?: {
|
|
4
11
|
worker?: Worker;
|
|
5
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
|
+
* @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.
|
|
19
|
+
*/
|
|
6
20
|
export declare function Client<Procedures extends ProceduresMap>(procedures: Procedures, { worker }?: {
|
|
7
21
|
worker?: Worker;
|
|
8
22
|
}): SwarpcClient<Procedures>;
|
package/dist/swarpc.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"swarpc.d.ts","sourceRoot":"","sources":["../src/swarpc.ts"],"names":[],"mappings":"AACA,OAAO,
|
|
1
|
+
{"version":3,"file":"swarpc.d.ts","sourceRoot":"","sources":["../src/swarpc.ts"],"names":[],"mappings":"AACA,OAAO,EAKL,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,CA0G1B;AAoFD;;;;;;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,CA0D1B"}
|
package/dist/swarpc.js
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
import { type } from "arktype";
|
|
2
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
|
+
*/
|
|
3
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.
|
|
4
15
|
const instance = {
|
|
5
16
|
[zProcedures]: procedures,
|
|
6
17
|
[zImplementations]: {},
|
|
7
18
|
start: (self) => { },
|
|
8
19
|
};
|
|
20
|
+
// Set all implementation-setter methods
|
|
9
21
|
for (const functionName in procedures) {
|
|
10
22
|
instance[functionName] = ((implementation) => {
|
|
11
23
|
if (!instance[zProcedures][functionName]) {
|
|
@@ -14,77 +26,116 @@ export function Server(procedures, { worker } = {}) {
|
|
|
14
26
|
instance[zImplementations][functionName] = implementation;
|
|
15
27
|
});
|
|
16
28
|
}
|
|
29
|
+
// Define payload schema for incoming messages
|
|
17
30
|
const PayloadSchema = type.or(...Object.entries(procedures).map(([functionName, { input }]) => ({
|
|
18
31
|
functionName: type(`"${functionName}"`),
|
|
19
32
|
requestId: type("string >= 1"),
|
|
20
33
|
input,
|
|
21
34
|
})));
|
|
22
35
|
instance.start = (self) => {
|
|
36
|
+
// Used to post messages back to the client
|
|
23
37
|
const postMessage = async (data) => {
|
|
38
|
+
const transfer = data.autotransfer === "never" ? [] : findTransferables(data);
|
|
24
39
|
if (worker) {
|
|
25
|
-
self.postMessage(data);
|
|
40
|
+
self.postMessage(data, { transfer });
|
|
26
41
|
}
|
|
27
42
|
else {
|
|
28
43
|
await self.clients.matchAll().then((clients) => {
|
|
29
|
-
clients.forEach((client) => client.postMessage(data));
|
|
44
|
+
clients.forEach((client) => client.postMessage(data, { transfer }));
|
|
30
45
|
});
|
|
31
46
|
}
|
|
32
47
|
};
|
|
48
|
+
// Listen for messages from the client
|
|
33
49
|
self.addEventListener("message", async (event) => {
|
|
50
|
+
// Decode the payload
|
|
34
51
|
const { functionName, requestId, input } = PayloadSchema.assert(event.data);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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({ functionName, requestId, autotransfer, ...data });
|
|
57
|
+
// Prepare a function to post errors back to the client
|
|
58
|
+
const postError = async (error) => postMsg({
|
|
38
59
|
error: {
|
|
39
60
|
message: "message" in error ? error.message : String(error),
|
|
40
61
|
},
|
|
41
62
|
});
|
|
63
|
+
// Retrieve the implementation for the requested function
|
|
42
64
|
const implementation = instance[zImplementations][functionName];
|
|
43
65
|
if (!implementation) {
|
|
44
66
|
await postError("No implementation found");
|
|
45
67
|
return;
|
|
46
68
|
}
|
|
69
|
+
// Call the implementation with the input and a progress callback
|
|
47
70
|
await implementation(input, async (progress) => {
|
|
48
|
-
|
|
71
|
+
l.server.debug(requestId, `Progress for ${functionName}`, progress);
|
|
72
|
+
await postMsg({ progress });
|
|
49
73
|
})
|
|
74
|
+
// Send errors
|
|
50
75
|
.catch(async (error) => {
|
|
76
|
+
l.server.error(requestId, `Error in ${functionName}`, error);
|
|
51
77
|
await postError(error);
|
|
52
78
|
})
|
|
79
|
+
// Send results
|
|
53
80
|
.then(async (result) => {
|
|
54
|
-
|
|
81
|
+
l.server.debug(requestId, `Result for ${functionName}`, result);
|
|
82
|
+
await postMsg({ result });
|
|
55
83
|
});
|
|
56
84
|
});
|
|
57
85
|
};
|
|
58
86
|
return instance;
|
|
59
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Generate a random request ID, used to identify requests between client and server.
|
|
90
|
+
* @returns a 6-character hexadecimal string
|
|
91
|
+
*/
|
|
60
92
|
function generateRequestId() {
|
|
61
|
-
return Math.random().toString(
|
|
93
|
+
return Math.random().toString(16).substring(2, 8).toUpperCase();
|
|
62
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Pending requests are stored in a map, where the key is the request ID.
|
|
97
|
+
* Each request has a set of handlers: resolve, reject, and onProgress.
|
|
98
|
+
* This allows having a single listener for the client, and having multiple in-flight calls to the same procedure.
|
|
99
|
+
*/
|
|
63
100
|
const pendingRequests = new Map();
|
|
101
|
+
// Have we started the client listener?
|
|
64
102
|
let _clientListenerStarted = false;
|
|
103
|
+
/**
|
|
104
|
+
* Starts the client listener, which listens for messages from the sw&rpc server.
|
|
105
|
+
* @param worker if provided, the client will use this worker to listen for messages, instead of using the service worker
|
|
106
|
+
* @returns
|
|
107
|
+
*/
|
|
65
108
|
async function startClientListener(worker) {
|
|
66
109
|
if (_clientListenerStarted)
|
|
67
110
|
return;
|
|
111
|
+
// Get service worker registration if no worker is provided
|
|
68
112
|
if (!worker) {
|
|
69
113
|
const sw = await navigator.serviceWorker.ready;
|
|
70
114
|
if (!sw?.active) {
|
|
71
115
|
throw new Error("[SWARPC Client] Service Worker is not active");
|
|
72
116
|
}
|
|
73
117
|
if (!navigator.serviceWorker.controller) {
|
|
74
|
-
|
|
118
|
+
l.client.warn("", "Service Worker is not controlling the page");
|
|
75
119
|
}
|
|
76
120
|
}
|
|
77
121
|
const w = worker ?? navigator.serviceWorker;
|
|
78
|
-
|
|
122
|
+
// Start listening for messages
|
|
123
|
+
l.client.debug("", "Starting client listener on", w);
|
|
79
124
|
w.addEventListener("message", (event) => {
|
|
125
|
+
// Get the data from the event
|
|
126
|
+
// We don't use a arktype schema here, we trust the server to send valid data
|
|
80
127
|
const { functionName, requestId, ...data } = event.data || {};
|
|
128
|
+
// Sanity check in case we somehow receive a message without requestId
|
|
81
129
|
if (!requestId) {
|
|
82
130
|
throw new Error("[SWARPC Client] Message received without requestId");
|
|
83
131
|
}
|
|
132
|
+
// Get the associated pending request handlers
|
|
84
133
|
const handlers = pendingRequests.get(requestId);
|
|
85
134
|
if (!handlers) {
|
|
86
135
|
throw new Error(`[SWARPC Client] ${requestId} has no active request handlers`);
|
|
87
136
|
}
|
|
137
|
+
// React to the data received
|
|
138
|
+
// Unless it's a progress update, the request is finished, thus we can remove it from the pending requests
|
|
88
139
|
if ("error" in data) {
|
|
89
140
|
handlers.reject(new Error(data.error.message));
|
|
90
141
|
pendingRequests.delete(requestId);
|
|
@@ -99,24 +150,101 @@ async function startClientListener(worker) {
|
|
|
99
150
|
});
|
|
100
151
|
_clientListenerStarted = true;
|
|
101
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
*
|
|
155
|
+
* @param procedures procedures the client will be able to call
|
|
156
|
+
* @param param1 various options
|
|
157
|
+
* @param param1.worker if provided, the client will use this worker to post messages.
|
|
158
|
+
* @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.
|
|
159
|
+
*/
|
|
102
160
|
export function Client(procedures, { worker } = {}) {
|
|
161
|
+
// Store procedures on a symbol key, to avoid conflicts with procedure names
|
|
103
162
|
const instance = { [zProcedures]: procedures };
|
|
104
163
|
for (const functionName of Object.keys(procedures)) {
|
|
164
|
+
if (typeof functionName !== "string") {
|
|
165
|
+
throw new Error(`[SWARPC Client] Invalid function name, don't use symbols`);
|
|
166
|
+
}
|
|
167
|
+
// Set the method on the instance
|
|
168
|
+
// @ts-expect-error
|
|
105
169
|
instance[functionName] = (async (input, onProgress = () => { }) => {
|
|
170
|
+
// Validate the input against the procedure's input schema
|
|
106
171
|
procedures[functionName].input.assert(input);
|
|
172
|
+
// Ensure that we're listening for messages from the server
|
|
107
173
|
await startClientListener(worker);
|
|
174
|
+
// If no worker is provided, we use the service worker
|
|
108
175
|
const w = worker ?? (await navigator.serviceWorker.ready.then((r) => r.active));
|
|
109
176
|
if (!w) {
|
|
110
177
|
throw new Error("[SWARPC Client] No active service worker found");
|
|
111
178
|
}
|
|
112
179
|
return new Promise((resolve, reject) => {
|
|
113
180
|
if (!worker && !navigator.serviceWorker.controller)
|
|
114
|
-
|
|
181
|
+
l.client.warn("", "Service Worker is not controlling the page");
|
|
115
182
|
const requestId = generateRequestId();
|
|
183
|
+
// Store promise handlers (as well as progress updates handler)
|
|
184
|
+
// so the client listener can resolve/reject the promise (and react to progress updates)
|
|
185
|
+
// when the server sends messages back
|
|
116
186
|
pendingRequests.set(requestId, { resolve, onProgress, reject });
|
|
117
|
-
|
|
187
|
+
// Post the message to the server
|
|
188
|
+
l.client.debug(requestId, `Requesting ${functionName} with`, input);
|
|
189
|
+
w.postMessage({ functionName, input, requestId }, {
|
|
190
|
+
transfer: procedures[functionName].autotransfer === "always"
|
|
191
|
+
? findTransferables(input)
|
|
192
|
+
: [],
|
|
193
|
+
});
|
|
118
194
|
});
|
|
119
195
|
});
|
|
120
196
|
}
|
|
121
197
|
return instance;
|
|
122
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* Convenience shortcuts for logging.
|
|
201
|
+
*/
|
|
202
|
+
const l = {
|
|
203
|
+
server: {
|
|
204
|
+
debug: logger("debug", "server"),
|
|
205
|
+
info: logger("info", "server"),
|
|
206
|
+
warn: logger("warn", "server"),
|
|
207
|
+
error: logger("error", "server"),
|
|
208
|
+
},
|
|
209
|
+
client: {
|
|
210
|
+
debug: logger("debug", "client"),
|
|
211
|
+
info: logger("info", "client"),
|
|
212
|
+
warn: logger("warn", "client"),
|
|
213
|
+
error: logger("error", "client"),
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
/**
|
|
217
|
+
* Creates partially-applied logging functions given the first 2 args
|
|
218
|
+
* @param severity
|
|
219
|
+
* @param side
|
|
220
|
+
* @returns
|
|
221
|
+
*/
|
|
222
|
+
function logger(severity, side) {
|
|
223
|
+
return (rqid, message, ...args) => log(severity, side, rqid, message, ...args);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Send log messages to the console, with a helpful prefix.
|
|
227
|
+
* @param severity
|
|
228
|
+
* @param side
|
|
229
|
+
* @param rqid request ID
|
|
230
|
+
* @param message
|
|
231
|
+
* @param args passed to console methods directly
|
|
232
|
+
*/
|
|
233
|
+
function log(severity, side, rqid, message, ...args) {
|
|
234
|
+
const prefix = "[" +
|
|
235
|
+
["SWARPC", side, rqid ? `%c${rqid}%c` : ""].filter(Boolean).join(" ") +
|
|
236
|
+
"]";
|
|
237
|
+
const prefixStyles = rqid ? ["color: cyan;", "color: inherit;"] : [];
|
|
238
|
+
if (severity === "debug") {
|
|
239
|
+
console.debug(prefix, ...prefixStyles, message, ...args);
|
|
240
|
+
}
|
|
241
|
+
else if (severity === "info") {
|
|
242
|
+
console.info(prefix, ...prefixStyles, message, ...args);
|
|
243
|
+
}
|
|
244
|
+
else if (severity === "warn") {
|
|
245
|
+
console.warn(prefix, ...prefixStyles, message, ...args);
|
|
246
|
+
}
|
|
247
|
+
else if (severity === "error") {
|
|
248
|
+
console.error(prefix, ...prefixStyles, message, ...args);
|
|
249
|
+
}
|
|
250
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,22 +1,71 @@
|
|
|
1
1
|
import type { Type } from "arktype";
|
|
2
|
+
/**
|
|
3
|
+
* A procedure declaration
|
|
4
|
+
*/
|
|
2
5
|
export type Procedure<I extends Type, P extends Type, S extends Type> = {
|
|
6
|
+
/**
|
|
7
|
+
* ArkType type for the input (first argument) of the procedure, when calling it from the client.
|
|
8
|
+
*/
|
|
3
9
|
input: I;
|
|
10
|
+
/**
|
|
11
|
+
* ArkType type for the data as the first argument given to the `onProgress` callback
|
|
12
|
+
* when calling the procedure from the client.
|
|
13
|
+
*/
|
|
4
14
|
progress: P;
|
|
15
|
+
/**
|
|
16
|
+
* ArkType type for the output (return value) of the procedure, when calling it from the client.
|
|
17
|
+
*/
|
|
5
18
|
success: S;
|
|
19
|
+
/**
|
|
20
|
+
* When should the procedure automatically add ArrayBuffers and other transferable objects
|
|
21
|
+
* to the [transfer list](https://developer.mozilla.org/en-US/docs/Web/API/DedicatedWorkerGlobalScope/postMessage#transfer)
|
|
22
|
+
* when sending messages, both from the client to the server and vice versa.
|
|
23
|
+
*
|
|
24
|
+
* Transferring objects can improve performance by avoiding copies of large objects,
|
|
25
|
+
* but _moves_ them to the other context, meaning that they cannot be used in the original context after being sent.
|
|
26
|
+
*
|
|
27
|
+
* 'output-only' by default: only transferables sent from the server to the client will be transferred.
|
|
28
|
+
*/
|
|
29
|
+
autotransfer?: "always" | "never" | "output-only";
|
|
6
30
|
};
|
|
7
|
-
|
|
31
|
+
/**
|
|
32
|
+
* An implementation of a procedure
|
|
33
|
+
*/
|
|
34
|
+
export type ProcedureImplementation<I extends Type, P extends Type, S extends Type> = (input: I["inferOut"], onProgress: (progress: P["inferIn"]) => void) => Promise<S["inferIn"]>;
|
|
35
|
+
/**
|
|
36
|
+
* Declarations of procedures by name
|
|
37
|
+
*/
|
|
8
38
|
export type ProceduresMap = Record<string, Procedure<Type, Type, Type>>;
|
|
39
|
+
/**
|
|
40
|
+
* Implementations of procedures by name
|
|
41
|
+
*/
|
|
9
42
|
export type ImplementationsMap<Procedures extends ProceduresMap> = {
|
|
10
43
|
[F in keyof Procedures]: ProcedureImplementation<Procedures[F]["input"], Procedures[F]["progress"], Procedures[F]["success"]>;
|
|
11
44
|
};
|
|
45
|
+
/**
|
|
46
|
+
* A procedure's corresponding method on the client instance -- used to call the procedure
|
|
47
|
+
*/
|
|
12
48
|
export type ClientMethod<P extends Procedure<Type, Type, Type>> = (input: P["input"]["inferIn"], onProgress?: (progress: P["progress"]["inferOut"]) => void) => Promise<P["success"]["inferOut"]>;
|
|
49
|
+
/**
|
|
50
|
+
* Symbol used as the key for the procedures map on the server instance
|
|
51
|
+
*/
|
|
13
52
|
export declare const zImplementations: unique symbol;
|
|
53
|
+
/**
|
|
54
|
+
* Symbol used as the key for the procedures map on instances
|
|
55
|
+
*/
|
|
14
56
|
export declare const zProcedures: unique symbol;
|
|
57
|
+
/**
|
|
58
|
+
* The sw&rpc client instance, which provides methods to call procedures
|
|
59
|
+
*/
|
|
15
60
|
export type SwarpcClient<Procedures extends ProceduresMap> = {
|
|
16
61
|
[zProcedures]: Procedures;
|
|
17
62
|
} & {
|
|
18
63
|
[F in keyof Procedures]: ClientMethod<Procedures[F]>;
|
|
19
64
|
};
|
|
65
|
+
/**
|
|
66
|
+
* The sw&rpc server instance, which provides methods to register procedure implementations,
|
|
67
|
+
* and listens for incoming messages that call those procedures
|
|
68
|
+
*/
|
|
20
69
|
export type SwarpcServer<Procedures extends ProceduresMap> = {
|
|
21
70
|
[zProcedures]: Procedures;
|
|
22
71
|
[zImplementations]: ImplementationsMap<Procedures>;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAA;AAEnC,MAAM,MAAM,SAAS,CAAC,CAAC,SAAS,IAAI,EAAE,CAAC,SAAS,IAAI,EAAE,CAAC,SAAS,IAAI,IAAI;IACtE,KAAK,EAAE,CAAC,CAAA;IACR,QAAQ,EAAE,CAAC,CAAA;IACX,OAAO,EAAE,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAA;AAEnC;;GAEG;AACH,MAAM,MAAM,SAAS,CAAC,CAAC,SAAS,IAAI,EAAE,CAAC,SAAS,IAAI,EAAE,CAAC,SAAS,IAAI,IAAI;IACtE;;OAEG;IACH,KAAK,EAAE,CAAC,CAAA;IACR;;;OAGG;IACH,QAAQ,EAAE,CAAC,CAAA;IACX;;OAEG;IACH,OAAO,EAAE,CAAC,CAAA;IACV;;;;;;;;;OASG;IACH,YAAY,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,aAAa,CAAA;CAClD,CAAA;AAED;;GAEG;AACH,MAAM,MAAM,uBAAuB,CACjC,CAAC,SAAS,IAAI,EACd,CAAC,SAAS,IAAI,EACd,CAAC,SAAS,IAAI,IACZ,CACF,KAAK,EAAE,CAAC,CAAC,UAAU,CAAC,EACpB,UAAU,EAAE,CAAC,QAAQ,EAAE,CAAC,CAAC,SAAS,CAAC,KAAK,IAAI,KACzC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAA;AAE1B;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAA;AAEvE;;GAEG;AACH,MAAM,MAAM,kBAAkB,CAAC,UAAU,SAAS,aAAa,IAAI;KAChE,CAAC,IAAI,MAAM,UAAU,GAAG,uBAAuB,CAC9C,UAAU,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EACtB,UAAU,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,EACzB,UAAU,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CACzB;CACF,CAAA;AAED;;GAEG;AACH,MAAM,MAAM,YAAY,CAAC,CAAC,SAAS,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAChE,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,EAC5B,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC,UAAU,CAAC,KAAK,IAAI,KACvD,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,UAAU,CAAC,CAAC,CAAA;AAEtC;;GAEG;AACH,eAAO,MAAM,gBAAgB,eAAmC,CAAA;AAChE;;GAEG;AACH,eAAO,MAAM,WAAW,eAA8B,CAAA;AAEtD;;GAEG;AACH,MAAM,MAAM,YAAY,CAAC,UAAU,SAAS,aAAa,IAAI;IAC3D,CAAC,WAAW,CAAC,EAAE,UAAU,CAAA;CAC1B,GAAG;KACD,CAAC,IAAI,MAAM,UAAU,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;CACrD,CAAA;AAED;;;GAGG;AACH,MAAM,MAAM,YAAY,CAAC,UAAU,SAAS,aAAa,IAAI;IAC3D,CAAC,WAAW,CAAC,EAAE,UAAU,CAAA;IACzB,CAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,UAAU,CAAC,CAAA;IAClD,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1B,GAAG;KACD,CAAC,IAAI,MAAM,UAAU,GAAG,CACvB,IAAI,EAAE,uBAAuB,CAC3B,UAAU,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EACtB,UAAU,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,EACzB,UAAU,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CACzB,KACE,IAAI;CACV,CAAA"}
|
package/dist/types.js
CHANGED
|
@@ -1,2 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symbol used as the key for the procedures map on the server instance
|
|
3
|
+
*/
|
|
1
4
|
export const zImplementations = Symbol("SWARPC implementations");
|
|
5
|
+
/**
|
|
6
|
+
* Symbol used as the key for the procedures map on instances
|
|
7
|
+
*/
|
|
2
8
|
export const zProcedures = Symbol("SWARPC procedures");
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAiBA,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,GAAG,GAAG,YAAY,EAAE,CAsB5D"}
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// TODO: keep it in sync with web standards, how?
|
|
2
|
+
const transferableClasses = [
|
|
3
|
+
OffscreenCanvas,
|
|
4
|
+
ImageBitmap,
|
|
5
|
+
MessagePort,
|
|
6
|
+
MediaSourceHandle,
|
|
7
|
+
ReadableStream,
|
|
8
|
+
WritableStream,
|
|
9
|
+
TransformStream,
|
|
10
|
+
AudioData,
|
|
11
|
+
VideoFrame,
|
|
12
|
+
RTCDataChannel,
|
|
13
|
+
ArrayBuffer,
|
|
14
|
+
];
|
|
15
|
+
export function findTransferables(value) {
|
|
16
|
+
if (value === null || value === undefined) {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
if (typeof value === "object") {
|
|
20
|
+
if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) {
|
|
21
|
+
return [value];
|
|
22
|
+
}
|
|
23
|
+
if (transferableClasses.some((cls) => value instanceof cls)) {
|
|
24
|
+
return [value];
|
|
25
|
+
}
|
|
26
|
+
if (Array.isArray(value)) {
|
|
27
|
+
return value.flatMap(findTransferables);
|
|
28
|
+
}
|
|
29
|
+
return Object.values(value).flatMap(findTransferables);
|
|
30
|
+
}
|
|
31
|
+
return [];
|
|
32
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "swarpc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Full type-safe RPC library for service worker -- move things off of the UI thread with ease!",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"service-workers",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"types": "dist/swarpc.d.ts",
|
|
29
29
|
"scripts": {
|
|
30
30
|
"build": "tsc",
|
|
31
|
+
"dev": "tsc --watch",
|
|
31
32
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
32
33
|
"typedoc": "typedoc src/swarpc.ts src/types.ts --readme README.md"
|
|
33
34
|
},
|
package/src/swarpc.ts
CHANGED
|
@@ -1,25 +1,38 @@
|
|
|
1
|
-
import { type } from "arktype"
|
|
1
|
+
import { type, type Type } from "arktype"
|
|
2
2
|
import {
|
|
3
3
|
ImplementationsMap,
|
|
4
|
+
Procedure,
|
|
4
5
|
zImplementations,
|
|
5
6
|
zProcedures,
|
|
6
7
|
type ProceduresMap,
|
|
7
8
|
type SwarpcClient,
|
|
8
9
|
type SwarpcServer,
|
|
9
10
|
} from "./types.js"
|
|
11
|
+
import { findTransferables } from "./utils.js"
|
|
10
12
|
|
|
11
13
|
export type { ProceduresMap, SwarpcClient, SwarpcServer } from "./types.js"
|
|
12
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Creates a sw&rpc server instance.
|
|
17
|
+
* @param procedures procedures the server will implement
|
|
18
|
+
* @param param1 various options
|
|
19
|
+
* @param param1.worker if provided, the server will use this worker to post messages, instead of sending it to all clients
|
|
20
|
+
* @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.
|
|
21
|
+
*/
|
|
13
22
|
export function Server<Procedures extends ProceduresMap>(
|
|
14
23
|
procedures: Procedures,
|
|
15
24
|
{ worker }: { worker?: Worker } = {}
|
|
16
25
|
): SwarpcServer<Procedures> {
|
|
26
|
+
// Initialize the instance.
|
|
27
|
+
// Procedures and implementations are stored on properties with symbol keys,
|
|
28
|
+
// to avoid any conflicts with procedure names, and also discourage direct access to them.
|
|
17
29
|
const instance = {
|
|
18
30
|
[zProcedures]: procedures,
|
|
19
31
|
[zImplementations]: {} as ImplementationsMap<Procedures>,
|
|
20
32
|
start: (self: Window) => {},
|
|
21
33
|
} as SwarpcServer<Procedures>
|
|
22
34
|
|
|
35
|
+
// Set all implementation-setter methods
|
|
23
36
|
for (const functionName in procedures) {
|
|
24
37
|
instance[functionName] = ((implementation) => {
|
|
25
38
|
if (!instance[zProcedures][functionName]) {
|
|
@@ -29,6 +42,7 @@ export function Server<Procedures extends ProceduresMap>(
|
|
|
29
42
|
}) as SwarpcServer<Procedures>[typeof functionName]
|
|
30
43
|
}
|
|
31
44
|
|
|
45
|
+
// Define payload schema for incoming messages
|
|
32
46
|
const PayloadSchema = type.or(
|
|
33
47
|
...Object.entries(procedures).map(([functionName, { input }]) => ({
|
|
34
48
|
functionName: type(`"${functionName}"`),
|
|
@@ -38,49 +52,77 @@ export function Server<Procedures extends ProceduresMap>(
|
|
|
38
52
|
)
|
|
39
53
|
|
|
40
54
|
instance.start = (self: Window) => {
|
|
55
|
+
// Used to post messages back to the client
|
|
41
56
|
const postMessage = async (
|
|
42
|
-
data: {
|
|
57
|
+
data: {
|
|
58
|
+
functionName: string
|
|
59
|
+
requestId: string
|
|
60
|
+
autotransfer: Procedure<Type, Type, Type>["autotransfer"]
|
|
61
|
+
} & Partial<{
|
|
43
62
|
result: any
|
|
44
63
|
error: any
|
|
45
64
|
progress: any
|
|
46
65
|
}>
|
|
47
66
|
) => {
|
|
67
|
+
const transfer =
|
|
68
|
+
data.autotransfer === "never" ? [] : findTransferables(data)
|
|
69
|
+
|
|
48
70
|
if (worker) {
|
|
49
|
-
self.postMessage(data)
|
|
71
|
+
self.postMessage(data, { transfer })
|
|
50
72
|
} else {
|
|
51
73
|
await (self as any).clients.matchAll().then((clients: any[]) => {
|
|
52
|
-
clients.forEach((client) => client.postMessage(data))
|
|
74
|
+
clients.forEach((client) => client.postMessage(data, { transfer }))
|
|
53
75
|
})
|
|
54
76
|
}
|
|
55
77
|
}
|
|
56
78
|
|
|
79
|
+
// Listen for messages from the client
|
|
57
80
|
self.addEventListener("message", async (event: MessageEvent) => {
|
|
81
|
+
// Decode the payload
|
|
58
82
|
const { functionName, requestId, input } = PayloadSchema.assert(
|
|
59
83
|
event.data
|
|
60
84
|
)
|
|
85
|
+
|
|
86
|
+
l.server.debug(requestId, `Received request for ${functionName}`, input)
|
|
87
|
+
|
|
88
|
+
// Get autotransfer preference from the procedure definition
|
|
89
|
+
const { autotransfer = "output-only" } =
|
|
90
|
+
instance[zProcedures][functionName]
|
|
91
|
+
|
|
92
|
+
// Shorthand function with functionName, requestId, etc. set
|
|
93
|
+
const postMsg = async (
|
|
94
|
+
data: { result: any } | { error: any } | { progress: any }
|
|
95
|
+
) => postMessage({ functionName, requestId, autotransfer, ...data })
|
|
96
|
+
|
|
97
|
+
// Prepare a function to post errors back to the client
|
|
61
98
|
const postError = async (error: any) =>
|
|
62
|
-
|
|
63
|
-
functionName,
|
|
64
|
-
requestId,
|
|
99
|
+
postMsg({
|
|
65
100
|
error: {
|
|
66
101
|
message: "message" in error ? error.message : String(error),
|
|
67
102
|
},
|
|
68
103
|
})
|
|
69
104
|
|
|
105
|
+
// Retrieve the implementation for the requested function
|
|
70
106
|
const implementation = instance[zImplementations][functionName]
|
|
71
107
|
if (!implementation) {
|
|
72
108
|
await postError("No implementation found")
|
|
73
109
|
return
|
|
74
110
|
}
|
|
75
111
|
|
|
112
|
+
// Call the implementation with the input and a progress callback
|
|
76
113
|
await implementation(input, async (progress: any) => {
|
|
77
|
-
|
|
114
|
+
l.server.debug(requestId, `Progress for ${functionName}`, progress)
|
|
115
|
+
await postMsg({ progress })
|
|
78
116
|
})
|
|
117
|
+
// Send errors
|
|
79
118
|
.catch(async (error: any) => {
|
|
119
|
+
l.server.error(requestId, `Error in ${functionName}`, error)
|
|
80
120
|
await postError(error)
|
|
81
121
|
})
|
|
122
|
+
// Send results
|
|
82
123
|
.then(async (result: any) => {
|
|
83
|
-
|
|
124
|
+
l.server.debug(requestId, `Result for ${functionName}`, result)
|
|
125
|
+
await postMsg({ result })
|
|
84
126
|
})
|
|
85
127
|
})
|
|
86
128
|
}
|
|
@@ -88,22 +130,38 @@ export function Server<Procedures extends ProceduresMap>(
|
|
|
88
130
|
return instance
|
|
89
131
|
}
|
|
90
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Generate a random request ID, used to identify requests between client and server.
|
|
135
|
+
* @returns a 6-character hexadecimal string
|
|
136
|
+
*/
|
|
91
137
|
function generateRequestId(): string {
|
|
92
|
-
return Math.random().toString(
|
|
138
|
+
return Math.random().toString(16).substring(2, 8).toUpperCase()
|
|
93
139
|
}
|
|
94
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Pending requests are stored in a map, where the key is the request ID.
|
|
143
|
+
* Each request has a set of handlers: resolve, reject, and onProgress.
|
|
144
|
+
* This allows having a single listener for the client, and having multiple in-flight calls to the same procedure.
|
|
145
|
+
*/
|
|
146
|
+
const pendingRequests = new Map<string, PendingRequest>()
|
|
95
147
|
type PendingRequest = {
|
|
96
148
|
reject: (err: Error) => void
|
|
97
149
|
onProgress: (progress: any) => void
|
|
98
150
|
resolve: (result: any) => void
|
|
99
151
|
}
|
|
100
152
|
|
|
101
|
-
|
|
102
|
-
|
|
153
|
+
// Have we started the client listener?
|
|
103
154
|
let _clientListenerStarted = false
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Starts the client listener, which listens for messages from the sw&rpc server.
|
|
158
|
+
* @param worker if provided, the client will use this worker to listen for messages, instead of using the service worker
|
|
159
|
+
* @returns
|
|
160
|
+
*/
|
|
104
161
|
async function startClientListener(worker?: Worker) {
|
|
105
162
|
if (_clientListenerStarted) return
|
|
106
163
|
|
|
164
|
+
// Get service worker registration if no worker is provided
|
|
107
165
|
if (!worker) {
|
|
108
166
|
const sw = await navigator.serviceWorker.ready
|
|
109
167
|
if (!sw?.active) {
|
|
@@ -111,20 +169,26 @@ async function startClientListener(worker?: Worker) {
|
|
|
111
169
|
}
|
|
112
170
|
|
|
113
171
|
if (!navigator.serviceWorker.controller) {
|
|
114
|
-
|
|
172
|
+
l.client.warn("", "Service Worker is not controlling the page")
|
|
115
173
|
}
|
|
116
174
|
}
|
|
117
175
|
|
|
118
176
|
const w = worker ?? navigator.serviceWorker
|
|
119
|
-
|
|
177
|
+
|
|
178
|
+
// Start listening for messages
|
|
179
|
+
l.client.debug("", "Starting client listener on", w)
|
|
120
180
|
w.addEventListener("message", (event) => {
|
|
181
|
+
// Get the data from the event
|
|
182
|
+
// We don't use a arktype schema here, we trust the server to send valid data
|
|
121
183
|
const { functionName, requestId, ...data } =
|
|
122
184
|
(event as MessageEvent).data || {}
|
|
123
185
|
|
|
186
|
+
// Sanity check in case we somehow receive a message without requestId
|
|
124
187
|
if (!requestId) {
|
|
125
188
|
throw new Error("[SWARPC Client] Message received without requestId")
|
|
126
189
|
}
|
|
127
190
|
|
|
191
|
+
// Get the associated pending request handlers
|
|
128
192
|
const handlers = pendingRequests.get(requestId)
|
|
129
193
|
if (!handlers) {
|
|
130
194
|
throw new Error(
|
|
@@ -132,6 +196,8 @@ async function startClientListener(worker?: Worker) {
|
|
|
132
196
|
)
|
|
133
197
|
}
|
|
134
198
|
|
|
199
|
+
// React to the data received
|
|
200
|
+
// Unless it's a progress update, the request is finished, thus we can remove it from the pending requests
|
|
135
201
|
if ("error" in data) {
|
|
136
202
|
handlers.reject(new Error(data.error.message))
|
|
137
203
|
pendingRequests.delete(requestId)
|
|
@@ -146,10 +212,18 @@ async function startClientListener(worker?: Worker) {
|
|
|
146
212
|
_clientListenerStarted = true
|
|
147
213
|
}
|
|
148
214
|
|
|
215
|
+
/**
|
|
216
|
+
*
|
|
217
|
+
* @param procedures procedures the client will be able to call
|
|
218
|
+
* @param param1 various options
|
|
219
|
+
* @param param1.worker if provided, the client will use this worker to post messages.
|
|
220
|
+
* @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.
|
|
221
|
+
*/
|
|
149
222
|
export function Client<Procedures extends ProceduresMap>(
|
|
150
223
|
procedures: Procedures,
|
|
151
224
|
{ worker }: { worker?: Worker } = {}
|
|
152
225
|
): SwarpcClient<Procedures> {
|
|
226
|
+
// Store procedures on a symbol key, to avoid conflicts with procedure names
|
|
153
227
|
const instance = { [zProcedures]: procedures } as Partial<
|
|
154
228
|
SwarpcClient<Procedures>
|
|
155
229
|
>
|
|
@@ -157,10 +231,21 @@ export function Client<Procedures extends ProceduresMap>(
|
|
|
157
231
|
for (const functionName of Object.keys(procedures) as Array<
|
|
158
232
|
keyof Procedures
|
|
159
233
|
>) {
|
|
234
|
+
if (typeof functionName !== "string") {
|
|
235
|
+
throw new Error(
|
|
236
|
+
`[SWARPC Client] Invalid function name, don't use symbols`
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Set the method on the instance
|
|
241
|
+
// @ts-expect-error
|
|
160
242
|
instance[functionName] = (async (input: unknown, onProgress = () => {}) => {
|
|
243
|
+
// Validate the input against the procedure's input schema
|
|
161
244
|
procedures[functionName].input.assert(input)
|
|
245
|
+
// Ensure that we're listening for messages from the server
|
|
162
246
|
await startClientListener(worker)
|
|
163
247
|
|
|
248
|
+
// If no worker is provided, we use the service worker
|
|
164
249
|
const w =
|
|
165
250
|
worker ?? (await navigator.serviceWorker.ready.then((r) => r.active))
|
|
166
251
|
|
|
@@ -170,18 +255,94 @@ export function Client<Procedures extends ProceduresMap>(
|
|
|
170
255
|
|
|
171
256
|
return new Promise((resolve, reject) => {
|
|
172
257
|
if (!worker && !navigator.serviceWorker.controller)
|
|
173
|
-
|
|
174
|
-
"[SWARPC Client] Service Worker is not controlling the page"
|
|
175
|
-
)
|
|
258
|
+
l.client.warn("", "Service Worker is not controlling the page")
|
|
176
259
|
|
|
177
260
|
const requestId = generateRequestId()
|
|
178
261
|
|
|
262
|
+
// Store promise handlers (as well as progress updates handler)
|
|
263
|
+
// so the client listener can resolve/reject the promise (and react to progress updates)
|
|
264
|
+
// when the server sends messages back
|
|
179
265
|
pendingRequests.set(requestId, { resolve, onProgress, reject })
|
|
180
266
|
|
|
181
|
-
|
|
267
|
+
// Post the message to the server
|
|
268
|
+
l.client.debug(requestId, `Requesting ${functionName} with`, input)
|
|
269
|
+
w.postMessage(
|
|
270
|
+
{ functionName, input, requestId },
|
|
271
|
+
{
|
|
272
|
+
transfer:
|
|
273
|
+
procedures[functionName].autotransfer === "always"
|
|
274
|
+
? findTransferables(input)
|
|
275
|
+
: [],
|
|
276
|
+
}
|
|
277
|
+
)
|
|
182
278
|
})
|
|
183
279
|
}) as SwarpcClient<Procedures>[typeof functionName]
|
|
184
280
|
}
|
|
185
281
|
|
|
186
282
|
return instance as SwarpcClient<Procedures>
|
|
187
283
|
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Convenience shortcuts for logging.
|
|
287
|
+
*/
|
|
288
|
+
const l = {
|
|
289
|
+
server: {
|
|
290
|
+
debug: logger("debug", "server"),
|
|
291
|
+
info: logger("info", "server"),
|
|
292
|
+
warn: logger("warn", "server"),
|
|
293
|
+
error: logger("error", "server"),
|
|
294
|
+
},
|
|
295
|
+
client: {
|
|
296
|
+
debug: logger("debug", "client"),
|
|
297
|
+
info: logger("info", "client"),
|
|
298
|
+
warn: logger("warn", "client"),
|
|
299
|
+
error: logger("error", "client"),
|
|
300
|
+
},
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Creates partially-applied logging functions given the first 2 args
|
|
305
|
+
* @param severity
|
|
306
|
+
* @param side
|
|
307
|
+
* @returns
|
|
308
|
+
*/
|
|
309
|
+
function logger(
|
|
310
|
+
severity: "debug" | "info" | "warn" | "error",
|
|
311
|
+
side: "server" | "client"
|
|
312
|
+
) {
|
|
313
|
+
return (rqid: string | null, message: string, ...args: any[]) =>
|
|
314
|
+
log(severity, side, rqid, message, ...args)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Send log messages to the console, with a helpful prefix.
|
|
319
|
+
* @param severity
|
|
320
|
+
* @param side
|
|
321
|
+
* @param rqid request ID
|
|
322
|
+
* @param message
|
|
323
|
+
* @param args passed to console methods directly
|
|
324
|
+
*/
|
|
325
|
+
function log(
|
|
326
|
+
severity: "debug" | "info" | "warn" | "error",
|
|
327
|
+
side: "server" | "client",
|
|
328
|
+
rqid: string | null,
|
|
329
|
+
message: string,
|
|
330
|
+
...args: any[]
|
|
331
|
+
) {
|
|
332
|
+
const prefix =
|
|
333
|
+
"[" +
|
|
334
|
+
["SWARPC", side, rqid ? `%c${rqid}%c` : ""].filter(Boolean).join(" ") +
|
|
335
|
+
"]"
|
|
336
|
+
|
|
337
|
+
const prefixStyles = rqid ? ["color: cyan;", "color: inherit;"] : []
|
|
338
|
+
|
|
339
|
+
if (severity === "debug") {
|
|
340
|
+
console.debug(prefix, ...prefixStyles, message, ...args)
|
|
341
|
+
} else if (severity === "info") {
|
|
342
|
+
console.info(prefix, ...prefixStyles, message, ...args)
|
|
343
|
+
} else if (severity === "warn") {
|
|
344
|
+
console.warn(prefix, ...prefixStyles, message, ...args)
|
|
345
|
+
} else if (severity === "error") {
|
|
346
|
+
console.error(prefix, ...prefixStyles, message, ...args)
|
|
347
|
+
}
|
|
348
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,22 +1,55 @@
|
|
|
1
1
|
import type { Type } from "arktype"
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* A procedure declaration
|
|
5
|
+
*/
|
|
3
6
|
export type Procedure<I extends Type, P extends Type, S extends Type> = {
|
|
7
|
+
/**
|
|
8
|
+
* ArkType type for the input (first argument) of the procedure, when calling it from the client.
|
|
9
|
+
*/
|
|
4
10
|
input: I
|
|
11
|
+
/**
|
|
12
|
+
* ArkType type for the data as the first argument given to the `onProgress` callback
|
|
13
|
+
* when calling the procedure from the client.
|
|
14
|
+
*/
|
|
5
15
|
progress: P
|
|
16
|
+
/**
|
|
17
|
+
* ArkType type for the output (return value) of the procedure, when calling it from the client.
|
|
18
|
+
*/
|
|
6
19
|
success: S
|
|
20
|
+
/**
|
|
21
|
+
* When should the procedure automatically add ArrayBuffers and other transferable objects
|
|
22
|
+
* to the [transfer list](https://developer.mozilla.org/en-US/docs/Web/API/DedicatedWorkerGlobalScope/postMessage#transfer)
|
|
23
|
+
* when sending messages, both from the client to the server and vice versa.
|
|
24
|
+
*
|
|
25
|
+
* Transferring objects can improve performance by avoiding copies of large objects,
|
|
26
|
+
* but _moves_ them to the other context, meaning that they cannot be used in the original context after being sent.
|
|
27
|
+
*
|
|
28
|
+
* 'output-only' by default: only transferables sent from the server to the client will be transferred.
|
|
29
|
+
*/
|
|
30
|
+
autotransfer?: "always" | "never" | "output-only"
|
|
7
31
|
}
|
|
8
32
|
|
|
33
|
+
/**
|
|
34
|
+
* An implementation of a procedure
|
|
35
|
+
*/
|
|
9
36
|
export type ProcedureImplementation<
|
|
10
37
|
I extends Type,
|
|
11
38
|
P extends Type,
|
|
12
39
|
S extends Type
|
|
13
40
|
> = (
|
|
14
41
|
input: I["inferOut"],
|
|
15
|
-
onProgress: (progress: P["
|
|
16
|
-
) => Promise<S["
|
|
42
|
+
onProgress: (progress: P["inferIn"]) => void
|
|
43
|
+
) => Promise<S["inferIn"]>
|
|
17
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Declarations of procedures by name
|
|
47
|
+
*/
|
|
18
48
|
export type ProceduresMap = Record<string, Procedure<Type, Type, Type>>
|
|
19
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Implementations of procedures by name
|
|
52
|
+
*/
|
|
20
53
|
export type ImplementationsMap<Procedures extends ProceduresMap> = {
|
|
21
54
|
[F in keyof Procedures]: ProcedureImplementation<
|
|
22
55
|
Procedures[F]["input"],
|
|
@@ -25,20 +58,36 @@ export type ImplementationsMap<Procedures extends ProceduresMap> = {
|
|
|
25
58
|
>
|
|
26
59
|
}
|
|
27
60
|
|
|
61
|
+
/**
|
|
62
|
+
* A procedure's corresponding method on the client instance -- used to call the procedure
|
|
63
|
+
*/
|
|
28
64
|
export type ClientMethod<P extends Procedure<Type, Type, Type>> = (
|
|
29
65
|
input: P["input"]["inferIn"],
|
|
30
66
|
onProgress?: (progress: P["progress"]["inferOut"]) => void
|
|
31
67
|
) => Promise<P["success"]["inferOut"]>
|
|
32
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Symbol used as the key for the procedures map on the server instance
|
|
71
|
+
*/
|
|
33
72
|
export const zImplementations = Symbol("SWARPC implementations")
|
|
73
|
+
/**
|
|
74
|
+
* Symbol used as the key for the procedures map on instances
|
|
75
|
+
*/
|
|
34
76
|
export const zProcedures = Symbol("SWARPC procedures")
|
|
35
77
|
|
|
78
|
+
/**
|
|
79
|
+
* The sw&rpc client instance, which provides methods to call procedures
|
|
80
|
+
*/
|
|
36
81
|
export type SwarpcClient<Procedures extends ProceduresMap> = {
|
|
37
82
|
[zProcedures]: Procedures
|
|
38
83
|
} & {
|
|
39
84
|
[F in keyof Procedures]: ClientMethod<Procedures[F]>
|
|
40
85
|
}
|
|
41
86
|
|
|
87
|
+
/**
|
|
88
|
+
* The sw&rpc server instance, which provides methods to register procedure implementations,
|
|
89
|
+
* and listens for incoming messages that call those procedures
|
|
90
|
+
*/
|
|
42
91
|
export type SwarpcServer<Procedures extends ProceduresMap> = {
|
|
43
92
|
[zProcedures]: Procedures
|
|
44
93
|
[zImplementations]: ImplementationsMap<Procedures>
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
type Constructor<T> = new (...args: any[]) => T
|
|
2
|
+
|
|
3
|
+
// TODO: keep it in sync with web standards, how?
|
|
4
|
+
const transferableClasses: Constructor<Transferable>[] = [
|
|
5
|
+
OffscreenCanvas,
|
|
6
|
+
ImageBitmap,
|
|
7
|
+
MessagePort,
|
|
8
|
+
MediaSourceHandle,
|
|
9
|
+
ReadableStream,
|
|
10
|
+
WritableStream,
|
|
11
|
+
TransformStream,
|
|
12
|
+
AudioData,
|
|
13
|
+
VideoFrame,
|
|
14
|
+
RTCDataChannel,
|
|
15
|
+
ArrayBuffer,
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
export function findTransferables(value: any): Transferable[] {
|
|
19
|
+
if (value === null || value === undefined) {
|
|
20
|
+
return []
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (typeof value === "object") {
|
|
24
|
+
if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) {
|
|
25
|
+
return [value]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (transferableClasses.some((cls) => value instanceof cls)) {
|
|
29
|
+
return [value as Transferable]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (Array.isArray(value)) {
|
|
33
|
+
return value.flatMap(findTransferables)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return Object.values(value).flatMap(findTransferables)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return []
|
|
40
|
+
}
|