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.
Files changed (45) hide show
  1. package/README.md +44 -0
  2. package/dist/src/client.d.ts +22 -0
  3. package/dist/src/client.d.ts.map +1 -0
  4. package/dist/src/client.js +159 -0
  5. package/dist/src/index.d.ts +4 -0
  6. package/dist/src/index.d.ts.map +1 -0
  7. package/dist/src/index.js +2 -0
  8. package/dist/src/log.d.ts +20 -0
  9. package/dist/src/log.d.ts.map +1 -0
  10. package/dist/src/log.js +45 -0
  11. package/dist/src/server.d.ts +15 -0
  12. package/dist/src/server.d.ts.map +1 -0
  13. package/dist/src/server.js +132 -0
  14. package/dist/src/types.d.ts +260 -0
  15. package/dist/src/types.d.ts.map +1 -0
  16. package/dist/src/types.js +28 -0
  17. package/dist/src/utils.d.ts.map +1 -0
  18. package/dist/tests/core.procedures.d.ts +45 -0
  19. package/dist/tests/core.procedures.d.ts.map +1 -0
  20. package/dist/tests/core.procedures.js +49 -0
  21. package/dist/tests/core.test.d.ts +2 -0
  22. package/dist/tests/core.test.d.ts.map +1 -0
  23. package/dist/tests/core.test.js +100 -0
  24. package/dist/tests/core.worker.d.ts +2 -0
  25. package/dist/tests/core.worker.d.ts.map +1 -0
  26. package/dist/tests/core.worker.js +30 -0
  27. package/dist/vite.config.d.ts +3 -0
  28. package/dist/vite.config.d.ts.map +1 -0
  29. package/dist/vite.config.js +7 -0
  30. package/package.json +10 -6
  31. package/src/client.ts +245 -0
  32. package/src/index.ts +3 -0
  33. package/src/log.ts +62 -0
  34. package/src/server.ts +193 -0
  35. package/src/types.ts +66 -12
  36. package/dist/swarpc.d.ts +0 -25
  37. package/dist/swarpc.d.ts.map +0 -1
  38. package/dist/swarpc.js +0 -264
  39. package/dist/types.d.ts +0 -114
  40. package/dist/types.d.ts.map +0 -1
  41. package/dist/types.js +0 -8
  42. package/dist/utils.d.ts.map +0 -1
  43. package/src/swarpc.ts +0 -359
  44. /package/dist/{utils.d.ts → src/utils.d.ts} +0 -0
  45. /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,4 @@
1
+ export * from "./client.js";
2
+ export * from "./server.js";
3
+ export type { ProceduresMap, CancelablePromise } from "./types.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -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,2 @@
1
+ export * from "./client.js";
2
+ export * from "./server.js";
@@ -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"}
@@ -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
+ }