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/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 = 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.
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 = false;
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
- return postMessage(l, worker, hooks, {
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(requestId, `Requesting ${functionName} with`, input);
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, worker, {
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(l, worker, hooks, message, options) {
105
- await startClientListener(l, worker, hooks);
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("", "Service Worker is not controlling the page");
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(l, worker, hooks = {}) {
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", { worker, w, hooks });
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 { requestId, ...data } = eventData;
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 = true;
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
@@ -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";
@@ -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
- 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
+ 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,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"}
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, 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
+ 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, rqid, message, ...args);
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 rqid request ID
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
- ["SWARPC", side, rqid ? `%c${rqid}%c` : ""].filter(Boolean).join(" ") +
41
- "]";
42
- const prefixStyles = rqid ? ["color: cyan;", "color: inherit;"] : [];
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
  }
@@ -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
+ }
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.worker The worker scope to use, defaults to the `self` of the file where Server() is called.
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.
@@ -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,EAKlB,uBAAuB,EACvB,gBAAgB,EAChB,WAAW,EACX,KAAK,aAAa,EACnB,MAAM,YAAY,CAAA;AAgBnB;;;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,CAgN1B"}
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"}