import { isArchived } from './archivedResource'; import { olderThan } from './versionOrder'; type Versioned = { resourceType?: string; id?: string; meta?: { versionId?: string } }; /** A search answered below what is known of a record, with no copy at that version to show instead. */ export class RecordReadBehindError extends Error { constructor(noun: string) { super(`The ${noun} list is still catching up with a recent change. Try again in a moment.`); this.name = 'RecordReadBehindError'; } } /** What is known of one record: its newest version, whether it is active, removed (archived) or deleted, and - while * active - that newest copy. */ export type FloorEntry = { version?: string; state: 'active' | 'removed' | 'deleted'; record?: T }; /** * One list's publication boundary, per subject. Every publisher - a search, a save's answer, a broadcast - is admitted * by it and teaches it, so whichever arrives last, an older copy never replaces a newer one and never brings back a * removed or deleted record. `store` is the subject-keyed memory (one per service, cleared with its cache owner). */ /** A server version this floor can order: HealthLake's versions are whole numbers. */ const usableVersion = (record: Versioned): boolean => /^\d+$/.test(record.meta?.versionId ?? ''); export const createRecordFloor = ( store: Map>>, subjectOf: (record: T) => string | undefined, // `requireUsableVersion`: once anything is known of a record, a copy without an orderable version is never admitted // (it cannot replace a known version, clear a removal or lower the floor). // `olderIsUnresolved`: a search answering below what is known is not proof of lag (the server may serve an earlier // version on purpose), so it is reported unresolved - never replaced by a body this read may not show, never dropped. options: { requireUsableVersion?: boolean; olderIsUnresolved?: boolean } = {}, ) => { const versionsOf = (record: T) => { const subject = subjectOf(record) ?? ''; let versions = store.get(subject); if (!versions) { versions = new Map(); store.set(subject, versions); } return versions; }; const entryOf = (record: T) => (record.id ? versionsOf(record).get(record.id) : undefined); const archived = (record: T) => isArchived(record as Parameters[0]); /** Not older than what is known, and not deleted; an active copy at a version known as removed predates it. */ const admits = (record: T): boolean => { const entry = entryOf(record); if (!entry) return true; if (entry.state === 'deleted') return false; if (options.requireUsableVersion && !usableVersion(record)) return false; if (entry.state === 'removed' && !archived(record) && record.meta?.versionId === entry.version) return false; return !(entry.version && olderThan(record, { meta: { versionId: entry.version } })); }; /** Admitted, it becomes what is known (removed if archived). Answers whether it was admitted. */ const learn = (record: T): boolean => { if (!admits(record)) return false; if (record.id && record.meta?.versionId && (!options.requireUsableVersion || usableVersion(record))) { versionsOf(record).set(record.id, { version: record.meta.versionId, state: archived(record) ? 'removed' : 'active', record: archived(record) ? undefined : record, }); } return true; }; return { admits, learn, /** Archived at this version, or deleted. Answers whether it was admitted. */ remove: (record: T, deleted = false): boolean => { if (!record.id) return false; if (!deleted) return learn(record); versionsOf(record).set(record.id, { version: record.meta?.versionId, state: 'deleted' }); return true; }, /** A search's entries as they may be published: an admitted entry teaches; an older one gives way to the newest * copy known, and is dropped when the record was removed or deleted since. `behind`: with no newer copy at hand, * the read is reported unsettled rather than shown without the record. */ settle: (entries: T[], behind?: () => Error): T[] => entries.flatMap((entry) => { if (learn(entry)) return [entry]; if (options.olderIsUnresolved && behind) throw behind(); const known = entryOf(entry); if (known?.state !== 'active') return []; if (known.record) return [known.record]; if (behind) throw behind(); return []; }), }; }; export type RecordFloor = ReturnType>;