@mateosuarezdev/brpc 1.0.84 → 1.0.85

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
@@ -662,9 +662,12 @@ unsubscribe();
662
662
 
663
663
  Subscriptions wait for WebSocket authentication by default. Public feeds can subscribe immediately while auth is refreshing with `{ auth: "none" }`.
664
664
 
665
+ The React `useSubscription` options also accept `enabled`. Set it to `false` to skip subscription setup; changing it back to `true` attaches the topic again, and disabling or unmounting cleans it up.
666
+
665
667
  ### Dynamic Client Route Params
666
668
 
667
669
  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.
670
+ The client infers every `:param` segment in the route and requires the complete `params` object for dynamic routes.
668
671
 
669
672
  ```ts
670
673
  await client.routes.users[":userId"].query(
@@ -782,14 +785,13 @@ function ChatRoom({ roomId }: { roomId: string }) {
782
785
  const [messages, setMessages] = useState<Message[]>([]);
783
786
 
784
787
  const { publish } = useSubscription(
785
- (callback) =>
786
- client.routes.messages[":roomId"].subscription(callback, {
787
- params: { roomId },
788
- }),
788
+ (callback, options) =>
789
+ client.routes.messages[":roomId"].subscription(callback, options),
789
790
  (error, message) => {
790
791
  if (error || !message) return;
791
792
  setMessages((prev) => [...prev, message]);
792
793
  },
794
+ { params: { roomId } },
793
795
  );
794
796
 
795
797
  return (
@@ -1,4 +1,4 @@
1
1
  // @bun
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};
2
+ function c(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 d(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 u(q,J){let X=J?`/${J}/ws`:"/ws";return`${q.replace(/^http/,"ws")}${X}`}function n(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,l=J.s3Endpoint,p=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??{},k=async()=>{if(typeof w==="function")return await w();return w},H=d(q),g=y(J.prefix),i=y(J.apiPrefix),K=n(g,i),P=u(H,g),E=new b(P,N,k,C,p),U=async(Y=!0)=>{let M={};if(Y)M["Content-Type"]="application/json";let Q=await k();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("/")},m=(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,v)=>{let D=v?`${v}[${$}]`:$;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 m([...Y,Q])}})},t=m();if(C)console.log("BRPC client created",{baseUrl:H,prefix:K,wsUrl:P});return{routes:t,storage:{getObjectUrl:c(l)},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{c as createGetObjectUrl,jq as createBrpcClient,f as BrpcClientSubscriptionError,O as BrpcClientError};
3
3
 
4
- //# debugId=16BAF87F34BADF9264756E2164756E21
4
+ //# debugId=6A275280D49BF12164756E2164756E21
@@ -1,4 +1,4 @@
1
1
  // @bun
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};
2
+ import{useCallback as B,useEffect as D,useRef as F}from"react";function I(q,w,x={}){let m=F(null);return D(()=>{if(x.enabled===!1){m.current=null;return}let{publish:j,unsubscribe:z}=q(w??((y,A)=>{if(y){console.error("BRPC subscription error:",y);return}console.log("New data received",A)}),x);return m.current=j,()=>{m.current=null,z()}},[q,w,x]),{publish:B((j)=>{m.current?.(j)},[])}}export{I as useSubscription};
3
3
 
4
- //# debugId=22280F97277F1D8B64756E2164756E21
4
+ //# debugId=DBE7A95679FC784164756E2164756E21
@@ -1,14 +1,14 @@
1
- import type { SubscriptionCallback } from "../types";
1
+ import type { RuntimeSubscriptionOptions, SubscriptionCallback } from "../types";
2
+ export type UseSubscriptionOptions = RuntimeSubscriptionOptions & {
3
+ enabled?: boolean;
4
+ };
2
5
  /**
3
- * Custom hook for managing brpc subscriptions
4
- * @param subscription A function that sets up a subscription and returns publish/unsubscribe handlers
5
- * @param callback Optional error-first callback for incoming data
6
- * @returns A stable publish function that can be called directly
6
+ * Connects React state to a BRPC subscription.
7
7
  */
8
- declare function useSubscription<TInput, TOutput>(subscription: (callback: SubscriptionCallback<TOutput>) => {
8
+ declare function useSubscription<TInput, TOutput>(subscription: (callback: SubscriptionCallback<TOutput>, options?: RuntimeSubscriptionOptions) => {
9
9
  publish: (data: TInput) => void;
10
10
  unsubscribe: () => void;
11
- }, callback?: SubscriptionCallback<TOutput>): {
11
+ }, callback?: SubscriptionCallback<TOutput>, options?: UseSubscriptionOptions): {
12
12
  publish: (data: TInput) => void;
13
13
  };
14
14
  export { useSubscription };
@@ -16,55 +16,58 @@ 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 RouteParams = Record<string, string | number | boolean>;
20
- export type ProcedureCallOptions = {
19
+ type RouteParamValue = string | number | boolean;
20
+ type EmptyRouteParams = Record<never, never>;
21
+ export type RouteParams = Record<string, RouteParamValue>;
22
+ type HasRouteParams<TParams extends RouteParams> = keyof TParams extends never ? false : true;
23
+ /**
24
+ * Options for one typed procedure call.
25
+ */
26
+ export type ProcedureCallOptions<TParams extends RouteParams> = HasRouteParams<TParams> extends true ? {
27
+ params: TParams;
28
+ } : {
29
+ params?: never;
30
+ };
31
+ /**
32
+ * Runtime options used internally before a concrete client route is inferred.
33
+ */
34
+ export type RuntimeProcedureCallOptions = {
21
35
  params?: RouteParams;
22
36
  };
23
- export type SubscriptionOptions = ProcedureCallOptions & {
37
+ export type SubscriptionOptions<TParams extends RouteParams> = ProcedureCallOptions<TParams> & {
38
+ auth?: "required" | "none";
39
+ };
40
+ export type RuntimeSubscriptionOptions = RuntimeProcedureCallOptions & {
24
41
  auth?: "required" | "none";
25
42
  };
26
43
  export type SubscriptionHandlers<InputType, _OutputType> = {
27
44
  unsubscribe: () => void;
28
45
  publish: (data: InputType) => void;
29
46
  };
30
- export type ProcedureMethods<T extends BaseProcedure> = T["_type"] extends "query" | "streamQuery" | "file" | "html" ? IsOptionalInput<T> extends true ? {
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)[];
35
- } : {
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)[];
40
- } : T["_type"] extends "mutation" | "streamMutation" ? IsOptionalInput<T> extends true ? {
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)[];
45
- } : {
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)[];
50
- } : T["_type"] extends "formMutation" ? IsOptionalInput<T> extends true ? {
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)[];
55
- } : {
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)[];
60
- } : T["_type"] extends "subscription" ? {
61
- subscription: (callback: SubscriptionCallback<InferProcedureOutput<T>>, options?: SubscriptionOptions) => SubscriptionHandlers<InferProcedureInput<T>, InferProcedureOutput<T>>;
62
- getStringKey: (input?: InferProcedureInput<T>, options?: ProcedureCallOptions) => string;
63
- getArrayKey: (input?: InferProcedureInput<T>, options?: ProcedureCallOptions) => string[];
64
- getNoInputsArrayKey: (options?: ProcedureCallOptions) => string[];
65
- } : never;
66
- export type InferClientRoutes<T> = {
67
- [K in keyof T]: T[K] extends BaseProcedure ? ProcedureMethods<T[K]> : T[K] extends Record<string, any> ? InferClientRoutes<T[K]> : never;
47
+ type CallOptionsArgs<TParams extends RouteParams> = HasRouteParams<TParams> extends true ? [options: ProcedureCallOptions<TParams>] : [options?: ProcedureCallOptions<TParams>];
48
+ type SubscriptionOptionsArgs<TParams extends RouteParams> = HasRouteParams<TParams> extends true ? [options: SubscriptionOptions<TParams>] : [options?: SubscriptionOptions<TParams>];
49
+ type InputCallArgs<T extends BaseProcedure, TParams extends RouteParams> = HasRouteParams<TParams> extends true ? IsOptionalInput<T> extends true ? [input: InferProcedureInput<T> | undefined, ...CallOptionsArgs<TParams>] : [input: InferProcedureInput<T>, ...CallOptionsArgs<TParams>] : IsOptionalInput<T> extends true ? [input?: InferProcedureInput<T>, ...CallOptionsArgs<TParams>] : [input: InferProcedureInput<T>, ...CallOptionsArgs<TParams>];
50
+ type CacheKeyMethods<T extends BaseProcedure, TParams extends RouteParams> = {
51
+ getStringKey: (...args: InputCallArgs<T, TParams>) => string;
52
+ getArrayKey: (...args: InputCallArgs<T, TParams>) => (string | number | boolean)[];
53
+ getNoInputsArrayKey: (...args: CallOptionsArgs<TParams>) => (string | number | boolean)[];
54
+ };
55
+ type QueryMethods<T extends BaseProcedure, TParams extends RouteParams> = {
56
+ query: (...args: InputCallArgs<T, TParams>) => Promise<InferProcedureOutput<T>>;
57
+ };
58
+ type MutationMethods<T extends BaseProcedure, TParams extends RouteParams> = {
59
+ mutation: (...args: InputCallArgs<T, TParams>) => Promise<InferProcedureOutput<T>>;
60
+ };
61
+ type FormMutationMethods<T extends BaseProcedure, TParams extends RouteParams> = {
62
+ formMutation: (...args: InputCallArgs<T, TParams>) => Promise<InferProcedureOutput<T>>;
63
+ };
64
+ type SubscriptionMethods<T extends BaseProcedure, TParams extends RouteParams> = {
65
+ subscription: (callback: SubscriptionCallback<InferProcedureOutput<T>>, ...args: SubscriptionOptionsArgs<TParams>) => SubscriptionHandlers<InferProcedureInput<T>, InferProcedureOutput<T>>;
66
+ };
67
+ export type ProcedureMethods<T extends BaseProcedure, TParams extends RouteParams = EmptyRouteParams> = T["_type"] extends "query" | "streamQuery" | "file" | "html" ? QueryMethods<T, TParams> & CacheKeyMethods<T, TParams> : T["_type"] extends "mutation" | "streamMutation" ? MutationMethods<T, TParams> & CacheKeyMethods<T, TParams> : T["_type"] extends "formMutation" ? FormMutationMethods<T, TParams> & CacheKeyMethods<T, TParams> : T["_type"] extends "subscription" ? SubscriptionMethods<T, TParams> & CacheKeyMethods<T, TParams> : never;
68
+ type RouteParamsForKey<TParams extends RouteParams, TKey extends string> = TKey extends `:${infer TParamName}` ? TParams & Record<TParamName, RouteParamValue> : TParams;
69
+ export type InferClientRoutes<T, TParams extends RouteParams = EmptyRouteParams> = {
70
+ [K in keyof T]: K extends string ? T[K] extends BaseProcedure ? ProcedureMethods<T[K], RouteParamsForKey<TParams, K>> : T[K] extends Record<string, any> ? InferClientRoutes<T[K], RouteParamsForKey<TParams, K>> : never : never;
68
71
  };
69
72
  export interface BrpcClientOptions {
70
73
  /**
@@ -85,7 +88,7 @@ export interface BrpcClientOptions {
85
88
  */
86
89
  prefix?: string;
87
90
  /**
88
- * API path prefix. Will be prepended to all procedure and topic paths.
91
+ * API path prefix. Will be prepended to all procedure URLs only.
89
92
  * Example: "/api" for "https://example.com/api/procedure"
90
93
  */
91
94
  apiPrefix?: string;
@@ -107,10 +110,7 @@ export interface BrpcClientOptions {
107
110
  nodeEnv?: string;
108
111
  }
109
112
  /**
110
- * Creates a type-safe client for the BRPC API
111
- * @param baseUrl The base URL of the BRPC server
112
- * @param options Client configuration options
113
- * @returns A client object that mirrors the server routes
113
+ * Creates a type-safe client for the BRPC API.
114
114
  */
115
115
  export type BrpcClient<T> = {
116
116
  routes: InferClientRoutes<T>;
@@ -125,3 +125,4 @@ export type BrpcClient<T> = {
125
125
  };
126
126
  export type Headers = Record<string, string>;
127
127
  export type HeadersResolver = Headers | (() => Promise<Headers> | Headers);
128
+ export {};
@@ -1,4 +1,4 @@
1
- import type { HeadersResolver, SubscriptionCallback, SubscriptionOptions } from "./types";
1
+ import type { HeadersResolver, SubscriptionCallback, RuntimeSubscriptionOptions } from "./types";
2
2
  declare class WebSocketManager {
3
3
  private baseUrl;
4
4
  private ws;
@@ -24,7 +24,7 @@ declare class WebSocketManager {
24
24
  send(message: any, options?: {
25
25
  allowDuringPendingAuth?: boolean;
26
26
  }): void;
27
- subscribe(topic: string, callback: SubscriptionCallback<any>, options?: SubscriptionOptions): {
27
+ subscribe(topic: string, callback: SubscriptionCallback<any>, options?: RuntimeSubscriptionOptions): {
28
28
  unsubscribe: () => void;
29
29
  publish: (data: any) => void;
30
30
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mateosuarezdev/brpc",
3
- "version": "1.0.84",
3
+ "version": "1.0.85",
4
4
  "description": "A Type-Safe, Flexible Web application framework for Bun",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",