@mateosuarezdev/brpc 1.0.89 → 1.0.90

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
@@ -776,23 +776,26 @@ try {
776
776
 
777
777
  ### React
778
778
 
779
- Import from `@mateosuarezdev/brpc/client/react`:
779
+ Create one subscription service, then bind the React hook once:
780
780
 
781
781
  ```ts
782
- import { useSubscription } from "@mateosuarezdev/brpc/client/react";
782
+ import { SubscriptionService } from "@mateosuarezdev/brpc/client";
783
+ import { createUseSubscription } from "@mateosuarezdev/brpc/client/react";
784
+
785
+ const subscriptionService = new SubscriptionService();
786
+ const useSubscription = createUseSubscription(subscriptionService);
783
787
 
784
788
  function ChatRoom({ roomId }: { roomId: string }) {
785
789
  const [messages, setMessages] = useState<Message[]>([]);
786
790
 
787
- const { publish } = useSubscription(
788
- (callback, options) =>
789
- client.routes.messages[":roomId"].subscription(callback, options),
790
- (error, message) => {
791
- if (error || !message) return;
791
+ const { publish } = useSubscription({
792
+ procedure: client.routes.messages[":roomId"],
793
+ options: { params: { roomId } },
794
+ callback: (error, message) => {
795
+ if (error) return;
792
796
  setMessages((prev) => [...prev, message]);
793
797
  },
794
- { params: { roomId } },
795
- );
798
+ });
796
799
 
797
800
  return (
798
801
  <button onClick={() => publish({ roomId, text: "hello" })}>
@@ -1,4 +1,5 @@
1
1
  export * from "./storage";
2
2
  export * from "./service";
3
3
  export * from "./brpc-client-error";
4
+ export * from "./subscription-service";
4
5
  export type { Headers, InferRouterOutput } from "./types";
@@ -1,4 +1,4 @@
1
1
  // @bun
2
- function d(q){return(J)=>{if(!J)return null;return`${q}/${J}`}}class M extends Error{status;code;clientCode;data;constructor(q,J,Q,N,z){super(q);this.name="BrpcClientError",this.status=J,this.code=Q,this.clientCode=N,this.data=z}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 M{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}}function e(){return function(J,Q){if(Q)return J instanceof M&&J.clientCode===Q;return J instanceof M}}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 u(q,J){let Q=J?`/${J}/ws`:"/ws";return`${q.replace(/^http/,"ws")}${Q}`}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,Q,N=!1,z){this.baseUrl=q;this.WebSocketImpl=J,this.debug=N&&z==="development",this.getHeaders=async()=>{if(typeof Q==="function")return await Q();return Q??{}},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 Q of this.subscriptions.keys())this.startTopicSubscription(Q);this.authActions.push(()=>{if(this.messageQueue.length>0&&this.debug)console.log(`Sending ${this.messageQueue.length} queued messages`);this.messageQueue.forEach((Q)=>this.send(Q)),this.messageQueue=[]})}else{for(let Q of this.subscriptions.keys())this.startTopicSubscription(Q);if(this.messageQueue.length>0&&this.debug)console.log(`Sending ${this.messageQueue.length} queued messages`);this.messageQueue.forEach((Q)=>this.send(Q)),this.messageQueue=[]}},this.ws.onmessage=(Q)=>{try{if(this.debug)console.log("WebSocket message received:",Q.data);let N=JSON.parse(Q.data);if(N.type==="auth_success"){let z=N.authenticated!==!1;if(this.debug)console.log(`WebSocket ${z?"authentication":"deauthentication"} successful`);if(this.pendingAuth=!1,z)this.authActions.forEach((D)=>D());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:z}=this.subscriptions.get(N.topic);if(this.debug)console.log("Calling subscription callbacks");z?.forEach((D)=>D(void 0,N.data))}else if(["subscribe_error","publish_error"].includes(N.type)&&N.topic&&this.subscriptions.has(N.topic)){let z=this.createSubscriptionError(N.type,N.error),D=this.subscriptions.get(N.topic);D.status="failed",D.callbacks.forEach((F)=>F(z,void 0))}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:",Q.data)}},this.ws.onclose=(Q)=>{if(this.isConnected=!1,this.pendingAuth=!1,this.subscriptions.forEach((N)=>{N.status="stale"}),this.debug)console.log(`WebSocket connection closed. Code: ${Q.code}, Reason: ${Q.reason}`);this.reconnectTimeout=setTimeout(()=>this.connect(),2000)},this.ws.onerror=(Q)=>{if(this.debug)console.log("WebSocket error:",Q)}}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 M(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 Q=JSON.stringify(q);if(this.ws.send(Q),this.debug)console.log("WebSocket message sent:",Q)}else if(this.messageQueue.push(q),this.debug)console.log("WebSocket message queued (not connected):",q)}subscribe(q,J,Q={}){let N=this.subscriptions.get(q);if(!N){if(N={auth:Q.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:(z)=>this.publish(q,z,J)}}unsubscribe(q,J){let Q=this.subscriptions.get(q);if(Q){if(Q.callbacks.delete(J),Q.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,Q){let N=this.subscriptions.get(q);if(N?.status!=="subscribed"){Q(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"}}),void 0);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||"",Q=J.startsWith("Bearer ")?J.substring(7).trim():J.trim(),N=Q.length>0,z=this.authToken!==null&&this.authToken.trim().length>0;if((N!==z||Q!==this.authToken)&&this.isConnected){if(this.authToken=Q,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 Gq(q,J={}){let Q=J.fetch||fetch,N=J.WebSocket??(typeof WebSocket<"u"?WebSocket:null),z=J.prefix??"",D=J.apiPrefix??"",F=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 A=J.headers??{},k=async()=>{if(typeof A==="function")return await A();return A},E=c(q),g=y(J.prefix),i=y(J.apiPrefix),H=n(g,i),m=u(E,g),C=new b(m,N,k,F,p),U=async(Y=!0)=>{let R={};if(Y)R["Content-Type"]="application/json";let X=await k();return{...R,...X}},o=async(Y)=>{if(!Y.ok){let X;try{X=await Y.json()}catch{try{X={error:await Y.text()}}catch{X={error:"Failed to parse error response"}}}if(X?.error&&typeof X.error==="object"){let V=X.error;throw new M(V.message||Y.statusText,Y.status,V.code,V.clientCode,V.data)}let I=X?.error||X?.message||Y.statusText;throw new M(I,Y.status,void 0,void 0,X)}let R=Y.headers.get("Content-Type")||"";try{if(R.includes("application/json"))return(await Y.json()).data;else if(R.includes("text/"))return Y.text();else return Y.blob()}catch(X){throw new M(`Failed to parse response: ${X instanceof Error?X.message:"Unknown error"}`,Y.status,"PARSE_ERROR")}},x=async(Y,R)=>{try{let X=await Q(Y,R);return await o(X)}catch(X){if(X instanceof M)throw X;throw new M(X instanceof Error?X.message:"Network request failed",0,"NETWORK_ERROR")}},K=(Y,R)=>{return Y.split("/").map((X)=>{if(!X.startsWith(":"))return X;let I=X.slice(1),V=R?.[I];if(V===void 0||V===null)throw new M(`Missing route parameter: ${I}`,400,"MISSING_ROUTE_PARAM","MISSING_ROUTE_PARAM",{param:I});return encodeURIComponent(String(V))}).join("/")},v=(Y=[])=>{return new Proxy({},{get(R,X){if(typeof X==="symbol")return;if(X==="query"||X==="mutation"||X==="formMutation"||X==="subscription"||X==="getStringKey"||X==="getArrayKey"||X==="getNoInputsArrayKey"){let I=Y.join("/"),V=(Z)=>`${E}${H}/${Z}`;if(F)console.log(`BRPC ${X} procedure path: ${I}`);if(X==="query")return async(Z,w={})=>{let _=K(I,w.params),j=await U(!0);if(F)console.log(`BRPC query request to ${_}`,{input:Z,headers:j});let G=new URL(V(_));if(Z&&typeof Z==="object")Object.entries(Z).forEach(([O,$])=>{if($!==void 0)G.searchParams.append(O,String($))});let W=await x(G.toString(),{method:"GET",headers:j,credentials:"include"});if(F)console.log(`BRPC query response from ${_}:`,W);return W};else if(X==="mutation")return async(Z,w={})=>{let _=K(I,w.params),j=await U(!0);if(F)console.log(`BRPC mutation request to ${_}`,{input:Z,headers:j});let G=JSON.stringify(Z),W=await x(V(_),{method:"POST",headers:j,body:G,credentials:"include"});if(F)console.log(`BRPC mutation response from ${_}:`,W);return W};else if(X==="formMutation")return async(Z,w={})=>{let _=K(I,w.params),j=await U(!1);if(F)console.log(`BRPC formMutation request to ${_}`,{input:Z,headers:j});let G=new FormData,W=($,L,P)=>{let S=P?`${P}[${$}]`:$;if(L===null||L===void 0)return;else if(h(L))G.append(S,L);else if(Array.isArray(L))L.forEach((T,B)=>{if(h(T))G.append(S,T);else if(typeof T==="object"&&T!==null)Object.entries(T).forEach(([a,s])=>{W(a,s,`${S}[${B}]`)});else G.append(`${S}[${B}]`,String(T))});else if(typeof L==="object"&&L!==null)Object.entries(L).forEach(([T,B])=>{W(T,B,S)});else G.append(S,String(L))};Object.entries(Z).forEach(([$,L])=>{W($,L)});let O=await x(V(_),{method:"POST",headers:j,body:G,credentials:"include"});if(F)console.log(`BRPC formMutation response from ${_}:`,O);return O};else if(X==="subscription")return(Z,w={})=>{let _=K(I,w.params);if(F)console.log(`BRPC subscription to ${_}`);let j=H?`${H.slice(1)}/${_}`:_;return C.updateAuth(),C.subscribe(j,Z,w)};else if(X==="getStringKey")return(Z,w={})=>{let _=K(I,w.params);if(!Z||Object.keys(Z).length===0)return _;let j=Object.keys(Z).sort().reduce((W,O)=>{let $=Z[O];if($!==void 0&&$!==null)W[O]=$;return W},{});if(Object.keys(j).length===0)return _;let G=JSON.stringify(j);return`${_}?${G}`};else if(X==="getArrayKey")return(Z,w={})=>{let _=K(I,w.params),j=Z??{};if(Object.keys(j).length===0)return[_];let G=[];if(Object.keys(j).sort().forEach((O)=>{let $=j[O];if($!==void 0&&$!==null){let L=$;if(typeof $==="boolean"||typeof $==="number")L=String($);else if(typeof $==="object")L=JSON.stringify($);G.push([O,L])}}),G.length===0)return[_];let W=G.flatMap(([O,$])=>[O,$]);return[_,...W]};else if(X==="getNoInputsArrayKey")return(Z={})=>{return[K(I,Z.params)]}}return v([...Y,X])}})},t=v();if(F)console.log("BRPC client created",{baseUrl:E,prefix:H,wsUrl:m});return{routes:t,storage:{getObjectUrl:d(l)},utils:{updateWsAuth:async()=>await C.updateAuth(),setHeader:async(Y,R)=>{if(typeof A==="function"){console.warn("Cannot use setHeader with function-based headers resolver");return}A={...A,[Y]:R},await C.updateAuth()},setHeaders:async(Y)=>{if(typeof A==="function"){console.warn("Cannot use setHeaders with function-based headers resolver");return}A={...A,...Y},await C.updateAuth()}}}}export{e as isBrpcClientErrorUtility,d as createGetObjectUrl,Gq as createBrpcClient,f as BrpcClientSubscriptionError,M as BrpcClientError};
2
+ function u(q){return(J)=>{if(!J)return null;return`${q}/${J}`}}class F extends Error{status;code;clientCode;data;constructor(q,J,Q,N,G){super(q);this.name="BrpcClientError",this.status=J,this.code=Q,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 b extends F{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}}function qq(){return function(J,Q){if(Q)return J instanceof F&&J.clientCode===Q;return J instanceof F}}var v=(q)=>{return q instanceof File||q instanceof Blob||typeof q==="object"&&q!==null&&"uri"in q&&"name"in q&&"type"in q};function m(q){if(!q)return null;return q.replace(/\//g,"")}function l(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 p(q,J){let Q=J?`/${J}/ws`:"/ws";return`${q.replace(/^http/,"ws")}${Q}`}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 P{baseUrl;ws=null;subscriptions=new Map;messageQueue=[];isConnected=!1;reconnectTimeout=null;WebSocketImpl;getHeaders;authToken=null;debug;pendingAuth=!1;authActions=[];constructor(q,J,Q,N=!1,G){this.baseUrl=q;this.WebSocketImpl=J,this.debug=N&&G==="development",this.getHeaders=async()=>{if(typeof Q==="function")return await Q();return Q??{}},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 Q of this.subscriptions.keys())this.startTopicSubscription(Q);this.authActions.push(()=>{if(this.messageQueue.length>0&&this.debug)console.log(`Sending ${this.messageQueue.length} queued messages`);this.messageQueue.forEach((Q)=>this.send(Q)),this.messageQueue=[]})}else{for(let Q of this.subscriptions.keys())this.startTopicSubscription(Q);if(this.messageQueue.length>0&&this.debug)console.log(`Sending ${this.messageQueue.length} queued messages`);this.messageQueue.forEach((Q)=>this.send(Q)),this.messageQueue=[]}},this.ws.onmessage=(Q)=>{try{if(this.debug)console.log("WebSocket message received:",Q.data);let N=JSON.parse(Q.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((w)=>w());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((w)=>w(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),w=this.subscriptions.get(N.topic);w.status="failed",w.callbacks.forEach((Z)=>Z(G,void 0))}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:",Q.data)}},this.ws.onclose=(Q)=>{if(this.isConnected=!1,this.pendingAuth=!1,this.subscriptions.forEach((N)=>{N.status="stale"}),this.debug)console.log(`WebSocket connection closed. Code: ${Q.code}, Reason: ${Q.reason}`);this.reconnectTimeout=setTimeout(()=>this.connect(),2000)},this.ws.onerror=(Q)=>{if(this.debug)console.log("WebSocket error:",Q)}}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 F(q?.message??"WebSocket request failed",q?.httpStatus??0,q?.code,q?.clientCode,q?.data)}createSubscriptionError(q,J){return new b(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 Q=JSON.stringify(q);if(this.ws.send(Q),this.debug)console.log("WebSocket message sent:",Q)}else if(this.messageQueue.push(q),this.debug)console.log("WebSocket message queued (not connected):",q)}subscribe(q,J,Q={}){let N=this.subscriptions.get(q);if(!N){if(N={auth:Q.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 Q=this.subscriptions.get(q);if(Q){if(Q.callbacks.delete(J),Q.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,Q){let N=this.subscriptions.get(q);if(N?.status!=="subscribed"){Q(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"}}),void 0);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||"",Q=J.startsWith("Bearer ")?J.substring(7).trim():J.trim(),N=Q.length>0,G=this.authToken!==null&&this.authToken.trim().length>0;if((N!==G||Q!==this.authToken)&&this.isConnected){if(this.authToken=Q,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 Lq(q,J={}){let Q=J.fetch||fetch,N=J.WebSocket??(typeof WebSocket<"u"?WebSocket:null),G=J.prefix??"",w=J.apiPrefix??"",Z=J.debug??!1,D=J.s3Endpoint,U=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 T=J.headers??{},k=async()=>{if(typeof T==="function")return await T();return T},x=l(q),g=m(J.prefix),i=m(J.apiPrefix),B=n(g,i),y=p(x,g),E=new P(y,N,k,Z,U),f=async(Y=!0)=>{let R={};if(Y)R["Content-Type"]="application/json";let X=await k();return{...R,...X}},o=async(Y)=>{if(!Y.ok){let X;try{X=await Y.json()}catch{try{X={error:await Y.text()}}catch{X={error:"Failed to parse error response"}}}if(X?.error&&typeof X.error==="object"){let V=X.error;throw new F(V.message||Y.statusText,Y.status,V.code,V.clientCode,V.data)}let W=X?.error||X?.message||Y.statusText;throw new F(W,Y.status,void 0,void 0,X)}let R=Y.headers.get("Content-Type")||"";try{if(R.includes("application/json"))return(await Y.json()).data;else if(R.includes("text/"))return Y.text();else return Y.blob()}catch(X){throw new F(`Failed to parse response: ${X instanceof Error?X.message:"Unknown error"}`,Y.status,"PARSE_ERROR")}},h=async(Y,R)=>{try{let X=await Q(Y,R);return await o(X)}catch(X){if(X instanceof F)throw X;throw new F(X instanceof Error?X.message:"Network request failed",0,"NETWORK_ERROR")}},H=(Y,R)=>{return Y.split("/").map((X)=>{if(!X.startsWith(":"))return X;let W=X.slice(1),V=R?.[W];if(V===void 0||V===null)throw new F(`Missing route parameter: ${W}`,400,"MISSING_ROUTE_PARAM","MISSING_ROUTE_PARAM",{param:W});return encodeURIComponent(String(V))}).join("/")},d=(Y=[])=>{return new Proxy({},{get(R,X){if(typeof X==="symbol")return;if(X==="query"||X==="mutation"||X==="formMutation"||X==="subscription"||X==="getStringKey"||X==="getArrayKey"||X==="getNoInputsArrayKey"){let W=Y.join("/"),V=(_)=>`${x}${B}/${_}`;if(Z)console.log(`BRPC ${X} procedure path: ${W}`);if(X==="query")return async(_,O={})=>{let $=H(W,O.params),L=await f(!0);if(Z)console.log(`BRPC query request to ${$}`,{input:_,headers:L});let z=new URL(V($));if(_&&typeof _==="object")Object.entries(_).forEach(([A,j])=>{if(j!==void 0)z.searchParams.append(A,String(j))});let M=await h(z.toString(),{method:"GET",headers:L,credentials:"include"});if(Z)console.log(`BRPC query response from ${$}:`,M);return M};else if(X==="mutation")return async(_,O={})=>{let $=H(W,O.params),L=await f(!0);if(Z)console.log(`BRPC mutation request to ${$}`,{input:_,headers:L});let z=JSON.stringify(_),M=await h(V($),{method:"POST",headers:L,body:z,credentials:"include"});if(Z)console.log(`BRPC mutation response from ${$}:`,M);return M};else if(X==="formMutation")return async(_,O={})=>{let $=H(W,O.params),L=await f(!1);if(Z)console.log(`BRPC formMutation request to ${$}`,{input:_,headers:L});let z=new FormData,M=(j,I,c)=>{let C=c?`${c}[${j}]`:j;if(I===null||I===void 0)return;else if(v(I))z.append(C,I);else if(Array.isArray(I))I.forEach((K,S)=>{if(v(K))z.append(C,K);else if(typeof K==="object"&&K!==null)Object.entries(K).forEach(([a,s])=>{M(a,s,`${C}[${S}]`)});else z.append(`${C}[${S}]`,String(K))});else if(typeof I==="object"&&I!==null)Object.entries(I).forEach(([K,S])=>{M(K,S,C)});else z.append(C,String(I))};Object.entries(_).forEach(([j,I])=>{M(j,I)});let A=await h(V($),{method:"POST",headers:L,body:z,credentials:"include"});if(Z)console.log(`BRPC formMutation response from ${$}:`,A);return A};else if(X==="subscription")return(_,O={})=>{let $=H(W,O.params);if(Z)console.log(`BRPC subscription to ${$}`);let L=B?`${B.slice(1)}/${$}`:$;return E.updateAuth(),E.subscribe(L,_,O)};else if(X==="getStringKey")return(_,O={})=>{let $=H(W,O.params);if(!_||Object.keys(_).length===0)return $;let L=Object.keys(_).sort().reduce((M,A)=>{let j=_[A];if(j!==void 0&&j!==null)M[A]=j;return M},{});if(Object.keys(L).length===0)return $;let z=JSON.stringify(L);return`${$}?${z}`};else if(X==="getArrayKey")return(_,O={})=>{let $=H(W,O.params),L=_??{};if(Object.keys(L).length===0)return[$];let z=[];if(Object.keys(L).sort().forEach((A)=>{let j=L[A];if(j!==void 0&&j!==null){let I=j;if(typeof j==="boolean"||typeof j==="number")I=String(j);else if(typeof j==="object")I=JSON.stringify(j);z.push([A,I])}}),z.length===0)return[$];let M=z.flatMap(([A,j])=>[A,j]);return[$,...M]};else if(X==="getNoInputsArrayKey")return(_={})=>{return[H(W,_.params)]}}return d([...Y,X])}})},t=d();if(Z)console.log("BRPC client created",{baseUrl:x,prefix:B,wsUrl:y});return{routes:t,storage:{getObjectUrl:u(D)},utils:{updateWsAuth:async()=>await E.updateAuth(),setHeader:async(Y,R)=>{if(typeof T==="function"){console.warn("Cannot use setHeader with function-based headers resolver");return}T={...T,[Y]:R},await E.updateAuth()},setHeaders:async(Y)=>{if(typeof T==="function"){console.warn("Cannot use setHeaders with function-based headers resolver");return}T={...T,...Y},await E.updateAuth()}}}}class r{entries=new Map;subscribe({key:q,subscription:J,options:Q,listener:N}){let G=JSON.stringify(q),w=(...D)=>N(...D),Z=this.entries.get(G);if(!Z){Z={listeners:new Set,publish:()=>{},unsubscribe:()=>{}},this.entries.set(G,Z);try{let D=J((...U)=>{Z?.listeners.forEach((T)=>T(...U))},Q);Z.publish=D.publish,Z.unsubscribe=D.unsubscribe}catch(D){throw this.entries.delete(G),D}}return Z.listeners.add(w),{publish:(D)=>{if(this.entries.get(G)===Z)Z.publish(D)},unsubscribe:()=>this.unsubscribe(G,Z,w)}}unsubscribe(q,J,Q){if(J.listeners.delete(Q),J.listeners.size>0||this.entries.get(q)!==J)return;this.entries.delete(q),J.unsubscribe()}}export{qq as isBrpcClientErrorUtility,u as createGetObjectUrl,Lq as createBrpcClient,r as SubscriptionService,b as BrpcClientSubscriptionError,F as BrpcClientError};
3
3
 
4
- //# debugId=6A46A4B49FFA34B564756E2164756E21
4
+ //# debugId=9779B2DB508C399A64756E2164756E21
@@ -1,4 +1,4 @@
1
1
  // @bun
2
- import{useCallback as F,useEffect as G,useRef as H}from"react";function K(j,q,...y){let x=y[0],m=H(null);return G(()=>{if(x?.enabled===!1){m.current=null;return}let{enabled:w,...z}=x??{},{publish:B,unsubscribe:D}=j(q,z);return m.current=B,()=>{m.current=null,D()}},[j,q,x]),{publish:F((w)=>{m.current?.(w)},[])}}export{K as useSubscription};
2
+ import{useCallback as D,useEffect as K,useRef as F}from"react";function O(G){return function({procedure:q,options:j,callback:w,enabled:z=!0}){let A=q.getNoInputsArrayKey(j),H=JSON.stringify(A),B=F(w),x=F(null);B.current=w;let I=D((...m)=>B.current(...m),[]);return K(()=>{if(!z){x.current=null;return}let{publish:m,unsubscribe:J}=G.subscribe({key:A,subscription:q.subscription,options:j,listener:I});return x.current=m,()=>{x.current=null,J()}},[z,H,j.auth]),{publish:D((m)=>{x.current?.(m)},[])}}}export{O as createUseSubscription};
3
3
 
4
- //# debugId=6D0EB72372875B3364756E2164756E21
4
+ //# debugId=1F36C573C33D7C1C64756E2164756E21
@@ -0,0 +1,14 @@
1
+ import type { RuntimeSubscriptionOptions, SubscriptionCallback, SubscriptionHandlers } from "../types";
2
+ export type UseSubscriptionDeprecatedOptions<TOptions extends RuntimeSubscriptionOptions = RuntimeSubscriptionOptions> = TOptions & {
3
+ enabled?: boolean;
4
+ };
5
+ type UseSubscriptionDeprecatedOptionsArgs<TOptions extends RuntimeSubscriptionOptions> = TOptions extends {
6
+ params: infer _Params;
7
+ } ? [options: UseSubscriptionDeprecatedOptions<TOptions>] : [options?: UseSubscriptionDeprecatedOptions<TOptions>];
8
+ /**
9
+ * @deprecated Use `createUseSubscription` with `SubscriptionService`.
10
+ */
11
+ export declare function useSubscriptionDeprecated<TInput, TOutput, TOptions extends RuntimeSubscriptionOptions>(subscription: (callback: SubscriptionCallback<TOutput>, options: TOptions) => SubscriptionHandlers<TInput, TOutput>, callback: SubscriptionCallback<TOutput>, ...optionsArgs: UseSubscriptionDeprecatedOptionsArgs<TOptions>): {
12
+ publish: (data: TInput) => void;
13
+ };
14
+ export {};
@@ -1,14 +1,18 @@
1
- import type { RuntimeSubscriptionOptions, SubscriptionCallback, SubscriptionHandlers } from "../types";
2
- export type UseSubscriptionOptions<TOptions extends RuntimeSubscriptionOptions = RuntimeSubscriptionOptions> = TOptions & {
1
+ import type { SubscriptionKey, SubscriptionService, SubscriptionSource } from "../subscription-service";
2
+ import type { RuntimeSubscriptionOptions, SubscriptionCallback } from "../types";
3
+ export type UseSubscriptionOptions<TInput, TOutput, TOptions extends RuntimeSubscriptionOptions> = {
4
+ procedure: SubscriptionProcedure<TInput, TOutput, TOptions>;
5
+ options: TOptions;
6
+ callback: SubscriptionCallback<TOutput>;
3
7
  enabled?: boolean;
4
8
  };
5
- type UseSubscriptionOptionsArgs<TOptions extends RuntimeSubscriptionOptions> = TOptions extends {
6
- params: infer _Params;
7
- } ? [options: UseSubscriptionOptions<TOptions>] : [options?: UseSubscriptionOptions<TOptions>];
9
+ export type SubscriptionProcedure<TInput, TOutput, TOptions extends RuntimeSubscriptionOptions> = {
10
+ subscription: SubscriptionSource<TInput, TOutput, TOptions>;
11
+ getNoInputsArrayKey: (options: TOptions) => SubscriptionKey;
12
+ };
8
13
  /**
9
- * Connects React state to a BRPC subscription.
14
+ * Creates a React hook backed by one keyed subscription registry.
10
15
  */
11
- export declare function useSubscription<TInput, TOutput, TOptions extends RuntimeSubscriptionOptions>(subscription: (callback: SubscriptionCallback<TOutput>, options: TOptions) => SubscriptionHandlers<TInput, TOutput>, callback: SubscriptionCallback<TOutput>, ...optionsArgs: UseSubscriptionOptionsArgs<TOptions>): {
12
- publish: (data: TInput) => void;
16
+ export declare function createUseSubscription(subscriptionService: SubscriptionService): <TInput, TOutput, TOptions extends RuntimeSubscriptionOptions>({ procedure, options, callback, enabled, }: UseSubscriptionOptions<TInput, TOutput, TOptions>) => {
17
+ publish: (input: TInput) => void;
13
18
  };
14
- export {};
@@ -0,0 +1,20 @@
1
+ import type { RuntimeSubscriptionOptions, SubscriptionCallback, SubscriptionHandlers } from "./types";
2
+ export type SubscriptionKey = readonly (string | number | boolean)[];
3
+ export type SubscriptionSource<TInput, TOutput, TOptions extends RuntimeSubscriptionOptions> = (callback: SubscriptionCallback<TOutput>, options: TOptions) => SubscriptionHandlers<TInput, TOutput>;
4
+ /**
5
+ * Shares one underlying BRPC topic subscription between local listeners with
6
+ * the same deterministic key.
7
+ */
8
+ export declare class SubscriptionService {
9
+ private entries;
10
+ subscribe<TInput, TOutput, TOptions extends RuntimeSubscriptionOptions>({ key, subscription, options, listener, }: {
11
+ key: SubscriptionKey;
12
+ subscription: SubscriptionSource<TInput, TOutput, TOptions>;
13
+ options: TOptions;
14
+ listener: SubscriptionCallback<TOutput>;
15
+ }): {
16
+ publish: (input: TInput) => void;
17
+ unsubscribe: () => void;
18
+ };
19
+ private unsubscribe;
20
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mateosuarezdev/brpc",
3
- "version": "1.0.89",
3
+ "version": "1.0.90",
4
4
  "description": "A Type-Safe, Flexible Web application framework for Bun",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",