Skip to content

Commit e71f262

Browse files
authored
fix: prevent Worker hangs and fix undefined .includes() crash (#43)
* fix: prevent Worker hangs and fix undefined .includes() crash - Add MongoDB connection timeouts (serverSelectionTimeoutMS, connectTimeoutMS, socketTimeoutMS) to prevent indefinite hangs when DB is unreachable - Eagerly initialize Payload at module-load time with a 15s timeout guard, reducing cold-start penalty on Cloudflare Workers - Guard against undefined order data in OrderSteps and OrderContainer to prevent TypeError: Cannot read properties of undefined (reading 'includes') * fix: simplify conditional check for order payment status
1 parent 9f2c535 commit e71f262

4 files changed

Lines changed: 47 additions & 14 deletions

File tree

components/ordercontainer/OrderContainer.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ const OrderContainerInner = () => {
9494
const data = await res.json();
9595
const order = data.order;
9696

97+
if (!order?.status) {
98+
setResumeChecked(true);
99+
return;
100+
}
101+
97102
if (order.status === OrderStatus.EXPIRED) {
98103
// Can't resume expired orders
99104
setResumeChecked(true);

components/ordercontainer/OrderSteps.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,13 @@ export default function OrderSteps({
6666
const res = await fetch(`/api/shop/${orderId}`);
6767
if (res.ok) {
6868
const data = await res.json();
69-
setOrderDetails(data.order);
69+
const order = data.order;
70+
if (!order?.status) return;
71+
72+
setOrderDetails(order);
7073

7174
// If order is already PAID, jump to pickup
72-
if (
73-
data.order.status === OrderStatus.PAID &&
74-
step === OrderStep.PAYMENT
75-
) {
75+
if (order.status === OrderStatus.PAID && step === OrderStep.PAYMENT) {
7676
onPaymentSuccess();
7777
}
7878
// If already has a timeslot, jump to complete
@@ -81,7 +81,7 @@ export default function OrderSteps({
8181
OrderStatus.AWAITING_PICKUP,
8282
OrderStatus.PRINTED,
8383
OrderStatus.PICKED_UP,
84-
].includes(data.order.status) &&
84+
].includes(order.status) &&
8585
step !== OrderStep.COMPLETE
8686
) {
8787
onPickupConfirmed();
@@ -102,7 +102,7 @@ export default function OrderSteps({
102102
const res = await fetch(`/api/shop/${orderId}`);
103103
if (res.ok) {
104104
const data = await res.json();
105-
if (data.order.status === OrderStatus.PAID) {
105+
if (data.order?.status === OrderStatus.PAID) {
106106
clearInterval(interval);
107107
setWaitingForStripe(false);
108108
onPaymentSuccess();

lib/payload.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,37 @@ import type { Payload } from "payload";
22
import { getPayload } from "payload";
33
import configPromise from "../payload.config";
44

5-
let cachedPayload: Payload | null = null;
5+
/** Timeout (ms) for Payload initialisation — prevents Workers from hanging. */
6+
const INIT_TIMEOUT_MS = 15_000;
7+
8+
/**
9+
* Eagerly start Payload initialization at module-load time.
10+
*
11+
* On Cloudflare Workers the first request after a cold start must wait for
12+
* Payload + MongoDB to initialise. By kicking this off at import time the
13+
* connection is established in parallel with request routing, which greatly
14+
* reduces (or eliminates) the cold-start penalty for the first request.
15+
*
16+
* A timeout wrapper ensures the Worker never hangs indefinitely if the
17+
* database is unreachable.
18+
*/
19+
const payloadPromise: Promise<Payload> = Promise.race([
20+
getPayload({ config: configPromise }),
21+
new Promise<never>((_, reject) =>
22+
setTimeout(
23+
() => reject(new Error("Payload initialization timed out")),
24+
INIT_TIMEOUT_MS,
25+
),
26+
),
27+
]);
628

729
/**
830
* Returns a cached Payload CMS client instance.
931
* Uses the Payload Local API for server-side data fetching.
32+
*
33+
* The first call awaits the eagerly-started initialisation promise;
34+
* subsequent calls return the already-resolved instance immediately.
1035
*/
1136
export const getPayloadClient = async (): Promise<Payload> => {
12-
if (cachedPayload) {
13-
return cachedPayload;
14-
}
15-
16-
cachedPayload = await getPayload({ config: configPromise });
17-
return cachedPayload;
37+
return payloadPromise;
1838
};

payload.config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ export default buildConfig({
2222
secret: process.env.PAYLOAD_SECRET || "CHANGE-ME-IN-PRODUCTION",
2323
db: mongooseAdapter({
2424
url: process.env.DATABASE_URI || process.env.MONGODB_URI || "",
25+
connectOptions: {
26+
// Prevent indefinite hangs on Cloudflare Workers (stateless runtime).
27+
// Without these, a slow or unreachable MongoDB will cause the Worker
28+
// to hang until Cloudflare cancels the request.
29+
serverSelectionTimeoutMS: 5000,
30+
connectTimeoutMS: 5000,
31+
socketTimeoutMS: 10000,
32+
},
2533
}),
2634
editor: lexicalEditor(),
2735
collections: [Users, Customers, Orders, Timeslots, AboutSections, Media],

0 commit comments

Comments
 (0)