@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 +74 -23
- 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/dist/storage/index.js +3 -3
- package/dist/storage/s3/delete.d.ts +11 -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
|
|
|
@@ -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
|
-
|
|
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
|
-
#### `
|
|
1106
|
-
|
|
1107
|
-
Deletes multiple keys in parallel. Returns a per-key result array.
|
|
1108
|
-
|
|
1109
|
-
```ts
|
|
1110
|
-
const results = await
|
|
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
|
|
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;
|
package/dist/storage/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var
|
|
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
|
|
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=
|
|
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
|
|
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 {};
|