rspace-online/shared/markwhen/creations.ts

99 lines
3.2 KiB
TypeScript

/**
* Universal creation-log enumerators.
*
* Where `MarkwhenSourceFactory` projects *scheduled* events (when will X
* happen), a `CreationEnumerator` projects *creations* (when did X come
* into being). Every rApp that stores CRDT records with a `createdAt`
* field registers one here — and automatically appears in rPast, the
* unified chronicle-of-self viewer.
*
* The contract is deliberately minimal: the enumerator walks whichever
* docs it owns and emits one `Creation` per record. The meta-adapter
* below turns the merged stream into a single `MwSource` per module.
*/
import type { MwSource, MwEvent } from './types';
export interface Creation {
/** Unix ms — when the record was created. Required. */
createdAt: number;
/** Unix ms — last edit. Omit if never updated. */
updatedAt?: number;
/** Single-line display name. */
title: string;
/** Record-type label (e.g. "event", "note", "task", "vote"). Used as a sub-tag. */
recordType: string;
/** Stable ID within the module (prefix not required; registry prefixes with module). */
recordId: string;
/** Optional deep-link back to the canonical record. */
href?: string;
/** Extra per-record tags (status, category, etc.). */
tags?: string[];
/** Short body shown indented under the event. */
description?: string;
}
export interface CreationEnumerator {
/** Module id, e.g. "rcal". */
module: string;
/** Human label shown in the rPast chip bar / filter UI. */
label: string;
/** Emoji for chip + section header. */
icon: string;
/** Color for the module tag in frontmatter. */
color?: string;
/** Walk owned docs and emit one Creation per record. */
enumerate(ctx: { space: string; from?: number; to?: number }): Promise<Creation[]>;
}
const enumerators: Map<string, CreationEnumerator> = new Map();
export function registerCreationEnumerator(e: CreationEnumerator): void {
enumerators.set(e.module, e);
}
export function listCreationEnumerators(): CreationEnumerator[] {
return [...enumerators.values()];
}
export async function enumerateCreations(
space: string,
opts: { modules?: string[]; from?: number; to?: number } = {},
): Promise<MwSource[]> {
const chosen = opts.modules
? opts.modules.map(m => enumerators.get(m)).filter((e): e is CreationEnumerator => !!e)
: [...enumerators.values()];
const sources: MwSource[] = [];
for (const e of chosen) {
try {
const creations = await e.enumerate({ space, from: opts.from, to: opts.to });
if (creations.length === 0) continue;
const events: MwEvent[] = creations.map(c => ({
start: c.createdAt,
// A creation is a point in time by default. If updatedAt is
// meaningfully later, span it so the timeline can show the
// "alive" range of the record.
end: c.updatedAt && c.updatedAt - c.createdAt > 24 * 3600_000 ? c.updatedAt : undefined,
title: `${c.title}`,
description: c.description,
tags: [c.recordType, ...(c.tags ?? [])],
href: c.href,
sourceId: `${e.module}:${c.recordType}:${c.recordId}`,
}));
sources.push({
id: e.module,
label: `${e.icon} ${e.label}`,
tag: e.module,
color: e.color,
events,
});
} catch (err) {
console.warn(`[rpast] enumerator ${e.module} failed:`, err);
}
}
return sources;
}