swarpc 0.9.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
@@ -3,7 +3,7 @@
3
3
  * @mergeModuleWith <project>
4
4
  */
5
5
  import { type Logger, type LogLevel } from "./log.js";
6
- import { ClientMethod, Hooks, zProcedures, type ProceduresMap } from "./types.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,27 +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
23
- * @param options.restartListener if true, will force the listener to restart even if it has already been started
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.
24
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}
25
29
  *
26
30
  * An example of defining and using a client:
27
31
  * {@includeCode ../example/src/routes/+page.svelte}
28
32
  */
29
33
  export declare function Client<Procedures extends ProceduresMap>(procedures: Procedures, { worker, loglevel, restartListener, hooks, }?: {
30
- worker?: Worker;
34
+ worker?: Worker | SharedWorker;
31
35
  hooks?: Hooks<Procedures>;
32
36
  loglevel?: LogLevel;
33
37
  restartListener?: boolean;
34
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;
35
48
  /**
36
49
  * Starts the client listener, which listens for messages from the sw&rpc server.
37
50
  * @param worker if provided, the client will use this worker to listen for messages, instead of using the service worker
38
51
  * @param force if true, will force the listener to restart even if it has already been started
39
52
  * @returns
40
53
  */
41
- export declare function startClientListener<Procedures extends ProceduresMap>(l: Logger, worker?: Worker, hooks?: Hooks<Procedures>): Promise<void>;
54
+ export declare function startClientListener<Procedures extends ProceduresMap>(l: Logger, worker?: Worker | SharedWorker, hooks?: Hooks<Procedures>): Promise<void>;
42
55
  /**
43
56
  * Generate a random request ID, used to identify requests between client and server.
44
57
  * @source
@@ -1 +1 @@
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,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;;;;;;;;;;;GAWG;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,CAAA;IACf,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,CAiG1B;AA6BD;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,UAAU,SAAS,aAAa,EACxE,CAAC,EAAE,MAAM,EACT,MAAM,CAAC,EAAE,MAAM,EACf,KAAK,GAAE,KAAK,CAAC,UAAU,CAAM,iBA4D9B;AAED;;;;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,9 +17,13 @@ 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
22
- * @param options.restartListener if true, will force the listener to restart even if it has already been started
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.
23
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}
24
28
  *
25
29
  * An example of defining and using a client:
@@ -63,7 +67,7 @@ export function Client(procedures, { worker, loglevel = "debug", restartListener
63
67
  : [];
64
68
  // Post the message to the server
65
69
  l.debug(requestId, `Requesting ${functionName} with`, input);
66
- send(requestId, { input }, { transfer })
70
+ return send(requestId, { input }, { transfer })
67
71
  .then(() => { })
68
72
  .catch(reject);
69
73
  });
@@ -74,13 +78,18 @@ export function Client(procedures, { worker, loglevel = "debug", restartListener
74
78
  const requestId = makeRequestId();
75
79
  return {
76
80
  request: _runProcedure(input, onProgress, requestId),
77
- async cancel(reason) {
81
+ cancel(reason) {
78
82
  if (!pendingRequests.has(requestId)) {
79
83
  l.warn(requestId, `Cannot cancel ${functionName} request, it has already been resolved or rejected`);
80
84
  return;
81
85
  }
82
86
  l.debug(requestId, `Cancelling ${functionName} with`, reason);
83
- await send(requestId, { abort: { reason } });
87
+ postMessageSync(l, worker, {
88
+ by: "sw&rpc",
89
+ requestId,
90
+ functionName,
91
+ abort: { reason },
92
+ });
84
93
  pendingRequests.delete(requestId);
85
94
  },
86
95
  };
@@ -97,7 +106,33 @@ async function postMessage(l, worker, hooks, message, options) {
97
106
  if (!worker && !navigator.serviceWorker.controller)
98
107
  l.warn("", "Service Worker is not controlling the page");
99
108
  // If no worker is provided, we use the service worker
100
- 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;
101
136
  if (!w) {
102
137
  throw new Error("[SWARPC Client] No active service worker found");
103
138
  }
@@ -125,7 +160,7 @@ export async function startClientListener(l, worker, hooks = {}) {
125
160
  const w = worker ?? navigator.serviceWorker;
126
161
  // Start listening for messages
127
162
  l.debug(null, "Starting client listener", { worker, w, hooks });
128
- w.addEventListener("message", (event) => {
163
+ const listener = (event) => {
129
164
  // Get the data from the event
130
165
  const eventData = event.data || {};
131
166
  // Ignore other messages that aren't for us
@@ -158,7 +193,14 @@ export async function startClientListener(l, worker, hooks = {}) {
158
193
  handlers.resolve(data.result);
159
194
  pendingRequests.delete(requestId);
160
195
  }
161
- });
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
+ }
162
204
  _clientListenerStarted = true;
163
205
  }
164
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,CA8J1B"}
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);
@@ -134,7 +166,24 @@ export function Server(procedures, { worker, loglevel = "debug" } = {}) {
134
166
  finally {
135
167
  abortedRequests.delete(requestId);
136
168
  }
137
- });
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.9.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,10 +50,10 @@
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",
package/src/client.ts CHANGED
@@ -45,9 +45,13 @@ 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
50
- * @param options.restartListener if true, will force the listener to restart even if it has already been started
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.
51
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}
52
56
  *
53
57
  * An example of defining and using a client:
@@ -61,7 +65,7 @@ export function Client<Procedures extends ProceduresMap>(
61
65
  restartListener = false,
62
66
  hooks = {},
63
67
  }: {
64
- worker?: Worker
68
+ worker?: Worker | SharedWorker
65
69
  hooks?: Hooks<Procedures>
66
70
  loglevel?: LogLevel
67
71
  restartListener?: boolean
@@ -133,7 +137,7 @@ export function Client<Procedures extends ProceduresMap>(
133
137
 
134
138
  // Post the message to the server
135
139
  l.debug(requestId, `Requesting ${functionName} with`, input)
136
- send(requestId, { input }, { transfer })
140
+ return send(requestId, { input }, { transfer })
137
141
  .then(() => {})
138
142
  .catch(reject)
139
143
  })
@@ -145,7 +149,7 @@ export function Client<Procedures extends ProceduresMap>(
145
149
  const requestId = makeRequestId()
146
150
  return {
147
151
  request: _runProcedure(input, onProgress, requestId),
148
- async cancel(reason: string) {
152
+ cancel(reason: string) {
149
153
  if (!pendingRequests.has(requestId)) {
150
154
  l.warn(
151
155
  requestId,
@@ -155,7 +159,12 @@ export function Client<Procedures extends ProceduresMap>(
155
159
  }
156
160
 
157
161
  l.debug(requestId, `Cancelling ${functionName} with`, reason)
158
- await send(requestId, { abort: { reason } })
162
+ postMessageSync(l, worker, {
163
+ by: "sw&rpc",
164
+ requestId,
165
+ functionName,
166
+ abort: { reason },
167
+ })
159
168
  pendingRequests.delete(requestId)
160
169
  },
161
170
  }
@@ -171,7 +180,7 @@ export function Client<Procedures extends ProceduresMap>(
171
180
  */
172
181
  async function postMessage<Procedures extends ProceduresMap>(
173
182
  l: Logger,
174
- worker: Worker | undefined,
183
+ worker: Worker | SharedWorker | undefined,
175
184
  hooks: Hooks<Procedures>,
176
185
  message: Payload<Procedures>,
177
186
  options?: StructuredSerializeOptions
@@ -183,7 +192,43 @@ async function postMessage<Procedures extends ProceduresMap>(
183
192
 
184
193
  // If no worker is provided, we use the service worker
185
194
  const w =
186
- 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
187
232
 
188
233
  if (!w) {
189
234
  throw new Error("[SWARPC Client] No active service worker found")
@@ -200,7 +245,7 @@ async function postMessage<Procedures extends ProceduresMap>(
200
245
  */
201
246
  export async function startClientListener<Procedures extends ProceduresMap>(
202
247
  l: Logger,
203
- worker?: Worker,
248
+ worker?: Worker | SharedWorker,
204
249
  hooks: Hooks<Procedures> = {}
205
250
  ) {
206
251
  if (_clientListenerStarted) return
@@ -221,7 +266,7 @@ export async function startClientListener<Procedures extends ProceduresMap>(
221
266
 
222
267
  // Start listening for messages
223
268
  l.debug(null, "Starting client listener", { worker, w, hooks })
224
- w.addEventListener("message", (event) => {
269
+ const listener = (event: Event): void => {
225
270
  // Get the data from the event
226
271
  const eventData = (event as MessageEvent).data || {}
227
272
 
@@ -240,7 +285,7 @@ export async function startClientListener<Procedures extends ProceduresMap>(
240
285
  const handlers = pendingRequests.get(requestId)
241
286
  if (!handlers) {
242
287
  throw new Error(
243
- `[SWARPC Client] ${requestId} has no active request handlers, cannot process ${JSON.stringify(data)}`,
288
+ `[SWARPC Client] ${requestId} has no active request handlers, cannot process ${JSON.stringify(data)}`
244
289
  )
245
290
  }
246
291
 
@@ -258,7 +303,14 @@ export async function startClientListener<Procedures extends ProceduresMap>(
258
303
  handlers.resolve(data.result)
259
304
  pendingRequests.delete(requestId)
260
305
  }
261
- })
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
+ }
262
314
 
263
315
  _clientListenerStarted = true
264
316
  }
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,7 +57,9 @@ 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:
@@ -51,17 +67,49 @@ const abortedRequests = new Set<string>()
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))
@@ -206,7 +266,21 @@ export function Server<Procedures extends ProceduresMap>(
206
266
  } finally {
207
267
  abortedRequests.delete(requestId)
208
268
  }
209
- })
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
+ }
210
284
  }
211
285
 
212
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
  */