swarpc 0.8.0 → 0.10.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/client.d.ts CHANGED
@@ -2,8 +2,8 @@
2
2
  * @module
3
3
  * @mergeModuleWith <project>
4
4
  */
5
- import { type LogLevel } from "./log.js";
6
- import { ClientMethod, Hooks, zProcedures, type ProceduresMap } from "./types.js";
5
+ import { type Logger, type LogLevel } from "./log.js";
6
+ import { ClientMethod, Hooks, Payload, zProcedures, type ProceduresMap } from "./types.js";
7
7
  /**
8
8
  * The sw&rpc client instance, which provides {@link ClientMethod | methods to call procedures}.
9
9
  * Each property of the procedures map will be a method, that accepts an input, an optional onProgress callback and an optional request ID.
@@ -18,18 +18,40 @@ export type SwarpcClient<Procedures extends ProceduresMap> = {
18
18
  *
19
19
  * @param procedures procedures the client will be able to call, see {@link ProceduresMap}
20
20
  * @param options various options
21
- * @param options.worker if provided, the client will use this worker to post messages.
22
- * @param options.hooks hooks to run on messages received from the server
21
+ * @param options.worker The instantiated worker object. If not provided, the client will use the service worker.
22
+ * Example: `new Worker("./worker.js")`
23
+ * See {@link Worker} (used by both dedicated workers and service workers), {@link SharedWorker}, and
24
+ * the different [worker types](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API#worker_types) that exist
25
+ * @param options.hooks Hooks to run on messages received from the server. See {@link Hooks}
26
+ * @param options.loglevel Maximum log level to use, defaults to "debug" (shows everything). "info" will not show debug messages, "warn" will only show warnings and errors, "error" will only show errors.
27
+ * @param options.restartListener If true, will force the listener to restart even if it has already been started. You should probably leave this to false, unless you are testing and want to reset the client state.
23
28
  * @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, see {@link ClientMethod}
24
29
  *
25
30
  * An example of defining and using a client:
26
31
  * {@includeCode ../example/src/routes/+page.svelte}
27
32
  */
28
- export declare function Client<Procedures extends ProceduresMap>(procedures: Procedures, { worker, loglevel, hooks, }?: {
29
- worker?: Worker;
33
+ export declare function Client<Procedures extends ProceduresMap>(procedures: Procedures, { worker, loglevel, restartListener, hooks, }?: {
34
+ worker?: Worker | SharedWorker;
30
35
  hooks?: Hooks<Procedures>;
31
36
  loglevel?: LogLevel;
37
+ restartListener?: boolean;
32
38
  }): SwarpcClient<Procedures>;
39
+ /**
40
+ * A quicker version of postMessage that does not try to start the client listener, await the service worker, etc.
41
+ * esp. useful for abort logic that needs to not be... put behind everything else on the event loop.
42
+ * @param l
43
+ * @param worker
44
+ * @param message
45
+ * @param options
46
+ */
47
+ export declare function postMessageSync<Procedures extends ProceduresMap>(l: Logger, worker: Worker | SharedWorker | undefined, message: Payload<Procedures>, options?: StructuredSerializeOptions): void;
48
+ /**
49
+ * Starts the client listener, which listens for messages from the sw&rpc server.
50
+ * @param worker if provided, the client will use this worker to listen for messages, instead of using the service worker
51
+ * @param force if true, will force the listener to restart even if it has already been started
52
+ * @returns
53
+ */
54
+ export declare function startClientListener<Procedures extends ProceduresMap>(l: Logger, worker?: Worker | SharedWorker, hooks?: Hooks<Procedures>): Promise<void>;
33
55
  /**
34
56
  * Generate a random request ID, used to identify requests between client and server.
35
57
  * @source
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAA6B,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAA;AACnE,OAAO,EACL,YAAY,EACZ,KAAK,EAGL,WAAW,EACX,KAAK,aAAa,EACnB,MAAM,YAAY,CAAA;AAGnB;;;;GAIG;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;AAkBD;;;;;;;;;;GAUG;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;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAgB,KAAK,MAAM,EAAE,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAA;AACnE,OAAO,EACL,YAAY,EACZ,KAAK,EACL,OAAO,EAEP,WAAW,EACX,KAAK,aAAa,EACnB,MAAM,YAAY,CAAA;AAGnB;;;;GAIG;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;AAkBD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,MAAM,CAAC,UAAU,SAAS,aAAa,EACrD,UAAU,EAAE,UAAU,EACtB,EACE,MAAM,EACN,QAAkB,EAClB,eAAuB,EACvB,KAAU,GACX,GAAE;IACD,MAAM,CAAC,EAAE,MAAM,GAAG,YAAY,CAAA;IAC9B,KAAK,CAAC,EAAE,KAAK,CAAC,UAAU,CAAC,CAAA;IACzB,QAAQ,CAAC,EAAE,QAAQ,CAAA;IACnB,eAAe,CAAC,EAAE,OAAO,CAAA;CACrB,GACL,YAAY,CAAC,UAAU,CAAC,CAsG1B;AAiCD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,UAAU,SAAS,aAAa,EAC9D,CAAC,EAAE,MAAM,EACT,MAAM,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,EACzC,OAAO,EAAE,OAAO,CAAC,UAAU,CAAC,EAC5B,OAAO,CAAC,EAAE,0BAA0B,GACnC,IAAI,CAiBN;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,UAAU,SAAS,aAAa,EACxE,CAAC,EAAE,MAAM,EACT,MAAM,CAAC,EAAE,MAAM,GAAG,YAAY,EAC9B,KAAK,GAAE,KAAK,CAAC,UAAU,CAAM,iBAmE9B;AAED;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC"}
package/dist/client.js CHANGED
@@ -17,15 +17,22 @@ let _clientListenerStarted = false;
17
17
  *
18
18
  * @param procedures procedures the client will be able to call, see {@link ProceduresMap}
19
19
  * @param options various options
20
- * @param options.worker if provided, the client will use this worker to post messages.
21
- * @param options.hooks hooks to run on messages received from the server
20
+ * @param options.worker The instantiated worker object. If not provided, the client will use the service worker.
21
+ * Example: `new Worker("./worker.js")`
22
+ * See {@link Worker} (used by both dedicated workers and service workers), {@link SharedWorker}, and
23
+ * the different [worker types](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API#worker_types) that exist
24
+ * @param options.hooks Hooks to run on messages received from the server. See {@link Hooks}
25
+ * @param options.loglevel Maximum log level to use, defaults to "debug" (shows everything). "info" will not show debug messages, "warn" will only show warnings and errors, "error" will only show errors.
26
+ * @param options.restartListener If true, will force the listener to restart even if it has already been started. You should probably leave this to false, unless you are testing and want to reset the client state.
22
27
  * @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, see {@link ClientMethod}
23
28
  *
24
29
  * An example of defining and using a client:
25
30
  * {@includeCode ../example/src/routes/+page.svelte}
26
31
  */
27
- export function Client(procedures, { worker, loglevel = "debug", hooks = {}, } = {}) {
32
+ export function Client(procedures, { worker, loglevel = "debug", restartListener = false, hooks = {}, } = {}) {
28
33
  const l = createLogger("client", loglevel);
34
+ if (restartListener)
35
+ _clientListenerStarted = false;
29
36
  // Store procedures on a symbol key, to avoid conflicts with procedure names
30
37
  const instance = { [zProcedures]: procedures };
31
38
  for (const functionName of Object.keys(procedures)) {
@@ -60,7 +67,7 @@ export function Client(procedures, { worker, loglevel = "debug", hooks = {}, } =
60
67
  : [];
61
68
  // Post the message to the server
62
69
  l.debug(requestId, `Requesting ${functionName} with`, input);
63
- send(requestId, { input }, { transfer })
70
+ return send(requestId, { input }, { transfer })
64
71
  .then(() => { })
65
72
  .catch(reject);
66
73
  });
@@ -71,13 +78,18 @@ export function Client(procedures, { worker, loglevel = "debug", hooks = {}, } =
71
78
  const requestId = makeRequestId();
72
79
  return {
73
80
  request: _runProcedure(input, onProgress, requestId),
74
- async cancel(reason) {
81
+ cancel(reason) {
75
82
  if (!pendingRequests.has(requestId)) {
76
83
  l.warn(requestId, `Cannot cancel ${functionName} request, it has already been resolved or rejected`);
77
84
  return;
78
85
  }
79
86
  l.debug(requestId, `Cancelling ${functionName} with`, reason);
80
- await send(requestId, { abort: { reason } });
87
+ postMessageSync(l, worker, {
88
+ by: "sw&rpc",
89
+ requestId,
90
+ functionName,
91
+ abort: { reason },
92
+ });
81
93
  pendingRequests.delete(requestId);
82
94
  },
83
95
  };
@@ -94,7 +106,33 @@ async function postMessage(l, worker, hooks, message, options) {
94
106
  if (!worker && !navigator.serviceWorker.controller)
95
107
  l.warn("", "Service Worker is not controlling the page");
96
108
  // If no worker is provided, we use the service worker
97
- const w = worker ?? (await navigator.serviceWorker.ready.then((r) => r.active));
109
+ const w = worker instanceof SharedWorker
110
+ ? worker.port
111
+ : worker === undefined
112
+ ? await navigator.serviceWorker.ready.then((r) => r.active)
113
+ : worker;
114
+ if (!w) {
115
+ throw new Error("[SWARPC Client] No active service worker found");
116
+ }
117
+ w.postMessage(message, options);
118
+ }
119
+ /**
120
+ * A quicker version of postMessage that does not try to start the client listener, await the service worker, etc.
121
+ * esp. useful for abort logic that needs to not be... put behind everything else on the event loop.
122
+ * @param l
123
+ * @param worker
124
+ * @param message
125
+ * @param options
126
+ */
127
+ export function postMessageSync(l, worker, message, options) {
128
+ if (!worker && !navigator.serviceWorker.controller)
129
+ l.warn("", "Service Worker is not controlling the page");
130
+ // If no worker is provided, we use the service worker
131
+ const w = worker instanceof SharedWorker
132
+ ? worker.port
133
+ : worker === undefined
134
+ ? navigator.serviceWorker.controller
135
+ : worker;
98
136
  if (!w) {
99
137
  throw new Error("[SWARPC Client] No active service worker found");
100
138
  }
@@ -103,9 +141,10 @@ async function postMessage(l, worker, hooks, message, options) {
103
141
  /**
104
142
  * Starts the client listener, which listens for messages from the sw&rpc server.
105
143
  * @param worker if provided, the client will use this worker to listen for messages, instead of using the service worker
144
+ * @param force if true, will force the listener to restart even if it has already been started
106
145
  * @returns
107
146
  */
108
- async function startClientListener(l, worker, hooks = {}) {
147
+ export async function startClientListener(l, worker, hooks = {}) {
109
148
  if (_clientListenerStarted)
110
149
  return;
111
150
  // Get service worker registration if no worker is provided
@@ -120,8 +159,8 @@ async function startClientListener(l, worker, hooks = {}) {
120
159
  }
121
160
  const w = worker ?? navigator.serviceWorker;
122
161
  // Start listening for messages
123
- l.debug("", "Starting client listener on", w);
124
- w.addEventListener("message", (event) => {
162
+ l.debug(null, "Starting client listener", { worker, w, hooks });
163
+ const listener = (event) => {
125
164
  // Get the data from the event
126
165
  const eventData = event.data || {};
127
166
  // Ignore other messages that aren't for us
@@ -136,7 +175,7 @@ async function startClientListener(l, worker, hooks = {}) {
136
175
  // Get the associated pending request handlers
137
176
  const handlers = pendingRequests.get(requestId);
138
177
  if (!handlers) {
139
- throw new Error(`[SWARPC Client] ${requestId} has no active request handlers`);
178
+ throw new Error(`[SWARPC Client] ${requestId} has no active request handlers, cannot process ${JSON.stringify(data)}`);
140
179
  }
141
180
  // React to the data received: call hook, call handler,
142
181
  // and remove the request from pendingRequests (unless it's a progress update)
@@ -154,7 +193,14 @@ async function startClientListener(l, worker, hooks = {}) {
154
193
  handlers.resolve(data.result);
155
194
  pendingRequests.delete(requestId);
156
195
  }
157
- });
196
+ };
197
+ if (w instanceof SharedWorker) {
198
+ w.port.addEventListener("message", listener);
199
+ w.port.start();
200
+ }
201
+ else {
202
+ w.addEventListener("message", listener);
203
+ }
158
204
  _clientListenerStarted = true;
159
205
  }
160
206
  /**
package/dist/server.d.ts CHANGED
@@ -11,7 +11,7 @@ import { ImplementationsMap, ProcedureImplementation, zImplementations, zProcedu
11
11
  export type SwarpcServer<Procedures extends ProceduresMap> = {
12
12
  [zProcedures]: Procedures;
13
13
  [zImplementations]: ImplementationsMap<Procedures>;
14
- start(self: Window | Worker): void;
14
+ start(): Promise<void>;
15
15
  } & {
16
16
  [F in keyof Procedures]: (impl: ProcedureImplementation<Procedures[F]["input"], Procedures[F]["progress"], Procedures[F]["success"]>) => void;
17
17
  };
@@ -19,14 +19,17 @@ export type SwarpcServer<Procedures extends ProceduresMap> = {
19
19
  * Creates a sw&rpc server instance.
20
20
  * @param procedures procedures the server will implement, see {@link ProceduresMap}
21
21
  * @param options various options
22
- * @param options.worker if provided, the server will use this worker to post messages, instead of sending it to all clients
22
+ * @param options.worker The worker scope to use, defaults to the `self` of the file where Server() is called.
23
+ * @param options.loglevel Maximum log level to use, defaults to "debug" (shows everything). "info" will not show debug messages, "warn" will only show warnings and errors, "error" will only show errors.
24
+ * @param options._scopeType @internal Don't touch, this is used in testing environments because the mock is subpar. Manually overrides worker scope type detection.
23
25
  * @returns a SwarpcServer instance. Each property of the procedures map will be a method, that accepts a function implementing the procedure (see {@link ProcedureImplementation}). There is also .start(), to be called after implementing all procedures.
24
26
  *
25
27
  * An example of defining a server:
26
28
  * {@includeCode ../example/src/service-worker.ts}
27
29
  */
28
- export declare function Server<Procedures extends ProceduresMap>(procedures: Procedures, { worker, loglevel }?: {
29
- worker?: Worker;
30
+ export declare function Server<Procedures extends ProceduresMap>(procedures: Procedures, { loglevel, scope, _scopeType, }?: {
31
+ scope?: WorkerGlobalScope;
30
32
  loglevel?: LogLevel;
33
+ _scopeType?: "dedicated" | "shared" | "service";
31
34
  }): SwarpcServer<Procedures>;
32
35
  //# sourceMappingURL=server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAgB,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAA;AACtD,OAAO,EACL,kBAAkB,EAKlB,uBAAuB,EACvB,gBAAgB,EAChB,WAAW,EACX,KAAK,aAAa,EACnB,MAAM,YAAY,CAAA;AAGnB;;;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,MAAM,GAAG,IAAI,CAAA;CACnC,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;AAKD;;;;;;;;;GASG;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,CA+J1B"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,EAAgB,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAA;AACtD,OAAO,EACL,kBAAkB,EAKlB,uBAAuB,EACvB,gBAAgB,EAChB,WAAW,EACX,KAAK,aAAa,EACnB,MAAM,YAAY,CAAA;AAgBnB;;;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,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB,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;AAKD;;;;;;;;;;;GAWG;AACH,wBAAgB,MAAM,CAAC,UAAU,SAAS,aAAa,EACrD,UAAU,EAAE,UAAU,EACtB,EACE,QAAkB,EAClB,KAAK,EACL,UAAU,GACX,GAAE;IACD,KAAK,CAAC,EAAE,iBAAiB,CAAA;IACzB,QAAQ,CAAC,EAAE,QAAQ,CAAA;IACnB,UAAU,CAAC,EAAE,WAAW,GAAG,QAAQ,GAAG,SAAS,CAAA;CAC3C,GACL,YAAY,CAAC,UAAU,CAAC,CAgN1B"}
package/dist/server.js CHANGED
@@ -2,31 +2,52 @@
2
2
  * @module
3
3
  * @mergeModuleWith <project>
4
4
  */
5
+ /// <reference lib="webworker" />
5
6
  import { type } from "arktype";
6
7
  import { createLogger } from "./log.js";
7
8
  import { PayloadHeaderSchema, PayloadSchema, zImplementations, zProcedures, } from "./types.js";
8
9
  import { findTransferables } from "./utils.js";
10
+ class MockedWorkerGlobalScope {
11
+ constructor() { }
12
+ }
13
+ const SharedWorkerGlobalScope = globalThis.SharedWorkerGlobalScope ?? MockedWorkerGlobalScope;
14
+ const DedicatedWorkerGlobalScope = globalThis.DedicatedWorkerGlobalScope ?? MockedWorkerGlobalScope;
15
+ const ServiceWorkerGlobalScope = globalThis.ServiceWorkerGlobalScope ?? MockedWorkerGlobalScope;
9
16
  const abortControllers = new Map();
10
17
  const abortedRequests = new Set();
11
18
  /**
12
19
  * Creates a sw&rpc server instance.
13
20
  * @param procedures procedures the server will implement, see {@link ProceduresMap}
14
21
  * @param options various options
15
- * @param options.worker if provided, the server will use this worker to post messages, instead of sending it to all clients
22
+ * @param options.worker The worker scope to use, defaults to the `self` of the file where Server() is called.
23
+ * @param options.loglevel Maximum log level to use, defaults to "debug" (shows everything). "info" will not show debug messages, "warn" will only show warnings and errors, "error" will only show errors.
24
+ * @param options._scopeType @internal Don't touch, this is used in testing environments because the mock is subpar. Manually overrides worker scope type detection.
16
25
  * @returns a SwarpcServer instance. Each property of the procedures map will be a method, that accepts a function implementing the procedure (see {@link ProcedureImplementation}). There is also .start(), to be called after implementing all procedures.
17
26
  *
18
27
  * An example of defining a server:
19
28
  * {@includeCode ../example/src/service-worker.ts}
20
29
  */
21
- export function Server(procedures, { worker, loglevel = "debug" } = {}) {
30
+ export function Server(procedures, { loglevel = "debug", scope, _scopeType, } = {}) {
22
31
  const l = createLogger("server", loglevel);
32
+ // If scope is not provided, use the global scope
33
+ // This function is meant to be used in a worker, so `self` is a WorkerGlobalScope
34
+ scope ??= self;
35
+ function scopeIsShared(scope) {
36
+ return scope instanceof SharedWorkerGlobalScope || _scopeType === "shared";
37
+ }
38
+ function scopeIsDedicated(scope) {
39
+ return (scope instanceof DedicatedWorkerGlobalScope || _scopeType === "dedicated");
40
+ }
41
+ function scopeIsService(scope) {
42
+ return scope instanceof ServiceWorkerGlobalScope || _scopeType === "service";
43
+ }
23
44
  // Initialize the instance.
24
45
  // Procedures and implementations are stored on properties with symbol keys,
25
46
  // to avoid any conflicts with procedure names, and also discourage direct access to them.
26
47
  const instance = {
27
48
  [zProcedures]: procedures,
28
49
  [zImplementations]: {},
29
- start: (self) => { },
50
+ start: async () => { },
30
51
  };
31
52
  // Set all implementation-setter methods
32
53
  for (const functionName in procedures) {
@@ -47,21 +68,32 @@ export function Server(procedures, { worker, loglevel = "debug" } = {}) {
47
68
  };
48
69
  });
49
70
  }
50
- instance.start = (self) => {
71
+ instance.start = async () => {
72
+ const port = await new Promise((resolve) => {
73
+ if (!scopeIsShared(scope))
74
+ return resolve(undefined);
75
+ console.log("Awaiting shared worker connection...");
76
+ scope.addEventListener("connect", ({ ports: [port] }) => {
77
+ console.log("Shared worker connected with port", port);
78
+ resolve(port);
79
+ });
80
+ });
51
81
  // Used to post messages back to the client
52
82
  const postMessage = async (autotransfer, data) => {
53
83
  const transfer = autotransfer ? [] : findTransferables(data);
54
- if (worker) {
55
- self.postMessage(data, { transfer });
84
+ if (port) {
85
+ port.postMessage(data, { transfer });
56
86
  }
57
- else {
58
- await self.clients.matchAll().then((clients) => {
87
+ else if (scopeIsDedicated(scope)) {
88
+ scope.postMessage(data, { transfer });
89
+ }
90
+ else if (scopeIsService(scope)) {
91
+ await scope.clients.matchAll().then((clients) => {
59
92
  clients.forEach((client) => client.postMessage(data, { transfer }));
60
93
  });
61
94
  }
62
95
  };
63
- // Listen for messages from the client
64
- self.addEventListener("message", async (event) => {
96
+ const listener = async (event) => {
65
97
  // Decode the payload
66
98
  const { requestId, functionName } = PayloadHeaderSchema(type.enumerated(...Object.keys(procedures))).assert(event.data);
67
99
  l.debug(requestId, `Received request for ${functionName}`, event.data);
@@ -106,16 +138,21 @@ export function Server(procedures, { worker, loglevel = "debug" } = {}) {
106
138
  await postError("No input provided");
107
139
  return;
108
140
  }
109
- // Call the implementation with the input and a progress callback
110
- await implementation(payload.input, async (progress) => {
111
- l.debug(requestId, `Progress for ${functionName}`, progress);
112
- await postMsg({ progress });
113
- }, {
114
- abortSignal: abortControllers.get(requestId)?.signal,
115
- logger: createLogger("server", loglevel, requestId),
116
- })
141
+ try {
142
+ // Call the implementation with the input and a progress callback
143
+ const result = await implementation(payload.input, async (progress) => {
144
+ l.debug(requestId, `Progress for ${functionName}`, progress);
145
+ await postMsg({ progress });
146
+ }, {
147
+ abortSignal: abortControllers.get(requestId)?.signal,
148
+ logger: createLogger("server", loglevel, requestId),
149
+ });
150
+ // Send results
151
+ l.debug(requestId, `Result for ${functionName}`, result);
152
+ await postMsg({ result });
153
+ }
154
+ catch (error) {
117
155
  // Send errors
118
- .catch(async (error) => {
119
156
  // Handle errors caused by abortions
120
157
  if ("aborted" in error) {
121
158
  l.debug(requestId, `Received abort error for ${functionName}`, error.aborted);
@@ -123,18 +160,30 @@ export function Server(procedures, { worker, loglevel = "debug" } = {}) {
123
160
  abortControllers.delete(requestId);
124
161
  return;
125
162
  }
126
- l.error(requestId, `Error in ${functionName}`, error);
163
+ l.info(requestId, `Error in ${functionName}`, error);
127
164
  await postError(error);
128
- })
129
- // Send results
130
- .then(async (result) => {
131
- l.debug(requestId, `Result for ${functionName}`, result);
132
- await postMsg({ result });
133
- })
134
- .finally(() => {
165
+ }
166
+ finally {
135
167
  abortedRequests.delete(requestId);
136
- });
137
- });
168
+ }
169
+ };
170
+ // Listen for messages from the client
171
+ if (scopeIsShared(scope)) {
172
+ if (!port)
173
+ throw new Error("SharedWorker port not initialized");
174
+ console.log("Listening for shared worker messages on port", port);
175
+ port.addEventListener("message", listener);
176
+ port.start();
177
+ }
178
+ else if (scopeIsDedicated(scope)) {
179
+ scope.addEventListener("message", listener);
180
+ }
181
+ else if (scopeIsService(scope)) {
182
+ scope.addEventListener("message", listener);
183
+ }
184
+ else {
185
+ throw new Error(`Unsupported worker scope ${scope}`);
186
+ }
138
187
  };
139
188
  return instance;
140
189
  }
package/dist/types.d.ts CHANGED
@@ -48,7 +48,7 @@ export type CancelablePromise<T = unknown> = {
48
48
  * Abort the request.
49
49
  * @param reason The reason for cancelling the request.
50
50
  */
51
- cancel: (reason: string) => Promise<void>;
51
+ cancel: (reason: string) => void;
52
52
  };
53
53
  /**
54
54
  * An implementation of a procedure
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swarpc",
3
- "version": "0.8.0",
3
+ "version": "0.10.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",
@@ -50,13 +50,17 @@
50
50
  "sirv-cli": "^3.0.1",
51
51
  "typedoc": "^0.28.9",
52
52
  "typedoc-material-theme": "^1.4.0",
53
- "typedoc-plugin-dt-links": "^2.0.12",
53
+ "typedoc-plugin-dt-links": "^2.0.13",
54
54
  "typedoc-plugin-extras": "^4.0.1",
55
55
  "typedoc-plugin-inline-sources": "^1.3.0",
56
- "typedoc-plugin-mdn-links": "^5.0.6",
56
+ "typedoc-plugin-mdn-links": "^5.0.7",
57
57
  "typedoc-plugin-redirect": "^1.2.0",
58
58
  "typescript": "^5.9.2",
59
59
  "vite": "^7.0.6",
60
60
  "vitest": "^3.2.4"
61
+ },
62
+ "volta": {
63
+ "node": "22.18.0",
64
+ "npm": "11.5.2"
61
65
  }
62
66
  }
package/src/client.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @module
2
+ * @module
3
3
  * @mergeModuleWith <project>
4
4
  */
5
5
 
@@ -45,10 +45,15 @@ let _clientListenerStarted = false
45
45
  *
46
46
  * @param procedures procedures the client will be able to call, see {@link ProceduresMap}
47
47
  * @param options various options
48
- * @param options.worker if provided, the client will use this worker to post messages.
49
- * @param options.hooks hooks to run on messages received from the server
48
+ * @param options.worker The instantiated worker object. If not provided, the client will use the service worker.
49
+ * Example: `new Worker("./worker.js")`
50
+ * See {@link Worker} (used by both dedicated workers and service workers), {@link SharedWorker}, and
51
+ * the different [worker types](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API#worker_types) that exist
52
+ * @param options.hooks Hooks to run on messages received from the server. See {@link Hooks}
53
+ * @param options.loglevel Maximum log level to use, defaults to "debug" (shows everything). "info" will not show debug messages, "warn" will only show warnings and errors, "error" will only show errors.
54
+ * @param options.restartListener If true, will force the listener to restart even if it has already been started. You should probably leave this to false, unless you are testing and want to reset the client state.
50
55
  * @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, see {@link ClientMethod}
51
- *
56
+ *
52
57
  * An example of defining and using a client:
53
58
  * {@includeCode ../example/src/routes/+page.svelte}
54
59
  */
@@ -57,11 +62,19 @@ export function Client<Procedures extends ProceduresMap>(
57
62
  {
58
63
  worker,
59
64
  loglevel = "debug",
65
+ restartListener = false,
60
66
  hooks = {},
61
- }: { worker?: Worker; hooks?: Hooks<Procedures>; loglevel?: LogLevel } = {}
67
+ }: {
68
+ worker?: Worker | SharedWorker
69
+ hooks?: Hooks<Procedures>
70
+ loglevel?: LogLevel
71
+ restartListener?: boolean
72
+ } = {}
62
73
  ): SwarpcClient<Procedures> {
63
74
  const l = createLogger("client", loglevel)
64
75
 
76
+ if (restartListener) _clientListenerStarted = false
77
+
65
78
  // Store procedures on a symbol key, to avoid conflicts with procedure names
66
79
  const instance = { [zProcedures]: procedures } as Partial<
67
80
  SwarpcClient<Procedures>
@@ -124,7 +137,7 @@ export function Client<Procedures extends ProceduresMap>(
124
137
 
125
138
  // Post the message to the server
126
139
  l.debug(requestId, `Requesting ${functionName} with`, input)
127
- send(requestId, { input }, { transfer })
140
+ return send(requestId, { input }, { transfer })
128
141
  .then(() => {})
129
142
  .catch(reject)
130
143
  })
@@ -136,7 +149,7 @@ export function Client<Procedures extends ProceduresMap>(
136
149
  const requestId = makeRequestId()
137
150
  return {
138
151
  request: _runProcedure(input, onProgress, requestId),
139
- async cancel(reason: string) {
152
+ cancel(reason: string) {
140
153
  if (!pendingRequests.has(requestId)) {
141
154
  l.warn(
142
155
  requestId,
@@ -146,7 +159,12 @@ export function Client<Procedures extends ProceduresMap>(
146
159
  }
147
160
 
148
161
  l.debug(requestId, `Cancelling ${functionName} with`, reason)
149
- await send(requestId, { abort: { reason } })
162
+ postMessageSync(l, worker, {
163
+ by: "sw&rpc",
164
+ requestId,
165
+ functionName,
166
+ abort: { reason },
167
+ })
150
168
  pendingRequests.delete(requestId)
151
169
  },
152
170
  }
@@ -162,7 +180,7 @@ export function Client<Procedures extends ProceduresMap>(
162
180
  */
163
181
  async function postMessage<Procedures extends ProceduresMap>(
164
182
  l: Logger,
165
- worker: Worker | undefined,
183
+ worker: Worker | SharedWorker | undefined,
166
184
  hooks: Hooks<Procedures>,
167
185
  message: Payload<Procedures>,
168
186
  options?: StructuredSerializeOptions
@@ -174,7 +192,43 @@ async function postMessage<Procedures extends ProceduresMap>(
174
192
 
175
193
  // If no worker is provided, we use the service worker
176
194
  const w =
177
- worker ?? (await navigator.serviceWorker.ready.then((r) => r.active))
195
+ worker instanceof SharedWorker
196
+ ? worker.port
197
+ : worker === undefined
198
+ ? await navigator.serviceWorker.ready.then((r) => r.active)
199
+ : worker
200
+
201
+ if (!w) {
202
+ throw new Error("[SWARPC Client] No active service worker found")
203
+ }
204
+
205
+ w.postMessage(message, options)
206
+ }
207
+
208
+ /**
209
+ * A quicker version of postMessage that does not try to start the client listener, await the service worker, etc.
210
+ * esp. useful for abort logic that needs to not be... put behind everything else on the event loop.
211
+ * @param l
212
+ * @param worker
213
+ * @param message
214
+ * @param options
215
+ */
216
+ export function postMessageSync<Procedures extends ProceduresMap>(
217
+ l: Logger,
218
+ worker: Worker | SharedWorker | undefined,
219
+ message: Payload<Procedures>,
220
+ options?: StructuredSerializeOptions
221
+ ): void {
222
+ if (!worker && !navigator.serviceWorker.controller)
223
+ l.warn("", "Service Worker is not controlling the page")
224
+
225
+ // If no worker is provided, we use the service worker
226
+ const w =
227
+ worker instanceof SharedWorker
228
+ ? worker.port
229
+ : worker === undefined
230
+ ? navigator.serviceWorker.controller
231
+ : worker
178
232
 
179
233
  if (!w) {
180
234
  throw new Error("[SWARPC Client] No active service worker found")
@@ -186,11 +240,12 @@ async function postMessage<Procedures extends ProceduresMap>(
186
240
  /**
187
241
  * Starts the client listener, which listens for messages from the sw&rpc server.
188
242
  * @param worker if provided, the client will use this worker to listen for messages, instead of using the service worker
243
+ * @param force if true, will force the listener to restart even if it has already been started
189
244
  * @returns
190
245
  */
191
- async function startClientListener<Procedures extends ProceduresMap>(
246
+ export async function startClientListener<Procedures extends ProceduresMap>(
192
247
  l: Logger,
193
- worker?: Worker,
248
+ worker?: Worker | SharedWorker,
194
249
  hooks: Hooks<Procedures> = {}
195
250
  ) {
196
251
  if (_clientListenerStarted) return
@@ -210,8 +265,8 @@ async function startClientListener<Procedures extends ProceduresMap>(
210
265
  const w = worker ?? navigator.serviceWorker
211
266
 
212
267
  // Start listening for messages
213
- l.debug("", "Starting client listener on", w)
214
- w.addEventListener("message", (event) => {
268
+ l.debug(null, "Starting client listener", { worker, w, hooks })
269
+ const listener = (event: Event): void => {
215
270
  // Get the data from the event
216
271
  const eventData = (event as MessageEvent).data || {}
217
272
 
@@ -230,7 +285,7 @@ async function startClientListener<Procedures extends ProceduresMap>(
230
285
  const handlers = pendingRequests.get(requestId)
231
286
  if (!handlers) {
232
287
  throw new Error(
233
- `[SWARPC Client] ${requestId} has no active request handlers`
288
+ `[SWARPC Client] ${requestId} has no active request handlers, cannot process ${JSON.stringify(data)}`
234
289
  )
235
290
  }
236
291
 
@@ -248,14 +303,21 @@ async function startClientListener<Procedures extends ProceduresMap>(
248
303
  handlers.resolve(data.result)
249
304
  pendingRequests.delete(requestId)
250
305
  }
251
- })
306
+ }
307
+
308
+ if (w instanceof SharedWorker) {
309
+ w.port.addEventListener("message", listener)
310
+ w.port.start()
311
+ } else {
312
+ w.addEventListener("message", listener)
313
+ }
252
314
 
253
315
  _clientListenerStarted = true
254
316
  }
255
317
 
256
318
  /**
257
319
  * Generate a random request ID, used to identify requests between client and server.
258
- * @source
320
+ * @source
259
321
  * @returns a 6-character hexadecimal string
260
322
  */
261
323
  export function makeRequestId(): string {
package/src/server.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  * @mergeModuleWith <project>
4
4
  */
5
5
 
6
+ /// <reference lib="webworker" />
6
7
  import { type } from "arktype"
7
8
  import { createLogger, type LogLevel } from "./log.js"
8
9
  import {
@@ -18,6 +19,19 @@ import {
18
19
  } from "./types.js"
19
20
  import { findTransferables } from "./utils.js"
20
21
 
22
+ class MockedWorkerGlobalScope {
23
+ constructor() {}
24
+ }
25
+
26
+ const SharedWorkerGlobalScope =
27
+ globalThis.SharedWorkerGlobalScope ?? MockedWorkerGlobalScope
28
+
29
+ const DedicatedWorkerGlobalScope =
30
+ globalThis.DedicatedWorkerGlobalScope ?? MockedWorkerGlobalScope
31
+
32
+ const ServiceWorkerGlobalScope =
33
+ globalThis.ServiceWorkerGlobalScope ?? MockedWorkerGlobalScope
34
+
21
35
  /**
22
36
  * The sw&rpc server instance, which provides methods to register {@link ProcedureImplementation | procedure implementations},
23
37
  * and listens for incoming messages that call those procedures
@@ -25,7 +39,7 @@ import { findTransferables } from "./utils.js"
25
39
  export type SwarpcServer<Procedures extends ProceduresMap> = {
26
40
  [zProcedures]: Procedures
27
41
  [zImplementations]: ImplementationsMap<Procedures>
28
- start(self: Window | Worker): void
42
+ start(): Promise<void>
29
43
  } & {
30
44
  [F in keyof Procedures]: (
31
45
  impl: ProcedureImplementation<
@@ -43,25 +57,59 @@ const abortedRequests = new Set<string>()
43
57
  * Creates a sw&rpc server instance.
44
58
  * @param procedures procedures the server will implement, see {@link ProceduresMap}
45
59
  * @param options various options
46
- * @param options.worker if provided, the server will use this worker to post messages, instead of sending it to all clients
60
+ * @param options.worker The worker scope to use, defaults to the `self` of the file where Server() is called.
61
+ * @param options.loglevel Maximum log level to use, defaults to "debug" (shows everything). "info" will not show debug messages, "warn" will only show warnings and errors, "error" will only show errors.
62
+ * @param options._scopeType @internal Don't touch, this is used in testing environments because the mock is subpar. Manually overrides worker scope type detection.
47
63
  * @returns a SwarpcServer instance. Each property of the procedures map will be a method, that accepts a function implementing the procedure (see {@link ProcedureImplementation}). There is also .start(), to be called after implementing all procedures.
48
- *
64
+ *
49
65
  * An example of defining a server:
50
66
  * {@includeCode ../example/src/service-worker.ts}
51
67
  */
52
68
  export function Server<Procedures extends ProceduresMap>(
53
69
  procedures: Procedures,
54
- { worker, loglevel = "debug" }: { worker?: Worker; loglevel?: LogLevel } = {}
70
+ {
71
+ loglevel = "debug",
72
+ scope,
73
+ _scopeType,
74
+ }: {
75
+ scope?: WorkerGlobalScope
76
+ loglevel?: LogLevel
77
+ _scopeType?: "dedicated" | "shared" | "service"
78
+ } = {}
55
79
  ): SwarpcServer<Procedures> {
56
80
  const l = createLogger("server", loglevel)
57
81
 
82
+ // If scope is not provided, use the global scope
83
+ // This function is meant to be used in a worker, so `self` is a WorkerGlobalScope
84
+ scope ??= self as WorkerGlobalScope
85
+
86
+ function scopeIsShared(
87
+ scope: WorkerGlobalScope
88
+ ): scope is SharedWorkerGlobalScope {
89
+ return scope instanceof SharedWorkerGlobalScope || _scopeType === "shared"
90
+ }
91
+
92
+ function scopeIsDedicated(
93
+ scope: WorkerGlobalScope
94
+ ): scope is DedicatedWorkerGlobalScope {
95
+ return (
96
+ scope instanceof DedicatedWorkerGlobalScope || _scopeType === "dedicated"
97
+ )
98
+ }
99
+
100
+ function scopeIsService(
101
+ scope: WorkerGlobalScope
102
+ ): scope is ServiceWorkerGlobalScope {
103
+ return scope instanceof ServiceWorkerGlobalScope || _scopeType === "service"
104
+ }
105
+
58
106
  // Initialize the instance.
59
107
  // Procedures and implementations are stored on properties with symbol keys,
60
108
  // to avoid any conflicts with procedure names, and also discourage direct access to them.
61
109
  const instance = {
62
110
  [zProcedures]: procedures,
63
111
  [zImplementations]: {} as ImplementationsMap<Procedures>,
64
- start: (self: Window) => {},
112
+ start: async () => {},
65
113
  } as SwarpcServer<Procedures>
66
114
 
67
115
  // Set all implementation-setter methods
@@ -85,7 +133,16 @@ export function Server<Procedures extends ProceduresMap>(
85
133
  }) as SwarpcServer<Procedures>[typeof functionName]
86
134
  }
87
135
 
88
- instance.start = (self: Window) => {
136
+ instance.start = async () => {
137
+ const port = await new Promise<MessagePort | undefined>((resolve) => {
138
+ if (!scopeIsShared(scope)) return resolve(undefined)
139
+ console.log("Awaiting shared worker connection...")
140
+ scope.addEventListener("connect", ({ ports: [port] }) => {
141
+ console.log("Shared worker connected with port", port)
142
+ resolve(port)
143
+ })
144
+ })
145
+
89
146
  // Used to post messages back to the client
90
147
  const postMessage = async (
91
148
  autotransfer: boolean,
@@ -93,17 +150,20 @@ export function Server<Procedures extends ProceduresMap>(
93
150
  ) => {
94
151
  const transfer = autotransfer ? [] : findTransferables(data)
95
152
 
96
- if (worker) {
97
- self.postMessage(data, { transfer })
98
- } else {
99
- await (self as any).clients.matchAll().then((clients: any[]) => {
153
+ if (port) {
154
+ port.postMessage(data, { transfer })
155
+ } else if (scopeIsDedicated(scope)) {
156
+ scope.postMessage(data, { transfer })
157
+ } else if (scopeIsService(scope)) {
158
+ await scope.clients.matchAll().then((clients) => {
100
159
  clients.forEach((client) => client.postMessage(data, { transfer }))
101
160
  })
102
161
  }
103
162
  }
104
163
 
105
- // Listen for messages from the client
106
- self.addEventListener("message", async (event: MessageEvent) => {
164
+ const listener = async (
165
+ event: MessageEvent<any> | ExtendableMessageEvent
166
+ ): Promise<void> => {
107
167
  // Decode the payload
108
168
  const { requestId, functionName } = PayloadHeaderSchema(
109
169
  type.enumerated(...Object.keys(procedures))
@@ -170,44 +230,57 @@ export function Server<Procedures extends ProceduresMap>(
170
230
  return
171
231
  }
172
232
 
173
- // Call the implementation with the input and a progress callback
174
- await implementation(
175
- payload.input,
176
- async (progress: any) => {
177
- l.debug(requestId, `Progress for ${functionName}`, progress)
178
- await postMsg({ progress })
179
- },
180
- {
181
- abortSignal: abortControllers.get(requestId)?.signal,
182
- logger: createLogger("server", loglevel, requestId),
183
- }
184
- )
185
- // Send errors
186
- .catch(async (error: any) => {
187
- // Handle errors caused by abortions
188
- if ("aborted" in error) {
189
- l.debug(
190
- requestId,
191
- `Received abort error for ${functionName}`,
192
- error.aborted
193
- )
194
- abortedRequests.add(requestId)
195
- abortControllers.delete(requestId)
196
- return
233
+ try {
234
+ // Call the implementation with the input and a progress callback
235
+ const result = await implementation(
236
+ payload.input,
237
+ async (progress: any) => {
238
+ l.debug(requestId, `Progress for ${functionName}`, progress)
239
+ await postMsg({ progress })
240
+ },
241
+ {
242
+ abortSignal: abortControllers.get(requestId)?.signal,
243
+ logger: createLogger("server", loglevel, requestId),
197
244
  }
245
+ )
198
246
 
199
- l.error(requestId, `Error in ${functionName}`, error)
200
- await postError(error)
201
- })
202
247
  // Send results
203
- .then(async (result: any) => {
204
- l.debug(requestId, `Result for ${functionName}`, result)
205
- await postMsg({ result })
206
- })
207
- .finally(() => {
208
- abortedRequests.delete(requestId)
209
- })
210
- })
248
+ l.debug(requestId, `Result for ${functionName}`, result)
249
+ await postMsg({ result })
250
+ } catch (error: any) {
251
+ // Send errors
252
+ // Handle errors caused by abortions
253
+ if ("aborted" in error) {
254
+ l.debug(
255
+ requestId,
256
+ `Received abort error for ${functionName}`,
257
+ error.aborted
258
+ )
259
+ abortedRequests.add(requestId)
260
+ abortControllers.delete(requestId)
261
+ return
262
+ }
263
+
264
+ l.info(requestId, `Error in ${functionName}`, error)
265
+ await postError(error)
266
+ } finally {
267
+ abortedRequests.delete(requestId)
268
+ }
269
+ }
270
+
271
+ // Listen for messages from the client
272
+ if (scopeIsShared(scope)) {
273
+ if (!port) throw new Error("SharedWorker port not initialized")
274
+ console.log("Listening for shared worker messages on port", port)
275
+ port.addEventListener("message", listener)
276
+ port.start()
277
+ } else if (scopeIsDedicated(scope)) {
278
+ scope.addEventListener("message", listener)
279
+ } else if (scopeIsService(scope)) {
280
+ scope.addEventListener("message", listener)
281
+ } else {
282
+ throw new Error(`Unsupported worker scope ${scope}`)
283
+ }
211
284
  }
212
285
 
213
286
  return instance
package/src/types.ts CHANGED
@@ -51,7 +51,7 @@ export type CancelablePromise<T = unknown> = {
51
51
  * Abort the request.
52
52
  * @param reason The reason for cancelling the request.
53
53
  */
54
- cancel: (reason: string) => Promise<void>
54
+ cancel: (reason: string) => void
55
55
  }
56
56
 
57
57
  /**
@@ -87,7 +87,7 @@ export type ProcedureImplementation<
87
87
 
88
88
  /**
89
89
  * Declarations of procedures by name.
90
- *
90
+ *
91
91
  * An example of declaring procedures:
92
92
  * {@includeCode ../example/src/lib/procedures.ts}
93
93
  */