swarpc 0.1.2 → 0.2.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/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  <div align=center>
2
- <h1>sw&rpc</h1>
2
+ <h1>
3
+ <img src="./logo.svg" alt="sw&rpc" />
4
+ </h1>
3
5
 
4
6
  RPC for Service Workers -- move that heavy computation off of your UI thread!
5
7
 
package/dist/swarpc.d.ts CHANGED
@@ -10,9 +10,13 @@ export function Server<Procedures extends ProceduresMap>(procedures: Procedures)
10
10
  /**
11
11
  * @template {ProceduresMap} Procedures
12
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
13
15
  * @returns {SwarpcClient<Procedures>}
14
16
  */
15
- export function Client<Procedures extends ProceduresMap>(procedures: Procedures): SwarpcClient<Procedures>;
17
+ export function Client<Procedures extends ProceduresMap>(procedures: Procedures, { worker }?: {
18
+ worker?: Worker;
19
+ }): SwarpcClient<Procedures>;
16
20
  import type { ProceduresMap } from './typings.js';
17
21
  import type { SwarpcServer } from './typings.js';
18
22
  import type { SwarpcClient } from './typings.js';
@@ -1 +1 @@
1
- {"version":3,"file":"swarpc.d.ts","sourceRoot":"","sources":["../src/swarpc.js"],"names":[],"mappings":"AAEA;;GAEG;AAEH;;;;GAIG;AACH,uBAJ6B,UAAU,SAA1B,aAAe,cACjB,UAAU,GACR,aAAa,UAAU,CAAC,CAgFpC;AAED;;;;GAIG;AACH,uBAJ6B,UAAU,SAA1B,aAAe,cACjB,UAAU,GACR,aAAa,UAAU,CAAC,CA+BpC;mCA1H6D,cAAc;kCAAd,cAAc;kCAAd,cAAc"}
1
+ {"version":3,"file":"swarpc.d.ts","sourceRoot":"","sources":["../src/swarpc.js"],"names":[],"mappings":"AAEA;;GAEG;AAEH;;;;GAIG;AACH,uBAJ6B,UAAU,SAA1B,aAAe,cACjB,UAAU,GACR,aAAa,UAAU,CAAC,CAsGpC;AA0DD;;;;;;GAMG;AACH,uBAN6B,UAAU,SAA1B,aAAe,cACjB,UAAU,eAElB;IAAyB,MAAM,GAAvB,MAAM;CACd,GAAU,aAAa,UAAU,CAAC,CAiCpC;mCA5M6D,cAAc;kCAAd,cAAc;kCAAd,cAAc"}
package/dist/typings.d.ts CHANGED
@@ -5,7 +5,7 @@ export type Procedure<I extends Type, P extends Type, S extends Type> = {
5
5
  };
6
6
  export type ProcedureImplementation<I extends Type, P extends Type, S extends Type> = (input: I["inferOut"], onProgress: (progress: P["inferOut"]) => void) => Promise<NoInfer<S>["inferOut"] | NoInfer<S>["inferOut"]>;
7
7
  export type ProceduresMap = Record<string, Procedure<Type, Type, Type>>;
8
- export type ClientMethod<P extends Procedure<Type, Type, Type>> = (input: P["input"]["inferOut"], onProgress?: (progress: P["progress"]["inferOut"]) => void) => Promise<P["success"]["inferOut"]>;
8
+ export type ClientMethod<P extends Procedure<Type, Type, Type>> = (input: P["input"]["inferIn"], onProgress?: (progress: P["progress"]["inferOut"]) => void) => Promise<P["success"]["inferOut"]>;
9
9
  export type SwarpcClient<Procedures extends ProceduresMap> = {
10
10
  procedures: Procedures;
11
11
  } & { [F in keyof Procedures]: ClientMethod<Procedures[F]>; };
@@ -1 +1 @@
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,UAAU,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;yBAIhH,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"}
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swarpc",
3
- "version": "0.1.2",
3
+ "version": "0.2.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",
@@ -28,12 +28,14 @@
28
28
  "types": "dist/swarpc.d.ts",
29
29
  "scripts": {
30
30
  "build": "tsc",
31
- "test": "echo \"Error: no test specified\" && exit 1"
31
+ "test": "echo \"Error: no test specified\" && exit 1",
32
+ "typedoc": "typedoc src/swarpc.js src/typings.js --readme README.md"
32
33
  },
33
34
  "dependencies": {
34
35
  "arktype": "^2.1.20"
35
36
  },
36
37
  "devDependencies": {
38
+ "typedoc": "^0.28.7",
37
39
  "typescript": "^5.8.3"
38
40
  }
39
41
  }
package/src/swarpc.js CHANGED
@@ -7,9 +7,11 @@ import { type } from "arktype"
7
7
  /**
8
8
  * @template {ProceduresMap} Procedures
9
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
10
12
  * @returns {SwarpcServer<Procedures>}
11
13
  */
12
- export function Server(procedures) {
14
+ export function Server(procedures, { worker } = { worker: undefined }) {
13
15
  /** @type {SwarpcServer<Procedures>} */
14
16
  // @ts-expect-error
15
17
  const instance = {
@@ -35,6 +37,7 @@ export function Server(procedures) {
35
37
  const PayloadSchema = type.or(
36
38
  ...Object.entries(procedures).map(([functionName, { input }]) => ({
37
39
  functionName: type(`"${functionName}"`),
40
+ requestId: type("string >= 1"),
38
41
  input,
39
42
  }))
40
43
  )
@@ -48,18 +51,26 @@ export function Server(procedures) {
48
51
  * @param {{functionName: string} & Partial<{ result: any; error: any; progress: any }>} data
49
52
  */
50
53
  const postMessage = async (data) => {
51
- await self.clients
52
- .matchAll()
53
- .then((clients) =>
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)
54
59
  clients.forEach((client) => client.postMessage(data))
55
- )
60
+ })
61
+ }
56
62
  }
57
63
 
58
64
  console.log("[SWARPC Server] Starting message listener on", self)
59
65
 
60
66
  self.addEventListener("message", async (event) => {
61
- const { functionName, input } = PayloadSchema.assert(event.data)
62
- console.log("[SWARPC Server] Running", functionName, "with", input)
67
+ const { functionName, requestId, input } = PayloadSchema.assert(
68
+ event.data
69
+ )
70
+ console.log(
71
+ `[SWARPC Server] ${requestId} Running ${functionName} with`,
72
+ input
73
+ )
63
74
 
64
75
  /**
65
76
  * @param {*} error
@@ -67,6 +78,7 @@ export function Server(procedures) {
67
78
  const postError = async (error) =>
68
79
  postMessage({
69
80
  functionName,
81
+ requestId,
70
82
  error: {
71
83
  message: "message" in error ? error.message : String(error),
72
84
  },
@@ -78,23 +90,97 @@ export function Server(procedures) {
78
90
  return
79
91
  }
80
92
 
81
- await implementation(input, async (progress) =>
82
- postMessage({ functionName, progress })
83
- )
84
- .catch(async (error) => postError(error))
85
- .then(async (result) => postMessage({ functionName, result }))
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
+ })
86
114
  })
87
115
  }
88
116
 
89
117
  return instance
90
118
  }
91
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
+
92
176
  /**
93
177
  * @template {ProceduresMap} Procedures
94
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
95
181
  * @returns {SwarpcClient<Procedures>}
96
182
  */
97
- export function Client(procedures) {
183
+ export function Client(procedures, { worker } = { worker: undefined }) {
98
184
  /** @type {SwarpcClient<Procedures>} */
99
185
  // @ts-expect-error
100
186
  const instance = { procedures }
@@ -102,22 +188,25 @@ export function Client(procedures) {
102
188
  for (const functionName of Object.keys(procedures)) {
103
189
  instance[functionName] = async (input, onProgress = () => {}) => {
104
190
  procedures[functionName].input.assert(input)
105
- console.log("[SWARPC Client] Calling", functionName, "with", input)
106
- navigator.serviceWorker.controller?.postMessage({ functionName, input })
191
+ await startClientListener(worker)
192
+
193
+ const w =
194
+ worker ?? (await navigator.serviceWorker.ready.then((r) => r.active))
107
195
  return new Promise((resolve, reject) => {
108
- navigator.serviceWorker.addEventListener("message", (event) => {
109
- const { functionName: fn, ...data } = event.data
110
-
111
- if (fn !== functionName) return
112
-
113
- if ("error" in data) {
114
- reject(new Error(data.error.message))
115
- } else if ("progress" in data) {
116
- onProgress(data.progress)
117
- } else if ("result" in data) {
118
- resolve(data.result)
119
- }
120
- })
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 })
121
210
  })
122
211
  }
123
212
  }
package/src/typings.js CHANGED
@@ -25,7 +25,7 @@
25
25
 
26
26
  /**
27
27
  * @template {Procedure<Type, Type, Type>} P
28
- * @typedef {(input: P['input']['inferOut'], onProgress?: (progress: P['progress']['inferOut']) => void) => Promise<P['success']['inferOut']>} ClientMethod
28
+ * @typedef {(input: P['input']['inferIn'], onProgress?: (progress: P['progress']['inferOut']) => void) => Promise<P['success']['inferOut']>} ClientMethod
29
29
  */
30
30
 
31
31
  /**