swarpc 0.10.0 → 0.12.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 +240 -153
- package/dist/client.d.ts +38 -10
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +80 -26
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/localstorage.d.ts +14 -0
- package/dist/localstorage.d.ts.map +1 -0
- package/dist/localstorage.js +39 -0
- package/dist/log.d.ts +4 -3
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +31 -21
- package/dist/nodes.d.ts +12 -0
- package/dist/nodes.d.ts.map +1 -0
- package/dist/nodes.js +36 -0
- package/dist/polyfills.d.ts +2 -0
- package/dist/polyfills.d.ts.map +1 -0
- package/dist/polyfills.js +20 -0
- package/dist/scopes.d.ts +4 -0
- package/dist/scopes.d.ts.map +1 -0
- package/dist/scopes.js +15 -0
- package/dist/server.d.ts +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +28 -30
- package/dist/types.d.ts +77 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +9 -4
- package/package.json +22 -9
- package/src/client.ts +208 -103
- package/src/index.ts +9 -8
- package/src/localstorage.ts +46 -0
- package/src/log.ts +136 -118
- package/src/nodes.ts +55 -0
- package/src/polyfills.ts +22 -0
- package/src/scopes.ts +35 -0
- package/src/server.ts +273 -287
- package/src/types.ts +258 -231
- package/src/utils.ts +34 -34
package/src/client.ts
CHANGED
|
@@ -3,16 +3,23 @@
|
|
|
3
3
|
* @mergeModuleWith <project>
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
createLogger,
|
|
8
|
+
RequestBoundLogger,
|
|
9
|
+
type Logger,
|
|
10
|
+
type LogLevel,
|
|
11
|
+
} from "./log.js";
|
|
12
|
+
import { makeNodeId, whoToSendTo } from "./nodes.js";
|
|
7
13
|
import {
|
|
8
14
|
ClientMethod,
|
|
9
15
|
Hooks,
|
|
10
16
|
Payload,
|
|
11
17
|
PayloadCore,
|
|
18
|
+
WorkerConstructor,
|
|
12
19
|
zProcedures,
|
|
13
20
|
type ProceduresMap,
|
|
14
|
-
} from "./types.js"
|
|
15
|
-
import { findTransferables } from "./utils.js"
|
|
21
|
+
} from "./types.js";
|
|
22
|
+
import { findTransferables } from "./utils.js";
|
|
16
23
|
|
|
17
24
|
/**
|
|
18
25
|
* The sw&rpc client instance, which provides {@link ClientMethod | methods to call procedures}.
|
|
@@ -20,38 +27,60 @@ import { findTransferables } from "./utils.js"
|
|
|
20
27
|
* If you want to be able to cancel the request, you can set the request's ID yourself, and call `.abort(requestId, reason)` on the client instance to cancel it.
|
|
21
28
|
*/
|
|
22
29
|
export type SwarpcClient<Procedures extends ProceduresMap> = {
|
|
23
|
-
[zProcedures]: Procedures
|
|
30
|
+
[zProcedures]: Procedures;
|
|
24
31
|
} & {
|
|
25
|
-
[F in keyof Procedures]: ClientMethod<Procedures[F]
|
|
26
|
-
}
|
|
32
|
+
[F in keyof Procedures]: ClientMethod<Procedures[F]>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Context for passing around data useful for requests
|
|
37
|
+
*/
|
|
38
|
+
type Context<Procedures extends ProceduresMap> = {
|
|
39
|
+
/** A logger, bound to the client */
|
|
40
|
+
logger: Logger;
|
|
41
|
+
/** The node to use */
|
|
42
|
+
node: Worker | SharedWorker | undefined;
|
|
43
|
+
/** The ID of the node to use */
|
|
44
|
+
nodeId: string | undefined;
|
|
45
|
+
/** Hooks defined by the client */
|
|
46
|
+
hooks: Hooks<Procedures>;
|
|
47
|
+
/** Local storage data defined by the client for the faux local storage */
|
|
48
|
+
localStorage: Record<string, any>;
|
|
49
|
+
};
|
|
27
50
|
|
|
28
51
|
/**
|
|
29
52
|
* Pending requests are stored in a map, where the key is the request ID.
|
|
30
53
|
* Each request has a set of handlers: resolve, reject, and onProgress.
|
|
31
54
|
* This allows having a single listener for the client, and having multiple in-flight calls to the same procedure.
|
|
32
55
|
*/
|
|
33
|
-
const pendingRequests = new Map<string, PendingRequest>()
|
|
34
|
-
type PendingRequest = {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
56
|
+
const pendingRequests = new Map<string, PendingRequest>();
|
|
57
|
+
export type PendingRequest = {
|
|
58
|
+
/** ID of the node the request was sent to. udefined if running on a service worker */
|
|
59
|
+
nodeId?: string;
|
|
60
|
+
functionName: string;
|
|
61
|
+
reject: (err: Error) => void;
|
|
62
|
+
onProgress: (progress: any) => void;
|
|
63
|
+
resolve: (result: any) => void;
|
|
64
|
+
};
|
|
40
65
|
|
|
41
66
|
// Have we started the client listener?
|
|
42
|
-
let _clientListenerStarted =
|
|
67
|
+
let _clientListenerStarted: Set<string> = new Set();
|
|
68
|
+
|
|
69
|
+
export type ClientOptions = Parameters<typeof Client>[1];
|
|
43
70
|
|
|
44
71
|
/**
|
|
45
72
|
*
|
|
46
73
|
* @param procedures procedures the client will be able to call, see {@link ProceduresMap}
|
|
47
74
|
* @param options various options
|
|
48
|
-
* @param options.worker The instantiated
|
|
49
|
-
* Example: `
|
|
75
|
+
* @param options.worker The worker class, **not instantiated**, or a path to the source code. If not provided, the client will use the service worker. If a string is provided, it'll instantiate a regular `Worker`, not a `SharedWorker`.
|
|
76
|
+
* Example: `"./worker.js"`
|
|
50
77
|
* See {@link Worker} (used by both dedicated workers and service workers), {@link SharedWorker}, and
|
|
51
78
|
* the different [worker types](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API#worker_types) that exist
|
|
52
79
|
* @param options.hooks Hooks to run on messages received from the server. See {@link Hooks}
|
|
53
80
|
* @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
81
|
* @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.
|
|
82
|
+
* @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)
|
|
83
|
+
* @param options.nodes the number of workers to use for the server, defaults to {@link navigator.hardwareConcurrency}.
|
|
55
84
|
* @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
85
|
*
|
|
57
86
|
* An example of defining and using a client:
|
|
@@ -61,117 +90,179 @@ export function Client<Procedures extends ProceduresMap>(
|
|
|
61
90
|
procedures: Procedures,
|
|
62
91
|
{
|
|
63
92
|
worker,
|
|
93
|
+
nodes: nodeCount,
|
|
64
94
|
loglevel = "debug",
|
|
65
95
|
restartListener = false,
|
|
66
96
|
hooks = {},
|
|
97
|
+
localStorage = {},
|
|
67
98
|
}: {
|
|
68
|
-
worker?:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
99
|
+
worker?: WorkerConstructor | string;
|
|
100
|
+
nodes?: number;
|
|
101
|
+
hooks?: Hooks<Procedures>;
|
|
102
|
+
loglevel?: LogLevel;
|
|
103
|
+
restartListener?: boolean;
|
|
104
|
+
localStorage?: Record<string, any>;
|
|
105
|
+
} = {},
|
|
73
106
|
): SwarpcClient<Procedures> {
|
|
74
|
-
const l = createLogger("client", loglevel)
|
|
107
|
+
const l = createLogger("client", loglevel);
|
|
75
108
|
|
|
76
|
-
if (restartListener) _clientListenerStarted
|
|
109
|
+
if (restartListener) _clientListenerStarted.clear();
|
|
77
110
|
|
|
78
111
|
// Store procedures on a symbol key, to avoid conflicts with procedure names
|
|
79
112
|
const instance = { [zProcedures]: procedures } as Partial<
|
|
80
113
|
SwarpcClient<Procedures>
|
|
81
|
-
|
|
114
|
+
>;
|
|
115
|
+
|
|
116
|
+
nodeCount ??= navigator.hardwareConcurrency || 1;
|
|
117
|
+
|
|
118
|
+
let nodes: undefined | Record<string, Worker | SharedWorker>;
|
|
119
|
+
if (worker) {
|
|
120
|
+
nodes = {};
|
|
121
|
+
for (const _ of Array.from({ length: nodeCount })) {
|
|
122
|
+
const id = makeNodeId();
|
|
123
|
+
if (typeof worker === "string") {
|
|
124
|
+
nodes[id] = new Worker(worker, { name: id });
|
|
125
|
+
} else {
|
|
126
|
+
nodes[id] = new worker({ name: id });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
l.info(
|
|
131
|
+
null,
|
|
132
|
+
`Started ${nodeCount} node${nodeCount > 1 ? "s" : ""}`,
|
|
133
|
+
Object.keys(nodes),
|
|
134
|
+
);
|
|
135
|
+
}
|
|
82
136
|
|
|
83
137
|
for (const functionName of Object.keys(procedures) as Array<
|
|
84
138
|
keyof Procedures
|
|
85
139
|
>) {
|
|
86
140
|
if (typeof functionName !== "string") {
|
|
87
141
|
throw new Error(
|
|
88
|
-
`[SWARPC Client] Invalid function name, don't use symbols
|
|
89
|
-
)
|
|
142
|
+
`[SWARPC Client] Invalid function name, don't use symbols`,
|
|
143
|
+
);
|
|
90
144
|
}
|
|
91
145
|
|
|
92
146
|
const send = async (
|
|
147
|
+
node: Worker | SharedWorker | undefined,
|
|
148
|
+
nodeId: string | undefined,
|
|
93
149
|
requestId: string,
|
|
94
150
|
msg: PayloadCore<Procedures, typeof functionName>,
|
|
95
|
-
options?: StructuredSerializeOptions
|
|
151
|
+
options?: StructuredSerializeOptions,
|
|
96
152
|
) => {
|
|
97
|
-
|
|
98
|
-
l,
|
|
99
|
-
|
|
153
|
+
const ctx: Context<Procedures> = {
|
|
154
|
+
logger: l,
|
|
155
|
+
node,
|
|
156
|
+
nodeId,
|
|
100
157
|
hooks,
|
|
158
|
+
localStorage,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
return postMessage(
|
|
162
|
+
ctx,
|
|
101
163
|
{
|
|
102
164
|
...msg,
|
|
103
165
|
by: "sw&rpc",
|
|
104
166
|
requestId,
|
|
105
167
|
functionName,
|
|
106
168
|
},
|
|
107
|
-
options
|
|
108
|
-
)
|
|
109
|
-
}
|
|
169
|
+
options,
|
|
170
|
+
);
|
|
171
|
+
};
|
|
110
172
|
|
|
111
173
|
// Set the method on the instance
|
|
112
174
|
const _runProcedure = async (
|
|
113
175
|
input: unknown,
|
|
114
176
|
onProgress: (progress: unknown) => void | Promise<void> = () => {},
|
|
115
|
-
reqid?: string
|
|
177
|
+
reqid?: string,
|
|
178
|
+
nodeId?: string,
|
|
116
179
|
) => {
|
|
117
180
|
// Validate the input against the procedure's input schema
|
|
118
|
-
procedures[functionName].input.assert(input)
|
|
181
|
+
procedures[functionName].input.assert(input);
|
|
182
|
+
|
|
183
|
+
const requestId = reqid ?? makeRequestId();
|
|
119
184
|
|
|
120
|
-
|
|
185
|
+
// Choose which node to use
|
|
186
|
+
nodeId ??= whoToSendTo(nodes, pendingRequests);
|
|
187
|
+
const node = nodes && nodeId ? nodes[nodeId] : undefined;
|
|
188
|
+
|
|
189
|
+
const l = createLogger("client", loglevel, nodeId ?? "(SW)", requestId);
|
|
121
190
|
|
|
122
191
|
return new Promise((resolve, reject) => {
|
|
123
192
|
// Store promise handlers (as well as progress updates handler)
|
|
124
193
|
// so the client listener can resolve/reject the promise (and react to progress updates)
|
|
125
194
|
// when the server sends messages back
|
|
126
195
|
pendingRequests.set(requestId, {
|
|
196
|
+
nodeId,
|
|
127
197
|
functionName,
|
|
128
198
|
resolve,
|
|
129
199
|
onProgress,
|
|
130
200
|
reject,
|
|
131
|
-
})
|
|
201
|
+
});
|
|
132
202
|
|
|
133
203
|
const transfer =
|
|
134
204
|
procedures[functionName].autotransfer === "always"
|
|
135
205
|
? findTransferables(input)
|
|
136
|
-
: []
|
|
206
|
+
: [];
|
|
137
207
|
|
|
138
208
|
// Post the message to the server
|
|
139
|
-
l.debug(
|
|
140
|
-
return send(requestId, { input }, { transfer })
|
|
209
|
+
l.debug(`Requesting ${functionName} with`, input);
|
|
210
|
+
return send(node, nodeId, requestId, { input }, { transfer })
|
|
141
211
|
.then(() => {})
|
|
142
|
-
.catch(reject)
|
|
143
|
-
})
|
|
144
|
-
}
|
|
212
|
+
.catch(reject);
|
|
213
|
+
});
|
|
214
|
+
};
|
|
145
215
|
|
|
146
216
|
// @ts-expect-error
|
|
147
|
-
instance[functionName] = _runProcedure
|
|
217
|
+
instance[functionName] = _runProcedure;
|
|
218
|
+
instance[functionName]!.broadcast = async (
|
|
219
|
+
input,
|
|
220
|
+
onProgress,
|
|
221
|
+
nodesCount,
|
|
222
|
+
) => {
|
|
223
|
+
let nodesToUse: Array<string | undefined> = [undefined];
|
|
224
|
+
if (nodes) nodesToUse = Object.keys(nodes);
|
|
225
|
+
if (nodesCount) nodesToUse = nodesToUse.slice(0, nodesCount);
|
|
226
|
+
|
|
227
|
+
const results = await Promise.allSettled(
|
|
228
|
+
nodesToUse.map(async (id) =>
|
|
229
|
+
_runProcedure(input, onProgress, undefined, id),
|
|
230
|
+
),
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
return results.map((r, i) => ({ ...r, node: nodesToUse[i] ?? "(SW)" }));
|
|
234
|
+
};
|
|
148
235
|
instance[functionName]!.cancelable = (input, onProgress) => {
|
|
149
|
-
const requestId = makeRequestId()
|
|
236
|
+
const requestId = makeRequestId();
|
|
237
|
+
const nodeId = whoToSendTo(nodes, pendingRequests);
|
|
238
|
+
|
|
239
|
+
const l = createLogger("client", loglevel, nodeId ?? "(SW)", requestId);
|
|
240
|
+
|
|
150
241
|
return {
|
|
151
|
-
request: _runProcedure(input, onProgress, requestId),
|
|
242
|
+
request: _runProcedure(input, onProgress, requestId, nodeId),
|
|
152
243
|
cancel(reason: string) {
|
|
153
244
|
if (!pendingRequests.has(requestId)) {
|
|
154
245
|
l.warn(
|
|
155
246
|
requestId,
|
|
156
|
-
`Cannot cancel ${functionName} request, it has already been resolved or rejected
|
|
157
|
-
)
|
|
158
|
-
return
|
|
247
|
+
`Cannot cancel ${functionName} request, it has already been resolved or rejected`,
|
|
248
|
+
);
|
|
249
|
+
return;
|
|
159
250
|
}
|
|
160
251
|
|
|
161
|
-
l.debug(requestId, `Cancelling ${functionName} with`, reason)
|
|
162
|
-
postMessageSync(l,
|
|
252
|
+
l.debug(requestId, `Cancelling ${functionName} with`, reason);
|
|
253
|
+
postMessageSync(l, nodeId ? nodes?.[nodeId] : undefined, {
|
|
163
254
|
by: "sw&rpc",
|
|
164
255
|
requestId,
|
|
165
256
|
functionName,
|
|
166
257
|
abort: { reason },
|
|
167
|
-
})
|
|
168
|
-
pendingRequests.delete(requestId)
|
|
258
|
+
});
|
|
259
|
+
pendingRequests.delete(requestId);
|
|
169
260
|
},
|
|
170
|
-
}
|
|
171
|
-
}
|
|
261
|
+
};
|
|
262
|
+
};
|
|
172
263
|
}
|
|
173
264
|
|
|
174
|
-
return instance as SwarpcClient<Procedures
|
|
265
|
+
return instance as SwarpcClient<Procedures>;
|
|
175
266
|
}
|
|
176
267
|
|
|
177
268
|
/**
|
|
@@ -179,16 +270,16 @@ export function Client<Procedures extends ProceduresMap>(
|
|
|
179
270
|
* @returns the worker to use
|
|
180
271
|
*/
|
|
181
272
|
async function postMessage<Procedures extends ProceduresMap>(
|
|
182
|
-
|
|
183
|
-
worker: Worker | SharedWorker | undefined,
|
|
184
|
-
hooks: Hooks<Procedures>,
|
|
273
|
+
ctx: Context<Procedures>,
|
|
185
274
|
message: Payload<Procedures>,
|
|
186
|
-
options?: StructuredSerializeOptions
|
|
275
|
+
options?: StructuredSerializeOptions,
|
|
187
276
|
) {
|
|
188
|
-
await startClientListener(
|
|
277
|
+
await startClientListener(ctx);
|
|
278
|
+
|
|
279
|
+
const { logger: l, node: worker } = ctx;
|
|
189
280
|
|
|
190
281
|
if (!worker && !navigator.serviceWorker.controller)
|
|
191
|
-
l.warn("", "Service Worker is not controlling the page")
|
|
282
|
+
l.warn("", "Service Worker is not controlling the page");
|
|
192
283
|
|
|
193
284
|
// If no worker is provided, we use the service worker
|
|
194
285
|
const w =
|
|
@@ -196,13 +287,13 @@ async function postMessage<Procedures extends ProceduresMap>(
|
|
|
196
287
|
? worker.port
|
|
197
288
|
: worker === undefined
|
|
198
289
|
? await navigator.serviceWorker.ready.then((r) => r.active)
|
|
199
|
-
: worker
|
|
290
|
+
: worker;
|
|
200
291
|
|
|
201
292
|
if (!w) {
|
|
202
|
-
throw new Error("[SWARPC Client] No active service worker found")
|
|
293
|
+
throw new Error("[SWARPC Client] No active service worker found");
|
|
203
294
|
}
|
|
204
295
|
|
|
205
|
-
w.postMessage(message, options)
|
|
296
|
+
w.postMessage(message, options);
|
|
206
297
|
}
|
|
207
298
|
|
|
208
299
|
/**
|
|
@@ -214,13 +305,13 @@ async function postMessage<Procedures extends ProceduresMap>(
|
|
|
214
305
|
* @param options
|
|
215
306
|
*/
|
|
216
307
|
export function postMessageSync<Procedures extends ProceduresMap>(
|
|
217
|
-
l:
|
|
308
|
+
l: RequestBoundLogger,
|
|
218
309
|
worker: Worker | SharedWorker | undefined,
|
|
219
310
|
message: Payload<Procedures>,
|
|
220
|
-
options?: StructuredSerializeOptions
|
|
311
|
+
options?: StructuredSerializeOptions,
|
|
221
312
|
): void {
|
|
222
313
|
if (!worker && !navigator.serviceWorker.controller)
|
|
223
|
-
l.warn("
|
|
314
|
+
l.warn("Service Worker is not controlling the page");
|
|
224
315
|
|
|
225
316
|
// If no worker is provided, we use the service worker
|
|
226
317
|
const w =
|
|
@@ -228,91 +319,105 @@ export function postMessageSync<Procedures extends ProceduresMap>(
|
|
|
228
319
|
? worker.port
|
|
229
320
|
: worker === undefined
|
|
230
321
|
? navigator.serviceWorker.controller
|
|
231
|
-
: worker
|
|
322
|
+
: worker;
|
|
232
323
|
|
|
233
324
|
if (!w) {
|
|
234
|
-
throw new Error("[SWARPC Client] No active service worker found")
|
|
325
|
+
throw new Error("[SWARPC Client] No active service worker found");
|
|
235
326
|
}
|
|
236
327
|
|
|
237
|
-
w.postMessage(message, options)
|
|
328
|
+
w.postMessage(message, options);
|
|
238
329
|
}
|
|
239
330
|
|
|
240
331
|
/**
|
|
241
332
|
* 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
|
|
333
|
+
* @param ctx.worker if provided, the client will use this worker to listen for messages, instead of using the service worker
|
|
244
334
|
* @returns
|
|
245
335
|
*/
|
|
246
336
|
export async function startClientListener<Procedures extends ProceduresMap>(
|
|
247
|
-
|
|
248
|
-
worker?: Worker | SharedWorker,
|
|
249
|
-
hooks: Hooks<Procedures> = {}
|
|
337
|
+
ctx: Context<Procedures>,
|
|
250
338
|
) {
|
|
251
|
-
if (_clientListenerStarted) return
|
|
339
|
+
if (_clientListenerStarted.has(ctx.nodeId ?? "(SW)")) return;
|
|
340
|
+
|
|
341
|
+
const { logger: l, node: worker } = ctx;
|
|
252
342
|
|
|
253
343
|
// Get service worker registration if no worker is provided
|
|
254
344
|
if (!worker) {
|
|
255
|
-
const sw = await navigator.serviceWorker.ready
|
|
345
|
+
const sw = await navigator.serviceWorker.ready;
|
|
256
346
|
if (!sw?.active) {
|
|
257
|
-
throw new Error("[SWARPC Client] Service Worker is not active")
|
|
347
|
+
throw new Error("[SWARPC Client] Service Worker is not active");
|
|
258
348
|
}
|
|
259
349
|
|
|
260
350
|
if (!navigator.serviceWorker.controller) {
|
|
261
|
-
l.warn("", "Service Worker is not controlling the page")
|
|
351
|
+
l.warn("", "Service Worker is not controlling the page");
|
|
262
352
|
}
|
|
263
353
|
}
|
|
264
354
|
|
|
265
|
-
const w = worker ?? navigator.serviceWorker
|
|
355
|
+
const w = worker ?? navigator.serviceWorker;
|
|
266
356
|
|
|
267
357
|
// Start listening for messages
|
|
268
|
-
l.debug(null, "Starting client listener", {
|
|
358
|
+
l.debug(null, "Starting client listener", { w, ...ctx });
|
|
269
359
|
const listener = (event: Event): void => {
|
|
270
360
|
// Get the data from the event
|
|
271
|
-
const eventData = (event as MessageEvent).data || {}
|
|
361
|
+
const eventData = (event as MessageEvent).data || {};
|
|
272
362
|
|
|
273
363
|
// Ignore other messages that aren't for us
|
|
274
|
-
if (eventData?.by !== "sw&rpc") return
|
|
364
|
+
if (eventData?.by !== "sw&rpc") return;
|
|
275
365
|
|
|
276
366
|
// We don't use a arktype schema here, we trust the server to send valid data
|
|
277
|
-
const
|
|
367
|
+
const payload = eventData as Payload<Procedures>;
|
|
368
|
+
|
|
369
|
+
// Ignore #initialize request, it's client->server only
|
|
370
|
+
if ("localStorageData" in payload) {
|
|
371
|
+
l.warn(null, "Ignoring unexpected #initialize from server", payload);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const { requestId, ...data } = payload;
|
|
278
376
|
|
|
279
377
|
// Sanity check in case we somehow receive a message without requestId
|
|
280
378
|
if (!requestId) {
|
|
281
|
-
throw new Error("[SWARPC Client] Message received without requestId")
|
|
379
|
+
throw new Error("[SWARPC Client] Message received without requestId");
|
|
282
380
|
}
|
|
283
381
|
|
|
284
382
|
// Get the associated pending request handlers
|
|
285
|
-
const handlers = pendingRequests.get(requestId)
|
|
383
|
+
const handlers = pendingRequests.get(requestId);
|
|
286
384
|
if (!handlers) {
|
|
287
385
|
throw new Error(
|
|
288
|
-
`[SWARPC Client] ${requestId} has no active request handlers, cannot process ${JSON.stringify(data)}
|
|
289
|
-
)
|
|
386
|
+
`[SWARPC Client] ${requestId} has no active request handlers, cannot process ${JSON.stringify(data)}`,
|
|
387
|
+
);
|
|
290
388
|
}
|
|
291
389
|
|
|
292
390
|
// React to the data received: call hook, call handler,
|
|
293
391
|
// and remove the request from pendingRequests (unless it's a progress update)
|
|
294
392
|
if ("error" in data) {
|
|
295
|
-
hooks.error?.(data.functionName, new Error(data.error.message))
|
|
296
|
-
handlers.reject(new Error(data.error.message))
|
|
297
|
-
pendingRequests.delete(requestId)
|
|
393
|
+
ctx.hooks.error?.(data.functionName, new Error(data.error.message));
|
|
394
|
+
handlers.reject(new Error(data.error.message));
|
|
395
|
+
pendingRequests.delete(requestId);
|
|
298
396
|
} else if ("progress" in data) {
|
|
299
|
-
hooks.progress?.(data.functionName, data.progress)
|
|
300
|
-
handlers.onProgress(data.progress)
|
|
397
|
+
ctx.hooks.progress?.(data.functionName, data.progress);
|
|
398
|
+
handlers.onProgress(data.progress);
|
|
301
399
|
} else if ("result" in data) {
|
|
302
|
-
hooks.success?.(data.functionName, data.result)
|
|
303
|
-
handlers.resolve(data.result)
|
|
304
|
-
pendingRequests.delete(requestId)
|
|
400
|
+
ctx.hooks.success?.(data.functionName, data.result);
|
|
401
|
+
handlers.resolve(data.result);
|
|
402
|
+
pendingRequests.delete(requestId);
|
|
305
403
|
}
|
|
306
|
-
}
|
|
404
|
+
};
|
|
307
405
|
|
|
308
406
|
if (w instanceof SharedWorker) {
|
|
309
|
-
w.port.addEventListener("message", listener)
|
|
310
|
-
w.port.start()
|
|
407
|
+
w.port.addEventListener("message", listener);
|
|
408
|
+
w.port.start();
|
|
311
409
|
} else {
|
|
312
|
-
w.addEventListener("message", listener)
|
|
410
|
+
w.addEventListener("message", listener);
|
|
313
411
|
}
|
|
314
412
|
|
|
315
|
-
_clientListenerStarted
|
|
413
|
+
_clientListenerStarted.add(ctx.nodeId ?? "(SW)");
|
|
414
|
+
|
|
415
|
+
// Recursive terminal case is ensured by calling this *after* _clientListenerStarted is set to true: startClientListener() will therefore not be called in postMessage() again.
|
|
416
|
+
await postMessage(ctx, {
|
|
417
|
+
by: "sw&rpc",
|
|
418
|
+
functionName: "#initialize",
|
|
419
|
+
localStorageData: ctx.localStorage,
|
|
420
|
+
});
|
|
316
421
|
}
|
|
317
422
|
|
|
318
423
|
/**
|
|
@@ -321,5 +426,5 @@ export async function startClientListener<Procedures extends ProceduresMap>(
|
|
|
321
426
|
* @returns a 6-character hexadecimal string
|
|
322
427
|
*/
|
|
323
428
|
export function makeRequestId(): string {
|
|
324
|
-
return Math.random().toString(16).substring(2, 8).toUpperCase()
|
|
429
|
+
return Math.random().toString(16).substring(2, 8).toUpperCase();
|
|
325
430
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module
|
|
3
|
-
* @mergeModuleWith <project>
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export * from "./
|
|
8
|
-
export
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* @mergeModuleWith <project>
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import "./polyfills.js";
|
|
7
|
+
export * from "./client.js";
|
|
8
|
+
export * from "./server.js";
|
|
9
|
+
export type { ProceduresMap, CancelablePromise } from "./types.js";
|
|
@@ -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
|
+
}
|