@mateosuarezdev/brpc 1.0.105 → 1.0.107
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -47
- 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/types.d.ts +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,10 +29,9 @@ npm install @mateosuarezdev/brpc zod
|
|
|
29
29
|
- [Schemas](#schemas)
|
|
30
30
|
- [Errors](#errors)
|
|
31
31
|
- [Streaming Media](#streaming-media)
|
|
32
|
-
- [Storage](#storage)
|
|
33
|
-
- [S3](#s3)
|
|
34
|
-
- [Images](#images)
|
|
35
|
-
- [Local](#local)
|
|
32
|
+
- [Storage](#storage)
|
|
33
|
+
- [S3](#s3)
|
|
34
|
+
- [Images](#images)
|
|
36
35
|
- [Auth](#auth)
|
|
37
36
|
- [Server Setup](#server-setup)
|
|
38
37
|
- [Auth Routes](#auth-routes)
|
|
@@ -825,14 +824,20 @@ const billingAccount = useQuery({
|
|
|
825
824
|
|
|
826
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.
|
|
827
826
|
|
|
828
|
-
### Utilities
|
|
829
|
-
|
|
830
|
-
```ts
|
|
831
|
-
// Update WebSocket auth token (e.g. after token refresh)
|
|
832
|
-
await client.utils.updateWsAuth();
|
|
833
|
-
|
|
834
|
-
//
|
|
835
|
-
|
|
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}`);
|
|
836
841
|
await client.utils.setHeaders({ "X-Custom": "value" });
|
|
837
842
|
```
|
|
838
843
|
|
|
@@ -859,7 +864,7 @@ try {
|
|
|
859
864
|
|
|
860
865
|
### React
|
|
861
866
|
|
|
862
|
-
Create one subscription service, then bind the React hook once:
|
|
867
|
+
Create one subscription service, then bind the React hook once:
|
|
863
868
|
|
|
864
869
|
```ts
|
|
865
870
|
import { SubscriptionService } from "@mateosuarezdev/brpc/client";
|
|
@@ -886,7 +891,28 @@ function ChatRoom({ roomId }: { roomId: string }) {
|
|
|
886
891
|
</button>
|
|
887
892
|
);
|
|
888
893
|
}
|
|
889
|
-
```
|
|
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.
|
|
890
916
|
|
|
891
917
|
---
|
|
892
918
|
|
|
@@ -1004,14 +1030,20 @@ import {
|
|
|
1004
1030
|
uploadGeneratedVariants,
|
|
1005
1031
|
deleteOne,
|
|
1006
1032
|
deleteMany,
|
|
1007
|
-
generateVariants,
|
|
1008
|
-
getOptimizedImage,
|
|
1009
|
-
getImageMetadata,
|
|
1010
|
-
|
|
1011
|
-
} from "@mateosuarezdev/brpc";
|
|
1033
|
+
generateVariants,
|
|
1034
|
+
getOptimizedImage,
|
|
1035
|
+
getImageMetadata,
|
|
1036
|
+
} from "@mateosuarezdev/brpc";
|
|
1012
1037
|
```
|
|
1013
1038
|
|
|
1014
|
-
Requires Bun's S3 client to be configured via environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION`, `AWS_ENDPOINT`, `AWS_BUCKET`).
|
|
1039
|
+
Requires Bun's S3 client to be configured via environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION`, `AWS_ENDPOINT`, `AWS_BUCKET`).
|
|
1040
|
+
|
|
1041
|
+
S3 is the only persisted-object provider. Every stored upload has a non-empty
|
|
1042
|
+
`variants` object: `default` for a single representation, or named image sizes
|
|
1043
|
+
from `xs` through `xl`. A variant always has its S3 `key`, stored `name`, and
|
|
1044
|
+
typed metadata. Keys are canonical: derive public URLs from the configured
|
|
1045
|
+
endpoint and authorize/presign private URLs in the consuming application's
|
|
1046
|
+
domain route; do not store either URL on a variant.
|
|
1015
1047
|
|
|
1016
1048
|
### S3
|
|
1017
1049
|
|
|
@@ -1056,7 +1088,7 @@ const upload = await uploadSingleObject("/payouts", pdfFile, {
|
|
|
1056
1088
|
});
|
|
1057
1089
|
|
|
1058
1090
|
// upload.shared — name, resolvedType ("document" for a PDF), acl, isPrivate
|
|
1059
|
-
// upload.variants.default — key,
|
|
1091
|
+
// upload.variants.default — key, name, metadata
|
|
1060
1092
|
```
|
|
1061
1093
|
|
|
1062
1094
|
Use this rather than `uploadOne` when the result is going into storage. `uploadOne` returns a `RawUploadResult`, which is what S3 returned; `UploadResult` is what an application stores, and this performs the conversion between them.
|
|
@@ -1075,10 +1107,14 @@ const uploadResult = await generateAndUploadImage("/exercises", imageFile, {
|
|
|
1075
1107
|
});
|
|
1076
1108
|
|
|
1077
1109
|
// uploadResult.shared — name, resolvedType, provider, acl, isPrivate
|
|
1078
|
-
// uploadResult.variants — keyed object with each variant's key,
|
|
1110
|
+
// uploadResult.variants — keyed object with each variant's key, name, metadata
|
|
1079
1111
|
```
|
|
1080
1112
|
|
|
1081
|
-
Variants are keyed by `VariantName`, which is an image size (`xs`, `sm`, `md`, `lg`, `xl`) or `default` for a single-representation file.
|
|
1113
|
+
Variants are keyed by `VariantName`, which is an image size (`xs`, `sm`, `md`, `lg`, `xl`) or `default` for a single-representation file. An upload result is sparse: it contains only variants it actually wrote.
|
|
1114
|
+
|
|
1115
|
+
`uploadGeneratedVariants` settles all variant writes. If one fails, it deletes
|
|
1116
|
+
every fulfilled sibling key before rethrowing the original failure, so a failed
|
|
1117
|
+
generated upload does not leave partial S3 objects behind.
|
|
1082
1118
|
|
|
1083
1119
|
#### `uploadGeneratedVariants(path, generated, opts?)`
|
|
1084
1120
|
|
|
@@ -1140,30 +1176,7 @@ if (!file) throw BRPCError.notFound();
|
|
|
1140
1176
|
return file;
|
|
1141
1177
|
```
|
|
1142
1178
|
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
The `local` object provides a filesystem storage backend that mirrors the S3 API. Files are stored under `./buckets/`.
|
|
1146
|
-
|
|
1147
|
-
```ts
|
|
1148
|
-
// Upload
|
|
1149
|
-
const { data, error } = await local.uploadOne("public/avatars", file);
|
|
1150
|
-
if (error) throw BRPCError.internalServerError();
|
|
1151
|
-
// data.key, data.bytesWritten
|
|
1152
|
-
|
|
1153
|
-
// Upload many
|
|
1154
|
-
const results = await local.uploadMany("public/photos", files);
|
|
1155
|
-
|
|
1156
|
-
// Get
|
|
1157
|
-
const file = await local.getOne("public/avatars/abc.jpg");
|
|
1158
|
-
if (!file) throw BRPCError.notFound();
|
|
1159
|
-
|
|
1160
|
-
// Delete
|
|
1161
|
-
const deleted = await local.deleteOne("public/avatars/abc.jpg");
|
|
1162
|
-
```
|
|
1163
|
-
|
|
1164
|
-
---
|
|
1165
|
-
|
|
1166
|
-
## Auth
|
|
1179
|
+
## Auth
|
|
1167
1180
|
|
|
1168
1181
|
Built-in auth service for SurrealDB. Supports email/phone sign-up and sign-in with password or OTP (passwordless), JWT sessions with refresh tokens, and cookie management.
|
|
1169
1182
|
|
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 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
|
|
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
|
|
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};
|
|
6
6
|
|
|
7
|
-
//# debugId=
|
|
7
|
+
//# debugId=D07C2FC1790C739264756E2164756E21
|
|
@@ -22,8 +22,8 @@ type DeleteOneResult = DeleteOneNiceResult | DeleteOneBadResult;
|
|
|
22
22
|
* surrender that everywhere to accommodate one case.
|
|
23
23
|
*/
|
|
24
24
|
export type VariantName = ImageVariantName | "default";
|
|
25
|
-
export type UploadVariants = Record<VariantName, StorageObjectVariant
|
|
26
|
-
/** Builds the
|
|
25
|
+
export type UploadVariants = Partial<Record<VariantName, StorageObjectVariant>>;
|
|
26
|
+
/** Builds the sparse stored-object variant shape from uploaded representations. */
|
|
27
27
|
export declare function createUploadVariants(values?: Partial<Record<VariantName, StorageObjectVariant>>): UploadVariants;
|
|
28
28
|
export type UploadResult = {
|
|
29
29
|
shared: {
|