UNPKG

@spoolcms/nextjs

Version:

The beautiful headless CMS for Next.js developers

277 lines (276 loc) 11.4 kB
"use strict"; /** * Spool Live Updates Hook - Convex Version * This provides real-time content updates for customer Next.js apps */ 'use client'; /** * Spool Live Updates Hook - Convex Version * This provides real-time content updates for customer Next.js apps */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.useSpoolLiveUpdates = useSpoolLiveUpdates; exports.SpoolLiveUpdatesProvider = SpoolLiveUpdatesProvider; const react_1 = __importStar(require("react")); const react_2 = require("convex/react"); // Spool's Convex deployment URL (hardcoded - customers don't need to configure this) const SPOOL_CONVEX_URL = 'https://sincere-hyena-934.convex.cloud'; // Global Convex client instance let convexClient = null; function getConvexClient() { if (!convexClient) { convexClient = new react_2.ConvexReactClient(SPOOL_CONVEX_URL); // Ensure the client connects immediately for real-time updates if (process.env.NODE_ENV === 'development') { console.log('[DEV] 🔌 Initializing Convex client connection to:', SPOOL_CONVEX_URL); } } return convexClient; } /** * Hook for subscribing to Spool live updates * This is what customers will use in their Next.js apps */ function useSpoolLiveUpdates(config = {}) { const [isConnected, setIsConnected] = (0, react_1.useState)(false); const [error, setError] = (0, react_1.useState)(null); const onUpdateRef = (0, react_1.useRef)(config.onUpdate); // Auto-detect credentials from environment const apiKey = config.apiKey || (typeof window !== 'undefined' ? window.__NEXT_DATA__?.props?.pageProps?.env?.NEXT_PUBLIC_SPOOL_API_KEY || process.env.NEXT_PUBLIC_SPOOL_API_KEY : null); const siteId = config.siteId || (typeof window !== 'undefined' ? window.__NEXT_DATA__?.props?.pageProps?.env?.NEXT_PUBLIC_SPOOL_SITE_ID || process.env.NEXT_PUBLIC_SPOOL_SITE_ID : null); // Update the callback ref when it changes (0, react_1.useEffect)(() => { onUpdateRef.current = config.onUpdate; }, [config.onUpdate]); // Subscribe to live updates via Convex // Note: Function name format for external deployment access const updates = (0, react_2.useQuery)('liveUpdates:subscribe', config.enabled !== false && apiKey && siteId ? { siteId, apiKey, limit: 10, } : 'skip'); // Debug: Track every time useQuery result changes (0, react_1.useEffect)(() => { if (process.env.NODE_ENV === 'development') { console.log('[DEV] 🔄 useQuery result changed. Latest update ID:', updates?.[0]?._id); console.log('[DEV] 🔄 useQuery result changed. Total updates:', updates?.length); console.log('[DEV] 🔄 useQuery result changed. Full updates array:', updates); } }, [updates]); // Handle connection state (0, react_1.useEffect)(() => { if (updates !== undefined) { setIsConnected(true); setError(null); if (process.env.NODE_ENV === 'development') { console.log(`[DEV] ✅ Connected to Spool Realtime for site: ${siteId}`); console.log(`[DEV] 📊 Updates received:`, updates); console.log(`[DEV] 🔄 useQuery fired - latest update ID:`, updates[0]?._id); console.log(`[DEV] 🔄 useQuery fired - latest timestamp:`, updates[0]?.timestamp); } } }, [updates, siteId]); // Track the latest update ID we've processed (per hook instance) const [lastProcessedId, setLastProcessedId] = (0, react_1.useState)(null); // Handle updates - process only new ones based on update ID (0, react_1.useEffect)(() => { if (!updates || updates.length === 0) return; // Get the most recent update (first in the array, since Convex returns newest first) const latestUpdate = updates[0]; // Only process if this is a different update than we've seen before if (latestUpdate && latestUpdate._id !== lastProcessedId) { // Update the last processed ID setLastProcessedId(latestUpdate._id); if (process.env.NODE_ENV === 'development') { console.log(`[DEV] 🔄 Live update: ${latestUpdate.collection}/${latestUpdate.slug || 'no-slug'}`); console.log(`[DEV] 📅 Update timestamp: ${latestUpdate.timestamp}`); console.log(`[DEV] 🎯 Update ID: ${latestUpdate._id}`); console.log(`[DEV] 🚨 About to call user's onUpdate callback...`); } // Call user's callback with the latest update if (onUpdateRef.current) { onUpdateRef.current(latestUpdate); if (process.env.NODE_ENV === 'development') { console.log(`[DEV] ✅ User callback executed. If page doesn't update, it's a Next.js caching issue.`); } } } }, [updates, lastProcessedId]); // Handle errors and debug info (0, react_1.useEffect)(() => { // Debug logging for connection issues if (process.env.NODE_ENV === 'development') { console.log(`[DEV] 🔍 Debug info:`, { apiKey: apiKey ? `${apiKey.substring(0, 10)}...` : 'missing', siteId: siteId || 'missing', enabled: config.enabled !== false, updatesState: updates === undefined ? 'undefined' : Array.isArray(updates) ? `array(${updates.length})` : typeof updates }); } // Convex will throw errors if authentication fails // We can catch them here and provide user-friendly messages if (updates === undefined && config.enabled !== false && apiKey && siteId) { // Still loading, not an error yet return; } }, [updates, config.enabled, apiKey, siteId]); return { isConnected, error, updates: updates || [], latestUpdate: updates?.[0] || null, }; } /** * Automatic revalidation logic * This handles cache invalidation when content updates */ async function handleAutomaticRevalidation(update) { if (typeof window !== 'undefined') { // Client-side: trigger router refresh if ('router' in window && typeof window.router?.refresh === 'function') { window.router.refresh(); } return; } // Server-side: trigger revalidation via HTTP try { const baseUrl = getAppBaseUrl(); const pathsToRevalidate = generateRevalidationPaths(update); // Wait 2 seconds for API propagation await new Promise(resolve => setTimeout(resolve, 2000)); const revalidationPromises = pathsToRevalidate.map(async (path) => { try { const response = await fetch(`${baseUrl}/api/revalidate?path=${encodeURIComponent(path)}`, { method: 'POST', headers: { 'Cache-Control': 'no-cache', }, signal: AbortSignal.timeout(5000), }); if (response.ok) { console.log(`[DEV] ✅ Revalidated: ${path}`); } else { console.log(`[DEV] ❌ Revalidation failed for ${path}: ${response.status}`); } } catch (err) { console.log(`[DEV] ❌ Revalidation error for ${path}:`, err instanceof Error ? err.message : String(err)); } }); await Promise.allSettled(revalidationPromises); } catch (error) { console.error('[DEV] Error in automatic revalidation:', error); } } /** * Generate paths that need revalidation based on the update */ function generateRevalidationPaths(update) { const paths = []; // Always revalidate root paths.push('/'); // Collection-specific paths if (update.collection === 'blog') { paths.push('/blog'); if (update.slug) { paths.push(`/blog/${update.slug}`); } } else { paths.push(`/${update.collection}`); if (update.slug) { paths.push(`/${update.collection}/${update.slug}`); } } // Common paths paths.push('/sitemap.xml'); return paths; } /** * Detect the current app URL */ function getAppBaseUrl() { if (process.env.NODE_ENV === 'development') { const port = process.env.PORT || process.env.NEXT_PUBLIC_PORT || '3000'; return `http://localhost:${port}`; } return process.env.NEXT_PUBLIC_SITE_URL || process.env.NEXT_PUBLIC_APP_URL || (process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : 'http://localhost:3000'); } /** * Provider component that automatically sets up Convex connection to Spool's infrastructure * Customers just need to wrap their app with this - no additional configuration required! * * Usage: * import { SpoolLiveUpdatesProvider } from '@spoolcms/nextjs'; * * <SpoolLiveUpdatesProvider> * <YourApp /> * </SpoolLiveUpdatesProvider> */ function SpoolLiveUpdatesProvider({ children }) { const convexClient = getConvexClient(); // Try to import ConvexProvider synchronously first let ConvexProvider = null; try { const convexReact = require('convex/react'); ConvexProvider = convexReact.ConvexProvider; } catch (error) { // Convex not installed - provide fallback console.warn('[SpoolLiveUpdates] Convex not installed. Live updates disabled. Install with: npm install convex'); return children; } if (!ConvexProvider) { console.warn('[SpoolLiveUpdates] ConvexProvider not available. Live updates disabled.'); return children; } // Wrap with ConvexProvider using Spool's deployment return react_1.default.createElement(ConvexProvider, { client: convexClient }, children); }