swarpc 0.1.1 → 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
 
@@ -10,14 +12,15 @@ RPC for Service Workers -- move that heavy computation off of your UI thread!
10
12
  ## Installation
11
13
 
12
14
  ```bash
13
- npm add swarpc
15
+ npm add swarpc arktype
14
16
  ```
15
17
 
16
18
  ## Usage
17
19
 
18
20
  ### 1. Declare your procedures in a shared file
19
21
 
20
- ```javascript
22
+ ```typescript
23
+ import type { ProceduresMap } from "swarpc"
21
24
  import { type } from "arktype"
22
25
 
23
26
  export const procedures = {
@@ -25,7 +28,7 @@ export const procedures = {
25
28
  // Input for the procedure
26
29
  input: type({ query: "string", "pageSize?": "number" }),
27
30
  // Function to be called whenever you can update progress while the procedure is running -- long computations are a first-class concern here. Examples include using the fetch-progress NPM package.
28
- progress: type({ transmitted: "number", total: "number" }),
31
+ progress: type({ transferred: "number", total: "number" }),
29
32
  // Output of a successful procedure call
30
33
  success: type({
31
34
  id: "string",
@@ -33,7 +36,7 @@ export const procedures = {
33
36
  genres: "string[]",
34
37
  }).array(),
35
38
  },
36
- }
39
+ } as const satisfies ProceduresMap
37
40
  ```
38
41
 
39
42
  ### 2. Register your procedures in the service worker
@@ -58,6 +61,7 @@ swarpc.searchIMDb(async ({ query, pageSize = 10 }, onProgress) => {
58
61
  return fetch(`https://rest.imdbapi.dev/v2/search/titles?${queryParams}`)
59
62
  .then(fetchProgress({ onProgress }))
60
63
  .then((response) => response.json())
64
+ .then(({ titles } => titles)
61
65
  })
62
66
 
63
67
  // ...
@@ -84,9 +88,9 @@ Here's a Svelte example!
84
88
 
85
89
  <search>
86
90
  <input type="text" bind:value={query} placeholder="Search IMDb" />
87
- <button on:click={async () => {
91
+ <button onclick={async () => {
88
92
  results = await swarpc.searchIMDb({ query }, (p) => {
89
- progress = p.transmitted / p.total
93
+ progress = p.transferred / p.total
90
94
  })
91
95
  }}>
92
96
  Search
package/dist/swarpc.d.ts CHANGED
@@ -1,64 +1,23 @@
1
1
  /**
2
- * @import { Type } from 'arktype';
3
- */
4
- /**
5
- * @template {Type} I
6
- * @template {Type} P
7
- * @template {Type} S
8
- * @typedef {Object} Procedure
9
- * @property {I} input
10
- * @property {P} progress
11
- * @property {S} success
12
- */
13
- /**
14
- * @template {Type} I
15
- * @template {Type} P
16
- * @template {Type} S
17
- * @typedef {(input: I['inferOut'], onProgress: (progress: P['inferOut']) => void) => Promise<NoInfer<S>['inferOut'] | NoInfer<S>['inferOut']>} ProcedureImplementation
18
- */
19
- /**
20
- * @typedef {Record<string, Procedure<Type, Type, Type>>} ProceduresMap
21
- */
22
- /**
23
- * @template {ProceduresMap} Procedures
24
- * @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
2
+ * @import { ProceduresMap, SwarpcClient, SwarpcServer } from './typings.js'
25
3
  */
26
4
  /**
27
5
  * @template {ProceduresMap} Procedures
28
6
  * @param {Procedures} procedures
29
- * @returns {SwarpServer<Procedures>}
30
- */
31
- export function Server<Procedures extends ProceduresMap>(procedures: Procedures): SwarpServer<Procedures>;
32
- /**
33
- * @template {Procedure<Type, Type, Type>} P
34
- * @typedef {(input: P['input']['inferOut'], onProgress?: (progress: P['progress']['inferOut']) => void) => Promise<P['success']['inferOut']>} ClientMethod
35
- */
36
- /**
37
- * @template {ProceduresMap} Procedures
38
- * @typedef {{ procedures: Procedures } & { [F in keyof Procedures]: ClientMethod<Procedures[F]> }} SwarpClient
7
+ * @returns {SwarpcServer<Procedures>}
39
8
  */
9
+ export function Server<Procedures extends ProceduresMap>(procedures: Procedures): SwarpcServer<Procedures>;
40
10
  /**
41
11
  * @template {ProceduresMap} Procedures
42
12
  * @param {Procedures} procedures
43
- * @returns {SwarpClient<Procedures>}
44
- */
45
- export function Client<Procedures extends ProceduresMap>(procedures: Procedures): SwarpClient<Procedures>;
46
- export type Procedure<I extends Type, P extends Type, S extends Type> = {
47
- input: I;
48
- progress: P;
49
- success: S;
50
- };
51
- 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"]>;
52
- export type ProceduresMap = Record<string, Procedure<Type, Type, Type>>;
53
- export type SwarpServer<Procedures extends ProceduresMap> = {
54
- procedures: Procedures;
55
- implementations: { [F in keyof Procedures]: ProcedureImplementation<Procedures[F]["input"], Procedures[F]["progress"], Procedures[F]["success"]>; };
56
- start: (self: Window) => void;
57
- } & { [F in keyof Procedures]: (impl: NoInfer<ProcedureImplementation<Procedures[F]["input"], Procedures[F]["progress"], Procedures[F]["success"]>>) => void; };
58
- export type ClientMethod<P extends Procedure<Type, Type, Type>> = (input: P["input"]["inferOut"], onProgress?: (progress: P["progress"]["inferOut"]) => void) => Promise<P["success"]["inferOut"]>;
59
- export type SwarpClient<Procedures extends ProceduresMap> = {
60
- procedures: Procedures;
61
- } & { [F in keyof Procedures]: ClientMethod<Procedures[F]>; };
62
- import type { Type } from 'arktype';
63
- import type { Type as Type_1 } from 'arktype';
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 }?: {
18
+ worker?: Worker;
19
+ }): SwarpcClient<Procedures>;
20
+ import type { ProceduresMap } from './typings.js';
21
+ import type { SwarpcServer } from './typings.js';
22
+ import type { SwarpcClient } from './typings.js';
64
23
  //# sourceMappingURL=swarpc.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"swarpc.d.ts","sourceRoot":"","sources":["../src/swarpc.js"],"names":[],"mappings":"AACA;;GAEG;AAEH;;;;;;;;GAQG;AAEH;;;;;GAKG;AAEH;;GAEG;AAEH;;;GAGG;AAEH;;;;GAIG;AACH,uBAJ6B,UAAU,SAAzB,aAAc,cACjB,UAAU,GACR,WAAW,CAAC,UAAU,CAAC,CAgFnC;AAED;;;GAGG;AAEH;;;GAGG;AAEH;;;;GAIG;AACH,uBAJ6B,UAAU,SAAzB,aAAc,cACjB,UAAU,GACR,WAAW,CAAC,UAAU,CAAC,CA+BnC;sBA1JmB,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;wBAI3B,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;yBAyF7U,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;wBAIhH,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;0BAzHzE,SAAS;oCAAT,SAAS"}
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"}
@@ -0,0 +1,18 @@
1
+ export type Procedure<I extends Type, P extends Type, S extends Type> = {
2
+ input: I;
3
+ progress: P;
4
+ success: S;
5
+ };
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
+ export type ProceduresMap = Record<string, Procedure<Type, Type, Type>>;
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
+ export type SwarpcClient<Procedures extends ProceduresMap> = {
10
+ procedures: Procedures;
11
+ } & { [F in keyof Procedures]: ClientMethod<Procedures[F]>; };
12
+ export type SwarpcServer<Procedures extends ProceduresMap> = {
13
+ procedures: Procedures;
14
+ implementations: { [F in keyof Procedures]: ProcedureImplementation<Procedures[F]["input"], Procedures[F]["progress"], Procedures[F]["success"]>; };
15
+ start: (self: Window) => void;
16
+ } & { [F in keyof Procedures]: (impl: NoInfer<ProcedureImplementation<Procedures[F]["input"], Procedures[F]["progress"], Procedures[F]["success"]>>) => void; };
17
+ import type { Type } from 'arktype';
18
+ //# sourceMappingURL=typings.d.ts.map
@@ -0,0 +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,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.1",
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
@@ -1,41 +1,18 @@
1
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<Type, Type, Type>>} ProceduresMap
25
- */
26
2
 
27
3
  /**
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
4
+ * @import { ProceduresMap, SwarpcClient, SwarpcServer } from './typings.js'
30
5
  */
31
6
 
32
7
  /**
33
8
  * @template {ProceduresMap} Procedures
34
9
  * @param {Procedures} procedures
35
- * @returns {SwarpServer<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>}
36
13
  */
37
- export function Server(procedures) {
38
- /** @type {SwarpServer<Procedures>} */
14
+ export function Server(procedures, { worker } = { worker: undefined }) {
15
+ /** @type {SwarpcServer<Procedures>} */
39
16
  // @ts-expect-error
40
17
  const instance = {
41
18
  procedures,
@@ -60,6 +37,7 @@ export function Server(procedures) {
60
37
  const PayloadSchema = type.or(
61
38
  ...Object.entries(procedures).map(([functionName, { input }]) => ({
62
39
  functionName: type(`"${functionName}"`),
40
+ requestId: type("string >= 1"),
63
41
  input,
64
42
  }))
65
43
  )
@@ -73,18 +51,26 @@ export function Server(procedures) {
73
51
  * @param {{functionName: string} & Partial<{ result: any; error: any; progress: any }>} data
74
52
  */
75
53
  const postMessage = async (data) => {
76
- await self.clients
77
- .matchAll()
78
- .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)
79
59
  clients.forEach((client) => client.postMessage(data))
80
- )
60
+ })
61
+ }
81
62
  }
82
63
 
83
64
  console.log("[SWARPC Server] Starting message listener on", self)
84
65
 
85
66
  self.addEventListener("message", async (event) => {
86
- const { functionName, input } = PayloadSchema.assert(event.data)
87
- 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
+ )
88
74
 
89
75
  /**
90
76
  * @param {*} error
@@ -92,6 +78,7 @@ export function Server(procedures) {
92
78
  const postError = async (error) =>
93
79
  postMessage({
94
80
  functionName,
81
+ requestId,
95
82
  error: {
96
83
  message: "message" in error ? error.message : String(error),
97
84
  },
@@ -103,56 +90,123 @@ export function Server(procedures) {
103
90
  return
104
91
  }
105
92
 
106
- await implementation(input, async (progress) =>
107
- postMessage({ functionName, progress })
108
- )
109
- .catch(async (error) => postError(error))
110
- .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
+ })
111
114
  })
112
115
  }
113
116
 
114
117
  return instance
115
118
  }
116
119
 
120
+ function generateRequestId() {
121
+ return Math.random().toString(36).substring(2, 15)
122
+ }
123
+
117
124
  /**
118
- * @template {Procedure<Type, Type, Type>} P
119
- * @typedef {(input: P['input']['inferOut'], onProgress?: (progress: P['progress']['inferOut']) => void) => Promise<P['success']['inferOut']>} ClientMethod
125
+ * @type {Map<string, { reject: (err: Error) => void; onProgress: (progress: any) => void; resolve: (result: any) => void }>}
120
126
  */
127
+ const pendingRequests = new Map()
121
128
 
129
+ let _clientListenerStarted = false
122
130
  /**
123
- * @template {ProceduresMap} Procedures
124
- * @typedef {{ procedures: Procedures } & { [F in keyof Procedures]: ClientMethod<Procedures[F]> }} SwarpClient
131
+ * @param {Worker} [worker] the worker instance to connect to -- uses the navigator's service worker if not provided
125
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
+ }
126
175
 
127
176
  /**
128
177
  * @template {ProceduresMap} Procedures
129
178
  * @param {Procedures} procedures
130
- * @returns {SwarpClient<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>}
131
182
  */
132
- export function Client(procedures) {
133
- /** @type {SwarpClient<Procedures>} */
183
+ export function Client(procedures, { worker } = { worker: undefined }) {
184
+ /** @type {SwarpcClient<Procedures>} */
134
185
  // @ts-expect-error
135
186
  const instance = { procedures }
136
187
 
137
188
  for (const functionName of Object.keys(procedures)) {
138
189
  instance[functionName] = async (input, onProgress = () => {}) => {
139
190
  procedures[functionName].input.assert(input)
140
- console.log("[SWARPC Client] Calling", functionName, "with", input)
141
- navigator.serviceWorker.controller?.postMessage({ functionName, input })
191
+ await startClientListener(worker)
192
+
193
+ const w =
194
+ worker ?? (await navigator.serviceWorker.ready.then((r) => r.active))
142
195
  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
- })
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 })
156
210
  })
157
211
  }
158
212
  }
package/src/typings.js ADDED
@@ -0,0 +1,42 @@
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 {}