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/dist/client.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* @module
|
|
3
3
|
* @mergeModuleWith <project>
|
|
4
4
|
*/
|
|
5
|
-
import { createLogger } from "./log.js";
|
|
5
|
+
import { createLogger, } from "./log.js";
|
|
6
|
+
import { makeNodeId, whoToSendTo } from "./nodes.js";
|
|
6
7
|
import { zProcedures, } from "./types.js";
|
|
7
8
|
import { findTransferables } from "./utils.js";
|
|
8
9
|
/**
|
|
@@ -12,35 +13,59 @@ import { findTransferables } from "./utils.js";
|
|
|
12
13
|
*/
|
|
13
14
|
const pendingRequests = new Map();
|
|
14
15
|
// Have we started the client listener?
|
|
15
|
-
let _clientListenerStarted =
|
|
16
|
+
let _clientListenerStarted = new Set();
|
|
16
17
|
/**
|
|
17
18
|
*
|
|
18
19
|
* @param procedures procedures the client will be able to call, see {@link ProceduresMap}
|
|
19
20
|
* @param options various options
|
|
20
|
-
* @param options.worker The instantiated
|
|
21
|
-
* Example: `
|
|
21
|
+
* @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`.
|
|
22
|
+
* Example: `"./worker.js"`
|
|
22
23
|
* See {@link Worker} (used by both dedicated workers and service workers), {@link SharedWorker}, and
|
|
23
24
|
* the different [worker types](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API#worker_types) that exist
|
|
24
25
|
* @param options.hooks Hooks to run on messages received from the server. See {@link Hooks}
|
|
25
26
|
* @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
27
|
* @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.
|
|
28
|
+
* @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)
|
|
29
|
+
* @param options.nodes the number of workers to use for the server, defaults to {@link navigator.hardwareConcurrency}.
|
|
27
30
|
* @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
31
|
*
|
|
29
32
|
* An example of defining and using a client:
|
|
30
33
|
* {@includeCode ../example/src/routes/+page.svelte}
|
|
31
34
|
*/
|
|
32
|
-
export function Client(procedures, { worker, loglevel = "debug", restartListener = false, hooks = {}, } = {}) {
|
|
35
|
+
export function Client(procedures, { worker, nodes: nodeCount, loglevel = "debug", restartListener = false, hooks = {}, localStorage = {}, } = {}) {
|
|
33
36
|
const l = createLogger("client", loglevel);
|
|
34
37
|
if (restartListener)
|
|
35
|
-
_clientListenerStarted
|
|
38
|
+
_clientListenerStarted.clear();
|
|
36
39
|
// Store procedures on a symbol key, to avoid conflicts with procedure names
|
|
37
40
|
const instance = { [zProcedures]: procedures };
|
|
41
|
+
nodeCount ??= navigator.hardwareConcurrency || 1;
|
|
42
|
+
let nodes;
|
|
43
|
+
if (worker) {
|
|
44
|
+
nodes = {};
|
|
45
|
+
for (const _ of Array.from({ length: nodeCount })) {
|
|
46
|
+
const id = makeNodeId();
|
|
47
|
+
if (typeof worker === "string") {
|
|
48
|
+
nodes[id] = new Worker(worker, { name: id });
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
nodes[id] = new worker({ name: id });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
l.info(null, `Started ${nodeCount} node${nodeCount > 1 ? "s" : ""}`, Object.keys(nodes));
|
|
55
|
+
}
|
|
38
56
|
for (const functionName of Object.keys(procedures)) {
|
|
39
57
|
if (typeof functionName !== "string") {
|
|
40
58
|
throw new Error(`[SWARPC Client] Invalid function name, don't use symbols`);
|
|
41
59
|
}
|
|
42
|
-
const send = async (requestId, msg, options) => {
|
|
43
|
-
|
|
60
|
+
const send = async (node, nodeId, requestId, msg, options) => {
|
|
61
|
+
const ctx = {
|
|
62
|
+
logger: l,
|
|
63
|
+
node,
|
|
64
|
+
nodeId,
|
|
65
|
+
hooks,
|
|
66
|
+
localStorage,
|
|
67
|
+
};
|
|
68
|
+
return postMessage(ctx, {
|
|
44
69
|
...msg,
|
|
45
70
|
by: "sw&rpc",
|
|
46
71
|
requestId,
|
|
@@ -48,15 +73,20 @@ export function Client(procedures, { worker, loglevel = "debug", restartListener
|
|
|
48
73
|
}, options);
|
|
49
74
|
};
|
|
50
75
|
// Set the method on the instance
|
|
51
|
-
const _runProcedure = async (input, onProgress = () => { }, reqid) => {
|
|
76
|
+
const _runProcedure = async (input, onProgress = () => { }, reqid, nodeId) => {
|
|
52
77
|
// Validate the input against the procedure's input schema
|
|
53
78
|
procedures[functionName].input.assert(input);
|
|
54
79
|
const requestId = reqid ?? makeRequestId();
|
|
80
|
+
// Choose which node to use
|
|
81
|
+
nodeId ??= whoToSendTo(nodes, pendingRequests);
|
|
82
|
+
const node = nodes && nodeId ? nodes[nodeId] : undefined;
|
|
83
|
+
const l = createLogger("client", loglevel, nodeId ?? "(SW)", requestId);
|
|
55
84
|
return new Promise((resolve, reject) => {
|
|
56
85
|
// Store promise handlers (as well as progress updates handler)
|
|
57
86
|
// so the client listener can resolve/reject the promise (and react to progress updates)
|
|
58
87
|
// when the server sends messages back
|
|
59
88
|
pendingRequests.set(requestId, {
|
|
89
|
+
nodeId,
|
|
60
90
|
functionName,
|
|
61
91
|
resolve,
|
|
62
92
|
onProgress,
|
|
@@ -66,25 +96,36 @@ export function Client(procedures, { worker, loglevel = "debug", restartListener
|
|
|
66
96
|
? findTransferables(input)
|
|
67
97
|
: [];
|
|
68
98
|
// Post the message to the server
|
|
69
|
-
l.debug(
|
|
70
|
-
return send(requestId, { input }, { transfer })
|
|
99
|
+
l.debug(`Requesting ${functionName} with`, input);
|
|
100
|
+
return send(node, nodeId, requestId, { input }, { transfer })
|
|
71
101
|
.then(() => { })
|
|
72
102
|
.catch(reject);
|
|
73
103
|
});
|
|
74
104
|
};
|
|
75
105
|
// @ts-expect-error
|
|
76
106
|
instance[functionName] = _runProcedure;
|
|
107
|
+
instance[functionName].broadcast = async (input, onProgress, nodesCount) => {
|
|
108
|
+
let nodesToUse = [undefined];
|
|
109
|
+
if (nodes)
|
|
110
|
+
nodesToUse = Object.keys(nodes);
|
|
111
|
+
if (nodesCount)
|
|
112
|
+
nodesToUse = nodesToUse.slice(0, nodesCount);
|
|
113
|
+
const results = await Promise.allSettled(nodesToUse.map(async (id) => _runProcedure(input, onProgress, undefined, id)));
|
|
114
|
+
return results.map((r, i) => ({ ...r, node: nodesToUse[i] ?? "(SW)" }));
|
|
115
|
+
};
|
|
77
116
|
instance[functionName].cancelable = (input, onProgress) => {
|
|
78
117
|
const requestId = makeRequestId();
|
|
118
|
+
const nodeId = whoToSendTo(nodes, pendingRequests);
|
|
119
|
+
const l = createLogger("client", loglevel, nodeId ?? "(SW)", requestId);
|
|
79
120
|
return {
|
|
80
|
-
request: _runProcedure(input, onProgress, requestId),
|
|
121
|
+
request: _runProcedure(input, onProgress, requestId, nodeId),
|
|
81
122
|
cancel(reason) {
|
|
82
123
|
if (!pendingRequests.has(requestId)) {
|
|
83
124
|
l.warn(requestId, `Cannot cancel ${functionName} request, it has already been resolved or rejected`);
|
|
84
125
|
return;
|
|
85
126
|
}
|
|
86
127
|
l.debug(requestId, `Cancelling ${functionName} with`, reason);
|
|
87
|
-
postMessageSync(l,
|
|
128
|
+
postMessageSync(l, nodeId ? nodes?.[nodeId] : undefined, {
|
|
88
129
|
by: "sw&rpc",
|
|
89
130
|
requestId,
|
|
90
131
|
functionName,
|
|
@@ -101,8 +142,9 @@ export function Client(procedures, { worker, loglevel = "debug", restartListener
|
|
|
101
142
|
* Warms up the client by starting the listener and getting the worker, then posts a message to the worker.
|
|
102
143
|
* @returns the worker to use
|
|
103
144
|
*/
|
|
104
|
-
async function postMessage(
|
|
105
|
-
await startClientListener(
|
|
145
|
+
async function postMessage(ctx, message, options) {
|
|
146
|
+
await startClientListener(ctx);
|
|
147
|
+
const { logger: l, node: worker } = ctx;
|
|
106
148
|
if (!worker && !navigator.serviceWorker.controller)
|
|
107
149
|
l.warn("", "Service Worker is not controlling the page");
|
|
108
150
|
// If no worker is provided, we use the service worker
|
|
@@ -126,7 +168,7 @@ async function postMessage(l, worker, hooks, message, options) {
|
|
|
126
168
|
*/
|
|
127
169
|
export function postMessageSync(l, worker, message, options) {
|
|
128
170
|
if (!worker && !navigator.serviceWorker.controller)
|
|
129
|
-
l.warn("
|
|
171
|
+
l.warn("Service Worker is not controlling the page");
|
|
130
172
|
// If no worker is provided, we use the service worker
|
|
131
173
|
const w = worker instanceof SharedWorker
|
|
132
174
|
? worker.port
|
|
@@ -140,13 +182,13 @@ export function postMessageSync(l, worker, message, options) {
|
|
|
140
182
|
}
|
|
141
183
|
/**
|
|
142
184
|
* 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
|
|
185
|
+
* @param ctx.worker if provided, the client will use this worker to listen for messages, instead of using the service worker
|
|
145
186
|
* @returns
|
|
146
187
|
*/
|
|
147
|
-
export async function startClientListener(
|
|
148
|
-
if (_clientListenerStarted)
|
|
188
|
+
export async function startClientListener(ctx) {
|
|
189
|
+
if (_clientListenerStarted.has(ctx.nodeId ?? "(SW)"))
|
|
149
190
|
return;
|
|
191
|
+
const { logger: l, node: worker } = ctx;
|
|
150
192
|
// Get service worker registration if no worker is provided
|
|
151
193
|
if (!worker) {
|
|
152
194
|
const sw = await navigator.serviceWorker.ready;
|
|
@@ -159,7 +201,7 @@ export async function startClientListener(l, worker, hooks = {}) {
|
|
|
159
201
|
}
|
|
160
202
|
const w = worker ?? navigator.serviceWorker;
|
|
161
203
|
// Start listening for messages
|
|
162
|
-
l.debug(null, "Starting client listener", {
|
|
204
|
+
l.debug(null, "Starting client listener", { w, ...ctx });
|
|
163
205
|
const listener = (event) => {
|
|
164
206
|
// Get the data from the event
|
|
165
207
|
const eventData = event.data || {};
|
|
@@ -167,7 +209,13 @@ export async function startClientListener(l, worker, hooks = {}) {
|
|
|
167
209
|
if (eventData?.by !== "sw&rpc")
|
|
168
210
|
return;
|
|
169
211
|
// We don't use a arktype schema here, we trust the server to send valid data
|
|
170
|
-
const
|
|
212
|
+
const payload = eventData;
|
|
213
|
+
// Ignore #initialize request, it's client->server only
|
|
214
|
+
if ("localStorageData" in payload) {
|
|
215
|
+
l.warn(null, "Ignoring unexpected #initialize from server", payload);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const { requestId, ...data } = payload;
|
|
171
219
|
// Sanity check in case we somehow receive a message without requestId
|
|
172
220
|
if (!requestId) {
|
|
173
221
|
throw new Error("[SWARPC Client] Message received without requestId");
|
|
@@ -180,16 +228,16 @@ export async function startClientListener(l, worker, hooks = {}) {
|
|
|
180
228
|
// React to the data received: call hook, call handler,
|
|
181
229
|
// and remove the request from pendingRequests (unless it's a progress update)
|
|
182
230
|
if ("error" in data) {
|
|
183
|
-
hooks.error?.(data.functionName, new Error(data.error.message));
|
|
231
|
+
ctx.hooks.error?.(data.functionName, new Error(data.error.message));
|
|
184
232
|
handlers.reject(new Error(data.error.message));
|
|
185
233
|
pendingRequests.delete(requestId);
|
|
186
234
|
}
|
|
187
235
|
else if ("progress" in data) {
|
|
188
|
-
hooks.progress?.(data.functionName, data.progress);
|
|
236
|
+
ctx.hooks.progress?.(data.functionName, data.progress);
|
|
189
237
|
handlers.onProgress(data.progress);
|
|
190
238
|
}
|
|
191
239
|
else if ("result" in data) {
|
|
192
|
-
hooks.success?.(data.functionName, data.result);
|
|
240
|
+
ctx.hooks.success?.(data.functionName, data.result);
|
|
193
241
|
handlers.resolve(data.result);
|
|
194
242
|
pendingRequests.delete(requestId);
|
|
195
243
|
}
|
|
@@ -201,7 +249,13 @@ export async function startClientListener(l, worker, hooks = {}) {
|
|
|
201
249
|
else {
|
|
202
250
|
w.addEventListener("message", listener);
|
|
203
251
|
}
|
|
204
|
-
_clientListenerStarted
|
|
252
|
+
_clientListenerStarted.add(ctx.nodeId ?? "(SW)");
|
|
253
|
+
// Recursive terminal case is ensured by calling this *after* _clientListenerStarted is set to true: startClientListener() will therefore not be called in postMessage() again.
|
|
254
|
+
await postMessage(ctx, {
|
|
255
|
+
by: "sw&rpc",
|
|
256
|
+
functionName: "#initialize",
|
|
257
|
+
localStorageData: ctx.localStorage,
|
|
258
|
+
});
|
|
205
259
|
}
|
|
206
260
|
/**
|
|
207
261
|
* Generate a random request ID, used to identify requests between client and server.
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,cAAc,aAAa,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,gBAAgB,CAAC;AACxB,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,YAAY,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -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,CAAC;IAC1B,SAAS,EAAE,MAAM,EAAE,CAAC;gBAER,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/log.d.ts
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
/**
|
|
6
6
|
* @ignore
|
|
7
7
|
*/
|
|
8
|
-
export declare function createLogger(side: "server" | "client", level: LogLevel): Logger;
|
|
9
|
-
export declare function createLogger(side: "server" | "client", level: LogLevel, rqid: string): RequestBoundLogger;
|
|
8
|
+
export declare function createLogger(side: "server" | "client", level: LogLevel, nid?: string): Logger;
|
|
9
|
+
export declare function createLogger(side: "server" | "client", level: LogLevel, nid: string, rqid: string): RequestBoundLogger;
|
|
10
10
|
/**
|
|
11
11
|
* @ignore
|
|
12
12
|
*/
|
|
@@ -23,6 +23,7 @@ export type RequestBoundLogger = {
|
|
|
23
23
|
error: (message: string, ...args: any[]) => void;
|
|
24
24
|
};
|
|
25
25
|
/** @source */
|
|
26
|
-
|
|
26
|
+
declare const LOG_LEVELS: readonly ["debug", "info", "warn", "error"];
|
|
27
27
|
export type LogLevel = (typeof LOG_LEVELS)[number];
|
|
28
|
+
export {};
|
|
28
29
|
//# sourceMappingURL=log.d.ts.map
|
package/dist/log.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;GAEG;AACH,wBAAgB,YAAY,
|
|
1
|
+
{"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;GAEG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,QAAQ,GAAG,QAAQ,EACzB,KAAK,EAAE,QAAQ,EACf,GAAG,CAAC,EAAE,MAAM,GACX,MAAM,CAAC;AACV,wBAAgB,YAAY,CAC1B,IAAI,EAAE,QAAQ,GAAG,QAAQ,EACzB,KAAK,EAAE,QAAQ,EACf,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,GACX,kBAAkB,CAAC;AA2BtB;;GAEG;AACH,MAAM,MAAM,MAAM,GAAG;IACnB,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAC;IACtE,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAC;IACrE,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAC;IACrE,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAC;CACvE,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAC;IACjD,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAC;IAChD,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAC;IAChD,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAC;CAClD,CAAC;AAEF,cAAc;AACd,QAAA,MAAM,UAAU,6CAA8C,CAAC;AAE/D,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC"}
|
package/dist/log.js
CHANGED
|
@@ -2,44 +2,54 @@
|
|
|
2
2
|
* @module
|
|
3
3
|
* @mergeModuleWith <project>
|
|
4
4
|
*/
|
|
5
|
-
export function createLogger(side, level = "debug", rqid) {
|
|
5
|
+
export function createLogger(side, level = "debug", nid, rqid) {
|
|
6
6
|
const lvls = LOG_LEVELS.slice(LOG_LEVELS.indexOf(level));
|
|
7
|
-
if (rqid) {
|
|
7
|
+
if (rqid && nid) {
|
|
8
|
+
const ids = { rqid, nid };
|
|
8
9
|
return {
|
|
9
|
-
debug: lvls.includes("debug") ? logger("debug", side,
|
|
10
|
-
info: lvls.includes("info") ? logger("info", side,
|
|
11
|
-
warn: lvls.includes("warn") ? logger("warn", side,
|
|
12
|
-
error: lvls.includes("error") ? logger("error", side,
|
|
10
|
+
debug: lvls.includes("debug") ? logger("debug", side, ids) : () => { },
|
|
11
|
+
info: lvls.includes("info") ? logger("info", side, ids) : () => { },
|
|
12
|
+
warn: lvls.includes("warn") ? logger("warn", side, ids) : () => { },
|
|
13
|
+
error: lvls.includes("error") ? logger("error", side, ids) : () => { },
|
|
13
14
|
};
|
|
14
15
|
}
|
|
15
16
|
return {
|
|
16
|
-
debug: lvls.includes("debug") ? logger("debug", side) : () => { },
|
|
17
|
-
info: lvls.includes("info") ? logger("info", side) : () => { },
|
|
18
|
-
warn: lvls.includes("warn") ? logger("warn", side) : () => { },
|
|
19
|
-
error: lvls.includes("error") ? logger("error", side) : () => { },
|
|
17
|
+
debug: lvls.includes("debug") ? logger("debug", side, nid) : () => { },
|
|
18
|
+
info: lvls.includes("info") ? logger("info", side, nid) : () => { },
|
|
19
|
+
warn: lvls.includes("warn") ? logger("warn", side, nid) : () => { },
|
|
20
|
+
error: lvls.includes("error") ? logger("error", side, nid) : () => { },
|
|
20
21
|
};
|
|
21
22
|
}
|
|
22
23
|
/** @source */
|
|
23
|
-
|
|
24
|
-
function logger(severity, side,
|
|
25
|
-
if (
|
|
26
|
-
|
|
24
|
+
const LOG_LEVELS = ["debug", "info", "warn", "error"];
|
|
25
|
+
function logger(severity, side, ids) {
|
|
26
|
+
if (ids === undefined || typeof ids === "string") {
|
|
27
|
+
const nid = ids ?? null;
|
|
28
|
+
return (rqid, message, ...args) => log(severity, side, { nid, rqid }, message, ...args);
|
|
27
29
|
}
|
|
28
|
-
return (message, ...args) => log(severity, side,
|
|
30
|
+
return (message, ...args) => log(severity, side, ids, message, ...args);
|
|
29
31
|
}
|
|
30
32
|
/**
|
|
31
33
|
* Send log messages to the console, with a helpful prefix.
|
|
32
34
|
* @param severity
|
|
33
35
|
* @param side
|
|
34
|
-
* @param
|
|
36
|
+
* @param ids request ID and node ID
|
|
35
37
|
* @param message
|
|
36
38
|
* @param args passed to console methods directly
|
|
37
39
|
*/
|
|
38
|
-
function log(severity, side, rqid, message, ...args) {
|
|
39
|
-
const prefix =
|
|
40
|
-
[
|
|
41
|
-
"
|
|
42
|
-
|
|
40
|
+
function log(severity, side, { rqid, nid }, message, ...args) {
|
|
41
|
+
const prefix = [
|
|
42
|
+
`[SWARPC ${side}]`,
|
|
43
|
+
rqid ? `%c${rqid}%c` : "",
|
|
44
|
+
nid ? `%c@ ${nid}%c` : "",
|
|
45
|
+
]
|
|
46
|
+
.filter(Boolean)
|
|
47
|
+
.join(" ");
|
|
48
|
+
const prefixStyles = [];
|
|
49
|
+
if (rqid)
|
|
50
|
+
prefixStyles.push("color: cyan", "color: inherit");
|
|
51
|
+
if (nid)
|
|
52
|
+
prefixStyles.push("color: hotpink", "color: inherit");
|
|
43
53
|
if (severity === "debug") {
|
|
44
54
|
console.debug(prefix, ...prefixStyles, message, ...args);
|
|
45
55
|
}
|
package/dist/nodes.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { PendingRequest } from "./client.js";
|
|
2
|
+
/**
|
|
3
|
+
* Returns to which node to send the next request, given the state of the currently pending requests
|
|
4
|
+
*/
|
|
5
|
+
export declare function whoToSendTo(nodes: undefined | Record<string, unknown>, requests: Map<string, PendingRequest>): undefined | string;
|
|
6
|
+
export declare function nodeIdFromScope(scope: WorkerGlobalScope, _scopeType?: "dedicated" | "shared" | "service"): string;
|
|
7
|
+
/**
|
|
8
|
+
* Generate a random request ID, used to identify nodes in the client
|
|
9
|
+
* @source
|
|
10
|
+
*/
|
|
11
|
+
export declare function makeNodeId(): string;
|
|
12
|
+
//# sourceMappingURL=nodes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nodes.d.ts","sourceRoot":"","sources":["../src/nodes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAG7C;;GAEG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC1C,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,GACpC,SAAS,GAAG,MAAM,CA0BpB;AAED,wBAAgB,eAAe,CAC7B,KAAK,EAAE,iBAAiB,EACxB,UAAU,CAAC,EAAE,WAAW,GAAG,QAAQ,GAAG,SAAS,GAC9C,MAAM,CAMR;AAED;;;GAGG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAEnC"}
|
package/dist/nodes.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { scopeIsDedicated, scopeIsShared } from "./scopes.js";
|
|
2
|
+
/**
|
|
3
|
+
* Returns to which node to send the next request, given the state of the currently pending requests
|
|
4
|
+
*/
|
|
5
|
+
export function whoToSendTo(nodes, requests) {
|
|
6
|
+
if (!nodes)
|
|
7
|
+
return undefined;
|
|
8
|
+
let chosen = Object.keys(nodes)[0];
|
|
9
|
+
const requestsPerNode = Map.groupBy(requests.values(), (req) => req.nodeId);
|
|
10
|
+
for (const node of Object.keys(nodes)) {
|
|
11
|
+
if (!requestsPerNode.has(node))
|
|
12
|
+
requestsPerNode.set(node, []);
|
|
13
|
+
}
|
|
14
|
+
for (const [node, reqs] of requestsPerNode.entries()) {
|
|
15
|
+
if (!node)
|
|
16
|
+
continue;
|
|
17
|
+
// Send to the least busy node
|
|
18
|
+
if (reqs.length < requestsPerNode.get(chosen).length)
|
|
19
|
+
chosen = node;
|
|
20
|
+
}
|
|
21
|
+
console.debug("[SWARPC Load balancer] Choosing", chosen, "load map is", requestsPerNode);
|
|
22
|
+
return chosen;
|
|
23
|
+
}
|
|
24
|
+
export function nodeIdFromScope(scope, _scopeType) {
|
|
25
|
+
if (scopeIsDedicated(scope, _scopeType) || scopeIsShared(scope, _scopeType)) {
|
|
26
|
+
return scope.name;
|
|
27
|
+
}
|
|
28
|
+
return "(SW)";
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Generate a random request ID, used to identify nodes in the client
|
|
32
|
+
* @source
|
|
33
|
+
*/
|
|
34
|
+
export function makeNodeId() {
|
|
35
|
+
return "N" + Math.random().toString(16).substring(2, 5).toUpperCase();
|
|
36
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"polyfills.d.ts","sourceRoot":"","sources":["../src/polyfills.ts"],"names":[],"mappings":"AAqBA,OAAO,EAAE,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Groups elements from an iterable into a Map based on a callback function.
|
|
3
|
+
*
|
|
4
|
+
* @template K, T
|
|
5
|
+
* @param {Iterable<T>} iterable - The iterable to group.
|
|
6
|
+
* @param {function(T, number): K} callbackfn - The callback function to
|
|
7
|
+
* determine the grouping key.
|
|
8
|
+
* @returns {Map<K, T[]>} A Map where keys are the grouping keys and values are
|
|
9
|
+
* arrays of grouped elements.
|
|
10
|
+
*/
|
|
11
|
+
Map.groupBy ??= function groupBy(iterable, callbackfn) {
|
|
12
|
+
const map = new Map();
|
|
13
|
+
let i = 0;
|
|
14
|
+
for (const value of iterable) {
|
|
15
|
+
const key = callbackfn(value, i++), list = map.get(key);
|
|
16
|
+
list ? list.push(value) : map.set(key, [value]);
|
|
17
|
+
}
|
|
18
|
+
return map;
|
|
19
|
+
};
|
|
20
|
+
export {};
|
package/dist/scopes.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function scopeIsShared(scope: WorkerGlobalScope, _scopeType?: "dedicated" | "shared" | "service"): scope is SharedWorkerGlobalScope;
|
|
2
|
+
export declare function scopeIsDedicated(scope: WorkerGlobalScope, _scopeType?: "dedicated" | "shared" | "service"): scope is DedicatedWorkerGlobalScope;
|
|
3
|
+
export declare function scopeIsService(scope: WorkerGlobalScope, _scopeType?: "dedicated" | "shared" | "service"): scope is ServiceWorkerGlobalScope;
|
|
4
|
+
//# sourceMappingURL=scopes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scopes.d.ts","sourceRoot":"","sources":["../src/scopes.ts"],"names":[],"mappings":"AAaA,wBAAgB,aAAa,CAC3B,KAAK,EAAE,iBAAiB,EACxB,UAAU,CAAC,EAAE,WAAW,GAAG,QAAQ,GAAG,SAAS,GAC9C,KAAK,IAAI,uBAAuB,CAElC;AAED,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,iBAAiB,EACxB,UAAU,CAAC,EAAE,WAAW,GAAG,QAAQ,GAAG,SAAS,GAC9C,KAAK,IAAI,0BAA0B,CAIrC;AAED,wBAAgB,cAAc,CAC5B,KAAK,EAAE,iBAAiB,EACxB,UAAU,CAAC,EAAE,WAAW,GAAG,QAAQ,GAAG,SAAS,GAC9C,KAAK,IAAI,wBAAwB,CAEnC"}
|
package/dist/scopes.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class MockedWorkerGlobalScope {
|
|
2
|
+
constructor() { }
|
|
3
|
+
}
|
|
4
|
+
const SharedWorkerGlobalScope = globalThis.SharedWorkerGlobalScope ?? MockedWorkerGlobalScope;
|
|
5
|
+
const DedicatedWorkerGlobalScope = globalThis.DedicatedWorkerGlobalScope ?? MockedWorkerGlobalScope;
|
|
6
|
+
const ServiceWorkerGlobalScope = globalThis.ServiceWorkerGlobalScope ?? MockedWorkerGlobalScope;
|
|
7
|
+
export function scopeIsShared(scope, _scopeType) {
|
|
8
|
+
return scope instanceof SharedWorkerGlobalScope || _scopeType === "shared";
|
|
9
|
+
}
|
|
10
|
+
export function scopeIsDedicated(scope, _scopeType) {
|
|
11
|
+
return (scope instanceof DedicatedWorkerGlobalScope || _scopeType === "dedicated");
|
|
12
|
+
}
|
|
13
|
+
export function scopeIsService(scope, _scopeType) {
|
|
14
|
+
return scope instanceof ServiceWorkerGlobalScope || _scopeType === "service";
|
|
15
|
+
}
|
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,
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,EAAgB,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAC;AACvD,OAAO,EACL,kBAAkB,EAMlB,uBAAuB,EACvB,gBAAgB,EAChB,WAAW,EACX,KAAK,aAAa,EACnB,MAAM,YAAY,CAAC;AAMpB;;;GAGG;AACH,MAAM,MAAM,YAAY,CAAC,UAAU,SAAS,aAAa,IAAI;IAC3D,CAAC,WAAW,CAAC,EAAE,UAAU,CAAC;IAC1B,CAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,UAAU,CAAC,CAAC;IACnD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB,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,CAAC;AAKF;;;;;;;;;;;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,CAAC;IAC1B,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,UAAU,CAAC,EAAE,WAAW,GAAG,QAAQ,GAAG,SAAS,CAAC;CAC5C,GACL,YAAY,CAAC,UAAU,CAAC,CA2M1B"}
|