@mateosuarezdev/brpc 1.0.83 → 1.0.84

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
@@ -278,6 +278,18 @@ router.publish(
278
278
 
279
279
  Parameters in the route path (e.g. `":roomId"`) scope the subscription topic — subscribers only receive messages matching their params.
280
280
 
281
+ Use `.onSubscribe()` for admission checks. It runs after dynamic topic params are resolved and before the socket joins the topic. Normal `.use()` middleware runs only when a client publishes to a subscription; server-originated `router.publish()` calls remain trusted.
282
+
283
+ ```ts
284
+ const userChanges = procedure
285
+ .onSubscribe(async (ctx) => {
286
+ if (!ctx.session || ctx.params.userId !== ctx.session.sub) {
287
+ throw BRPCError.unauthorized("Unauthorized");
288
+ }
289
+ })
290
+ .subscription(async ({ ctx }) => ({ changed: true, userId: ctx.params.userId }));
291
+ ```
292
+
281
293
  ---
282
294
 
283
295
  ## Middlewares
@@ -638,12 +650,39 @@ const html = await client.routes.page.html();
638
650
 
639
651
  // Subscription
640
652
  const { unsubscribe, publish } = client.routes.messages[":roomId"].subscription(
641
- (message) => console.log("received:", message),
653
+ (error, message) => {
654
+ if (error) return console.error(error.type, error.message);
655
+ console.log("received:", message);
656
+ },
657
+ { params: { roomId: "general" } },
642
658
  );
643
659
  publish({ roomId: "general", text: "hello" });
644
660
  unsubscribe();
645
661
  ```
646
662
 
663
+ Subscriptions wait for WebSocket authentication by default. Public feeds can subscribe immediately while auth is refreshing with `{ auth: "none" }`.
664
+
665
+ ### Dynamic Client Route Params
666
+
667
+ Pass dynamic route values through the second options object. Client inputs stay separate from path params, and the resolved path is used for transport and cache-key identity.
668
+
669
+ ```ts
670
+ await client.routes.users[":userId"].query(
671
+ { includePosts: true },
672
+ { params: { userId: "usr_123" } },
673
+ );
674
+
675
+ client.routes.users[":userId"].getArrayKey(
676
+ { includePosts: true },
677
+ { params: { userId: "usr_123" } },
678
+ );
679
+
680
+ client.routes.billingChanges[":userId"].subscription(
681
+ (error, message) => {},
682
+ { params: { userId: "usr_123" }, auth: "required" },
683
+ );
684
+ ```
685
+
647
686
  ### BrpcClientOptions
648
687
 
649
688
  | Option | Type | Description |
@@ -743,8 +782,14 @@ function ChatRoom({ roomId }: { roomId: string }) {
743
782
  const [messages, setMessages] = useState<Message[]>([]);
744
783
 
745
784
  const { publish } = useSubscription(
746
- (callback) => client.routes.messages[":roomId"].subscription(callback),
747
- (message) => setMessages((prev) => [...prev, message]),
785
+ (callback) =>
786
+ client.routes.messages[":roomId"].subscription(callback, {
787
+ params: { roomId },
788
+ }),
789
+ (error, message) => {
790
+ if (error || !message) return;
791
+ setMessages((prev) => [...prev, message]);
792
+ },
748
793
  );
749
794
 
750
795
  return (
@@ -1,4 +1,4 @@
1
1
  // @bun
2
- function y(q){return(J)=>{if(!J)return null;return`${q}/${J}`}}class V extends Error{status;code;clientCode;data;constructor(q,J,O,N,G){super(q);this.name="BrpcClientError",this.status=J,this.code=O,this.clientCode=N,this.data=G}isClientError(q){return this.clientCode===q}isUnauthorized(){return this.status===401}isForbidden(){return this.status===403}isNotFound(){return this.status===404}isValidationError(){return this.status===400||this.code==="BAD_REQUEST"}}class U extends V{type;constructor(q,J){super(J.message??"WebSocket request failed",J.httpStatus??0,J.code,J.clientCode,J.data);this.name="BrpcClientSubscriptionError",this.type=q}}var x=(q)=>{return q instanceof File||q instanceof Blob||typeof q==="object"&&q!==null&&"uri"in q&&"name"in q&&"type"in q};function f(q){if(!q)return null;return q.replace(/\//g,"")}function g(q){let J=q&&q.length>0?q:typeof window<"u"?window.location.origin:"";return J=J.endsWith("/")?J.slice(0,-1):J,J}function m(q,J){let O=J?`/${J}/ws`:"/ws";return`${q.replace(/^http/,"ws")}${O}`}function k(q,J){if(q&&J)return`/${q}/${J}`;else if(q&&!J)return`/${q}`;else if(!q&&J)return`/${J}`;else return""}class h{baseUrl;ws=null;subscriptions=new Map;messageQueue=[];isConnected=!1;reconnectTimeout=null;WebSocketImpl;getHeaders;authToken=null;debug;pendingAuth=!1;authActions=[];constructor(q,J,O,N=!1,G){this.baseUrl=q;this.WebSocketImpl=J,this.debug=N&&G==="development",this.getHeaders=async()=>{if(typeof O==="function")return await O();return O??{}},this.connect()}startTopicSubscription(q){let J=this.subscriptions.get(q);if(!J)return;if(J.status="pending",J.auth==="required"&&this.pendingAuth){this.authActions.push(()=>this.startTopicSubscription(q));return}this.send({type:"subscribe",topic:q},{allowDuringPendingAuth:J.auth==="none"})}async connect(){try{let q=await this.getHeaders();this.authToken=q.Authorization||null;let J=this.baseUrl;if(this.debug)console.log(`Connecting to WebSocket: ${J}`);this.ws=new this.WebSocketImpl(J),this.ws.onopen=async()=>{if(this.debug)console.log("WebSocket connection established");if(this.isConnected=!0,this.authActions=[],this.authToken){if(this.pendingAuth=!0,this.send({type:"authenticate",token:this.authToken}),this.debug)console.log("Sent authentication token, waiting for response");for(let O of this.subscriptions.keys())this.startTopicSubscription(O);this.authActions.push(()=>{if(this.messageQueue.length>0&&this.debug)console.log(`Sending ${this.messageQueue.length} queued messages`);this.messageQueue.forEach((O)=>this.send(O)),this.messageQueue=[]})}else{for(let O of this.subscriptions.keys())this.startTopicSubscription(O);if(this.messageQueue.length>0&&this.debug)console.log(`Sending ${this.messageQueue.length} queued messages`);this.messageQueue.forEach((O)=>this.send(O)),this.messageQueue=[]}},this.ws.onmessage=(O)=>{try{if(this.debug)console.log("WebSocket message received:",O.data);let N=JSON.parse(O.data);if(N.type==="auth_success"){let G=N.authenticated!==!1;if(this.debug)console.log(`WebSocket ${G?"authentication":"deauthentication"} successful`);if(this.pendingAuth=!1,G)this.authActions.forEach((T)=>T());this.authActions=[];return}if(N.type==="auth_error"){console.error("WebSocket authentication failed:",this.createClientError(N.error)),this.pendingAuth=!1,this.authActions=[];return}if(this.debug)console.log("Got new message",N);if(N.type==="subscribe_success"&&N.topic&&this.subscriptions.has(N.topic))this.subscriptions.get(N.topic).status="subscribed";else if(N.type==="publish"&&N.topic&&this.subscriptions.has(N.topic)){let{callbacks:G}=this.subscriptions.get(N.topic);if(this.debug)console.log("Calling subscription callbacks");G?.forEach((T)=>T(void 0,N.data))}else if(["subscribe_error","publish_error"].includes(N.type)&&N.topic&&this.subscriptions.has(N.topic)){let G=this.createSubscriptionError(N.type,N.error),T=this.subscriptions.get(N.topic);T.status="failed",T.callbacks.forEach((z)=>z(G))}else if(N.type==="unsubscribe_error")console.error(`WebSocket unsubscribe failed for topic ${N.topic??"unknown"}:`,this.createClientError(N.error));else if(N.error)console.error("WebSocket error:",this.createClientError(N.error));else console.warn("Unhandled WebSocket message format:",N)}catch(N){if(this.debug)console.error("Error processing WebSocket message:",N),console.error("Raw message:",O.data)}},this.ws.onclose=(O)=>{if(this.isConnected=!1,this.pendingAuth=!1,this.subscriptions.forEach((N)=>{N.status="stale"}),this.debug)console.log(`WebSocket connection closed. Code: ${O.code}, Reason: ${O.reason}`);this.reconnectTimeout=setTimeout(()=>this.connect(),2000)},this.ws.onerror=(O)=>{if(this.debug)console.log("WebSocket error:",O)}}catch(q){if(this.debug)console.log("Failed to create WebSocket connection:",q);this.pendingAuth=!1,this.reconnectTimeout=setTimeout(()=>this.connect(),2000)}}createClientError(q){return new V(q?.message??"WebSocket request failed",q?.httpStatus??0,q?.code,q?.clientCode,q?.data)}createSubscriptionError(q,J){return new U(q,J??{})}disconnect(){if(this.reconnectTimeout)clearTimeout(this.reconnectTimeout),this.reconnectTimeout=null;if(this.ws)this.ws.close(),this.ws=null;this.isConnected=!1,this.pendingAuth=!1,this.authActions=[],this.subscriptions.clear()}send(q,J={}){if(this.isConnected&&this.ws?.readyState===this.WebSocketImpl.OPEN){if(this.pendingAuth&&q.type!=="authenticate"&&!J.allowDuringPendingAuth){if(this.messageQueue.push(q),this.debug)console.log("Message queued until authentication completes:",q);return}let O=JSON.stringify(q);if(this.ws.send(O),this.debug)console.log("WebSocket message sent:",O)}else if(this.messageQueue.push(q),this.debug)console.log("WebSocket message queued (not connected):",q)}subscribe(q,J,O={}){let N=this.subscriptions.get(q);if(!N){if(N={auth:O.auth??"required",callbacks:new Set,status:"pending"},this.subscriptions.set(q,N),N.callbacks.add(J),this.isConnected)this.startTopicSubscription(q)}else N.callbacks.add(J);return{unsubscribe:()=>this.unsubscribe(q,J),publish:(G)=>this.publish(q,G,J)}}unsubscribe(q,J){let O=this.subscriptions.get(q);if(O){if(O.callbacks.delete(J),O.callbacks.size===0){if(this.subscriptions.delete(q),this.send({type:"unsubscribe",topic:q}),this.debug)console.log(`Unsubscribed from topic: ${q}`)}}}publish(q,J,O){let N=this.subscriptions.get(q);if(N?.status!=="subscribed"){O(this.createSubscriptionError("publish_error",{message:"Cannot publish to a topic that is not subscribed",code:"SUBSCRIPTION_NOT_SUBSCRIBED",clientCode:"SUBSCRIPTION_NOT_SUBSCRIBED",data:{status:N?.status??"unsubscribed"}}));return}if(this.send({type:"publish",topic:q,data:J}),this.debug)console.log(`Published to topic: ${q}`,J)}async updateAuth(){let J=(await this.getHeaders()).Authorization||"",O=J.startsWith("Bearer ")?J.substring(7).trim():J.trim(),N=O.length>0,G=this.authToken!==null&&this.authToken.trim().length>0;if((N!==G||O!==this.authToken)&&this.isConnected){if(this.authToken=O,this.pendingAuth=N,this.send({type:"authenticate",token:J}),this.debug)if(N)console.log("Updated authentication token, waiting for confirmation");else console.log("Sent empty token for deauthentication"),this.pendingAuth=!1,this.authActions=[]}}}function Yq(q,J={}){let O=J.fetch||fetch,N=J.WebSocket??(typeof WebSocket<"u"?WebSocket:null),G=J.prefix??"",T=J.apiPrefix??"",z=J.debug??!1,c=J.s3Endpoint,d=J.nodeEnv;if(!J.s3Endpoint)throw Error("BRPC Client: Pass s3Endpoint option to createBrpcClient");if(!J.nodeEnv)throw Error("BRPC Client: Pass nodeEnv option to createBrpcClient");if(!N)throw Error("WebSocket is not available in this environment");let W=J.headers??{},u=async()=>{if(typeof W==="function")return await W();return W},K=g(q),P=f(J.prefix),l=f(J.apiPrefix),E=k(P,l),v=m(K,P),B=new h(v,N,W,z,d),S=async(X=!0)=>{let M={};if(X)M["Content-Type"]="application/json";let Q=await u();return{...M,...Q}},n=async(X)=>{if(!X.ok){let Q;try{Q=await X.json()}catch{try{Q={error:await X.text()}}catch{Q={error:"Failed to parse error response"}}}if(Q?.error&&typeof Q.error==="object"){let w=Q.error;throw new V(w.message||X.statusText,X.status,w.code,w.clientCode,w.data)}let $=Q?.error||Q?.message||X.statusText;throw new V($,X.status,void 0,void 0,Q)}let M=X.headers.get("Content-Type")||"";try{if(M.includes("application/json"))return(await X.json()).data;else if(M.includes("text/"))return X.text();else return X.blob()}catch(Q){throw new V(`Failed to parse response: ${Q instanceof Error?Q.message:"Unknown error"}`,X.status,"PARSE_ERROR")}},H=async(X,M)=>{try{let Q=await O(X,M);return await n(Q)}catch(Q){if(Q instanceof V)throw Q;throw new V(Q instanceof Error?Q.message:"Network request failed",0,"NETWORK_ERROR")}},b=(X=[])=>{return new Proxy({},{get(M,Q){if(typeof Q==="symbol")return;if(Q==="query"||Q==="mutation"||Q==="formMutation"||Q==="subscription"||Q==="getStringKey"||Q==="getArrayKey"||Q==="getNoInputsArrayKey"){let $=X.join("/"),w=`${K}${E}/${$}`;if(z)console.log(`BRPC ${Q} procedure path: ${$}`),console.log(`BRPC ${Q} full URL: ${w}`);if(Q==="query")return async(Z)=>{let j=await S(!0);if(z)console.log(`BRPC query request to ${$}`,{input:Z,headers:j});let _=new URL(w);if(Z&&typeof Z==="object")Object.entries(Z).forEach(([C,F])=>{if(F!==void 0)_.searchParams.append(C,String(F))});let L=await H(_.toString(),{method:"GET",headers:j,credentials:"include"});if(z)console.log(`BRPC query response from ${$}:`,L);return L};else if(Q==="mutation")return async(Z)=>{let j=await S(!0);if(z)console.log(`BRPC mutation request to ${$}`,{input:Z,headers:j});let _=JSON.stringify(Z),L=await H(w,{method:"POST",headers:j,body:_,credentials:"include"});if(z)console.log(`BRPC mutation response from ${$}:`,L);return L};else if(Q==="formMutation")return async(Z)=>{let j=await S(!1);if(z)console.log(`BRPC formMutation request to ${$}`,{input:Z,headers:j});let _=new FormData,L=(F,Y,I)=>{let R=I?`${I}[${F}]`:F;if(Y===null||Y===void 0)return;else if(x(Y))_.append(R,Y);else if(Array.isArray(Y))Y.forEach((A,D)=>{if(x(A))_.append(R,A);else if(typeof A==="object"&&A!==null)Object.entries(A).forEach(([i,o])=>{L(i,o,`${R}[${D}]`)});else _.append(`${R}[${D}]`,String(A))});else if(typeof Y==="object"&&Y!==null)Object.entries(Y).forEach(([A,D])=>{L(A,D,R)});else _.append(R,String(Y))};Object.entries(Z).forEach(([F,Y])=>{L(F,Y)});let C=await H(w,{method:"POST",headers:j,body:_,credentials:"include"});if(z)console.log(`BRPC formMutation response from ${$}:`,C);return C};else if(Q==="subscription")return(Z,j)=>{if(z)console.log(`BRPC subscription to ${$}`);let _=E?`${E.slice(1)}/${$}`:$;return B.updateAuth(),B.subscribe(_,Z,j)};else if(Q==="getStringKey")return(Z)=>{let j=$;if(!Z||Object.keys(Z).length===0)return j;let _=Object.keys(Z).sort().reduce((C,F)=>{let Y=Z[F];if(Y!==void 0&&Y!==null)C[F]=Y;return C},{});if(Object.keys(_).length===0)return j;let L=JSON.stringify(_);return`${j}?${L}`};else if(Q==="getArrayKey")return(Z,j)=>{let _=$,L={...Z??{},...j??{}};if(Object.keys(L).length===0)return[_];let C=[];if(Object.keys(L).sort().forEach((Y)=>{let I=L[Y];if(I!==void 0&&I!==null){let R=I;if(typeof I==="boolean"||typeof I==="number")R=String(I);else if(typeof I==="object")R=JSON.stringify(I);C.push([Y,R])}}),C.length===0)return[_];let F=C.flatMap(([Y,I])=>[Y,I]);return[_,...F]};else if(Q==="getNoInputsArrayKey")return()=>{return[$]}}return b([...X,Q])}})},p=b();if(z)console.log("BRPC client created",{baseUrl:K,prefix:E,wsUrl:v});return{routes:p,storage:{getObjectUrl:y(c)},utils:{updateWsAuth:async()=>await B.updateAuth(),setHeader:async(X,M)=>{if(typeof W==="function"){console.warn("Cannot use setHeader with function-based headers resolver");return}W={...W,[X]:M},await B.updateAuth()},setHeaders:async(X)=>{if(typeof W==="function"){console.warn("Cannot use setHeaders with function-based headers resolver");return}W={...W,...X},await B.updateAuth()}}}}export{y as createGetObjectUrl,Yq as createBrpcClient,U as BrpcClientSubscriptionError,V as BrpcClientError};
2
+ function m(q){return(J)=>{if(!J)return null;return`${q}/${J}`}}class O extends Error{status;code;clientCode;data;constructor(q,J,X,N,I){super(q);this.name="BrpcClientError",this.status=J,this.code=X,this.clientCode=N,this.data=I}isClientError(q){return this.clientCode===q}isUnauthorized(){return this.status===401}isForbidden(){return this.status===403}isNotFound(){return this.status===404}isValidationError(){return this.status===400||this.code==="BAD_REQUEST"}}class f extends O{type;constructor(q,J){super(J.message??"WebSocket request failed",J.httpStatus??0,J.code,J.clientCode,J.data);this.name="BrpcClientSubscriptionError",this.type=q}}var h=(q)=>{return q instanceof File||q instanceof Blob||typeof q==="object"&&q!==null&&"uri"in q&&"name"in q&&"type"in q};function y(q){if(!q)return null;return q.replace(/\//g,"")}function c(q){let J=q&&q.length>0?q:typeof window<"u"?window.location.origin:"";return J=J.endsWith("/")?J.slice(0,-1):J,J}function d(q,J){let X=J?`/${J}/ws`:"/ws";return`${q.replace(/^http/,"ws")}${X}`}function u(q,J){if(q&&J)return`/${q}/${J}`;else if(q&&!J)return`/${q}`;else if(!q&&J)return`/${J}`;else return""}class b{baseUrl;ws=null;subscriptions=new Map;messageQueue=[];isConnected=!1;reconnectTimeout=null;WebSocketImpl;getHeaders;authToken=null;debug;pendingAuth=!1;authActions=[];constructor(q,J,X,N=!1,I){this.baseUrl=q;this.WebSocketImpl=J,this.debug=N&&I==="development",this.getHeaders=async()=>{if(typeof X==="function")return await X();return X??{}},this.connect()}startTopicSubscription(q){let J=this.subscriptions.get(q);if(!J)return;if(J.status="pending",J.auth==="required"&&this.pendingAuth){this.authActions.push(()=>this.startTopicSubscription(q));return}this.send({type:"subscribe",topic:q},{allowDuringPendingAuth:J.auth==="none"})}async connect(){try{let q=await this.getHeaders();this.authToken=q.Authorization||null;let J=this.baseUrl;if(this.debug)console.log(`Connecting to WebSocket: ${J}`);this.ws=new this.WebSocketImpl(J),this.ws.onopen=async()=>{if(this.debug)console.log("WebSocket connection established");if(this.isConnected=!0,this.authActions=[],this.authToken){if(this.pendingAuth=!0,this.send({type:"authenticate",token:this.authToken}),this.debug)console.log("Sent authentication token, waiting for response");for(let X of this.subscriptions.keys())this.startTopicSubscription(X);this.authActions.push(()=>{if(this.messageQueue.length>0&&this.debug)console.log(`Sending ${this.messageQueue.length} queued messages`);this.messageQueue.forEach((X)=>this.send(X)),this.messageQueue=[]})}else{for(let X of this.subscriptions.keys())this.startTopicSubscription(X);if(this.messageQueue.length>0&&this.debug)console.log(`Sending ${this.messageQueue.length} queued messages`);this.messageQueue.forEach((X)=>this.send(X)),this.messageQueue=[]}},this.ws.onmessage=(X)=>{try{if(this.debug)console.log("WebSocket message received:",X.data);let N=JSON.parse(X.data);if(N.type==="auth_success"){let I=N.authenticated!==!1;if(this.debug)console.log(`WebSocket ${I?"authentication":"deauthentication"} successful`);if(this.pendingAuth=!1,I)this.authActions.forEach((T)=>T());this.authActions=[];return}if(N.type==="auth_error"){console.error("WebSocket authentication failed:",this.createClientError(N.error)),this.pendingAuth=!1,this.authActions=[];return}if(this.debug)console.log("Got new message",N);if(N.type==="subscribe_success"&&N.topic&&this.subscriptions.has(N.topic))this.subscriptions.get(N.topic).status="subscribed";else if(N.type==="publish"&&N.topic&&this.subscriptions.has(N.topic)){let{callbacks:I}=this.subscriptions.get(N.topic);if(this.debug)console.log("Calling subscription callbacks");I?.forEach((T)=>T(void 0,N.data))}else if(["subscribe_error","publish_error"].includes(N.type)&&N.topic&&this.subscriptions.has(N.topic)){let I=this.createSubscriptionError(N.type,N.error),T=this.subscriptions.get(N.topic);T.status="failed",T.callbacks.forEach((C)=>C(I))}else if(N.type==="unsubscribe_error")console.error(`WebSocket unsubscribe failed for topic ${N.topic??"unknown"}:`,this.createClientError(N.error));else if(N.error)console.error("WebSocket error:",this.createClientError(N.error));else console.warn("Unhandled WebSocket message format:",N)}catch(N){if(this.debug)console.error("Error processing WebSocket message:",N),console.error("Raw message:",X.data)}},this.ws.onclose=(X)=>{if(this.isConnected=!1,this.pendingAuth=!1,this.subscriptions.forEach((N)=>{N.status="stale"}),this.debug)console.log(`WebSocket connection closed. Code: ${X.code}, Reason: ${X.reason}`);this.reconnectTimeout=setTimeout(()=>this.connect(),2000)},this.ws.onerror=(X)=>{if(this.debug)console.log("WebSocket error:",X)}}catch(q){if(this.debug)console.log("Failed to create WebSocket connection:",q);this.pendingAuth=!1,this.reconnectTimeout=setTimeout(()=>this.connect(),2000)}}createClientError(q){return new O(q?.message??"WebSocket request failed",q?.httpStatus??0,q?.code,q?.clientCode,q?.data)}createSubscriptionError(q,J){return new f(q,J??{})}disconnect(){if(this.reconnectTimeout)clearTimeout(this.reconnectTimeout),this.reconnectTimeout=null;if(this.ws)this.ws.close(),this.ws=null;this.isConnected=!1,this.pendingAuth=!1,this.authActions=[],this.subscriptions.clear()}send(q,J={}){if(this.isConnected&&this.ws?.readyState===this.WebSocketImpl.OPEN){if(this.pendingAuth&&q.type!=="authenticate"&&!J.allowDuringPendingAuth){if(this.messageQueue.push(q),this.debug)console.log("Message queued until authentication completes:",q);return}let X=JSON.stringify(q);if(this.ws.send(X),this.debug)console.log("WebSocket message sent:",X)}else if(this.messageQueue.push(q),this.debug)console.log("WebSocket message queued (not connected):",q)}subscribe(q,J,X={}){let N=this.subscriptions.get(q);if(!N){if(N={auth:X.auth??"required",callbacks:new Set,status:"pending"},this.subscriptions.set(q,N),N.callbacks.add(J),this.isConnected)this.startTopicSubscription(q)}else N.callbacks.add(J);return{unsubscribe:()=>this.unsubscribe(q,J),publish:(I)=>this.publish(q,I,J)}}unsubscribe(q,J){let X=this.subscriptions.get(q);if(X){if(X.callbacks.delete(J),X.callbacks.size===0){if(this.subscriptions.delete(q),this.send({type:"unsubscribe",topic:q}),this.debug)console.log(`Unsubscribed from topic: ${q}`)}}}publish(q,J,X){let N=this.subscriptions.get(q);if(N?.status!=="subscribed"){X(this.createSubscriptionError("publish_error",{message:"Cannot publish to a topic that is not subscribed",code:"SUBSCRIPTION_NOT_SUBSCRIBED",clientCode:"SUBSCRIPTION_NOT_SUBSCRIBED",data:{status:N?.status??"unsubscribed"}}));return}if(this.send({type:"publish",topic:q,data:J}),this.debug)console.log(`Published to topic: ${q}`,J)}async updateAuth(){let J=(await this.getHeaders()).Authorization||"",X=J.startsWith("Bearer ")?J.substring(7).trim():J.trim(),N=X.length>0,I=this.authToken!==null&&this.authToken.trim().length>0;if((N!==I||X!==this.authToken)&&this.isConnected){if(this.authToken=X,this.pendingAuth=N,this.send({type:"authenticate",token:J}),this.debug)if(N)console.log("Updated authentication token, waiting for confirmation");else console.log("Sent empty token for deauthentication"),this.pendingAuth=!1,this.authActions=[]}}}function jq(q,J={}){let X=J.fetch||fetch,N=J.WebSocket??(typeof WebSocket<"u"?WebSocket:null),I=J.prefix??"",T=J.apiPrefix??"",C=J.debug??!1,n=J.s3Endpoint,l=J.nodeEnv;if(!J.s3Endpoint)throw Error("BRPC Client: Pass s3Endpoint option to createBrpcClient");if(!J.nodeEnv)throw Error("BRPC Client: Pass nodeEnv option to createBrpcClient");if(!N)throw Error("WebSocket is not available in this environment");let w=J.headers??{},p=async()=>{if(typeof w==="function")return await w();return w},H=c(q),k=y(J.prefix),i=y(J.apiPrefix),K=u(k,i),g=d(H,k),E=new b(g,N,w,C,l),U=async(Y=!0)=>{let M={};if(Y)M["Content-Type"]="application/json";let Q=await p();return{...M,...Q}},o=async(Y)=>{if(!Y.ok){let Q;try{Q=await Y.json()}catch{try{Q={error:await Y.text()}}catch{Q={error:"Failed to parse error response"}}}if(Q?.error&&typeof Q.error==="object"){let F=Q.error;throw new O(F.message||Y.statusText,Y.status,F.code,F.clientCode,F.data)}let z=Q?.error||Q?.message||Y.statusText;throw new O(z,Y.status,void 0,void 0,Q)}let M=Y.headers.get("Content-Type")||"";try{if(M.includes("application/json"))return(await Y.json()).data;else if(M.includes("text/"))return Y.text();else return Y.blob()}catch(Q){throw new O(`Failed to parse response: ${Q instanceof Error?Q.message:"Unknown error"}`,Y.status,"PARSE_ERROR")}},x=async(Y,M)=>{try{let Q=await X(Y,M);return await o(Q)}catch(Q){if(Q instanceof O)throw Q;throw new O(Q instanceof Error?Q.message:"Network request failed",0,"NETWORK_ERROR")}},B=(Y,M)=>{return Y.split("/").map((Q)=>{if(!Q.startsWith(":"))return Q;let z=Q.slice(1),F=M?.[z];if(F===void 0||F===null)throw new O(`Missing route parameter: ${z}`,400,"MISSING_ROUTE_PARAM","MISSING_ROUTE_PARAM",{param:z});return encodeURIComponent(String(F))}).join("/")},v=(Y=[])=>{return new Proxy({},{get(M,Q){if(typeof Q==="symbol")return;if(Q==="query"||Q==="mutation"||Q==="formMutation"||Q==="subscription"||Q==="getStringKey"||Q==="getArrayKey"||Q==="getNoInputsArrayKey"){let z=Y.join("/"),F=(Z)=>`${H}${K}/${Z}`;if(C)console.log(`BRPC ${Q} procedure path: ${z}`);if(Q==="query")return async(Z,R={})=>{let _=B(z,R.params),j=await U(!0);if(C)console.log(`BRPC query request to ${_}`,{input:Z,headers:j});let G=new URL(F(_));if(Z&&typeof Z==="object")Object.entries(Z).forEach(([V,$])=>{if($!==void 0)G.searchParams.append(V,String($))});let W=await x(G.toString(),{method:"GET",headers:j,credentials:"include"});if(C)console.log(`BRPC query response from ${_}:`,W);return W};else if(Q==="mutation")return async(Z,R={})=>{let _=B(z,R.params),j=await U(!0);if(C)console.log(`BRPC mutation request to ${_}`,{input:Z,headers:j});let G=JSON.stringify(Z),W=await x(F(_),{method:"POST",headers:j,body:G,credentials:"include"});if(C)console.log(`BRPC mutation response from ${_}:`,W);return W};else if(Q==="formMutation")return async(Z,R={})=>{let _=B(z,R.params),j=await U(!1);if(C)console.log(`BRPC formMutation request to ${_}`,{input:Z,headers:j});let G=new FormData,W=($,L,P)=>{let D=P?`${P}[${$}]`:$;if(L===null||L===void 0)return;else if(h(L))G.append(D,L);else if(Array.isArray(L))L.forEach((A,S)=>{if(h(A))G.append(D,A);else if(typeof A==="object"&&A!==null)Object.entries(A).forEach(([a,s])=>{W(a,s,`${D}[${S}]`)});else G.append(`${D}[${S}]`,String(A))});else if(typeof L==="object"&&L!==null)Object.entries(L).forEach(([A,S])=>{W(A,S,D)});else G.append(D,String(L))};Object.entries(Z).forEach(([$,L])=>{W($,L)});let V=await x(F(_),{method:"POST",headers:j,body:G,credentials:"include"});if(C)console.log(`BRPC formMutation response from ${_}:`,V);return V};else if(Q==="subscription")return(Z,R={})=>{let _=B(z,R.params);if(C)console.log(`BRPC subscription to ${_}`);let j=K?`${K.slice(1)}/${_}`:_;return E.updateAuth(),E.subscribe(j,Z,R)};else if(Q==="getStringKey")return(Z,R={})=>{let _=B(z,R.params);if(!Z||Object.keys(Z).length===0)return _;let j=Object.keys(Z).sort().reduce((W,V)=>{let $=Z[V];if($!==void 0&&$!==null)W[V]=$;return W},{});if(Object.keys(j).length===0)return _;let G=JSON.stringify(j);return`${_}?${G}`};else if(Q==="getArrayKey")return(Z,R={})=>{let _=B(z,R.params),j=Z??{};if(Object.keys(j).length===0)return[_];let G=[];if(Object.keys(j).sort().forEach((V)=>{let $=j[V];if($!==void 0&&$!==null){let L=$;if(typeof $==="boolean"||typeof $==="number")L=String($);else if(typeof $==="object")L=JSON.stringify($);G.push([V,L])}}),G.length===0)return[_];let W=G.flatMap(([V,$])=>[V,$]);return[_,...W]};else if(Q==="getNoInputsArrayKey")return(Z={})=>{return[B(z,Z.params)]}}return v([...Y,Q])}})},t=v();if(C)console.log("BRPC client created",{baseUrl:H,prefix:K,wsUrl:g});return{routes:t,storage:{getObjectUrl:m(n)},utils:{updateWsAuth:async()=>await E.updateAuth(),setHeader:async(Y,M)=>{if(typeof w==="function"){console.warn("Cannot use setHeader with function-based headers resolver");return}w={...w,[Y]:M},await E.updateAuth()},setHeaders:async(Y)=>{if(typeof w==="function"){console.warn("Cannot use setHeaders with function-based headers resolver");return}w={...w,...Y},await E.updateAuth()}}}}export{m as createGetObjectUrl,jq as createBrpcClient,f as BrpcClientSubscriptionError,O as BrpcClientError};
3
3
 
4
- //# debugId=0A98DC5F1697239C64756E2164756E21
4
+ //# debugId=16BAF87F34BADF9264756E2164756E21
@@ -1,4 +1,4 @@
1
1
  // @bun
2
- import{useCallback as z,useEffect as A,useRef as B}from"react";function G(j,q){let m=B(null);return A(()=>{let{publish:x,unsubscribe:w}=j(q||((y)=>console.log("New data received",y)));return m.current=x,()=>{m.current=null,w()}},[j,q]),{publish:z((x)=>{m.current?.(x)},[])}}export{G as useSubscription};
2
+ import{useCallback as A,useEffect as B,useRef as D}from"react";function H(j,q){let m=D(null);return B(()=>{let{publish:x,unsubscribe:y}=j(q??((w,z)=>{if(w){console.error("BRPC subscription error:",w);return}console.log("New data received",z)}));return m.current=x,()=>{m.current=null,y()}},[j,q]),{publish:A((x)=>{m.current?.(x)},[])}}export{H as useSubscription};
3
3
 
4
- //# debugId=12418FDC428A9F4964756E2164756E21
4
+ //# debugId=22280F97277F1D8B64756E2164756E21
@@ -1,13 +1,14 @@
1
+ import type { SubscriptionCallback } from "../types";
1
2
  /**
2
3
  * Custom hook for managing brpc subscriptions
3
4
  * @param subscription A function that sets up a subscription and returns publish/unsubscribe handlers
4
- * @param callback Optional callback function to handle incoming data
5
+ * @param callback Optional error-first callback for incoming data
5
6
  * @returns A stable publish function that can be called directly
6
7
  */
7
- declare function useSubscription<TInput, TOutput>(subscription: (callback: (data: TOutput) => void) => {
8
+ declare function useSubscription<TInput, TOutput>(subscription: (callback: SubscriptionCallback<TOutput>) => {
8
9
  publish: (data: TInput) => void;
9
10
  unsubscribe: () => void;
10
- }, callback?: (data: TOutput) => void): {
11
+ }, callback?: SubscriptionCallback<TOutput>): {
11
12
  publish: (data: TInput) => void;
12
13
  };
13
14
  export { useSubscription };
@@ -16,7 +16,11 @@ export type InferRouterOutput<T> = {
16
16
  };
17
17
  export type IsOptionalInput<T> = undefined extends InferProcedureInput<T> ? true : {} extends InferProcedureInput<T> & {} ? true : false;
18
18
  export type SubscriptionCallback<T> = (error: BrpcClientSubscriptionError | undefined, message?: T) => void;
19
- export type SubscriptionOptions = {
19
+ export type RouteParams = Record<string, string | number | boolean>;
20
+ export type ProcedureCallOptions = {
21
+ params?: RouteParams;
22
+ };
23
+ export type SubscriptionOptions = ProcedureCallOptions & {
20
24
  auth?: "required" | "none";
21
25
  };
22
26
  export type SubscriptionHandlers<InputType, _OutputType> = {
@@ -24,39 +28,40 @@ export type SubscriptionHandlers<InputType, _OutputType> = {
24
28
  publish: (data: InputType) => void;
25
29
  };
26
30
  export type ProcedureMethods<T extends BaseProcedure> = T["_type"] extends "query" | "streamQuery" | "file" | "html" ? IsOptionalInput<T> extends true ? {
27
- query: (input?: InferProcedureInput<T>) => Promise<InferProcedureOutput<T>>;
28
- getStringKey: (input?: InferProcedureInput<T>) => string;
29
- getArrayKey: (input?: InferProcedureInput<T>, context?: Record<string, string | number | boolean>) => (string | number | boolean)[];
30
- getNoInputsArrayKey: () => (string | number | boolean)[];
31
+ query: (input?: InferProcedureInput<T>, options?: ProcedureCallOptions) => Promise<InferProcedureOutput<T>>;
32
+ getStringKey: (input?: InferProcedureInput<T>, options?: ProcedureCallOptions) => string;
33
+ getArrayKey: (input?: InferProcedureInput<T>, options?: ProcedureCallOptions) => (string | number | boolean)[];
34
+ getNoInputsArrayKey: (options?: ProcedureCallOptions) => (string | number | boolean)[];
31
35
  } : {
32
- query: (input: InferProcedureInput<T>) => Promise<InferProcedureOutput<T>>;
33
- getStringKey: (input?: InferProcedureInput<T>) => string;
34
- getArrayKey: (input?: InferProcedureInput<T>, context?: Record<string, string | number | boolean>) => (string | number | boolean)[];
35
- getNoInputsArrayKey: () => (string | number | boolean)[];
36
+ query: (input: InferProcedureInput<T>, options?: ProcedureCallOptions) => Promise<InferProcedureOutput<T>>;
37
+ getStringKey: (input?: InferProcedureInput<T>, options?: ProcedureCallOptions) => string;
38
+ getArrayKey: (input?: InferProcedureInput<T>, options?: ProcedureCallOptions) => (string | number | boolean)[];
39
+ getNoInputsArrayKey: (options?: ProcedureCallOptions) => (string | number | boolean)[];
36
40
  } : T["_type"] extends "mutation" | "streamMutation" ? IsOptionalInput<T> extends true ? {
37
- mutation: (input?: InferProcedureInput<T>) => Promise<InferProcedureOutput<T>>;
38
- getStringKey: (input?: InferProcedureInput<T>) => string;
39
- getArrayKey: (input?: InferProcedureInput<T>, context?: Record<string, string | number | boolean>) => (string | number | boolean)[];
40
- getNoInputsArrayKey: () => (string | number | boolean)[];
41
+ mutation: (input?: InferProcedureInput<T>, options?: ProcedureCallOptions) => Promise<InferProcedureOutput<T>>;
42
+ getStringKey: (input?: InferProcedureInput<T>, options?: ProcedureCallOptions) => string;
43
+ getArrayKey: (input?: InferProcedureInput<T>, options?: ProcedureCallOptions) => (string | number | boolean)[];
44
+ getNoInputsArrayKey: (options?: ProcedureCallOptions) => (string | number | boolean)[];
41
45
  } : {
42
- mutation: (input: InferProcedureInput<T>) => Promise<InferProcedureOutput<T>>;
43
- getStringKey: (input?: InferProcedureInput<T>) => string;
44
- getArrayKey: (input?: InferProcedureInput<T>, context?: Record<string, string | number | boolean>) => (string | number | boolean)[];
45
- getNoInputsArrayKey: () => (string | number | boolean)[];
46
+ mutation: (input: InferProcedureInput<T>, options?: ProcedureCallOptions) => Promise<InferProcedureOutput<T>>;
47
+ getStringKey: (input?: InferProcedureInput<T>, options?: ProcedureCallOptions) => string;
48
+ getArrayKey: (input?: InferProcedureInput<T>, options?: ProcedureCallOptions) => (string | number | boolean)[];
49
+ getNoInputsArrayKey: (options?: ProcedureCallOptions) => (string | number | boolean)[];
46
50
  } : T["_type"] extends "formMutation" ? IsOptionalInput<T> extends true ? {
47
- formMutation: (input?: InferProcedureInput<T>) => Promise<InferProcedureOutput<T>>;
48
- getStringKey: (input?: InferProcedureInput<T>) => string;
49
- getArrayKey: (input?: InferProcedureInput<T>, context?: Record<string, string | number | boolean>) => (string | number | boolean)[];
50
- getNoInputsArrayKey: () => (string | number | boolean)[];
51
+ formMutation: (input?: InferProcedureInput<T>, options?: ProcedureCallOptions) => Promise<InferProcedureOutput<T>>;
52
+ getStringKey: (input?: InferProcedureInput<T>, options?: ProcedureCallOptions) => string;
53
+ getArrayKey: (input?: InferProcedureInput<T>, options?: ProcedureCallOptions) => (string | number | boolean)[];
54
+ getNoInputsArrayKey: (options?: ProcedureCallOptions) => (string | number | boolean)[];
51
55
  } : {
52
- formMutation: (input: InferProcedureInput<T>) => Promise<InferProcedureOutput<T>>;
53
- getStringKey: (input?: InferProcedureInput<T>) => string;
54
- getArrayKey: (input?: InferProcedureInput<T>, context?: Record<string, string | number | boolean>) => (string | number | boolean)[];
55
- getNoInputsArrayKey: () => (string | number | boolean)[];
56
+ formMutation: (input: InferProcedureInput<T>, options?: ProcedureCallOptions) => Promise<InferProcedureOutput<T>>;
57
+ getStringKey: (input?: InferProcedureInput<T>, options?: ProcedureCallOptions) => string;
58
+ getArrayKey: (input?: InferProcedureInput<T>, options?: ProcedureCallOptions) => (string | number | boolean)[];
59
+ getNoInputsArrayKey: (options?: ProcedureCallOptions) => (string | number | boolean)[];
56
60
  } : T["_type"] extends "subscription" ? {
57
61
  subscription: (callback: SubscriptionCallback<InferProcedureOutput<T>>, options?: SubscriptionOptions) => SubscriptionHandlers<InferProcedureInput<T>, InferProcedureOutput<T>>;
58
- getStringKey: (input?: InferProcedureInput<T>) => string;
59
- getArrayKey: (input?: InferProcedureInput<T>, context?: Record<string, string | number | boolean>) => string[];
62
+ getStringKey: (input?: InferProcedureInput<T>, options?: ProcedureCallOptions) => string;
63
+ getArrayKey: (input?: InferProcedureInput<T>, options?: ProcedureCallOptions) => string[];
64
+ getNoInputsArrayKey: (options?: ProcedureCallOptions) => string[];
60
65
  } : never;
61
66
  export type InferClientRoutes<T> = {
62
67
  [K in keyof T]: T[K] extends BaseProcedure ? ProcedureMethods<T[K]> : T[K] extends Record<string, any> ? InferClientRoutes<T[K]> : never;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mateosuarezdev/brpc",
3
- "version": "1.0.83",
3
+ "version": "1.0.84",
4
4
  "description": "A Type-Safe, Flexible Web application framework for Bun",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",