swarpc 0.8.0 → 0.10.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.d.ts +28 -6
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +58 -12
- package/dist/server.d.ts +7 -4
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +78 -29
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -3
- package/src/client.ts +79 -17
- package/src/server.ts +120 -47
- package/src/types.ts +2 -2
package/dist/client.d.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* @module
|
|
3
3
|
* @mergeModuleWith <project>
|
|
4
4
|
*/
|
|
5
|
-
import { type LogLevel } from "./log.js";
|
|
6
|
-
import { ClientMethod, Hooks, zProcedures, type ProceduresMap } from "./types.js";
|
|
5
|
+
import { type Logger, type LogLevel } from "./log.js";
|
|
6
|
+
import { ClientMethod, Hooks, Payload, zProcedures, type ProceduresMap } from "./types.js";
|
|
7
7
|
/**
|
|
8
8
|
* The sw&rpc client instance, which provides {@link ClientMethod | methods to call procedures}.
|
|
9
9
|
* Each property of the procedures map will be a method, that accepts an input, an optional onProgress callback and an optional request ID.
|
|
@@ -18,18 +18,40 @@ export type SwarpcClient<Procedures extends ProceduresMap> = {
|
|
|
18
18
|
*
|
|
19
19
|
* @param procedures procedures the client will be able to call, see {@link ProceduresMap}
|
|
20
20
|
* @param options various options
|
|
21
|
-
* @param options.worker
|
|
22
|
-
*
|
|
21
|
+
* @param options.worker The instantiated worker object. If not provided, the client will use the service worker.
|
|
22
|
+
* Example: `new Worker("./worker.js")`
|
|
23
|
+
* See {@link Worker} (used by both dedicated workers and service workers), {@link SharedWorker}, and
|
|
24
|
+
* the different [worker types](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API#worker_types) that exist
|
|
25
|
+
* @param options.hooks Hooks to run on messages received from the server. See {@link Hooks}
|
|
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.
|
|
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.
|
|
23
28
|
* @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}
|
|
24
29
|
*
|
|
25
30
|
* An example of defining and using a client:
|
|
26
31
|
* {@includeCode ../example/src/routes/+page.svelte}
|
|
27
32
|
*/
|
|
28
|
-
export declare function Client<Procedures extends ProceduresMap>(procedures: Procedures, { worker, loglevel, hooks, }?: {
|
|
29
|
-
worker?: Worker;
|
|
33
|
+
export declare function Client<Procedures extends ProceduresMap>(procedures: Procedures, { worker, loglevel, restartListener, hooks, }?: {
|
|
34
|
+
worker?: Worker | SharedWorker;
|
|
30
35
|
hooks?: Hooks<Procedures>;
|
|
31
36
|
loglevel?: LogLevel;
|
|
37
|
+
restartListener?: boolean;
|
|
32
38
|
}): SwarpcClient<Procedures>;
|
|
39
|
+
/**
|
|
40
|
+
* A quicker version of postMessage that does not try to start the client listener, await the service worker, etc.
|
|
41
|
+
* esp. useful for abort logic that needs to not be... put behind everything else on the event loop.
|
|
42
|
+
* @param l
|
|
43
|
+
* @param worker
|
|
44
|
+
* @param message
|
|
45
|
+
* @param options
|
|
46
|
+
*/
|
|
47
|
+
export declare function postMessageSync<Procedures extends ProceduresMap>(l: Logger, worker: Worker | SharedWorker | undefined, message: Payload<Procedures>, options?: StructuredSerializeOptions): void;
|
|
48
|
+
/**
|
|
49
|
+
* Starts the client listener, which listens for messages from the sw&rpc server.
|
|
50
|
+
* @param worker if provided, the client will use this worker to listen for messages, instead of using the service worker
|
|
51
|
+
* @param force if true, will force the listener to restart even if it has already been started
|
|
52
|
+
* @returns
|
|
53
|
+
*/
|
|
54
|
+
export declare function startClientListener<Procedures extends ProceduresMap>(l: Logger, worker?: Worker | SharedWorker, hooks?: Hooks<Procedures>): Promise<void>;
|
|
33
55
|
/**
|
|
34
56
|
* Generate a random request ID, used to identify requests between client and server.
|
|
35
57
|
* @source
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAgB,KAAK,MAAM,EAAE,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAA;AACnE,OAAO,EACL,YAAY,EACZ,KAAK,EACL,OAAO,EAEP,WAAW,EACX,KAAK,aAAa,EACnB,MAAM,YAAY,CAAA;AAGnB;;;;GAIG;AACH,MAAM,MAAM,YAAY,CAAC,UAAU,SAAS,aAAa,IAAI;IAC3D,CAAC,WAAW,CAAC,EAAE,UAAU,CAAA;CAC1B,GAAG;KACD,CAAC,IAAI,MAAM,UAAU,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;CACrD,CAAA;AAkBD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,MAAM,CAAC,UAAU,SAAS,aAAa,EACrD,UAAU,EAAE,UAAU,EACtB,EACE,MAAM,EACN,QAAkB,EAClB,eAAuB,EACvB,KAAU,GACX,GAAE;IACD,MAAM,CAAC,EAAE,MAAM,GAAG,YAAY,CAAA;IAC9B,KAAK,CAAC,EAAE,KAAK,CAAC,UAAU,CAAC,CAAA;IACzB,QAAQ,CAAC,EAAE,QAAQ,CAAA;IACnB,eAAe,CAAC,EAAE,OAAO,CAAA;CACrB,GACL,YAAY,CAAC,UAAU,CAAC,CAsG1B;AAiCD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,UAAU,SAAS,aAAa,EAC9D,CAAC,EAAE,MAAM,EACT,MAAM,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,EACzC,OAAO,EAAE,OAAO,CAAC,UAAU,CAAC,EAC5B,OAAO,CAAC,EAAE,0BAA0B,GACnC,IAAI,CAiBN;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,UAAU,SAAS,aAAa,EACxE,CAAC,EAAE,MAAM,EACT,MAAM,CAAC,EAAE,MAAM,GAAG,YAAY,EAC9B,KAAK,GAAE,KAAK,CAAC,UAAU,CAAM,iBAmE9B;AAED;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC"}
|
package/dist/client.js
CHANGED
|
@@ -17,15 +17,22 @@ let _clientListenerStarted = false;
|
|
|
17
17
|
*
|
|
18
18
|
* @param procedures procedures the client will be able to call, see {@link ProceduresMap}
|
|
19
19
|
* @param options various options
|
|
20
|
-
* @param options.worker
|
|
21
|
-
*
|
|
20
|
+
* @param options.worker The instantiated worker object. If not provided, the client will use the service worker.
|
|
21
|
+
* Example: `new Worker("./worker.js")`
|
|
22
|
+
* See {@link Worker} (used by both dedicated workers and service workers), {@link SharedWorker}, and
|
|
23
|
+
* the different [worker types](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API#worker_types) that exist
|
|
24
|
+
* @param options.hooks Hooks to run on messages received from the server. See {@link Hooks}
|
|
25
|
+
* @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
|
+
* @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.
|
|
22
27
|
* @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}
|
|
23
28
|
*
|
|
24
29
|
* An example of defining and using a client:
|
|
25
30
|
* {@includeCode ../example/src/routes/+page.svelte}
|
|
26
31
|
*/
|
|
27
|
-
export function Client(procedures, { worker, loglevel = "debug", hooks = {}, } = {}) {
|
|
32
|
+
export function Client(procedures, { worker, loglevel = "debug", restartListener = false, hooks = {}, } = {}) {
|
|
28
33
|
const l = createLogger("client", loglevel);
|
|
34
|
+
if (restartListener)
|
|
35
|
+
_clientListenerStarted = false;
|
|
29
36
|
// Store procedures on a symbol key, to avoid conflicts with procedure names
|
|
30
37
|
const instance = { [zProcedures]: procedures };
|
|
31
38
|
for (const functionName of Object.keys(procedures)) {
|
|
@@ -60,7 +67,7 @@ export function Client(procedures, { worker, loglevel = "debug", hooks = {}, } =
|
|
|
60
67
|
: [];
|
|
61
68
|
// Post the message to the server
|
|
62
69
|
l.debug(requestId, `Requesting ${functionName} with`, input);
|
|
63
|
-
send(requestId, { input }, { transfer })
|
|
70
|
+
return send(requestId, { input }, { transfer })
|
|
64
71
|
.then(() => { })
|
|
65
72
|
.catch(reject);
|
|
66
73
|
});
|
|
@@ -71,13 +78,18 @@ export function Client(procedures, { worker, loglevel = "debug", hooks = {}, } =
|
|
|
71
78
|
const requestId = makeRequestId();
|
|
72
79
|
return {
|
|
73
80
|
request: _runProcedure(input, onProgress, requestId),
|
|
74
|
-
|
|
81
|
+
cancel(reason) {
|
|
75
82
|
if (!pendingRequests.has(requestId)) {
|
|
76
83
|
l.warn(requestId, `Cannot cancel ${functionName} request, it has already been resolved or rejected`);
|
|
77
84
|
return;
|
|
78
85
|
}
|
|
79
86
|
l.debug(requestId, `Cancelling ${functionName} with`, reason);
|
|
80
|
-
|
|
87
|
+
postMessageSync(l, worker, {
|
|
88
|
+
by: "sw&rpc",
|
|
89
|
+
requestId,
|
|
90
|
+
functionName,
|
|
91
|
+
abort: { reason },
|
|
92
|
+
});
|
|
81
93
|
pendingRequests.delete(requestId);
|
|
82
94
|
},
|
|
83
95
|
};
|
|
@@ -94,7 +106,33 @@ async function postMessage(l, worker, hooks, message, options) {
|
|
|
94
106
|
if (!worker && !navigator.serviceWorker.controller)
|
|
95
107
|
l.warn("", "Service Worker is not controlling the page");
|
|
96
108
|
// If no worker is provided, we use the service worker
|
|
97
|
-
const w = worker
|
|
109
|
+
const w = worker instanceof SharedWorker
|
|
110
|
+
? worker.port
|
|
111
|
+
: worker === undefined
|
|
112
|
+
? await navigator.serviceWorker.ready.then((r) => r.active)
|
|
113
|
+
: worker;
|
|
114
|
+
if (!w) {
|
|
115
|
+
throw new Error("[SWARPC Client] No active service worker found");
|
|
116
|
+
}
|
|
117
|
+
w.postMessage(message, options);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* A quicker version of postMessage that does not try to start the client listener, await the service worker, etc.
|
|
121
|
+
* esp. useful for abort logic that needs to not be... put behind everything else on the event loop.
|
|
122
|
+
* @param l
|
|
123
|
+
* @param worker
|
|
124
|
+
* @param message
|
|
125
|
+
* @param options
|
|
126
|
+
*/
|
|
127
|
+
export function postMessageSync(l, worker, message, options) {
|
|
128
|
+
if (!worker && !navigator.serviceWorker.controller)
|
|
129
|
+
l.warn("", "Service Worker is not controlling the page");
|
|
130
|
+
// If no worker is provided, we use the service worker
|
|
131
|
+
const w = worker instanceof SharedWorker
|
|
132
|
+
? worker.port
|
|
133
|
+
: worker === undefined
|
|
134
|
+
? navigator.serviceWorker.controller
|
|
135
|
+
: worker;
|
|
98
136
|
if (!w) {
|
|
99
137
|
throw new Error("[SWARPC Client] No active service worker found");
|
|
100
138
|
}
|
|
@@ -103,9 +141,10 @@ async function postMessage(l, worker, hooks, message, options) {
|
|
|
103
141
|
/**
|
|
104
142
|
* Starts the client listener, which listens for messages from the sw&rpc server.
|
|
105
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
|
|
106
145
|
* @returns
|
|
107
146
|
*/
|
|
108
|
-
async function startClientListener(l, worker, hooks = {}) {
|
|
147
|
+
export async function startClientListener(l, worker, hooks = {}) {
|
|
109
148
|
if (_clientListenerStarted)
|
|
110
149
|
return;
|
|
111
150
|
// Get service worker registration if no worker is provided
|
|
@@ -120,8 +159,8 @@ async function startClientListener(l, worker, hooks = {}) {
|
|
|
120
159
|
}
|
|
121
160
|
const w = worker ?? navigator.serviceWorker;
|
|
122
161
|
// Start listening for messages
|
|
123
|
-
l.debug(
|
|
124
|
-
|
|
162
|
+
l.debug(null, "Starting client listener", { worker, w, hooks });
|
|
163
|
+
const listener = (event) => {
|
|
125
164
|
// Get the data from the event
|
|
126
165
|
const eventData = event.data || {};
|
|
127
166
|
// Ignore other messages that aren't for us
|
|
@@ -136,7 +175,7 @@ async function startClientListener(l, worker, hooks = {}) {
|
|
|
136
175
|
// Get the associated pending request handlers
|
|
137
176
|
const handlers = pendingRequests.get(requestId);
|
|
138
177
|
if (!handlers) {
|
|
139
|
-
throw new Error(`[SWARPC Client] ${requestId} has no active request handlers`);
|
|
178
|
+
throw new Error(`[SWARPC Client] ${requestId} has no active request handlers, cannot process ${JSON.stringify(data)}`);
|
|
140
179
|
}
|
|
141
180
|
// React to the data received: call hook, call handler,
|
|
142
181
|
// and remove the request from pendingRequests (unless it's a progress update)
|
|
@@ -154,7 +193,14 @@ async function startClientListener(l, worker, hooks = {}) {
|
|
|
154
193
|
handlers.resolve(data.result);
|
|
155
194
|
pendingRequests.delete(requestId);
|
|
156
195
|
}
|
|
157
|
-
}
|
|
196
|
+
};
|
|
197
|
+
if (w instanceof SharedWorker) {
|
|
198
|
+
w.port.addEventListener("message", listener);
|
|
199
|
+
w.port.start();
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
w.addEventListener("message", listener);
|
|
203
|
+
}
|
|
158
204
|
_clientListenerStarted = true;
|
|
159
205
|
}
|
|
160
206
|
/**
|
package/dist/server.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { ImplementationsMap, ProcedureImplementation, zImplementations, zProcedu
|
|
|
11
11
|
export type SwarpcServer<Procedures extends ProceduresMap> = {
|
|
12
12
|
[zProcedures]: Procedures;
|
|
13
13
|
[zImplementations]: ImplementationsMap<Procedures>;
|
|
14
|
-
start(
|
|
14
|
+
start(): Promise<void>;
|
|
15
15
|
} & {
|
|
16
16
|
[F in keyof Procedures]: (impl: ProcedureImplementation<Procedures[F]["input"], Procedures[F]["progress"], Procedures[F]["success"]>) => void;
|
|
17
17
|
};
|
|
@@ -19,14 +19,17 @@ 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
|
|
22
|
+
* @param options.worker The worker scope to use, defaults to the `self` of the file where Server() is called.
|
|
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
|
+
* @param options._scopeType @internal Don't touch, this is used in testing environments because the mock is subpar. Manually overrides worker scope type detection.
|
|
23
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.
|
|
24
26
|
*
|
|
25
27
|
* An example of defining a server:
|
|
26
28
|
* {@includeCode ../example/src/service-worker.ts}
|
|
27
29
|
*/
|
|
28
|
-
export declare function Server<Procedures extends ProceduresMap>(procedures: Procedures, {
|
|
29
|
-
|
|
30
|
+
export declare function Server<Procedures extends ProceduresMap>(procedures: Procedures, { loglevel, scope, _scopeType, }?: {
|
|
31
|
+
scope?: WorkerGlobalScope;
|
|
30
32
|
loglevel?: LogLevel;
|
|
33
|
+
_scopeType?: "dedicated" | "shared" | "service";
|
|
31
34
|
}): SwarpcServer<Procedures>;
|
|
32
35
|
//# sourceMappingURL=server.d.ts.map
|
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;
|
|
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"}
|
package/dist/server.js
CHANGED
|
@@ -2,31 +2,52 @@
|
|
|
2
2
|
* @module
|
|
3
3
|
* @mergeModuleWith <project>
|
|
4
4
|
*/
|
|
5
|
+
/// <reference lib="webworker" />
|
|
5
6
|
import { type } from "arktype";
|
|
6
7
|
import { createLogger } from "./log.js";
|
|
7
8
|
import { PayloadHeaderSchema, PayloadSchema, zImplementations, zProcedures, } from "./types.js";
|
|
8
9
|
import { findTransferables } from "./utils.js";
|
|
10
|
+
class MockedWorkerGlobalScope {
|
|
11
|
+
constructor() { }
|
|
12
|
+
}
|
|
13
|
+
const SharedWorkerGlobalScope = globalThis.SharedWorkerGlobalScope ?? MockedWorkerGlobalScope;
|
|
14
|
+
const DedicatedWorkerGlobalScope = globalThis.DedicatedWorkerGlobalScope ?? MockedWorkerGlobalScope;
|
|
15
|
+
const ServiceWorkerGlobalScope = globalThis.ServiceWorkerGlobalScope ?? MockedWorkerGlobalScope;
|
|
9
16
|
const abortControllers = new Map();
|
|
10
17
|
const abortedRequests = new Set();
|
|
11
18
|
/**
|
|
12
19
|
* Creates a sw&rpc server instance.
|
|
13
20
|
* @param procedures procedures the server will implement, see {@link ProceduresMap}
|
|
14
21
|
* @param options various options
|
|
15
|
-
* @param options.worker
|
|
22
|
+
* @param options.worker The worker scope to use, defaults to the `self` of the file where Server() is called.
|
|
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
|
+
* @param options._scopeType @internal Don't touch, this is used in testing environments because the mock is subpar. Manually overrides worker scope type detection.
|
|
16
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.
|
|
17
26
|
*
|
|
18
27
|
* An example of defining a server:
|
|
19
28
|
* {@includeCode ../example/src/service-worker.ts}
|
|
20
29
|
*/
|
|
21
|
-
export function Server(procedures, {
|
|
30
|
+
export function Server(procedures, { loglevel = "debug", scope, _scopeType, } = {}) {
|
|
22
31
|
const l = createLogger("server", loglevel);
|
|
32
|
+
// If scope is not provided, use the global scope
|
|
33
|
+
// This function is meant to be used in a worker, so `self` is a WorkerGlobalScope
|
|
34
|
+
scope ??= self;
|
|
35
|
+
function scopeIsShared(scope) {
|
|
36
|
+
return scope instanceof SharedWorkerGlobalScope || _scopeType === "shared";
|
|
37
|
+
}
|
|
38
|
+
function scopeIsDedicated(scope) {
|
|
39
|
+
return (scope instanceof DedicatedWorkerGlobalScope || _scopeType === "dedicated");
|
|
40
|
+
}
|
|
41
|
+
function scopeIsService(scope) {
|
|
42
|
+
return scope instanceof ServiceWorkerGlobalScope || _scopeType === "service";
|
|
43
|
+
}
|
|
23
44
|
// Initialize the instance.
|
|
24
45
|
// Procedures and implementations are stored on properties with symbol keys,
|
|
25
46
|
// to avoid any conflicts with procedure names, and also discourage direct access to them.
|
|
26
47
|
const instance = {
|
|
27
48
|
[zProcedures]: procedures,
|
|
28
49
|
[zImplementations]: {},
|
|
29
|
-
start: (
|
|
50
|
+
start: async () => { },
|
|
30
51
|
};
|
|
31
52
|
// Set all implementation-setter methods
|
|
32
53
|
for (const functionName in procedures) {
|
|
@@ -47,21 +68,32 @@ export function Server(procedures, { worker, loglevel = "debug" } = {}) {
|
|
|
47
68
|
};
|
|
48
69
|
});
|
|
49
70
|
}
|
|
50
|
-
instance.start = (
|
|
71
|
+
instance.start = async () => {
|
|
72
|
+
const port = await new Promise((resolve) => {
|
|
73
|
+
if (!scopeIsShared(scope))
|
|
74
|
+
return resolve(undefined);
|
|
75
|
+
console.log("Awaiting shared worker connection...");
|
|
76
|
+
scope.addEventListener("connect", ({ ports: [port] }) => {
|
|
77
|
+
console.log("Shared worker connected with port", port);
|
|
78
|
+
resolve(port);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
51
81
|
// Used to post messages back to the client
|
|
52
82
|
const postMessage = async (autotransfer, data) => {
|
|
53
83
|
const transfer = autotransfer ? [] : findTransferables(data);
|
|
54
|
-
if (
|
|
55
|
-
|
|
84
|
+
if (port) {
|
|
85
|
+
port.postMessage(data, { transfer });
|
|
56
86
|
}
|
|
57
|
-
else {
|
|
58
|
-
|
|
87
|
+
else if (scopeIsDedicated(scope)) {
|
|
88
|
+
scope.postMessage(data, { transfer });
|
|
89
|
+
}
|
|
90
|
+
else if (scopeIsService(scope)) {
|
|
91
|
+
await scope.clients.matchAll().then((clients) => {
|
|
59
92
|
clients.forEach((client) => client.postMessage(data, { transfer }));
|
|
60
93
|
});
|
|
61
94
|
}
|
|
62
95
|
};
|
|
63
|
-
|
|
64
|
-
self.addEventListener("message", async (event) => {
|
|
96
|
+
const listener = async (event) => {
|
|
65
97
|
// Decode the payload
|
|
66
98
|
const { requestId, functionName } = PayloadHeaderSchema(type.enumerated(...Object.keys(procedures))).assert(event.data);
|
|
67
99
|
l.debug(requestId, `Received request for ${functionName}`, event.data);
|
|
@@ -106,16 +138,21 @@ export function Server(procedures, { worker, loglevel = "debug" } = {}) {
|
|
|
106
138
|
await postError("No input provided");
|
|
107
139
|
return;
|
|
108
140
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
141
|
+
try {
|
|
142
|
+
// Call the implementation with the input and a progress callback
|
|
143
|
+
const result = await implementation(payload.input, async (progress) => {
|
|
144
|
+
l.debug(requestId, `Progress for ${functionName}`, progress);
|
|
145
|
+
await postMsg({ progress });
|
|
146
|
+
}, {
|
|
147
|
+
abortSignal: abortControllers.get(requestId)?.signal,
|
|
148
|
+
logger: createLogger("server", loglevel, requestId),
|
|
149
|
+
});
|
|
150
|
+
// Send results
|
|
151
|
+
l.debug(requestId, `Result for ${functionName}`, result);
|
|
152
|
+
await postMsg({ result });
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
117
155
|
// Send errors
|
|
118
|
-
.catch(async (error) => {
|
|
119
156
|
// Handle errors caused by abortions
|
|
120
157
|
if ("aborted" in error) {
|
|
121
158
|
l.debug(requestId, `Received abort error for ${functionName}`, error.aborted);
|
|
@@ -123,18 +160,30 @@ export function Server(procedures, { worker, loglevel = "debug" } = {}) {
|
|
|
123
160
|
abortControllers.delete(requestId);
|
|
124
161
|
return;
|
|
125
162
|
}
|
|
126
|
-
l.
|
|
163
|
+
l.info(requestId, `Error in ${functionName}`, error);
|
|
127
164
|
await postError(error);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
.then(async (result) => {
|
|
131
|
-
l.debug(requestId, `Result for ${functionName}`, result);
|
|
132
|
-
await postMsg({ result });
|
|
133
|
-
})
|
|
134
|
-
.finally(() => {
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
135
167
|
abortedRequests.delete(requestId);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
// Listen for messages from the client
|
|
171
|
+
if (scopeIsShared(scope)) {
|
|
172
|
+
if (!port)
|
|
173
|
+
throw new Error("SharedWorker port not initialized");
|
|
174
|
+
console.log("Listening for shared worker messages on port", port);
|
|
175
|
+
port.addEventListener("message", listener);
|
|
176
|
+
port.start();
|
|
177
|
+
}
|
|
178
|
+
else if (scopeIsDedicated(scope)) {
|
|
179
|
+
scope.addEventListener("message", listener);
|
|
180
|
+
}
|
|
181
|
+
else if (scopeIsService(scope)) {
|
|
182
|
+
scope.addEventListener("message", listener);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
throw new Error(`Unsupported worker scope ${scope}`);
|
|
186
|
+
}
|
|
138
187
|
};
|
|
139
188
|
return instance;
|
|
140
189
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -48,7 +48,7 @@ export type CancelablePromise<T = unknown> = {
|
|
|
48
48
|
* Abort the request.
|
|
49
49
|
* @param reason The reason for cancelling the request.
|
|
50
50
|
*/
|
|
51
|
-
cancel: (reason: string) =>
|
|
51
|
+
cancel: (reason: string) => void;
|
|
52
52
|
};
|
|
53
53
|
/**
|
|
54
54
|
* An implementation of a procedure
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,KAAK,IAAI,EAAE,MAAM,SAAS,CAAA;AACzC,OAAO,EAAU,kBAAkB,EAAE,MAAM,UAAU,CAAA;AAErD;;GAEG;AACH,MAAM,MAAM,SAAS,CAAC,CAAC,SAAS,IAAI,EAAE,CAAC,SAAS,IAAI,EAAE,CAAC,SAAS,IAAI,IAAI;IACtE;;OAEG;IACH,KAAK,EAAE,CAAC,CAAA;IACR;;;OAGG;IACH,QAAQ,EAAE,CAAC,CAAA;IACX;;OAEG;IACH,OAAO,EAAE,CAAC,CAAA;IACV;;;;;;;;;OASG;IACH,YAAY,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,aAAa,CAAA;CAClD,CAAA;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,iBAAiB,CAAC,CAAC,GAAG,OAAO,IAAI;IAC3C,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;IACnB;;;OAGG;IACH,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "swarpc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Full type-safe RPC library for service worker -- move things off of the UI thread with ease!",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"service-workers",
|
|
@@ -50,13 +50,17 @@
|
|
|
50
50
|
"sirv-cli": "^3.0.1",
|
|
51
51
|
"typedoc": "^0.28.9",
|
|
52
52
|
"typedoc-material-theme": "^1.4.0",
|
|
53
|
-
"typedoc-plugin-dt-links": "^2.0.
|
|
53
|
+
"typedoc-plugin-dt-links": "^2.0.13",
|
|
54
54
|
"typedoc-plugin-extras": "^4.0.1",
|
|
55
55
|
"typedoc-plugin-inline-sources": "^1.3.0",
|
|
56
|
-
"typedoc-plugin-mdn-links": "^5.0.
|
|
56
|
+
"typedoc-plugin-mdn-links": "^5.0.7",
|
|
57
57
|
"typedoc-plugin-redirect": "^1.2.0",
|
|
58
58
|
"typescript": "^5.9.2",
|
|
59
59
|
"vite": "^7.0.6",
|
|
60
60
|
"vitest": "^3.2.4"
|
|
61
|
+
},
|
|
62
|
+
"volta": {
|
|
63
|
+
"node": "22.18.0",
|
|
64
|
+
"npm": "11.5.2"
|
|
61
65
|
}
|
|
62
66
|
}
|
package/src/client.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @module
|
|
2
|
+
* @module
|
|
3
3
|
* @mergeModuleWith <project>
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -45,10 +45,15 @@ let _clientListenerStarted = false
|
|
|
45
45
|
*
|
|
46
46
|
* @param procedures procedures the client will be able to call, see {@link ProceduresMap}
|
|
47
47
|
* @param options various options
|
|
48
|
-
* @param options.worker
|
|
49
|
-
*
|
|
48
|
+
* @param options.worker The instantiated worker object. If not provided, the client will use the service worker.
|
|
49
|
+
* Example: `new Worker("./worker.js")`
|
|
50
|
+
* See {@link Worker} (used by both dedicated workers and service workers), {@link SharedWorker}, and
|
|
51
|
+
* the different [worker types](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API#worker_types) that exist
|
|
52
|
+
* @param options.hooks Hooks to run on messages received from the server. See {@link Hooks}
|
|
53
|
+
* @param options.loglevel Maximum log level to use, defaults to "debug" (shows everything). "info" will not show debug messages, "warn" will only show warnings and errors, "error" will only show errors.
|
|
54
|
+
* @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.
|
|
50
55
|
* @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}
|
|
51
|
-
*
|
|
56
|
+
*
|
|
52
57
|
* An example of defining and using a client:
|
|
53
58
|
* {@includeCode ../example/src/routes/+page.svelte}
|
|
54
59
|
*/
|
|
@@ -57,11 +62,19 @@ export function Client<Procedures extends ProceduresMap>(
|
|
|
57
62
|
{
|
|
58
63
|
worker,
|
|
59
64
|
loglevel = "debug",
|
|
65
|
+
restartListener = false,
|
|
60
66
|
hooks = {},
|
|
61
|
-
}: {
|
|
67
|
+
}: {
|
|
68
|
+
worker?: Worker | SharedWorker
|
|
69
|
+
hooks?: Hooks<Procedures>
|
|
70
|
+
loglevel?: LogLevel
|
|
71
|
+
restartListener?: boolean
|
|
72
|
+
} = {}
|
|
62
73
|
): SwarpcClient<Procedures> {
|
|
63
74
|
const l = createLogger("client", loglevel)
|
|
64
75
|
|
|
76
|
+
if (restartListener) _clientListenerStarted = false
|
|
77
|
+
|
|
65
78
|
// Store procedures on a symbol key, to avoid conflicts with procedure names
|
|
66
79
|
const instance = { [zProcedures]: procedures } as Partial<
|
|
67
80
|
SwarpcClient<Procedures>
|
|
@@ -124,7 +137,7 @@ export function Client<Procedures extends ProceduresMap>(
|
|
|
124
137
|
|
|
125
138
|
// Post the message to the server
|
|
126
139
|
l.debug(requestId, `Requesting ${functionName} with`, input)
|
|
127
|
-
send(requestId, { input }, { transfer })
|
|
140
|
+
return send(requestId, { input }, { transfer })
|
|
128
141
|
.then(() => {})
|
|
129
142
|
.catch(reject)
|
|
130
143
|
})
|
|
@@ -136,7 +149,7 @@ export function Client<Procedures extends ProceduresMap>(
|
|
|
136
149
|
const requestId = makeRequestId()
|
|
137
150
|
return {
|
|
138
151
|
request: _runProcedure(input, onProgress, requestId),
|
|
139
|
-
|
|
152
|
+
cancel(reason: string) {
|
|
140
153
|
if (!pendingRequests.has(requestId)) {
|
|
141
154
|
l.warn(
|
|
142
155
|
requestId,
|
|
@@ -146,7 +159,12 @@ export function Client<Procedures extends ProceduresMap>(
|
|
|
146
159
|
}
|
|
147
160
|
|
|
148
161
|
l.debug(requestId, `Cancelling ${functionName} with`, reason)
|
|
149
|
-
|
|
162
|
+
postMessageSync(l, worker, {
|
|
163
|
+
by: "sw&rpc",
|
|
164
|
+
requestId,
|
|
165
|
+
functionName,
|
|
166
|
+
abort: { reason },
|
|
167
|
+
})
|
|
150
168
|
pendingRequests.delete(requestId)
|
|
151
169
|
},
|
|
152
170
|
}
|
|
@@ -162,7 +180,7 @@ export function Client<Procedures extends ProceduresMap>(
|
|
|
162
180
|
*/
|
|
163
181
|
async function postMessage<Procedures extends ProceduresMap>(
|
|
164
182
|
l: Logger,
|
|
165
|
-
worker: Worker | undefined,
|
|
183
|
+
worker: Worker | SharedWorker | undefined,
|
|
166
184
|
hooks: Hooks<Procedures>,
|
|
167
185
|
message: Payload<Procedures>,
|
|
168
186
|
options?: StructuredSerializeOptions
|
|
@@ -174,7 +192,43 @@ async function postMessage<Procedures extends ProceduresMap>(
|
|
|
174
192
|
|
|
175
193
|
// If no worker is provided, we use the service worker
|
|
176
194
|
const w =
|
|
177
|
-
worker
|
|
195
|
+
worker instanceof SharedWorker
|
|
196
|
+
? worker.port
|
|
197
|
+
: worker === undefined
|
|
198
|
+
? await navigator.serviceWorker.ready.then((r) => r.active)
|
|
199
|
+
: worker
|
|
200
|
+
|
|
201
|
+
if (!w) {
|
|
202
|
+
throw new Error("[SWARPC Client] No active service worker found")
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
w.postMessage(message, options)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* A quicker version of postMessage that does not try to start the client listener, await the service worker, etc.
|
|
210
|
+
* esp. useful for abort logic that needs to not be... put behind everything else on the event loop.
|
|
211
|
+
* @param l
|
|
212
|
+
* @param worker
|
|
213
|
+
* @param message
|
|
214
|
+
* @param options
|
|
215
|
+
*/
|
|
216
|
+
export function postMessageSync<Procedures extends ProceduresMap>(
|
|
217
|
+
l: Logger,
|
|
218
|
+
worker: Worker | SharedWorker | undefined,
|
|
219
|
+
message: Payload<Procedures>,
|
|
220
|
+
options?: StructuredSerializeOptions
|
|
221
|
+
): void {
|
|
222
|
+
if (!worker && !navigator.serviceWorker.controller)
|
|
223
|
+
l.warn("", "Service Worker is not controlling the page")
|
|
224
|
+
|
|
225
|
+
// If no worker is provided, we use the service worker
|
|
226
|
+
const w =
|
|
227
|
+
worker instanceof SharedWorker
|
|
228
|
+
? worker.port
|
|
229
|
+
: worker === undefined
|
|
230
|
+
? navigator.serviceWorker.controller
|
|
231
|
+
: worker
|
|
178
232
|
|
|
179
233
|
if (!w) {
|
|
180
234
|
throw new Error("[SWARPC Client] No active service worker found")
|
|
@@ -186,11 +240,12 @@ async function postMessage<Procedures extends ProceduresMap>(
|
|
|
186
240
|
/**
|
|
187
241
|
* Starts the client listener, which listens for messages from the sw&rpc server.
|
|
188
242
|
* @param worker if provided, the client will use this worker to listen for messages, instead of using the service worker
|
|
243
|
+
* @param force if true, will force the listener to restart even if it has already been started
|
|
189
244
|
* @returns
|
|
190
245
|
*/
|
|
191
|
-
async function startClientListener<Procedures extends ProceduresMap>(
|
|
246
|
+
export async function startClientListener<Procedures extends ProceduresMap>(
|
|
192
247
|
l: Logger,
|
|
193
|
-
worker?: Worker,
|
|
248
|
+
worker?: Worker | SharedWorker,
|
|
194
249
|
hooks: Hooks<Procedures> = {}
|
|
195
250
|
) {
|
|
196
251
|
if (_clientListenerStarted) return
|
|
@@ -210,8 +265,8 @@ async function startClientListener<Procedures extends ProceduresMap>(
|
|
|
210
265
|
const w = worker ?? navigator.serviceWorker
|
|
211
266
|
|
|
212
267
|
// Start listening for messages
|
|
213
|
-
l.debug(
|
|
214
|
-
|
|
268
|
+
l.debug(null, "Starting client listener", { worker, w, hooks })
|
|
269
|
+
const listener = (event: Event): void => {
|
|
215
270
|
// Get the data from the event
|
|
216
271
|
const eventData = (event as MessageEvent).data || {}
|
|
217
272
|
|
|
@@ -230,7 +285,7 @@ async function startClientListener<Procedures extends ProceduresMap>(
|
|
|
230
285
|
const handlers = pendingRequests.get(requestId)
|
|
231
286
|
if (!handlers) {
|
|
232
287
|
throw new Error(
|
|
233
|
-
`[SWARPC Client] ${requestId} has no active request handlers`
|
|
288
|
+
`[SWARPC Client] ${requestId} has no active request handlers, cannot process ${JSON.stringify(data)}`
|
|
234
289
|
)
|
|
235
290
|
}
|
|
236
291
|
|
|
@@ -248,14 +303,21 @@ async function startClientListener<Procedures extends ProceduresMap>(
|
|
|
248
303
|
handlers.resolve(data.result)
|
|
249
304
|
pendingRequests.delete(requestId)
|
|
250
305
|
}
|
|
251
|
-
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (w instanceof SharedWorker) {
|
|
309
|
+
w.port.addEventListener("message", listener)
|
|
310
|
+
w.port.start()
|
|
311
|
+
} else {
|
|
312
|
+
w.addEventListener("message", listener)
|
|
313
|
+
}
|
|
252
314
|
|
|
253
315
|
_clientListenerStarted = true
|
|
254
316
|
}
|
|
255
317
|
|
|
256
318
|
/**
|
|
257
319
|
* Generate a random request ID, used to identify requests between client and server.
|
|
258
|
-
* @source
|
|
320
|
+
* @source
|
|
259
321
|
* @returns a 6-character hexadecimal string
|
|
260
322
|
*/
|
|
261
323
|
export function makeRequestId(): string {
|
package/src/server.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* @mergeModuleWith <project>
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
/// <reference lib="webworker" />
|
|
6
7
|
import { type } from "arktype"
|
|
7
8
|
import { createLogger, type LogLevel } from "./log.js"
|
|
8
9
|
import {
|
|
@@ -18,6 +19,19 @@ import {
|
|
|
18
19
|
} from "./types.js"
|
|
19
20
|
import { findTransferables } from "./utils.js"
|
|
20
21
|
|
|
22
|
+
class MockedWorkerGlobalScope {
|
|
23
|
+
constructor() {}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const SharedWorkerGlobalScope =
|
|
27
|
+
globalThis.SharedWorkerGlobalScope ?? MockedWorkerGlobalScope
|
|
28
|
+
|
|
29
|
+
const DedicatedWorkerGlobalScope =
|
|
30
|
+
globalThis.DedicatedWorkerGlobalScope ?? MockedWorkerGlobalScope
|
|
31
|
+
|
|
32
|
+
const ServiceWorkerGlobalScope =
|
|
33
|
+
globalThis.ServiceWorkerGlobalScope ?? MockedWorkerGlobalScope
|
|
34
|
+
|
|
21
35
|
/**
|
|
22
36
|
* The sw&rpc server instance, which provides methods to register {@link ProcedureImplementation | procedure implementations},
|
|
23
37
|
* and listens for incoming messages that call those procedures
|
|
@@ -25,7 +39,7 @@ import { findTransferables } from "./utils.js"
|
|
|
25
39
|
export type SwarpcServer<Procedures extends ProceduresMap> = {
|
|
26
40
|
[zProcedures]: Procedures
|
|
27
41
|
[zImplementations]: ImplementationsMap<Procedures>
|
|
28
|
-
start(
|
|
42
|
+
start(): Promise<void>
|
|
29
43
|
} & {
|
|
30
44
|
[F in keyof Procedures]: (
|
|
31
45
|
impl: ProcedureImplementation<
|
|
@@ -43,25 +57,59 @@ const abortedRequests = new Set<string>()
|
|
|
43
57
|
* Creates a sw&rpc server instance.
|
|
44
58
|
* @param procedures procedures the server will implement, see {@link ProceduresMap}
|
|
45
59
|
* @param options various options
|
|
46
|
-
* @param options.worker
|
|
60
|
+
* @param options.worker The worker scope to use, defaults to the `self` of the file where Server() is called.
|
|
61
|
+
* @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.
|
|
62
|
+
* @param options._scopeType @internal Don't touch, this is used in testing environments because the mock is subpar. Manually overrides worker scope type detection.
|
|
47
63
|
* @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.
|
|
48
|
-
*
|
|
64
|
+
*
|
|
49
65
|
* An example of defining a server:
|
|
50
66
|
* {@includeCode ../example/src/service-worker.ts}
|
|
51
67
|
*/
|
|
52
68
|
export function Server<Procedures extends ProceduresMap>(
|
|
53
69
|
procedures: Procedures,
|
|
54
|
-
{
|
|
70
|
+
{
|
|
71
|
+
loglevel = "debug",
|
|
72
|
+
scope,
|
|
73
|
+
_scopeType,
|
|
74
|
+
}: {
|
|
75
|
+
scope?: WorkerGlobalScope
|
|
76
|
+
loglevel?: LogLevel
|
|
77
|
+
_scopeType?: "dedicated" | "shared" | "service"
|
|
78
|
+
} = {}
|
|
55
79
|
): SwarpcServer<Procedures> {
|
|
56
80
|
const l = createLogger("server", loglevel)
|
|
57
81
|
|
|
82
|
+
// If scope is not provided, use the global scope
|
|
83
|
+
// This function is meant to be used in a worker, so `self` is a WorkerGlobalScope
|
|
84
|
+
scope ??= self as WorkerGlobalScope
|
|
85
|
+
|
|
86
|
+
function scopeIsShared(
|
|
87
|
+
scope: WorkerGlobalScope
|
|
88
|
+
): scope is SharedWorkerGlobalScope {
|
|
89
|
+
return scope instanceof SharedWorkerGlobalScope || _scopeType === "shared"
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function scopeIsDedicated(
|
|
93
|
+
scope: WorkerGlobalScope
|
|
94
|
+
): scope is DedicatedWorkerGlobalScope {
|
|
95
|
+
return (
|
|
96
|
+
scope instanceof DedicatedWorkerGlobalScope || _scopeType === "dedicated"
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function scopeIsService(
|
|
101
|
+
scope: WorkerGlobalScope
|
|
102
|
+
): scope is ServiceWorkerGlobalScope {
|
|
103
|
+
return scope instanceof ServiceWorkerGlobalScope || _scopeType === "service"
|
|
104
|
+
}
|
|
105
|
+
|
|
58
106
|
// Initialize the instance.
|
|
59
107
|
// Procedures and implementations are stored on properties with symbol keys,
|
|
60
108
|
// to avoid any conflicts with procedure names, and also discourage direct access to them.
|
|
61
109
|
const instance = {
|
|
62
110
|
[zProcedures]: procedures,
|
|
63
111
|
[zImplementations]: {} as ImplementationsMap<Procedures>,
|
|
64
|
-
start: (
|
|
112
|
+
start: async () => {},
|
|
65
113
|
} as SwarpcServer<Procedures>
|
|
66
114
|
|
|
67
115
|
// Set all implementation-setter methods
|
|
@@ -85,7 +133,16 @@ export function Server<Procedures extends ProceduresMap>(
|
|
|
85
133
|
}) as SwarpcServer<Procedures>[typeof functionName]
|
|
86
134
|
}
|
|
87
135
|
|
|
88
|
-
instance.start = (
|
|
136
|
+
instance.start = async () => {
|
|
137
|
+
const port = await new Promise<MessagePort | undefined>((resolve) => {
|
|
138
|
+
if (!scopeIsShared(scope)) return resolve(undefined)
|
|
139
|
+
console.log("Awaiting shared worker connection...")
|
|
140
|
+
scope.addEventListener("connect", ({ ports: [port] }) => {
|
|
141
|
+
console.log("Shared worker connected with port", port)
|
|
142
|
+
resolve(port)
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
89
146
|
// Used to post messages back to the client
|
|
90
147
|
const postMessage = async (
|
|
91
148
|
autotransfer: boolean,
|
|
@@ -93,17 +150,20 @@ export function Server<Procedures extends ProceduresMap>(
|
|
|
93
150
|
) => {
|
|
94
151
|
const transfer = autotransfer ? [] : findTransferables(data)
|
|
95
152
|
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
} else {
|
|
99
|
-
|
|
153
|
+
if (port) {
|
|
154
|
+
port.postMessage(data, { transfer })
|
|
155
|
+
} else if (scopeIsDedicated(scope)) {
|
|
156
|
+
scope.postMessage(data, { transfer })
|
|
157
|
+
} else if (scopeIsService(scope)) {
|
|
158
|
+
await scope.clients.matchAll().then((clients) => {
|
|
100
159
|
clients.forEach((client) => client.postMessage(data, { transfer }))
|
|
101
160
|
})
|
|
102
161
|
}
|
|
103
162
|
}
|
|
104
163
|
|
|
105
|
-
|
|
106
|
-
|
|
164
|
+
const listener = async (
|
|
165
|
+
event: MessageEvent<any> | ExtendableMessageEvent
|
|
166
|
+
): Promise<void> => {
|
|
107
167
|
// Decode the payload
|
|
108
168
|
const { requestId, functionName } = PayloadHeaderSchema(
|
|
109
169
|
type.enumerated(...Object.keys(procedures))
|
|
@@ -170,44 +230,57 @@ export function Server<Procedures extends ProceduresMap>(
|
|
|
170
230
|
return
|
|
171
231
|
}
|
|
172
232
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
)
|
|
185
|
-
// Send errors
|
|
186
|
-
.catch(async (error: any) => {
|
|
187
|
-
// Handle errors caused by abortions
|
|
188
|
-
if ("aborted" in error) {
|
|
189
|
-
l.debug(
|
|
190
|
-
requestId,
|
|
191
|
-
`Received abort error for ${functionName}`,
|
|
192
|
-
error.aborted
|
|
193
|
-
)
|
|
194
|
-
abortedRequests.add(requestId)
|
|
195
|
-
abortControllers.delete(requestId)
|
|
196
|
-
return
|
|
233
|
+
try {
|
|
234
|
+
// Call the implementation with the input and a progress callback
|
|
235
|
+
const result = await implementation(
|
|
236
|
+
payload.input,
|
|
237
|
+
async (progress: any) => {
|
|
238
|
+
l.debug(requestId, `Progress for ${functionName}`, progress)
|
|
239
|
+
await postMsg({ progress })
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
abortSignal: abortControllers.get(requestId)?.signal,
|
|
243
|
+
logger: createLogger("server", loglevel, requestId),
|
|
197
244
|
}
|
|
245
|
+
)
|
|
198
246
|
|
|
199
|
-
l.error(requestId, `Error in ${functionName}`, error)
|
|
200
|
-
await postError(error)
|
|
201
|
-
})
|
|
202
247
|
// Send results
|
|
203
|
-
.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
248
|
+
l.debug(requestId, `Result for ${functionName}`, result)
|
|
249
|
+
await postMsg({ result })
|
|
250
|
+
} catch (error: any) {
|
|
251
|
+
// Send errors
|
|
252
|
+
// Handle errors caused by abortions
|
|
253
|
+
if ("aborted" in error) {
|
|
254
|
+
l.debug(
|
|
255
|
+
requestId,
|
|
256
|
+
`Received abort error for ${functionName}`,
|
|
257
|
+
error.aborted
|
|
258
|
+
)
|
|
259
|
+
abortedRequests.add(requestId)
|
|
260
|
+
abortControllers.delete(requestId)
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
l.info(requestId, `Error in ${functionName}`, error)
|
|
265
|
+
await postError(error)
|
|
266
|
+
} finally {
|
|
267
|
+
abortedRequests.delete(requestId)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Listen for messages from the client
|
|
272
|
+
if (scopeIsShared(scope)) {
|
|
273
|
+
if (!port) throw new Error("SharedWorker port not initialized")
|
|
274
|
+
console.log("Listening for shared worker messages on port", port)
|
|
275
|
+
port.addEventListener("message", listener)
|
|
276
|
+
port.start()
|
|
277
|
+
} else if (scopeIsDedicated(scope)) {
|
|
278
|
+
scope.addEventListener("message", listener)
|
|
279
|
+
} else if (scopeIsService(scope)) {
|
|
280
|
+
scope.addEventListener("message", listener)
|
|
281
|
+
} else {
|
|
282
|
+
throw new Error(`Unsupported worker scope ${scope}`)
|
|
283
|
+
}
|
|
211
284
|
}
|
|
212
285
|
|
|
213
286
|
return instance
|
package/src/types.ts
CHANGED
|
@@ -51,7 +51,7 @@ export type CancelablePromise<T = unknown> = {
|
|
|
51
51
|
* Abort the request.
|
|
52
52
|
* @param reason The reason for cancelling the request.
|
|
53
53
|
*/
|
|
54
|
-
cancel: (reason: string) =>
|
|
54
|
+
cancel: (reason: string) => void
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/**
|
|
@@ -87,7 +87,7 @@ export type ProcedureImplementation<
|
|
|
87
87
|
|
|
88
88
|
/**
|
|
89
89
|
* Declarations of procedures by name.
|
|
90
|
-
*
|
|
90
|
+
*
|
|
91
91
|
* An example of declaring procedures:
|
|
92
92
|
* {@includeCode ../example/src/lib/procedures.ts}
|
|
93
93
|
*/
|