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/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 = false;
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 worker object. If not provided, the client will use the service worker.
21
- * Example: `new Worker("./worker.js")`
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 = false;
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
- worker,
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(requestId, `Requesting ${functionName} with`, input);
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, worker, {
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("", "Service Worker is not controlling the page");
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 ("localStorageData" in payload) {
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 = true;
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
@@ -2,6 +2,7 @@
2
2
  * @module
3
3
  * @mergeModuleWith <project>
4
4
  */
5
+ import "./polyfills.js";
5
6
  export * from "./client.js";
6
7
  export * from "./server.js";
7
8
  export type { ProceduresMap, CancelablePromise } from "./types.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,YAAY,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA"}
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
@@ -2,5 +2,6 @@
2
2
  * @module
3
3
  * @mergeModuleWith <project>
4
4
  */
5
+ import "./polyfills.js";
5
6
  export * from "./client.js";
6
7
  export * from "./server.js";
@@ -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,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"}
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
- export declare const LOG_LEVELS: readonly ["debug", "info", "warn", "error"];
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,CAAC,IAAI,EAAE,QAAQ,GAAG,QAAQ,EAAE,KAAK,EAAE,QAAQ,GAAG,MAAM,CAAA;AAChF,wBAAgB,YAAY,CAC1B,IAAI,EAAE,QAAQ,GAAG,QAAQ,EACzB,KAAK,EAAE,QAAQ,EACf,IAAI,EAAE,MAAM,GACX,kBAAkB,CAAA;AAyBrB;;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,CAAA;IACrE,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IACpE,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IACpE,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;CACtE,CAAA;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IAChD,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IAC/C,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IAC/C,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;CACjD,CAAA;AAED,cAAc;AACd,eAAO,MAAM,UAAU,6CAA8C,CAAA;AAErE,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAA"}
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, rqid) : () => { },
10
- info: lvls.includes("info") ? logger("info", side, rqid) : () => { },
11
- warn: lvls.includes("warn") ? logger("warn", side, rqid) : () => { },
12
- error: lvls.includes("error") ? logger("error", side, rqid) : () => { },
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
- export const LOG_LEVELS = ["debug", "info", "warn", "error"];
24
- function logger(severity, side, rqid) {
25
- if (rqid === undefined) {
26
- return (rqid, message, ...args) => log(severity, side, rqid, message, ...args);
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 (message, ...args) => log(severity, side, rqid, message, ...args);
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 severity
45
+ * @param method
33
46
  * @param side
34
- * @param rqid request ID
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(severity, side, rqid, message, ...args) {
39
- const prefix = "[" +
40
- ["SWARPC", side, rqid ? `%c${rqid}%c` : ""].filter(Boolean).join(" ") +
41
- "]";
42
- const prefixStyles = rqid ? ["color: cyan;", "color: inherit;"] : [];
43
- if (severity === "debug") {
44
- console.debug(prefix, ...prefixStyles, message, ...args);
45
- }
46
- else if (severity === "info") {
47
- console.info(prefix, ...prefixStyles, message, ...args);
48
- }
49
- else if (severity === "warn") {
50
- console.warn(prefix, ...prefixStyles, message, ...args);
51
- }
52
- else if (severity === "error") {
53
- console.error(prefix, ...prefixStyles, message, ...args);
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
  }
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=polyfills.d.ts.map
@@ -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 {};
@@ -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
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,EAAgB,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAA;AACtD,OAAO,EACL,kBAAkB,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"}
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
- class MockedWorkerGlobalScope {
12
- constructor() { }
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
- 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
- }
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?.reason;
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 ("localStorageData" in payload)
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
- console.log("Listening for shared worker messages on port", port);
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 {