rspace-online/shared/local-first/memory-card.ts

164 lines
4.3 KiB
TypeScript

/**
* Memory Card — Cross-module data interchange format.
*
* Any module can export items as Memory Cards; any module can import/reference them.
* Think of it as a universal "clip" format for data flowing between rSpace modules.
*/
import type { DocumentId } from './document';
// ============================================================================
// TYPES
// ============================================================================
/**
* Core Memory Card — the universal interchange unit.
*/
export interface MemoryCard {
/** Unique ID (UUID v4 or module-specific) */
id: string;
/** Semantic type */
type: MemoryCardType | string;
/** Human-readable title */
title: string;
/** Optional body text (markdown) */
body?: string;
/** Source provenance */
source: {
module: string;
docId: DocumentId;
itemId?: string;
};
/** Freeform tags for filtering/grouping */
tags: string[];
/** Unix timestamp (ms) */
createdAt: number;
/** Module-specific structured data */
data?: Record<string, unknown>;
}
/**
* Well-known card types. Modules can extend with custom strings.
*/
export type MemoryCardType =
| 'note'
| 'task'
| 'event'
| 'link'
| 'file'
| 'vote'
| 'transaction'
| 'trip'
| 'contact'
| 'shape'
| 'book'
| 'product'
| 'post';
// ============================================================================
// HELPERS
// ============================================================================
/**
* Create a Memory Card with defaults.
*/
export function createCard(
fields: Pick<MemoryCard, 'type' | 'title' | 'source'> & Partial<MemoryCard>
): MemoryCard {
return {
id: fields.id ?? generateId(),
type: fields.type,
title: fields.title,
body: fields.body,
source: fields.source,
tags: fields.tags ?? [],
createdAt: fields.createdAt ?? Date.now(),
data: fields.data,
};
}
/**
* Filter cards by type.
*/
export function filterByType(cards: MemoryCard[], type: string): MemoryCard[] {
return cards.filter((c) => c.type === type);
}
/**
* Filter cards by tag (any match).
*/
export function filterByTag(cards: MemoryCard[], tag: string): MemoryCard[] {
return cards.filter((c) => c.tags.includes(tag));
}
/**
* Filter cards by source module.
*/
export function filterByModule(cards: MemoryCard[], module: string): MemoryCard[] {
return cards.filter((c) => c.source.module === module);
}
/**
* Sort cards by creation time (newest first).
*/
export function sortByNewest(cards: MemoryCard[]): MemoryCard[] {
return [...cards].sort((a, b) => b.createdAt - a.createdAt);
}
/**
* Search cards by title/body text (case-insensitive substring match).
*/
export function searchCards(cards: MemoryCard[], query: string): MemoryCard[] {
const q = query.toLowerCase();
return cards.filter(
(c) =>
c.title.toLowerCase().includes(q) ||
(c.body && c.body.toLowerCase().includes(q))
);
}
// ============================================================================
// CARD EXPORTER INTERFACE
// ============================================================================
/**
* Modules implement this to export their data as Memory Cards.
*/
export interface CardExporter {
module: string;
/** Export all items from a document as cards */
exportCards(docId: DocumentId, doc: any): MemoryCard[];
/** Export a single item as a card */
exportCard?(docId: DocumentId, doc: any, itemId: string): MemoryCard | null;
}
/**
* Registry of card exporters.
*/
const exporters = new Map<string, CardExporter>();
export function registerExporter(exporter: CardExporter): void {
exporters.set(exporter.module, exporter);
}
export function getExporter(module: string): CardExporter | undefined {
return exporters.get(module);
}
export function getAllExporters(): CardExporter[] {
return Array.from(exporters.values());
}
// ============================================================================
// UTILITIES
// ============================================================================
function generateId(): string {
// crypto.randomUUID() is available in all modern browsers and Bun
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
return crypto.randomUUID();
}
// Fallback
return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
}