@mateosuarezdev/brpc 1.0.106 → 1.0.107
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 +38 -11
- package/dist/client/index.js +2 -2
- package/dist/client/react/index.d.ts +1 -0
- package/dist/client/react/index.js +2 -2
- package/dist/client/react/use-ws-reconnect.d.ts +10 -0
- package/dist/client/types.d.ts +1 -0
- package/dist/client/ws-manager.d.ts +10 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -824,14 +824,20 @@ const billingAccount = useQuery({
|
|
|
824
824
|
|
|
825
825
|
Because the key changes with the session, signing in as somebody else reads that account's own entry, while the previous user's entry stays cached and is still there if they sign back in. That is the reason to scope keys rather than clear the cache on sign-out: clearing would throw away the offline data persistence exists to keep.
|
|
826
826
|
|
|
827
|
-
### Utilities
|
|
828
|
-
|
|
829
|
-
```ts
|
|
830
|
-
// Update WebSocket auth token (e.g. after token refresh)
|
|
831
|
-
await client.utils.updateWsAuth();
|
|
832
|
-
|
|
833
|
-
//
|
|
834
|
-
|
|
827
|
+
### Utilities
|
|
828
|
+
|
|
829
|
+
```ts
|
|
830
|
+
// Update WebSocket auth token (e.g. after token refresh)
|
|
831
|
+
await client.utils.updateWsAuth();
|
|
832
|
+
|
|
833
|
+
// Heal client-side state after a socket reconnect. The initial connection does
|
|
834
|
+
// not call this; the returned function removes this listener.
|
|
835
|
+
const unsubscribe = client.utils.onWsReconnect(() => {
|
|
836
|
+
invalidateLiveData();
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
// Update headers at runtime
|
|
840
|
+
await client.utils.setHeader("Authorization", `Bearer ${newToken}`);
|
|
835
841
|
await client.utils.setHeaders({ "X-Custom": "value" });
|
|
836
842
|
```
|
|
837
843
|
|
|
@@ -858,7 +864,7 @@ try {
|
|
|
858
864
|
|
|
859
865
|
### React
|
|
860
866
|
|
|
861
|
-
Create one subscription service, then bind the React hook once:
|
|
867
|
+
Create one subscription service, then bind the React hook once:
|
|
862
868
|
|
|
863
869
|
```ts
|
|
864
870
|
import { SubscriptionService } from "@mateosuarezdev/brpc/client";
|
|
@@ -885,7 +891,28 @@ function ChatRoom({ roomId }: { roomId: string }) {
|
|
|
885
891
|
</button>
|
|
886
892
|
);
|
|
887
893
|
}
|
|
888
|
-
```
|
|
894
|
+
```
|
|
895
|
+
|
|
896
|
+
For ordinary React state that needs to heal after a missed WebSocket publish,
|
|
897
|
+
bind the reconnect listener once to the client instance. The hook retains the
|
|
898
|
+
latest callback across renders and removes its listener on unmount:
|
|
899
|
+
|
|
900
|
+
```ts
|
|
901
|
+
import { createUseOnWsReconnect } from "@mateosuarezdev/brpc/client/react";
|
|
902
|
+
|
|
903
|
+
const useOnWsReconnect = createUseOnWsReconnect(client);
|
|
904
|
+
|
|
905
|
+
function BillingState() {
|
|
906
|
+
useOnWsReconnect(() => {
|
|
907
|
+
subscriptionsInfo.invalidate();
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
Reconnect listeners are client lifecycle signals, not subscription messages:
|
|
913
|
+
they fire after a connection following a disconnection, including when no topic
|
|
914
|
+
is currently subscribed. Use them to refetch/invalidate local cache; server
|
|
915
|
+
authorization must continue to read authoritative state.
|
|
889
916
|
|
|
890
917
|
---
|
|
891
918
|
|
|
@@ -1083,7 +1110,7 @@ const uploadResult = await generateAndUploadImage("/exercises", imageFile, {
|
|
|
1083
1110
|
// uploadResult.variants — keyed object with each variant's key, name, metadata
|
|
1084
1111
|
```
|
|
1085
1112
|
|
|
1086
|
-
Variants are keyed by `VariantName`, which is an image size (`xs`, `sm`, `md`, `lg`, `xl`) or `default` for a single-representation file.
|
|
1113
|
+
Variants are keyed by `VariantName`, which is an image size (`xs`, `sm`, `md`, `lg`, `xl`) or `default` for a single-representation file. An upload result is sparse: it contains only variants it actually wrote.
|
|
1087
1114
|
|
|
1088
1115
|
`uploadGeneratedVariants` settles all variant writes. If one fails, it deletes
|
|
1089
1116
|
every fulfilled sibling key before rethrowing the original failure, so a failed
|
package/dist/client/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
function u(q){return(J)=>{if(!J)return null;return`${q}/${J}`}}class V 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 m 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}}function e(){return function(J,Q){if(Q)return J instanceof V&&J.clientCode===Q;return J instanceof V}}var c=(q)=>{return q instanceof File||q instanceof Blob||typeof q==="object"&&q!==null&&"uri"in q&&"name"in q&&"type"in q};function P(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 i(q,J){if(q&&J)return`/${q}/${J}`;else if(q&&!J)return`/${q}`;else if(!q&&J)return`/${J}`;else return""}class v{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((_)=>_(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 V(q?.message??"WebSocket request failed",q?.httpStatus??0,q?.code,q?.clientCode,q?.data)}createSubscriptionError(q,J){return new m(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 n(q){if(!q)return[];let J=[];return Object.keys(q).sort().forEach((Q)=>{let N=q[Q];if(N===void 0||N===null||N==="")return;J.push(Q,String(N))}),J}function Gq(q,J={}){let Q=J.fetch||fetch,N=J.WebSocket??(typeof WebSocket<"u"?WebSocket:null),G=J.prefix??"",w=J.apiPrefix??"",_=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},S=l(q),g=P(J.prefix),o=P(J.apiPrefix),E=i(g,o),y=p(S,g),C=new v(y,N,k,_,U),x=async(Y=!0)=>{let R={};if(Y)R["Content-Type"]="application/json";let X=await k();return{...R,...X}},t=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 A=X.error;throw new V(A.message||Y.statusText,Y.status,A.code,A.clientCode,A.data)}let W=X?.error||X?.message||Y.statusText;throw new V(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 V(`Failed to parse response: ${X instanceof Error?X.message:"Unknown error"}`,Y.status,"PARSE_ERROR")}},f=async(Y,R)=>{try{let X=await Q(Y,R);return await t(X)}catch(X){if(X instanceof V)throw X;throw new V(X instanceof Error?X.message:"Network request failed",0,"NETWORK_ERROR")}},O=(Y,R)=>{return Y.split("/").map((X)=>{if(!X.startsWith(":"))return X;let W=X.slice(1),A=R?.[W];if(A===void 0||A===null)throw new V(`Missing route parameter: ${W}`,400,"MISSING_ROUTE_PARAM","MISSING_ROUTE_PARAM",{param:W});return encodeURIComponent(String(A))}).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("/"),A=(Z)=>`${S}${E}/${Z}`;if(_)console.log(`BRPC ${X} procedure path: ${W}`);if(X==="query")return async(Z,M={})=>{let $=O(W,M.params),L=await x(!0);if(_)console.log(`BRPC query request to ${$}`,{input:Z,headers:L});let F=new URL(A($));if(Z&&typeof Z==="object")Object.entries(Z).forEach(([K,I])=>{if(I!==void 0)F.searchParams.append(K,String(I))});let z=await f(F.toString(),{method:"GET",headers:L,credentials:"include"});if(_)console.log(`BRPC query response from ${$}:`,z);return z};else if(X==="mutation")return async(Z,M={})=>{let $=O(W,M.params),L=await x(!0);if(_)console.log(`BRPC mutation request to ${$}`,{input:Z,headers:L});let F=JSON.stringify(Z??{}),z=await f(A($),{method:"POST",headers:L,body:F,credentials:"include"});if(_)console.log(`BRPC mutation response from ${$}:`,z);return z};else if(X==="formMutation")return async(Z,M={})=>{let $=O(W,M.params),L=await x(!1);if(_)console.log(`BRPC formMutation request to ${$}`,{input:Z,headers:L});let F=new FormData,z=(I,j,H)=>{let B=H?`${H}[${I}]`:I;if(j===null||j===void 0)return;else if(c(j))F.append(B,j);else if(Array.isArray(j))j.forEach((b,h)=>{z(String(h),b,B)});else if(typeof j==="object"&&j!==null)Object.entries(j).forEach(([b,h])=>{z(b,h,B)});else F.append(B,String(j))};Object.entries(Z).forEach(([I,j])=>{z(I,j)});let K=await f(A($),{method:"POST",headers:L,body:F,credentials:"include"});if(_)console.log(`BRPC formMutation response from ${$}:`,K);return K};else if(X==="subscription")return(Z,M={})=>{let $=O(W,M.params);if(_)console.log(`BRPC subscription to ${$}`);let L=E?`${E.slice(1)}/${$}`:$;return C.updateAuth(),C.subscribe(L,Z,M)};else if(X==="getStringKey")return(Z,M={})=>{let $=O(W,M.params);if(!Z||Object.keys(Z).length===0)return $;let L=Object.keys(Z).sort().reduce((z,K)=>{let I=Z[K];if(I!==void 0&&I!==null)z[K]=I;return z},{});if(Object.keys(L).length===0)return $;let F=JSON.stringify(L);return`${$}?${F}`};else if(X==="getArrayKey")return(Z,M={})=>{let $=O(W,M.params),L=n(M.keys),F=Z??{};if(Object.keys(F).length===0)return[$,...L];let z=[];if(Object.keys(F).sort().forEach((I)=>{let j=F[I];if(j!==void 0&&j!==null){let H=j;if(typeof j==="boolean"||typeof j==="number")H=String(j);else if(typeof j==="object")H=JSON.stringify(j);z.push([I,H])}}),z.length===0)return[$,...L];let K=z.flatMap(([I,j])=>[I,j]);return[$,...K,...L]};else if(X==="getNoInputsArrayKey")return(Z={})=>{return[O(W,Z.params),...n(Z.keys)]}}return d([...Y,X])}})},a=d();if(_)console.log("BRPC client created",{baseUrl:S,prefix:E,wsUrl:y});return{routes:a,storage:{getObjectUrl:u(D)},utils:{updateWsAuth:async()=>await C.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 C.updateAuth()},setHeaders:async(Y)=>{if(typeof T==="function"){console.warn("Cannot use setHeaders with function-based headers resolver");return}T={...T,...Y},await C.updateAuth()}}}}class s{entries=new Map;subscribe({key:q,subscription:J,options:Q,listener:N}){let G=JSON.stringify(q),w=(...D)=>N(...D),_=this.entries.get(G);if(!_){_={listeners:new Set,publish:()=>{},unsubscribe:()=>{}},this.entries.set(G,_);try{let D=J((...U)=>{_?.listeners.forEach((T)=>T(...U))},Q);_.publish=D.publish,_.unsubscribe=D.unsubscribe}catch(D){throw this.entries.delete(G),D}}return _.listeners.add(w),{publish:(D)=>{if(this.entries.get(G)===_)_.publish(D)},unsubscribe:()=>this.unsubscribe(G,_,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{e as isBrpcClientErrorUtility,u as createGetObjectUrl,Gq as createBrpcClient,s as SubscriptionService,m as BrpcClientSubscriptionError,V as BrpcClientError};
|
|
2
|
+
function u(q){return(J)=>{if(!J)return null;return`${q}/${J}`}}class M extends Error{status;code;clientCode;data;constructor(q,J,Q,N,_){super(q);this.name="BrpcClientError",this.status=J,this.code=Q,this.clientCode=N,this.data=_}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 m 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 c=(q)=>{return q instanceof File||q instanceof Blob||typeof q==="object"&&q!==null&&"uri"in q&&"name"in q&&"type"in q};function P(q){if(!q)return null;return q.replace(/\//g,"")}function p(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 l(q,J){let Q=J?`/${J}/ws`:"/ws";return`${q.replace(/^http/,"ws")}${Q}`}function i(q,J){if(q&&J)return`/${q}/${J}`;else if(q&&!J)return`/${q}`;else if(!q&&J)return`/${J}`;else return""}class v{baseUrl;reconnectDelayMs;ws=null;subscriptions=new Map;messageQueue=[];isConnected=!1;reconnectTimeout=null;hasConnected=!1;reconnectListeners=new Set;WebSocketImpl;getHeaders;authToken=null;debug;pendingAuth=!1;authActions=[];constructor(q,J,Q,N=!1,_,T=2000){this.baseUrl=q;this.reconnectDelayMs=T;this.WebSocketImpl=J,this.debug=N&&_==="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");let Q=this.hasConnected;if(this.hasConnected=!0,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 N of this.subscriptions.keys())this.startTopicSubscription(N);this.authActions.push(()=>{if(this.messageQueue.length>0&&this.debug)console.log(`Sending ${this.messageQueue.length} queued messages`);this.messageQueue.forEach((N)=>this.send(N)),this.messageQueue=[]})}else{for(let N of this.subscriptions.keys())this.startTopicSubscription(N);if(this.messageQueue.length>0&&this.debug)console.log(`Sending ${this.messageQueue.length} queued messages`);this.messageQueue.forEach((N)=>this.send(N)),this.messageQueue=[]}if(Q)queueMicrotask(()=>{this.reconnectListeners.forEach((N)=>{try{N()}catch(_){console.error("WebSocket reconnect listener failed:",_)}})})},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 _=N.authenticated!==!1;if(this.debug)console.log(`WebSocket ${_?"authentication":"deauthentication"} successful`);if(this.pendingAuth=!1,_)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:_}=this.subscriptions.get(N.topic);if(this.debug)console.log("Calling subscription callbacks");_?.forEach((T)=>T(void 0,N.data))}else if(["subscribe_error","publish_error"].includes(N.type)&&N.topic&&this.subscriptions.has(N.topic)){let _=this.createSubscriptionError(N.type,N.error),T=this.subscriptions.get(N.topic);T.status="failed",T.callbacks.forEach(($)=>$(_,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(),this.reconnectDelayMs)},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(),this.reconnectDelayMs)}}createClientError(q){return new M(q?.message??"WebSocket request failed",q?.httpStatus??0,q?.code,q?.clientCode,q?.data)}createSubscriptionError(q,J){return new m(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(),this.reconnectListeners.clear()}onReconnect(q){return this.reconnectListeners.add(q),()=>this.reconnectListeners.delete(q)}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:(_)=>this.publish(q,_,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,_=this.authToken!==null&&this.authToken.trim().length>0;if((N!==_||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 n(q){if(!q)return[];let J=[];return Object.keys(q).sort().forEach((Q)=>{let N=q[Q];if(N===void 0||N===null||N==="")return;J.push(Q,String(N))}),J}function Gq(q,J={}){let Q=J.fetch||fetch,N=J.WebSocket??(typeof WebSocket<"u"?WebSocket:null),_=J.prefix??"",T=J.apiPrefix??"",$=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 w=J.headers??{},k=async()=>{if(typeof w==="function")return await w();return w},S=p(q),g=P(J.prefix),o=P(J.apiPrefix),E=i(g,o),y=l(S,g),H=new v(y,N,k,$,U),x=async(Y=!0)=>{let R={};if(Y)R["Content-Type"]="application/json";let X=await k();return{...R,...X}},t=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 A=X.error;throw new M(A.message||Y.statusText,Y.status,A.code,A.clientCode,A.data)}let W=X?.error||X?.message||Y.statusText;throw new M(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 M(`Failed to parse response: ${X instanceof Error?X.message:"Unknown error"}`,Y.status,"PARSE_ERROR")}},f=async(Y,R)=>{try{let X=await Q(Y,R);return await t(X)}catch(X){if(X instanceof M)throw X;throw new M(X instanceof Error?X.message:"Network request failed",0,"NETWORK_ERROR")}},O=(Y,R)=>{return Y.split("/").map((X)=>{if(!X.startsWith(":"))return X;let W=X.slice(1),A=R?.[W];if(A===void 0||A===null)throw new M(`Missing route parameter: ${W}`,400,"MISSING_ROUTE_PARAM","MISSING_ROUTE_PARAM",{param:W});return encodeURIComponent(String(A))}).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("/"),A=(Z)=>`${S}${E}/${Z}`;if($)console.log(`BRPC ${X} procedure path: ${W}`);if(X==="query")return async(Z,V={})=>{let j=O(W,V.params),L=await x(!0);if($)console.log(`BRPC query request to ${j}`,{input:Z,headers:L});let F=new URL(A(j));if(Z&&typeof Z==="object")Object.entries(Z).forEach(([K,I])=>{if(I!==void 0)F.searchParams.append(K,String(I))});let z=await f(F.toString(),{method:"GET",headers:L,credentials:"include"});if($)console.log(`BRPC query response from ${j}:`,z);return z};else if(X==="mutation")return async(Z,V={})=>{let j=O(W,V.params),L=await x(!0);if($)console.log(`BRPC mutation request to ${j}`,{input:Z,headers:L});let F=JSON.stringify(Z??{}),z=await f(A(j),{method:"POST",headers:L,body:F,credentials:"include"});if($)console.log(`BRPC mutation response from ${j}:`,z);return z};else if(X==="formMutation")return async(Z,V={})=>{let j=O(W,V.params),L=await x(!1);if($)console.log(`BRPC formMutation request to ${j}`,{input:Z,headers:L});let F=new FormData,z=(I,G,C)=>{let B=C?`${C}[${I}]`:I;if(G===null||G===void 0)return;else if(c(G))F.append(B,G);else if(Array.isArray(G))G.forEach((b,h)=>{z(String(h),b,B)});else if(typeof G==="object"&&G!==null)Object.entries(G).forEach(([b,h])=>{z(b,h,B)});else F.append(B,String(G))};Object.entries(Z).forEach(([I,G])=>{z(I,G)});let K=await f(A(j),{method:"POST",headers:L,body:F,credentials:"include"});if($)console.log(`BRPC formMutation response from ${j}:`,K);return K};else if(X==="subscription")return(Z,V={})=>{let j=O(W,V.params);if($)console.log(`BRPC subscription to ${j}`);let L=E?`${E.slice(1)}/${j}`:j;return H.updateAuth(),H.subscribe(L,Z,V)};else if(X==="getStringKey")return(Z,V={})=>{let j=O(W,V.params);if(!Z||Object.keys(Z).length===0)return j;let L=Object.keys(Z).sort().reduce((z,K)=>{let I=Z[K];if(I!==void 0&&I!==null)z[K]=I;return z},{});if(Object.keys(L).length===0)return j;let F=JSON.stringify(L);return`${j}?${F}`};else if(X==="getArrayKey")return(Z,V={})=>{let j=O(W,V.params),L=n(V.keys),F=Z??{};if(Object.keys(F).length===0)return[j,...L];let z=[];if(Object.keys(F).sort().forEach((I)=>{let G=F[I];if(G!==void 0&&G!==null){let C=G;if(typeof G==="boolean"||typeof G==="number")C=String(G);else if(typeof G==="object")C=JSON.stringify(G);z.push([I,C])}}),z.length===0)return[j,...L];let K=z.flatMap(([I,G])=>[I,G]);return[j,...K,...L]};else if(X==="getNoInputsArrayKey")return(Z={})=>{return[O(W,Z.params),...n(Z.keys)]}}return d([...Y,X])}})},a=d();if($)console.log("BRPC client created",{baseUrl:S,prefix:E,wsUrl:y});return{routes:a,storage:{getObjectUrl:u(D)},utils:{updateWsAuth:async()=>await H.updateAuth(),onWsReconnect:(Y)=>H.onReconnect(Y),setHeader:async(Y,R)=>{if(typeof w==="function"){console.warn("Cannot use setHeader with function-based headers resolver");return}w={...w,[Y]:R},await H.updateAuth()},setHeaders:async(Y)=>{if(typeof w==="function"){console.warn("Cannot use setHeaders with function-based headers resolver");return}w={...w,...Y},await H.updateAuth()}}}}class s{entries=new Map;subscribe({key:q,subscription:J,options:Q,listener:N}){let _=JSON.stringify(q),T=(...D)=>N(...D),$=this.entries.get(_);if(!$){$={listeners:new Set,publish:()=>{},unsubscribe:()=>{}},this.entries.set(_,$);try{let D=J((...U)=>{$?.listeners.forEach((w)=>w(...U))},Q);$.publish=D.publish,$.unsubscribe=D.unsubscribe}catch(D){throw this.entries.delete(_),D}}return $.listeners.add(T),{publish:(D)=>{if(this.entries.get(_)===$)$.publish(D)},unsubscribe:()=>this.unsubscribe(_,$,T)}}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{e as isBrpcClientErrorUtility,u as createGetObjectUrl,Gq as createBrpcClient,s as SubscriptionService,m as BrpcClientSubscriptionError,M as BrpcClientError};
|
|
3
3
|
|
|
4
|
-
//# debugId=
|
|
4
|
+
//# debugId=0D9F302BFC80CAE164756E2164756E21
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{useCallback as
|
|
2
|
+
import{useCallback as G,useEffect as M,useRef as H}from"react";function T(w){return function({procedure:j,options:x,callback:z,enabled:A=!0}){let D=j.getNoInputsArrayKey(x),J=JSON.stringify(D),F=H(z),q=H(null);F.current=z;let K=G((...m)=>F.current(...m),[]);return M(()=>{if(!A){q.current=null;return}let{publish:m,unsubscribe:L}=w.subscribe({key:D,subscription:j.subscription,options:x,listener:K});return q.current=m,()=>{q.current=null,L()}},[A,J,x.auth]),{publish:G((m)=>{q.current?.(m)},[])}}}import{useEffect as N,useRef as O}from"react";function X(w){return function(j){let x=O(j);x.current=j,N(()=>{return w.utils.onWsReconnect(()=>x.current())},[])}}export{T as createUseSubscription,X as createUseOnWsReconnect};
|
|
3
3
|
|
|
4
|
-
//# debugId=
|
|
4
|
+
//# debugId=6911078E96D70E4164756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type WsReconnectSource = {
|
|
2
|
+
utils: {
|
|
3
|
+
onWsReconnect: (listener: () => void) => () => void;
|
|
4
|
+
};
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Binds a client-scoped WebSocket reconnect listener to a component lifetime.
|
|
8
|
+
*/
|
|
9
|
+
export declare function createUseOnWsReconnect(client: WsReconnectSource): (callback: () => void) => void;
|
|
10
|
+
export {};
|
package/dist/client/types.d.ts
CHANGED
|
@@ -137,6 +137,7 @@ export type BrpcClient<T> = {
|
|
|
137
137
|
};
|
|
138
138
|
utils: {
|
|
139
139
|
updateWsAuth: () => Promise<void>;
|
|
140
|
+
onWsReconnect: (listener: () => void) => () => void;
|
|
140
141
|
setHeader: (key: string, value: string) => Promise<void>;
|
|
141
142
|
setHeaders: (headers: Record<string, string>) => Promise<void>;
|
|
142
143
|
};
|
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import type { HeadersResolver, SubscriptionCallback, RuntimeSubscriptionOptions } from "./types";
|
|
2
|
+
type WsReconnectListener = () => void;
|
|
2
3
|
declare class WebSocketManager {
|
|
3
4
|
private baseUrl;
|
|
5
|
+
private reconnectDelayMs;
|
|
4
6
|
private ws;
|
|
5
7
|
private subscriptions;
|
|
6
8
|
private messageQueue;
|
|
7
9
|
private isConnected;
|
|
8
10
|
private reconnectTimeout;
|
|
11
|
+
private hasConnected;
|
|
12
|
+
private reconnectListeners;
|
|
9
13
|
private WebSocketImpl;
|
|
10
14
|
private getHeaders;
|
|
11
15
|
private authToken;
|
|
12
16
|
private debug;
|
|
13
17
|
private pendingAuth;
|
|
14
18
|
private authActions;
|
|
15
|
-
constructor(baseUrl: string, WebSocketImpl: typeof WebSocket, headersResolver: HeadersResolver, debug: boolean | undefined, nodeEnv: string);
|
|
19
|
+
constructor(baseUrl: string, WebSocketImpl: typeof WebSocket, headersResolver: HeadersResolver, debug: boolean | undefined, nodeEnv: string, reconnectDelayMs?: number);
|
|
16
20
|
/**
|
|
17
21
|
* Sends a topic subscription when its authentication requirement is met.
|
|
18
22
|
*/
|
|
@@ -21,6 +25,11 @@ declare class WebSocketManager {
|
|
|
21
25
|
private createClientError;
|
|
22
26
|
private createSubscriptionError;
|
|
23
27
|
disconnect(): void;
|
|
28
|
+
/**
|
|
29
|
+
* Runs after the socket reconnects following a prior connection. The initial
|
|
30
|
+
* connection does not notify listeners.
|
|
31
|
+
*/
|
|
32
|
+
onReconnect(listener: WsReconnectListener): () => boolean;
|
|
24
33
|
send(message: any, options?: {
|
|
25
34
|
allowDuringPendingAuth?: boolean;
|
|
26
35
|
}): void;
|