@mateosuarezdev/brpc 1.0.106 → 1.0.108

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
@@ -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
- // Update headers at runtime
834
- await client.utils.setHeader("Authorization", `Bearer ${newToken}`);
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
 
@@ -998,11 +1025,13 @@ All storage utilities are exported from the main package:
998
1025
  import {
999
1026
  uploadOne,
1000
1027
  uploadMany,
1001
- uploadSingleObject,
1002
- generateAndUploadImage,
1003
- uploadGeneratedVariants,
1004
- deleteOne,
1005
- deleteMany,
1028
+ uploadSingleObject,
1029
+ generateAndUploadImage,
1030
+ uploadGeneratedVariants,
1031
+ deleteOne,
1032
+ deleteManyKeys,
1033
+ deleteManyObjects,
1034
+ extractObjectVariantKeys,
1006
1035
  generateVariants,
1007
1036
  getOptimizedImage,
1008
1037
  getImageMetadata,
@@ -1083,7 +1112,7 @@ const uploadResult = await generateAndUploadImage("/exercises", imageFile, {
1083
1112
  // uploadResult.variants — keyed object with each variant's key, name, metadata
1084
1113
  ```
1085
1114
 
1086
- Variants are keyed by `VariantName`, which is an image size (`xs`, `sm`, `md`, `lg`, `xl`) or `default` for a single-representation file.
1115
+ 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
1116
 
1088
1117
  `uploadGeneratedVariants` settles all variant writes. If one fails, it deletes
1089
1118
  every fulfilled sibling key before rethrowing the original failure, so a failed
@@ -1102,13 +1131,35 @@ const { deleted, error } = await deleteOne("images/avatars/abc.webp");
1102
1131
  if (!deleted) console.error("Failed to delete:", error);
1103
1132
  ```
1104
1133
 
1105
- #### `deleteMany(keys, opts?)`
1106
-
1107
- Deletes multiple keys in parallel. Returns a per-key result array.
1108
-
1109
- ```ts
1110
- const results = await deleteMany(variantKeys);
1111
- ```
1134
+ #### `deleteManyKeys(keys, opts?)`
1135
+
1136
+ Deletes multiple keys in parallel. Returns a per-key result array.
1137
+
1138
+ ```ts
1139
+ const results = await deleteManyKeys(variantKeys);
1140
+ ```
1141
+
1142
+ #### `extractObjectVariantKeys(objects)` and `deleteManyObjects(objects, opts?)`
1143
+
1144
+ Stored objects hold sparse variants: an image may have `xs` through `xl`, while
1145
+ a document normally has only `default`. `extractObjectVariantKeys` reads every
1146
+ real variant key and removes duplicates. `deleteManyObjects` is the convenience
1147
+ form that extracts those keys and deletes them.
1148
+
1149
+ ```ts
1150
+ const results = await deleteManyObjects([voucher, previousAvatar]);
1151
+
1152
+ if (results.some((result) => !result.deleted)) {
1153
+ throw new Error("Could not delete every stored representation");
1154
+ }
1155
+ ```
1156
+
1157
+ These functions delete S3 data only; an application owns its database records
1158
+ and domain authorization. When removing a domain record and its objects, run
1159
+ the object delete plus database deletes in its database transaction and abort
1160
+ the transaction on any failed result. S3 is not transactional, so a partial S3
1161
+ failure cannot restore already-deleted siblings; log and reconcile that rare
1162
+ case rather than claiming distributed atomicity.
1112
1163
 
1113
1164
  ### Images
1114
1165
 
@@ -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=A332861D5D3C841F64756E2164756E21
4
+ //# debugId=0D9F302BFC80CAE164756E2164756E21
@@ -1 +1,2 @@
1
1
  export * from "./use-subscription";
2
+ export * from "./use-ws-reconnect";
@@ -1,4 +1,4 @@
1
1
  // @bun
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};
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=1F36C573C33D7C1C64756E2164756E21
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 {};
@@ -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;
@@ -1,7 +1,7 @@
1
1
  // @bun
2
- var N=($)=>$.startsWith("image/"),z=($)=>$.startsWith("video/"),T=($)=>$.startsWith("audio/"),w=($)=>$.startsWith("text/")||$.includes("pdf")||$.includes("word")||$.includes("sheet")||$.includes("presentation")||$.includes("opendocument")||$==="application/rtf",I=($)=>$.includes("zip")||$.includes("rar")||$.includes("tar")||$.includes("7z")||$.includes("gzip"),k=($)=>$==="application/javascript"||$==="application/json"||$==="text/html"||$==="text/css"||$==="application/xml"||$.includes("javascript"),R=($)=>$.startsWith("font/")||$.includes("font"),b=($)=>{if(!$)return"other";let W=$.toLowerCase();if(N(W))return"image";if(z(W))return"video";if(T(W))return"audio";if(w(W))return"document";if(I(W))return"archive";if(k(W))return"code";if(R(W))return"font";return"other"},U={isImage:N,isVideo:z,isAudio:T,isDocument:w,isArchive:I,isCode:k,isFont:R};function e(){let W=Buffer.from("Hello, this is a test file for S3 upload!","utf-8");return new File([W],"test.txt",{type:"text/plain"})}function O($,W){if(!$||$<1||!W||W<1)return"1:1";let H=(Q,Y)=>Y===0?Q:H(Y,Q%Y),J=H($,W);return`${$/J}:${W/J}`}function W0($,W){if(!$||!W||$<1||W<1)return"1:1";let H=$/W,J=[{ratio:1,label:"1:1"},{ratio:0.8,label:"4:5"},{ratio:0.75,label:"3:4"},{ratio:0.6666666666666666,label:"2:3"},{ratio:0.5625,label:"9:16"},{ratio:1.5,label:"3:2"},{ratio:1.3333333333333333,label:"4:3"},{ratio:1.7777777777777777,label:"16:9"},{ratio:2.3333333333333335,label:"21:9"}],Q=0.03;for(let{ratio:X,label:K}of J)if(Math.abs(H-X)/X<Q)return K;let Y=(X,K)=>K===0?X:Y(K,X%K),j=Y($,W);return`${$/j}:${W/j}`}class L extends Error{code;clientCode;httpStatus;data;cause;static STATUS_MAP={BAD_REQUEST:400,UNAUTHORIZED:401,FORBIDDEN:403,NOT_FOUND:404,METHOD_NOT_SUPPORTED:405,TIMEOUT:408,CONFLICT:409,PRECONDITION_FAILED:412,PAYLOAD_TOO_LARGE:413,UNPROCESSABLE_CONTENT:422,TOO_MANY_REQUESTS:429,CLIENT_CLOSED_REQUEST:499,INTERNAL_SERVER_ERROR:500,NOT_IMPLEMENTED:501,BAD_GATEWAY:502,SERVICE_UNAVAILABLE:503,GATEWAY_TIMEOUT:504};constructor($){super($.message);if(this.name="BRPCError",this.code=$.code,this.clientCode=$.clientCode,this.httpStatus=L.STATUS_MAP[$.code],this.data=$.data,this.cause=$.cause,Error.captureStackTrace)Error.captureStackTrace(this,L)}toJSON(){return{name:this.name,code:this.code,clientCode:this.clientCode,message:this.message,data:this.data,httpStatus:this.httpStatus}}static badRequest($,W,H){return new L({code:"BAD_REQUEST",message:$,clientCode:W,data:H})}static unauthorized($="Unauthorized",W,H){return new L({code:"UNAUTHORIZED",message:$,clientCode:W,data:H})}static forbidden($="Forbidden",W,H){return new L({code:"FORBIDDEN",message:$,clientCode:W,data:H})}static notFound($="Not Found",W,H){return new L({code:"NOT_FOUND",message:$,clientCode:W,data:H})}static preconditionFailed($="Precondition failed",W,H){//! Intentionally maps to NOT_FOUND instead of PRECONDITION_FAILED — this was changed to fix a bug
2
+ var T=($)=>$.startsWith("image/"),w=($)=>$.startsWith("video/"),z=($)=>$.startsWith("audio/"),k=($)=>$.startsWith("text/")||$.includes("pdf")||$.includes("word")||$.includes("sheet")||$.includes("presentation")||$.includes("opendocument")||$==="application/rtf",I=($)=>$.includes("zip")||$.includes("rar")||$.includes("tar")||$.includes("7z")||$.includes("gzip"),R=($)=>$==="application/javascript"||$==="application/json"||$==="text/html"||$==="text/css"||$==="application/xml"||$.includes("javascript"),b=($)=>$.startsWith("font/")||$.includes("font"),P=($)=>{if(!$)return"other";let W=$.toLowerCase();if(T(W))return"image";if(w(W))return"video";if(z(W))return"audio";if(k(W))return"document";if(I(W))return"archive";if(R(W))return"code";if(b(W))return"font";return"other"},U={isImage:T,isVideo:w,isAudio:z,isDocument:k,isArchive:I,isCode:R,isFont:b};function $0(){let W=Buffer.from("Hello, this is a test file for S3 upload!","utf-8");return new File([W],"test.txt",{type:"text/plain"})}function q($,W){if(!$||$<1||!W||W<1)return"1:1";let H=(Q,Y)=>Y===0?Q:H(Y,Q%Y),J=H($,W);return`${$/J}:${W/J}`}function H0($,W){if(!$||!W||$<1||W<1)return"1:1";let H=$/W,J=[{ratio:1,label:"1:1"},{ratio:0.8,label:"4:5"},{ratio:0.75,label:"3:4"},{ratio:0.6666666666666666,label:"2:3"},{ratio:0.5625,label:"9:16"},{ratio:1.5,label:"3:2"},{ratio:1.3333333333333333,label:"4:3"},{ratio:1.7777777777777777,label:"16:9"},{ratio:2.3333333333333335,label:"21:9"}],Q=0.03;for(let{ratio:Z,label:_}of J)if(Math.abs(H-Z)/Z<Q)return _;let Y=(Z,_)=>_===0?Z:Y(_,Z%_),L=Y($,W);return`${$/L}:${W/L}`}class X extends Error{code;clientCode;httpStatus;data;cause;static STATUS_MAP={BAD_REQUEST:400,UNAUTHORIZED:401,FORBIDDEN:403,NOT_FOUND:404,METHOD_NOT_SUPPORTED:405,TIMEOUT:408,CONFLICT:409,PRECONDITION_FAILED:412,PAYLOAD_TOO_LARGE:413,UNPROCESSABLE_CONTENT:422,TOO_MANY_REQUESTS:429,CLIENT_CLOSED_REQUEST:499,INTERNAL_SERVER_ERROR:500,NOT_IMPLEMENTED:501,BAD_GATEWAY:502,SERVICE_UNAVAILABLE:503,GATEWAY_TIMEOUT:504};constructor($){super($.message);if(this.name="BRPCError",this.code=$.code,this.clientCode=$.clientCode,this.httpStatus=X.STATUS_MAP[$.code],this.data=$.data,this.cause=$.cause,Error.captureStackTrace)Error.captureStackTrace(this,X)}toJSON(){return{name:this.name,code:this.code,clientCode:this.clientCode,message:this.message,data:this.data,httpStatus:this.httpStatus}}static badRequest($,W,H){return new X({code:"BAD_REQUEST",message:$,clientCode:W,data:H})}static unauthorized($="Unauthorized",W,H){return new X({code:"UNAUTHORIZED",message:$,clientCode:W,data:H})}static forbidden($="Forbidden",W,H){return new X({code:"FORBIDDEN",message:$,clientCode:W,data:H})}static notFound($="Not Found",W,H){return new X({code:"NOT_FOUND",message:$,clientCode:W,data:H})}static preconditionFailed($="Precondition failed",W,H){//! Intentionally maps to NOT_FOUND instead of PRECONDITION_FAILED — this was changed to fix a bug
3
3
  //! but the exact cause is unknown. Suspected: some client/proxy/middleware misbehaved on 412
4
4
  //! (not a browser auto-retry issue — 412 doesn't trigger those). Investigate before changing back.
5
- return new L({code:"NOT_FOUND",message:$,clientCode:W,data:H})}static conflict($,W,H){return new L({code:"CONFLICT",message:$,clientCode:W,data:H})}static unprocessableContent($,W,H){return new L({code:"UNPROCESSABLE_CONTENT",message:$,clientCode:W,data:H})}static tooManyRequests($="Too many requests",W,H){return new L({code:"TOO_MANY_REQUESTS",message:$,clientCode:W,data:H})}static internalServerError($="Internal Server Error",W,H){return new L({code:"INTERNAL_SERVER_ERROR",message:$,clientCode:W,data:H})}static timeout($="Request timeout",W,H){return new L({code:"TIMEOUT",message:$,clientCode:W,data:H})}}import m from"sharp";var v={xs:200,sm:400,md:800,lg:1200,xl:1920},V=1920;async function A($,W,H,J,Q){try{let{data:Y,info:j}=await m($,{limitInputPixels:1e8}).rotate().resize({width:W,height:W,fit:"inside",withoutEnlargement:!0}).webp({quality:H}).toBuffer({resolveWithObject:!0});return{buffer:Y,object:{name:J.replace(/\.[^/.]+$/,".webp"),resolvedType:"image",metadata:{type:"image/webp",size:Y.length,extension:".webp",width:j.width,height:j.height,aspectRatio:j.width&&j.height?j.width/j.height:1,aspectRatioStr:j.width&&j.height?O(j.width,j.height):"1:1",acl:Q}}}}catch(Y){throw L.conflict(`Failed to optimize image: ${Y instanceof Error?Y.message:"Unknown error"}`)}}async function P($){if(!$)throw L.conflict("A file must be provided");let W,H="unknown.webp";if(typeof $==="string"){if(console.log("I'm going to fail here for file",$),!$.startsWith("data:"))throw console.log("I never got here"),L.conflict("Invalid base64 format");let[,J]=$.split(",");if(!J)throw L.conflict("Invalid base64 data");W=Buffer.from(J,"base64")}else{if(!U.isImage($.type))throw L.conflict("File must be an image");let J=await $.arrayBuffer();W=Buffer.from(J),H=$.name??H}return{buffer:W,name:H}}async function K0($,{size:W,quality:H,acl:J}={}){let Q=await P($);return A(Q.buffer,W??V,H??80,Q.name,J??"public-read")}async function M0($,{name:W="unknown.webp",size:H,quality:J,acl:Q}={}){if(!$?.length)throw L.conflict("A buffer must be provided");return A($,H??V,J??80,W,Q??"public-read")}async function E($,{quality:W=80,acl:H="public-read"}={}){let J=await P($),Q=await Promise.all(Object.entries(v).map(async([Y,j])=>{let X=await A(J.buffer,j,W,J.name,H);return[Y,X]}));return{variants:Object.fromEntries(Q)}}import p from"sharp";import{randomUUID as y}from"crypto";import{extname as u}from"path";async function B($,W,H){if(!W)return{data:null,error:Error("File not found")};let J=$.replace(/^\/+|\/+$/g,""),Q=u(W.name),Y=`${y()}${Q}`,j=`${J}/${Y}`,X=`./buckets/${J}/${Y}`;try{let K=await Bun.write(X,W);return{data:{key:j,file:W,bytesWritten:K},error:null}}catch(K){return{data:null,error:Error(`Failed to upload ${W.name} to local filesystem`)}}}async function f($,W,H){if(!W||W.length===0)return[];let J=W.map((Y)=>{let j=H?.(Y)??Y.name;return B($,Y,j)});return await Promise.all(J)}async function d($){let W=Bun.file(`./buckets/${$}`);if(!await W.exists())return null;return W}async function n($){let W=Bun.file(`./buckets/${$}`);if(!await W.exists())return!1;return await W.delete(),!0}var S={uploadOne:B,uploadMany:f,getOne:d,deleteOne:n};async function V0($){let W=`./cache/images/${$.key}_${$.width}_${$.quality}.webp`,H=await S.getOne(W);if(H){if(!U.isImage(H.type))return null;return H}let J=$.key,Q=await S.getOne(J);if(!Q)return null;if(!U.isImage(Q.type))return null;let Y=await Q.arrayBuffer(),j=await p(Y).resize($.width,null,{withoutEnlargement:!0}).webp({quality:$.quality??75}).toBuffer();await Bun.write(W,j);let X=Bun.file(W);if(!await X.exists())return null;return X}import s from"sharp";async function N0($){if(!$?.length)return null;try{let W=await s($).metadata(),H=W.width??0,J=W.height??0,Q=W.height>0?H/J:1;return{type:`image/${W.format}`,size:W.size??0,extension:`.${W.format}`,width:H,height:J,aspectRatio:Q,aspectRatioStr:O(H,J)}}catch(W){return console.warn("Failed to read image metadata:",W),null}}var C=process.env.AWS_FOLDER,g=C?`${C}/`:"";var{s3:r}=globalThis.Bun;import{extname as t}from"path";function c($={}){return $}var{s3:l}=globalThis.Bun;async function o($,W){if(!$||typeof $!=="string")throw L.badRequest("Key is required");let H=$.trim();if(!H||H.length===0)throw L.badRequest("Key must be a non-empty string");try{return await l.delete($),{key:$,deleted:!0,error:null}}catch(J){return{key:$,deleted:!1,error:J}}}async function h($,W){return(await Promise.allSettled($.map((J)=>o(J,W)))).map((J,Q)=>{if(J.status==="fulfilled")return J.value;return{key:$[Q],deleted:!1,error:J.reason}})}async function F($,W,H){let J="buffer"in W?W:{buffer:W,object:{name:W.name,resolvedType:b(W.type),metadata:{type:W.type,extension:t(W.name),size:W.size,acl:H?.acl??"public-read"}}};if("buffer"in W){if(!W.object?.name||!W.object?.metadata)throw L.badRequest("Complete metadata required when uploading buffer")}let Q=$.length>1?`${$.replace(/^\/+|\/+$/g,"")}/`:"",Y=J.object.metadata.extension.startsWith(".")?J.object.metadata.extension:`.${J.object.metadata.extension}`,j=J.object.name?J.object.name:`${crypto.randomUUID()}${Y}`,X=`${g}${Q}${j}`,K=r.file(X),_=J?.object?.metadata?.acl??"public-read",Z=_!=="public-read"&&_!=="public-read-write",x=await K.write(J.buffer,{acl:_,type:J.object.metadata.type});return{uploadedBy:H?.uploadedBy,key:X,name:J.object.name,resolvedType:J.object.resolvedType,provider:"s3",isPrivate:Z,metadata:{...J.object.metadata,size:x,acl:_},isActive:!0}}async function h0($,W,H={throwIf:"any"}){let J=await Promise.allSettled(W.map((j)=>F($,j,{acl:H?.acl,uploadedBy:H?.uploadedBy}))),Q=[],Y=[];if(J.forEach((j)=>{if(j.status==="fulfilled"){if(Q.push(j.value),H?.externalKeys)H.externalKeys.push(j.value.key)}else Y.push(j.reason)}),H?.throwIf==="any"&&Y.length>0||H?.throwIf==="all"&&Y.length===W.length)throw L.internalServerError(`Upload failed: ${Y.length} of ${W.length}`);return{data:Q,errors:Y.length?Y:void 0}}async function i($,W,H){let J=H?.baseId??crypto.randomUUID(),Q=Object.values(W.variants).find(Boolean),Y=await Promise.allSettled(Object.entries(W.variants).map(async([Z,x])=>{if(!x)return[Z,null];let G=x.object.metadata.extension,D=`${J}_${Z}${G}`,q=await F($,{...x,object:{...x.object,name:D}},H);if(!q.metadata)throw L.internalServerError("Upload returned no metadata, cannot build a storage variant");let M=q.metadata;return[Z,{key:q.key,name:D,metadata:{type:M.type,extension:M.extension,size:M.size,hash:M.hash,width:M.width,height:M.height,aspectRatio:M.aspectRatio,aspectRatioStr:M.aspectRatioStr,resolution:M.resolution,bitrate:M.bitrate,codec:M.codec,fps:M.fps}}]})),j=Y.flatMap((Z)=>Z.status==="fulfilled"?[Z.value]:[]),X=Y.flatMap((Z)=>Z.status==="rejected"?[Z.reason]:[]);if(X.length>0){let Z=j.flatMap(([,D])=>D?.key?[D.key]:[]),G=(await h(Z)).filter(({deleted:D})=>!D);if(G.length>0)console.error("Failed to clean up partially uploaded image variants",{keys:Z,failures:G});throw X[0]}let K=(H?.acl??"public-read")!=="public-read"&&(H?.acl??"public-read")!=="public-read-write",_={};for(let[Z,x]of j)if(x)_[Z]=x;return{shared:{name:Q.object.name,resolvedType:Q.object.resolvedType,provider:"s3",acl:H?.acl??"public-read",isPrivate:K,uploadedBy:H?.uploadedBy},variants:_}}async function m0($,W,H){let J=await F($,W,H);if(!J.metadata)throw L.internalServerError("Upload returned no metadata, cannot build a storage object");let Q=J.name??J.key.split("/").pop()??J.key;return{shared:{name:Q,resolvedType:J.resolvedType,provider:J.provider,acl:J.metadata.acl,isPrivate:J.isPrivate,uploadedBy:H?.uploadedBy},variants:c({default:{key:J.key,name:Q,metadata:{type:J.metadata.type,extension:J.metadata.extension,size:J.metadata.size,hash:J.metadata.hash,width:J.metadata.width,height:J.metadata.height,aspectRatio:J.metadata.aspectRatio,aspectRatioStr:J.metadata.aspectRatioStr,resolution:J.metadata.resolution,bitrate:J.metadata.bitrate,codec:J.metadata.codec,fps:J.metadata.fps}}})}}async function y0($,W,H){if(Array.isArray(W))throw Error("File can't be an array, pass a single file");let J=await E(W,{quality:H?.quality,acl:H?.acl});return i($,J,{acl:H?.acl,uploadedBy:H?.uploadedBy})}var{s3:a}=globalThis.Bun;async function d0($,W={debug:!0}){try{return{exists:await a.exists($),error:null}}catch(H){if(W?.debug)console.error("There was an error checking for file existance",H);return{exists:null,error:H}}}export{m0 as uploadSingleObject,F as uploadOne,h0 as uploadMany,i as uploadGeneratedVariants,M0 as optimizeImageBuffer,K0 as optimizeImage,b as mimeTypeToResolvedType,V0 as getOptimizedImage,N0 as getImageMetadata,W0 as getAspectRatioStringRound,O as getAspectRatioString,E as generateVariants,e as generateTestFile,y0 as generateAndUploadImage,U as fileTypePredicates,o as deleteOne,h as deleteMany,c as createUploadVariants,d0 as checkFileExistance};
5
+ return new X({code:"NOT_FOUND",message:$,clientCode:W,data:H})}static conflict($,W,H){return new X({code:"CONFLICT",message:$,clientCode:W,data:H})}static unprocessableContent($,W,H){return new X({code:"UNPROCESSABLE_CONTENT",message:$,clientCode:W,data:H})}static tooManyRequests($="Too many requests",W,H){return new X({code:"TOO_MANY_REQUESTS",message:$,clientCode:W,data:H})}static internalServerError($="Internal Server Error",W,H){return new X({code:"INTERNAL_SERVER_ERROR",message:$,clientCode:W,data:H})}static timeout($="Request timeout",W,H){return new X({code:"TIMEOUT",message:$,clientCode:W,data:H})}}import m from"sharp";var E={xs:200,sm:400,md:800,lg:1200,xl:1920},A=1920;async function S($,W,H,J,Q){try{let{data:Y,info:L}=await m($,{limitInputPixels:1e8}).rotate().resize({width:W,height:W,fit:"inside",withoutEnlargement:!0}).webp({quality:H}).toBuffer({resolveWithObject:!0});return{buffer:Y,object:{name:J.replace(/\.[^/.]+$/,".webp"),resolvedType:"image",metadata:{type:"image/webp",size:Y.length,extension:".webp",width:L.width,height:L.height,aspectRatio:L.width&&L.height?L.width/L.height:1,aspectRatioStr:L.width&&L.height?q(L.width,L.height):"1:1",acl:Q}}}}catch(Y){throw X.conflict(`Failed to optimize image: ${Y instanceof Error?Y.message:"Unknown error"}`)}}async function B($){if(!$)throw X.conflict("A file must be provided");let W,H="unknown.webp";if(typeof $==="string"){if(console.log("I'm going to fail here for file",$),!$.startsWith("data:"))throw console.log("I never got here"),X.conflict("Invalid base64 format");let[,J]=$.split(",");if(!J)throw X.conflict("Invalid base64 data");W=Buffer.from(J,"base64")}else{if(!U.isImage($.type))throw X.conflict("File must be an image");let J=await $.arrayBuffer();W=Buffer.from(J),H=$.name??H}return{buffer:W,name:H}}async function x0($,{size:W,quality:H,acl:J}={}){let Q=await B($);return S(Q.buffer,W??A,H??80,Q.name,J??"public-read")}async function D0($,{name:W="unknown.webp",size:H,quality:J,acl:Q}={}){if(!$?.length)throw X.conflict("A buffer must be provided");return S($,H??A,J??80,W,Q??"public-read")}async function v($,{quality:W=80,acl:H="public-read"}={}){let J=await B($),Q=await Promise.all(Object.entries(E).map(async([Y,L])=>{let Z=await S(J.buffer,L,W,J.name,H);return[Y,Z]}));return{variants:Object.fromEntries(Q)}}import n from"sharp";import{randomUUID as y}from"crypto";import{extname as u}from"path";async function C($,W,H){if(!W)return{data:null,error:Error("File not found")};let J=$.replace(/^\/+|\/+$/g,""),Q=u(W.name),Y=`${y()}${Q}`,L=`${J}/${Y}`,Z=`./buckets/${J}/${Y}`;try{let _=await Bun.write(Z,W);return{data:{key:L,file:W,bytesWritten:_},error:null}}catch(_){return{data:null,error:Error(`Failed to upload ${W.name} to local filesystem`)}}}async function f($,W,H){if(!W||W.length===0)return[];let J=W.map((Y)=>{let L=H?.(Y)??Y.name;return C($,Y,L)});return await Promise.all(J)}async function d($){let W=Bun.file(`./buckets/${$}`);if(!await W.exists())return null;return W}async function p($){let W=Bun.file(`./buckets/${$}`);if(!await W.exists())return!1;return await W.delete(),!0}var V={uploadOne:C,uploadMany:f,getOne:d,deleteOne:p};async function S0($){let W=`./cache/images/${$.key}_${$.width}_${$.quality}.webp`,H=await V.getOne(W);if(H){if(!U.isImage(H.type))return null;return H}let J=$.key,Q=await V.getOne(J);if(!Q)return null;if(!U.isImage(Q.type))return null;let Y=await Q.arrayBuffer(),L=await n(Y).resize($.width,null,{withoutEnlargement:!0}).webp({quality:$.quality??75}).toBuffer();await Bun.write(W,L);let Z=Bun.file(W);if(!await Z.exists())return null;return Z}import s from"sharp";async function T0($){if(!$?.length)return null;try{let W=await s($).metadata(),H=W.width??0,J=W.height??0,Q=W.height>0?H/J:1;return{type:`image/${W.format}`,size:W.size??0,extension:`.${W.format}`,width:H,height:J,aspectRatio:Q,aspectRatioStr:q(H,J)}}catch(W){return console.warn("Failed to read image metadata:",W),null}}var g=process.env.AWS_FOLDER,h=g?`${g}/`:"";var{s3:i}=globalThis.Bun;import{extname as t}from"path";function c($={}){return $}var{s3:l}=globalThis.Bun;function o($){return[...new Set($.flatMap(({variants:W})=>Object.values(W).flatMap((H)=>H&&typeof H==="object"&&("key"in H)&&typeof H.key==="string"&&H.key.length>0?[H.key]:[])))]}async function r($,W){if(!$||typeof $!=="string")throw X.badRequest("Key is required");let H=$.trim();if(!H||H.length===0)throw X.badRequest("Key must be a non-empty string");try{return await l.delete(H),{key:H,deleted:!0,error:null}}catch(J){return{key:$,deleted:!1,error:J}}}async function F($,W){return(await Promise.allSettled($.map((J)=>r(J,W)))).map((J,Q)=>{if(J.status==="fulfilled")return J.value;return{key:$[Q],deleted:!1,error:J.reason}})}async function b0($,W){return F(o($),W)}async function N($,W,H){let J="buffer"in W?W:{buffer:W,object:{name:W.name,resolvedType:P(W.type),metadata:{type:W.type,extension:t(W.name),size:W.size,acl:H?.acl??"public-read"}}};if("buffer"in W){if(!W.object?.name||!W.object?.metadata)throw X.badRequest("Complete metadata required when uploading buffer")}let Q=$.length>1?`${$.replace(/^\/+|\/+$/g,"")}/`:"",Y=J.object.metadata.extension.startsWith(".")?J.object.metadata.extension:`.${J.object.metadata.extension}`,L=J.object.name?J.object.name:`${crypto.randomUUID()}${Y}`,Z=`${h}${Q}${L}`,_=i.file(Z),G=J?.object?.metadata?.acl??"public-read",M=G!=="public-read"&&G!=="public-read-write",D=await _.write(J.buffer,{acl:G,type:J.object.metadata.type});return{uploadedBy:H?.uploadedBy,key:Z,name:J.object.name,resolvedType:J.object.resolvedType,provider:"s3",isPrivate:M,metadata:{...J.object.metadata,size:D,acl:G},isActive:!0}}async function y0($,W,H={throwIf:"any"}){let J=await Promise.allSettled(W.map((L)=>N($,L,{acl:H?.acl,uploadedBy:H?.uploadedBy}))),Q=[],Y=[];if(J.forEach((L)=>{if(L.status==="fulfilled"){if(Q.push(L.value),H?.externalKeys)H.externalKeys.push(L.value.key)}else Y.push(L.reason)}),H?.throwIf==="any"&&Y.length>0||H?.throwIf==="all"&&Y.length===W.length)throw X.internalServerError(`Upload failed: ${Y.length} of ${W.length}`);return{data:Q,errors:Y.length?Y:void 0}}async function a($,W,H){let J=H?.baseId??crypto.randomUUID(),Q=Object.values(W.variants).find(Boolean),Y=await Promise.allSettled(Object.entries(W.variants).map(async([M,D])=>{if(!D)return[M,null];let K=D.object.metadata.extension,j=`${J}_${M}${K}`,O=await N($,{...D,object:{...D.object,name:j}},H);if(!O.metadata)throw X.internalServerError("Upload returned no metadata, cannot build a storage variant");let x=O.metadata;return[M,{key:O.key,name:j,metadata:{type:x.type,extension:x.extension,size:x.size,hash:x.hash,width:x.width,height:x.height,aspectRatio:x.aspectRatio,aspectRatioStr:x.aspectRatioStr,resolution:x.resolution,bitrate:x.bitrate,codec:x.codec,fps:x.fps}}]})),L=Y.flatMap((M)=>M.status==="fulfilled"?[M.value]:[]),Z=Y.flatMap((M)=>M.status==="rejected"?[M.reason]:[]);if(Z.length>0){let M=L.flatMap(([,j])=>j?.key?[j.key]:[]),K=(await F(M)).filter(({deleted:j})=>!j);if(K.length>0)console.error("Failed to clean up partially uploaded image variants",{keys:M,failures:K});throw Z[0]}let _=(H?.acl??"public-read")!=="public-read"&&(H?.acl??"public-read")!=="public-read-write",G={};for(let[M,D]of L)if(D)G[M]=D;return{shared:{name:Q.object.name,resolvedType:Q.object.resolvedType,provider:"s3",acl:H?.acl??"public-read",isPrivate:_,uploadedBy:H?.uploadedBy},variants:G}}async function u0($,W,H){let J=await N($,W,H);if(!J.metadata)throw X.internalServerError("Upload returned no metadata, cannot build a storage object");let Q=J.name??J.key.split("/").pop()??J.key;return{shared:{name:Q,resolvedType:J.resolvedType,provider:J.provider,acl:J.metadata.acl,isPrivate:J.isPrivate,uploadedBy:H?.uploadedBy},variants:c({default:{key:J.key,name:Q,metadata:{type:J.metadata.type,extension:J.metadata.extension,size:J.metadata.size,hash:J.metadata.hash,width:J.metadata.width,height:J.metadata.height,aspectRatio:J.metadata.aspectRatio,aspectRatioStr:J.metadata.aspectRatioStr,resolution:J.metadata.resolution,bitrate:J.metadata.bitrate,codec:J.metadata.codec,fps:J.metadata.fps}}})}}async function f0($,W,H){if(Array.isArray(W))throw Error("File can't be an array, pass a single file");let J=await v(W,{quality:H?.quality,acl:H?.acl});return a($,J,{acl:H?.acl,uploadedBy:H?.uploadedBy})}var{s3:e}=globalThis.Bun;async function n0($,W={debug:!0}){try{return{exists:await e.exists($),error:null}}catch(H){if(W?.debug)console.error("There was an error checking for file existance",H);return{exists:null,error:H}}}export{u0 as uploadSingleObject,N as uploadOne,y0 as uploadMany,a as uploadGeneratedVariants,D0 as optimizeImageBuffer,x0 as optimizeImage,P as mimeTypeToResolvedType,S0 as getOptimizedImage,T0 as getImageMetadata,H0 as getAspectRatioStringRound,q as getAspectRatioString,v as generateVariants,$0 as generateTestFile,f0 as generateAndUploadImage,U as fileTypePredicates,o as extractObjectVariantKeys,r as deleteOne,b0 as deleteManyObjects,F as deleteManyKeys,c as createUploadVariants,n0 as checkFileExistance};
6
6
 
7
- //# debugId=D07C2FC1790C739264756E2164756E21
7
+ //# debugId=BF9212D9C6E0325364756E2164756E21
@@ -1,4 +1,9 @@
1
1
  import type { DeleteOneResult } from "./types";
2
+ type ObjectWithVariants = {
3
+ variants: object;
4
+ };
5
+ /** Returns the unique S3 keys represented by one or more sparse stored objects. */
6
+ export declare function extractObjectVariantKeys<T extends ObjectWithVariants>(objects: T[]): string[];
2
7
  export declare function deleteOne(key: string, opts?: {
3
8
  debug?: boolean;
4
9
  }): Promise<DeleteOneResult>;
@@ -6,6 +11,11 @@ export declare function deleteOne(key: string, opts?: {
6
11
  * Deletes multiple S3 keys in parallel.
7
12
  * Returns per-key result so caller can handle failures/rollback.
8
13
  */
9
- export declare function deleteMany(keys: string[], opts?: {
14
+ export declare function deleteManyKeys(keys: string[], opts?: {
10
15
  debug?: boolean;
11
16
  }): Promise<DeleteOneResult[]>;
17
+ /** Deletes every stored representation of the supplied sparse objects. */
18
+ export declare function deleteManyObjects<T extends ObjectWithVariants>(objects: T[], opts?: {
19
+ debug?: boolean;
20
+ }): Promise<DeleteOneResult[]>;
21
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mateosuarezdev/brpc",
3
- "version": "1.0.106",
3
+ "version": "1.0.108",
4
4
  "description": "A Type-Safe, Flexible Web application framework for Bun",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",