swarpc 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,6 +15,24 @@ RPC for Service Workers -- move that heavy computation off of your UI thread!
15
15
  npm add swarpc arktype
16
16
  ```
17
17
 
18
+ ### Bleeding edge
19
+
20
+ If you want to use the latest commit instead of a published version, you can, either by using the Git URL:
21
+
22
+ ```bash
23
+ npm add git+https://github.com/gwennlbh/swarpc.git
24
+ ```
25
+
26
+ Or by straight up cloning the repository and pointing to the local directory (very useful to hack on sw&rpc while testing out your changes on a more substantial project):
27
+
28
+ ```bash
29
+ mkdir -p vendored
30
+ git clone https://github.com/gwennlbh/swarpc.git vendored/swarpc
31
+ npm add file:vendored/swarpc
32
+ ```
33
+
34
+ This works thanks to the fact that `dist/` is published on the repository (and kept up to date with a CI workflow).
35
+
18
36
  ## Usage
19
37
 
20
38
  ### 1. Declare your procedures in a shared file
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.
@@ -14,35 +14,63 @@ export type SwarpcClient<Procedures extends ProceduresMap> = {
14
14
  } & {
15
15
  [F in keyof Procedures]: ClientMethod<Procedures[F]>;
16
16
  };
17
+ /**
18
+ * Context for passing around data useful for requests
19
+ */
20
+ type Context<Procedures extends ProceduresMap> = {
21
+ /** A logger, bound to the client */
22
+ logger: Logger;
23
+ /** The worker instance to use */
24
+ worker: Worker | SharedWorker | undefined;
25
+ /** Hooks defined by the client */
26
+ hooks: Hooks<Procedures>;
27
+ /** Local storage data defined by the client for the faux local storage */
28
+ localStorage: Record<string, any>;
29
+ };
17
30
  /**
18
31
  *
19
32
  * @param procedures procedures the client will be able to call, see {@link ProceduresMap}
20
33
  * @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
34
+ * @param options.worker The instantiated worker object. If not provided, the client will use the service worker.
35
+ * Example: `new Worker("./worker.js")`
36
+ * See {@link Worker} (used by both dedicated workers and service workers), {@link SharedWorker}, and
37
+ * the different [worker types](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API#worker_types) that exist
38
+ * @param options.hooks Hooks to run on messages received from the server. See {@link Hooks}
39
+ * @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.
40
+ * @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.
41
+ * @param options.localStorage Define a in-memory localStorage with the given key-value pairs. Allows code called on the server to access localStorage (even though SharedWorkers don't have access to the browser's real localStorage)
24
42
  * @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
43
  *
26
44
  * An example of defining and using a client:
27
45
  * {@includeCode ../example/src/routes/+page.svelte}
28
46
  */
29
- export declare function Client<Procedures extends ProceduresMap>(procedures: Procedures, { worker, loglevel, restartListener, hooks, }?: {
30
- worker?: Worker;
47
+ export declare function Client<Procedures extends ProceduresMap>(procedures: Procedures, { worker, loglevel, restartListener, hooks, localStorage, }?: {
48
+ worker?: Worker | SharedWorker;
31
49
  hooks?: Hooks<Procedures>;
32
50
  loglevel?: LogLevel;
33
51
  restartListener?: boolean;
52
+ localStorage?: Record<string, any>;
34
53
  }): SwarpcClient<Procedures>;
54
+ /**
55
+ * A quicker version of postMessage that does not try to start the client listener, await the service worker, etc.
56
+ * esp. useful for abort logic that needs to not be... put behind everything else on the event loop.
57
+ * @param l
58
+ * @param worker
59
+ * @param message
60
+ * @param options
61
+ */
62
+ export declare function postMessageSync<Procedures extends ProceduresMap>(l: Logger, worker: Worker | SharedWorker | undefined, message: Payload<Procedures>, options?: StructuredSerializeOptions): void;
35
63
  /**
36
64
  * Starts the client listener, which listens for messages from the sw&rpc server.
37
- * @param worker if provided, the client will use this worker to listen for messages, instead of using the service worker
38
- * @param force if true, will force the listener to restart even if it has already been started
65
+ * @param ctx.worker if provided, the client will use this worker to listen for messages, instead of using the service worker
39
66
  * @returns
40
67
  */
41
- export declare function startClientListener<Procedures extends ProceduresMap>(l: Logger, worker?: Worker, hooks?: Hooks<Procedures>): Promise<void>;
68
+ export declare function startClientListener<Procedures extends ProceduresMap>(ctx: Context<Procedures>): Promise<void>;
42
69
  /**
43
70
  * Generate a random request ID, used to identify requests between client and server.
44
71
  * @source
45
72
  * @returns a 6-character hexadecimal string
46
73
  */
47
74
  export declare function makeRequestId(): string;
75
+ export {};
48
76
  //# sourceMappingURL=client.d.ts.map
@@ -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;AAED;;GAEG;AACH,KAAK,OAAO,CAAC,UAAU,SAAS,aAAa,IAAI;IAC/C,oCAAoC;IACpC,MAAM,EAAE,MAAM,CAAA;IACd,iCAAiC;IACjC,MAAM,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAAA;IACzC,kCAAkC;IAClC,KAAK,EAAE,KAAK,CAAC,UAAU,CAAC,CAAA;IACxB,0EAA0E;IAC1E,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CAClC,CAAA;AAkBD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,MAAM,CAAC,UAAU,SAAS,aAAa,EACrD,UAAU,EAAE,UAAU,EACtB,EACE,MAAM,EACN,QAAkB,EAClB,eAAuB,EACvB,KAAU,EACV,YAAiB,GAClB,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;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CAC9B,GACL,YAAY,CAAC,UAAU,CAAC,CA2G1B;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;;;;GAIG;AACH,wBAAsB,mBAAmB,CAAC,UAAU,SAAS,aAAa,EACxE,GAAG,EAAE,OAAO,CAAC,UAAU,CAAC,iBAoFzB;AAED;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC"}
package/dist/client.js CHANGED
@@ -17,15 +17,20 @@ 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.
27
+ * @param options.localStorage Define a in-memory localStorage with the given key-value pairs. Allows code called on the server to access localStorage (even though SharedWorkers don't have access to the browser's real localStorage)
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 function Client(procedures, { worker, loglevel = "debug", restartListener = false, hooks = {}, } = {}) {
33
+ export function Client(procedures, { worker, loglevel = "debug", restartListener = false, hooks = {}, localStorage = {}, } = {}) {
29
34
  const l = createLogger("client", loglevel);
30
35
  if (restartListener)
31
36
  _clientListenerStarted = false;
@@ -36,7 +41,13 @@ export function Client(procedures, { worker, loglevel = "debug", restartListener
36
41
  throw new Error(`[SWARPC Client] Invalid function name, don't use symbols`);
37
42
  }
38
43
  const send = async (requestId, msg, options) => {
39
- return postMessage(l, worker, hooks, {
44
+ const ctx = {
45
+ logger: l,
46
+ worker,
47
+ hooks,
48
+ localStorage,
49
+ };
50
+ return postMessage(ctx, {
40
51
  ...msg,
41
52
  by: "sw&rpc",
42
53
  requestId,
@@ -63,7 +74,7 @@ export function Client(procedures, { worker, loglevel = "debug", restartListener
63
74
  : [];
64
75
  // Post the message to the server
65
76
  l.debug(requestId, `Requesting ${functionName} with`, input);
66
- send(requestId, { input }, { transfer })
77
+ return send(requestId, { input }, { transfer })
67
78
  .then(() => { })
68
79
  .catch(reject);
69
80
  });
@@ -74,13 +85,18 @@ export function Client(procedures, { worker, loglevel = "debug", restartListener
74
85
  const requestId = makeRequestId();
75
86
  return {
76
87
  request: _runProcedure(input, onProgress, requestId),
77
- async cancel(reason) {
88
+ cancel(reason) {
78
89
  if (!pendingRequests.has(requestId)) {
79
90
  l.warn(requestId, `Cannot cancel ${functionName} request, it has already been resolved or rejected`);
80
91
  return;
81
92
  }
82
93
  l.debug(requestId, `Cancelling ${functionName} with`, reason);
83
- await send(requestId, { abort: { reason } });
94
+ postMessageSync(l, worker, {
95
+ by: "sw&rpc",
96
+ requestId,
97
+ functionName,
98
+ abort: { reason },
99
+ });
84
100
  pendingRequests.delete(requestId);
85
101
  },
86
102
  };
@@ -92,12 +108,39 @@ export function Client(procedures, { worker, loglevel = "debug", restartListener
92
108
  * Warms up the client by starting the listener and getting the worker, then posts a message to the worker.
93
109
  * @returns the worker to use
94
110
  */
95
- async function postMessage(l, worker, hooks, message, options) {
96
- await startClientListener(l, worker, hooks);
111
+ async function postMessage(ctx, message, options) {
112
+ await startClientListener(ctx);
113
+ const { logger: l, worker } = ctx;
97
114
  if (!worker && !navigator.serviceWorker.controller)
98
115
  l.warn("", "Service Worker is not controlling the page");
99
116
  // If no worker is provided, we use the service worker
100
- const w = worker ?? (await navigator.serviceWorker.ready.then((r) => r.active));
117
+ const w = worker instanceof SharedWorker
118
+ ? worker.port
119
+ : worker === undefined
120
+ ? await navigator.serviceWorker.ready.then((r) => r.active)
121
+ : worker;
122
+ if (!w) {
123
+ throw new Error("[SWARPC Client] No active service worker found");
124
+ }
125
+ w.postMessage(message, options);
126
+ }
127
+ /**
128
+ * A quicker version of postMessage that does not try to start the client listener, await the service worker, etc.
129
+ * esp. useful for abort logic that needs to not be... put behind everything else on the event loop.
130
+ * @param l
131
+ * @param worker
132
+ * @param message
133
+ * @param options
134
+ */
135
+ export function postMessageSync(l, worker, message, options) {
136
+ if (!worker && !navigator.serviceWorker.controller)
137
+ l.warn("", "Service Worker is not controlling the page");
138
+ // If no worker is provided, we use the service worker
139
+ const w = worker instanceof SharedWorker
140
+ ? worker.port
141
+ : worker === undefined
142
+ ? navigator.serviceWorker.controller
143
+ : worker;
101
144
  if (!w) {
102
145
  throw new Error("[SWARPC Client] No active service worker found");
103
146
  }
@@ -105,13 +148,13 @@ async function postMessage(l, worker, hooks, message, options) {
105
148
  }
106
149
  /**
107
150
  * Starts the client listener, which listens for messages from the sw&rpc server.
108
- * @param worker if provided, the client will use this worker to listen for messages, instead of using the service worker
109
- * @param force if true, will force the listener to restart even if it has already been started
151
+ * @param ctx.worker if provided, the client will use this worker to listen for messages, instead of using the service worker
110
152
  * @returns
111
153
  */
112
- export async function startClientListener(l, worker, hooks = {}) {
154
+ export async function startClientListener(ctx) {
113
155
  if (_clientListenerStarted)
114
156
  return;
157
+ const { logger: l, worker } = ctx;
115
158
  // Get service worker registration if no worker is provided
116
159
  if (!worker) {
117
160
  const sw = await navigator.serviceWorker.ready;
@@ -124,15 +167,21 @@ export async function startClientListener(l, worker, hooks = {}) {
124
167
  }
125
168
  const w = worker ?? navigator.serviceWorker;
126
169
  // Start listening for messages
127
- l.debug(null, "Starting client listener", { worker, w, hooks });
128
- w.addEventListener("message", (event) => {
170
+ l.debug(null, "Starting client listener", { w, ...ctx });
171
+ const listener = (event) => {
129
172
  // Get the data from the event
130
173
  const eventData = event.data || {};
131
174
  // Ignore other messages that aren't for us
132
175
  if (eventData?.by !== "sw&rpc")
133
176
  return;
134
177
  // We don't use a arktype schema here, we trust the server to send valid data
135
- const { requestId, ...data } = eventData;
178
+ const payload = eventData;
179
+ // Ignore #initialize request, it's client->server only
180
+ if ("localStorageData" in payload) {
181
+ l.warn(null, "Ignoring unexpected #initialize from server", payload);
182
+ return;
183
+ }
184
+ const { requestId, ...data } = payload;
136
185
  // Sanity check in case we somehow receive a message without requestId
137
186
  if (!requestId) {
138
187
  throw new Error("[SWARPC Client] Message received without requestId");
@@ -145,21 +194,34 @@ export async function startClientListener(l, worker, hooks = {}) {
145
194
  // React to the data received: call hook, call handler,
146
195
  // and remove the request from pendingRequests (unless it's a progress update)
147
196
  if ("error" in data) {
148
- hooks.error?.(data.functionName, new Error(data.error.message));
197
+ ctx.hooks.error?.(data.functionName, new Error(data.error.message));
149
198
  handlers.reject(new Error(data.error.message));
150
199
  pendingRequests.delete(requestId);
151
200
  }
152
201
  else if ("progress" in data) {
153
- hooks.progress?.(data.functionName, data.progress);
202
+ ctx.hooks.progress?.(data.functionName, data.progress);
154
203
  handlers.onProgress(data.progress);
155
204
  }
156
205
  else if ("result" in data) {
157
- hooks.success?.(data.functionName, data.result);
206
+ ctx.hooks.success?.(data.functionName, data.result);
158
207
  handlers.resolve(data.result);
159
208
  pendingRequests.delete(requestId);
160
209
  }
161
- });
210
+ };
211
+ if (w instanceof SharedWorker) {
212
+ w.port.addEventListener("message", listener);
213
+ w.port.start();
214
+ }
215
+ else {
216
+ w.addEventListener("message", listener);
217
+ }
162
218
  _clientListenerStarted = true;
219
+ // Recursive terminal case is ensured by calling this *after* _clientListenerStarted is set to true: startClientListener() will therefore not be called in postMessage() again.
220
+ await postMessage(ctx, {
221
+ by: "sw&rpc",
222
+ functionName: "#initialize",
223
+ localStorageData: ctx.localStorage,
224
+ });
163
225
  }
164
226
  /**
165
227
  * Generate a random request ID, used to identify requests between client and server.
@@ -0,0 +1,14 @@
1
+ export declare class FauxLocalStorage {
2
+ data: Record<string, any>;
3
+ keysOrder: string[];
4
+ constructor(data: Record<string, any>);
5
+ setItem(key: string, value: string): void;
6
+ getItem(key: string): any;
7
+ hasItem(key: string): boolean;
8
+ removeItem(key: string): void;
9
+ clear(): void;
10
+ key(index: number): string;
11
+ get length(): number;
12
+ register(subject: WorkerGlobalScope | SharedWorkerGlobalScope): void;
13
+ }
14
+ //# sourceMappingURL=localstorage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"localstorage.d.ts","sourceRoot":"","sources":["../src/localstorage.ts"],"names":[],"mappings":"AAAA,qBAAa,gBAAgB;IAC3B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACzB,SAAS,EAAE,MAAM,EAAE,CAAA;gBAEP,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IAKrC,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;IAKlC,OAAO,CAAC,GAAG,EAAE,MAAM;IAInB,OAAO,CAAC,GAAG,EAAE,MAAM;IAInB,UAAU,CAAC,GAAG,EAAE,MAAM;IAMtB,KAAK;IAKL,GAAG,CAAC,KAAK,EAAE,MAAM;IAIjB,IAAI,MAAM,WAET;IAED,QAAQ,CAAC,OAAO,EAAE,iBAAiB,GAAG,uBAAuB;CAI9D"}
@@ -0,0 +1,39 @@
1
+ export class FauxLocalStorage {
2
+ data;
3
+ keysOrder;
4
+ constructor(data) {
5
+ this.data = data;
6
+ this.keysOrder = Object.keys(data);
7
+ }
8
+ setItem(key, value) {
9
+ if (!this.hasItem(key))
10
+ this.keysOrder.push(key);
11
+ this.data[key] = value;
12
+ }
13
+ getItem(key) {
14
+ return this.data[key];
15
+ }
16
+ hasItem(key) {
17
+ return Object.hasOwn(this.data, key);
18
+ }
19
+ removeItem(key) {
20
+ if (!this.hasItem(key))
21
+ return;
22
+ delete this.data[key];
23
+ this.keysOrder = this.keysOrder.filter((k) => k !== key);
24
+ }
25
+ clear() {
26
+ this.data = {};
27
+ this.keysOrder = [];
28
+ }
29
+ key(index) {
30
+ return this.keysOrder[index];
31
+ }
32
+ get length() {
33
+ return this.keysOrder.length;
34
+ }
35
+ register(subject) {
36
+ // @ts-expect-error
37
+ subject.localStorage = this;
38
+ }
39
+ }
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.scope 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,EAMlB,uBAAuB,EACvB,gBAAgB,EAChB,WAAW,EACX,KAAK,aAAa,EACnB,MAAM,YAAY,CAAA;AAiBnB;;;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,CA0N1B"}
package/dist/server.js CHANGED
@@ -2,31 +2,53 @@
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
- import { PayloadHeaderSchema, PayloadSchema, zImplementations, zProcedures, } from "./types.js";
8
+ import { PayloadHeaderSchema, PayloadInitializeSchema, PayloadSchema, zImplementations, zProcedures, } from "./types.js";
8
9
  import { findTransferables } from "./utils.js";
10
+ import { FauxLocalStorage } from "./localstorage.js";
11
+ class MockedWorkerGlobalScope {
12
+ constructor() { }
13
+ }
14
+ const SharedWorkerGlobalScope = globalThis.SharedWorkerGlobalScope ?? MockedWorkerGlobalScope;
15
+ const DedicatedWorkerGlobalScope = globalThis.DedicatedWorkerGlobalScope ?? MockedWorkerGlobalScope;
16
+ const ServiceWorkerGlobalScope = globalThis.ServiceWorkerGlobalScope ?? MockedWorkerGlobalScope;
9
17
  const abortControllers = new Map();
10
18
  const abortedRequests = new Set();
11
19
  /**
12
20
  * Creates a sw&rpc server instance.
13
21
  * @param procedures procedures the server will implement, see {@link ProceduresMap}
14
22
  * @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
23
+ * @param options.scope The worker scope to use, defaults to the `self` of the file where Server() is called.
24
+ * @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.
25
+ * @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
26
  * @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
27
  *
18
28
  * An example of defining a server:
19
29
  * {@includeCode ../example/src/service-worker.ts}
20
30
  */
21
- export function Server(procedures, { worker, loglevel = "debug" } = {}) {
31
+ export function Server(procedures, { loglevel = "debug", scope, _scopeType, } = {}) {
22
32
  const l = createLogger("server", loglevel);
33
+ // If scope is not provided, use the global scope
34
+ // This function is meant to be used in a worker, so `self` is a WorkerGlobalScope
35
+ scope ??= self;
36
+ function scopeIsShared(scope) {
37
+ return scope instanceof SharedWorkerGlobalScope || _scopeType === "shared";
38
+ }
39
+ function scopeIsDedicated(scope) {
40
+ return (scope instanceof DedicatedWorkerGlobalScope || _scopeType === "dedicated");
41
+ }
42
+ function scopeIsService(scope) {
43
+ return scope instanceof ServiceWorkerGlobalScope || _scopeType === "service";
44
+ }
23
45
  // Initialize the instance.
24
46
  // Procedures and implementations are stored on properties with symbol keys,
25
47
  // to avoid any conflicts with procedure names, and also discourage direct access to them.
26
48
  const instance = {
27
49
  [zProcedures]: procedures,
28
50
  [zImplementations]: {},
29
- start: (self) => { },
51
+ start: async () => { },
30
52
  };
31
53
  // Set all implementation-setter methods
32
54
  for (const functionName in procedures) {
@@ -47,21 +69,38 @@ export function Server(procedures, { worker, loglevel = "debug" } = {}) {
47
69
  };
48
70
  });
49
71
  }
50
- instance.start = (self) => {
72
+ instance.start = async () => {
73
+ const port = await new Promise((resolve) => {
74
+ if (!scopeIsShared(scope))
75
+ return resolve(undefined);
76
+ l.debug(null, "Awaiting shared worker connection...");
77
+ scope.addEventListener("connect", ({ ports: [port] }) => {
78
+ l.debug(null, "Shared worker connected with port", port);
79
+ resolve(port);
80
+ });
81
+ });
51
82
  // Used to post messages back to the client
52
83
  const postMessage = async (autotransfer, data) => {
53
84
  const transfer = autotransfer ? [] : findTransferables(data);
54
- if (worker) {
55
- self.postMessage(data, { transfer });
85
+ if (port) {
86
+ port.postMessage(data, { transfer });
87
+ }
88
+ else if (scopeIsDedicated(scope)) {
89
+ scope.postMessage(data, { transfer });
56
90
  }
57
- else {
58
- await self.clients.matchAll().then((clients) => {
91
+ else if (scopeIsService(scope)) {
92
+ await scope.clients.matchAll().then((clients) => {
59
93
  clients.forEach((client) => client.postMessage(data, { transfer }));
60
94
  });
61
95
  }
62
96
  };
63
- // Listen for messages from the client
64
- self.addEventListener("message", async (event) => {
97
+ const listener = async (event) => {
98
+ if (PayloadInitializeSchema.allows(event.data)) {
99
+ const { localStorageData } = event.data;
100
+ l.debug(null, "Setting up faux localStorage", localStorageData);
101
+ new FauxLocalStorage(localStorageData).register(scope);
102
+ return;
103
+ }
65
104
  // Decode the payload
66
105
  const { requestId, functionName } = PayloadHeaderSchema(type.enumerated(...Object.keys(procedures))).assert(event.data);
67
106
  l.debug(requestId, `Received request for ${functionName}`, event.data);
@@ -92,6 +131,8 @@ export function Server(procedures, { worker, loglevel = "debug" } = {}) {
92
131
  }
93
132
  // Define payload schema for incoming messages
94
133
  const payload = PayloadSchema(type(`"${functionName}"`), schemas.input, schemas.progress, schemas.success).assert(event.data);
134
+ if ("localStorageData" in payload)
135
+ throw "Unreachable: #initialize request payload should've been handled already";
95
136
  // Handle abortion requests (pro-choice ftw!!)
96
137
  if (payload.abort) {
97
138
  const controller = abortControllers.get(requestId);
@@ -134,7 +175,24 @@ export function Server(procedures, { worker, loglevel = "debug" } = {}) {
134
175
  finally {
135
176
  abortedRequests.delete(requestId);
136
177
  }
137
- });
178
+ };
179
+ // Listen for messages from the client
180
+ if (scopeIsShared(scope)) {
181
+ if (!port)
182
+ throw new Error("SharedWorker port not initialized");
183
+ console.log("Listening for shared worker messages on port", port);
184
+ port.addEventListener("message", listener);
185
+ port.start();
186
+ }
187
+ else if (scopeIsDedicated(scope)) {
188
+ scope.addEventListener("message", listener);
189
+ }
190
+ else if (scopeIsService(scope)) {
191
+ scope.addEventListener("message", listener);
192
+ }
193
+ else {
194
+ throw new Error(`Unsupported worker scope ${scope}`);
195
+ }
138
196
  };
139
197
  return instance;
140
198
  }