mcard-js
Version:
MCard - Content-addressable storage with cryptographic hashing, handle resolution, and vector search for Node.js and browsers
272 lines • 9.57 kB
JavaScript
import { MCard } from './MCard';
import { Maybe } from '../monads/Maybe';
/**
* CardCollection - High-level interface for MCard operations with monadic API
*
* ## DOTS Vocabulary Role: CARRIER CATEGORY Car(S)
*
* CardCollection is the **Carrier Category** in the Double Operadic Theory of Systems.
* - **Objects**: Individual MCards (data artifacts)
* - **Morphisms**: Hash references between MCards (content-addressable links)
* - **Composition**: Morphisms compose via hash chains (Merkle-DAG structure)
*
* The Carrier is where **actual systems live**. While the Target (CLM design space)
* defines possible interfaces and interactions, the Carrier holds the real data.
*
* ## The Action: Loose(I) ⊛ Car(S) → Car(S)
*
* In DOTS, the **Action** is how interactions (from Target) act on systems (in Carrier)
* to produce new systems. For CardCollection:
* - PCard (Chart/Lens) defines the interaction pattern
* - CardCollection.add() produces new MCard in the Carrier
* - The result is a new Object in Car(S)
*
* ## Empty Schema Principle
*
* CardCollection embodies the Wordless Book principle:
* - At t₀: Empty collection = "Wordless Book"
* - At t_n: Billions of cards = "Infinite Library"
* - Schema (structure) never changes; only data (content) grows
*
* ## CRD-Only Operations (No UPDATE)
*
* Following MCard design principles, this collection supports only:
* - **C**reate: `add()`, `addWithHandle()`
* - **R**ead: `get()`, `getByHandle()`, `getPage()`, `search*()`
* - **D**elete: `delete()`, `clear()`
*
* There is NO `update()` method. Instead, to "update":
* 1. Create a new MCard with new content
* 2. Use `updateHandle()` to point the handle to the new hash
* 3. The old MCard remains in the collection (immutable history)
*
* ## CRDT Foundation (G-Set)
*
* CardCollection implements **Grow-Only Set (G-Set) CRDT** semantics:
* - **Commutative**: merge(A, B) = merge(B, A)
* - **Associative**: merge(A, merge(B, C)) = merge(merge(A, B), C)
* - **Idempotent**: merge(A, A) = A (same hash = same card)
*
* This enables **Strong Eventual Consistency** without coordination.
*
* ## Monadic API
*
* The `*M` methods (getM, getByHandleM, etc.) return Maybe<T> monads
* for composable, null-safe operations following functional programming patterns.
*
* @see {@link MCard} for the individual Carrier objects
* @see {@link DOTSRole.CARRIER} for DOTS vocabulary definition
* @see docs/WorkingNotes/Permanent/Projects/PKC Kernel/MCard.md for full specification
*/
export class CardCollection {
engine;
constructor(engine) {
this.engine = engine;
}
// =========== Standard Operations ===========
/**
* Add a card to the collection
* Handles duplicates (same content, same hash) and collisions (diff content, same hash)
*/
async add(card) {
// Check existing by hash
const existingCard = await this.engine.get(card.hash);
if (existingCard) {
// Compare content
const isDuplicate = this.areContentsEqual(existingCard.content, card.content);
if (isDuplicate) {
// Duplicate detected
// console.debug(`Duplicate detected for hash ${card.hash}`);
const { generateDuplicationEvent } = await import('./EventProducer');
const eventStr = generateDuplicationEvent(card);
const eventCard = await MCard.create(eventStr);
// Store duplicate event but do not overwrite original card
await this.engine.add(eventCard);
return card.hash;
}
else {
// Collision detected
// console.warn(`Collision detected for hash ${card.hash}`);
const { generateCollisionEvent } = await import('./EventProducer');
const eventStr = await generateCollisionEvent(card);
// Store collision event
const eventCard = await MCard.create(eventStr);
await this.engine.add(eventCard);
// Store colliding card with upgraded hash function
const eventObj = JSON.parse(eventStr);
const nextAlgo = eventObj.upgraded_function;
if (!nextAlgo) {
throw new Error("Failed to determine next hash algorithm for collision");
}
const upgradedCard = await MCard.create(card.content, nextAlgo);
return this.engine.add(upgradedCard);
}
}
return this.engine.add(card);
}
areContentsEqual(a, b) {
if (a.length !== b.length)
return false;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i])
return false;
}
return true;
}
/**
* Get a card by hash
*/
async get(hash) {
return this.engine.get(hash);
}
/**
* Delete a card by hash
*/
async delete(hash) {
return this.engine.delete(hash);
}
/**
* Get a page of cards
*/
async getPage(pageNumber = 1, pageSize = 10) {
return this.engine.getPage(pageNumber, pageSize);
}
/**
* Count total cards
*/
async count() {
return this.engine.count();
}
// =========== Handle Operations ===========
/**
* Add a card and register a handle for it
*/
async addWithHandle(card, handle) {
const hash = await this.add(card);
await this.engine.registerHandle(handle, hash);
return hash;
}
/**
* Get card by handle
*/
async getByHandle(handle) {
return this.engine.getByHandle(handle);
}
/**
* Resolve handle to hash
*/
async resolveHandle(handle) {
return this.engine.resolveHandle(handle);
}
/**
* Update handle to point to new card
*/
async updateHandle(handle, newCard) {
const hash = await this.add(newCard);
await this.engine.updateHandle(handle, hash);
return hash;
}
/**
* Get version history for a handle
*/
async getHandleHistory(handle) {
return this.engine.getHandleHistory(handle);
}
// =========== Monadic Operations ===========
/**
* Monadic get - returns Maybe<MCard>
*/
async getM(hash) {
const card = await this.get(hash);
return card ? Maybe.just(card) : Maybe.nothing();
}
/**
* Monadic getByHandle - returns Maybe<MCard>
*/
async getByHandleM(handle) {
const card = await this.getByHandle(handle);
return card ? Maybe.just(card) : Maybe.nothing();
}
/**
* Monadic resolveHandle - returns Maybe<string>
*/
async resolveHandleM(handle) {
const hash = await this.resolveHandle(handle);
return hash ? Maybe.just(hash) : Maybe.nothing();
}
/**
* Resolve handle and get card in one monadic operation
*/
async resolveAndGetM(handle) {
const maybeHash = await this.resolveHandleM(handle);
if (maybeHash.isNothing)
return Maybe.nothing();
return this.getM(maybeHash.value);
}
/**
* Prune version history for a handle.
* @param handle The handle string.
* @param options Options for pruning (olderThan date, or deleteAll).
* @returns Number of deleted entries.
*/
async pruneHandleHistory(handle, options = {}) {
if (this.engine.pruneHandleHistory) {
return this.engine.pruneHandleHistory(handle, options);
}
return 0; // Or throw error if engine doesn't support it
}
// =========== Search & Bulk Operations ===========
async clear() {
return this.engine.clear();
}
async searchByString(query, pageNumber = 1, pageSize = 10) {
return this.engine.search(query, pageNumber, pageSize);
}
async searchByContent(query, pageNumber = 1, pageSize = 10) {
return this.engine.search(query, pageNumber, pageSize);
}
async searchByHash(hashPrefix) {
return this.engine.searchByHash(hashPrefix);
}
async getAllMCardsRaw() {
return this.engine.getAll();
}
async getAllCards(pageSize = 10, processCallback) {
const cards = [];
let pageNumber = 1;
let total = 0;
while (true) {
const page = await this.getPage(pageNumber, pageSize);
if (!page.items || page.items.length === 0)
break;
for (const card of page.items) {
if (processCallback) {
processCallback(card);
}
cards.push(card);
}
total = page.totalItems;
if (!page.hasNext)
break;
pageNumber++;
}
return { cards, total };
}
async printAllCards() {
const cards = await this.getAllMCardsRaw();
cards.forEach(card => {
console.log(`Hash: ${card.hash}`);
// Try to print as text if possible
try {
const text = new TextDecoder().decode(card.content);
const preview = text.slice(0, 100).replace(/\n/g, ' ');
console.log(`Content: ${preview}${text.length > 100 ? '...' : ''}`);
}
catch {
console.log(`Content (binary): ${card.content.length} bytes`);
}
console.log('---');
});
}
}
//# sourceMappingURL=CardCollection.js.map