swarpc 0.10.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
@@ -14,6 +14,19 @@ 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}
@@ -25,16 +38,18 @@ export type SwarpcClient<Procedures extends ProceduresMap> = {
25
38
  * @param options.hooks Hooks to run on messages received from the server. See {@link Hooks}
26
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.
27
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)
28
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}
29
43
  *
30
44
  * An example of defining and using a client:
31
45
  * {@includeCode ../example/src/routes/+page.svelte}
32
46
  */
33
- export declare function Client<Procedures extends ProceduresMap>(procedures: Procedures, { worker, loglevel, restartListener, hooks, }?: {
47
+ export declare function Client<Procedures extends ProceduresMap>(procedures: Procedures, { worker, loglevel, restartListener, hooks, localStorage, }?: {
34
48
  worker?: Worker | SharedWorker;
35
49
  hooks?: Hooks<Procedures>;
36
50
  loglevel?: LogLevel;
37
51
  restartListener?: boolean;
52
+ localStorage?: Record<string, any>;
38
53
  }): SwarpcClient<Procedures>;
39
54
  /**
40
55
  * A quicker version of postMessage that does not try to start the client listener, await the service worker, etc.
@@ -47,15 +62,15 @@ export declare function Client<Procedures extends ProceduresMap>(procedures: Pro
47
62
  export declare function postMessageSync<Procedures extends ProceduresMap>(l: Logger, worker: Worker | SharedWorker | undefined, message: Payload<Procedures>, options?: StructuredSerializeOptions): void;
48
63
  /**
49
64
  * 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
65
+ * @param ctx.worker if provided, the client will use this worker to listen for messages, instead of using the service worker
52
66
  * @returns
53
67
  */
54
- export declare function startClientListener<Procedures extends ProceduresMap>(l: Logger, worker?: Worker | SharedWorker, hooks?: Hooks<Procedures>): Promise<void>;
68
+ export declare function startClientListener<Procedures extends ProceduresMap>(ctx: Context<Procedures>): Promise<void>;
55
69
  /**
56
70
  * Generate a random request ID, used to identify requests between client and server.
57
71
  * @source
58
72
  * @returns a 6-character hexadecimal string
59
73
  */
60
74
  export declare function makeRequestId(): string;
75
+ export {};
61
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,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"}
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
@@ -24,12 +24,13 @@ let _clientListenerStarted = false;
24
24
  * @param options.hooks Hooks to run on messages received from the server. See {@link Hooks}
25
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
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)
27
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}
28
29
  *
29
30
  * An example of defining and using a client:
30
31
  * {@includeCode ../example/src/routes/+page.svelte}
31
32
  */
32
- export function Client(procedures, { worker, loglevel = "debug", restartListener = false, hooks = {}, } = {}) {
33
+ export function Client(procedures, { worker, loglevel = "debug", restartListener = false, hooks = {}, localStorage = {}, } = {}) {
33
34
  const l = createLogger("client", loglevel);
34
35
  if (restartListener)
35
36
  _clientListenerStarted = false;
@@ -40,7 +41,13 @@ export function Client(procedures, { worker, loglevel = "debug", restartListener
40
41
  throw new Error(`[SWARPC Client] Invalid function name, don't use symbols`);
41
42
  }
42
43
  const send = async (requestId, msg, options) => {
43
- return postMessage(l, worker, hooks, {
44
+ const ctx = {
45
+ logger: l,
46
+ worker,
47
+ hooks,
48
+ localStorage,
49
+ };
50
+ return postMessage(ctx, {
44
51
  ...msg,
45
52
  by: "sw&rpc",
46
53
  requestId,
@@ -101,8 +108,9 @@ export function Client(procedures, { worker, loglevel = "debug", restartListener
101
108
  * Warms up the client by starting the listener and getting the worker, then posts a message to the worker.
102
109
  * @returns the worker to use
103
110
  */
104
- async function postMessage(l, worker, hooks, message, options) {
105
- await startClientListener(l, worker, hooks);
111
+ async function postMessage(ctx, message, options) {
112
+ await startClientListener(ctx);
113
+ const { logger: l, worker } = ctx;
106
114
  if (!worker && !navigator.serviceWorker.controller)
107
115
  l.warn("", "Service Worker is not controlling the page");
108
116
  // If no worker is provided, we use the service worker
@@ -140,13 +148,13 @@ export function postMessageSync(l, worker, message, options) {
140
148
  }
141
149
  /**
142
150
  * Starts the client listener, which listens for messages from the sw&rpc server.
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
151
+ * @param ctx.worker if provided, the client will use this worker to listen for messages, instead of using the service worker
145
152
  * @returns
146
153
  */
147
- export async function startClientListener(l, worker, hooks = {}) {
154
+ export async function startClientListener(ctx) {
148
155
  if (_clientListenerStarted)
149
156
  return;
157
+ const { logger: l, worker } = ctx;
150
158
  // Get service worker registration if no worker is provided
151
159
  if (!worker) {
152
160
  const sw = await navigator.serviceWorker.ready;
@@ -159,7 +167,7 @@ export async function startClientListener(l, worker, hooks = {}) {
159
167
  }
160
168
  const w = worker ?? navigator.serviceWorker;
161
169
  // Start listening for messages
162
- l.debug(null, "Starting client listener", { worker, w, hooks });
170
+ l.debug(null, "Starting client listener", { w, ...ctx });
163
171
  const listener = (event) => {
164
172
  // Get the data from the event
165
173
  const eventData = event.data || {};
@@ -167,7 +175,13 @@ export async function startClientListener(l, worker, hooks = {}) {
167
175
  if (eventData?.by !== "sw&rpc")
168
176
  return;
169
177
  // We don't use a arktype schema here, we trust the server to send valid data
170
- 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;
171
185
  // Sanity check in case we somehow receive a message without requestId
172
186
  if (!requestId) {
173
187
  throw new Error("[SWARPC Client] Message received without requestId");
@@ -180,16 +194,16 @@ export async function startClientListener(l, worker, hooks = {}) {
180
194
  // React to the data received: call hook, call handler,
181
195
  // and remove the request from pendingRequests (unless it's a progress update)
182
196
  if ("error" in data) {
183
- hooks.error?.(data.functionName, new Error(data.error.message));
197
+ ctx.hooks.error?.(data.functionName, new Error(data.error.message));
184
198
  handlers.reject(new Error(data.error.message));
185
199
  pendingRequests.delete(requestId);
186
200
  }
187
201
  else if ("progress" in data) {
188
- hooks.progress?.(data.functionName, data.progress);
202
+ ctx.hooks.progress?.(data.functionName, data.progress);
189
203
  handlers.onProgress(data.progress);
190
204
  }
191
205
  else if ("result" in data) {
192
- hooks.success?.(data.functionName, data.result);
206
+ ctx.hooks.success?.(data.functionName, data.result);
193
207
  handlers.resolve(data.result);
194
208
  pendingRequests.delete(requestId);
195
209
  }
@@ -202,6 +216,12 @@ export async function startClientListener(l, worker, hooks = {}) {
202
216
  w.addEventListener("message", listener);
203
217
  }
204
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
+ });
205
225
  }
206
226
  /**
207
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
@@ -19,7 +19,7 @@ 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 The worker scope to use, defaults to the `self` of the file where Server() is called.
22
+ * @param options.scope The worker scope to use, defaults to the `self` of the file where Server() is called.
23
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
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.
25
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.
@@ -1 +1 @@
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"}
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
@@ -5,8 +5,9 @@
5
5
  /// <reference lib="webworker" />
6
6
  import { type } from "arktype";
7
7
  import { createLogger } from "./log.js";
8
- import { PayloadHeaderSchema, PayloadSchema, zImplementations, zProcedures, } from "./types.js";
8
+ import { PayloadHeaderSchema, PayloadInitializeSchema, PayloadSchema, zImplementations, zProcedures, } from "./types.js";
9
9
  import { findTransferables } from "./utils.js";
10
+ import { FauxLocalStorage } from "./localstorage.js";
10
11
  class MockedWorkerGlobalScope {
11
12
  constructor() { }
12
13
  }
@@ -19,7 +20,7 @@ const abortedRequests = new Set();
19
20
  * Creates a sw&rpc server instance.
20
21
  * @param procedures procedures the server will implement, see {@link ProceduresMap}
21
22
  * @param options various options
22
- * @param options.worker The worker scope to use, defaults to the `self` of the file where Server() is called.
23
+ * @param options.scope The worker scope to use, defaults to the `self` of the file where Server() is called.
23
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.
24
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.
25
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.
@@ -72,9 +73,9 @@ export function Server(procedures, { loglevel = "debug", scope, _scopeType, } =
72
73
  const port = await new Promise((resolve) => {
73
74
  if (!scopeIsShared(scope))
74
75
  return resolve(undefined);
75
- console.log("Awaiting shared worker connection...");
76
+ l.debug(null, "Awaiting shared worker connection...");
76
77
  scope.addEventListener("connect", ({ ports: [port] }) => {
77
- console.log("Shared worker connected with port", port);
78
+ l.debug(null, "Shared worker connected with port", port);
78
79
  resolve(port);
79
80
  });
80
81
  });
@@ -94,6 +95,12 @@ export function Server(procedures, { loglevel = "debug", scope, _scopeType, } =
94
95
  }
95
96
  };
96
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
+ }
97
104
  // Decode the payload
98
105
  const { requestId, functionName } = PayloadHeaderSchema(type.enumerated(...Object.keys(procedures))).assert(event.data);
99
106
  l.debug(requestId, `Received request for ${functionName}`, event.data);
@@ -124,6 +131,8 @@ export function Server(procedures, { loglevel = "debug", scope, _scopeType, } =
124
131
  }
125
132
  // Define payload schema for incoming messages
126
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";
127
136
  // Handle abortion requests (pro-choice ftw!!)
128
137
  if (payload.abort) {
129
138
  const controller = abortControllers.get(requestId);
package/dist/types.d.ts CHANGED
@@ -105,6 +105,12 @@ export type Hooks<Procedures extends ProceduresMap> = {
105
105
  */
106
106
  progress?: <Procedure extends keyof ProceduresMap>(procedure: Procedure, data: Procedures[Procedure]["progress"]["inferOut"]) => void;
107
107
  };
108
+ export declare const PayloadInitializeSchema: import("arktype/internal/methods/object.ts").ObjectType<{
109
+ by: "sw&rpc";
110
+ functionName: "#initialize";
111
+ localStorageData: Record<string, unknown>;
112
+ }, {}>;
113
+ export type PayloadInitialize = typeof PayloadInitializeSchema.infer;
108
114
  /**
109
115
  * @source
110
116
  */
@@ -150,7 +156,7 @@ export type PayloadCore<PM extends ProceduresMap, Name extends keyof PM = keyof
150
156
  /**
151
157
  * @source
152
158
  */
153
- export declare const PayloadSchema: import("arktype").Generic<[["Name", string], ["I", unknown], ["P", unknown], ["S", unknown]], readonly ["PayloadHeaderSchema<Name>", "&", "PayloadCoreSchema<I, P, S>"], {
159
+ export declare const PayloadSchema: import("arktype").Generic<[["Name", string], ["I", unknown], ["P", unknown], ["S", unknown]], readonly [readonly ["PayloadHeaderSchema<Name>", "&", "PayloadCoreSchema<I, P, S>"], "|", "PayloadInitializeSchema"], {
154
160
  PayloadCoreSchema: import("arktype/internal/scope.ts").bindGenericToScope<import("@ark/schema").GenericAst<[["I", unknown], ["P", unknown], ["S", unknown]], {
155
161
  readonly "input?": "I";
156
162
  readonly "progress?": "P";
@@ -178,6 +184,17 @@ export declare const PayloadSchema: import("arktype").Generic<[["Name", string],
178
184
  readonly functionName: "Name";
179
185
  readonly requestId: "string >= 1";
180
186
  }, {}, {}>;
187
+ PayloadInitializeSchema: import("arktype/internal/methods/object.ts").ObjectType<{
188
+ by: "sw&rpc";
189
+ functionName: "#initialize";
190
+ localStorageData: Record<string, unknown>;
191
+ }, {}> & {
192
+ readonly " brand": [import("arktype/internal/methods/object.ts").ObjectType<{
193
+ by: "sw&rpc";
194
+ functionName: "#initialize";
195
+ localStorageData: Record<string, unknown>;
196
+ }, {}>, "unparsed"];
197
+ };
181
198
  } & {}>;
182
199
  PayloadHeaderSchema: import("arktype/internal/scope.ts").bindGenericToScope<import("@ark/schema").GenericAst<[["Name", string]], {
183
200
  readonly by: "\"sw&rpc\"";
@@ -200,7 +217,23 @@ export declare const PayloadSchema: import("arktype").Generic<[["Name", string],
200
217
  readonly functionName: "Name";
201
218
  readonly requestId: "string >= 1";
202
219
  }, {}, {}>;
220
+ PayloadInitializeSchema: import("arktype/internal/methods/object.ts").ObjectType<{
221
+ by: "sw&rpc";
222
+ functionName: "#initialize";
223
+ localStorageData: Record<string, unknown>;
224
+ }, {}> & {
225
+ readonly " brand": [import("arktype/internal/methods/object.ts").ObjectType<{
226
+ by: "sw&rpc";
227
+ functionName: "#initialize";
228
+ localStorageData: Record<string, unknown>;
229
+ }, {}>, "unparsed"];
230
+ };
203
231
  } & {}>;
232
+ PayloadInitializeSchema: {
233
+ by: "sw&rpc";
234
+ functionName: "#initialize";
235
+ localStorageData: Record<string, unknown>;
236
+ };
204
237
  }, {
205
238
  PayloadCoreSchema: import("arktype/internal/scope.ts").bindGenericToScope<import("@ark/schema").GenericAst<[["I", unknown], ["P", unknown], ["S", unknown]], {
206
239
  readonly "input?": "I";
@@ -229,6 +262,17 @@ export declare const PayloadSchema: import("arktype").Generic<[["Name", string],
229
262
  readonly functionName: "Name";
230
263
  readonly requestId: "string >= 1";
231
264
  }, {}, {}>;
265
+ PayloadInitializeSchema: import("arktype/internal/methods/object.ts").ObjectType<{
266
+ by: "sw&rpc";
267
+ functionName: "#initialize";
268
+ localStorageData: Record<string, unknown>;
269
+ }, {}> & {
270
+ readonly " brand": [import("arktype/internal/methods/object.ts").ObjectType<{
271
+ by: "sw&rpc";
272
+ functionName: "#initialize";
273
+ localStorageData: Record<string, unknown>;
274
+ }, {}>, "unparsed"];
275
+ };
232
276
  } & {}>;
233
277
  PayloadHeaderSchema: import("arktype/internal/scope.ts").bindGenericToScope<import("@ark/schema").GenericAst<[["Name", string]], {
234
278
  readonly by: "\"sw&rpc\"";
@@ -251,12 +295,28 @@ export declare const PayloadSchema: import("arktype").Generic<[["Name", string],
251
295
  readonly functionName: "Name";
252
296
  readonly requestId: "string >= 1";
253
297
  }, {}, {}>;
298
+ PayloadInitializeSchema: import("arktype/internal/methods/object.ts").ObjectType<{
299
+ by: "sw&rpc";
300
+ functionName: "#initialize";
301
+ localStorageData: Record<string, unknown>;
302
+ }, {}> & {
303
+ readonly " brand": [import("arktype/internal/methods/object.ts").ObjectType<{
304
+ by: "sw&rpc";
305
+ functionName: "#initialize";
306
+ localStorageData: Record<string, unknown>;
307
+ }, {}>, "unparsed"];
308
+ };
254
309
  } & {}>;
310
+ PayloadInitializeSchema: {
311
+ by: "sw&rpc";
312
+ functionName: "#initialize";
313
+ localStorageData: Record<string, unknown>;
314
+ };
255
315
  }>;
256
316
  /**
257
317
  * The effective payload as sent by the server to the client
258
318
  */
259
- export type Payload<PM extends ProceduresMap, Name extends keyof PM = keyof PM> = PayloadHeader<PM, Name> & PayloadCore<PM, Name>;
319
+ export type Payload<PM extends ProceduresMap, Name extends keyof PM = keyof PM> = (PayloadHeader<PM, Name> & PayloadCore<PM, Name>) | PayloadInitialize;
260
320
  /**
261
321
  * A procedure's corresponding method on the client instance -- used to call the procedure. If you want to be able to cancel the request, you can use the `cancelable` method instead of running the procedure directly.
262
322
  */
@@ -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/dist/types.js CHANGED
@@ -3,6 +3,11 @@
3
3
  * @mergeModuleWith <project>
4
4
  */
5
5
  import { type } from "arktype";
6
+ export const PayloadInitializeSchema = type({
7
+ by: '"sw&rpc"',
8
+ functionName: '"#initialize"',
9
+ localStorageData: "Record<string, unknown>",
10
+ });
6
11
  /**
7
12
  * @source
8
13
  */
@@ -25,11 +30,11 @@ export const PayloadCoreSchema = type("<I, P, S>", {
25
30
  * @source
26
31
  */
27
32
  export const PayloadSchema = type
28
- .scope({ PayloadCoreSchema, PayloadHeaderSchema })
33
+ .scope({ PayloadCoreSchema, PayloadHeaderSchema, PayloadInitializeSchema })
29
34
  .type("<Name extends string, I, P, S>", [
30
- "PayloadHeaderSchema<Name>",
31
- "&",
32
- "PayloadCoreSchema<I, P, S>",
35
+ ["PayloadHeaderSchema<Name>", "&", "PayloadCoreSchema<I, P, S>"],
36
+ "|",
37
+ "PayloadInitializeSchema",
33
38
  ]);
34
39
  /**
35
40
  * Symbol used as the key for the procedures map on the server instance
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swarpc",
3
- "version": "0.10.0",
3
+ "version": "0.11.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",
@@ -48,15 +48,15 @@
48
48
  "nodemon": "^3.1.10",
49
49
  "prettier": "^3.6.2",
50
50
  "sirv-cli": "^3.0.1",
51
- "typedoc": "^0.28.9",
51
+ "typedoc": "^0.28.11",
52
52
  "typedoc-material-theme": "^1.4.0",
53
- "typedoc-plugin-dt-links": "^2.0.13",
53
+ "typedoc-plugin-dt-links": "^2.0.16",
54
54
  "typedoc-plugin-extras": "^4.0.1",
55
55
  "typedoc-plugin-inline-sources": "^1.3.0",
56
- "typedoc-plugin-mdn-links": "^5.0.7",
56
+ "typedoc-plugin-mdn-links": "^5.0.9",
57
57
  "typedoc-plugin-redirect": "^1.2.0",
58
58
  "typescript": "^5.9.2",
59
- "vite": "^7.0.6",
59
+ "vite": "^7.1.3",
60
60
  "vitest": "^3.2.4"
61
61
  },
62
62
  "volta": {
package/src/client.ts CHANGED
@@ -25,6 +25,20 @@ export type SwarpcClient<Procedures extends ProceduresMap> = {
25
25
  [F in keyof Procedures]: ClientMethod<Procedures[F]>
26
26
  }
27
27
 
28
+ /**
29
+ * Context for passing around data useful for requests
30
+ */
31
+ type Context<Procedures extends ProceduresMap> = {
32
+ /** A logger, bound to the client */
33
+ logger: Logger
34
+ /** The worker instance to use */
35
+ worker: Worker | SharedWorker | undefined
36
+ /** Hooks defined by the client */
37
+ hooks: Hooks<Procedures>
38
+ /** Local storage data defined by the client for the faux local storage */
39
+ localStorage: Record<string, any>
40
+ }
41
+
28
42
  /**
29
43
  * Pending requests are stored in a map, where the key is the request ID.
30
44
  * Each request has a set of handlers: resolve, reject, and onProgress.
@@ -52,6 +66,7 @@ let _clientListenerStarted = false
52
66
  * @param options.hooks Hooks to run on messages received from the server. See {@link Hooks}
53
67
  * @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
68
  * @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.
69
+ * @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)
55
70
  * @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}
56
71
  *
57
72
  * An example of defining and using a client:
@@ -64,11 +79,13 @@ export function Client<Procedures extends ProceduresMap>(
64
79
  loglevel = "debug",
65
80
  restartListener = false,
66
81
  hooks = {},
82
+ localStorage = {},
67
83
  }: {
68
84
  worker?: Worker | SharedWorker
69
85
  hooks?: Hooks<Procedures>
70
86
  loglevel?: LogLevel
71
87
  restartListener?: boolean
88
+ localStorage?: Record<string, any>
72
89
  } = {}
73
90
  ): SwarpcClient<Procedures> {
74
91
  const l = createLogger("client", loglevel)
@@ -94,10 +111,15 @@ export function Client<Procedures extends ProceduresMap>(
94
111
  msg: PayloadCore<Procedures, typeof functionName>,
95
112
  options?: StructuredSerializeOptions
96
113
  ) => {
97
- return postMessage(
98
- l,
114
+ const ctx: Context<Procedures> = {
115
+ logger: l,
99
116
  worker,
100
117
  hooks,
118
+ localStorage,
119
+ }
120
+
121
+ return postMessage(
122
+ ctx,
101
123
  {
102
124
  ...msg,
103
125
  by: "sw&rpc",
@@ -179,13 +201,13 @@ export function Client<Procedures extends ProceduresMap>(
179
201
  * @returns the worker to use
180
202
  */
181
203
  async function postMessage<Procedures extends ProceduresMap>(
182
- l: Logger,
183
- worker: Worker | SharedWorker | undefined,
184
- hooks: Hooks<Procedures>,
204
+ ctx: Context<Procedures>,
185
205
  message: Payload<Procedures>,
186
206
  options?: StructuredSerializeOptions
187
207
  ) {
188
- await startClientListener(l, worker, hooks)
208
+ await startClientListener(ctx)
209
+
210
+ const { logger: l, worker } = ctx
189
211
 
190
212
  if (!worker && !navigator.serviceWorker.controller)
191
213
  l.warn("", "Service Worker is not controlling the page")
@@ -239,17 +261,16 @@ export function postMessageSync<Procedures extends ProceduresMap>(
239
261
 
240
262
  /**
241
263
  * Starts the client listener, which listens for messages from the sw&rpc server.
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
264
+ * @param ctx.worker if provided, the client will use this worker to listen for messages, instead of using the service worker
244
265
  * @returns
245
266
  */
246
267
  export async function startClientListener<Procedures extends ProceduresMap>(
247
- l: Logger,
248
- worker?: Worker | SharedWorker,
249
- hooks: Hooks<Procedures> = {}
268
+ ctx: Context<Procedures>
250
269
  ) {
251
270
  if (_clientListenerStarted) return
252
271
 
272
+ const { logger: l, worker } = ctx
273
+
253
274
  // Get service worker registration if no worker is provided
254
275
  if (!worker) {
255
276
  const sw = await navigator.serviceWorker.ready
@@ -265,7 +286,7 @@ export async function startClientListener<Procedures extends ProceduresMap>(
265
286
  const w = worker ?? navigator.serviceWorker
266
287
 
267
288
  // Start listening for messages
268
- l.debug(null, "Starting client listener", { worker, w, hooks })
289
+ l.debug(null, "Starting client listener", { w, ...ctx })
269
290
  const listener = (event: Event): void => {
270
291
  // Get the data from the event
271
292
  const eventData = (event as MessageEvent).data || {}
@@ -274,7 +295,15 @@ export async function startClientListener<Procedures extends ProceduresMap>(
274
295
  if (eventData?.by !== "sw&rpc") return
275
296
 
276
297
  // We don't use a arktype schema here, we trust the server to send valid data
277
- const { requestId, ...data } = eventData as Payload<Procedures>
298
+ const payload = eventData as Payload<Procedures>
299
+
300
+ // Ignore #initialize request, it's client->server only
301
+ if ("localStorageData" in payload) {
302
+ l.warn(null, "Ignoring unexpected #initialize from server", payload)
303
+ return
304
+ }
305
+
306
+ const { requestId, ...data } = payload
278
307
 
279
308
  // Sanity check in case we somehow receive a message without requestId
280
309
  if (!requestId) {
@@ -292,14 +321,14 @@ export async function startClientListener<Procedures extends ProceduresMap>(
292
321
  // React to the data received: call hook, call handler,
293
322
  // and remove the request from pendingRequests (unless it's a progress update)
294
323
  if ("error" in data) {
295
- hooks.error?.(data.functionName, new Error(data.error.message))
324
+ ctx.hooks.error?.(data.functionName, new Error(data.error.message))
296
325
  handlers.reject(new Error(data.error.message))
297
326
  pendingRequests.delete(requestId)
298
327
  } else if ("progress" in data) {
299
- hooks.progress?.(data.functionName, data.progress)
328
+ ctx.hooks.progress?.(data.functionName, data.progress)
300
329
  handlers.onProgress(data.progress)
301
330
  } else if ("result" in data) {
302
- hooks.success?.(data.functionName, data.result)
331
+ ctx.hooks.success?.(data.functionName, data.result)
303
332
  handlers.resolve(data.result)
304
333
  pendingRequests.delete(requestId)
305
334
  }
@@ -313,6 +342,13 @@ export async function startClientListener<Procedures extends ProceduresMap>(
313
342
  }
314
343
 
315
344
  _clientListenerStarted = true
345
+
346
+ // Recursive terminal case is ensured by calling this *after* _clientListenerStarted is set to true: startClientListener() will therefore not be called in postMessage() again.
347
+ await postMessage(ctx, {
348
+ by: "sw&rpc",
349
+ functionName: "#initialize",
350
+ localStorageData: ctx.localStorage,
351
+ })
316
352
  }
317
353
 
318
354
  /**
@@ -0,0 +1,46 @@
1
+ export class FauxLocalStorage {
2
+ data: Record<string, any>
3
+ keysOrder: string[]
4
+
5
+ constructor(data: Record<string, any>) {
6
+ this.data = data
7
+ this.keysOrder = Object.keys(data)
8
+ }
9
+
10
+ setItem(key: string, value: string) {
11
+ if (!this.hasItem(key)) this.keysOrder.push(key)
12
+ this.data[key] = value
13
+ }
14
+
15
+ getItem(key: string) {
16
+ return this.data[key]
17
+ }
18
+
19
+ hasItem(key: string) {
20
+ return Object.hasOwn(this.data, key)
21
+ }
22
+
23
+ removeItem(key: string) {
24
+ if (!this.hasItem(key)) return
25
+ delete this.data[key]
26
+ this.keysOrder = this.keysOrder.filter((k) => k !== key)
27
+ }
28
+
29
+ clear() {
30
+ this.data = {}
31
+ this.keysOrder = []
32
+ }
33
+
34
+ key(index: number) {
35
+ return this.keysOrder[index]
36
+ }
37
+
38
+ get length() {
39
+ return this.keysOrder.length
40
+ }
41
+
42
+ register(subject: WorkerGlobalScope | SharedWorkerGlobalScope) {
43
+ // @ts-expect-error
44
+ subject.localStorage = this
45
+ }
46
+ }
package/src/server.ts CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  Payload,
12
12
  PayloadCore,
13
13
  PayloadHeaderSchema,
14
+ PayloadInitializeSchema,
14
15
  PayloadSchema,
15
16
  ProcedureImplementation,
16
17
  zImplementations,
@@ -18,6 +19,7 @@ import {
18
19
  type ProceduresMap,
19
20
  } from "./types.js"
20
21
  import { findTransferables } from "./utils.js"
22
+ import { FauxLocalStorage } from "./localstorage.js"
21
23
 
22
24
  class MockedWorkerGlobalScope {
23
25
  constructor() {}
@@ -57,7 +59,7 @@ const abortedRequests = new Set<string>()
57
59
  * Creates a sw&rpc server instance.
58
60
  * @param procedures procedures the server will implement, see {@link ProceduresMap}
59
61
  * @param options various options
60
- * @param options.worker The worker scope to use, defaults to the `self` of the file where Server() is called.
62
+ * @param options.scope The worker scope to use, defaults to the `self` of the file where Server() is called.
61
63
  * @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
64
  * @param options._scopeType @internal Don't touch, this is used in testing environments because the mock is subpar. Manually overrides worker scope type detection.
63
65
  * @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.
@@ -136,9 +138,9 @@ export function Server<Procedures extends ProceduresMap>(
136
138
  instance.start = async () => {
137
139
  const port = await new Promise<MessagePort | undefined>((resolve) => {
138
140
  if (!scopeIsShared(scope)) return resolve(undefined)
139
- console.log("Awaiting shared worker connection...")
141
+ l.debug(null, "Awaiting shared worker connection...")
140
142
  scope.addEventListener("connect", ({ ports: [port] }) => {
141
- console.log("Shared worker connected with port", port)
143
+ l.debug(null, "Shared worker connected with port", port)
142
144
  resolve(port)
143
145
  })
144
146
  })
@@ -164,6 +166,13 @@ export function Server<Procedures extends ProceduresMap>(
164
166
  const listener = async (
165
167
  event: MessageEvent<any> | ExtendableMessageEvent
166
168
  ): Promise<void> => {
169
+ if (PayloadInitializeSchema.allows(event.data)) {
170
+ const { localStorageData } = event.data
171
+ l.debug(null, "Setting up faux localStorage", localStorageData)
172
+ new FauxLocalStorage(localStorageData).register(scope)
173
+ return
174
+ }
175
+
167
176
  // Decode the payload
168
177
  const { requestId, functionName } = PayloadHeaderSchema(
169
178
  type.enumerated(...Object.keys(procedures))
@@ -211,6 +220,9 @@ export function Server<Procedures extends ProceduresMap>(
211
220
  schemas.success
212
221
  ).assert(event.data)
213
222
 
223
+ if ("localStorageData" in payload)
224
+ throw "Unreachable: #initialize request payload should've been handled already"
225
+
214
226
  // Handle abortion requests (pro-choice ftw!!)
215
227
  if (payload.abort) {
216
228
  const controller = abortControllers.get(requestId)
package/src/types.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { type, type Type } from "arktype"
7
- import { Logger, RequestBoundLogger } from "./log.js"
7
+ import { RequestBoundLogger } from "./log.js"
8
8
 
9
9
  /**
10
10
  * A procedure declaration
@@ -131,6 +131,14 @@ export type Hooks<Procedures extends ProceduresMap> = {
131
131
  ) => void
132
132
  }
133
133
 
134
+ export const PayloadInitializeSchema = type({
135
+ by: '"sw&rpc"',
136
+ functionName: '"#initialize"',
137
+ localStorageData: "Record<string, unknown>",
138
+ })
139
+
140
+ export type PayloadInitialize = typeof PayloadInitializeSchema.infer
141
+
134
142
  /**
135
143
  * @source
136
144
  */
@@ -184,11 +192,11 @@ export type PayloadCore<
184
192
  * @source
185
193
  */
186
194
  export const PayloadSchema = type
187
- .scope({ PayloadCoreSchema, PayloadHeaderSchema })
195
+ .scope({ PayloadCoreSchema, PayloadHeaderSchema, PayloadInitializeSchema })
188
196
  .type("<Name extends string, I, P, S>", [
189
- "PayloadHeaderSchema<Name>",
190
- "&",
191
- "PayloadCoreSchema<I, P, S>",
197
+ ["PayloadHeaderSchema<Name>", "&", "PayloadCoreSchema<I, P, S>"],
198
+ "|",
199
+ "PayloadInitializeSchema",
192
200
  ])
193
201
 
194
202
  /**
@@ -197,7 +205,7 @@ export const PayloadSchema = type
197
205
  export type Payload<
198
206
  PM extends ProceduresMap,
199
207
  Name extends keyof PM = keyof PM,
200
- > = PayloadHeader<PM, Name> & PayloadCore<PM, Name>
208
+ > = (PayloadHeader<PM, Name> & PayloadCore<PM, Name>) | PayloadInitialize
201
209
 
202
210
  /**
203
211
  * A procedure's corresponding method on the client instance -- used to call the procedure. If you want to be able to cancel the request, you can use the `cancelable` method instead of running the procedure directly.