added backup to queue

This commit is contained in:
John Yeo
2025-02-20 14:55:02 +00:00
parent a3c5085909
commit e5b9c16dec
20 changed files with 688 additions and 392 deletions

View File

@@ -27,5 +27,10 @@ export default async function DemoPage({ params }) {
);
}
return <DynamicDemoView />;
return (
<DynamicDemoView
publishableKey={data.publishable_key}
secretKey={data.secret_key}
/>
);
}

View File

@@ -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 || ""

View File

@@ -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,
},

View File

@@ -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}

View 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>
);
}

View File

@@ -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>
);
};

View 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
View File

@@ -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",

View File

@@ -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",

View File

@@ -31,7 +31,8 @@ export const initLogger = () => {
},
});
logger.info("Logger initialized");
// logger.info("Logger initialized");
console.log("Logger initialized");
return logger;
};

View File

@@ -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();
});

View File

@@ -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,
},
});
}
}
}
};

View File

@@ -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 });

View File

@@ -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,

View File

@@ -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)
);
}

View File

@@ -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;
}

View 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;
}
}

View File

@@ -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;
};

View File

@@ -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({

View File

@@ -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) => {