swarpc 0.5.0 → 0.6.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/dist/swarpc.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type ProceduresMap, type SwarpcClient, type SwarpcServer } from "./types.js";
1
+ import { Hooks, type ProceduresMap, type SwarpcClient, type SwarpcServer } from "./types.js";
2
2
  export type { ProceduresMap, SwarpcClient, SwarpcServer } from "./types.js";
3
3
  /**
4
4
  * Creates a sw&rpc server instance.
@@ -15,9 +15,11 @@ export declare function Server<Procedures extends ProceduresMap>(procedures: Pro
15
15
  * @param procedures procedures the client will be able to call
16
16
  * @param param1 various options
17
17
  * @param param1.worker if provided, the client will use this worker to post messages.
18
+ * @param param1.hooks hooks to run on messages received from the server
18
19
  * @returns a sw&rpc client instance. Each property of the procedures map will be a method, that accepts an input and an optional onProgress callback.
19
20
  */
20
- export declare function Client<Procedures extends ProceduresMap>(procedures: Procedures, { worker }?: {
21
+ export declare function Client<Procedures extends ProceduresMap>(procedures: Procedures, { worker, hooks }?: {
21
22
  worker?: Worker;
23
+ hooks?: Hooks<Procedures>;
22
24
  }): SwarpcClient<Procedures>;
23
25
  //# sourceMappingURL=swarpc.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"swarpc.d.ts","sourceRoot":"","sources":["../src/swarpc.ts"],"names":[],"mappings":"AACA,OAAO,EAKL,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,YAAY,EAClB,MAAM,YAAY,CAAA;AAGnB,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE3E;;;;;;GAMG;AACH,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,CA0G1B;AAoFD;;;;;;GAMG;AACH,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,CA0D1B"}
1
+ {"version":3,"file":"swarpc.d.ts","sourceRoot":"","sources":["../src/swarpc.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,EAML,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,YAAY,EAClB,MAAM,YAAY,CAAA;AAGnB,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE3E;;;;;;GAMG;AACH,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,CAuG1B;AA+FD;;;;;;;GAOG;AACH,wBAAgB,MAAM,CAAC,UAAU,SAAS,aAAa,EACrD,UAAU,EAAE,UAAU,EACtB,EAAE,MAAM,EAAE,KAAU,EAAE,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,KAAK,CAAC,UAAU,CAAC,CAAA;CAAO,GAC1E,YAAY,CAAC,UAAU,CAAC,CA0D1B"}
package/dist/swarpc.js CHANGED
@@ -53,7 +53,13 @@ export function Server(procedures, { worker } = {}) {
53
53
  // Get autotransfer preference from the procedure definition
54
54
  const { autotransfer = "output-only" } = instance[zProcedures][functionName];
55
55
  // Shorthand function with functionName, requestId, etc. set
56
- const postMsg = async (data) => postMessage({ functionName, requestId, autotransfer, ...data });
56
+ const postMsg = async (data) => postMessage({
57
+ by: "sw&rpc",
58
+ functionName,
59
+ requestId,
60
+ autotransfer,
61
+ ...data,
62
+ });
57
63
  // Prepare a function to post errors back to the client
58
64
  const postError = async (error) => postMsg({
59
65
  error: {
@@ -105,7 +111,7 @@ let _clientListenerStarted = false;
105
111
  * @param worker if provided, the client will use this worker to listen for messages, instead of using the service worker
106
112
  * @returns
107
113
  */
108
- async function startClientListener(worker) {
114
+ async function startClientListener(worker, hooks = {}) {
109
115
  if (_clientListenerStarted)
110
116
  return;
111
117
  // Get service worker registration if no worker is provided
@@ -123,8 +129,12 @@ async function startClientListener(worker) {
123
129
  l.client.debug("", "Starting client listener on", w);
124
130
  w.addEventListener("message", (event) => {
125
131
  // Get the data from the event
132
+ const eventData = event.data || {};
133
+ // Ignore other messages that aren't for us
134
+ if (eventData?.by !== "sw&rpc")
135
+ return;
126
136
  // We don't use a arktype schema here, we trust the server to send valid data
127
- const { functionName, requestId, ...data } = event.data || {};
137
+ const { functionName, requestId, ...data } = eventData;
128
138
  // Sanity check in case we somehow receive a message without requestId
129
139
  if (!requestId) {
130
140
  throw new Error("[SWARPC Client] Message received without requestId");
@@ -134,16 +144,19 @@ async function startClientListener(worker) {
134
144
  if (!handlers) {
135
145
  throw new Error(`[SWARPC Client] ${requestId} has no active request handlers`);
136
146
  }
137
- // React to the data received
138
- // Unless it's a progress update, the request is finished, thus we can remove it from the pending requests
147
+ // React to the data received: call hook, call handler,
148
+ // and remove the request from pendingRequests (unless it's a progress update)
139
149
  if ("error" in data) {
150
+ hooks.error?.(functionName, new Error(data.error.message));
140
151
  handlers.reject(new Error(data.error.message));
141
152
  pendingRequests.delete(requestId);
142
153
  }
143
154
  else if ("progress" in data) {
155
+ hooks.progress?.(functionName, data.progress);
144
156
  handlers.onProgress(data.progress);
145
157
  }
146
158
  else if ("result" in data) {
159
+ hooks.success?.(functionName, data.result);
147
160
  handlers.resolve(data.result);
148
161
  pendingRequests.delete(requestId);
149
162
  }
@@ -155,9 +168,10 @@ async function startClientListener(worker) {
155
168
  * @param procedures procedures the client will be able to call
156
169
  * @param param1 various options
157
170
  * @param param1.worker if provided, the client will use this worker to post messages.
171
+ * @param param1.hooks hooks to run on messages received from the server
158
172
  * @returns a sw&rpc client instance. Each property of the procedures map will be a method, that accepts an input and an optional onProgress callback.
159
173
  */
160
- export function Client(procedures, { worker } = {}) {
174
+ export function Client(procedures, { worker, hooks = {} } = {}) {
161
175
  // Store procedures on a symbol key, to avoid conflicts with procedure names
162
176
  const instance = { [zProcedures]: procedures };
163
177
  for (const functionName of Object.keys(procedures)) {
@@ -170,7 +184,7 @@ export function Client(procedures, { worker } = {}) {
170
184
  // Validate the input against the procedure's input schema
171
185
  procedures[functionName].input.assert(input);
172
186
  // Ensure that we're listening for messages from the server
173
- await startClientListener(worker);
187
+ await startClientListener(worker, hooks);
174
188
  // If no worker is provided, we use the service worker
175
189
  const w = worker ?? (await navigator.serviceWorker.ready.then((r) => r.active));
176
190
  if (!w) {
package/dist/types.d.ts CHANGED
@@ -33,7 +33,7 @@ export type Procedure<I extends Type, P extends Type, S extends Type> = {
33
33
  */
34
34
  export type ProcedureImplementation<I extends Type, P extends Type, S extends Type> = (input: I["inferOut"], onProgress: (progress: P["inferIn"]) => void) => Promise<S["inferIn"]>;
35
35
  /**
36
- * Declarations of procedures by name
36
+ * Declarations of procedures by name.
37
37
  */
38
38
  export type ProceduresMap = Record<string, Procedure<Type, Type, Type>>;
39
39
  /**
@@ -42,6 +42,44 @@ export type ProceduresMap = Record<string, Procedure<Type, Type, Type>>;
42
42
  export type ImplementationsMap<Procedures extends ProceduresMap> = {
43
43
  [F in keyof Procedures]: ProcedureImplementation<Procedures[F]["input"], Procedures[F]["progress"], Procedures[F]["success"]>;
44
44
  };
45
+ /**
46
+ * Declaration of hooks to run on messages received from the server
47
+ */
48
+ export type Hooks<Procedures extends ProceduresMap> = {
49
+ /**
50
+ * Called when a procedure call has been successful.
51
+ */
52
+ success?: <Procedure extends keyof ProceduresMap>(procedure: Procedure, data: Procedures[Procedure]["success"]["inferOut"]) => void;
53
+ /**
54
+ * Called when a procedure call has failed.
55
+ */
56
+ error?: <Procedure extends keyof ProceduresMap>(procedure: Procedure, error: Error) => void;
57
+ /**
58
+ * Called when a procedure call sends progress updates.
59
+ */
60
+ progress?: <Procedure extends keyof ProceduresMap>(procedure: Procedure, data: Procedures[Procedure]["progress"]["inferOut"]) => void;
61
+ };
62
+ export type PayloadHeader<PM extends ProceduresMap, Name extends keyof PM = keyof PM> = {
63
+ by: "sw&rpc";
64
+ functionName: Name & string;
65
+ requestId: string;
66
+ autotransfer: PM[Name]["autotransfer"];
67
+ };
68
+ export type PayloadCore<PM extends ProceduresMap, Name extends keyof PM = keyof PM> = {
69
+ input: PM[Name]["input"]["inferOut"];
70
+ } | {
71
+ progress: PM[Name]["progress"]["inferOut"];
72
+ } | {
73
+ result: PM[Name]["success"]["inferOut"];
74
+ } | {
75
+ error: {
76
+ message: string;
77
+ };
78
+ };
79
+ /**
80
+ * The effective payload as sent by the server to the client
81
+ */
82
+ export type Payload<PM extends ProceduresMap, Name extends keyof PM = keyof PM> = PayloadHeader<PM, Name> & PayloadCore<PM, Name>;
45
83
  /**
46
84
  * A procedure's corresponding method on the client instance -- used to call the procedure
47
85
  */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAA;AAEnC;;GAEG;AACH,MAAM,MAAM,SAAS,CAAC,CAAC,SAAS,IAAI,EAAE,CAAC,SAAS,IAAI,EAAE,CAAC,SAAS,IAAI,IAAI;IACtE;;OAEG;IACH,KAAK,EAAE,CAAC,CAAA;IACR;;;OAGG;IACH,QAAQ,EAAE,CAAC,CAAA;IACX;;OAEG;IACH,OAAO,EAAE,CAAC,CAAA;IACV;;;;;;;;;OASG;IACH,YAAY,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,aAAa,CAAA;CAClD,CAAA;AAED;;GAEG;AACH,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,SAAS,CAAC,KAAK,IAAI,KACzC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAA;AAE1B;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAA;AAEvE;;GAEG;AACH,MAAM,MAAM,kBAAkB,CAAC,UAAU,SAAS,aAAa,IAAI;KAChE,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;CACF,CAAA;AAED;;GAEG;AACH,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,CAAA;AAEtC;;GAEG;AACH,eAAO,MAAM,gBAAgB,eAAmC,CAAA;AAChE;;GAEG;AACH,eAAO,MAAM,WAAW,eAA8B,CAAA;AAEtD;;GAEG;AACH,MAAM,MAAM,YAAY,CAAC,UAAU,SAAS,aAAa,IAAI;IAC3D,CAAC,WAAW,CAAC,EAAE,UAAU,CAAA;CAC1B,GAAG;KACD,CAAC,IAAI,MAAM,UAAU,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;CACrD,CAAA;AAED;;;GAGG;AACH,MAAM,MAAM,YAAY,CAAC,UAAU,SAAS,aAAa,IAAI;IAC3D,CAAC,WAAW,CAAC,EAAE,UAAU,CAAA;IACzB,CAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,UAAU,CAAC,CAAA;IAClD,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1B,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,CAAA"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swarpc",
3
- "version": "0.5.0",
3
+ "version": "0.6.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",
@@ -30,12 +30,15 @@
30
30
  "build": "tsc",
31
31
  "dev": "tsc --watch",
32
32
  "test": "echo \"Error: no test specified\" && exit 1",
33
- "typedoc": "typedoc src/swarpc.ts src/types.ts --readme README.md"
33
+ "typedoc": "typedoc src/swarpc.ts src/types.ts --readme README.md",
34
+ "version": "kacl release && prettier -w CHANGELOG.md && git add CHANGELOG.md"
34
35
  },
35
36
  "dependencies": {
36
37
  "arktype": "^2.1.20"
37
38
  },
38
39
  "devDependencies": {
40
+ "kacl": "^1.1.1",
41
+ "prettier": "^3.6.2",
39
42
  "typedoc": "^0.28.7",
40
43
  "typescript": "^5.8.3"
41
44
  }
package/src/swarpc.ts CHANGED
@@ -1,7 +1,9 @@
1
- import { type, type Type } from "arktype"
1
+ import { type } from "arktype"
2
2
  import {
3
+ Hooks,
3
4
  ImplementationsMap,
4
- Procedure,
5
+ Payload,
6
+ PayloadCore,
5
7
  zImplementations,
6
8
  zProcedures,
7
9
  type ProceduresMap,
@@ -53,17 +55,7 @@ export function Server<Procedures extends ProceduresMap>(
53
55
 
54
56
  instance.start = (self: Window) => {
55
57
  // Used to post messages back to the client
56
- const postMessage = async (
57
- data: {
58
- functionName: string
59
- requestId: string
60
- autotransfer: Procedure<Type, Type, Type>["autotransfer"]
61
- } & Partial<{
62
- result: any
63
- error: any
64
- progress: any
65
- }>
66
- ) => {
58
+ const postMessage = async (data: Payload<Procedures>) => {
67
59
  const transfer =
68
60
  data.autotransfer === "never" ? [] : findTransferables(data)
69
61
 
@@ -91,8 +83,15 @@ export function Server<Procedures extends ProceduresMap>(
91
83
 
92
84
  // Shorthand function with functionName, requestId, etc. set
93
85
  const postMsg = async (
94
- data: { result: any } | { error: any } | { progress: any }
95
- ) => postMessage({ functionName, requestId, autotransfer, ...data })
86
+ data: PayloadCore<Procedures, typeof functionName>
87
+ ) =>
88
+ postMessage({
89
+ by: "sw&rpc",
90
+ functionName,
91
+ requestId,
92
+ autotransfer,
93
+ ...data,
94
+ })
96
95
 
97
96
  // Prepare a function to post errors back to the client
98
97
  const postError = async (error: any) =>
@@ -158,7 +157,10 @@ let _clientListenerStarted = false
158
157
  * @param worker if provided, the client will use this worker to listen for messages, instead of using the service worker
159
158
  * @returns
160
159
  */
161
- async function startClientListener(worker?: Worker) {
160
+ async function startClientListener<Procedures extends ProceduresMap>(
161
+ worker?: Worker,
162
+ hooks: Hooks<Procedures> = {}
163
+ ) {
162
164
  if (_clientListenerStarted) return
163
165
 
164
166
  // Get service worker registration if no worker is provided
@@ -179,9 +181,14 @@ async function startClientListener(worker?: Worker) {
179
181
  l.client.debug("", "Starting client listener on", w)
180
182
  w.addEventListener("message", (event) => {
181
183
  // Get the data from the event
184
+ const eventData = (event as MessageEvent).data || {}
185
+
186
+ // Ignore other messages that aren't for us
187
+ if (eventData?.by !== "sw&rpc") return
188
+
182
189
  // We don't use a arktype schema here, we trust the server to send valid data
183
190
  const { functionName, requestId, ...data } =
184
- (event as MessageEvent).data || {}
191
+ eventData as Payload<Procedures>
185
192
 
186
193
  // Sanity check in case we somehow receive a message without requestId
187
194
  if (!requestId) {
@@ -196,14 +203,17 @@ async function startClientListener(worker?: Worker) {
196
203
  )
197
204
  }
198
205
 
199
- // React to the data received
200
- // Unless it's a progress update, the request is finished, thus we can remove it from the pending requests
206
+ // React to the data received: call hook, call handler,
207
+ // and remove the request from pendingRequests (unless it's a progress update)
201
208
  if ("error" in data) {
209
+ hooks.error?.(functionName, new Error(data.error.message))
202
210
  handlers.reject(new Error(data.error.message))
203
211
  pendingRequests.delete(requestId)
204
212
  } else if ("progress" in data) {
213
+ hooks.progress?.(functionName, data.progress)
205
214
  handlers.onProgress(data.progress)
206
215
  } else if ("result" in data) {
216
+ hooks.success?.(functionName, data.result)
207
217
  handlers.resolve(data.result)
208
218
  pendingRequests.delete(requestId)
209
219
  }
@@ -217,11 +227,12 @@ async function startClientListener(worker?: Worker) {
217
227
  * @param procedures procedures the client will be able to call
218
228
  * @param param1 various options
219
229
  * @param param1.worker if provided, the client will use this worker to post messages.
230
+ * @param param1.hooks hooks to run on messages received from the server
220
231
  * @returns a sw&rpc client instance. Each property of the procedures map will be a method, that accepts an input and an optional onProgress callback.
221
232
  */
222
233
  export function Client<Procedures extends ProceduresMap>(
223
234
  procedures: Procedures,
224
- { worker }: { worker?: Worker } = {}
235
+ { worker, hooks = {} }: { worker?: Worker; hooks?: Hooks<Procedures> } = {}
225
236
  ): SwarpcClient<Procedures> {
226
237
  // Store procedures on a symbol key, to avoid conflicts with procedure names
227
238
  const instance = { [zProcedures]: procedures } as Partial<
@@ -243,7 +254,7 @@ export function Client<Procedures extends ProceduresMap>(
243
254
  // Validate the input against the procedure's input schema
244
255
  procedures[functionName].input.assert(input)
245
256
  // Ensure that we're listening for messages from the server
246
- await startClientListener(worker)
257
+ await startClientListener(worker, hooks)
247
258
 
248
259
  // If no worker is provided, we use the service worker
249
260
  const w =
package/src/types.ts CHANGED
@@ -43,7 +43,7 @@ export type ProcedureImplementation<
43
43
  ) => Promise<S["inferIn"]>
44
44
 
45
45
  /**
46
- * Declarations of procedures by name
46
+ * Declarations of procedures by name.
47
47
  */
48
48
  export type ProceduresMap = Record<string, Procedure<Type, Type, Type>>
49
49
 
@@ -58,6 +58,68 @@ export type ImplementationsMap<Procedures extends ProceduresMap> = {
58
58
  >
59
59
  }
60
60
 
61
+ /**
62
+ * Declaration of hooks to run on messages received from the server
63
+ */
64
+ export type Hooks<Procedures extends ProceduresMap> = {
65
+ /**
66
+ * Called when a procedure call has been successful.
67
+ */
68
+ success?: <Procedure extends keyof ProceduresMap>(
69
+ procedure: Procedure,
70
+ data: Procedures[Procedure]["success"]["inferOut"]
71
+ ) => void
72
+ /**
73
+ * Called when a procedure call has failed.
74
+ */
75
+ error?: <Procedure extends keyof ProceduresMap>(
76
+ procedure: Procedure,
77
+ error: Error
78
+ ) => void
79
+ /**
80
+ * Called when a procedure call sends progress updates.
81
+ */
82
+ progress?: <Procedure extends keyof ProceduresMap>(
83
+ procedure: Procedure,
84
+ data: Procedures[Procedure]["progress"]["inferOut"]
85
+ ) => void
86
+ }
87
+
88
+ export type PayloadHeader<
89
+ PM extends ProceduresMap,
90
+ Name extends keyof PM = keyof PM
91
+ > = {
92
+ by: "sw&rpc"
93
+ functionName: Name & string
94
+ requestId: string
95
+ autotransfer: PM[Name]["autotransfer"]
96
+ }
97
+
98
+ export type PayloadCore<
99
+ PM extends ProceduresMap,
100
+ Name extends keyof PM = keyof PM
101
+ > =
102
+ | {
103
+ input: PM[Name]["input"]["inferOut"]
104
+ }
105
+ | {
106
+ progress: PM[Name]["progress"]["inferOut"]
107
+ }
108
+ | {
109
+ result: PM[Name]["success"]["inferOut"]
110
+ }
111
+ | {
112
+ error: { message: string }
113
+ }
114
+
115
+ /**
116
+ * The effective payload as sent by the server to the client
117
+ */
118
+ export type Payload<
119
+ PM extends ProceduresMap,
120
+ Name extends keyof PM = keyof PM
121
+ > = PayloadHeader<PM, Name> & PayloadCore<PM, Name>
122
+
61
123
  /**
62
124
  * A procedure's corresponding method on the client instance -- used to call the procedure
63
125
  */