swarpc 0.4.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 +103 -16
- 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 +129 -28
- 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,61 +26,89 @@ 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
52
|
l.server.debug(requestId, `Received request for ${functionName}`, input);
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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({
|
|
39
59
|
error: {
|
|
40
60
|
message: "message" in error ? error.message : String(error),
|
|
41
61
|
},
|
|
42
62
|
});
|
|
63
|
+
// Retrieve the implementation for the requested function
|
|
43
64
|
const implementation = instance[zImplementations][functionName];
|
|
44
65
|
if (!implementation) {
|
|
45
66
|
await postError("No implementation found");
|
|
46
67
|
return;
|
|
47
68
|
}
|
|
69
|
+
// Call the implementation with the input and a progress callback
|
|
48
70
|
await implementation(input, async (progress) => {
|
|
49
71
|
l.server.debug(requestId, `Progress for ${functionName}`, progress);
|
|
50
|
-
await
|
|
72
|
+
await postMsg({ progress });
|
|
51
73
|
})
|
|
74
|
+
// Send errors
|
|
52
75
|
.catch(async (error) => {
|
|
53
76
|
l.server.error(requestId, `Error in ${functionName}`, error);
|
|
54
77
|
await postError(error);
|
|
55
78
|
})
|
|
79
|
+
// Send results
|
|
56
80
|
.then(async (result) => {
|
|
57
81
|
l.server.debug(requestId, `Result for ${functionName}`, result);
|
|
58
|
-
await
|
|
82
|
+
await postMsg({ result });
|
|
59
83
|
});
|
|
60
84
|
});
|
|
61
85
|
};
|
|
62
86
|
return instance;
|
|
63
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Generate a random request ID, used to identify requests between client and server.
|
|
90
|
+
* @returns a 6-character hexadecimal string
|
|
91
|
+
*/
|
|
64
92
|
function generateRequestId() {
|
|
65
93
|
return Math.random().toString(16).substring(2, 8).toUpperCase();
|
|
66
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
|
+
*/
|
|
67
100
|
const pendingRequests = new Map();
|
|
101
|
+
// Have we started the client listener?
|
|
68
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
|
+
*/
|
|
69
108
|
async function startClientListener(worker) {
|
|
70
109
|
if (_clientListenerStarted)
|
|
71
110
|
return;
|
|
111
|
+
// Get service worker registration if no worker is provided
|
|
72
112
|
if (!worker) {
|
|
73
113
|
const sw = await navigator.serviceWorker.ready;
|
|
74
114
|
if (!sw?.active) {
|
|
@@ -79,16 +119,23 @@ async function startClientListener(worker) {
|
|
|
79
119
|
}
|
|
80
120
|
}
|
|
81
121
|
const w = worker ?? navigator.serviceWorker;
|
|
122
|
+
// Start listening for messages
|
|
82
123
|
l.client.debug("", "Starting client listener on", w);
|
|
83
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
|
|
84
127
|
const { functionName, requestId, ...data } = event.data || {};
|
|
128
|
+
// Sanity check in case we somehow receive a message without requestId
|
|
85
129
|
if (!requestId) {
|
|
86
130
|
throw new Error("[SWARPC Client] Message received without requestId");
|
|
87
131
|
}
|
|
132
|
+
// Get the associated pending request handlers
|
|
88
133
|
const handlers = pendingRequests.get(requestId);
|
|
89
134
|
if (!handlers) {
|
|
90
135
|
throw new Error(`[SWARPC Client] ${requestId} has no active request handlers`);
|
|
91
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
|
|
92
139
|
if ("error" in data) {
|
|
93
140
|
handlers.reject(new Error(data.error.message));
|
|
94
141
|
pendingRequests.delete(requestId);
|
|
@@ -103,16 +150,28 @@ async function startClientListener(worker) {
|
|
|
103
150
|
});
|
|
104
151
|
_clientListenerStarted = true;
|
|
105
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
|
+
*/
|
|
106
160
|
export function Client(procedures, { worker } = {}) {
|
|
161
|
+
// Store procedures on a symbol key, to avoid conflicts with procedure names
|
|
107
162
|
const instance = { [zProcedures]: procedures };
|
|
108
163
|
for (const functionName of Object.keys(procedures)) {
|
|
109
164
|
if (typeof functionName !== "string") {
|
|
110
165
|
throw new Error(`[SWARPC Client] Invalid function name, don't use symbols`);
|
|
111
166
|
}
|
|
167
|
+
// Set the method on the instance
|
|
112
168
|
// @ts-expect-error
|
|
113
169
|
instance[functionName] = (async (input, onProgress = () => { }) => {
|
|
170
|
+
// Validate the input against the procedure's input schema
|
|
114
171
|
procedures[functionName].input.assert(input);
|
|
172
|
+
// Ensure that we're listening for messages from the server
|
|
115
173
|
await startClientListener(worker);
|
|
174
|
+
// If no worker is provided, we use the service worker
|
|
116
175
|
const w = worker ?? (await navigator.serviceWorker.ready.then((r) => r.active));
|
|
117
176
|
if (!w) {
|
|
118
177
|
throw new Error("[SWARPC Client] No active service worker found");
|
|
@@ -121,28 +180,56 @@ export function Client(procedures, { worker } = {}) {
|
|
|
121
180
|
if (!worker && !navigator.serviceWorker.controller)
|
|
122
181
|
l.client.warn("", "Service Worker is not controlling the page");
|
|
123
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
|
|
124
186
|
pendingRequests.set(requestId, { resolve, onProgress, reject });
|
|
187
|
+
// Post the message to the server
|
|
125
188
|
l.client.debug(requestId, `Requesting ${functionName} with`, input);
|
|
126
|
-
w.postMessage({ functionName, input, requestId }
|
|
189
|
+
w.postMessage({ functionName, input, requestId }, {
|
|
190
|
+
transfer: procedures[functionName].autotransfer === "always"
|
|
191
|
+
? findTransferables(input)
|
|
192
|
+
: [],
|
|
193
|
+
});
|
|
127
194
|
});
|
|
128
195
|
});
|
|
129
196
|
}
|
|
130
197
|
return instance;
|
|
131
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* Convenience shortcuts for logging.
|
|
201
|
+
*/
|
|
132
202
|
const l = {
|
|
133
203
|
server: {
|
|
134
|
-
debug: (
|
|
135
|
-
info: (
|
|
136
|
-
warn: (
|
|
137
|
-
error: (
|
|
204
|
+
debug: logger("debug", "server"),
|
|
205
|
+
info: logger("info", "server"),
|
|
206
|
+
warn: logger("warn", "server"),
|
|
207
|
+
error: logger("error", "server"),
|
|
138
208
|
},
|
|
139
209
|
client: {
|
|
140
|
-
debug: (
|
|
141
|
-
info: (
|
|
142
|
-
warn: (
|
|
143
|
-
error: (
|
|
210
|
+
debug: logger("debug", "client"),
|
|
211
|
+
info: logger("info", "client"),
|
|
212
|
+
warn: logger("warn", "client"),
|
|
213
|
+
error: logger("error", "client"),
|
|
144
214
|
},
|
|
145
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
|
+
*/
|
|
146
233
|
function log(severity, side, rqid, message, ...args) {
|
|
147
234
|
const prefix = "[" +
|
|
148
235
|
["SWARPC", side, rqid ? `%c${rqid}%c` : ""].filter(Boolean).join(" ") +
|
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,55 +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
|
)
|
|
61
85
|
|
|
62
86
|
l.server.debug(requestId, `Received request for ${functionName}`, input)
|
|
63
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
|
|
64
98
|
const postError = async (error: any) =>
|
|
65
|
-
|
|
66
|
-
functionName,
|
|
67
|
-
requestId,
|
|
99
|
+
postMsg({
|
|
68
100
|
error: {
|
|
69
101
|
message: "message" in error ? error.message : String(error),
|
|
70
102
|
},
|
|
71
103
|
})
|
|
72
104
|
|
|
105
|
+
// Retrieve the implementation for the requested function
|
|
73
106
|
const implementation = instance[zImplementations][functionName]
|
|
74
107
|
if (!implementation) {
|
|
75
108
|
await postError("No implementation found")
|
|
76
109
|
return
|
|
77
110
|
}
|
|
78
111
|
|
|
112
|
+
// Call the implementation with the input and a progress callback
|
|
79
113
|
await implementation(input, async (progress: any) => {
|
|
80
114
|
l.server.debug(requestId, `Progress for ${functionName}`, progress)
|
|
81
|
-
await
|
|
115
|
+
await postMsg({ progress })
|
|
82
116
|
})
|
|
117
|
+
// Send errors
|
|
83
118
|
.catch(async (error: any) => {
|
|
84
119
|
l.server.error(requestId, `Error in ${functionName}`, error)
|
|
85
120
|
await postError(error)
|
|
86
121
|
})
|
|
122
|
+
// Send results
|
|
87
123
|
.then(async (result: any) => {
|
|
88
124
|
l.server.debug(requestId, `Result for ${functionName}`, result)
|
|
89
|
-
await
|
|
125
|
+
await postMsg({ result })
|
|
90
126
|
})
|
|
91
127
|
})
|
|
92
128
|
}
|
|
@@ -94,22 +130,38 @@ export function Server<Procedures extends ProceduresMap>(
|
|
|
94
130
|
return instance
|
|
95
131
|
}
|
|
96
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Generate a random request ID, used to identify requests between client and server.
|
|
135
|
+
* @returns a 6-character hexadecimal string
|
|
136
|
+
*/
|
|
97
137
|
function generateRequestId(): string {
|
|
98
138
|
return Math.random().toString(16).substring(2, 8).toUpperCase()
|
|
99
139
|
}
|
|
100
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>()
|
|
101
147
|
type PendingRequest = {
|
|
102
148
|
reject: (err: Error) => void
|
|
103
149
|
onProgress: (progress: any) => void
|
|
104
150
|
resolve: (result: any) => void
|
|
105
151
|
}
|
|
106
152
|
|
|
107
|
-
|
|
108
|
-
|
|
153
|
+
// Have we started the client listener?
|
|
109
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
|
+
*/
|
|
110
161
|
async function startClientListener(worker?: Worker) {
|
|
111
162
|
if (_clientListenerStarted) return
|
|
112
163
|
|
|
164
|
+
// Get service worker registration if no worker is provided
|
|
113
165
|
if (!worker) {
|
|
114
166
|
const sw = await navigator.serviceWorker.ready
|
|
115
167
|
if (!sw?.active) {
|
|
@@ -122,15 +174,21 @@ async function startClientListener(worker?: Worker) {
|
|
|
122
174
|
}
|
|
123
175
|
|
|
124
176
|
const w = worker ?? navigator.serviceWorker
|
|
177
|
+
|
|
178
|
+
// Start listening for messages
|
|
125
179
|
l.client.debug("", "Starting client listener on", w)
|
|
126
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
|
|
127
183
|
const { functionName, requestId, ...data } =
|
|
128
184
|
(event as MessageEvent).data || {}
|
|
129
185
|
|
|
186
|
+
// Sanity check in case we somehow receive a message without requestId
|
|
130
187
|
if (!requestId) {
|
|
131
188
|
throw new Error("[SWARPC Client] Message received without requestId")
|
|
132
189
|
}
|
|
133
190
|
|
|
191
|
+
// Get the associated pending request handlers
|
|
134
192
|
const handlers = pendingRequests.get(requestId)
|
|
135
193
|
if (!handlers) {
|
|
136
194
|
throw new Error(
|
|
@@ -138,6 +196,8 @@ async function startClientListener(worker?: Worker) {
|
|
|
138
196
|
)
|
|
139
197
|
}
|
|
140
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
|
|
141
201
|
if ("error" in data) {
|
|
142
202
|
handlers.reject(new Error(data.error.message))
|
|
143
203
|
pendingRequests.delete(requestId)
|
|
@@ -152,10 +212,18 @@ async function startClientListener(worker?: Worker) {
|
|
|
152
212
|
_clientListenerStarted = true
|
|
153
213
|
}
|
|
154
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
|
+
*/
|
|
155
222
|
export function Client<Procedures extends ProceduresMap>(
|
|
156
223
|
procedures: Procedures,
|
|
157
224
|
{ worker }: { worker?: Worker } = {}
|
|
158
225
|
): SwarpcClient<Procedures> {
|
|
226
|
+
// Store procedures on a symbol key, to avoid conflicts with procedure names
|
|
159
227
|
const instance = { [zProcedures]: procedures } as Partial<
|
|
160
228
|
SwarpcClient<Procedures>
|
|
161
229
|
>
|
|
@@ -169,11 +237,15 @@ export function Client<Procedures extends ProceduresMap>(
|
|
|
169
237
|
)
|
|
170
238
|
}
|
|
171
239
|
|
|
240
|
+
// Set the method on the instance
|
|
172
241
|
// @ts-expect-error
|
|
173
242
|
instance[functionName] = (async (input: unknown, onProgress = () => {}) => {
|
|
243
|
+
// Validate the input against the procedure's input schema
|
|
174
244
|
procedures[functionName].input.assert(input)
|
|
245
|
+
// Ensure that we're listening for messages from the server
|
|
175
246
|
await startClientListener(worker)
|
|
176
247
|
|
|
248
|
+
// If no worker is provided, we use the service worker
|
|
177
249
|
const w =
|
|
178
250
|
worker ?? (await navigator.serviceWorker.ready.then((r) => r.active))
|
|
179
251
|
|
|
@@ -187,10 +259,22 @@ export function Client<Procedures extends ProceduresMap>(
|
|
|
187
259
|
|
|
188
260
|
const requestId = generateRequestId()
|
|
189
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
|
|
190
265
|
pendingRequests.set(requestId, { resolve, onProgress, reject })
|
|
191
266
|
|
|
267
|
+
// Post the message to the server
|
|
192
268
|
l.client.debug(requestId, `Requesting ${functionName} with`, input)
|
|
193
|
-
w.postMessage(
|
|
269
|
+
w.postMessage(
|
|
270
|
+
{ functionName, input, requestId },
|
|
271
|
+
{
|
|
272
|
+
transfer:
|
|
273
|
+
procedures[functionName].autotransfer === "always"
|
|
274
|
+
? findTransferables(input)
|
|
275
|
+
: [],
|
|
276
|
+
}
|
|
277
|
+
)
|
|
194
278
|
})
|
|
195
279
|
}) as SwarpcClient<Procedures>[typeof functionName]
|
|
196
280
|
}
|
|
@@ -198,29 +282,46 @@ export function Client<Procedures extends ProceduresMap>(
|
|
|
198
282
|
return instance as SwarpcClient<Procedures>
|
|
199
283
|
}
|
|
200
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Convenience shortcuts for logging.
|
|
287
|
+
*/
|
|
201
288
|
const l = {
|
|
202
289
|
server: {
|
|
203
|
-
debug: (
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
warn: (rqid: string | null, message: string, ...args: any[]) =>
|
|
208
|
-
log("warn", "server", rqid, message, ...args),
|
|
209
|
-
error: (rqid: string | null, message: string, ...args: any[]) =>
|
|
210
|
-
log("error", "server", rqid, message, ...args),
|
|
290
|
+
debug: logger("debug", "server"),
|
|
291
|
+
info: logger("info", "server"),
|
|
292
|
+
warn: logger("warn", "server"),
|
|
293
|
+
error: logger("error", "server"),
|
|
211
294
|
},
|
|
212
295
|
client: {
|
|
213
|
-
debug: (
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
warn: (rqid: string | null, message: string, ...args: any[]) =>
|
|
218
|
-
log("warn", "client", rqid, message, ...args),
|
|
219
|
-
error: (rqid: string | null, message: string, ...args: any[]) =>
|
|
220
|
-
log("error", "client", rqid, message, ...args),
|
|
296
|
+
debug: logger("debug", "client"),
|
|
297
|
+
info: logger("info", "client"),
|
|
298
|
+
warn: logger("warn", "client"),
|
|
299
|
+
error: logger("error", "client"),
|
|
221
300
|
},
|
|
222
301
|
}
|
|
223
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
|
+
*/
|
|
224
325
|
function log(
|
|
225
326
|
severity: "debug" | "info" | "warn" | "error",
|
|
226
327
|
side: "server" | "client",
|
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
|
+
}
|