swarpc 0.6.1 → 0.7.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/README.md +44 -0
- package/dist/src/client.d.ts +22 -0
- package/dist/src/client.d.ts.map +1 -0
- package/dist/src/client.js +159 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +2 -0
- package/dist/src/log.d.ts +20 -0
- package/dist/src/log.d.ts.map +1 -0
- package/dist/src/log.js +45 -0
- package/dist/src/server.d.ts +15 -0
- package/dist/src/server.d.ts.map +1 -0
- package/dist/src/server.js +132 -0
- package/dist/src/types.d.ts +260 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +28 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/tests/core.procedures.d.ts +45 -0
- package/dist/tests/core.procedures.d.ts.map +1 -0
- package/dist/tests/core.procedures.js +49 -0
- package/dist/tests/core.test.d.ts +2 -0
- package/dist/tests/core.test.d.ts.map +1 -0
- package/dist/tests/core.test.js +100 -0
- package/dist/tests/core.worker.d.ts +2 -0
- package/dist/tests/core.worker.d.ts.map +1 -0
- package/dist/tests/core.worker.js +30 -0
- package/dist/vite.config.d.ts +3 -0
- package/dist/vite.config.d.ts.map +1 -0
- package/dist/vite.config.js +7 -0
- package/package.json +10 -6
- package/src/client.ts +245 -0
- package/src/index.ts +3 -0
- package/src/log.ts +62 -0
- package/src/server.ts +193 -0
- package/src/types.ts +66 -12
- package/dist/swarpc.d.ts +0 -25
- package/dist/swarpc.d.ts.map +0 -1
- package/dist/swarpc.js +0 -264
- package/dist/types.d.ts +0 -114
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -8
- package/dist/utils.d.ts.map +0 -1
- package/src/swarpc.ts +0 -359
- /package/dist/{utils.d.ts → src/utils.d.ts} +0 -0
- /package/dist/{utils.js → src/utils.js} +0 -0
package/README.md
CHANGED
|
@@ -107,3 +107,47 @@ Here's a Svelte example!
|
|
|
107
107
|
{/each}
|
|
108
108
|
</ul>
|
|
109
109
|
```
|
|
110
|
+
|
|
111
|
+
### Make cancelable requests
|
|
112
|
+
|
|
113
|
+
#### Implementation
|
|
114
|
+
|
|
115
|
+
To make your procedures meaningfully cancelable, you have to make use of the [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) API. This is passed as a third argument when implementing your procedures:
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
server.searchIMDb(async ({ query }, onProgress, abort) => {
|
|
119
|
+
// If you're doing heavy computation without fetch:
|
|
120
|
+
let aborted = false
|
|
121
|
+
abort?.addEventListener("abort", () => {
|
|
122
|
+
aborted = true
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
// Use `aborted` to check if the request was canceled within your hot loop
|
|
126
|
+
for (...) {
|
|
127
|
+
/* here */ if (aborted) return
|
|
128
|
+
...
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// When using fetch:
|
|
132
|
+
await fetch(..., { signal: abort })
|
|
133
|
+
})
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
#### Call sites
|
|
137
|
+
|
|
138
|
+
Instead of calling `await client.myProcedure()` directly, call `client.myProcedure.cancelable()`. You'll get back an object with
|
|
139
|
+
|
|
140
|
+
- `async cancel(reason)`: a function to cancel the request
|
|
141
|
+
- `request`: a Promise that resolves to the result of the procedure call. `await` it to wait for the request to finish.
|
|
142
|
+
|
|
143
|
+
Example:
|
|
144
|
+
|
|
145
|
+
```js
|
|
146
|
+
// Normal call:
|
|
147
|
+
const result = await swarpc.searchIMDb({ query })
|
|
148
|
+
|
|
149
|
+
// Cancelable call:
|
|
150
|
+
const { request, cancel } = swarpc.searchIMDb.cancelable({ query })
|
|
151
|
+
setTimeout(() => cancel().then(() => console.warn("Took too long!!")), 5_000)
|
|
152
|
+
await request
|
|
153
|
+
```
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type LogLevel } from "./log.js";
|
|
2
|
+
import { Hooks, type ProceduresMap, type SwarpcClient } from "./types.js";
|
|
3
|
+
export type { SwarpcClient } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
*
|
|
6
|
+
* @param procedures procedures the client will be able to call
|
|
7
|
+
* @param options various options
|
|
8
|
+
* @param options.worker if provided, the client will use this worker to post messages.
|
|
9
|
+
* @param options.hooks hooks to run on messages received from the server
|
|
10
|
+
* @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.
|
|
11
|
+
*/
|
|
12
|
+
export declare function Client<Procedures extends ProceduresMap>(procedures: Procedures, { worker, loglevel, hooks, }?: {
|
|
13
|
+
worker?: Worker;
|
|
14
|
+
hooks?: Hooks<Procedures>;
|
|
15
|
+
loglevel?: LogLevel;
|
|
16
|
+
}): SwarpcClient<Procedures>;
|
|
17
|
+
/**
|
|
18
|
+
* Generate a random request ID, used to identify requests between client and server.
|
|
19
|
+
* @returns a 6-character hexadecimal string
|
|
20
|
+
*/
|
|
21
|
+
export declare function makeRequestId(): string;
|
|
22
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAA6B,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAA;AACnE,OAAO,EACL,KAAK,EAIL,KAAK,aAAa,EAClB,KAAK,YAAY,EAClB,MAAM,YAAY,CAAA;AAGnB,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAkB9C;;;;;;;GAOG;AACH,wBAAgB,MAAM,CAAC,UAAU,SAAS,aAAa,EACrD,UAAU,EAAE,UAAU,EACtB,EACE,MAAM,EACN,QAAkB,EAClB,KAAU,GACX,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;IAAC,QAAQ,CAAC,EAAE,QAAQ,CAAA;CAAO,GAC1E,YAAY,CAAC,UAAU,CAAC,CA+F1B;AAmGD;;;GAGG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC"}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { createLogger } from "./log.js";
|
|
2
|
+
import { zProcedures } from "./types.js";
|
|
3
|
+
import { findTransferables } from "./utils.js";
|
|
4
|
+
/**
|
|
5
|
+
* Pending requests are stored in a map, where the key is the request ID.
|
|
6
|
+
* Each request has a set of handlers: resolve, reject, and onProgress.
|
|
7
|
+
* This allows having a single listener for the client, and having multiple in-flight calls to the same procedure.
|
|
8
|
+
*/
|
|
9
|
+
const pendingRequests = new Map();
|
|
10
|
+
// Have we started the client listener?
|
|
11
|
+
let _clientListenerStarted = false;
|
|
12
|
+
/**
|
|
13
|
+
*
|
|
14
|
+
* @param procedures procedures the client will be able to call
|
|
15
|
+
* @param options various options
|
|
16
|
+
* @param options.worker if provided, the client will use this worker to post messages.
|
|
17
|
+
* @param options.hooks hooks to run on messages received from the server
|
|
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
|
+
*/
|
|
20
|
+
export function Client(procedures, { worker, loglevel = "debug", hooks = {}, } = {}) {
|
|
21
|
+
const l = createLogger("client", loglevel);
|
|
22
|
+
// Store procedures on a symbol key, to avoid conflicts with procedure names
|
|
23
|
+
const instance = { [zProcedures]: procedures };
|
|
24
|
+
for (const functionName of Object.keys(procedures)) {
|
|
25
|
+
if (typeof functionName !== "string") {
|
|
26
|
+
throw new Error(`[SWARPC Client] Invalid function name, don't use symbols`);
|
|
27
|
+
}
|
|
28
|
+
const send = async (requestId, msg, options) => {
|
|
29
|
+
return postMessage(l, worker, hooks, {
|
|
30
|
+
...msg,
|
|
31
|
+
by: "sw&rpc",
|
|
32
|
+
requestId,
|
|
33
|
+
functionName,
|
|
34
|
+
}, options);
|
|
35
|
+
};
|
|
36
|
+
// Set the method on the instance
|
|
37
|
+
const _runProcedure = async (input, onProgress = () => { }, reqid) => {
|
|
38
|
+
// Validate the input against the procedure's input schema
|
|
39
|
+
procedures[functionName].input.assert(input);
|
|
40
|
+
const requestId = reqid ?? makeRequestId();
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
// Store promise handlers (as well as progress updates handler)
|
|
43
|
+
// so the client listener can resolve/reject the promise (and react to progress updates)
|
|
44
|
+
// when the server sends messages back
|
|
45
|
+
pendingRequests.set(requestId, {
|
|
46
|
+
functionName,
|
|
47
|
+
resolve,
|
|
48
|
+
onProgress,
|
|
49
|
+
reject,
|
|
50
|
+
});
|
|
51
|
+
const transfer = procedures[functionName].autotransfer === "always"
|
|
52
|
+
? findTransferables(input)
|
|
53
|
+
: [];
|
|
54
|
+
// Post the message to the server
|
|
55
|
+
l.debug(requestId, `Requesting ${functionName} with`, input);
|
|
56
|
+
send(requestId, { input }, { transfer })
|
|
57
|
+
.then(() => { })
|
|
58
|
+
.catch(reject);
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
// @ts-expect-error
|
|
62
|
+
instance[functionName] = _runProcedure;
|
|
63
|
+
instance[functionName].cancelable = (input, onProgress) => {
|
|
64
|
+
const requestId = makeRequestId();
|
|
65
|
+
return {
|
|
66
|
+
request: _runProcedure(input, onProgress, requestId),
|
|
67
|
+
async cancel(reason) {
|
|
68
|
+
if (!pendingRequests.has(requestId)) {
|
|
69
|
+
l.warn(requestId, `Cannot cancel ${functionName} request, it has already been resolved or rejected`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
l.debug(requestId, `Cancelling ${functionName} with`, reason);
|
|
73
|
+
await send(requestId, { abort: { reason } });
|
|
74
|
+
pendingRequests.delete(requestId);
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return instance;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Warms up the client by starting the listener and getting the worker, then posts a message to the worker.
|
|
83
|
+
* @returns the worker to use
|
|
84
|
+
*/
|
|
85
|
+
async function postMessage(l, worker, hooks, message, options) {
|
|
86
|
+
await startClientListener(l, worker, hooks);
|
|
87
|
+
if (!worker && !navigator.serviceWorker.controller)
|
|
88
|
+
l.warn("", "Service Worker is not controlling the page");
|
|
89
|
+
// If no worker is provided, we use the service worker
|
|
90
|
+
const w = worker ?? (await navigator.serviceWorker.ready.then((r) => r.active));
|
|
91
|
+
if (!w) {
|
|
92
|
+
throw new Error("[SWARPC Client] No active service worker found");
|
|
93
|
+
}
|
|
94
|
+
w.postMessage(message, options);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Starts the client listener, which listens for messages from the sw&rpc server.
|
|
98
|
+
* @param worker if provided, the client will use this worker to listen for messages, instead of using the service worker
|
|
99
|
+
* @returns
|
|
100
|
+
*/
|
|
101
|
+
async function startClientListener(l, worker, hooks = {}) {
|
|
102
|
+
if (_clientListenerStarted)
|
|
103
|
+
return;
|
|
104
|
+
// Get service worker registration if no worker is provided
|
|
105
|
+
if (!worker) {
|
|
106
|
+
const sw = await navigator.serviceWorker.ready;
|
|
107
|
+
if (!sw?.active) {
|
|
108
|
+
throw new Error("[SWARPC Client] Service Worker is not active");
|
|
109
|
+
}
|
|
110
|
+
if (!navigator.serviceWorker.controller) {
|
|
111
|
+
l.warn("", "Service Worker is not controlling the page");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const w = worker ?? navigator.serviceWorker;
|
|
115
|
+
// Start listening for messages
|
|
116
|
+
l.debug("", "Starting client listener on", w);
|
|
117
|
+
w.addEventListener("message", (event) => {
|
|
118
|
+
// Get the data from the event
|
|
119
|
+
const eventData = event.data || {};
|
|
120
|
+
// Ignore other messages that aren't for us
|
|
121
|
+
if (eventData?.by !== "sw&rpc")
|
|
122
|
+
return;
|
|
123
|
+
// We don't use a arktype schema here, we trust the server to send valid data
|
|
124
|
+
const { requestId, ...data } = eventData;
|
|
125
|
+
// Sanity check in case we somehow receive a message without requestId
|
|
126
|
+
if (!requestId) {
|
|
127
|
+
throw new Error("[SWARPC Client] Message received without requestId");
|
|
128
|
+
}
|
|
129
|
+
// Get the associated pending request handlers
|
|
130
|
+
const handlers = pendingRequests.get(requestId);
|
|
131
|
+
if (!handlers) {
|
|
132
|
+
throw new Error(`[SWARPC Client] ${requestId} has no active request handlers`);
|
|
133
|
+
}
|
|
134
|
+
// React to the data received: call hook, call handler,
|
|
135
|
+
// and remove the request from pendingRequests (unless it's a progress update)
|
|
136
|
+
if ("error" in data) {
|
|
137
|
+
hooks.error?.(data.functionName, new Error(data.error.message));
|
|
138
|
+
handlers.reject(new Error(data.error.message));
|
|
139
|
+
pendingRequests.delete(requestId);
|
|
140
|
+
}
|
|
141
|
+
else if ("progress" in data) {
|
|
142
|
+
hooks.progress?.(data.functionName, data.progress);
|
|
143
|
+
handlers.onProgress(data.progress);
|
|
144
|
+
}
|
|
145
|
+
else if ("result" in data) {
|
|
146
|
+
hooks.success?.(data.functionName, data.result);
|
|
147
|
+
handlers.resolve(data.result);
|
|
148
|
+
pendingRequests.delete(requestId);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
_clientListenerStarted = true;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Generate a random request ID, used to identify requests between client and server.
|
|
155
|
+
* @returns a 6-character hexadecimal string
|
|
156
|
+
*/
|
|
157
|
+
export function makeRequestId() {
|
|
158
|
+
return Math.random().toString(16).substring(2, 8).toUpperCase();
|
|
159
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,YAAY,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare function createLogger(side: "server" | "client", level?: LogLevel): {
|
|
2
|
+
debug: (rqid: string | null, message: string, ...args: any[]) => void;
|
|
3
|
+
info: (rqid: string | null, message: string, ...args: any[]) => void;
|
|
4
|
+
warn: (rqid: string | null, message: string, ...args: any[]) => void;
|
|
5
|
+
error: (rqid: string | null, message: string, ...args: any[]) => void;
|
|
6
|
+
};
|
|
7
|
+
export type Logger = ReturnType<typeof createLogger>;
|
|
8
|
+
declare const LOG_LEVELS: readonly ["debug", "info", "warn", "error"];
|
|
9
|
+
export type LogLevel = (typeof LOG_LEVELS)[number];
|
|
10
|
+
/**
|
|
11
|
+
* Send log messages to the console, with a helpful prefix.
|
|
12
|
+
* @param severity
|
|
13
|
+
* @param side
|
|
14
|
+
* @param rqid request ID
|
|
15
|
+
* @param message
|
|
16
|
+
* @param args passed to console methods directly
|
|
17
|
+
*/
|
|
18
|
+
export declare function log(severity: "debug" | "info" | "warn" | "error", side: "server" | "client", rqid: string | null, message: string, ...args: any[]): void;
|
|
19
|
+
export {};
|
|
20
|
+
//# sourceMappingURL=log.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../../src/log.ts"],"names":[],"mappings":"AAAA,wBAAgB,YAAY,CAC1B,IAAI,EAAE,QAAQ,GAAG,QAAQ,EACzB,KAAK,GAAE,QAAkB;kBAwBX,MAAM,GAAG,IAAI,WAAW,MAAM,WAAW,GAAG,EAAE;iBAA9C,MAAM,GAAG,IAAI,WAAW,MAAM,WAAW,GAAG,EAAE;iBAA9C,MAAM,GAAG,IAAI,WAAW,MAAM,WAAW,GAAG,EAAE;kBAA9C,MAAM,GAAG,IAAI,WAAW,MAAM,WAAW,GAAG,EAAE;EAd7D;AAED,MAAM,MAAM,MAAM,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAA;AAEpD,QAAA,MAAM,UAAU,6CAA8C,CAAA;AAC9D,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAA;AAalD;;;;;;;GAOG;AACH,wBAAgB,GAAG,CACjB,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,EAC7C,IAAI,EAAE,QAAQ,GAAG,QAAQ,EACzB,IAAI,EAAE,MAAM,GAAG,IAAI,EACnB,OAAO,EAAE,MAAM,EACf,GAAG,IAAI,EAAE,GAAG,EAAE,QAkBf"}
|
package/dist/src/log.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export function createLogger(side, level = "debug") {
|
|
2
|
+
const enabledLevels = LOG_LEVELS.slice(LOG_LEVELS.indexOf(level));
|
|
3
|
+
return {
|
|
4
|
+
debug: enabledLevels.includes("debug") ? logger("debug", side) : () => { },
|
|
5
|
+
info: enabledLevels.includes("info") ? logger("info", side) : () => { },
|
|
6
|
+
warn: enabledLevels.includes("warn") ? logger("warn", side) : () => { },
|
|
7
|
+
error: enabledLevels.includes("error") ? logger("error", side) : () => { },
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
const LOG_LEVELS = ["debug", "info", "warn", "error"];
|
|
11
|
+
/**
|
|
12
|
+
* Creates partially-applied logging functions given the first 2 args
|
|
13
|
+
* @param severity
|
|
14
|
+
* @param side
|
|
15
|
+
* @returns
|
|
16
|
+
*/
|
|
17
|
+
function logger(severity, side) {
|
|
18
|
+
return (rqid, message, ...args) => log(severity, side, rqid, message, ...args);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Send log messages to the console, with a helpful prefix.
|
|
22
|
+
* @param severity
|
|
23
|
+
* @param side
|
|
24
|
+
* @param rqid request ID
|
|
25
|
+
* @param message
|
|
26
|
+
* @param args passed to console methods directly
|
|
27
|
+
*/
|
|
28
|
+
export function log(severity, side, rqid, message, ...args) {
|
|
29
|
+
const prefix = "[" +
|
|
30
|
+
["SWARPC", side, rqid ? `%c${rqid}%c` : ""].filter(Boolean).join(" ") +
|
|
31
|
+
"]";
|
|
32
|
+
const prefixStyles = rqid ? ["color: cyan;", "color: inherit;"] : [];
|
|
33
|
+
if (severity === "debug") {
|
|
34
|
+
console.debug(prefix, ...prefixStyles, message, ...args);
|
|
35
|
+
}
|
|
36
|
+
else if (severity === "info") {
|
|
37
|
+
console.info(prefix, ...prefixStyles, message, ...args);
|
|
38
|
+
}
|
|
39
|
+
else if (severity === "warn") {
|
|
40
|
+
console.warn(prefix, ...prefixStyles, message, ...args);
|
|
41
|
+
}
|
|
42
|
+
else if (severity === "error") {
|
|
43
|
+
console.error(prefix, ...prefixStyles, message, ...args);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type LogLevel } from "./log.js";
|
|
2
|
+
import { type ProceduresMap, type SwarpcServer } from "./types.js";
|
|
3
|
+
export type { SwarpcServer } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Creates a sw&rpc server instance.
|
|
6
|
+
* @param procedures procedures the server will implement
|
|
7
|
+
* @param options various options
|
|
8
|
+
* @param options.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 declare function Server<Procedures extends ProceduresMap>(procedures: Procedures, { worker, loglevel }?: {
|
|
12
|
+
worker?: Worker;
|
|
13
|
+
loglevel?: LogLevel;
|
|
14
|
+
}): SwarpcServer<Procedures>;
|
|
15
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/server.ts"],"names":[],"mappings":"AACA,OAAO,EAAgB,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAA;AACtD,OAAO,EAQL,KAAK,aAAa,EAClB,KAAK,YAAY,EAClB,MAAM,YAAY,CAAA;AAGnB,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAK9C;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,UAAU,SAAS,aAAa,EACrD,UAAU,EAAE,UAAU,EACtB,EAAE,MAAM,EAAE,QAAkB,EAAE,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,QAAQ,CAAA;CAAO,GAC5E,YAAY,CAAC,UAAU,CAAC,CAkK1B"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { type } from "arktype";
|
|
2
|
+
import { createLogger } from "./log.js";
|
|
3
|
+
import { PayloadHeaderSchema, PayloadSchema, zImplementations, zProcedures, } from "./types.js";
|
|
4
|
+
import { findTransferables } from "./utils.js";
|
|
5
|
+
const abortControllers = new Map();
|
|
6
|
+
const abortedRequests = new Set();
|
|
7
|
+
/**
|
|
8
|
+
* Creates a sw&rpc server instance.
|
|
9
|
+
* @param procedures procedures the server will implement
|
|
10
|
+
* @param options various options
|
|
11
|
+
* @param options.worker if provided, the server will use this worker to post messages, instead of sending it to all clients
|
|
12
|
+
* @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.
|
|
13
|
+
*/
|
|
14
|
+
export function Server(procedures, { worker, loglevel = "debug" } = {}) {
|
|
15
|
+
const l = createLogger("server", loglevel);
|
|
16
|
+
// Initialize the instance.
|
|
17
|
+
// Procedures and implementations are stored on properties with symbol keys,
|
|
18
|
+
// to avoid any conflicts with procedure names, and also discourage direct access to them.
|
|
19
|
+
const instance = {
|
|
20
|
+
[zProcedures]: procedures,
|
|
21
|
+
[zImplementations]: {},
|
|
22
|
+
start: (self) => { },
|
|
23
|
+
};
|
|
24
|
+
// Set all implementation-setter methods
|
|
25
|
+
for (const functionName in procedures) {
|
|
26
|
+
instance[functionName] = ((implementation) => {
|
|
27
|
+
if (!instance[zProcedures][functionName]) {
|
|
28
|
+
throw new Error(`No procedure found for function name: ${functionName}`);
|
|
29
|
+
}
|
|
30
|
+
instance[zImplementations][functionName] = (input, onProgress, abortSignal) => {
|
|
31
|
+
abortSignal?.throwIfAborted();
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
abortSignal?.addEventListener("abort", () => {
|
|
34
|
+
let { requestId, reason } = abortSignal?.reason;
|
|
35
|
+
l.debug(requestId, `Aborted ${functionName} request: ${reason}`);
|
|
36
|
+
reject({ aborted: reason });
|
|
37
|
+
});
|
|
38
|
+
implementation(input, onProgress, abortSignal)
|
|
39
|
+
.then(resolve)
|
|
40
|
+
.catch(reject);
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
instance.start = (self) => {
|
|
46
|
+
// Used to post messages back to the client
|
|
47
|
+
const postMessage = async (autotransfer, data) => {
|
|
48
|
+
const transfer = autotransfer ? [] : findTransferables(data);
|
|
49
|
+
if (worker) {
|
|
50
|
+
self.postMessage(data, { transfer });
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
await self.clients.matchAll().then((clients) => {
|
|
54
|
+
clients.forEach((client) => client.postMessage(data, { transfer }));
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
// Listen for messages from the client
|
|
59
|
+
self.addEventListener("message", async (event) => {
|
|
60
|
+
// Decode the payload
|
|
61
|
+
const { requestId, functionName } = PayloadHeaderSchema(type.enumerated(...Object.keys(procedures))).assert(event.data);
|
|
62
|
+
l.debug(requestId, `Received request for ${functionName}`, event.data);
|
|
63
|
+
// Get autotransfer preference from the procedure definition
|
|
64
|
+
const { autotransfer = "output-only", ...schemas } = instance[zProcedures][functionName];
|
|
65
|
+
// Shorthand function with functionName, requestId, etc. set
|
|
66
|
+
const postMsg = async (data) => {
|
|
67
|
+
if (abortedRequests.has(requestId))
|
|
68
|
+
return;
|
|
69
|
+
await postMessage(autotransfer !== "never", {
|
|
70
|
+
by: "sw&rpc",
|
|
71
|
+
functionName,
|
|
72
|
+
requestId,
|
|
73
|
+
...data,
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
// Prepare a function to post errors back to the client
|
|
77
|
+
const postError = async (error) => postMsg({
|
|
78
|
+
error: {
|
|
79
|
+
message: "message" in error ? error.message : String(error),
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
// Retrieve the implementation for the requested function
|
|
83
|
+
const implementation = instance[zImplementations][functionName];
|
|
84
|
+
if (!implementation) {
|
|
85
|
+
await postError("No implementation found");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// Define payload schema for incoming messages
|
|
89
|
+
const payload = PayloadSchema(type(`"${functionName}"`), schemas.input, schemas.progress, schemas.success).assert(event.data);
|
|
90
|
+
// Handle abortion requests (pro-choice ftw!!)
|
|
91
|
+
if (payload.abort) {
|
|
92
|
+
const controller = abortControllers.get(requestId);
|
|
93
|
+
if (!controller)
|
|
94
|
+
await postError("No abort controller found for request");
|
|
95
|
+
controller?.abort(payload.abort.reason);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
// Set up the abort controller for this request
|
|
99
|
+
abortControllers.set(requestId, new AbortController());
|
|
100
|
+
if (!payload.input) {
|
|
101
|
+
await postError("No input provided");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
// Call the implementation with the input and a progress callback
|
|
105
|
+
await implementation(payload.input, async (progress) => {
|
|
106
|
+
l.debug(requestId, `Progress for ${functionName}`, progress);
|
|
107
|
+
await postMsg({ progress });
|
|
108
|
+
}, abortControllers.get(requestId)?.signal)
|
|
109
|
+
// Send errors
|
|
110
|
+
.catch(async (error) => {
|
|
111
|
+
// Handle errors caused by abortions
|
|
112
|
+
if ("aborted" in error) {
|
|
113
|
+
l.debug(requestId, `Received abort error for ${functionName}`, error.aborted);
|
|
114
|
+
abortedRequests.add(requestId);
|
|
115
|
+
abortControllers.delete(requestId);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
l.error(requestId, `Error in ${functionName}`, error);
|
|
119
|
+
await postError(error);
|
|
120
|
+
})
|
|
121
|
+
// Send results
|
|
122
|
+
.then(async (result) => {
|
|
123
|
+
l.debug(requestId, `Result for ${functionName}`, result);
|
|
124
|
+
await postMsg({ result });
|
|
125
|
+
})
|
|
126
|
+
.finally(() => {
|
|
127
|
+
abortedRequests.delete(requestId);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
return instance;
|
|
132
|
+
}
|