You've already forked domainstack.io
mirror of
https://github.com/jakejarvis/domainstack.io.git
synced 2025-12-02 19:33:48 -05:00
refactor: remove cron job and related pricing refresh logic, transitioning to Next.js caching for improved performance
This commit is contained in:
@@ -1,58 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { refreshAllProviderPricing } from "@/server/services/pricing";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
// Verify Vercel cron secret
|
||||
const authHeader = request.headers.get("authorization");
|
||||
const expectedAuth = process.env.CRON_SECRET
|
||||
? `Bearer ${process.env.CRON_SECRET}`
|
||||
: null;
|
||||
|
||||
if (!expectedAuth) {
|
||||
console.error("[pricing-refresh] cron misconfigured: CRON_SECRET missing");
|
||||
return NextResponse.json(
|
||||
{ error: "CRON_SECRET not configured" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
if (authHeader !== expectedAuth) {
|
||||
console.warn("[pricing-refresh] cron unauthorized");
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const startedAt = Date.now();
|
||||
const result = await refreshAllProviderPricing();
|
||||
|
||||
const durationMs = Date.now() - startedAt;
|
||||
if (result.failed.length > 0) {
|
||||
console.warn(
|
||||
`[pricing-refresh] completed with errors: refreshed=${result.refreshed.length} failed=${result.failed.length} ${durationMs}ms`,
|
||||
);
|
||||
} else {
|
||||
console.info(
|
||||
`[pricing-refresh] completed: refreshed=${result.refreshed.length} ${durationMs}ms`,
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
refreshed: result.refreshed,
|
||||
failed: result.failed,
|
||||
durationMs,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(
|
||||
"[pricing-refresh] cron failed",
|
||||
err instanceof Error ? err : new Error(String(err)),
|
||||
);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Internal error",
|
||||
message: err instanceof Error ? err.message : "unknown",
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,343 +0,0 @@
|
||||
/* @vitest-environment node */
|
||||
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
|
||||
let getPricingForTld: typeof import("./pricing").getPricingForTld;
|
||||
let refreshAllProviderPricing: typeof import("./pricing").refreshAllProviderPricing;
|
||||
|
||||
// Mock registrar pricing responses (standardized format)
|
||||
const mockPorkbunResponse = {
|
||||
com: { registration: "9.99", renewal: "10.99" },
|
||||
dev: { registration: "12.00", renewal: "12.00" },
|
||||
org: { registration: "11.50" },
|
||||
};
|
||||
|
||||
const mockCloudflareResponse = {
|
||||
com: { registration: "10.44" },
|
||||
dev: { registration: "13.50" },
|
||||
net: { registration: "12.88" },
|
||||
};
|
||||
|
||||
describe("pricing service", () => {
|
||||
beforeAll(async () => {
|
||||
const { makeInMemoryRedis } = await import("@/lib/redis-mock");
|
||||
const impl = makeInMemoryRedis();
|
||||
vi.doMock("@/lib/redis", () => impl);
|
||||
const module = await import("./pricing");
|
||||
getPricingForTld = module.getPricingForTld;
|
||||
refreshAllProviderPricing = module.refreshAllProviderPricing;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
const { resetInMemoryRedis } = await import("@/lib/redis-mock");
|
||||
resetInMemoryRedis();
|
||||
});
|
||||
|
||||
describe("getPricingForTld", () => {
|
||||
it("returns empty providers for single-label hosts", async () => {
|
||||
const res = await getPricingForTld("localhost");
|
||||
expect(res.tld).toBeNull();
|
||||
expect(res.providers).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns empty providers for empty input", async () => {
|
||||
const res = await getPricingForTld("");
|
||||
expect(res.tld).toBeNull();
|
||||
expect(res.providers).toEqual([]);
|
||||
});
|
||||
|
||||
it("fetches from all providers and caches responses on miss", async () => {
|
||||
// Mock both Porkbun and Cloudflare APIs (raw formats)
|
||||
const fetchSpy = vi
|
||||
.spyOn(global, "fetch")
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
status: "SUCCESS",
|
||||
pricing: mockPorkbunResponse,
|
||||
}),
|
||||
} as unknown as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => mockCloudflareResponse,
|
||||
} as unknown as Response);
|
||||
|
||||
const res = await getPricingForTld("example.com");
|
||||
|
||||
expect(res.tld).toBe("com");
|
||||
expect(res.providers).toHaveLength(2);
|
||||
expect(res.providers).toContainEqual({
|
||||
provider: "porkbun",
|
||||
price: "9.99",
|
||||
});
|
||||
expect(res.providers).toContainEqual({
|
||||
provider: "cloudflare",
|
||||
price: "10.44",
|
||||
});
|
||||
|
||||
// Verify both caches were set
|
||||
const { redis } = await import("@/lib/redis");
|
||||
expect(await redis.exists("pricing:porkbun")).toBe(1);
|
||||
expect(await redis.exists("pricing:cloudflare")).toBe(1);
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("uses cached provider data without fetching again", async () => {
|
||||
// Seed cache with both provider data
|
||||
const { redis } = await import("@/lib/redis");
|
||||
await redis.set("pricing:porkbun", mockPorkbunResponse);
|
||||
await redis.set("pricing:cloudflare", mockCloudflareResponse);
|
||||
|
||||
const fetchSpy = vi.spyOn(global, "fetch");
|
||||
const res = await getPricingForTld("something.dev");
|
||||
|
||||
expect(res.tld).toBe("dev");
|
||||
expect(res.providers).toHaveLength(2);
|
||||
expect(res.providers).toContainEqual({
|
||||
provider: "porkbun",
|
||||
price: "12.00",
|
||||
});
|
||||
expect(res.providers).toContainEqual({
|
||||
provider: "cloudflare",
|
||||
price: "13.50",
|
||||
});
|
||||
expect(fetchSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("handles upstream API error gracefully", async () => {
|
||||
vi.spyOn(global, "fetch")
|
||||
.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 500,
|
||||
} as unknown as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 500,
|
||||
} as unknown as Response);
|
||||
|
||||
const res = await getPricingForTld("example.com");
|
||||
expect(res.tld).toBe("com");
|
||||
expect(res.providers).toEqual([]);
|
||||
});
|
||||
|
||||
it("handles network fetch error gracefully", async () => {
|
||||
vi.spyOn(global, "fetch")
|
||||
.mockRejectedValueOnce(new Error("Network timeout"))
|
||||
.mockRejectedValueOnce(new Error("Network timeout"));
|
||||
|
||||
const res = await getPricingForTld("example.org");
|
||||
expect(res.tld).toBe("org");
|
||||
expect(res.providers).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns empty providers when TLD is not found in any provider response", async () => {
|
||||
// Seed cache with data that doesn't include .xyz
|
||||
const { redis } = await import("@/lib/redis");
|
||||
await redis.set("pricing:porkbun", mockPorkbunResponse);
|
||||
await redis.set("pricing:cloudflare", mockCloudflareResponse);
|
||||
|
||||
const res = await getPricingForTld("example.xyz");
|
||||
expect(res.tld).toBe("xyz");
|
||||
expect(res.providers).toEqual([]);
|
||||
});
|
||||
|
||||
it("normalizes domain input (case, whitespace)", async () => {
|
||||
const { redis } = await import("@/lib/redis");
|
||||
await redis.set("pricing:porkbun", mockPorkbunResponse);
|
||||
await redis.set("pricing:cloudflare", mockCloudflareResponse);
|
||||
|
||||
const res = await getPricingForTld(" EXAMPLE.COM ");
|
||||
expect(res.tld).toBe("com");
|
||||
expect(res.providers).toHaveLength(2);
|
||||
expect(res.providers).toContainEqual({
|
||||
provider: "porkbun",
|
||||
price: "9.99",
|
||||
});
|
||||
expect(res.providers).toContainEqual({
|
||||
provider: "cloudflare",
|
||||
price: "10.44",
|
||||
});
|
||||
});
|
||||
|
||||
it("handles subdomains correctly by extracting TLD", async () => {
|
||||
const { redis } = await import("@/lib/redis");
|
||||
await redis.set("pricing:porkbun", mockPorkbunResponse);
|
||||
await redis.set("pricing:cloudflare", mockCloudflareResponse);
|
||||
|
||||
const res = await getPricingForTld("www.example.dev");
|
||||
expect(res.tld).toBe("dev");
|
||||
expect(res.providers).toHaveLength(2);
|
||||
expect(res.providers).toContainEqual({
|
||||
provider: "porkbun",
|
||||
price: "12.00",
|
||||
});
|
||||
expect(res.providers).toContainEqual({
|
||||
provider: "cloudflare",
|
||||
price: "13.50",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("refreshAllProviderPricing", () => {
|
||||
it("successfully refreshes all providers and populates cache", async () => {
|
||||
// Mock both Porkbun and Cloudflare APIs (raw formats)
|
||||
const fetchSpy = vi
|
||||
.spyOn(global, "fetch")
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
status: "SUCCESS",
|
||||
pricing: mockPorkbunResponse,
|
||||
}),
|
||||
} as unknown as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => mockCloudflareResponse,
|
||||
} as unknown as Response);
|
||||
|
||||
const result = await refreshAllProviderPricing();
|
||||
|
||||
expect(result.refreshed).toEqual(["porkbun", "cloudflare"]);
|
||||
expect(result.failed).toEqual([]);
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
||||
|
||||
// Verify both caches were populated
|
||||
const { redis } = await import("@/lib/redis");
|
||||
const porkbunCached = await redis.get("pricing:porkbun");
|
||||
expect(porkbunCached).toEqual(mockPorkbunResponse);
|
||||
const cloudflareCached = await redis.get("pricing:cloudflare");
|
||||
expect(cloudflareCached).toEqual(mockCloudflareResponse);
|
||||
});
|
||||
|
||||
it("handles API errors and reports failed providers", async () => {
|
||||
vi.spyOn(global, "fetch")
|
||||
.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 503,
|
||||
} as unknown as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 503,
|
||||
} as unknown as Response);
|
||||
|
||||
const result = await refreshAllProviderPricing();
|
||||
|
||||
expect(result.refreshed).toEqual([]);
|
||||
expect(result.failed).toEqual(["porkbun", "cloudflare"]);
|
||||
|
||||
// Verify caches were not populated
|
||||
const { redis } = await import("@/lib/redis");
|
||||
expect(await redis.exists("pricing:porkbun")).toBe(0);
|
||||
expect(await redis.exists("pricing:cloudflare")).toBe(0);
|
||||
});
|
||||
|
||||
it("handles network errors and reports failed providers", async () => {
|
||||
vi.spyOn(global, "fetch")
|
||||
.mockRejectedValueOnce(new Error("ECONNREFUSED"))
|
||||
.mockRejectedValueOnce(new Error("ECONNREFUSED"));
|
||||
|
||||
const result = await refreshAllProviderPricing();
|
||||
|
||||
expect(result.refreshed).toEqual([]);
|
||||
expect(result.failed).toEqual(["porkbun", "cloudflare"]);
|
||||
});
|
||||
|
||||
it("refreshes even when cache already exists", async () => {
|
||||
// Pre-populate cache with old data
|
||||
const { redis } = await import("@/lib/redis");
|
||||
const oldData = { com: { registration: "8.88" } };
|
||||
await redis.set("pricing:porkbun", oldData);
|
||||
await redis.set("pricing:cloudflare", oldData);
|
||||
|
||||
// Mock APIs with new data (raw formats)
|
||||
const fetchSpy = vi
|
||||
.spyOn(global, "fetch")
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
status: "SUCCESS",
|
||||
pricing: mockPorkbunResponse,
|
||||
}),
|
||||
} as unknown as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => mockCloudflareResponse,
|
||||
} as unknown as Response);
|
||||
|
||||
const result = await refreshAllProviderPricing();
|
||||
|
||||
expect(result.refreshed).toEqual(["porkbun", "cloudflare"]);
|
||||
expect(result.failed).toEqual([]);
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
||||
|
||||
// Verify caches were updated with new data
|
||||
const porkbunCached = await redis.get("pricing:porkbun");
|
||||
expect(porkbunCached).toEqual(mockPorkbunResponse);
|
||||
expect(porkbunCached).not.toEqual(oldData);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getPricingForTld - negative caching", () => {
|
||||
it("returns pricing successfully from API and caches it", async () => {
|
||||
const { redis } = await import("@/lib/redis");
|
||||
|
||||
vi.spyOn(global, "fetch")
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
status: "SUCCESS",
|
||||
pricing: mockPorkbunResponse,
|
||||
}),
|
||||
} as unknown as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => mockCloudflareResponse,
|
||||
} as unknown as Response);
|
||||
|
||||
await getPricingForTld("example.com");
|
||||
|
||||
// Verify pricing data was cached
|
||||
const porkbunCached = await redis.get("pricing:porkbun");
|
||||
expect(porkbunCached).toEqual(mockPorkbunResponse);
|
||||
const cloudflareCached = await redis.get("pricing:cloudflare");
|
||||
expect(cloudflareCached).toEqual(mockCloudflareResponse);
|
||||
});
|
||||
|
||||
it("caches sentinel value with short TTL when fetch fails", async () => {
|
||||
const { redis } = await import("@/lib/redis");
|
||||
|
||||
vi.spyOn(global, "fetch")
|
||||
.mockRejectedValueOnce(new Error("Network error"))
|
||||
.mockRejectedValueOnce(new Error("Network error"));
|
||||
|
||||
await getPricingForTld("example.com");
|
||||
|
||||
// Verify negative cache (null) was set to prevent repeated failed fetches
|
||||
const porkbunCached = await redis.get("pricing:porkbun");
|
||||
expect(porkbunCached).toBe("error");
|
||||
const cloudflareCached = await redis.get("pricing:cloudflare");
|
||||
expect(cloudflareCached).toBe("error");
|
||||
});
|
||||
|
||||
it("sets negative cache with short TTL on fetch error", async () => {
|
||||
const { redis } = await import("@/lib/redis");
|
||||
|
||||
vi.spyOn(global, "fetch")
|
||||
.mockRejectedValueOnce(new Error("API down"))
|
||||
.mockRejectedValueOnce(new Error("API down"));
|
||||
|
||||
await getPricingForTld("example.com");
|
||||
|
||||
// Verify negative cache was set for both providers
|
||||
const porkbunCached = await redis.get("pricing:porkbun");
|
||||
expect(porkbunCached).toBe("error");
|
||||
const cloudflareCached = await redis.get("pricing:cloudflare");
|
||||
expect(cloudflareCached).toBe("error");
|
||||
|
||||
// Verify TTL is short (should be 60 seconds for negative cache)
|
||||
const porkbunTtl = await redis.ttl("pricing:porkbun");
|
||||
expect(porkbunTtl).toBeGreaterThan(0);
|
||||
expect(porkbunTtl).toBeLessThanOrEqual(60);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,23 @@
|
||||
import { after } from "next/server";
|
||||
"use cache";
|
||||
|
||||
import { getDomainTld } from "rdapper";
|
||||
import { ns, redis } from "@/lib/redis";
|
||||
import type { Pricing } from "@/lib/schemas";
|
||||
|
||||
/**
|
||||
* Domain registration pricing service.
|
||||
*
|
||||
* Caching Strategy:
|
||||
* - Uses Next.js 16 Data Cache with "use cache" directive
|
||||
* - Automatic stale-while-revalidate (SWR): serves cached data instantly,
|
||||
* revalidates in background when cache expires
|
||||
* - Cache TTLs: 7 days (Porkbun and Cloudflare)
|
||||
* - No manual cron jobs needed - Next.js handles revalidation automatically
|
||||
* - Gracefully handles slow/failed API responses by returning null
|
||||
*
|
||||
* When registrar APIs are slow (common), users see cached pricing immediately
|
||||
* while fresh data fetches in the background. This provides the best UX.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Normalized pricing response shape that all registrars conform to.
|
||||
* Maps TLD to pricing information: { "com": { "registration": "10.99", ... }, ... }
|
||||
@@ -18,8 +33,6 @@ type RegistrarPricingResponse = Record<
|
||||
interface PricingProvider {
|
||||
/** Provider name for logging */
|
||||
name: string;
|
||||
/** Redis key for cached pricing data */
|
||||
cacheKey: string;
|
||||
/** How long to cache the pricing data (seconds) */
|
||||
cacheTtlSeconds: number;
|
||||
/** Fetch pricing data from the registrar API */
|
||||
@@ -32,61 +45,21 @@ interface PricingProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic function to fetch and cache pricing data from any provider.
|
||||
* Uses simple fail-open caching without distributed locks.
|
||||
* Fetch pricing data from a provider with Next.js Data Cache.
|
||||
* Uses Next.js 16's "use cache" directive for automatic caching.
|
||||
*/
|
||||
async function fetchProviderPricing(
|
||||
provider: PricingProvider,
|
||||
): Promise<RegistrarPricingResponse | null> {
|
||||
// Try cache first
|
||||
const cached = await redis
|
||||
.get<RegistrarPricingResponse | "error">(provider.cacheKey)
|
||||
.catch((err) => {
|
||||
console.error(
|
||||
`[pricing] cache read error ${provider.name}`,
|
||||
{ cacheKey: provider.cacheKey },
|
||||
err instanceof Error ? err : new Error(String(err)),
|
||||
);
|
||||
return null;
|
||||
});
|
||||
if (cached && cached !== "error") return cached;
|
||||
if (cached === "error") {
|
||||
console.debug(`[pricing] cached failure ${provider.name}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Fetch fresh pricing
|
||||
try {
|
||||
const payload = await provider.fetchPricing();
|
||||
// Cache for next time
|
||||
after(() => {
|
||||
redis
|
||||
.set(provider.cacheKey, payload, { ex: provider.cacheTtlSeconds })
|
||||
.catch((err) => {
|
||||
console.error(
|
||||
`[pricing] cache write error ${provider.name}`,
|
||||
{ cacheKey: provider.cacheKey },
|
||||
err instanceof Error ? err : new Error(String(err)),
|
||||
);
|
||||
});
|
||||
});
|
||||
console.info(`[pricing] fetch ok ${provider.name} (not cached)`);
|
||||
console.info(`[pricing] fetch ok ${provider.name}`);
|
||||
return payload;
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`[pricing] fetch error ${provider.name}`,
|
||||
err instanceof Error ? err : new Error(String(err)),
|
||||
);
|
||||
// Short TTL negative cache
|
||||
after(() => {
|
||||
redis.set(provider.cacheKey, "error", { ex: 60 }).catch((cacheErr) => {
|
||||
console.error(
|
||||
`[pricing] negative cache write error ${provider.name}`,
|
||||
{ cacheKey: provider.cacheKey },
|
||||
cacheErr instanceof Error ? cacheErr : new Error(String(cacheErr)),
|
||||
);
|
||||
});
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -97,7 +70,6 @@ async function fetchProviderPricing(
|
||||
|
||||
const porkbunProvider: PricingProvider = {
|
||||
name: "porkbun",
|
||||
cacheKey: ns("pricing:porkbun"),
|
||||
cacheTtlSeconds: 7 * 24 * 60 * 60, // 7 days
|
||||
|
||||
async fetchPricing(): Promise<RegistrarPricingResponse> {
|
||||
@@ -114,6 +86,10 @@ const porkbunProvider: PricingProvider = {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: "{}",
|
||||
signal: controller.signal,
|
||||
next: {
|
||||
revalidate: this.cacheTtlSeconds,
|
||||
tags: ["pricing", "pricing-porkbun"],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -145,7 +121,6 @@ const porkbunProvider: PricingProvider = {
|
||||
|
||||
const cloudflareProvider: PricingProvider = {
|
||||
name: "cloudflare",
|
||||
cacheKey: ns("pricing:cloudflare"),
|
||||
cacheTtlSeconds: 7 * 24 * 60 * 60, // 7 days
|
||||
|
||||
async fetchPricing(): Promise<RegistrarPricingResponse> {
|
||||
@@ -159,6 +134,10 @@ const cloudflareProvider: PricingProvider = {
|
||||
method: "GET",
|
||||
headers: { Accept: "application/json" },
|
||||
signal: controller.signal,
|
||||
next: {
|
||||
revalidate: this.cacheTtlSeconds,
|
||||
tags: ["pricing", "pricing-cloudflare"],
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
@@ -260,50 +239,3 @@ export async function getPricingForTld(domain: string): Promise<Pricing> {
|
||||
|
||||
return { tld, providers: availableProviders };
|
||||
}
|
||||
|
||||
/**
|
||||
* Proactively refresh pricing data from all providers.
|
||||
* Called by the daily cron job to keep cache warm.
|
||||
* Runs all provider refreshes in parallel for efficiency.
|
||||
*/
|
||||
export async function refreshAllProviderPricing(): Promise<{
|
||||
refreshed: string[];
|
||||
failed: string[];
|
||||
}> {
|
||||
const refreshed: string[] = [];
|
||||
const failed: string[] = [];
|
||||
|
||||
// Run all provider refreshes in parallel
|
||||
const tasks = providers.map(async (provider) => {
|
||||
try {
|
||||
const payload = await provider.fetchPricing();
|
||||
await redis.set(provider.cacheKey, payload, {
|
||||
ex: provider.cacheTtlSeconds,
|
||||
});
|
||||
return { name: provider.name, success: true as const };
|
||||
} catch (err) {
|
||||
return { name: provider.name, success: false as const, error: err };
|
||||
}
|
||||
});
|
||||
|
||||
const results = await Promise.allSettled(tasks);
|
||||
|
||||
// Process results and emit logs
|
||||
for (const result of results) {
|
||||
if (result.status === "fulfilled") {
|
||||
const { name, success, error } = result.value;
|
||||
if (success) {
|
||||
refreshed.push(name);
|
||||
console.info(`[pricing] refresh ok ${name}`);
|
||||
} else {
|
||||
failed.push(name);
|
||||
console.error(`[pricing] refresh failed ${name}`, error);
|
||||
}
|
||||
} else {
|
||||
// Promise itself was rejected (shouldn't happen with our error handling)
|
||||
console.error("[pricing] refresh unexpected", result.reason);
|
||||
}
|
||||
}
|
||||
|
||||
return { refreshed, failed };
|
||||
}
|
||||
|
||||
@@ -6,11 +6,5 @@
|
||||
"NPM_CONFIG_NODE_LINKER": "hoisted",
|
||||
"PUPPETEER_SKIP_DOWNLOAD": "1"
|
||||
}
|
||||
},
|
||||
"crons": [
|
||||
{
|
||||
"path": "/api/cron/pricing-refresh",
|
||||
"schedule": "0 4 * * *"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user