// Store KidLisp, 2025.01.16 // Caches KidLisp source code and generates short URLs for QR codes import { authorize, getHandleOrEmail } from "../../backend/authorization.mjs"; import { connect } from "../../backend/database.mjs"; import { respond } from "../../backend/http.mjs"; import { generateUniqueCode } from "../../backend/generate-short-code.mjs"; import { createMediaRecord, MediaTypes } from "../../backend/media-atproto.mjs"; import { publishProfileEvent } from "../../backend/profile-stream.mjs"; import { kidlispDatomicEnabled } from "../../backend/kidlisp-sidecar.mjs"; import { handler as datomicHandler } from "./store-kidlisp-datomic.mjs"; import crypto from 'crypto'; // Feature flag for Tezos integration (disabled by default until integration file exists) const TEZOS_ENABLED = process.env.TEZOS_ENABLED === 'true'; const NO_CACHE_HEADERS = { "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0", "Pragma": "no-cache", "Expires": "0", }; // Dynamically extract KidLisp function names from kidlisp.mjs async function getKidLispFunctionNames() { try { // Import the kidlisp module const kidlispModule = await import('../../public/aesthetic.computer/lib/kidlisp.mjs'); // Create a temporary KidLisp instance to access the global environment const { KidLisp } = kidlispModule; if (!KidLisp) { throw new Error('KidLisp class not found in kidlisp.mjs'); } const tempLisp = new KidLisp(); const globalEnv = tempLisp.getGlobalEnv(); // Extract all function names from the global environment const functionNames = Object.keys(globalEnv).filter(key => { const value = globalEnv[key]; return typeof value === 'function' || (typeof value === 'object' && value !== null); }); // Add common colors that might appear in KidLisp code const commonColors = ['red', 'green', 'blue', 'yellow', 'white', 'black', 'gray', 'purple', 'orange', 'pink', 'brown', 'cyan', 'magenta']; return [...new Set([...functionNames, ...commonColors])]; } catch (error) { console.warn('Failed to dynamically load KidLisp functions, using fallback list:', error.message); // Fallback to a basic list if dynamic loading fails return ['wipe', 'ink', 'line', 'box', 'circle', 'rect', 'def', 'later', 'scroll', 'resolution', 'gap', 'frame', 'brush', 'clear', 'cls', 'help', 'reset', 'dot', 'pixel', 'stamp', 'paste', 'copy', 'move', 'rotate', 'scale', 'translate', 'fill', 'stroke', 'point', 'arc', 'bezier', 'noise', 'random', 'sin', 'cos', 'tan', 'sqrt', 'abs', 'floor', 'ceil', 'round', 'min', 'max', 'pow', 'log', 'exp', 'atan2', 'dist', 'lerp', 'map', 'norm', 'constrain', 'hue', 'sat', 'bright', 'alpha', 'red', 'green', 'blue', 'rgb', 'hsb', 'gray', 'background', 'foreground', 'text', 'font', 'repeat', 'rep', 'choose', 'overtone', 'rainbow', 'mic', 'amplitude']; } } // Ensure indexes exist (reentrant - safe to call multiple times) async function ensureIndexes(collection) { try { // Create unique index on code field await collection.createIndex({ code: 1 }, { unique: true, background: true, name: 'kidlisp_code_unique' }); // Create unique index on hash field for deduplication. // PARTIAL on string hashes only: rows without a real hash (e.g. Datomic-only // pieces upserted by keep-confirm) must NOT all collide on { hash: null }. const hashIndexOptions = { unique: true, background: true, name: 'kidlisp_hash_unique', partialFilterExpression: { hash: { $type: 'string' } }, }; try { await collection.createIndex({ hash: 1 }, hashIndexOptions); } catch (err) { // A pre-existing plain-unique index has different options — migrate it. const conflict = err?.code === 85 || err?.code === 86 || err?.codeName === 'IndexOptionsConflict' || err?.codeName === 'IndexKeySpecsConflict'; if (conflict) { console.warn('♻️ Migrating kidlisp_hash_unique → partial unique index…'); await collection.dropIndex('kidlisp_hash_unique'); await collection.createIndex({ hash: 1 }, hashIndexOptions); } else { throw err; } } // Create index on when field for analytics/cleanup queries await collection.createIndex({ when: 1 }, { background: true, name: 'kidlisp_when' }); // Create index on user field for user-specific queries await collection.createIndex({ user: 1 }, { background: true, sparse: true, // Only index documents that have the user field name: 'kidlisp_user' }); // Create index on kept.network for filtering kept pieces by network await collection.createIndex({ "kept.network": 1 }, { background: true, sparse: true, // Only index documents that have the kept field name: 'kidlisp_kept_network' }); // Create indexes for active contract profile/version filtering await collection.createIndex({ "kept.contractVersion": 1 }, { background: true, sparse: true, name: "kidlisp_kept_contract_version", }); await collection.createIndex({ "kept.contractProfile": 1 }, { background: true, sparse: true, name: "kidlisp_kept_contract_profile", }); console.log('📦 Kidlisp indexes ensured'); } catch (error) { // Ignore index creation errors (they're likely already created) console.warn('Index creation warning (likely already exist):', error.message); } } function normalizeTokenId(value) { if (value === null || typeof value === "undefined" || value === "") return null; const parsed = Number(value); if (!Number.isInteger(parsed) || parsed < 0) return null; return parsed; } function normalizeAddress(value) { return typeof value === "string" ? value.trim().toLowerCase() : ""; } function normalizeContractProfile(value) { if (typeof value !== "string") return null; const trimmed = value.trim().toLowerCase(); return trimmed || null; } function normalizeContractVersion(value) { if (typeof value !== "string") return null; const trimmed = value.trim(); return trimmed || null; } function toEpochMs(value) { if (!value) return 0; const ms = new Date(value).getTime(); return Number.isFinite(ms) ? ms : 0; } function normalizeKeepRecord(raw = {}, defaults = {}) { const tokenId = normalizeTokenId(raw.tokenId); if (tokenId === null) return null; const contractAddress = raw.contractAddress || defaults.contractAddress || null; if (!contractAddress) return null; return { tokenId, network: raw.network || defaults.network || "mainnet", txHash: raw.txHash || defaults.txHash || null, contractAddress, contractProfile: normalizeContractProfile( raw.contractProfile || raw.profile || defaults.contractProfile || defaults.profile ), contractVersion: normalizeContractVersion( raw.contractVersion || raw.version || defaults.contractVersion || defaults.version ), keptAt: raw.keptAt || raw.mintedAt || defaults.keptAt || null, keptBy: raw.keptBy || defaults.keptBy || null, walletAddress: raw.walletAddress || raw.owner || defaults.walletAddress || null, artifactUri: raw.artifactUri || defaults.artifactUri || null, thumbnailUri: raw.thumbnailUri || defaults.thumbnailUri || null, metadataUri: raw.metadataUri || defaults.metadataUri || null, source: defaults.source || "unknown", }; } function selectPrimaryKeepRecord(records = [], preferredContract = null) { if (!Array.isArray(records) || records.length === 0) return null; const normalizedPreferred = normalizeAddress(preferredContract); if (!normalizedPreferred) return records[0]; const preferred = records.find((record) => normalizeAddress(record?.contractAddress) === normalizedPreferred ); return preferred || records[0]; } function filterKeepRecords(records = [], options = {}) { if (!Array.isArray(records) || records.length === 0) return []; const normalizedContract = normalizeAddress(options.contract || null); const normalizedProfile = normalizeContractProfile(options.contractProfile || null); const normalizedVersion = normalizeContractVersion(options.contractVersion || null); return records.filter((record) => { const recordContract = normalizeAddress(record?.contractAddress); const recordProfile = normalizeContractProfile(record?.contractProfile); const recordVersion = normalizeContractVersion(record?.contractVersion); if (normalizedContract && normalizeAddress(record?.contractAddress) !== normalizedContract) { return false; } if (normalizedProfile) { if (recordProfile && recordProfile !== normalizedProfile) return false; if (!recordProfile && (!normalizedContract || recordContract !== normalizedContract)) return false; } if (normalizedVersion) { if (recordVersion && recordVersion !== normalizedVersion) return false; if (!recordVersion && (!normalizedContract || recordContract !== normalizedContract)) return false; } return true; }); } function extractKeepRecords(doc = {}) { const records = []; const push = (record) => { if (!record) return; records.push(record); }; if (doc.kept && typeof doc.kept === "object") { push( normalizeKeepRecord(doc.kept, { source: "kept", }) ); } if (doc.tezos?.minted) { push( normalizeKeepRecord(doc.tezos, { source: "legacy_tezos", contractAddress: doc.tezos.contractAddress || doc.tezos.contract || null, keptAt: doc.tezos.mintedAt || null, }) ); } const contractRecords = doc.tezos?.contracts; if (contractRecords && typeof contractRecords === "object" && !Array.isArray(contractRecords)) { for (const [contractAddress, value] of Object.entries(contractRecords)) { if (!value || typeof value !== "object") continue; push( normalizeKeepRecord(value, { source: "contract_keyed", contractAddress, }) ); } } const deduped = new Map(); for (const record of records) { const key = `${record.contractAddress || ""}:${record.tokenId}:${record.network || ""}`; const existing = deduped.get(key); if (!existing || toEpochMs(record.keptAt) > toEpochMs(existing.keptAt)) { deduped.set(key, record); } } return Array.from(deduped.values()).sort((a, b) => { const delta = toEpochMs(b.keptAt) - toEpochMs(a.keptAt); if (delta !== 0) return delta; return b.tokenId - a.tokenId; }); } export async function handler(event, context) { // Feature flag: route to the Datomic-backed implementation when on. // Default off — existing Mongo behavior runs unchanged. if (kidlispDatomicEnabled()) { return datomicHandler(event, context); } // Log all incoming requests for debugging console.log(`📥 Kidlisp store request: ${event.httpMethod} ${event.path || event.rawUrl || 'unknown'}`); console.log(`📊 Headers:`, Object.keys(event.headers || {}).length > 0 ? Object.keys(event.headers) : 'none'); if (event.body) { console.log(`📦 Body length: ${event.body.length} characters`); } // Log environment variables (without sensitive data) console.log(`🔧 Environment check:`, { mongoDbConfigured: !!process.env.MONGODB_CONNECTION_STRING, mongoDbNameConfigured: !!process.env.MONGODB_NAME, tezosEnabled: process.env.TEZOS_ENABLED !== 'false' }); if (event.httpMethod === 'OPTIONS') { console.log(`✅ Handling OPTIONS preflight request`); return respond(204, ''); } let database; try { database = await connect(); } catch (connectError) { console.error('❌ MongoDB connection failed:', connectError.message); // Return a graceful error response instead of crashing return respond(503, { error: 'Database temporarily unavailable' }); } try { const collection = database.db.collection('kidlisp'); // Ensure indexes exist (safe to call multiple times) await ensureIndexes(collection); if (event.httpMethod === 'POST') { const { source } = JSON.parse(event.body || '{}'); if (!source || typeof source !== 'string' || source.length > 50000) { return respond(400, { error: 'Invalid source' }); } // Extract user from authorization (optional, with timeout) let user; try { // Add timeout to prevent Auth0 issues from slowing down caching const authPromise = authorize(event.headers); const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Auth timeout')), 3000) ); user = await Promise.race([authPromise, timeoutPromise]); console.log(`👤 User authorized: ${user ? user.sub : 'none'}`); } catch (error) { console.log(`🔓 No user authorization (anonymous cache): ${error.message}`); } let profileHandle = null; if (user?.sub) { try { const handleOrEmail = await getHandleOrEmail(user.sub); if (typeof handleOrEmail === "string" && handleOrEmail.startsWith("@")) { profileHandle = handleOrEmail; } } catch (err) { console.warn("⚠️ Could not resolve profile handle for kidlisp event:", err?.message || err); } } const hash = crypto.createHash('sha256').update(source.trim()).digest('hex'); console.log(`🔍 Source hash: ${hash.substring(0, 16)}...`); // Check for existing by hash first const existing = await collection.findOne({ hash }); if (existing) { // Paranoid collision check: verify source actually matches if (existing.source === source.trim()) { await collection.updateOne( { hash }, { $inc: { hits: 1 }, $set: { lastAccessed: new Date() } } ); console.log(`♻️ Found existing cache: ${existing.code}`); await database.disconnect(); return respond(200, { code: existing.code, cached: true }); } else { // Extremely rare: hash collision detected! console.error('💥 SHA-256 collision detected!', { hash, existing: existing.source, new: source.trim() }); // Continue to create new entry with different code } } // Generate unique code using shared module with intelligent inference const code = await generateUniqueCode(collection, { mode: 'inferred', sourceText: source, type: 'kidlisp' }); console.log(`✨ Generated code: ${code}`); // Create document with optional user attribution (matching painting structure) const doc = { code, source: source.trim(), hash, when: new Date(), lastAccessed: new Date(), hits: 1 }; // Add user sub if authenticated (same as painting records) if (user?.sub) { doc.user = user.sub; console.log(`🔗 Linked to user: ${user.sub}`); } try { const insertResult = await collection.insertOne(doc); console.log(`💾 Cached new source: ${code}`); if (profileHandle) { publishProfileEvent({ handle: profileHandle, event: { type: "kidlisp", when: Date.now(), label: `KidLisp $${code}`, ref: code, }, countsDelta: { kidlisp: 1 }, }).catch((err) => { console.warn("⚠️ kidlisp profile-event publish failed:", err?.message || err); }); } // Sync to ATProto in background (don't wait for it) const kidlispId = insertResult.insertedId; const savedRecord = await collection.findOne({ _id: kidlispId }); if (savedRecord) { createMediaRecord(database, MediaTypes.KIDLISP, savedRecord, { userSub: user?.sub }) .then(result => { if (result.error) { console.error(`⚠️ ATProto sync failed: ${result.error}`); } else { console.log(`✅ Synced kidlisp to ATProto: ${result.rkey}`); // Update MongoDB with rkey (fire and forget) collection.updateOne( { _id: kidlispId }, { $set: { "atproto.rkey": result.rkey } } ).catch(err => console.error(`⚠️ Failed to update rkey: ${err.message}`)); } }) .catch(err => console.error(`⚠️ ATProto sync error: ${err.message}`)); } // 🪙 Tezos Integration: Attempt to mint KidLisp meme coin let tezosResult = null; if (TEZOS_ENABLED && user?.sub) { try { // Import Tezos integration (only when needed) const { integrateWithKidLispCache } = await import('../../../tezos/src/integration.js'); console.log(`🪙 Attempting Tezos token mint for user: ${user.sub}`); tezosResult = await integrateWithKidLispCache(source.trim(), user, code); if (tezosResult.minted) { console.log(`✨ Tezos token minted successfully: Token ID ${tezosResult.tokenId} on ${tezosResult.network}`); // Update the document with Tezos information await collection.updateOne( { code }, { $set: { tezos: { minted: true, tokenId: tezosResult.tokenId, txHash: tezosResult.txHash, creatorAddress: tezosResult.creatorAddress, codeHash: tezosResult.codeHash, network: tezosResult.network, mintedAt: new Date() } } } ); } else if (tezosResult.exists) { console.log(`🎯 Tezos token already exists: Token ID ${tezosResult.tokenId} on ${tezosResult.network}`); // Update with existing token info await collection.updateOne( { code }, { $set: { tezos: { minted: false, exists: true, tokenId: tezosResult.tokenId, codeHash: tezosResult.codeHash, network: tezosResult.network, reason: tezosResult.reason, checkedAt: new Date() } } } ); } else { console.log(`⚠️ Tezos token operation skipped: ${tezosResult.reason}`); // Store the reason for debugging await collection.updateOne( { code }, { $set: { tezos: { minted: false, exists: false, reason: tezosResult.reason, error: tezosResult.error, attemptedAt: new Date() } } } ); } } catch (tezosError) { console.error('🚨 Tezos integration error:', tezosError); // Store error information for debugging await collection.updateOne( { code }, { $set: { tezos: { minted: false, error: tezosError.message, failedAt: new Date() } } } ); } } else if (!TEZOS_ENABLED) { console.log(`🚫 Tezos integration disabled by feature flag`); } await database.disconnect(); // Include Tezos information in response const responseData = { code, cached: false, ...(tezosResult && { tezos: tezosResult }) }; return respond(201, responseData); } catch (insertError) { // Handle duplicate key errors (race conditions) if (insertError.code === 11000) { if (insertError.keyPattern?.hash) { // Hash collision - return existing record const existing = await collection.findOne({ hash }); if (existing) { await collection.updateOne( { hash }, { $inc: { hits: 1 }, $set: { lastAccessed: new Date() } } ); console.log(`🔄 Race condition resolved: ${existing.code}`); await database.disconnect(); return respond(200, { code: existing.code, cached: true }); } } else if (insertError.keyPattern?.code) { await database.disconnect(); return respond(500, { error: 'Code generation collision, please retry' }); } } throw insertError; } } else if (event.httpMethod === 'GET') { const code = event.queryStringParameters?.code; const codes = event.queryStringParameters?.codes; const recent = event.queryStringParameters?.recent; const stats = event.queryStringParameters?.stats; // Handle function usage statistics across all pieces if (stats === 'functions') { console.log(`📊 Function stats request`); // Fetch top pieces sorted by hits (covers most real usage) const scanLimit = parseInt(event.queryStringParameters?.limit) || 5000; const docs = await collection.find( {}, { projection: { source: 1, hits: 1, _id: 0 } } ).sort({ hits: -1 }).limit(scanLimit).toArray(); // Parse function calls from source code const rawCounts = {}; // unweighted: each piece counts once const weightedCounts = {}; // weighted by piece hits let totalHits = 0; const funcPattern = /\(\s*([a-zA-Z_+\-*/%?][a-zA-Z0-9_]*)/g; // Known bare-word commands (functions that work without parens) const bareCommands = new Set([ 'wipe', 'ink', 'line', 'box', 'circle', 'plot', 'point', 'flood', 'scroll', 'spin', 'zoom', 'blur', 'contrast', 'suck', 'sort', 'bake', 'fill', 'outline', 'stroke', 'nofill', 'nostroke', 'resolution', 'mask', 'unmask', 'steal', 'putback', 'rainbow', 'zebra', 'noise', 'unpan', 'resetSpin', ]); // Known CSS color names used as bare wipe commands const bareColors = new Set([ 'red', 'green', 'blue', 'yellow', 'orange', 'purple', 'pink', 'cyan', 'magenta', 'black', 'white', 'gray', 'grey', 'brown', 'lime', 'navy', 'teal', 'olive', 'maroon', 'aqua', 'fuchsia', 'silver', 'gold', 'coral', 'salmon', 'khaki', 'indigo', 'violet', 'turquoise', 'tomato', 'crimson', 'lavender', 'beige', 'plum', 'orchid', 'tan', 'chocolate', 'sienna', 'peru', 'wheat', 'deepskyblue', 'hotpink', 'springgreen', 'darkslategray', ]); for (const doc of docs) { const src = doc.source || ''; const hits = doc.hits || 1; totalHits += hits; const seenInPiece = new Set(); // track unique functions per piece // Extract parenthesized function calls let match; funcPattern.lastIndex = 0; while ((match = funcPattern.exec(src)) !== null) { const fn = match[1]; seenInPiece.add(fn); } // Detect bare-word commands (words at start of line or after comma) const tokens = src.split(/[,\n]/).map(t => t.trim().split(/\s+/)[0]); for (const token of tokens) { if (bareCommands.has(token)) seenInPiece.add(token); if (bareColors.has(token)) seenInPiece.add('wipe'); // bare color = implicit wipe } // Detect embedded piece references ($codeId) if (/\$[a-zA-Z0-9]+/.test(src)) seenInPiece.add('embed'); // Detect timing expressions if (/\d+\.?\d*s[.!]?/.test(src)) seenInPiece.add('timing'); // Detect fade gradient syntax if (/fade:/.test(src)) seenInPiece.add('fade'); // Aggregate for (const fn of seenInPiece) { rawCounts[fn] = (rawCounts[fn] || 0) + 1; weightedCounts[fn] = (weightedCounts[fn] || 0) + hits; } } // Sort by weighted count descending const sorted = Object.entries(weightedCounts) .sort((a, b) => b[1] - a[1]) .map(([name, weighted]) => ({ name, pieces: rawCounts[name] || 0, weighted, })); await database.disconnect(); return respond(200, { functions: sorted, total_pieces: docs.length, total_hits: totalHits, }); } const requestedContract = event.queryStringParameters?.contract || null; const requestedContractVersion = normalizeContractVersion(event.queryStringParameters?.contractVersion || null); const requestedContractProfile = normalizeContractProfile(event.queryStringParameters?.contractProfile || null); // Handle recent codes feed (for $.mjs piece) if (recent) { const limit = parseInt(event.queryStringParameters?.limit) || 50; const maxLimit = 100000; // Very high limit for comprehensive searches const actualLimit = Math.min(limit, maxLimit); const sortBy = event.queryStringParameters?.sort || 'recent'; // 'recent' or 'hits' const filterHandle = event.queryStringParameters?.handle; // Optional handle filter const since = event.queryStringParameters?.since; // ISO timestamp — only return entries newer than this console.log(`📊 Codes request: limit=${actualLimit}, sort=${sortBy}, handle=${filterHandle || 'all'}${since ? `, since=${since}` : ''}`); // Mirror tv.mjs/fetchKidlisp's working pattern: bare $arrayElemAt // (no $cond/$concat), upstream $match, $sort+$limit before $lookup. // Earlier $cond/$concat variant returned handle=null for every doc // even when @handles had a matching entry — empirically reproducible // against the same data the batch endpoint resolves correctly. const handleLookupStages = [ { $lookup: { from: "@handles", localField: "user", foreignField: "_id", as: "handleInfo" } }, { $addFields: { handleRaw: { $arrayElemAt: ["$handleInfo.handle", 0] } } } ]; const pipeline = [ { $match: { code: { $exists: true } } } ]; if (since) { const sinceDate = new Date(since); if (!isNaN(sinceDate.getTime())) { pipeline.push({ $match: { when: { $gt: sinceDate } } }); } } pipeline.push({ $sort: sortBy === 'hits' ? { hits: -1, when: -1 } : { when: -1 } }); if (filterHandle) { // Filter is on the computed handle, so the lookup has to precede $limit. pipeline.push(...handleLookupStages); const bareHandle = filterHandle.startsWith('@') ? filterHandle.slice(1) : filterHandle; pipeline.push({ $match: { handleRaw: bareHandle } }); pipeline.push({ $limit: actualLimit }); } else { pipeline.push({ $limit: actualLimit }); pipeline.push(...handleLookupStages); } pipeline.push({ $project: { _id: 0, code: 1, source: 1, when: 1, hits: 1, user: 1, kept: 1, tezos: 1, pendingRebake: 1, handleRaw: 1 } }); const docs = await collection.aggregate(pipeline, { allowDiskUse: true }).toArray(); // Create preview versions of source code (truncate long sources) const recentCodes = docs.map(doc => { const result = { code: doc.code, source: doc.source, preview: doc.source.length > 40 ? doc.source.substring(0, 37) + "..." : doc.source, when: doc.when, hits: doc.hits, user: doc.user || null, handle: doc.handleRaw ? `@${doc.handleRaw}` : null }; const keepRecords = filterKeepRecords(extractKeepRecords(doc), { contract: requestedContract, contractProfile: requestedContractProfile, contractVersion: requestedContractVersion, }); if (keepRecords.length > 0) { result.kept = selectPrimaryKeepRecord(keepRecords, requestedContract); if (keepRecords.length > 1) { result.keptRecords = keepRecords; } } if (doc.pendingRebake && typeof doc.pendingRebake === "object") { const pendingContract = normalizeAddress(doc.pendingRebake.contractAddress || ""); const contractMatches = !requestedContract || pendingContract === normalizeAddress(requestedContract); const pendingProfile = normalizeContractProfile(doc.pendingRebake.contractProfile); const pendingVersion = normalizeContractVersion(doc.pendingRebake.contractVersion); const profileMatches = !requestedContractProfile || (pendingProfile ? pendingProfile === requestedContractProfile : contractMatches); const versionMatches = !requestedContractVersion || (pendingVersion ? pendingVersion === requestedContractVersion : contractMatches); if (contractMatches && profileMatches && versionMatches) { result.pendingRebake = doc.pendingRebake; } } return result; }); console.log(`📤 Recent codes retrieved: ${recentCodes.length} codes`); await database.disconnect(); return respond(200, { recent: recentCodes, count: recentCodes.length, limit: actualLimit }, NO_CACHE_HEADERS); } // Handle batch retrieval of multiple codes if (codes) { console.log(`🔍 Batch lookup request for codes parameter: ${codes}`); let codeList; try { // Support both comma-separated string and JSON array format if (codes.startsWith('[')) { codeList = JSON.parse(codes); } else { codeList = codes.split(',').map(c => c.trim()).filter(c => c.length > 0); } } catch (error) { return respond(400, { error: 'Invalid codes format. Use comma-separated or JSON array.' }); } if (!Array.isArray(codeList) || codeList.length === 0) { return respond(400, { error: 'Codes must be a non-empty array' }); } if (codeList.length > 50) { // Limit batch size return respond(400, { error: 'Too many codes. Maximum 50 per request.' }); } console.log(`🔍 Looking up ${codeList.length} codes: ${codeList.join(', ')}`); // Fetch all documents with handle lookup (aggregation pipeline) const docs = await collection.aggregate([ { $match: { code: { $in: codeList } } }, { $lookup: { from: "@handles", localField: "user", foreignField: "_id", as: "handleInfo" } }, { $addFields: { handle: { $cond: { if: { $gt: [{ $size: "$handleInfo" }, 0] }, then: { $concat: ["@", { $arrayElemAt: ["$handleInfo.handle", 0] }] }, else: null } } } } ]).toArray(); // Update hit counts for found documents if (docs.length > 0) { const foundCodes = docs.map(doc => doc.code); await collection.updateMany( { code: { $in: foundCodes } }, { $inc: { hits: 1 }, $set: { lastAccessed: new Date() } } ); } // Create response map with found and missing codes const results = {}; const found = []; const missing = []; codeList.forEach(requestedCode => { const doc = docs.find(d => d.code === requestedCode); if (doc) { const result = { source: doc.source, when: doc.when, hits: doc.hits + 1, user: doc.user || null, handle: doc.handle || null }; const keepRecords = filterKeepRecords(extractKeepRecords(doc), { contract: requestedContract, contractProfile: requestedContractProfile, contractVersion: requestedContractVersion, }); if (keepRecords.length > 0) { result.kept = selectPrimaryKeepRecord(keepRecords, requestedContract); if (keepRecords.length > 1) { result.keptRecords = keepRecords; } } results[requestedCode] = result; found.push(requestedCode); } else { results[requestedCode] = null; missing.push(requestedCode); } }); console.log(`📤 Batch retrieved: ${found.length} found, ${missing.length} missing`); if (found.length > 0) console.log(`✅ Found: ${found.join(', ')}`); if (missing.length > 0) console.log(`❌ Missing: ${missing.join(', ')}`); await database.disconnect(); return respond(200, { results, summary: { requested: codeList.length, found: found.length, missing: missing.length, foundCodes: found, missingCodes: missing } }, NO_CACHE_HEADERS); } // Handle single code retrieval (existing functionality) if (!code) { return respond(400, { error: 'Code or codes parameter required' }); } console.log(`🔍 Looking up code: ${code}`); // Use aggregation pipeline to fetch doc with handle lookup const pipeline = [ { $match: { code } }, { $lookup: { from: "@handles", localField: "user", foreignField: "_id", as: "handleInfo" } }, { $addFields: { handle: { $cond: { if: { $gt: [{ $size: "$handleInfo" }, 0] }, then: { $concat: ["@", { $arrayElemAt: ["$handleInfo.handle", 0] }] }, else: null } } } }, { $limit: 1 } ]; const docs = await collection.aggregate(pipeline).toArray(); const doc = docs[0]; if (!doc) { await database.disconnect(); return respond(404, { error: 'Not found' }); } await collection.updateOne( { code }, { $inc: { hits: 1 }, $set: { lastAccessed: new Date() } } ); console.log(`📤 Retrieved source: ${code} (${doc.source.length} chars, ${doc.hits + 1} hits)`); await database.disconnect(); // Build response with kept status if present const response = { source: doc.source, when: doc.when, hits: doc.hits + 1, user: doc.user || null, handle: doc.handle || null, }; // Include cached IPFS media if present (from bundle generation) if (doc.ipfsMedia) { response.ipfsMedia = { artifactUri: doc.ipfsMedia.artifactUri, thumbnailUri: doc.ipfsMedia.thumbnailUri, sourceHash: doc.ipfsMedia.sourceHash, createdAt: doc.ipfsMedia.createdAt, authorHandle: doc.ipfsMedia.authorHandle, depCount: doc.ipfsMedia.depCount, packDate: doc.ipfsMedia.packDate, }; } const keepRecords = filterKeepRecords(extractKeepRecords(doc), { contract: requestedContract, contractProfile: requestedContractProfile, contractVersion: requestedContractVersion, }); if (keepRecords.length > 0) { response.kept = selectPrimaryKeepRecord(keepRecords, requestedContract); if (keepRecords.length > 1) { response.keptRecords = keepRecords; } } // Include pending rebake info if present (rebaked but not yet updated on chain) if (doc.pendingRebake && typeof doc.pendingRebake === "object") { const pendingContract = normalizeAddress(doc.pendingRebake.contractAddress || ""); const contractMatches = !requestedContract || pendingContract === normalizeAddress(requestedContract); const pendingProfile = normalizeContractProfile(doc.pendingRebake.contractProfile); const pendingVersion = normalizeContractVersion(doc.pendingRebake.contractVersion); const profileMatches = !requestedContractProfile || (pendingProfile ? pendingProfile === requestedContractProfile : contractMatches); const versionMatches = !requestedContractVersion || (pendingVersion ? pendingVersion === requestedContractVersion : contractMatches); if (contractMatches && profileMatches && versionMatches) { response.pendingRebake = { artifactUri: doc.pendingRebake.artifactUri, thumbnailUri: doc.pendingRebake.thumbnailUri, metadataUri: doc.pendingRebake.metadataUri || null, createdAt: doc.pendingRebake.createdAt, contractAddress: doc.pendingRebake.contractAddress || null, contractProfile: doc.pendingRebake.contractProfile || null, contractVersion: doc.pendingRebake.contractVersion || null, }; } } return respond(200, response, NO_CACHE_HEADERS); } await database.disconnect(); return respond(405, { error: 'Method not allowed' }); } catch (error) { console.error('❌ Kidlisp cache error:', error); console.error('❌ Error details:', { message: error.message, stack: error.stack, name: error.name }); return respond(500, { error: 'Internal server error', details: error.message }); } }