swarpc 0.1.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/package.json +30 -0
- package/src/swarp.js +161 -0
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "swarpc",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Full type-safe RPC library for service worker -- move things off of the UI thread with ease!",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"service-workers",
|
|
7
|
+
"sw",
|
|
8
|
+
"rpc",
|
|
9
|
+
"trpc",
|
|
10
|
+
"typesafe"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/gwennlbh/swarpc#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/gwennlbh/swarpc/issues"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/gwennlbh/swarpc.git"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"author": "Gwenn Le Bihan <gwenn.lebihan7@gmail.com>",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "src/swarpc.js",
|
|
24
|
+
"scripts": {
|
|
25
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"arktype": "^2.1.20"
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/swarp.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { type } from "arktype"
|
|
2
|
+
/**
|
|
3
|
+
* @import { Type } from 'arktype';
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @template {Type} I
|
|
8
|
+
* @template {Type} P
|
|
9
|
+
* @template {Type} S
|
|
10
|
+
* @typedef {Object} Procedure
|
|
11
|
+
* @property {I} input
|
|
12
|
+
* @property {P} progress
|
|
13
|
+
* @property {S} success
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @template {Type} I
|
|
18
|
+
* @template {Type} P
|
|
19
|
+
* @template {Type} S
|
|
20
|
+
* @typedef {(input: I['inferOut'], onProgress: (progress: P['inferOut']) => void) => Promise<NoInfer<S>['inferOut'] | NoInfer<S>['inferOut']>} ProcedureImplementation
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {Record<string, Procedure<any, any, any>>} ProceduresMap
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @template {ProceduresMap} Procedures
|
|
29
|
+
* @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 }} SwarpServer
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @template {ProceduresMap} Procedures
|
|
34
|
+
* @param {Procedures} procedures
|
|
35
|
+
* @returns {SwarpServer<Procedures>}
|
|
36
|
+
*/
|
|
37
|
+
export function Server(procedures) {
|
|
38
|
+
/** @type {SwarpServer<Procedures>} */
|
|
39
|
+
// @ts-expect-error
|
|
40
|
+
const instance = {
|
|
41
|
+
procedures,
|
|
42
|
+
implementations: {},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (const functionName in procedures) {
|
|
46
|
+
instance[functionName] = (
|
|
47
|
+
/**
|
|
48
|
+
* @type {ProcedureImplementation<ProceduresMap[typeof functionName]['input'], ProceduresMap[typeof functionName]['progress'], ProceduresMap[typeof functionName]['success']>}
|
|
49
|
+
*/
|
|
50
|
+
implementation
|
|
51
|
+
) => {
|
|
52
|
+
if (!instance.procedures[functionName]) {
|
|
53
|
+
throw new Error(`No procedure found for function name: ${functionName}`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
instance.implementations[functionName] = implementation
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const PayloadSchema = type.or(
|
|
61
|
+
...Object.entries(procedures).map(([functionName, { input }]) => ({
|
|
62
|
+
functionName: type(`"${functionName}"`),
|
|
63
|
+
input,
|
|
64
|
+
}))
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Starts the message event handler. Needs to be called within the Service Worker context.
|
|
69
|
+
* @param {Window} self
|
|
70
|
+
*/
|
|
71
|
+
instance.start = (self) => {
|
|
72
|
+
/**
|
|
73
|
+
* @param {{functionName: string} & Partial<{ result: any; error: any; progress: any }>} data
|
|
74
|
+
*/
|
|
75
|
+
const postMessage = async (data) => {
|
|
76
|
+
await self.clients
|
|
77
|
+
.matchAll()
|
|
78
|
+
.then((clients) =>
|
|
79
|
+
clients.forEach((client) => client.postMessage(data))
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.log("[SWARPC Server] Starting message listener on", self)
|
|
84
|
+
|
|
85
|
+
self.addEventListener("message", async (event) => {
|
|
86
|
+
const { functionName, input } = PayloadSchema.assert(event.data)
|
|
87
|
+
console.log("[SWARPC Server] Running", functionName, "with", input)
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {*} error
|
|
91
|
+
*/
|
|
92
|
+
const postError = async (error) =>
|
|
93
|
+
postMessage({
|
|
94
|
+
functionName,
|
|
95
|
+
error: {
|
|
96
|
+
message: "message" in error ? error.message : String(error),
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const implementation = instance.implementations[functionName]
|
|
101
|
+
if (!implementation) {
|
|
102
|
+
await postError("No implementation found")
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
await implementation(input, async (progress) =>
|
|
107
|
+
postMessage({ functionName, progress })
|
|
108
|
+
)
|
|
109
|
+
.catch(async (error) => postError(error))
|
|
110
|
+
.then(async (result) => postMessage({ functionName, result }))
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return instance
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* @template {Procedure<Type, Type, Type>} P
|
|
119
|
+
* @typedef {(input: P['input']['inferOut'], onProgress?: (progress: P['progress']['inferOut']) => void) => Promise<P['success']['inferOut']>} ClientMethod
|
|
120
|
+
*/
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @template {ProceduresMap} Procedures
|
|
124
|
+
* @typedef {{ procedures: Procedures } & { [F in keyof Procedures]: ClientMethod<Procedures[F]> }} SwarpClient
|
|
125
|
+
*/
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @template {ProceduresMap} Procedures
|
|
129
|
+
* @param {Procedures} procedures
|
|
130
|
+
* @returns {SwarpClient<Procedures>}
|
|
131
|
+
*/
|
|
132
|
+
export function Client(procedures) {
|
|
133
|
+
/** @type {SwarpClient<Procedures>} */
|
|
134
|
+
// @ts-expect-error
|
|
135
|
+
const instance = { procedures }
|
|
136
|
+
|
|
137
|
+
for (const functionName of Object.keys(procedures)) {
|
|
138
|
+
instance[functionName] = async (input, onProgress = () => {}) => {
|
|
139
|
+
procedures[functionName].input.assert(input)
|
|
140
|
+
console.log("[SWARPC Client] Calling", functionName, "with", input)
|
|
141
|
+
navigator.serviceWorker.controller?.postMessage({ functionName, input })
|
|
142
|
+
return new Promise((resolve, reject) => {
|
|
143
|
+
navigator.serviceWorker.addEventListener("message", (event) => {
|
|
144
|
+
const { functionName: fn, ...data } = event.data
|
|
145
|
+
|
|
146
|
+
if (fn !== functionName) return
|
|
147
|
+
|
|
148
|
+
if ("error" in data) {
|
|
149
|
+
reject(new Error(data.error.message))
|
|
150
|
+
} else if ("progress" in data) {
|
|
151
|
+
onProgress(data.progress)
|
|
152
|
+
} else if ("result" in data) {
|
|
153
|
+
resolve(data.result)
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return instance
|
|
161
|
+
}
|