@xentobias/worker-rpc 1.0.14 → 1.0.16

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.
@@ -34,10 +34,6 @@ export declare class BroadcastEndpoint extends Endpoint {
34
34
  * @returns Promise that resolves with a BroadcastResult containing results and errors
35
35
  */
36
36
  call(path: MethodPath, args: unknown[]): Promise<BroadcastResult>;
37
- /**
38
- * Send a broadcast message and set up multi-response handling
39
- */
40
- private sendBroadcast;
41
37
  /**
42
38
  * Handle timeout for a broadcast call
43
39
  */
@@ -1,4 +1,5 @@
1
1
  import { type RpcMessage, type CallMessage, type ResultMessage, type ErrorMessage, type MethodPath, type PendingCall, type CallbackRegistration, type MessageTarget, type TransferableValue } from './types';
2
+ import { type Transformer } from './transformer';
2
3
  /** Unique identifier for tracking RPC calls */
3
4
  export type CallId = `c:${string}`;
4
5
  /** Unique identifier for callback functions */
@@ -19,6 +20,32 @@ export interface EndpointOptions {
19
20
  onError?: (error: Error) => void;
20
21
  /** Callback invoked when the remote endpoint is released */
21
22
  onRelease?: () => void;
23
+ /**
24
+ * Codecs for non-cloneable values. Checked in order; first match wins.
25
+ * The scanning depth into plain objects is controlled by `transformDepth`.
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * import { responseTransformer } from '@xentobias/worker-rpc/transformers';
30
+ *
31
+ * const endpoint = createEndpoint(worker, {
32
+ * transformers: [responseTransformer],
33
+ * });
34
+ * ```
35
+ */
36
+ transformers?: Transformer[];
37
+ /**
38
+ * How many levels deep to scan plain-object properties for transformer-
39
+ * matched values.
40
+ *
41
+ * - `1` (default) — only first-level properties are scanned (backward-compatible).
42
+ * - `2` — scans two levels deep (e.g. `{ a: { req: new Request(...) } }`).
43
+ * - `Infinity` — recurse without limit.
44
+ *
45
+ * The top-level value itself is always checked regardless of this setting.
46
+ * Function extraction in return values is unaffected and remains first-level only.
47
+ */
48
+ transformDepth?: number;
22
49
  }
23
50
  /** Configuration for exposing an API */
24
51
  export interface ExposeOptions {
@@ -41,10 +68,6 @@ export interface ShutdownResult {
41
68
  * RPC Endpoint - handles bidirectional communication with a worker
42
69
  */
43
70
  export declare class Endpoint {
44
- /**
45
- * Generate a random ID string for use as a default endpoint ID.
46
- * @returns A random 6-character alphanumeric string
47
- */
48
71
  private static generateRandomId;
49
72
  /**
50
73
  * Extract the endpoint ID from a call ID.
@@ -63,7 +86,9 @@ export declare class Endpoint {
63
86
  */
64
87
  static extractEndpointId(callId: CallId): string;
65
88
  protected target: MessageTarget;
66
- protected options: Required<EndpointOptions>;
89
+ protected options: Required<Omit<EndpointOptions, 'transformers'>> & {
90
+ transformers: Transformer[];
91
+ };
67
92
  private exposedApi;
68
93
  private exposeOptions;
69
94
  /** Unique identifier for this endpoint */
@@ -80,132 +105,84 @@ export declare class Endpoint {
80
105
  protected remoteCallbacks: Map<`cb:${string}`, Function>;
81
106
  /** Message handler bound to this instance */
82
107
  private boundMessageHandler;
108
+ /** Close/error handler bound to this instance (detects worker death) */
109
+ private boundCloseHandler;
83
110
  /** Whether this endpoint has been released */
84
111
  protected released: boolean;
112
+ /** Release handlers from returned objects with [CALLBACK_RELEASE] */
113
+ protected releaseHandlers: Set<() => void>;
85
114
  constructor(target: MessageTarget, options?: EndpointOptions);
86
- /**
87
- * Generate a unique call ID for this endpoint
88
- */
89
115
  protected generateCallId(): CallId;
90
- /**
91
- * Generate a unique callback ID for this endpoint
92
- */
93
116
  protected generateCallbackId(): CallbackId;
94
- /**
95
- * Attach the message listener to the target
96
- */
97
117
  private attachListener;
98
- /**
99
- * Detach the message listener from the target
100
- */
101
118
  private detachListener;
102
- /**
103
- * Log a debug message
104
- */
119
+ /** Handles worker death / port close — rejects pending calls and cleans up without sending messages. */
120
+ private handleTargetClose;
105
121
  protected debug(...args: unknown[]): void;
106
- /**
107
- * Expose an API object for remote invocation
108
- */
109
122
  expose(api: object, options?: ExposeOptions): void;
110
- /**
111
- * Handle incoming messages
112
- */
113
123
  private handleMessage;
114
- /**
115
- * Handle a method call request
116
- */
117
124
  private handleCall;
118
- /**
119
- * Resolve a method from the exposed API by path
120
- */
121
125
  private resolveMethod;
122
- /**
123
- * Handle a successful result
124
- */
126
+ protected resolveResult(value: unknown, callbackMap?: Record<string, CallbackId>): Promise<unknown>;
125
127
  protected handleResult(message: ResultMessage): void;
126
- /**
127
- * Handle an error result
128
- */
129
128
  protected handleError(message: ErrorMessage): void;
130
- /**
131
- * Handle a callback invocation
132
- */
133
129
  private handleCallback;
134
- /**
135
- * Handle release of callbacks
136
- */
137
130
  private handleCallbackRelease;
138
- /**
139
- * Handle notification that the remote endpoint has been released
140
- */
141
131
  private handleEndpointRelease;
142
- /**
143
- * Register a local callback function
144
- */
145
132
  protected registerCallback(fn: Function, remaining?: number): CallbackId;
146
- /**
147
- * Create a proxy function for a remote callback
148
- */
149
133
  protected createRemoteCallback(callbackId: CallbackId): Function;
134
+ private invokeCallback;
150
135
  /**
151
- * Invoke a remote callback
136
+ * Encode a single value through transformers, recursively scanning plain-
137
+ * object properties up to `depth` levels deep.
138
+ * This is the symmetric counterpart of `decodeValue`.
139
+ *
140
+ * @param value - The value to encode.
141
+ * @param depth - Remaining levels of plain-object recursion. The top-level
142
+ * transformer match is always attempted regardless of this value.
152
143
  */
153
- private invokeCallback;
144
+ private encodeValue;
154
145
  /**
155
- * Process arguments for a call - separates functions into callbacks and collects transferables
146
+ * Decode a single value through transformers, recursively scanning plain-
147
+ * object properties up to `depth` levels deep.
148
+ * This is the symmetric counterpart of `encodeValue`.
149
+ *
150
+ * @param value - The value to decode.
151
+ * @param depth - Remaining levels of plain-object recursion. Encoded
152
+ * envelopes are always decoded regardless of this value.
156
153
  */
157
- protected processArgs(args: unknown[]): {
154
+ private decodeValue;
155
+ /** Extracts functions and MessagePorts from args. Used as the no-transformer fast path. */
156
+ protected processArgsSync(args: unknown[]): {
158
157
  rawArgs: unknown[];
159
158
  callbackMap: Record<number, CallbackId>;
160
159
  transferables: TransferableValue[];
161
160
  };
161
+ /** Like `processArgsSync`, but also encodes values through transformers. */
162
+ protected processArgs(args: unknown[]): Promise<{
163
+ rawArgs: unknown[];
164
+ callbackMap: Record<number, CallbackId>;
165
+ transferables: TransferableValue[];
166
+ }>;
162
167
  /**
163
- * Process a return value - extracts functions from first-level object properties.
164
- * For performance, only scans the first level of plain objects.
168
+ * Extracts first-level functions into the callback map and encodes
169
+ * non-cloneable values through transformers.
165
170
  */
166
- protected processResult(value: unknown): {
171
+ protected processResult(value: unknown): Promise<{
167
172
  rawValue: unknown;
168
173
  callbackMap: Record<string, CallbackId>;
169
- };
170
- /**
171
- * Reconstruct a result value by merging data with callback proxies.
172
- */
173
- protected reconstructResult(value: unknown, callbackMap: Record<string, CallbackId>): unknown;
174
- /**
175
- * Build a call message
176
- */
174
+ transferables: TransferableValue[];
175
+ }>;
176
+ protected reconstructResult(value: unknown, callbackMap: Record<string, CallbackId>): Promise<unknown>;
177
177
  protected buildCallMessage(id: CallId, path: MethodPath, rawArgs: unknown[], callbackMap: Record<number, CallbackId>): CallMessage;
178
178
  /**
179
179
  * Call a remote method
180
180
  */
181
181
  call(path: MethodPath, args: unknown[]): Promise<unknown>;
182
- /**
183
- * Send a message to the target
184
- */
185
182
  protected send(message: RpcMessage, transferables?: TransferableValue[]): void;
186
- /**
187
- * Get the number of pending calls
188
- */
189
183
  getPendingCallCount(): number;
190
- /**
191
- * Check if there are any pending calls
192
- */
193
184
  hasPendingCalls(): boolean;
194
- /**
195
- * Gracefully shut down the endpoint, waiting for pending calls to complete
196
- *
197
- * @param options - Shutdown options
198
- * @returns Promise that resolves with the shutdown result
199
- *
200
- * @example
201
- * ```typescript
202
- * // Wait up to 5 seconds for pending calls to complete
203
- * const result = await endpoint.shutdown({ timeout: 5000 });
204
- * if (result.timedOut) {
205
- * console.log(`${result.terminatedCalls} calls were forcefully terminated`);
206
- * }
207
- * ```
208
- */
185
+ /** Waits for pending calls to finish, then releases. Resolves with `{ timeout: true }` if they don't finish in time. */
209
186
  shutdown(options?: ShutdownOptions): Promise<ShutdownResult>;
210
187
  /**
211
188
  * Release this endpoint and clean up resources
@@ -216,20 +193,8 @@ export declare class Endpoint {
216
193
  release(options?: {
217
194
  silent?: boolean;
218
195
  }): void;
219
- /**
220
- * Get the underlying message target
221
- */
222
196
  getTarget(): MessageTarget;
223
197
  }
224
- /**
225
- * Check if a value is a function
226
- */
227
198
  export declare function isFunction(value: unknown): value is Function;
228
- /**
229
- * Check if a value is a MessagePort
230
- */
231
199
  export declare function isMessagePort(value: unknown): value is MessagePort;
232
- /**
233
- * Create an endpoint for a worker or message port
234
- */
235
200
  export declare function createEndpoint(target: MessageTarget, options?: EndpointOptions): Endpoint;
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ export * from "./error";
5
5
  export * from "./endpoint";
6
6
  export * from "./proxy";
7
7
  export * from "./broadcast";
8
+ export * from "./transformer";
8
9
  /**
9
10
  * Expose an API object for remote invocation
10
11
  * Call this in the worker to expose methods to the parent
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // @bun
2
- var B;((K)=>{K[K.Call=0]="Call";K[K.Result=1]="Result";K[K.Error=2]="Error";K[K.Callback=3]="Callback";K[K.CallbackRelease=4]="CallbackRelease";K[K.EndpointRelease=5]="EndpointRelease"})(B||={});var h=new Set(["message","name","stack","cause"]);function Y(q){let G={e:q.message,n:q.name,s:q.stack},Q={};for(let Z of Object.keys(q))if(!h.has(Z))Q[Z]=q[Z];if(Object.keys(Q).length>0)G.d=Q;if(q.cause instanceof Error)G.c=Y(q.cause);else if(q.cause!==void 0)G.c={e:String(q.cause),d:{cause:q.cause}};return G}function H(q){let G=q.c?{cause:H(q.c)}:void 0,Q=Error(q.e,G);if(q.n)Q.name=q.n;if(q.s)Q.stack=q.s;if(q.d)Object.assign(Q,q.d);return Q}var O=30000;class L{static generateRandomId(){return Math.random().toString(36).slice(2,8)}static extractEndpointId(q){return q.split(":")[1]??""}target;options;exposedApi=null;exposeOptions={};id;callCounter=0;callbackCounter=0;pendingCalls=new Map;callbacks=new Map;remoteCallbacks=new Map;boundMessageHandler;released=!1;constructor(q,G={}){this.target=q,this.id=G.id??L.generateRandomId(),this.options={id:this.id,timeout:G.timeout??O,onError:G.onError??console.error,debug:G.debug??!1,onRelease:G.onRelease??(()=>{})},this.boundMessageHandler=this.handleMessage.bind(this),this.attachListener()}generateCallId(){return`c:${this.id}:${++this.callCounter}`}generateCallbackId(){return`cb:${this.id}:${++this.callbackCounter}`}attachListener(){if(this.target.addEventListener)this.target.addEventListener("message",this.boundMessageHandler);else if(this.target.onmessage!==void 0)this.target.onmessage=this.boundMessageHandler}detachListener(){if(this.target.removeEventListener)this.target.removeEventListener("message",this.boundMessageHandler);else if(this.target.onmessage!==void 0)this.target.onmessage=null}debug(...q){if(this.options.debug)console.log("[worker-rpc]",...q)}expose(q,G={}){this.exposedApi=q,this.exposeOptions={maxDepth:G.maxDepth??10}}handleMessage(q){let G=q.data;if(typeof G!=="object"||G===null||!("t"in G))return;switch(this.debug("received",B[G.t],G),G.t){case 0:this.handleCall(G);break;case 1:this.handleResult(G);break;case 2:this.handleError(G);break;case 3:this.handleCallback(G);break;case 4:this.handleCallbackRelease(G);break;case 5:this.handleEndpointRelease();break}}async handleCall(q){let{id:G,p:Q,a:Z,c:$}=q;try{let{method:J,thisArg:K}=this.resolveMethod(Q);if(typeof J!=="function")throw Error(`Method not found: ${Q.join(".")}`);let W=Z.map((C,X)=>{let x=$?.[X];if(x)return this.createRemoteCallback(x);return C}),V=await J.apply(K,W),{rawValue:U,callbackMap:S}=this.processResult(V);this.send({t:1,id:G,v:U,...Object.keys(S).length>0&&{cm:S}})}catch(J){let K=J instanceof Error?J:Error(String(J));this.send({t:2,id:G,...Y(K)})}}resolveMethod(q){if(!this.exposedApi)throw Error("No API exposed");let G=this.exposedApi,Q=null;for(let Z=0;Z<q.length;Z++){let $=q[Z];if(G===null||G===void 0)throw Error(`Cannot access property '${$}' of ${G}`);if($===void 0)throw Error(`Invalid path at index ${Z}`);if(Q=G,G=G[$],Z>=(this.exposeOptions.maxDepth??10))throw Error("Maximum nesting depth exceeded")}return{target:this.exposedApi,method:G,thisArg:Q}}handleResult(q){let G=this.pendingCalls.get(q.id);if(!G){this.debug("Received result for unknown call:",q.id);return}if(this.pendingCalls.delete(q.id),G.timer)clearTimeout(G.timer);if(q.cm&&Object.keys(q.cm).length>0)G.resolve(this.reconstructResult(q.v,q.cm));else G.resolve(q.v)}handleError(q){let G=this.pendingCalls.get(q.id);if(!G){this.debug("Received error for unknown call:",q.id);return}if(this.pendingCalls.delete(q.id),G.timer)clearTimeout(G.timer);G.reject(H(q))}async handleCallback(q){let{id:G,c:Q,a:Z,cb:$}=q,J=this.callbacks.get(Q);if(!J){this.send({t:2,id:G,e:`Callback not found: ${Q}`});return}try{let K=Z.map((S,C)=>{let X=$?.[C];if(X)return this.createRemoteCallback(X);return S}),W=await J.fn(...K);if(J.remaining>0){if(J.remaining--,J.remaining===0)this.callbacks.delete(Q)}let{rawValue:V,callbackMap:U}=this.processResult(W);this.send({t:1,id:G,v:V,...Object.keys(U).length>0&&{cm:U}})}catch(K){let W=K instanceof Error?K:Error(String(K));this.send({t:2,id:G,...Y(W)})}}handleCallbackRelease(q){for(let G of q.c)this.callbacks.delete(G)}handleEndpointRelease(){this.debug("Remote endpoint released"),this.options.onRelease(),this.release({silent:!0})}registerCallback(q,G=-1){let Q=this.generateCallbackId();return this.callbacks.set(Q,{fn:q,remaining:G}),Q}createRemoteCallback(q){let G=this.remoteCallbacks.get(q);if(G)return G;return G=async(...Q)=>{return this.invokeCallback(q,Q)},this.remoteCallbacks.set(q,G),G}async invokeCallback(q,G){let Q=this.generateCallId(),Z=[],$={},J=[];for(let K=0;K<G.length;K++){let W=G[K];if(j(W))$[K]=this.registerCallback(W),Z.push(null);else if(D(W))J.push(W),Z.push(W);else Z.push(W)}return new Promise((K,W)=>{let V=setTimeout(()=>{this.pendingCalls.delete(Q),W(Error(`Callback invocation timed out: ${q}`))},this.options.timeout);this.pendingCalls.set(Q,{resolve:K,reject:W,timer:V});let U={t:3,id:Q,c:q,a:Z,...Object.keys($).length>0&&{cb:$}};this.send(U,J)})}processArgs(q){let G=[],Q={},Z=[];for(let $=0;$<q.length;$++){let J=q[$];if(j(J))Q[$]=this.registerCallback(J),G.push(null);else if(D(J))Z.push(J),G.push(J);else G.push(J)}return{rawArgs:G,callbackMap:Q,transferables:Z}}processResult(q){if(j(q))return{rawValue:null,callbackMap:{"":this.registerCallback(q)}};if(q!==null&&typeof q==="object"&&!Array.isArray(q)&&!D(q)&&!(q instanceof ArrayBuffer)){let G={},Q={};for(let Z in q){let $=q[Z];if(j($))G[Z]=this.registerCallback($);else Q[Z]=$}if(Object.keys(G).length>0)return{rawValue:Q,callbackMap:G}}return{rawValue:q,callbackMap:{}}}reconstructResult(q,G){if(""in G)return this.createRemoteCallback(G[""]);let Q=q&&typeof q==="object"?{...q}:{};for(let Z in G){let $=G[Z];if($)Q[Z]=this.createRemoteCallback($)}return Q}buildCallMessage(q,G,Q,Z){return{t:0,id:q,p:G,a:Q,...Object.keys(Z).length>0&&{c:Z}}}call(q,G){if(this.released)return Promise.reject(Error("Endpoint has been released"));let{rawArgs:Q,callbackMap:Z,transferables:$}=this.processArgs(G),J=this.generateCallId(),K=this.buildCallMessage(J,q,Q,Z);return new Promise((W,V)=>{let U=setTimeout(()=>{this.pendingCalls.delete(J),V(Error(`Call timed out: ${q.join(".")}`))},this.options.timeout);this.pendingCalls.set(J,{resolve:W,reject:V,timer:U}),this.send(K,$)})}send(q,G=[]){this.debug("sending",B[q.t],q),this.target.postMessage(q,G)}getPendingCallCount(){return this.pendingCalls.size}hasPendingCalls(){return this.pendingCalls.size>0}async shutdown(q={}){let{timeout:G=O}=q;if(this.released)return{success:!0,timeout:!1};if(!this.hasPendingCalls())return this.release(),{success:!0,timeout:!1};return new Promise((Q)=>{let Z=!1,$=(K)=>{if(Z)return;if(Z=!0,clearTimeout(J),!this.released)this.release();Q(K)},J=setTimeout(()=>{$({success:!1,timeout:!0})},G);for(let[,K]of this.pendingCalls){let{resolve:W,reject:V}=K;K.resolve=(U)=>{if(W(U),!this.hasPendingCalls())$({success:!0,timeout:!1})},K.reject=(U)=>{if(V(U),!this.hasPendingCalls())$({success:!0,timeout:!1})}}})}release(q={}){if(this.released)return;this.released=!0;for(let[,Q]of this.pendingCalls){if(Q.timer)clearTimeout(Q.timer);Q.reject(Error("Endpoint released"))}this.pendingCalls.clear();let{silent:G=!1}=q;if(!G){if(this.remoteCallbacks.size>0){let Q=[...this.remoteCallbacks.keys()];this.send({t:4,id:this.generateCallId(),c:Q})}this.send({t:5,id:this.generateCallId()})}this.remoteCallbacks.clear(),this.callbacks.clear(),this.detachListener()}getTarget(){return this.target}}function j(q){return typeof q==="function"}function D(q){return typeof MessagePort<"u"&&q instanceof MessagePort}function _(q,G){return new L(q,G)}var z=Symbol("rpc:path"),F=Symbol("rpc:proxy-endpoint"),N=Symbol.for("rpc:remote-proxy"),R=Symbol.for("rpc:endpoint"),f=Symbol.for("rpc:release"),E={get(q,G){if(G===N)return!0;if(G===R)return q[F];if(G===f)return()=>{q[F].release()};if(G==="then"){if(q[z].length===0)return;let Q=q[F],Z=q[z],$=Q.call(Z,[]);return $.then.bind($)}if(G==="toJSON")return;if(G===Symbol.toStringTag||G===Symbol.iterator||G===Symbol.asyncIterator||G==="constructor"||G==="prototype")return;if(typeof G==="string"){let Q=q[F],$=[...q[z],G];return A(Q,$)}},apply(q,G,Q){let Z=q[F],$=q[z];return Z.call($,Q)},set(){throw Error("Cannot set properties on a remote proxy")},deleteProperty(){throw Error("Cannot delete properties on a remote proxy")},getPrototypeOf(){return Function.prototype},has(q,G){return G===N||G===R||G===f}};function A(q,G){let Q=function(){};return Q[F]=q,Q[z]=G,new Proxy(Q,E)}function P(q){return A(q,[])}function w(q){if(q===null||q===void 0)return!1;try{return q[N]===!0}catch{return!1}}function y(q){if(w(q))return q[R]}function u(q){if(w(q))q[f]()}class I extends L{pendingBroadcastCalls=new Map;get broadcastTarget(){return this.target}call(q,G){if(this.released)return Promise.reject(Error("Endpoint has been released"));let{rawArgs:Q,callbackMap:Z,transferables:$}=this.processArgs(G),J=this.generateCallId(),K=this.buildCallMessage(J,q,Q,Z);return this.sendBroadcast(J,K,$,q)}sendBroadcast(q,G,Q,Z){return this.debug("sending broadcast",B[G.t],G),new Promise(($,J)=>{let K={resolve:$,reject:J,timer:setTimeout(()=>this.handleTimeout(q,Z),this.options.timeout),expectedCount:1/0,results:[],errors:[]};this.pendingBroadcastCalls.set(q,K),Promise.resolve(this.broadcastTarget.postMessage(G,Q)).then((W)=>this.handleSubscriberCount(q,W))})}handleTimeout(q,G){let Q=this.pendingBroadcastCalls.get(q);if(!Q)return;this.pendingBroadcastCalls.delete(q);let Z=Q.results.length+Q.errors.length,$=Q.expectedCount===1/0?0:Q.expectedCount-Z;if($>0){let J=Error(`Broadcast timed out: ${G.join(".")}`);J.name="TimeoutError";for(let K=0;K<$;K++)Q.errors.push(J)}if(this.debug(`Broadcast timed out with ${Q.results.length} results, ${Q.errors.length} errors (${$} timed out)`),Q.results.length>0||Q.errors.length>0)Q.resolve({results:Q.results,errors:Q.errors});else Q.reject(Error(`Broadcast timed out: ${G.join(".")}`))}handleSubscriberCount(q,G){let Q=this.pendingBroadcastCalls.get(q);if(!Q)return;if(G===0){this.completeBroadcast(q,Q);return}Q.expectedCount=G,this.checkCompletion(q,Q)}checkCompletion(q,G){if(G.results.length+G.errors.length>=G.expectedCount)this.completeBroadcast(q,G)}completeBroadcast(q,G){this.pendingBroadcastCalls.delete(q),clearTimeout(G.timer),G.resolve({results:G.results,errors:G.errors})}handleResult(q){let G=this.pendingBroadcastCalls.get(q.id);if(!G){super.handleResult(q);return}if(q.cm&&Object.keys(q.cm).length>0)G.results.push(this.reconstructResult(q.v,q.cm));else G.results.push(q.v);this.debug(`Broadcast received ${G.results.length}/${G.expectedCount} responses`),this.checkCompletion(q.id,G)}handleError(q){let G=this.pendingBroadcastCalls.get(q.id);if(!G){super.handleError(q);return}G.errors.push(H(q)),this.debug(`Broadcast received error (${G.results.length} results, ${G.errors.length} errors / ${G.expectedCount} expected)`),this.checkCompletion(q.id,G)}getPendingCallCount(){return super.getPendingCallCount()+this.pendingBroadcastCalls.size}hasPendingCalls(){return super.hasPendingCalls()||this.pendingBroadcastCalls.size>0}release(q={}){for(let[,G]of this.pendingBroadcastCalls)clearTimeout(G.timer),G.reject(Error("Endpoint released"));this.pendingBroadcastCalls.clear(),super.release(q)}}function o(q,G){return new I(q,G)}function n(q,G){let Z=_(globalThis,G);return Z.expose(q),Z}function s(q,G){let Q=_(q,G);return P(Q)}export{P as wrap,Y as serializeError,s as remote,u as releaseProxy,w as isProxy,D as isMessagePort,j as isFunction,y as getEndpoint,n as expose,H as deserializeError,_ as createEndpoint,o as createBroadcastEndpoint,N as REMOTE_PROXY,f as RELEASE,F as PROXY_ENDPOINT,z as PATH,B as MessageType,L as Endpoint,R as ENDPOINT,I as BroadcastEndpoint};
2
+ var z;((J)=>{J[J.Call=0]="Call";J[J.Result=1]="Result";J[J.Error=2]="Error";J[J.Callback=3]="Callback";J[J.CallbackRelease=4]="CallbackRelease";J[J.EndpointRelease=5]="EndpointRelease"})(z||={});var y=new Set(["message","name","stack","cause"]);function D(q){let G={e:q.message,n:q.name,s:q.stack},Q={};for(let Z of Object.keys(q))if(!y.has(Z))Q[Z]=q[Z];if(Object.keys(Q).length>0)G.d=Q;if(q.cause instanceof Error)G.c=D(q.cause);else if(q.cause!==void 0)G.c={e:String(q.cause),d:{cause:q.cause}};return G}function S(q){let G=q.c?{cause:S(q.c)}:void 0,Q=Error(q.e,G);if(q.n)Q.name=q.n;if(q.s)Q.stack=q.s;if(q.d)Object.assign(Q,q.d);return Q}var X=Symbol("rpc:path"),V=Symbol("rpc:proxy-endpoint"),C=Symbol.for("rpc:remote-proxy"),_=Symbol.for("rpc:endpoint"),N=Symbol.for("rpc:release"),A=Symbol.for("rpc:callback-release"),b={get(q,G){if(G===C)return!0;if(G===_)return q[V];if(G===N)return()=>{q[V].release()};if(G==="then"){if(q[X].length===0)return;let Q=q[V],Z=q[X],$=Q.call(Z,[]);return $.then.bind($)}if(G==="toJSON")return;if(G===Symbol.toStringTag||G===Symbol.iterator||G===Symbol.asyncIterator||G==="constructor"||G==="prototype")return;if(typeof G==="string"){let Q=q[V],$=[...q[X],G];return w(Q,$)}},apply(q,G,Q){let Z=q[V],$=q[X];return Z.call($,Q)},set(){throw Error("Cannot set properties on a remote proxy")},deleteProperty(){throw Error("Cannot delete properties on a remote proxy")},getPrototypeOf(){return Function.prototype},has(q,G){return G===C||G===_||G===N}};function w(q,G){let Q=function(){};return Q[V]=q,Q[X]=G,new Proxy(Q,b)}function h(q){return w(q,[])}function T(q){if(q===null||q===void 0)return!1;try{return q[C]===!0}catch{return!1}}function u(q){if(T(q))return q[_]}function c(q){if(T(q))q[N]()}function O(q){return q!==null&&typeof q==="object"&&typeof q.__rpc_type==="string"}async function x(q,G){for(let Q of G)if(Q.canHandle(q)){let{transferables:Z=[],payload:$}=await Q.encode(q);return{transferables:Z,value:{__rpc_type:Q.type,...$}}}return{value:q,transferables:[]}}async function f(q,G){if(!O(q))return q;let{__rpc_type:Q,...Z}=q;for(let $ of G)if($.type===Q)return $.decode(Z);return q}var E=30000;class Y{static generateRandomId(){return Math.random().toString(36).slice(2,8)}static extractEndpointId(q){return q.split(":")[1]??""}target;options;exposedApi=null;exposeOptions={};id;callCounter=0;callbackCounter=0;pendingCalls=new Map;callbacks=new Map;remoteCallbacks=new Map;boundMessageHandler;boundCloseHandler;released=!1;releaseHandlers=new Set;constructor(q,G={}){this.target=q,this.id=G.id??Y.generateRandomId(),this.options={id:this.id,timeout:G.timeout??E,onError:G.onError??console.error,debug:G.debug??!1,onRelease:G.onRelease??(()=>{}),transformers:G.transformers??[],transformDepth:G.transformDepth??1},this.boundMessageHandler=this.handleMessage.bind(this),this.boundCloseHandler=this.handleTargetClose.bind(this),this.attachListener()}generateCallId(){return`c:${this.id}:${++this.callCounter}`}generateCallbackId(){return`cb:${this.id}:${++this.callbackCounter}`}attachListener(){if(this.target.addEventListener)this.target.addEventListener("message",this.boundMessageHandler),this.target.addEventListener("close",this.boundCloseHandler),this.target.addEventListener("error",this.boundCloseHandler);else if(this.target.onmessage!==void 0)this.target.onmessage=this.boundMessageHandler}detachListener(){if(this.target.removeEventListener)this.target.removeEventListener("message",this.boundMessageHandler),this.target.removeEventListener("close",this.boundCloseHandler),this.target.removeEventListener("error",this.boundCloseHandler);else if(this.target.onmessage!==void 0)this.target.onmessage=null}handleTargetClose(){if(this.released)return;this.debug("Target closed or errored (remote endpoint likely died)");for(let[q,G]of this.pendingCalls){if(G.timer)clearTimeout(G.timer);G.reject(Error("Remote endpoint disconnected"))}this.pendingCalls.clear();for(let q of this.releaseHandlers)try{q()}catch{}this.releaseHandlers.clear(),this.options.onRelease(),this.released=!0,this.remoteCallbacks.clear(),this.callbacks.clear(),this.detachListener()}debug(...q){if(this.options.debug)console.log("[worker-rpc]",...q)}expose(q,G={}){this.exposedApi=q,this.exposeOptions={maxDepth:G.maxDepth??10}}handleMessage(q){let G=q.data;if(typeof G!=="object"||G===null||!("t"in G))return;switch(this.debug("received",z[G.t],G),G.t){case 0:this.handleCall(G);break;case 1:this.handleResult(G);break;case 2:this.handleError(G);break;case 3:this.handleCallback(G);break;case 4:this.handleCallbackRelease(G);break;case 5:this.handleEndpointRelease();break}}async handleCall(q){let{id:G,p:Q,a:Z,c:$}=q;try{let{method:K,thisArg:J}=this.resolveMethod(Q);if(typeof K!=="function")throw Error(`Method not found: ${Q.join(".")}`);let F=await Promise.all(Z.map(async(L,j)=>{let I=$?.[j];if(I)return this.createRemoteCallback(I);return this.decodeValue(L)})),U=await K.apply(J,F),{rawValue:W,callbackMap:B,transferables:H}=await this.processResult(U);this.send({t:1,id:G,v:W,...Object.keys(B).length>0&&{cm:B}},H)}catch(K){let J=K instanceof Error?K:Error(String(K));this.send({t:2,id:G,...D(J)})}}resolveMethod(q){if(!this.exposedApi)throw Error("No API exposed");let G=this.exposedApi,Q=null;for(let Z=0;Z<q.length;Z++){let $=q[Z];if(G===null||G===void 0)throw Error(`Cannot access property '${$}' of ${G}`);if($===void 0)throw Error(`Invalid path at index ${Z}`);if(Q=G,G=G[$],Z>=(this.exposeOptions.maxDepth??10))throw Error("Maximum nesting depth exceeded")}return{target:this.exposedApi,method:G,thisArg:Q}}resolveResult(q,G){return G&&Object.keys(G).length>0?this.reconstructResult(q,G):this.decodeValue(q)}handleResult(q){let G=this.pendingCalls.get(q.id);if(!G){this.debug("Received result for unknown call:",q.id);return}if(this.pendingCalls.delete(q.id),G.timer)clearTimeout(G.timer);this.resolveResult(q.v,q.cm).then(G.resolve,G.reject)}handleError(q){let G=this.pendingCalls.get(q.id);if(!G){this.debug("Received error for unknown call:",q.id);return}if(this.pendingCalls.delete(q.id),G.timer)clearTimeout(G.timer);G.reject(S(q))}async handleCallback(q){let{id:G,c:Q,a:Z,cb:$}=q,K=this.callbacks.get(Q);if(!K){this.send({t:2,id:G,e:`Callback not found: ${Q}`});return}try{let J=await Promise.all(Z.map(async(H,L)=>{let j=$?.[L];if(j)return this.createRemoteCallback(j);return this.decodeValue(H)})),F=await K.fn(...J);if(K.remaining>0){if(K.remaining--,K.remaining===0)this.callbacks.delete(Q)}let{rawValue:U,callbackMap:W,transferables:B}=await this.processResult(F);this.send({t:1,id:G,v:U,...Object.keys(W).length>0&&{cm:W}},B)}catch(J){let F=J instanceof Error?J:Error(String(J));this.send({t:2,id:G,...D(F)})}}handleCallbackRelease(q){for(let G of q.c)this.callbacks.delete(G)}handleEndpointRelease(){this.debug("Remote endpoint released");for(let q of this.releaseHandlers)try{q()}catch{}this.releaseHandlers.clear(),this.options.onRelease(),this.release({silent:!0})}registerCallback(q,G=-1){let Q=this.generateCallbackId();return this.callbacks.set(Q,{fn:q,remaining:G}),Q}createRemoteCallback(q){let G=this.remoteCallbacks.get(q);if(G)return G;return G=async(...Q)=>this.invokeCallback(q,Q),this.remoteCallbacks.set(q,G),G}invokeCallback(q,G){let Q=this.generateCallId();return new Promise((Z,$)=>{let K=setTimeout(()=>{this.pendingCalls.delete(Q),$(Error(`Callback invocation timed out: ${q}`))},this.options.timeout);this.pendingCalls.set(Q,{resolve:Z,reject:$,timer:K});let J=({rawArgs:U,callbackMap:W,transferables:B})=>{if(!this.released)this.send({t:3,id:Q,c:q,a:U,...Object.keys(W).length>0&&{cb:W}},B)},F=(U)=>{let W=this.pendingCalls.get(Q);if(W)this.pendingCalls.delete(Q),clearTimeout(W.timer),W.reject(U instanceof Error?U:Error(String(U)))};if(this.options.transformers.length===0)J(this.processArgsSync(G));else this.processArgs(G).then(J,F)})}async encodeValue(q,G=this.options.transformDepth){let{value:Q,transferables:Z}=await x(q,this.options.transformers);if(Q!==q)return{encoded:Q,transferables:Z};if(G>0&&q!==null&&typeof q==="object"&&Object.getPrototypeOf(q)===Object.prototype){let $={},K=[];for(let J in q){let F=q[J],{encoded:U,transferables:W}=await this.encodeValue(F,G-1);$[J]=U,K.push(...W)}return{encoded:$,transferables:K}}return{encoded:q,transferables:[]}}async decodeValue(q,G=this.options.transformDepth){if(O(q))return f(q,this.options.transformers);if(G>0&&q!==null&&typeof q==="object"&&Object.getPrototypeOf(q)===Object.prototype){let Q={...q};for(let Z of Object.keys(Q))Q[Z]=await this.decodeValue(Q[Z],G-1);return Q}return q}processArgsSync(q){let G=[],Q={},Z=[];for(let $=0;$<q.length;$++){let K=q[$];if(R(K))Q[$]=this.registerCallback(K),G.push(null);else if(M(K))Z.push(K),G.push(K);else G.push(K)}return{rawArgs:G,callbackMap:Q,transferables:Z}}async processArgs(q){let{rawArgs:G,callbackMap:Q,transferables:Z}=this.processArgsSync(q);for(let $=0;$<G.length;$++){if(G[$]===null&&Q[$]!==void 0)continue;let{encoded:K,transferables:J}=await this.encodeValue(G[$]);G[$]=K,Z.push(...J)}return{rawArgs:G,callbackMap:Q,transferables:Z}}async processResult(q){let G=[];if(R(q))return{rawValue:null,callbackMap:{"":this.registerCallback(q)},transferables:G};if(q!==null&&typeof q==="object"&&!Array.isArray(q)&&!M(q)&&!(q instanceof ArrayBuffer)){let{value:$,transferables:K}=await x(q,this.options.transformers);if($!==q)return G.push(...K),{rawValue:$,callbackMap:{},transferables:G};let J={},F={};for(let W in q){let B=q[W];if(R(B))J[W]=this.registerCallback(B);else{let{encoded:H,transferables:L}=await this.encodeValue(B);F[W]=H,G.push(...L)}}let U=q[A];if(typeof U==="function")this.releaseHandlers.add(U);if(Object.keys(J).length>0)return{rawValue:F,callbackMap:J,transferables:G};return{rawValue:F,callbackMap:{},transferables:G}}let{encoded:Q,transferables:Z}=await this.encodeValue(q);return G.push(...Z),{rawValue:Q,callbackMap:{},transferables:G}}async reconstructResult(q,G){if(""in G)return this.createRemoteCallback(G[""]);let Q=q&&typeof q==="object"?{...q}:{};for(let Z of Object.keys(Q))Q[Z]=await this.decodeValue(Q[Z]);for(let Z in G){let $=G[Z];if($)Q[Z]=this.createRemoteCallback($)}return Q}buildCallMessage(q,G,Q,Z){return{t:0,id:q,p:G,a:Q,...Object.keys(Z).length>0&&{c:Z}}}call(q,G){if(this.released)return Promise.reject(Error("Endpoint has been released"));let Q=this.generateCallId();return new Promise((Z,$)=>{let K=setTimeout(()=>{this.pendingCalls.delete(Q),$(Error(`Call timed out: ${q.join(".")}`))},this.options.timeout);this.pendingCalls.set(Q,{resolve:Z,reject:$,timer:K});let J=({rawArgs:U,callbackMap:W,transferables:B})=>{if(!this.released)this.send(this.buildCallMessage(Q,q,U,W),B)},F=(U)=>{let W=this.pendingCalls.get(Q);if(W)this.pendingCalls.delete(Q),clearTimeout(W.timer),W.reject(U instanceof Error?U:Error(String(U)))};if(this.options.transformers.length===0)J(this.processArgsSync(G));else this.processArgs(G).then(J,F)})}send(q,G=[]){this.debug("sending",z[q.t],q),this.target.postMessage(q,G)}getPendingCallCount(){return this.pendingCalls.size}hasPendingCalls(){return this.pendingCalls.size>0}async shutdown(q={}){let{timeout:G=E}=q;if(this.released)return{success:!0,timeout:!1};if(!this.hasPendingCalls())return this.release(),{success:!0,timeout:!1};return new Promise((Q)=>{let Z=!1,$=(J)=>{if(Z)return;if(Z=!0,clearTimeout(K),!this.released)this.release();Q(J)},K=setTimeout(()=>{$({success:!1,timeout:!0})},G);for(let[,J]of this.pendingCalls){let{resolve:F,reject:U}=J;J.resolve=(W)=>{if(F(W),!this.hasPendingCalls())$({success:!0,timeout:!1})},J.reject=(W)=>{if(U(W),!this.hasPendingCalls())$({success:!0,timeout:!1})}}})}release(q={}){if(this.released)return;this.released=!0;for(let[,Q]of this.pendingCalls){if(Q.timer)clearTimeout(Q.timer);Q.reject(Error("Endpoint released"))}this.pendingCalls.clear();let{silent:G=!1}=q;if(!G){if(this.remoteCallbacks.size>0){let Q=[...this.remoteCallbacks.keys()];this.send({t:4,id:this.generateCallId(),c:Q})}this.send({t:5,id:this.generateCallId()})}this.remoteCallbacks.clear(),this.callbacks.clear(),this.releaseHandlers.clear(),this.detachListener()}getTarget(){return this.target}}function R(q){return typeof q==="function"}function M(q){return typeof MessagePort<"u"&&q instanceof MessagePort}function P(q,G){return new Y(q,G)}class k extends Y{pendingBroadcastCalls=new Map;get broadcastTarget(){return this.target}call(q,G){if(this.released)return Promise.reject(Error("Endpoint has been released"));let Q=this.generateCallId(),Z=($,K,J)=>{let F=this.buildCallMessage(Q,q,$,K);this.debug("sending broadcast",z[F.t],F),Promise.resolve(this.broadcastTarget.postMessage(F,J)).then((U)=>this.handleSubscriberCount(Q,U))};return new Promise(($,K)=>{let J={resolve:$,reject:K,timer:setTimeout(()=>this.handleTimeout(Q,q),this.options.timeout),expectedCount:1/0,results:[],errors:[]};if(this.pendingBroadcastCalls.set(Q,J),this.options.transformers.length===0){let{rawArgs:F,callbackMap:U,transferables:W}=this.processArgsSync(G);Z(F,U,W)}else this.processArgs(G).then(({rawArgs:F,callbackMap:U,transferables:W})=>{if(this.released)return;Z(F,U,W)},(F)=>{let U=this.pendingBroadcastCalls.get(Q);if(U)this.pendingBroadcastCalls.delete(Q),clearTimeout(U.timer),U.reject(F instanceof Error?F:Error(String(F)))})})}handleTimeout(q,G){let Q=this.pendingBroadcastCalls.get(q);if(!Q)return;this.pendingBroadcastCalls.delete(q);let Z=Q.results.length+Q.errors.length,$=Q.expectedCount===1/0?0:Q.expectedCount-Z;if($>0){let K=Error(`Broadcast timed out: ${G.join(".")}`);K.name="TimeoutError";for(let J=0;J<$;J++)Q.errors.push(K)}if(this.debug(`Broadcast timed out with ${Q.results.length} results, ${Q.errors.length} errors (${$} timed out)`),Q.results.length>0||Q.errors.length>0)Q.resolve({results:Q.results,errors:Q.errors});else Q.reject(Error(`Broadcast timed out: ${G.join(".")}`))}handleSubscriberCount(q,G){let Q=this.pendingBroadcastCalls.get(q);if(!Q)return;if(G===0){this.completeBroadcast(q,Q);return}Q.expectedCount=G,this.checkCompletion(q,Q)}checkCompletion(q,G){if(G.results.length+G.errors.length>=G.expectedCount)this.completeBroadcast(q,G)}completeBroadcast(q,G){this.pendingBroadcastCalls.delete(q),clearTimeout(G.timer),G.resolve({results:G.results,errors:G.errors})}handleResult(q){let G=this.pendingBroadcastCalls.get(q.id);if(!G){super.handleResult(q);return}this.resolveResult(q.v,q.cm).then((Q)=>{G.results.push(Q),this.debug(`Broadcast received ${G.results.length}/${G.expectedCount} responses`),this.checkCompletion(q.id,G)},(Q)=>{G.errors.push(Q instanceof Error?Q:Error(String(Q))),this.checkCompletion(q.id,G)})}handleError(q){let G=this.pendingBroadcastCalls.get(q.id);if(!G){super.handleError(q);return}G.errors.push(S(q)),this.debug(`Broadcast received error (${G.results.length} results, ${G.errors.length} errors / ${G.expectedCount} expected)`),this.checkCompletion(q.id,G)}getPendingCallCount(){return super.getPendingCallCount()+this.pendingBroadcastCalls.size}hasPendingCalls(){return super.hasPendingCalls()||this.pendingBroadcastCalls.size>0}release(q={}){for(let[,G]of this.pendingBroadcastCalls)clearTimeout(G.timer),G.reject(Error("Endpoint released"));this.pendingBroadcastCalls.clear(),super.release(q)}}function a(q,G){return new k(q,G)}function Zq(q,G){let Z=P(globalThis,G);return Z.expose(q),Z}function $q(q,G){let Q=P(q,G);return h(Q)}export{h as wrap,D as serializeError,$q as remote,c as releaseProxy,T as isProxy,M as isMessagePort,R as isFunction,O as isEncodedValue,u as getEndpoint,Zq as expose,S as deserializeError,P as createEndpoint,a as createBroadcastEndpoint,x as applyEncoders,f as applyDecoders,C as REMOTE_PROXY,N as RELEASE,V as PROXY_ENDPOINT,X as PATH,z as MessageType,Y as Endpoint,_ as ENDPOINT,A as CALLBACK_RELEASE,k as BroadcastEndpoint};
package/dist/proxy.d.ts CHANGED
@@ -11,6 +11,8 @@ export declare const REMOTE_PROXY: unique symbol;
11
11
  export declare const ENDPOINT: unique symbol;
12
12
  /** Symbol for releasing a proxy */
13
13
  export declare const RELEASE: unique symbol;
14
+ /** Symbol for registering a cleanup handler on a returned object, invoked when the remote endpoint releases */
15
+ export declare const CALLBACK_RELEASE: unique symbol;
14
16
  /**
15
17
  * Wrap a worker/endpoint to create a type-safe remote API proxy
16
18
  *
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Marker interface for types that cross the worker boundary via a transformer.
3
+ * A return type that extends (or intersects) `Transferred` is passed through
4
+ * `RemoteReturnValue` unchanged — the transformer handles encoding/decoding at
5
+ * runtime, so the remote type stays identical to the local type.
6
+ *
7
+ * The built-in `Response` and `Request` globals extend this automatically.
8
+ * For custom transformer-managed types, intersect at the return site:
9
+ *
10
+ * ```typescript
11
+ * interface WorkerApi {
12
+ * build(): MyClass & Transferred;
13
+ * }
14
+ * ```
15
+ */
16
+ export interface Transferred {
17
+ }
18
+ /**
19
+ * Wire envelope written by the library before sending and read after receiving.
20
+ * `__rpc_type` is injected by `applyEncoders` and stripped by `applyDecoders` —
21
+ * transformer implementations never touch this field.
22
+ */
23
+ export interface EncodedValue {
24
+ __rpc_type: string;
25
+ [key: string]: unknown;
26
+ }
27
+ /**
28
+ * Return value of `encode`: the plain data payload plus optional transferables.
29
+ *
30
+ * @template P - Shape of the payload (default: `Record<string, unknown>`).
31
+ */
32
+ export interface EncodeResult<P extends Record<string, unknown> = Record<string, unknown>> {
33
+ /** Structured-clone-safe payload. */
34
+ payload: P;
35
+ /** Transferables to zero-copy transfer via postMessage. */
36
+ transferables?: Transferable[];
37
+ }
38
+ /**
39
+ * Encodes a non-cloneable value into a structured-clone-safe payload before
40
+ * sending, and decodes it back on the receiving side.
41
+ *
42
+ * The library injects/strips `__rpc_type` automatically, so `encode` returns
43
+ * plain data and `decode` receives plain data.
44
+ *
45
+ * @template T - The type this transformer handles.
46
+ * @template P - Shape of the encoded payload. Typing this removes casts in `decode`.
47
+ *
48
+ * @example
49
+ * ```typescript
50
+ * interface MyPayload extends Record<string, unknown> { data: string }
51
+ *
52
+ * const myTransformer: Transformer<MyClass, MyPayload> = {
53
+ * type: 'MyClass',
54
+ * canHandle: (value): value is MyClass => value instanceof MyClass,
55
+ * async encode(value) {
56
+ * return { payload: { data: value.serialize() } };
57
+ * },
58
+ * decode(payload) {
59
+ * return MyClass.deserialize(payload.data);
60
+ * },
61
+ * };
62
+ * ```
63
+ */
64
+ export interface Transformer<T = unknown, P extends Record<string, unknown> = Record<string, unknown>> {
65
+ /**
66
+ * Unique wire tag. The library sets `__rpc_type` to this value when encoding
67
+ * and uses it to route incoming envelopes to the correct decoder.
68
+ */
69
+ type: string;
70
+ /** Return true if this transformer can handle the given value. */
71
+ canHandle(value: unknown): value is T;
72
+ /**
73
+ * Encode a value into a structured-clone-safe payload.
74
+ */
75
+ encode(value: T): Promise<EncodeResult<P>>;
76
+ /**
77
+ * Decode a payload back into the original type.
78
+ */
79
+ decode(payload: P): T | Promise<T>;
80
+ }
81
+ /** Returns true if `value` is a transformer-encoded wire envelope. */
82
+ export declare function isEncodedValue(value: unknown): value is EncodedValue;
83
+ /**
84
+ * Run the first matching transformer's encoder on `value`.
85
+ * Injects `__rpc_type` automatically. Returns the value unchanged if no transformer matches.
86
+ */
87
+ export declare function applyEncoders(value: unknown, transformers: Transformer[]): Promise<{
88
+ value: unknown;
89
+ transferables: Transferable[];
90
+ }>;
91
+ /**
92
+ * Run the first matching transformer's decoder on a wire envelope.
93
+ * Strips `__rpc_type` before calling `decode`. Returns the value unchanged
94
+ * if it is not an encoded envelope or no transformer matches.
95
+ */
96
+ export declare function applyDecoders(value: unknown, transformers: Transformer[]): Promise<unknown>;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Built-in transformers for non-cloneable Web API types.
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * import { createEndpoint } from '@xentobias/worker-rpc';
7
+ * import { responseTransformer, requestTransformer } from '@xentobias/worker-rpc/transformers';
8
+ *
9
+ * const endpoint = createEndpoint(worker, {
10
+ * transformers: [responseTransformer, requestTransformer],
11
+ * });
12
+ * ```
13
+ */
14
+ export { responseTransformer } from './response';
15
+ export { requestTransformer } from './request';
16
+ export { headersToTuples } from './utils';
17
+ export type { ResponsePayload } from './response';
18
+ export type { RequestPayload } from './request';
19
+ export type { Transformer, EncodedValue, EncodeResult, Transferred } from '../transformer';
@@ -0,0 +1,2 @@
1
+ // @bun
2
+ function T(g){let x=[];return g.forEach((m,H)=>x.push([H,m])),x}var j={type:"Response",canHandle(g){return typeof Response<"u"&&g instanceof Response},async encode(g){let x=g.bodyUsed?null:await g.arrayBuffer();return{transferables:x!==null?[x]:[],payload:{body:x,status:g.status,statusText:g.statusText,headers:T(g.headers)}}},decode(g){return new Response(g.body,{status:g.status,statusText:g.statusText,headers:new Headers(g.headers)})}};var E={type:"Request",canHandle(g){return typeof Request<"u"&&g instanceof Request},async encode(g){let x=g.bodyUsed?null:await g.arrayBuffer();return{transferables:x!==null?[x]:[],payload:{url:g.url,method:g.method,headers:T(g.headers),body:x,mode:g.mode,credentials:g.credentials,cache:g.cache,redirect:g.redirect,referrer:g.referrer,integrity:g.integrity}}},decode(g){return new Request(g.url,{method:g.method,headers:new Headers(g.headers),body:g.body,mode:g.mode,credentials:g.credentials,cache:g.cache,redirect:g.redirect,referrer:g.referrer,integrity:g.integrity})}};export{j as responseTransformer,E as requestTransformer,T as headersToTuples};
@@ -0,0 +1,36 @@
1
+ import type { Transformer, Transferred } from '../transformer';
2
+ declare global {
3
+ interface Request extends Transferred {
4
+ }
5
+ }
6
+ /** Wire-safe payload for a `Request`. */
7
+ export interface RequestPayload extends Record<string, unknown> {
8
+ url: string;
9
+ method: string;
10
+ headers: [string, string][];
11
+ body: ArrayBuffer | null;
12
+ mode: RequestMode;
13
+ credentials: RequestCredentials;
14
+ cache: RequestCache;
15
+ redirect: RequestRedirect;
16
+ referrer: string;
17
+ integrity: string;
18
+ }
19
+ /**
20
+ * Built-in transformer for the Web `Request` API.
21
+ * Buffers the body into an `ArrayBuffer` and zero-copy transfers it.
22
+ *
23
+ * Note: if `request.bodyUsed` is `true` when encoding, the decoded
24
+ * `Request` will have a `null` body. Read the body only after the RPC call returns.
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * import { createEndpoint } from '@xentobias/worker-rpc';
29
+ * import { requestTransformer } from '@xentobias/worker-rpc/transformers';
30
+ *
31
+ * const endpoint = createEndpoint(worker, {
32
+ * transformers: [requestTransformer],
33
+ * });
34
+ * ```
35
+ */
36
+ export declare const requestTransformer: Transformer<Request, RequestPayload>;
@@ -0,0 +1,30 @@
1
+ import type { Transformer, Transferred } from '../transformer';
2
+ declare global {
3
+ interface Response extends Transferred {
4
+ }
5
+ }
6
+ /** Wire-safe payload for a `Response`. */
7
+ export interface ResponsePayload extends Record<string, unknown> {
8
+ body: ArrayBuffer | null;
9
+ status: number;
10
+ statusText: string;
11
+ headers: [string, string][];
12
+ }
13
+ /**
14
+ * Built-in transformer for the Web `Response` API.
15
+ * Buffers the body into an `ArrayBuffer` and zero-copy transfers it.
16
+ *
17
+ * Note: if `response.bodyUsed` is `true` when encoding, the decoded
18
+ * `Response` will have a `null` body. Read the body only after the RPC call returns.
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * import { createEndpoint } from '@xentobias/worker-rpc';
23
+ * import { responseTransformer } from '@xentobias/worker-rpc/transformers';
24
+ *
25
+ * const endpoint = createEndpoint(worker, {
26
+ * transformers: [responseTransformer],
27
+ * });
28
+ * ```
29
+ */
30
+ export declare const responseTransformer: Transformer<Response, ResponsePayload>;
@@ -0,0 +1,2 @@
1
+ /** Converts a `Headers` object to an array of `[name, value]` tuples for structured-clone transfer. */
2
+ export declare function headersToTuples(headers: Headers): [string, string][];
package/dist/types.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { CallbackId, CallId } from './endpoint';
2
+ import type { Transferred } from './transformer';
2
3
  /** Path to a nested method (e.g., ['db', 'users', 'find']) */
3
4
  export type MethodPath = string[];
4
5
  /** Message types for the RPC protocol */
@@ -83,10 +84,10 @@ export type RpcMessage = CallMessage | ResultMessage | ErrorMessage | CallbackMe
83
84
  export type Unpromise<T> = T extends Promise<infer U> ? U : T;
84
85
  /**
85
86
  * Transform first-level functions in a return value to async versions.
86
- * This matches the runtime behavior where only first-level object properties
87
- * are scanned for functions (for performance reasons).
87
+ * Types that extend `Transferred` (e.g. `Response`, `Request`) are passed
88
+ * through unchanged their transformer handles encoding/decoding at runtime.
88
89
  */
89
- export type RemoteReturnValue<T> = T extends (...args: infer A) => infer R ? (...args: A) => Promise<Unpromise<R>> : T extends object ? {
90
+ export type RemoteReturnValue<T> = T extends Transferred ? T : T extends (...args: infer A) => infer R ? (...args: A) => Promise<Unpromise<R>> : T extends object ? {
90
91
  [K in keyof T]: T[K] extends (...args: infer A) => infer R ? (...args: A) => Promise<Unpromise<R>> : T[K];
91
92
  } : T;
92
93
  /** Helper to convert a single property to its remote version */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xentobias/worker-rpc",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
4
4
  "description": "High-performance, type-safe RPC for Workers",
5
5
  "module": "src/index.ts",
6
6
  "main": "dist/index.js",
@@ -13,10 +13,14 @@
13
13
  ".": {
14
14
  "types": "./dist/index.d.ts",
15
15
  "import": "./dist/index.js"
16
+ },
17
+ "./transformers": {
18
+ "types": "./dist/transformers/index.d.ts",
19
+ "import": "./dist/transformers/index.js"
16
20
  }
17
21
  },
18
22
  "scripts": {
19
- "build": "bun build ./src/index.ts --minify --outdir ./dist --target bun && tsc --declaration",
23
+ "build": "bun build ./src/index.ts --minify --outdir ./dist --target bun && bun build ./src/transformers/index.ts --minify --outdir ./dist/transformers --target bun && tsc --declaration",
20
24
  "typecheck": "tsc --noEmit",
21
25
  "test": "bun test",
22
26
  "prepublishOnly": "bun run build",