added backup to queue
This commit is contained in:
@@ -27,5 +27,10 @@ export default async function DemoPage({ params }) {
|
||||
);
|
||||
}
|
||||
|
||||
return <DynamicDemoView />;
|
||||
return (
|
||||
<DynamicDemoView
|
||||
publishableKey={data.publishable_key}
|
||||
secretKey={data.secret_key}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -43,6 +43,16 @@ export default async function RootLayout({
|
||||
const { sessionClaims }: { sessionClaims: any } = await auth();
|
||||
const { org_id, org } = sessionClaims || {};
|
||||
|
||||
if (path.includes("/demo")) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<PostHogProvider>
|
||||
<body suppressHydrationWarning>{children}</body>
|
||||
</PostHogProvider>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body
|
||||
@@ -62,6 +72,7 @@ export default async function RootLayout({
|
||||
env={env as AppEnv}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* <AutumnProvider
|
||||
publishableKey={
|
||||
process.env.NEXT_PUBLIC_AUTUMN_PUBLISHABLE_KEY || ""
|
||||
|
||||
@@ -63,10 +63,14 @@ export function useAxiosPostSWR({
|
||||
});
|
||||
}
|
||||
|
||||
export function useDemoSWR({ url, publishableKey, options = {} }) {
|
||||
export function useDemoSWR({
|
||||
url,
|
||||
publishableKey,
|
||||
options = {},
|
||||
endpoint = "https://api.useautumn.com",
|
||||
}) {
|
||||
const axiosInstance = axios.create({
|
||||
baseURL: "https://api.useautumn.com",
|
||||
// baseURL: "http://localhost:8080",
|
||||
baseURL: endpoint,
|
||||
headers: {
|
||||
"x-publishable-key": publishableKey,
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@ function CustomerBalances({ customer }: { customer: any }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
<div className="flex gap-4 flex-wrap">
|
||||
{customer.entitlements.map((entitlement) => (
|
||||
<div
|
||||
key={entitlement.feature_id}
|
||||
|
||||
53
frontend/src/views/demo/DemoSidebar.tsx
Normal file
53
frontend/src/views/demo/DemoSidebar.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
function SidebarItem({
|
||||
icon,
|
||||
text,
|
||||
active = false,
|
||||
}: {
|
||||
icon: string;
|
||||
text: string;
|
||||
active?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-md cursor-pointer hover:bg-gray-100 ${
|
||||
active ? "bg-gray-100" : ""
|
||||
}`}
|
||||
>
|
||||
<span>{icon}</span>
|
||||
<span className="text-sm text-t2">{text}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DemoSidebar() {
|
||||
return (
|
||||
<div className="w-[250px] bg-stone-100 border-r flex flex-col h-screen">
|
||||
<div className="p-4 flex items-center gap-2 border-b">
|
||||
<div className="w-6 h-6 rounded-full bg-gray-900"></div>
|
||||
<span className="font-medium">autumn</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col h-full p-2 space-y-1">
|
||||
<SidebarItem icon="📊" text="Overview" active />
|
||||
<SidebarItem icon="📝" text="Editor" />
|
||||
<SidebarItem icon="📈" text="Analytics" />
|
||||
<SidebarItem icon="⚙️" text="Settings" />
|
||||
|
||||
<div className="flex flex-col h-full justify-between">
|
||||
<div>
|
||||
<div className="text-xs text-gray-500 px-3 pt-4 pb-2">Products</div>
|
||||
<SidebarItem icon="💬" text="Chat" />
|
||||
<SidebarItem icon="🤖" text="Assistant" />
|
||||
<SidebarItem icon="🔒" text="Authentication" />
|
||||
<SidebarItem icon="🧩" text="Add-ons" />
|
||||
</div>
|
||||
<div className="">
|
||||
<SidebarItem icon="📚" text="Documentation" />
|
||||
<SidebarItem icon="👥" text="Invite" />
|
||||
<SidebarItem icon="❓" text="Support" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -11,33 +11,41 @@ import { AutumnProvider, PricingPage } from "@useautumn/react";
|
||||
import { useDemoSWR } from "@/services/useAxiosSwr";
|
||||
import CustomerBalances from "./CustomerBalances";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { APIPlayground } from "./APIPlayground";
|
||||
import Image from "next/image";
|
||||
import { checkAccess, createAxiosInstance, sendUsage } from "./autumnBackend";
|
||||
import DemoSidebar from "./DemoSidebar";
|
||||
|
||||
const colorizeJSON = (json: any) => {
|
||||
const jsonString = JSON.stringify(json, null, 2);
|
||||
return jsonString?.replace(/\btrue\b|\bfalse\b/g, (match) =>
|
||||
match === "true"
|
||||
? `<span class="text-lime-500">true</span>`
|
||||
: `<span class="text-red-400">false</span>`
|
||||
);
|
||||
// const endpoint = "https://api.useautumn.com";
|
||||
// const endpoint = "http://localhost:8080";
|
||||
const endpoint = process.env.NEXT_PUBLIC_BACKEND_URL;
|
||||
|
||||
const data = {
|
||||
companyName: "Keywords",
|
||||
customerId: "hahnbee",
|
||||
};
|
||||
|
||||
export default function DemoView() {
|
||||
const customerId = "hahnbee";
|
||||
const eventName = "chat-responses";
|
||||
const buttons = [
|
||||
{
|
||||
feature_id: "log-ingestion",
|
||||
text: "Ingest Logs",
|
||||
value: 100,
|
||||
},
|
||||
];
|
||||
|
||||
const baseUrl = "http://localhost:8080";
|
||||
const apiKey = process.env.NEXT_PUBLIC_AUTUMN_API_KEY;
|
||||
export default function DynamicDemo({
|
||||
publishableKey,
|
||||
secretKey,
|
||||
}: {
|
||||
publishableKey: string;
|
||||
secretKey: string;
|
||||
}) {
|
||||
const [eventName, setEventName] = useState("chat-responses");
|
||||
// const customerId = "hahnbee";
|
||||
|
||||
const headers = {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
};
|
||||
const { companyName, customerId } = data;
|
||||
|
||||
const axiosInstance = axios.create({
|
||||
baseURL: baseUrl,
|
||||
headers,
|
||||
});
|
||||
|
||||
const publishableKey = process.env.NEXT_PUBLIC_AUTUMN_PUBLISHABLE_KEY;
|
||||
const axiosInstance = createAxiosInstance(secretKey, endpoint!);
|
||||
|
||||
const {
|
||||
data: customer,
|
||||
@@ -47,6 +55,7 @@ export default function DemoView() {
|
||||
} = useDemoSWR({
|
||||
url: `/public/customers/${customerId}`,
|
||||
publishableKey: publishableKey || "",
|
||||
endpoint: endpoint,
|
||||
});
|
||||
|
||||
const hasAccessRequest = {
|
||||
@@ -54,7 +63,7 @@ export default function DemoView() {
|
||||
feature_id: eventName,
|
||||
};
|
||||
|
||||
const sendEventRequest = {
|
||||
const sendEventRequestChat = {
|
||||
customer_id: customerId,
|
||||
event_name: eventName,
|
||||
};
|
||||
@@ -65,234 +74,126 @@ export default function DemoView() {
|
||||
|
||||
const [hasAccessLoading, setHasAccessLoading] = useState(false);
|
||||
const [hasAccessResponse, setHasAccessResponse] = useState(null);
|
||||
const [sendEventRequest, setSendEventRequest] =
|
||||
useState(sendEventRequestChat);
|
||||
const [getCustomerResponse, setGetCustomerResponse] = useState(null);
|
||||
const [sendEventLoading, setSendEventLoading] = useState(false);
|
||||
const [sendEventResponse, setSendEventResponse] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [message, setMessage] = useState("");
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
setLoading(true);
|
||||
// const hasProModels = await checkPremiumModels();
|
||||
// setHasProModels(hasProModels);
|
||||
setLoading(false);
|
||||
};
|
||||
init();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setGetCustomerResponse(customer?.entitlements);
|
||||
}, [customer]);
|
||||
|
||||
//Check access to Pro features and email balance
|
||||
const checkAccess = async (featureId: string) => {
|
||||
const { data } = await axiosInstance.post("/entitled", {
|
||||
customer_id: customerId,
|
||||
feature_id: featureId,
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
//Send usage event for email
|
||||
const sendUsage = async (featureId: string) => {
|
||||
const { data } = await axiosInstance.post("/events", {
|
||||
customer_id: customerId,
|
||||
event_name: featureId,
|
||||
properties: {
|
||||
value: 1,
|
||||
},
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const handleClicked = async () => {
|
||||
const handleBtnClicked = async ({
|
||||
featureId,
|
||||
value,
|
||||
}: {
|
||||
featureId: string;
|
||||
value: number;
|
||||
}) => {
|
||||
setHasAccessLoading(true);
|
||||
const data = await checkAccess(eventName);
|
||||
const data = await checkAccess({
|
||||
axiosInstance,
|
||||
customerId,
|
||||
featureId,
|
||||
});
|
||||
setHasAccessResponse(data);
|
||||
setHasAccessLoading(false);
|
||||
|
||||
if (!data.allowed) {
|
||||
toast.error("You're out of " + eventName);
|
||||
toast.error("You're out of " + featureId);
|
||||
return;
|
||||
}
|
||||
|
||||
setSendEventLoading(true);
|
||||
const eventData = await sendUsage(eventName);
|
||||
const eventData = await sendUsage({
|
||||
axiosInstance,
|
||||
customerId,
|
||||
featureId,
|
||||
value,
|
||||
});
|
||||
setSendEventResponse(eventData);
|
||||
setSendEventLoading(false);
|
||||
await cusMutate();
|
||||
};
|
||||
|
||||
return (
|
||||
<AutumnProvider publishableKey={publishableKey || ""} endpoint={baseUrl}>
|
||||
<div className="w-full h-fit bg-white flex justify-start absolute top-0 left-0">
|
||||
<div className="flex p-4 w-full gap-32 relative">
|
||||
<div className="flex flex-col gap-4 min-w-[700px]">
|
||||
{loading ? (
|
||||
<div className="flex justify-center items-center h-[500px]">
|
||||
<LoaderCircle className="animate-spin text-primary" size={30} />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<CustomToaster />
|
||||
<div className="text-xl font-extrabold mt-2 -mb-2">
|
||||
Mintlify
|
||||
</div>
|
||||
<p className="text-lg">
|
||||
Start building modern documentation in under five minutes
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="Search or ask..."
|
||||
className="w-full"
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
isLoading={hasAccessLoading}
|
||||
onClick={async () => handleClicked()}
|
||||
className="font-semibold bg-gradient-to-b from-emerald-400 to-emerald-500 hover:bg-gradient-to-b hover:from-emerald-500 hover:to-emerald-600 border-t border-emerald-500"
|
||||
>
|
||||
Use AI Chat
|
||||
</Button>
|
||||
<Button
|
||||
variant="gradientSecondary"
|
||||
onClick={async () => {
|
||||
const { data } = await axiosInstance.post("/entitled", {
|
||||
customer_id: customerId,
|
||||
feature_id: "editors",
|
||||
event_data: {
|
||||
event_name: "editors",
|
||||
properties: {
|
||||
value: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
console.log(data);
|
||||
if (!data.allowed) {
|
||||
toast.error("You're out of editors");
|
||||
}
|
||||
await cusMutate();
|
||||
}}
|
||||
>
|
||||
Add Editor
|
||||
</Button>
|
||||
<Button
|
||||
variant="gradientSecondary"
|
||||
onClick={async () => {
|
||||
const { data } = await axiosInstance.post("/entitled", {
|
||||
customer_id: customerId,
|
||||
feature_id: "editors",
|
||||
required_quantity: -1,
|
||||
event_data: {
|
||||
event_name: "editors",
|
||||
properties: {
|
||||
value: -1,
|
||||
},
|
||||
},
|
||||
});
|
||||
console.log(data);
|
||||
await cusMutate();
|
||||
}}
|
||||
>
|
||||
Remove Editor
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-lg font-semibold mt-4 -mb-3">Account</div>
|
||||
<p className="text-sm text-t3">
|
||||
Hi {customer?.name ? customer?.name : customerId}, you have
|
||||
access to:
|
||||
</p>
|
||||
<CustomerBalances customer={customer} />
|
||||
<div className="text-lg font-semibold mt-2">Pricing</div>
|
||||
|
||||
<PricingPage customerId={customerId} />
|
||||
|
||||
<p className="text-xs text-t3">
|
||||
You can make a test purchase to see what happens. Use the
|
||||
Stripe test card{" "}
|
||||
<span className="font-bold">4242 4242 4242 4242</span> with
|
||||
any expiration date, CVC and cardholder details.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
<AutumnProvider publishableKey={publishableKey || ""} endpoint={endpoint}>
|
||||
<div className="w-screen h-screen flex justify-between overflow-x-hidden">
|
||||
<DemoSidebar />
|
||||
<div className="flex flex-col gap-4 flex-2 w-full px-10 pt-4">
|
||||
<CustomToaster />
|
||||
<div className="flex gap-4 items-center">
|
||||
<div className="text-xl font-extrabold">{companyName}</div>
|
||||
</div>
|
||||
<div className="w-2/4 space-y-4 flex flex-col gap-8 w-[500px] bg-gray-900 p-4 rounded-sm">
|
||||
<APIPlayground
|
||||
title="Check Feature Access"
|
||||
endpoint="GET /entitled"
|
||||
request={hasAccessRequest}
|
||||
response={hasAccessResponse}
|
||||
loading={hasAccessLoading}
|
||||
/>
|
||||
<APIPlayground
|
||||
title="Send Usage Event"
|
||||
endpoint="POST /events"
|
||||
request={sendEventRequest}
|
||||
response={sendEventResponse}
|
||||
loading={sendEventLoading}
|
||||
/>
|
||||
<APIPlayground
|
||||
title="Get Customer"
|
||||
endpoint="GET /customers/:customer_id"
|
||||
request={getCustomerRequest}
|
||||
response={getCustomerResponse}
|
||||
loading={customerLoading}
|
||||
/>
|
||||
|
||||
<div className="flex gap-2">
|
||||
{buttons.map((button, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
isLoading={hasAccessLoading}
|
||||
onClick={async () =>
|
||||
handleBtnClicked({
|
||||
featureId: button.feature_id,
|
||||
value: button.value,
|
||||
})
|
||||
}
|
||||
variant="gradientPrimary"
|
||||
>
|
||||
{button.text}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="text-lg font-medium mt-4 -mb-3">Billing</div>
|
||||
<p className="text-sm text-t3">
|
||||
{customer?.name ? customer?.name : customerId}, you have access to:
|
||||
</p>
|
||||
<CustomerBalances customer={customer} />
|
||||
<div className="text-lg font-medium mt-2">Pricing</div>
|
||||
|
||||
<div className="min-w-[600px]">
|
||||
<PricingPage customerId={customerId} />
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-t3">
|
||||
Make a test purchase to see how Autumn handles it. Use the Stripe
|
||||
test card <span className="font-bold">4242 4242 4242 4242</span>{" "}
|
||||
with any expiration date, CVC and cardholder details.
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-[600px] space-y-4 flex flex-col bg-gray-900 p-4 h-full min-h-fit">
|
||||
<Image
|
||||
src="/demo-assets/autumn-logo.png"
|
||||
alt="Autumn Logo"
|
||||
width={100}
|
||||
height={100}
|
||||
/>
|
||||
<APIPlayground
|
||||
title="Check Feature Access"
|
||||
endpoint="GET /entitled"
|
||||
request={hasAccessRequest}
|
||||
response={hasAccessResponse}
|
||||
loading={hasAccessLoading}
|
||||
/>
|
||||
<APIPlayground
|
||||
title="Send Usage Event"
|
||||
endpoint="POST /events"
|
||||
request={sendEventRequest}
|
||||
response={sendEventResponse}
|
||||
loading={sendEventLoading}
|
||||
/>
|
||||
{/* <APIPlayground
|
||||
title="Get Customer"
|
||||
endpoint="GET /customers/:customer_id"
|
||||
request={getCustomerRequest}
|
||||
response={getCustomerResponse}
|
||||
loading={customerLoading}
|
||||
/> */}
|
||||
</div>
|
||||
</div>
|
||||
</AutumnProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const APIPlayground = ({
|
||||
title,
|
||||
endpoint,
|
||||
request,
|
||||
response,
|
||||
loading,
|
||||
}: {
|
||||
title: string;
|
||||
endpoint: string;
|
||||
request: any;
|
||||
response: any;
|
||||
loading: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 bg-gray-900 p-4 rounded-sm">
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-md font-semibold text-white">{title}</p>
|
||||
<pre className="bg-gray-600 p-2 rounded text-sm text-gray-200">
|
||||
{endpoint}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-sm text-gray-400">Request</p>
|
||||
<pre className="bg-gray-600 p-2 rounded text-sm text-gray-200">
|
||||
{JSON.stringify(request, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-sm text-gray-400">Response</p>
|
||||
<pre className="bg-gray-600 p-2 rounded text-sm text-gray-200">
|
||||
{loading ? (
|
||||
<SmallSpinner />
|
||||
) : response === null ? (
|
||||
"No response"
|
||||
) : (
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: colorizeJSON(response),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
49
frontend/src/views/demo/autumnBackend.tsx
Normal file
49
frontend/src/views/demo/autumnBackend.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import axios, { AxiosInstance } from "axios";
|
||||
|
||||
export const createAxiosInstance = (secretKey: string, endpoint: string) => {
|
||||
return axios.create({
|
||||
baseURL: endpoint + "/v1",
|
||||
headers: {
|
||||
Authorization: `Bearer ${secretKey}`,
|
||||
},
|
||||
});
|
||||
};
|
||||
//Check access to Pro features and email balance
|
||||
export const checkAccess = async ({
|
||||
axiosInstance,
|
||||
customerId,
|
||||
featureId,
|
||||
}: {
|
||||
axiosInstance: AxiosInstance;
|
||||
customerId: string;
|
||||
featureId: string;
|
||||
}) => {
|
||||
const { data } = await axiosInstance.post("/entitled", {
|
||||
customer_id: customerId,
|
||||
feature_id: featureId,
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
//Send usage event for
|
||||
export const sendUsage = async ({
|
||||
axiosInstance,
|
||||
customerId,
|
||||
featureId,
|
||||
value,
|
||||
}: {
|
||||
axiosInstance: AxiosInstance;
|
||||
customerId: string;
|
||||
featureId: string;
|
||||
value: number;
|
||||
}) => {
|
||||
const { data } = await axiosInstance.post("/events", {
|
||||
customer_id: customerId,
|
||||
event_name: featureId,
|
||||
properties: {
|
||||
value: value,
|
||||
},
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
||||
184
package-lock.json
generated
184
package-lock.json
generated
@@ -9307,6 +9307,12 @@
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@ioredis/commands": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz",
|
||||
"integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
@@ -11112,9 +11118,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.0.tgz",
|
||||
"integrity": "sha512-ClIbNe36lawluuvq3+YYhnIN2CELi+6q8NpnM7PYp4hBn/TatfboPgVSm2rwKRfnV2M+Ty9GWDFI64KEe+kysA==",
|
||||
"version": "22.13.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.4.tgz",
|
||||
"integrity": "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.20.0"
|
||||
@@ -12035,6 +12041,15 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/cluster-key-slot": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
|
||||
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.0.tgz",
|
||||
@@ -12792,6 +12807,15 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/denque": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/dequal": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||
@@ -14278,6 +14302,30 @@
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/ioredis": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.5.0.tgz",
|
||||
"integrity": "sha512-7CutT89g23FfSa8MDoIFs2GYYa0PaNiW/OrT+nRyjRXHDZd17HmIgy+reOQ/yhh72NznNjGuS8kbCAcA4Ro4mw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ioredis/commands": "^1.1.1",
|
||||
"cluster-key-slot": "^1.1.0",
|
||||
"debug": "^4.3.4",
|
||||
"denque": "^2.1.0",
|
||||
"lodash.defaults": "^4.2.0",
|
||||
"lodash.isarguments": "^3.1.0",
|
||||
"redis-errors": "^1.2.0",
|
||||
"redis-parser": "^3.0.0",
|
||||
"standard-as-callback": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.22.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/ioredis"
|
||||
}
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
|
||||
@@ -15113,6 +15161,18 @@
|
||||
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.defaults": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
||||
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isarguments": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
|
||||
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.merge": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
@@ -17713,6 +17773,27 @@
|
||||
"node": ">= 12.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/redis-errors": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
|
||||
"integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/redis-parser": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
|
||||
"integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"redis-errors": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/refractor": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz",
|
||||
@@ -18437,6 +18518,12 @@
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/standard-as-callback": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
|
||||
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/std-env": {
|
||||
"version": "3.8.0",
|
||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz",
|
||||
@@ -19592,6 +19679,7 @@
|
||||
"express": "^4.21.1",
|
||||
"http-status-codes": "^2.3.0",
|
||||
"inngest": "^3.30.0",
|
||||
"ioredis": "^5.5.0",
|
||||
"ksuid": "^3.0.0",
|
||||
"nodemon": "^3.1.7",
|
||||
"openai": "^4.85.2",
|
||||
@@ -19611,7 +19699,7 @@
|
||||
"@types/chai": "^5.0.1",
|
||||
"@types/chai-http": "^3.0.5",
|
||||
"@types/mocha": "^10.0.10",
|
||||
"@types/node": "^22.13.0",
|
||||
"@types/node": "^22.13.4",
|
||||
"@types/pg": "^8.11.10",
|
||||
"mocha": "^11.1.0",
|
||||
"puppeteer": "^24.2.0",
|
||||
@@ -19873,10 +19961,6 @@
|
||||
"@types/node": ">=18"
|
||||
}
|
||||
},
|
||||
"server/node_modules/@ioredis/commands": {
|
||||
"version": "1.2.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"server/node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
|
||||
"version": "3.0.3",
|
||||
"cpu": [
|
||||
@@ -20338,13 +20422,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"server/node_modules/cluster-key-slot": {
|
||||
"version": "1.1.2",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"server/node_modules/commander": {
|
||||
"version": "12.1.0",
|
||||
"license": "MIT",
|
||||
@@ -20412,13 +20489,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"server/node_modules/denque": {
|
||||
"version": "2.1.0",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"server/node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"license": "MIT",
|
||||
@@ -20711,47 +20781,6 @@
|
||||
"@types/node": ">=18"
|
||||
}
|
||||
},
|
||||
"server/node_modules/ioredis": {
|
||||
"version": "5.4.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ioredis/commands": "^1.1.1",
|
||||
"cluster-key-slot": "^1.1.0",
|
||||
"debug": "^4.3.4",
|
||||
"denque": "^2.1.0",
|
||||
"lodash.defaults": "^4.2.0",
|
||||
"lodash.isarguments": "^3.1.0",
|
||||
"redis-errors": "^1.2.0",
|
||||
"redis-parser": "^3.0.0",
|
||||
"standard-as-callback": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.22.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/ioredis"
|
||||
}
|
||||
},
|
||||
"server/node_modules/ioredis/node_modules/debug": {
|
||||
"version": "4.4.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"server/node_modules/ioredis/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"license": "MIT"
|
||||
},
|
||||
"server/node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"license": "MIT",
|
||||
@@ -20803,14 +20832,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"server/node_modules/lodash.defaults": {
|
||||
"version": "4.2.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"server/node_modules/lodash.isarguments": {
|
||||
"version": "3.1.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"server/node_modules/lodash.isstring": {
|
||||
"version": "4.0.1",
|
||||
"license": "MIT"
|
||||
@@ -21309,23 +21330,6 @@
|
||||
"recase": "dist/cli.js"
|
||||
}
|
||||
},
|
||||
"server/node_modules/redis-errors": {
|
||||
"version": "1.2.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"server/node_modules/redis-parser": {
|
||||
"version": "3.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"redis-errors": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"server/node_modules/require-main-filename": {
|
||||
"version": "2.0.0",
|
||||
"license": "ISC"
|
||||
@@ -21469,10 +21473,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"server/node_modules/standard-as-callback": {
|
||||
"version": "2.1.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"server/node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "nodemon --no-deprecation --exec tsx src/index.ts",
|
||||
"dev": "nodemon --no-deprecation --exec tsx src/index.ts --ignore scripts",
|
||||
"start": "tsx src/index.ts",
|
||||
"queue:dev": "tsx watch src/queue.ts",
|
||||
"build": "tsc -b",
|
||||
@@ -51,6 +51,7 @@
|
||||
"express": "^4.21.1",
|
||||
"http-status-codes": "^2.3.0",
|
||||
"inngest": "^3.30.0",
|
||||
"ioredis": "^5.5.0",
|
||||
"ksuid": "^3.0.0",
|
||||
"nodemon": "^3.1.7",
|
||||
"openai": "^4.85.2",
|
||||
@@ -70,7 +71,7 @@
|
||||
"@types/chai": "^5.0.1",
|
||||
"@types/chai-http": "^3.0.5",
|
||||
"@types/mocha": "^10.0.10",
|
||||
"@types/node": "^22.13.0",
|
||||
"@types/node": "^22.13.4",
|
||||
"@types/pg": "^8.11.10",
|
||||
"mocha": "^11.1.0",
|
||||
"puppeteer": "^24.2.0",
|
||||
|
||||
@@ -31,7 +31,8 @@ export const initLogger = () => {
|
||||
},
|
||||
});
|
||||
|
||||
logger.info("Logger initialized");
|
||||
// logger.info("Logger initialized");
|
||||
console.log("Logger initialized");
|
||||
|
||||
return logger;
|
||||
};
|
||||
|
||||
@@ -10,28 +10,27 @@ import webhooksRouter from "./external/webhooks/webhooksRouter.js";
|
||||
import { envMiddleware } from "./middleware/envMiddleware.js";
|
||||
import pg from "pg";
|
||||
|
||||
import { initQueue, initWorkers } from "./queue/queue.js";
|
||||
import { initWorkers } from "./queue/queue.js";
|
||||
import http from "http";
|
||||
import { initWs } from "./websockets/initWs.js";
|
||||
import { publicRouter } from "./internal/public/publicRouter.js";
|
||||
import { initLogger } from "./errors/logger.js";
|
||||
import RecaseError, { handleRequestError } from "./utils/errorUtils.js";
|
||||
import { QueueManager } from "./queue/QueueManager.js";
|
||||
|
||||
const init = async () => {
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
const wss = initWs(server);
|
||||
|
||||
const logger = initLogger();
|
||||
const server = http.createServer(app);
|
||||
|
||||
const pgClient = new pg.Client(process.env.SUPABASE_CONNECTION_STRING || "");
|
||||
await pgClient.connect();
|
||||
|
||||
const queue = initQueue();
|
||||
const workers = initWorkers(queue);
|
||||
await QueueManager.getInstance(); // initialize the queue manager
|
||||
await initWorkers();
|
||||
|
||||
app.use((req: any, res, next) => {
|
||||
req.pg = pgClient;
|
||||
req.queue = queue;
|
||||
req.logger = logger;
|
||||
next();
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
Event,
|
||||
Feature,
|
||||
} from "@autumn/shared";
|
||||
import { handleRequestError } from "@/utils/errorUtils.js";
|
||||
import RecaseError, { handleRequestError } from "@/utils/errorUtils.js";
|
||||
import { generateId } from "@/utils/genUtils.js";
|
||||
|
||||
import { EventService } from "./EventService.js";
|
||||
@@ -16,6 +16,7 @@ import { Queue } from "bullmq";
|
||||
import { createNewCustomer } from "../customers/cusUtils.js";
|
||||
import { SupabaseClient } from "@supabase/supabase-js";
|
||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||
import { QueueManager } from "@/queue/QueueManager.js";
|
||||
|
||||
export const eventsRouter = Router();
|
||||
|
||||
@@ -147,15 +148,39 @@ export const handleEventSent = async ({
|
||||
});
|
||||
|
||||
if (affectedFeatures.length > 0) {
|
||||
let queue: Queue = req.queue;
|
||||
queue.add("update-balance", {
|
||||
// let queue: Queue = req.queue;
|
||||
|
||||
const payload = {
|
||||
customerId: customer.internal_id,
|
||||
customer,
|
||||
features: affectedFeatures,
|
||||
event,
|
||||
org,
|
||||
env,
|
||||
});
|
||||
};
|
||||
|
||||
const queue = await QueueManager.getQueue({ useBackup: false });
|
||||
|
||||
try {
|
||||
// Add timeout to queue operation
|
||||
await queue.add("update-balance", payload);
|
||||
// console.log("Added update-balance to queue");
|
||||
} catch (error: any) {
|
||||
try {
|
||||
console.log("Adding update-balance to backup queue");
|
||||
const backupQueue = await QueueManager.getQueue({ useBackup: true });
|
||||
await backupQueue.add("update-balance", payload);
|
||||
} catch (error: any) {
|
||||
throw new RecaseError({
|
||||
message: "Failed to add update-balance to queue (backup)",
|
||||
code: "EVENT_QUEUE_ERROR",
|
||||
statusCode: 500,
|
||||
data: {
|
||||
message: error.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -5,9 +5,43 @@ import { Router } from "express";
|
||||
import { ApiKeyService } from "./ApiKeyService.js";
|
||||
import { OrgService } from "../orgs/OrgService.js";
|
||||
import { createKey } from "./api-keys/apiKeyUtils.js";
|
||||
import { SupabaseClient } from "@supabase/supabase-js";
|
||||
|
||||
export const devRouter = Router();
|
||||
|
||||
export const handleCreateApiKey = async ({
|
||||
sb,
|
||||
env,
|
||||
name,
|
||||
orgId,
|
||||
orgSlug,
|
||||
}: {
|
||||
sb: SupabaseClient;
|
||||
env: AppEnv;
|
||||
name: string;
|
||||
orgId: string;
|
||||
orgSlug: string;
|
||||
}) => {
|
||||
// 1. Create API key
|
||||
let prefix = "am_sk_test";
|
||||
if (env === AppEnv.Live) {
|
||||
prefix = "am_sk_live";
|
||||
}
|
||||
|
||||
const apiKey = await createKey({
|
||||
sb,
|
||||
env,
|
||||
name,
|
||||
orgId,
|
||||
prefix,
|
||||
meta: {
|
||||
org_slug: orgSlug,
|
||||
},
|
||||
});
|
||||
|
||||
return apiKey;
|
||||
};
|
||||
|
||||
devRouter.get("/data", withOrgAuth, async (req: any, res) => {
|
||||
const apiKeys = await ApiKeyService.getByOrg(req.sb, req.orgId, req.env);
|
||||
const org = await OrgService.getFullOrg({ sb: req.sb, orgId: req.orgId });
|
||||
|
||||
@@ -86,6 +86,10 @@ export class OrgService {
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
if (error.code === "PGRST116") {
|
||||
return null;
|
||||
}
|
||||
|
||||
throw new RecaseError({
|
||||
message: "Failed to get org from supabase",
|
||||
code: ErrCode.OrgNotFound,
|
||||
|
||||
@@ -304,7 +304,7 @@ export const roundPriceAmounts = (price: Price) => {
|
||||
const config = price.config as UsagePriceConfig;
|
||||
for (let i = 0; i < config.usage_tiers.length; i++) {
|
||||
config.usage_tiers[i].amount = Number(
|
||||
config.usage_tiers[i].amount.toFixed(2)
|
||||
config.usage_tiers[i].amount.toFixed(10)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -44,19 +44,20 @@ export const apiAuthMiddleware = async (req: any, res: any, next: any) => {
|
||||
console.log(`Autumn API verification failed`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Failed to fetch key from Autumn");
|
||||
console.log("Error: Failed to fetch key from Autumn");
|
||||
console.log("Error: ", error);
|
||||
}
|
||||
|
||||
// Fallback: Verify via Unkey
|
||||
try {
|
||||
const result = await validateApiKey(apiKey);
|
||||
|
||||
await migrateKey({
|
||||
sb: req.sb,
|
||||
keyId: result.keyId ?? "",
|
||||
meta: { org_slug: result.meta?.org_slug },
|
||||
apiKey,
|
||||
});
|
||||
// await migrateKey({
|
||||
// sb: req.sb,
|
||||
// keyId: result.keyId ?? "",
|
||||
// meta: { org_slug: result.meta?.org_slug },
|
||||
// apiKey,
|
||||
// });
|
||||
|
||||
// console.log(`Unkey verification successul for ${result.meta?.org_slug}`);
|
||||
req.orgId = result.ownerId;
|
||||
@@ -68,7 +69,8 @@ export const apiAuthMiddleware = async (req: any, res: any, next: any) => {
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
console.log("Unkey API verification failed");
|
||||
console.log("WARNING: Unkey API verification failed");
|
||||
console.log(error);
|
||||
withOrgAuth(req, res, next);
|
||||
return;
|
||||
}
|
||||
|
||||
161
server/src/queue/QueueManager.ts
Normal file
161
server/src/queue/QueueManager.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { Queue } from "bullmq";
|
||||
import { getRedisConnection } from "./queue.js";
|
||||
import { Redis } from "ioredis";
|
||||
|
||||
export class QueueManager {
|
||||
private static instance: QueueManager;
|
||||
private queue: Queue | null = null;
|
||||
private backupQueue: Queue | null = null;
|
||||
|
||||
private mainConnection: Redis | null = null;
|
||||
private backupConnection: Redis | null = null;
|
||||
|
||||
private constructor() {
|
||||
this.initializePromise = this.initQueue();
|
||||
}
|
||||
|
||||
private initializePromise: Promise<void>;
|
||||
public static async getInstance(): Promise<QueueManager> {
|
||||
if (!QueueManager.instance) {
|
||||
QueueManager.instance = new QueueManager();
|
||||
}
|
||||
// Wait for initialization to complete
|
||||
await QueueManager.instance.initializePromise;
|
||||
return QueueManager.instance;
|
||||
}
|
||||
|
||||
// 1. Create main redis connection
|
||||
private async pingRedis({
|
||||
useBackup,
|
||||
keepConnection = false,
|
||||
}: {
|
||||
useBackup: boolean;
|
||||
keepConnection?: boolean;
|
||||
}) {
|
||||
// 1. Connect to redis
|
||||
|
||||
const redisUrl = useBackup
|
||||
? process.env.REDIS_BACKUP_URL
|
||||
: process.env.REDIS_URL;
|
||||
|
||||
const connection = new Redis(redisUrl!, {
|
||||
retryStrategy: (times) => {
|
||||
return 5000;
|
||||
},
|
||||
});
|
||||
|
||||
connection.on("error", (error) => {
|
||||
console.log(
|
||||
`Redis connection error (${useBackup ? "backup" : "main"}): ${
|
||||
error.message
|
||||
}`
|
||||
);
|
||||
|
||||
if (!keepConnection) {
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Check if connection is live...
|
||||
await connection.ping();
|
||||
|
||||
if (!keepConnection) {
|
||||
await connection.quit();
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
|
||||
private async createConnections() {
|
||||
console.log("2. Creating redis connections (for workers...)");
|
||||
|
||||
this.mainConnection = await this.pingRedis({
|
||||
useBackup: false,
|
||||
keepConnection: true,
|
||||
});
|
||||
this.backupConnection = await this.pingRedis({
|
||||
useBackup: true,
|
||||
keepConnection: true,
|
||||
});
|
||||
}
|
||||
|
||||
private async initQueue() {
|
||||
console.log("Initializing Queue Manager...");
|
||||
console.group();
|
||||
// 1. Create redis connections
|
||||
console.log("1. Pinging main & backup redis");
|
||||
this.mainConnection = await this.pingRedis({ useBackup: false });
|
||||
this.backupConnection = await this.pingRedis({ useBackup: true });
|
||||
|
||||
await this.createConnections();
|
||||
// 2. Initialize main and backup queues
|
||||
console.log("2. Initializing main & backup queues");
|
||||
const mainQueue = new Queue("autumn", {
|
||||
connection: {
|
||||
url: process.env.REDIS_URL,
|
||||
enableOfflineQueue: false,
|
||||
retryStrategy: (times) => {
|
||||
return 5000;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const backupQueue = new Queue("autumn", {
|
||||
connection: {
|
||||
url: process.env.REDIS_BACKUP_URL,
|
||||
enableOfflineQueue: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Set up error handling for the queue
|
||||
mainQueue.on("error", async (error: any) => {
|
||||
console.error("QUEUE ERROR:", error.message);
|
||||
if (error.code !== "ECONNREFUSED") {
|
||||
}
|
||||
});
|
||||
|
||||
backupQueue.on("error", async (error: any) => {
|
||||
console.error("BACKUP QUEUE ERROR:", error.message);
|
||||
if (error.code !== "ECONNREFUSED") {
|
||||
}
|
||||
});
|
||||
|
||||
this.queue = mainQueue;
|
||||
this.backupQueue = backupQueue;
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
// Create workers
|
||||
|
||||
public static async getQueue({
|
||||
useBackup,
|
||||
}: {
|
||||
useBackup: boolean;
|
||||
}): Promise<Queue> {
|
||||
const queueManager = await QueueManager.getInstance();
|
||||
if (!queueManager.queue || !queueManager.backupQueue) {
|
||||
throw new Error("Queue not initialized");
|
||||
}
|
||||
return useBackup ? queueManager.backupQueue : queueManager.queue;
|
||||
}
|
||||
|
||||
public static async getConnection({
|
||||
useBackup,
|
||||
}: {
|
||||
useBackup: boolean;
|
||||
}): Promise<Redis> {
|
||||
const queueManager = await QueueManager.getInstance();
|
||||
if (!queueManager.mainConnection || !queueManager.backupConnection) {
|
||||
throw new Error("Connection not initialized");
|
||||
}
|
||||
return useBackup
|
||||
? queueManager.backupConnection
|
||||
: queueManager.mainConnection;
|
||||
}
|
||||
|
||||
public getBackupConnection(): Redis {
|
||||
if (!this.backupConnection) {
|
||||
throw new Error("Backup connection not initialized");
|
||||
}
|
||||
return this.backupConnection;
|
||||
}
|
||||
}
|
||||
@@ -1,54 +1,74 @@
|
||||
import { Job, Queue, Worker } from "bullmq";
|
||||
import { runUpdateBalanceTask } from "@/trigger/updateBalanceTask.js";
|
||||
import { Redis } from "ioredis";
|
||||
import { QueueManager } from "./QueueManager.js";
|
||||
|
||||
const redis = new Redis(process.env.REDIS_URL || "redis://localhost:6379");
|
||||
const NUM_WORKERS = 5;
|
||||
|
||||
export const getRedisConnection = ({
|
||||
useBackup = false,
|
||||
}: {
|
||||
useBackup?: boolean;
|
||||
}) => {
|
||||
let redisUrl = process.env.REDIS_URL || "redis://localhost:6379";
|
||||
|
||||
if (useBackup) {
|
||||
redisUrl = process.env.REDIS_BACKUP_URL || "redis://localhost:6379";
|
||||
}
|
||||
|
||||
return {
|
||||
connection: {
|
||||
url: redisUrl,
|
||||
// enableOfflineQueue: false,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
async function acquireLock({
|
||||
customerId,
|
||||
timeout = 30000,
|
||||
useBackup = false,
|
||||
}: {
|
||||
customerId: string;
|
||||
timeout?: number;
|
||||
useBackup?: boolean;
|
||||
}): Promise<boolean> {
|
||||
// const redis = getRedisClient({ useBackup });
|
||||
const redis = await QueueManager.getConnection({ useBackup });
|
||||
|
||||
async function acquireLock(
|
||||
customerId: string,
|
||||
timeout = 30000
|
||||
): Promise<boolean> {
|
||||
const lockKey = `lock:customer:${customerId}`;
|
||||
const acquired = await redis.set(lockKey, "1", "PX", timeout, "NX");
|
||||
return acquired === "OK";
|
||||
}
|
||||
|
||||
async function releaseLock(customerId: string): Promise<void> {
|
||||
async function releaseLock({
|
||||
customerId,
|
||||
useBackup,
|
||||
}: {
|
||||
customerId: string;
|
||||
useBackup: boolean;
|
||||
}): Promise<void> {
|
||||
const redis = await QueueManager.getConnection({ useBackup });
|
||||
const lockKey = `lock:customer:${customerId}`;
|
||||
await redis.del(lockKey);
|
||||
}
|
||||
|
||||
const getRedisConnection = () => {
|
||||
let redisUrl = process.env.REDIS_URL || "redis://localhost:6379";
|
||||
|
||||
return {
|
||||
connection: {
|
||||
url: redisUrl,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const initQueue = () => {
|
||||
try {
|
||||
return new Queue("autumn", getRedisConnection());
|
||||
} catch (error) {
|
||||
console.error("Error initialising queue:\n", error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
const numWorkers = 5;
|
||||
|
||||
const initWorker = (id: number, queue: Queue) => {
|
||||
// Create supabase client
|
||||
|
||||
const initWorker = ({
|
||||
id,
|
||||
queue,
|
||||
useBackup,
|
||||
}: {
|
||||
id: number;
|
||||
queue: Queue;
|
||||
useBackup: boolean;
|
||||
}) => {
|
||||
let worker = new Worker(
|
||||
"autumn",
|
||||
async (job: Job) => {
|
||||
// console.log("JOB ID:", job.id, `(${useBackup ? "BACKUP" : "MAIN"})`);
|
||||
// console.log("EVENT ID:", job.data.event.id);
|
||||
const { customerId } = job.data;
|
||||
|
||||
while (!(await acquireLock(customerId, 10000))) {
|
||||
// console.log(`Customer ${customer.id} locked by another worker`);
|
||||
while (!(await acquireLock({ customerId, timeout: 10000, useBackup }))) {
|
||||
await queue.add(job.name, job.data, {
|
||||
delay: 50,
|
||||
});
|
||||
@@ -60,37 +80,57 @@ const initWorker = (id: number, queue: Queue) => {
|
||||
} catch (error) {
|
||||
console.error("Error updating balance:", error);
|
||||
} finally {
|
||||
await releaseLock(customerId);
|
||||
await releaseLock({ customerId, useBackup });
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
...getRedisConnection(),
|
||||
concurrency: 3,
|
||||
...getRedisConnection({ useBackup }),
|
||||
concurrency: 1,
|
||||
removeOnComplete: {
|
||||
count: 0,
|
||||
},
|
||||
removeOnFail: {
|
||||
count: 0,
|
||||
},
|
||||
drainDelay: 1000,
|
||||
maxStalledCount: 0,
|
||||
}
|
||||
);
|
||||
|
||||
worker.on("ready", () => {
|
||||
console.log(`Worker ${id} ready`);
|
||||
console.log(`Worker ${id} ready (${useBackup ? "BACKUP" : "MAIN"})`);
|
||||
});
|
||||
|
||||
worker.on("error", async (error) => {
|
||||
console.log("WORKER ERROR:\n");
|
||||
console.log(error);
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
worker.on("stalled", (jobId: string) => {
|
||||
console.log(`Worker ${id} stalled (${useBackup ? "BACKUP" : "MAIN"})`);
|
||||
console.log("JOB ID:", jobId);
|
||||
});
|
||||
|
||||
// Check jobs left in queue
|
||||
|
||||
worker.on("error", async (error: any) => {
|
||||
if (error.code !== "ECONNREFUSED") {
|
||||
console.log("WORKER ERROR:", error.message);
|
||||
}
|
||||
});
|
||||
|
||||
worker.on("failed", (job, error) => {
|
||||
console.log("WORKER FAILED:\n");
|
||||
console.log(error);
|
||||
console.log("WORKER FAILED:", error.message);
|
||||
});
|
||||
};
|
||||
|
||||
export const initWorkers = (queue: Queue) => {
|
||||
export const initWorkers = async () => {
|
||||
const workers = [];
|
||||
for (let i = 0; i < numWorkers; i++) {
|
||||
workers.push(initWorker(i, queue));
|
||||
|
||||
const mainQueue = await QueueManager.getQueue({ useBackup: false });
|
||||
const backupQueue = await QueueManager.getQueue({ useBackup: true });
|
||||
|
||||
for (let i = 0; i < NUM_WORKERS; i++) {
|
||||
workers.push(initWorker({ id: i, queue: mainQueue, useBackup: false }));
|
||||
workers.push(initWorker({ id: i, queue: backupQueue, useBackup: true }));
|
||||
}
|
||||
|
||||
// Get stalled jobs
|
||||
|
||||
return workers;
|
||||
};
|
||||
|
||||
@@ -256,7 +256,9 @@ export const updateCustomerBalance = async ({
|
||||
},
|
||||
});
|
||||
} else {
|
||||
console.log("No usage-based entitlement found");
|
||||
console.log(
|
||||
` - Remaining deduction: ${toDeduct}, no usage-based entitlement found`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,7 +274,9 @@ export const runUpdateBalanceTask = async (payload: any) => {
|
||||
const { customer, features, event, org, env } = payload;
|
||||
|
||||
console.log("--------------------------------");
|
||||
console.log("Inside updateBalanceTask...");
|
||||
console.log(
|
||||
`UPDATING BALANCE FOR CUSTOMER (${customer.id}), ORG: ${org.slug}`
|
||||
);
|
||||
|
||||
console.log("1. Updating customer balance...");
|
||||
const cusEnts: any = await updateCustomerBalance({
|
||||
|
||||
@@ -18,7 +18,9 @@ export const compareObjects = (obj1: any, obj2: any) => {
|
||||
};
|
||||
|
||||
export const keyToTitle = (key: string) => {
|
||||
return key.replace(/-/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
|
||||
return key
|
||||
.replace(/[-_]/g, " ")
|
||||
.replace(/\b\w/g, (char) => char.toUpperCase());
|
||||
};
|
||||
|
||||
export const notNullOrUndefined = (value: any) => {
|
||||
|
||||
Reference in New Issue
Block a user