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 +18 -0
- package/dist/client.d.ts +19 -4
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +32 -12
- package/dist/localstorage.d.ts +14 -0
- package/dist/localstorage.d.ts.map +1 -0
- package/dist/localstorage.js +39 -0
- package/dist/server.d.ts +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +13 -4
- package/dist/types.d.ts +62 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +9 -4
- package/package.json +5 -5
- package/src/client.ts +52 -16
- package/src/localstorage.ts +46 -0
- package/src/server.ts +15 -3
- package/src/types.ts +14 -6
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>(
|
|
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
|
package/dist/client.d.ts.map
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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(
|
|
105
|
-
await startClientListener(
|
|
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(
|
|
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", {
|
|
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
|
|
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.
|
|
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.
|
package/dist/server.d.ts.map
CHANGED
|
@@ -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,
|
|
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.
|
|
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
|
-
|
|
76
|
+
l.debug(null, "Awaiting shared worker connection...");
|
|
76
77
|
scope.addEventListener("connect", ({ ports: [port] }) => {
|
|
77
|
-
|
|
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
|
*/
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,KAAK,IAAI,EAAE,MAAM,SAAS,CAAA;AACzC,OAAO,
|
|
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
|
-
"
|
|
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.
|
|
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.
|
|
51
|
+
"typedoc": "^0.28.11",
|
|
52
52
|
"typedoc-material-theme": "^1.4.0",
|
|
53
|
-
"typedoc-plugin-dt-links": "^2.0.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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", {
|
|
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
|
|
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.
|
|
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
|
-
|
|
141
|
+
l.debug(null, "Awaiting shared worker connection...")
|
|
140
142
|
scope.addEventListener("connect", ({ ports: [port] }) => {
|
|
141
|
-
|
|
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 {
|
|
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
|
-
"
|
|
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.
|