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.
Files changed (2) hide show
  1. package/package.json +30 -0
  2. 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
+ }