/** Select typed content resources; never retain user/session envelopes or raw responses. */ import {normalizeBubbleRef} from './bubble-corpus-utils.mjs'; export const RESOURCE_TYPES = new Set(['custom.scrap', 'custom.relattachment', 'custom.attachment']); const validId = value => typeof value === 'string' && /^[0-9]+x[0-9]+$/.test(value); export function resourceParents(articles) { return Object.entries(articles).filter(([,a]) => a.hasattachments_boolean === true || (Object.hasOwn(a, '_list_custom_scrap') && !(Array.isArray(a._list_custom_scrap) && a._list_custom_scrap.length === 0))) .map(([id]) => normalizeBubbleRef(id)).filter(validId).sort(); } export function ingestResourceBody(text, output) { const visit = obj => { if (Array.isArray(obj)) { obj.forEach(visit); return; } if (!obj || typeof obj !== 'object') return; // Typed initialization is a terminal envelope. Never walk into unrelated // user/session data, even if it contains something resembling a resource. if (typeof obj.type === 'string' && Object.hasOwn(obj, 'data')) { const id = normalizeBubbleRef(obj.id), data = obj.data; if (!RESOURCE_TYPES.has(obj.type) || !validId(id)) return; if (data && typeof data === 'object' && !Array.isArray(data) && Object.keys(data).length === 0) { output.empty[`${obj.type}:${id}`] = {type:obj.type,id}; } else if (data && data._type === obj.type && normalizeBubbleRef(data._id) === id) { visit({_type:obj.type,_id:id,_source:data}); } return; } if (RESOURCE_TYPES.has(obj._type)) { const id = normalizeBubbleRef(obj._id), data = obj._source; if (validId(id) && data && typeof data === 'object' && !Array.isArray(data) && (!data._id || normalizeBubbleRef(data._id) === id) && (!data._type || data._type === obj._type)) { const key = `${obj._type}:${id}`; // Keep source observations, not a guessed union of different versions. const observed = {_type:obj._type,_id:id,...data}; const versions = output.records[key] ||= []; if (!versions.some(v => JSON.stringify(v) === JSON.stringify(observed))) versions.push(observed); } return; } if (Array.isArray(obj.responses)) obj.responses.forEach(visit); if (obj.hits && Array.isArray(obj.hits.hits)) obj.hits.hits.forEach(visit); }; try { visit(JSON.parse(text)); } catch { for (const line of text.split('\n')) { try { visit(JSON.parse(line)); } catch { /* Not a JSON record. */ } } } }