swarpc 0.2.0 → 0.2.2
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/swarpc.d.ts +5 -20
- package/dist/swarpc.d.ts.map +1 -1
- package/dist/swarpc.js +120 -0
- package/dist/{typings.d.ts → types.d.ts} +13 -7
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +3 -3
- package/src/swarpc.ts +175 -0
- package/src/types.ts +49 -0
- package/dist/typings.d.ts.map +0 -1
- package/src/swarpc.js +0 -215
- package/src/typings.js +0 -42
package/dist/swarpc.d.ts
CHANGED
|
@@ -1,23 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* @param {Procedures} procedures
|
|
7
|
-
* @returns {SwarpcServer<Procedures>}
|
|
8
|
-
*/
|
|
9
|
-
export function Server<Procedures extends ProceduresMap>(procedures: Procedures): SwarpcServer<Procedures>;
|
|
10
|
-
/**
|
|
11
|
-
* @template {ProceduresMap} Procedures
|
|
12
|
-
* @param {Procedures} procedures
|
|
13
|
-
* @param {object} [options]
|
|
14
|
-
* @param {Worker} [options.worker] the worker instance to connect to -- uses the navigator's service worker if not provided
|
|
15
|
-
* @returns {SwarpcClient<Procedures>}
|
|
16
|
-
*/
|
|
17
|
-
export function Client<Procedures extends ProceduresMap>(procedures: Procedures, { worker }?: {
|
|
1
|
+
import type { ProceduresMap, SwarpcClient, SwarpcServer } from "./types";
|
|
2
|
+
export declare function Server<Procedures extends ProceduresMap>(procedures: Procedures, { worker }?: {
|
|
3
|
+
worker?: Worker;
|
|
4
|
+
}): SwarpcServer<Procedures>;
|
|
5
|
+
export declare function Client<Procedures extends ProceduresMap>(procedures: Procedures, { worker }?: {
|
|
18
6
|
worker?: Worker;
|
|
19
7
|
}): SwarpcClient<Procedures>;
|
|
20
|
-
import type { ProceduresMap } from './typings.js';
|
|
21
|
-
import type { SwarpcServer } from './typings.js';
|
|
22
|
-
import type { SwarpcClient } from './typings.js';
|
|
23
8
|
//# sourceMappingURL=swarpc.d.ts.map
|
package/dist/swarpc.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"swarpc.d.ts","sourceRoot":"","sources":["../src/swarpc.
|
|
1
|
+
{"version":3,"file":"swarpc.d.ts","sourceRoot":"","sources":["../src/swarpc.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,aAAa,EACb,YAAY,EACZ,YAAY,EAEb,MAAM,SAAS,CAAA;AAEhB,wBAAgB,MAAM,CAAC,UAAU,SAAS,aAAa,EACrD,UAAU,EAAE,UAAU,EACtB,EAAE,MAAM,EAAE,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,GACnC,YAAY,CAAC,UAAU,CAAC,CAyE1B;AAwDD,wBAAgB,MAAM,CAAC,UAAU,SAAS,aAAa,EACrD,UAAU,EAAE,UAAU,EACtB,EAAE,MAAM,EAAE,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,GACnC,YAAY,CAAC,UAAU,CAAC,CAiC1B"}
|
package/dist/swarpc.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { type } from "arktype";
|
|
2
|
+
export function Server(procedures, { worker } = {}) {
|
|
3
|
+
const instance = {
|
|
4
|
+
procedures,
|
|
5
|
+
implementations: {},
|
|
6
|
+
start: () => { },
|
|
7
|
+
};
|
|
8
|
+
for (const functionName in procedures) {
|
|
9
|
+
instance[functionName] = ((implementation) => {
|
|
10
|
+
if (!instance.procedures[functionName]) {
|
|
11
|
+
throw new Error(`No procedure found for function name: ${functionName}`);
|
|
12
|
+
}
|
|
13
|
+
instance.implementations[functionName] = implementation;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
const PayloadSchema = type.or(...Object.entries(procedures).map(([functionName, { input }]) => ({
|
|
17
|
+
functionName: type(`"${functionName}"`),
|
|
18
|
+
requestId: type("string >= 1"),
|
|
19
|
+
input,
|
|
20
|
+
})));
|
|
21
|
+
instance.start = (self) => {
|
|
22
|
+
const postMessage = async (data) => {
|
|
23
|
+
if (worker) {
|
|
24
|
+
self.postMessage(data);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
await self.clients.matchAll().then((clients) => {
|
|
28
|
+
clients.forEach((client) => client.postMessage(data));
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
self.addEventListener("message", async (event) => {
|
|
33
|
+
const { functionName, requestId, input } = PayloadSchema.assert(event.data);
|
|
34
|
+
const postError = async (error) => postMessage({
|
|
35
|
+
functionName,
|
|
36
|
+
requestId,
|
|
37
|
+
error: {
|
|
38
|
+
message: "message" in error ? error.message : String(error),
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
const implementation = instance.implementations[functionName];
|
|
42
|
+
if (!implementation) {
|
|
43
|
+
await postError("No implementation found");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
await implementation(input, async (progress) => {
|
|
47
|
+
await postMessage({ functionName, requestId, progress });
|
|
48
|
+
})
|
|
49
|
+
.catch(async (error) => {
|
|
50
|
+
await postError(error);
|
|
51
|
+
})
|
|
52
|
+
.then(async (result) => {
|
|
53
|
+
await postMessage({ functionName, requestId, result });
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
return instance;
|
|
58
|
+
}
|
|
59
|
+
function generateRequestId() {
|
|
60
|
+
return Math.random().toString(36).substring(2, 15);
|
|
61
|
+
}
|
|
62
|
+
const pendingRequests = new Map();
|
|
63
|
+
let _clientListenerStarted = false;
|
|
64
|
+
async function startClientListener(worker) {
|
|
65
|
+
if (_clientListenerStarted)
|
|
66
|
+
return;
|
|
67
|
+
if (!worker) {
|
|
68
|
+
const sw = await navigator.serviceWorker.ready;
|
|
69
|
+
if (!sw?.active) {
|
|
70
|
+
throw new Error("[SWARPC Client] Service Worker is not active");
|
|
71
|
+
}
|
|
72
|
+
if (!navigator.serviceWorker.controller) {
|
|
73
|
+
console.warn("[SWARPC Client] Service Worker is not controlling the page");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const w = worker ?? navigator.serviceWorker;
|
|
77
|
+
w.addEventListener("message", (event) => {
|
|
78
|
+
const { functionName, requestId, ...data } = event.data || {};
|
|
79
|
+
if (!requestId) {
|
|
80
|
+
throw new Error("[SWARPC Client] Message received without requestId");
|
|
81
|
+
}
|
|
82
|
+
const handlers = pendingRequests.get(requestId);
|
|
83
|
+
if (!handlers) {
|
|
84
|
+
throw new Error(`[SWARPC Client] ${requestId} has no active request handlers`);
|
|
85
|
+
}
|
|
86
|
+
if ("error" in data) {
|
|
87
|
+
handlers.reject(new Error(data.error.message));
|
|
88
|
+
pendingRequests.delete(requestId);
|
|
89
|
+
}
|
|
90
|
+
else if ("progress" in data) {
|
|
91
|
+
handlers.onProgress(data.progress);
|
|
92
|
+
}
|
|
93
|
+
else if ("result" in data) {
|
|
94
|
+
handlers.resolve(data.result);
|
|
95
|
+
pendingRequests.delete(requestId);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
_clientListenerStarted = true;
|
|
99
|
+
}
|
|
100
|
+
export function Client(procedures, { worker } = {}) {
|
|
101
|
+
const instance = { procedures };
|
|
102
|
+
for (const functionName of Object.keys(procedures)) {
|
|
103
|
+
instance[functionName] = (async (input, onProgress = () => { }) => {
|
|
104
|
+
procedures[functionName].input.assert(input);
|
|
105
|
+
await startClientListener(worker);
|
|
106
|
+
const w = worker ?? (await navigator.serviceWorker.ready.then((r) => r.active));
|
|
107
|
+
if (!w) {
|
|
108
|
+
throw new Error("[SWARPC Client] No active service worker found");
|
|
109
|
+
}
|
|
110
|
+
return new Promise((resolve, reject) => {
|
|
111
|
+
if (!worker && !navigator.serviceWorker.controller)
|
|
112
|
+
console.warn("[SWARPC Client] Service Worker is not controlling the page");
|
|
113
|
+
const requestId = generateRequestId();
|
|
114
|
+
pendingRequests.set(requestId, { resolve, onProgress, reject });
|
|
115
|
+
w.postMessage({ functionName, input, requestId });
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return instance;
|
|
120
|
+
}
|
|
@@ -1,18 +1,24 @@
|
|
|
1
|
+
import type { Type } from "arktype";
|
|
1
2
|
export type Procedure<I extends Type, P extends Type, S extends Type> = {
|
|
2
3
|
input: I;
|
|
3
4
|
progress: P;
|
|
4
5
|
success: S;
|
|
5
6
|
};
|
|
6
|
-
export type ProcedureImplementation<I extends Type, P extends Type, S extends Type> = (input: I["inferOut"], onProgress: (progress: P["inferOut"]) => void) => Promise<
|
|
7
|
+
export type ProcedureImplementation<I extends Type, P extends Type, S extends Type> = (input: I["inferOut"], onProgress: (progress: P["inferOut"]) => void) => Promise<S["inferOut"]>;
|
|
7
8
|
export type ProceduresMap = Record<string, Procedure<Type, Type, Type>>;
|
|
8
9
|
export type ClientMethod<P extends Procedure<Type, Type, Type>> = (input: P["input"]["inferIn"], onProgress?: (progress: P["progress"]["inferOut"]) => void) => Promise<P["success"]["inferOut"]>;
|
|
9
10
|
export type SwarpcClient<Procedures extends ProceduresMap> = {
|
|
10
11
|
procedures: Procedures;
|
|
11
|
-
} & {
|
|
12
|
+
} & {
|
|
13
|
+
[F in keyof Procedures]: ClientMethod<Procedures[F]>;
|
|
14
|
+
};
|
|
12
15
|
export type SwarpcServer<Procedures extends ProceduresMap> = {
|
|
13
16
|
procedures: Procedures;
|
|
14
|
-
implementations: {
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
implementations: {
|
|
18
|
+
[F in keyof Procedures]: ProcedureImplementation<Procedures[F]["input"], Procedures[F]["progress"], Procedures[F]["success"]>;
|
|
19
|
+
};
|
|
20
|
+
start(self: Window): void;
|
|
21
|
+
} & {
|
|
22
|
+
[F in keyof Procedures]: (impl: ProcedureImplementation<Procedures[F]["input"], Procedures[F]["progress"], Procedures[F]["success"]>) => void;
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAEpC,MAAM,MAAM,SAAS,CAAC,CAAC,SAAS,IAAI,EAAE,CAAC,SAAS,IAAI,EAAE,CAAC,SAAS,IAAI,IAAI;IACtE,KAAK,EAAE,CAAC,CAAC;IACT,QAAQ,EAAE,CAAC,CAAC;IACZ,OAAO,EAAE,CAAC,CAAC;CACZ,CAAC;AAEF,MAAM,MAAM,uBAAuB,CACjC,CAAC,SAAS,IAAI,EACd,CAAC,SAAS,IAAI,EACd,CAAC,SAAS,IAAI,IACZ,CACF,KAAK,EAAE,CAAC,CAAC,UAAU,CAAC,EACpB,UAAU,EAAE,CAAC,QAAQ,EAAE,CAAC,CAAC,UAAU,CAAC,KAAK,IAAI,KAC1C,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC;AAE5B,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;AAExE,MAAM,MAAM,YAAY,CAAC,CAAC,SAAS,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAChE,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,EAC5B,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC,UAAU,CAAC,KAAK,IAAI,KACvD,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC;AAEvC,MAAM,MAAM,YAAY,CAAC,UAAU,SAAS,aAAa,IAAI;IAC3D,UAAU,EAAE,UAAU,CAAC;CACxB,GAAG;KACD,CAAC,IAAI,MAAM,UAAU,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;CACrD,CAAC;AAEF,MAAM,MAAM,YAAY,CAAC,UAAU,SAAS,aAAa,IAAI;IAC3D,UAAU,EAAE,UAAU,CAAC;IACvB,eAAe,EAAE;SACd,CAAC,IAAI,MAAM,UAAU,GAAG,uBAAuB,CAC9C,UAAU,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EACtB,UAAU,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,EACzB,UAAU,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CACzB;KACF,CAAC;IACF,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "swarpc",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
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",
|
|
@@ -24,12 +24,12 @@
|
|
|
24
24
|
"dist",
|
|
25
25
|
"src"
|
|
26
26
|
],
|
|
27
|
-
"main": "
|
|
27
|
+
"main": "dist/swarpc.js",
|
|
28
28
|
"types": "dist/swarpc.d.ts",
|
|
29
29
|
"scripts": {
|
|
30
30
|
"build": "tsc",
|
|
31
31
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
32
|
-
"typedoc": "typedoc src/swarpc.
|
|
32
|
+
"typedoc": "typedoc src/swarpc.ts src/types.ts --readme README.md"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"arktype": "^2.1.20"
|
package/src/swarpc.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { type } from "arktype"
|
|
2
|
+
import type { ProceduresMap, SwarpcClient, SwarpcServer } from "./types.js"
|
|
3
|
+
|
|
4
|
+
export function Server<Procedures extends ProceduresMap>(
|
|
5
|
+
procedures: Procedures,
|
|
6
|
+
{ worker }: { worker?: Worker } = {}
|
|
7
|
+
): SwarpcServer<Procedures> {
|
|
8
|
+
const instance = {
|
|
9
|
+
procedures,
|
|
10
|
+
implementations: {} as SwarpcServer<Procedures>["implementations"],
|
|
11
|
+
start: () => {},
|
|
12
|
+
} as SwarpcServer<Procedures>
|
|
13
|
+
|
|
14
|
+
for (const functionName in procedures) {
|
|
15
|
+
instance[functionName] = ((implementation) => {
|
|
16
|
+
if (!instance.procedures[functionName]) {
|
|
17
|
+
throw new Error(`No procedure found for function name: ${functionName}`)
|
|
18
|
+
}
|
|
19
|
+
instance.implementations[functionName] = implementation as any
|
|
20
|
+
}) as SwarpcServer<Procedures>[typeof functionName]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const PayloadSchema = type.or(
|
|
24
|
+
...Object.entries(procedures).map(([functionName, { input }]) => ({
|
|
25
|
+
functionName: type(`"${functionName}"`),
|
|
26
|
+
requestId: type("string >= 1"),
|
|
27
|
+
input,
|
|
28
|
+
}))
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
instance.start = (self: Window) => {
|
|
32
|
+
const postMessage = async (
|
|
33
|
+
data: { functionName: string; requestId: string } & Partial<{
|
|
34
|
+
result: any
|
|
35
|
+
error: any
|
|
36
|
+
progress: any
|
|
37
|
+
}>
|
|
38
|
+
) => {
|
|
39
|
+
if (worker) {
|
|
40
|
+
self.postMessage(data)
|
|
41
|
+
} else {
|
|
42
|
+
await (self as any).clients.matchAll().then((clients: any[]) => {
|
|
43
|
+
clients.forEach((client) => client.postMessage(data))
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
self.addEventListener("message", async (event: MessageEvent) => {
|
|
49
|
+
const { functionName, requestId, input } = PayloadSchema.assert(
|
|
50
|
+
event.data
|
|
51
|
+
)
|
|
52
|
+
const postError = async (error: any) =>
|
|
53
|
+
postMessage({
|
|
54
|
+
functionName,
|
|
55
|
+
requestId,
|
|
56
|
+
error: {
|
|
57
|
+
message: "message" in error ? error.message : String(error),
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const implementation = instance.implementations[functionName]
|
|
62
|
+
if (!implementation) {
|
|
63
|
+
await postError("No implementation found")
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
await implementation(input, async (progress: any) => {
|
|
68
|
+
await postMessage({ functionName, requestId, progress })
|
|
69
|
+
})
|
|
70
|
+
.catch(async (error: any) => {
|
|
71
|
+
await postError(error)
|
|
72
|
+
})
|
|
73
|
+
.then(async (result: any) => {
|
|
74
|
+
await postMessage({ functionName, requestId, result })
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return instance
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function generateRequestId(): string {
|
|
83
|
+
return Math.random().toString(36).substring(2, 15)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
type PendingRequest = {
|
|
87
|
+
reject: (err: Error) => void
|
|
88
|
+
onProgress: (progress: any) => void
|
|
89
|
+
resolve: (result: any) => void
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const pendingRequests = new Map<string, PendingRequest>()
|
|
93
|
+
|
|
94
|
+
let _clientListenerStarted = false
|
|
95
|
+
async function startClientListener(worker?: Worker) {
|
|
96
|
+
if (_clientListenerStarted) return
|
|
97
|
+
|
|
98
|
+
if (!worker) {
|
|
99
|
+
const sw = await navigator.serviceWorker.ready
|
|
100
|
+
if (!sw?.active) {
|
|
101
|
+
throw new Error("[SWARPC Client] Service Worker is not active")
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!navigator.serviceWorker.controller) {
|
|
105
|
+
console.warn("[SWARPC Client] Service Worker is not controlling the page")
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const w = worker ?? navigator.serviceWorker
|
|
110
|
+
w.addEventListener("message", (event) => {
|
|
111
|
+
const { functionName, requestId, ...data } =
|
|
112
|
+
(event as MessageEvent).data || {}
|
|
113
|
+
|
|
114
|
+
if (!requestId) {
|
|
115
|
+
throw new Error("[SWARPC Client] Message received without requestId")
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const handlers = pendingRequests.get(requestId)
|
|
119
|
+
if (!handlers) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`[SWARPC Client] ${requestId} has no active request handlers`
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if ("error" in data) {
|
|
126
|
+
handlers.reject(new Error(data.error.message))
|
|
127
|
+
pendingRequests.delete(requestId)
|
|
128
|
+
} else if ("progress" in data) {
|
|
129
|
+
handlers.onProgress(data.progress)
|
|
130
|
+
} else if ("result" in data) {
|
|
131
|
+
handlers.resolve(data.result)
|
|
132
|
+
pendingRequests.delete(requestId)
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
_clientListenerStarted = true
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function Client<Procedures extends ProceduresMap>(
|
|
140
|
+
procedures: Procedures,
|
|
141
|
+
{ worker }: { worker?: Worker } = {}
|
|
142
|
+
): SwarpcClient<Procedures> {
|
|
143
|
+
const instance = { procedures } as Partial<SwarpcClient<Procedures>>
|
|
144
|
+
|
|
145
|
+
for (const functionName of Object.keys(procedures) as Array<
|
|
146
|
+
keyof Procedures
|
|
147
|
+
>) {
|
|
148
|
+
instance[functionName] = (async (input: unknown, onProgress = () => {}) => {
|
|
149
|
+
procedures[functionName].input.assert(input)
|
|
150
|
+
await startClientListener(worker)
|
|
151
|
+
|
|
152
|
+
const w =
|
|
153
|
+
worker ?? (await navigator.serviceWorker.ready.then((r) => r.active))
|
|
154
|
+
|
|
155
|
+
if (!w) {
|
|
156
|
+
throw new Error("[SWARPC Client] No active service worker found")
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return new Promise((resolve, reject) => {
|
|
160
|
+
if (!worker && !navigator.serviceWorker.controller)
|
|
161
|
+
console.warn(
|
|
162
|
+
"[SWARPC Client] Service Worker is not controlling the page"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
const requestId = generateRequestId()
|
|
166
|
+
|
|
167
|
+
pendingRequests.set(requestId, { resolve, onProgress, reject })
|
|
168
|
+
|
|
169
|
+
w.postMessage({ functionName, input, requestId })
|
|
170
|
+
})
|
|
171
|
+
}) as SwarpcClient<Procedures>[typeof functionName]
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return instance as SwarpcClient<Procedures>
|
|
175
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Type } from "arktype";
|
|
2
|
+
|
|
3
|
+
export type Procedure<I extends Type, P extends Type, S extends Type> = {
|
|
4
|
+
input: I;
|
|
5
|
+
progress: P;
|
|
6
|
+
success: S;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type ProcedureImplementation<
|
|
10
|
+
I extends Type,
|
|
11
|
+
P extends Type,
|
|
12
|
+
S extends Type
|
|
13
|
+
> = (
|
|
14
|
+
input: I["inferOut"],
|
|
15
|
+
onProgress: (progress: P["inferOut"]) => void
|
|
16
|
+
) => Promise<S["inferOut"]>;
|
|
17
|
+
|
|
18
|
+
export type ProceduresMap = Record<string, Procedure<Type, Type, Type>>;
|
|
19
|
+
|
|
20
|
+
export type ClientMethod<P extends Procedure<Type, Type, Type>> = (
|
|
21
|
+
input: P["input"]["inferIn"],
|
|
22
|
+
onProgress?: (progress: P["progress"]["inferOut"]) => void
|
|
23
|
+
) => Promise<P["success"]["inferOut"]>;
|
|
24
|
+
|
|
25
|
+
export type SwarpcClient<Procedures extends ProceduresMap> = {
|
|
26
|
+
procedures: Procedures;
|
|
27
|
+
} & {
|
|
28
|
+
[F in keyof Procedures]: ClientMethod<Procedures[F]>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type SwarpcServer<Procedures extends ProceduresMap> = {
|
|
32
|
+
procedures: Procedures;
|
|
33
|
+
implementations: {
|
|
34
|
+
[F in keyof Procedures]: ProcedureImplementation<
|
|
35
|
+
Procedures[F]["input"],
|
|
36
|
+
Procedures[F]["progress"],
|
|
37
|
+
Procedures[F]["success"]
|
|
38
|
+
>;
|
|
39
|
+
};
|
|
40
|
+
start(self: Window): void;
|
|
41
|
+
} & {
|
|
42
|
+
[F in keyof Procedures]: (
|
|
43
|
+
impl: ProcedureImplementation<
|
|
44
|
+
Procedures[F]["input"],
|
|
45
|
+
Procedures[F]["progress"],
|
|
46
|
+
Procedures[F]["success"]
|
|
47
|
+
>
|
|
48
|
+
) => void;
|
|
49
|
+
};
|
package/dist/typings.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"typings.d.ts","sourceRoot":"","sources":["../src/typings.js"],"names":[],"mappings":"sBAKoB,CAAC,SAAR,IAAM,EACC,CAAC,SAAR,IAAM,EACC,CAAC,SAAR,IAAM;WAEL,CAAC;cACD,CAAC;aACD,CAAC;;oCAIK,CAAC,SAAR,IAAM,EACC,CAAC,SAAR,IAAM,EACC,CAAC,SAAR,IAAM,IACN,CAAC,KAAK,EAAE,CAAC,CAAC,UAAU,CAAC,EAAE,UAAU,EAAE,CAAC,QAAQ,EAAE,CAAC,CAAC,UAAU,CAAC,KAAK,IAAI,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;4BAIjI,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;yBAIb,CAAC,SAA9B,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAE,IAC7B,CAAC,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,EAAE,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC,UAAU,CAAC,KAAK,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,UAAU,CAAC,CAAC;yBAI/G,UAAU,SAAzB,aAAc,IACf;IAAE,UAAU,EAAE,UAAU,CAAA;CAAE,GAAG,GAAG,CAAC,IAAI,MAAM,UAAU,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,GAAE;yBAIrE,UAAU,SAAzB,aAAc,IACf;IAAE,UAAU,EAAE,UAAU,CAAC;IAAC,eAAe,EAAE,GAAE,CAAC,IAAI,MAAM,UAAU,GAAG,uBAAuB,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,GAAE,CAAC;IAAC,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CAAG,GAAG,GAAG,CAAC,IAAI,MAAM,UAAU,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,uBAAuB,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAE,CAAC,KAAK,IAAI,GAAE;0BApC/V,SAAS"}
|
package/src/swarpc.js
DELETED
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
import { type } from "arktype"
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @import { ProceduresMap, SwarpcClient, SwarpcServer } from './typings.js'
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* @template {ProceduresMap} Procedures
|
|
9
|
-
* @param {Procedures} procedures
|
|
10
|
-
* @param {object} [options]
|
|
11
|
-
* @param {Worker} [options.worker] the worker instance to connect to -- uses the navigator's service worker if not provided
|
|
12
|
-
* @returns {SwarpcServer<Procedures>}
|
|
13
|
-
*/
|
|
14
|
-
export function Server(procedures, { worker } = { worker: undefined }) {
|
|
15
|
-
/** @type {SwarpcServer<Procedures>} */
|
|
16
|
-
// @ts-expect-error
|
|
17
|
-
const instance = {
|
|
18
|
-
procedures,
|
|
19
|
-
implementations: {},
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
for (const functionName in procedures) {
|
|
23
|
-
instance[functionName] = (
|
|
24
|
-
/**
|
|
25
|
-
* @type {ProcedureImplementation<ProceduresMap[typeof functionName]['input'], ProceduresMap[typeof functionName]['progress'], ProceduresMap[typeof functionName]['success']>}
|
|
26
|
-
*/
|
|
27
|
-
implementation
|
|
28
|
-
) => {
|
|
29
|
-
if (!instance.procedures[functionName]) {
|
|
30
|
-
throw new Error(`No procedure found for function name: ${functionName}`)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
instance.implementations[functionName] = implementation
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const PayloadSchema = type.or(
|
|
38
|
-
...Object.entries(procedures).map(([functionName, { input }]) => ({
|
|
39
|
-
functionName: type(`"${functionName}"`),
|
|
40
|
-
requestId: type("string >= 1"),
|
|
41
|
-
input,
|
|
42
|
-
}))
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Starts the message event handler. Needs to be called within the Service Worker context.
|
|
47
|
-
* @param {Window} self
|
|
48
|
-
*/
|
|
49
|
-
instance.start = (self) => {
|
|
50
|
-
/**
|
|
51
|
-
* @param {{functionName: string} & Partial<{ result: any; error: any; progress: any }>} data
|
|
52
|
-
*/
|
|
53
|
-
const postMessage = async (data) => {
|
|
54
|
-
if (worker) {
|
|
55
|
-
self.postMessage(data)
|
|
56
|
-
} else {
|
|
57
|
-
await self.clients.matchAll().then((clients) => {
|
|
58
|
-
console.debug(`[SWARPC Server] Posting message to clients`, clients)
|
|
59
|
-
clients.forEach((client) => client.postMessage(data))
|
|
60
|
-
})
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
console.log("[SWARPC Server] Starting message listener on", self)
|
|
65
|
-
|
|
66
|
-
self.addEventListener("message", async (event) => {
|
|
67
|
-
const { functionName, requestId, input } = PayloadSchema.assert(
|
|
68
|
-
event.data
|
|
69
|
-
)
|
|
70
|
-
console.log(
|
|
71
|
-
`[SWARPC Server] ${requestId} Running ${functionName} with`,
|
|
72
|
-
input
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* @param {*} error
|
|
77
|
-
*/
|
|
78
|
-
const postError = async (error) =>
|
|
79
|
-
postMessage({
|
|
80
|
-
functionName,
|
|
81
|
-
requestId,
|
|
82
|
-
error: {
|
|
83
|
-
message: "message" in error ? error.message : String(error),
|
|
84
|
-
},
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
const implementation = instance.implementations[functionName]
|
|
88
|
-
if (!implementation) {
|
|
89
|
-
await postError("No implementation found")
|
|
90
|
-
return
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
await implementation(input, async (progress) => {
|
|
94
|
-
console.debug(
|
|
95
|
-
`[SWARPC Server] ${requestId} Progress for ${functionName}:`,
|
|
96
|
-
progress
|
|
97
|
-
)
|
|
98
|
-
await postMessage({ functionName, requestId, progress })
|
|
99
|
-
})
|
|
100
|
-
.catch(async (error) => {
|
|
101
|
-
console.debug(
|
|
102
|
-
`[SWARPC Server] ${requestId} Error in ${functionName}:`,
|
|
103
|
-
error
|
|
104
|
-
)
|
|
105
|
-
await postError(error)
|
|
106
|
-
})
|
|
107
|
-
.then(async (result) => {
|
|
108
|
-
console.debug(
|
|
109
|
-
`[SWARPC Server] ${requestId} Result for ${functionName}:`,
|
|
110
|
-
result
|
|
111
|
-
)
|
|
112
|
-
await postMessage({ functionName, requestId, result })
|
|
113
|
-
})
|
|
114
|
-
})
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return instance
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function generateRequestId() {
|
|
121
|
-
return Math.random().toString(36).substring(2, 15)
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* @type {Map<string, { reject: (err: Error) => void; onProgress: (progress: any) => void; resolve: (result: any) => void }>}
|
|
126
|
-
*/
|
|
127
|
-
const pendingRequests = new Map()
|
|
128
|
-
|
|
129
|
-
let _clientListenerStarted = false
|
|
130
|
-
/**
|
|
131
|
-
* @param {Worker} [worker] the worker instance to connect to -- uses the navigator's service worker if not provided
|
|
132
|
-
*/
|
|
133
|
-
async function startClientListener(worker) {
|
|
134
|
-
if (_clientListenerStarted) return
|
|
135
|
-
|
|
136
|
-
if (!worker) {
|
|
137
|
-
const sw = await navigator.serviceWorker.ready
|
|
138
|
-
if (!sw?.active) {
|
|
139
|
-
throw new Error("[SWARPC Client] Service Worker is not active")
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (!navigator.serviceWorker.controller) {
|
|
143
|
-
console.warn("[SWARPC Client] Service Worker is not controlling the page")
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const w = worker ?? navigator.serviceWorker
|
|
148
|
-
console.debug("[SWARPC Client] Registering message listener for client", w)
|
|
149
|
-
|
|
150
|
-
w.addEventListener("message", (event) => {
|
|
151
|
-
const { functionName, requestId, ...data } = event.data || {}
|
|
152
|
-
if (!requestId) {
|
|
153
|
-
throw new Error("[SWARPC Client] Message received without requestId")
|
|
154
|
-
}
|
|
155
|
-
const handlers = pendingRequests.get(requestId)
|
|
156
|
-
if (!handlers) {
|
|
157
|
-
throw new Error(
|
|
158
|
-
`[SWARPC Client] ${requestId} has no active request handlers`
|
|
159
|
-
)
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if ("error" in data) {
|
|
163
|
-
handlers.reject(new Error(data.error.message))
|
|
164
|
-
pendingRequests.delete(requestId)
|
|
165
|
-
} else if ("progress" in data) {
|
|
166
|
-
handlers.onProgress(data.progress)
|
|
167
|
-
} else if ("result" in data) {
|
|
168
|
-
handlers.resolve(data.result)
|
|
169
|
-
pendingRequests.delete(requestId)
|
|
170
|
-
}
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
_clientListenerStarted = true
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* @template {ProceduresMap} Procedures
|
|
178
|
-
* @param {Procedures} procedures
|
|
179
|
-
* @param {object} [options]
|
|
180
|
-
* @param {Worker} [options.worker] the worker instance to connect to -- uses the navigator's service worker if not provided
|
|
181
|
-
* @returns {SwarpcClient<Procedures>}
|
|
182
|
-
*/
|
|
183
|
-
export function Client(procedures, { worker } = { worker: undefined }) {
|
|
184
|
-
/** @type {SwarpcClient<Procedures>} */
|
|
185
|
-
// @ts-expect-error
|
|
186
|
-
const instance = { procedures }
|
|
187
|
-
|
|
188
|
-
for (const functionName of Object.keys(procedures)) {
|
|
189
|
-
instance[functionName] = async (input, onProgress = () => {}) => {
|
|
190
|
-
procedures[functionName].input.assert(input)
|
|
191
|
-
await startClientListener(worker)
|
|
192
|
-
|
|
193
|
-
const w =
|
|
194
|
-
worker ?? (await navigator.serviceWorker.ready.then((r) => r.active))
|
|
195
|
-
return new Promise((resolve, reject) => {
|
|
196
|
-
if (!worker && !navigator.serviceWorker.controller)
|
|
197
|
-
console.warn(
|
|
198
|
-
"[SWARPC Client] Service Worker is not controlling the page"
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
const requestId = generateRequestId()
|
|
202
|
-
|
|
203
|
-
pendingRequests.set(requestId, { resolve, onProgress, reject })
|
|
204
|
-
|
|
205
|
-
console.log(
|
|
206
|
-
`[SWARPC Client] ${requestId} Requesting ${functionName} with`,
|
|
207
|
-
input
|
|
208
|
-
)
|
|
209
|
-
w.postMessage({ functionName, input, requestId })
|
|
210
|
-
})
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return instance
|
|
215
|
-
}
|
package/src/typings.js
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @import { Type } from 'arktype';
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* @template {Type} I
|
|
7
|
-
* @template {Type} P
|
|
8
|
-
* @template {Type} S
|
|
9
|
-
* @typedef {Object} Procedure
|
|
10
|
-
* @property {I} input
|
|
11
|
-
* @property {P} progress
|
|
12
|
-
* @property {S} success
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* @template {Type} I
|
|
17
|
-
* @template {Type} P
|
|
18
|
-
* @template {Type} S
|
|
19
|
-
* @typedef {(input: I['inferOut'], onProgress: (progress: P['inferOut']) => void) => Promise<NoInfer<S>['inferOut'] | NoInfer<S>['inferOut']>} ProcedureImplementation
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* @typedef {Record<string, Procedure<Type, Type, Type>>} ProceduresMap
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* @template {Procedure<Type, Type, Type>} P
|
|
28
|
-
* @typedef {(input: P['input']['inferIn'], onProgress?: (progress: P['progress']['inferOut']) => void) => Promise<P['success']['inferOut']>} ClientMethod
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* @template {ProceduresMap} Procedures
|
|
33
|
-
* @typedef {{ procedures: Procedures } & { [F in keyof Procedures]: ClientMethod<Procedures[F]> }} SwarpcClient
|
|
34
|
-
*/
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* @template {ProceduresMap} Procedures
|
|
38
|
-
* @typedef {{ procedures: Procedures, implementations: {[F in keyof Procedures]: ProcedureImplementation<Procedures[F]['input'], Procedures[F]['progress'], Procedures[F]['success']> }, start: (self: Window) => void } & { [F in keyof Procedures]: (impl: NoInfer<ProcedureImplementation<Procedures[F]['input'], Procedures[F]['progress'], Procedures[F]['success'] >>) => void }} SwarpcServer
|
|
39
|
-
*/
|
|
40
|
-
|
|
41
|
-
// Required, otherwise nothing can be imported from this file
|
|
42
|
-
export {}
|