swarpc 0.11.0 → 0.13.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 +246 -171
- package/dist/client.d.ts +22 -9
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +55 -19
- 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.map +1 -1
- package/dist/log.d.ts +9 -3
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +52 -34
- 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.map +1 -1
- package/dist/server.js +20 -29
- package/dist/types.d.ts +41 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -0
- package/package.json +24 -9
- package/src/client.ts +180 -109
- package/src/index.ts +9 -8
- package/src/localstorage.ts +46 -46
- package/src/log.ts +155 -118
- package/src/nodes.ts +55 -0
- package/src/polyfills.ts +22 -0
- package/src/scopes.ts +35 -0
- package/src/server.ts +275 -299
- package/src/types.ts +264 -239
- 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,38 +13,55 @@ 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.
|
|
27
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}.
|
|
28
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}
|
|
29
31
|
*
|
|
30
32
|
* An example of defining and using a client:
|
|
31
33
|
* {@includeCode ../example/src/routes/+page.svelte}
|
|
32
34
|
*/
|
|
33
|
-
export function Client(procedures, { worker, loglevel = "debug", restartListener = false, hooks = {}, localStorage = {}, } = {}) {
|
|
35
|
+
export function Client(procedures, { worker, nodes: nodeCount, loglevel = "debug", restartListener = false, hooks = {}, localStorage = {}, } = {}) {
|
|
34
36
|
const l = createLogger("client", loglevel);
|
|
35
37
|
if (restartListener)
|
|
36
|
-
_clientListenerStarted
|
|
38
|
+
_clientListenerStarted.clear();
|
|
37
39
|
// Store procedures on a symbol key, to avoid conflicts with procedure names
|
|
38
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
|
+
}
|
|
39
56
|
for (const functionName of Object.keys(procedures)) {
|
|
40
57
|
if (typeof functionName !== "string") {
|
|
41
58
|
throw new Error(`[SWARPC Client] Invalid function name, don't use symbols`);
|
|
42
59
|
}
|
|
43
|
-
const send = async (requestId, msg, options) => {
|
|
60
|
+
const send = async (node, nodeId, requestId, msg, options) => {
|
|
44
61
|
const ctx = {
|
|
45
62
|
logger: l,
|
|
46
|
-
|
|
63
|
+
node,
|
|
64
|
+
nodeId,
|
|
47
65
|
hooks,
|
|
48
66
|
localStorage,
|
|
49
67
|
};
|
|
@@ -55,15 +73,20 @@ export function Client(procedures, { worker, loglevel = "debug", restartListener
|
|
|
55
73
|
}, options);
|
|
56
74
|
};
|
|
57
75
|
// Set the method on the instance
|
|
58
|
-
const _runProcedure = async (input, onProgress = () => { }, reqid) => {
|
|
76
|
+
const _runProcedure = async (input, onProgress = () => { }, reqid, nodeId) => {
|
|
59
77
|
// Validate the input against the procedure's input schema
|
|
60
78
|
procedures[functionName].input.assert(input);
|
|
61
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);
|
|
62
84
|
return new Promise((resolve, reject) => {
|
|
63
85
|
// Store promise handlers (as well as progress updates handler)
|
|
64
86
|
// so the client listener can resolve/reject the promise (and react to progress updates)
|
|
65
87
|
// when the server sends messages back
|
|
66
88
|
pendingRequests.set(requestId, {
|
|
89
|
+
nodeId,
|
|
67
90
|
functionName,
|
|
68
91
|
resolve,
|
|
69
92
|
onProgress,
|
|
@@ -73,25 +96,36 @@ export function Client(procedures, { worker, loglevel = "debug", restartListener
|
|
|
73
96
|
? findTransferables(input)
|
|
74
97
|
: [];
|
|
75
98
|
// Post the message to the server
|
|
76
|
-
l.debug(
|
|
77
|
-
return send(requestId, { input }, { transfer })
|
|
99
|
+
l.debug(`Requesting ${functionName} with`, input);
|
|
100
|
+
return send(node, nodeId, requestId, { input }, { transfer })
|
|
78
101
|
.then(() => { })
|
|
79
102
|
.catch(reject);
|
|
80
103
|
});
|
|
81
104
|
};
|
|
82
105
|
// @ts-expect-error
|
|
83
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
|
+
};
|
|
84
116
|
instance[functionName].cancelable = (input, onProgress) => {
|
|
85
117
|
const requestId = makeRequestId();
|
|
118
|
+
const nodeId = whoToSendTo(nodes, pendingRequests);
|
|
119
|
+
const l = createLogger("client", loglevel, nodeId ?? "(SW)", requestId);
|
|
86
120
|
return {
|
|
87
|
-
request: _runProcedure(input, onProgress, requestId),
|
|
121
|
+
request: _runProcedure(input, onProgress, requestId, nodeId),
|
|
88
122
|
cancel(reason) {
|
|
89
123
|
if (!pendingRequests.has(requestId)) {
|
|
90
124
|
l.warn(requestId, `Cannot cancel ${functionName} request, it has already been resolved or rejected`);
|
|
91
125
|
return;
|
|
92
126
|
}
|
|
93
127
|
l.debug(requestId, `Cancelling ${functionName} with`, reason);
|
|
94
|
-
postMessageSync(l,
|
|
128
|
+
postMessageSync(l, nodeId ? nodes?.[nodeId] : undefined, {
|
|
95
129
|
by: "sw&rpc",
|
|
96
130
|
requestId,
|
|
97
131
|
functionName,
|
|
@@ -110,7 +144,7 @@ export function Client(procedures, { worker, loglevel = "debug", restartListener
|
|
|
110
144
|
*/
|
|
111
145
|
async function postMessage(ctx, message, options) {
|
|
112
146
|
await startClientListener(ctx);
|
|
113
|
-
const { logger: l, worker } = ctx;
|
|
147
|
+
const { logger: l, node: worker } = ctx;
|
|
114
148
|
if (!worker && !navigator.serviceWorker.controller)
|
|
115
149
|
l.warn("", "Service Worker is not controlling the page");
|
|
116
150
|
// If no worker is provided, we use the service worker
|
|
@@ -134,7 +168,7 @@ async function postMessage(ctx, message, options) {
|
|
|
134
168
|
*/
|
|
135
169
|
export function postMessageSync(l, worker, message, options) {
|
|
136
170
|
if (!worker && !navigator.serviceWorker.controller)
|
|
137
|
-
l.warn("
|
|
171
|
+
l.warn("Service Worker is not controlling the page");
|
|
138
172
|
// If no worker is provided, we use the service worker
|
|
139
173
|
const w = worker instanceof SharedWorker
|
|
140
174
|
? worker.port
|
|
@@ -152,9 +186,9 @@ export function postMessageSync(l, worker, message, options) {
|
|
|
152
186
|
* @returns
|
|
153
187
|
*/
|
|
154
188
|
export async function startClientListener(ctx) {
|
|
155
|
-
if (_clientListenerStarted)
|
|
189
|
+
if (_clientListenerStarted.has(ctx.nodeId ?? "(SW)"))
|
|
156
190
|
return;
|
|
157
|
-
const { logger: l, worker } = ctx;
|
|
191
|
+
const { logger: l, node: worker } = ctx;
|
|
158
192
|
// Get service worker registration if no worker is provided
|
|
159
193
|
if (!worker) {
|
|
160
194
|
const sw = await navigator.serviceWorker.ready;
|
|
@@ -177,7 +211,7 @@ export async function startClientListener(ctx) {
|
|
|
177
211
|
// We don't use a arktype schema here, we trust the server to send valid data
|
|
178
212
|
const payload = eventData;
|
|
179
213
|
// Ignore #initialize request, it's client->server only
|
|
180
|
-
if ("
|
|
214
|
+
if ("isInitializeRequest" in payload) {
|
|
181
215
|
l.warn(null, "Ignoring unexpected #initialize from server", payload);
|
|
182
216
|
return;
|
|
183
217
|
}
|
|
@@ -215,12 +249,14 @@ export async function startClientListener(ctx) {
|
|
|
215
249
|
else {
|
|
216
250
|
w.addEventListener("message", listener);
|
|
217
251
|
}
|
|
218
|
-
_clientListenerStarted
|
|
252
|
+
_clientListenerStarted.add(ctx.nodeId ?? "(SW)");
|
|
219
253
|
// Recursive terminal case is ensured by calling this *after* _clientListenerStarted is set to true: startClientListener() will therefore not be called in postMessage() again.
|
|
220
254
|
await postMessage(ctx, {
|
|
221
255
|
by: "sw&rpc",
|
|
222
256
|
functionName: "#initialize",
|
|
257
|
+
isInitializeRequest: true,
|
|
223
258
|
localStorageData: ctx.localStorage,
|
|
259
|
+
nodeId: ctx.nodeId ?? "(SW)",
|
|
224
260
|
});
|
|
225
261
|
}
|
|
226
262
|
/**
|
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
|
@@ -1 +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,
|
|
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"}
|
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,12 @@ 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
|
+
/**
|
|
29
|
+
*
|
|
30
|
+
* @param scope
|
|
31
|
+
*/
|
|
32
|
+
export declare function injectIntoConsoleGlobal(scope: WorkerGlobalScope | SharedWorkerGlobalScope, nodeId: string): void;
|
|
33
|
+
export {};
|
|
28
34
|
//# 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;AA8EnD;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,iBAAiB,GAAG,uBAAuB,EAClD,MAAM,EAAE,MAAM,QAKf"}
|
package/dist/log.js
CHANGED
|
@@ -2,54 +2,72 @@
|
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
const LOG_LEVELS = ["debug", "info", "warn", "error"];
|
|
25
|
+
const PATCHABLE_LOG_METHODS = [
|
|
26
|
+
"debug",
|
|
27
|
+
"info",
|
|
28
|
+
"warn",
|
|
29
|
+
"error",
|
|
30
|
+
"log",
|
|
31
|
+
];
|
|
32
|
+
function logger(method, side, ids) {
|
|
33
|
+
if (ids === undefined || typeof ids === "string") {
|
|
34
|
+
const nid = ids ?? null;
|
|
35
|
+
return (rqid, ...args) => log(method, side, { nid, rqid }, ...args);
|
|
27
36
|
}
|
|
28
|
-
return (
|
|
37
|
+
return (...args) => log(method, side, ids, ...args);
|
|
29
38
|
}
|
|
39
|
+
const originalConsole = PATCHABLE_LOG_METHODS.reduce((result, method) => {
|
|
40
|
+
result[method] = console[method];
|
|
41
|
+
return result;
|
|
42
|
+
}, {});
|
|
30
43
|
/**
|
|
31
44
|
* Send log messages to the console, with a helpful prefix.
|
|
32
|
-
* @param
|
|
45
|
+
* @param method
|
|
33
46
|
* @param side
|
|
34
|
-
* @param
|
|
35
|
-
* @param message
|
|
47
|
+
* @param ids request ID and node ID
|
|
36
48
|
* @param args passed to console methods directly
|
|
37
49
|
*/
|
|
38
|
-
function log(
|
|
39
|
-
const prefix =
|
|
40
|
-
[
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
function log(method, side, { rqid, nid }, ...args) {
|
|
51
|
+
const prefix = [
|
|
52
|
+
`[SWARPC ${side}]`,
|
|
53
|
+
rqid ? `%c${rqid}%c` : "",
|
|
54
|
+
nid ? `%c@ ${nid}%c` : "",
|
|
55
|
+
]
|
|
56
|
+
.filter(Boolean)
|
|
57
|
+
.join(" ");
|
|
58
|
+
const prefixStyles = [];
|
|
59
|
+
if (rqid)
|
|
60
|
+
prefixStyles.push("color: cyan", "color: inherit");
|
|
61
|
+
if (nid)
|
|
62
|
+
prefixStyles.push("color: hotpink", "color: inherit");
|
|
63
|
+
return originalConsole[method](prefix, ...prefixStyles, ...args);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
*
|
|
67
|
+
* @param scope
|
|
68
|
+
*/
|
|
69
|
+
export function injectIntoConsoleGlobal(scope, nodeId) {
|
|
70
|
+
for (const method of PATCHABLE_LOG_METHODS) {
|
|
71
|
+
scope.self.console[method] = logger(method, "server", nodeId);
|
|
54
72
|
}
|
|
55
73
|
}
|
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.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,EAAyC,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAC;AAChF,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,CA6M1B"}
|
package/dist/server.js
CHANGED
|
@@ -4,16 +4,12 @@
|
|
|
4
4
|
*/
|
|
5
5
|
/// <reference lib="webworker" />
|
|
6
6
|
import { type } from "arktype";
|
|
7
|
-
import { createLogger } from "./log.js";
|
|
7
|
+
import { createLogger, injectIntoConsoleGlobal } from "./log.js";
|
|
8
8
|
import { PayloadHeaderSchema, PayloadInitializeSchema, PayloadSchema, zImplementations, zProcedures, } from "./types.js";
|
|
9
9
|
import { findTransferables } from "./utils.js";
|
|
10
10
|
import { FauxLocalStorage } from "./localstorage.js";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
const SharedWorkerGlobalScope = globalThis.SharedWorkerGlobalScope ?? MockedWorkerGlobalScope;
|
|
15
|
-
const DedicatedWorkerGlobalScope = globalThis.DedicatedWorkerGlobalScope ?? MockedWorkerGlobalScope;
|
|
16
|
-
const ServiceWorkerGlobalScope = globalThis.ServiceWorkerGlobalScope ?? MockedWorkerGlobalScope;
|
|
11
|
+
import { scopeIsDedicated, scopeIsShared, scopeIsService } from "./scopes.js";
|
|
12
|
+
import { nodeIdFromScope } from "./nodes.js";
|
|
17
13
|
const abortControllers = new Map();
|
|
18
14
|
const abortedRequests = new Set();
|
|
19
15
|
/**
|
|
@@ -29,19 +25,12 @@ const abortedRequests = new Set();
|
|
|
29
25
|
* {@includeCode ../example/src/service-worker.ts}
|
|
30
26
|
*/
|
|
31
27
|
export function Server(procedures, { loglevel = "debug", scope, _scopeType, } = {}) {
|
|
32
|
-
const l = createLogger("server", loglevel);
|
|
33
28
|
// If scope is not provided, use the global scope
|
|
34
29
|
// This function is meant to be used in a worker, so `self` is a WorkerGlobalScope
|
|
35
30
|
scope ??= self;
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
}
|
|
31
|
+
// Service workers don't have a name, but it's fine anyways cuz we don't have multiple nodes when running with a SW
|
|
32
|
+
const nodeId = nodeIdFromScope(scope, _scopeType);
|
|
33
|
+
const l = createLogger("server", loglevel, nodeId);
|
|
45
34
|
// Initialize the instance.
|
|
46
35
|
// Procedures and implementations are stored on properties with symbol keys,
|
|
47
36
|
// to avoid any conflicts with procedure names, and also discourage direct access to them.
|
|
@@ -60,7 +49,7 @@ export function Server(procedures, { loglevel = "debug", scope, _scopeType, } =
|
|
|
60
49
|
tools.abortSignal?.throwIfAborted();
|
|
61
50
|
return new Promise((resolve, reject) => {
|
|
62
51
|
tools.abortSignal?.addEventListener("abort", () => {
|
|
63
|
-
let { requestId, reason } = tools.abortSignal
|
|
52
|
+
let { requestId, reason } = tools.abortSignal.reason;
|
|
64
53
|
l.debug(requestId, `Aborted ${functionName} request: ${reason}`);
|
|
65
54
|
reject({ aborted: reason });
|
|
66
55
|
});
|
|
@@ -71,7 +60,7 @@ export function Server(procedures, { loglevel = "debug", scope, _scopeType, } =
|
|
|
71
60
|
}
|
|
72
61
|
instance.start = async () => {
|
|
73
62
|
const port = await new Promise((resolve) => {
|
|
74
|
-
if (!scopeIsShared(scope))
|
|
63
|
+
if (!scopeIsShared(scope, _scopeType))
|
|
75
64
|
return resolve(undefined);
|
|
76
65
|
l.debug(null, "Awaiting shared worker connection...");
|
|
77
66
|
scope.addEventListener("connect", ({ ports: [port] }) => {
|
|
@@ -85,10 +74,10 @@ export function Server(procedures, { loglevel = "debug", scope, _scopeType, } =
|
|
|
85
74
|
if (port) {
|
|
86
75
|
port.postMessage(data, { transfer });
|
|
87
76
|
}
|
|
88
|
-
else if (scopeIsDedicated(scope)) {
|
|
77
|
+
else if (scopeIsDedicated(scope, _scopeType)) {
|
|
89
78
|
scope.postMessage(data, { transfer });
|
|
90
79
|
}
|
|
91
|
-
else if (scopeIsService(scope)) {
|
|
80
|
+
else if (scopeIsService(scope, _scopeType)) {
|
|
92
81
|
await scope.clients.matchAll().then((clients) => {
|
|
93
82
|
clients.forEach((client) => client.postMessage(data, { transfer }));
|
|
94
83
|
});
|
|
@@ -96,9 +85,10 @@ export function Server(procedures, { loglevel = "debug", scope, _scopeType, } =
|
|
|
96
85
|
};
|
|
97
86
|
const listener = async (event) => {
|
|
98
87
|
if (PayloadInitializeSchema.allows(event.data)) {
|
|
99
|
-
const { localStorageData } = event.data;
|
|
88
|
+
const { localStorageData, nodeId } = event.data;
|
|
100
89
|
l.debug(null, "Setting up faux localStorage", localStorageData);
|
|
101
90
|
new FauxLocalStorage(localStorageData).register(scope);
|
|
91
|
+
injectIntoConsoleGlobal(scope, nodeId);
|
|
102
92
|
return;
|
|
103
93
|
}
|
|
104
94
|
// Decode the payload
|
|
@@ -131,7 +121,7 @@ export function Server(procedures, { loglevel = "debug", scope, _scopeType, } =
|
|
|
131
121
|
}
|
|
132
122
|
// Define payload schema for incoming messages
|
|
133
123
|
const payload = PayloadSchema(type(`"${functionName}"`), schemas.input, schemas.progress, schemas.success).assert(event.data);
|
|
134
|
-
if ("
|
|
124
|
+
if ("isInitializeRequest" in payload)
|
|
135
125
|
throw "Unreachable: #initialize request payload should've been handled already";
|
|
136
126
|
// Handle abortion requests (pro-choice ftw!!)
|
|
137
127
|
if (payload.abort) {
|
|
@@ -150,11 +140,12 @@ export function Server(procedures, { loglevel = "debug", scope, _scopeType, } =
|
|
|
150
140
|
try {
|
|
151
141
|
// Call the implementation with the input and a progress callback
|
|
152
142
|
const result = await implementation(payload.input, async (progress) => {
|
|
153
|
-
l.debug(requestId, `Progress for ${functionName}`, progress);
|
|
143
|
+
// l.debug(requestId, `Progress for ${functionName}`, progress);
|
|
154
144
|
await postMsg({ progress });
|
|
155
145
|
}, {
|
|
146
|
+
nodeId,
|
|
156
147
|
abortSignal: abortControllers.get(requestId)?.signal,
|
|
157
|
-
logger: createLogger("server", loglevel, requestId),
|
|
148
|
+
logger: createLogger("server", loglevel, nodeId, requestId),
|
|
158
149
|
});
|
|
159
150
|
// Send results
|
|
160
151
|
l.debug(requestId, `Result for ${functionName}`, result);
|
|
@@ -177,17 +168,17 @@ export function Server(procedures, { loglevel = "debug", scope, _scopeType, } =
|
|
|
177
168
|
}
|
|
178
169
|
};
|
|
179
170
|
// Listen for messages from the client
|
|
180
|
-
if (scopeIsShared(scope)) {
|
|
171
|
+
if (scopeIsShared(scope, _scopeType)) {
|
|
181
172
|
if (!port)
|
|
182
173
|
throw new Error("SharedWorker port not initialized");
|
|
183
|
-
|
|
174
|
+
l.info(null, "Listening for shared worker messages on port", port);
|
|
184
175
|
port.addEventListener("message", listener);
|
|
185
176
|
port.start();
|
|
186
177
|
}
|
|
187
|
-
else if (scopeIsDedicated(scope)) {
|
|
178
|
+
else if (scopeIsDedicated(scope, _scopeType)) {
|
|
188
179
|
scope.addEventListener("message", listener);
|
|
189
180
|
}
|
|
190
|
-
else if (scopeIsService(scope)) {
|
|
181
|
+
else if (scopeIsService(scope, _scopeType)) {
|
|
191
182
|
scope.addEventListener("message", listener);
|
|
192
183
|
}
|
|
193
184
|
else {
|