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 +18 -0
- package/dist/client.d.ts +37 -9
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +83 -21
- 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 +7 -4
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +70 -12
- package/dist/types.d.ts +63 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +9 -4
- package/package.json +5 -5
- package/src/client.ts +115 -27
- package/src/localstorage.ts +46 -0
- package/src/server.ts +98 -12
- package/src/types.ts +16 -8
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
|
|
22
|
-
*
|
|
23
|
-
* @
|
|
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>(
|
|
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
|
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,
|
|
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
|
|
21
|
-
*
|
|
22
|
-
* @
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
96
|
-
await startClientListener(
|
|
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
|
|
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(
|
|
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", {
|
|
128
|
-
|
|
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
|
|
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(
|
|
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.
|
|
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, {
|
|
29
|
-
|
|
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
|
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;
|
|
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.
|
|
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, {
|
|
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: (
|
|
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 = (
|
|
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 (
|
|
55
|
-
|
|
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
|
|
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
|
-
|
|
64
|
-
|
|
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
|
}
|