mcard-js
Version:
MCard - Content-addressable storage with cryptographic hashing, handle resolution, and vector search for Node.js and browsers
236 lines • 9.04 kB
JavaScript
/**
* IO Effects - Observable side-effects for Lambda Calculus computations
*
* Implements the IO Monad pattern for CLM:
* IO a = World → (a, World')
*
* In our context:
* Reduction = TermHash → (TermHash', IOEffects)
*
* IO effects are purely observational - they do not affect computation results.
* The same input will always produce the same output regardless of IO configuration.
*
* @module mcard-js/ptr/lambda/IOEffects
*/
// ─────────────────────────────────────────────────────────────────────────────
// IO Effects Handler
// ─────────────────────────────────────────────────────────────────────────────
export class IOEffectsHandler {
config;
events = [];
constructor(config = {}) {
this.config = {
enabled: config.enabled ?? false,
console: config.console ?? true,
network: config.network,
onStep: config.onStep ?? false,
onComplete: config.onComplete ?? true,
onError: config.onError ?? true,
format: config.format ?? 'minimal'
};
}
/**
* Check if IO effects are enabled
*/
isEnabled() {
return this.config.enabled;
}
/**
* Emit a step event
*/
async emitStep(stepNumber, termHash, prettyPrint) {
if (!this.config.enabled || !this.config.onStep)
return;
const event = {
type: 'step',
stepNumber,
termHash,
prettyPrint,
timestamp: new Date()
};
this.events.push(event);
await this.dispatch(event);
}
/**
* Emit a completion event
*/
async emitComplete(normalForm, prettyPrint, totalSteps, reductionPath) {
if (!this.config.enabled || !this.config.onComplete)
return;
const event = {
type: 'complete',
normalForm,
prettyPrint,
totalSteps,
reductionPath,
timestamp: new Date()
};
this.events.push(event);
await this.dispatch(event);
}
/**
* Emit an error event
*/
async emitError(message, partialSteps, lastTermHash) {
if (!this.config.enabled || !this.config.onError)
return;
const event = {
type: 'error',
message,
partialSteps,
lastTermHash,
timestamp: new Date()
};
this.events.push(event);
await this.dispatch(event);
}
/**
* Get all collected events
*/
getEvents() {
return [...this.events];
}
/**
* Clear collected events
*/
clearEvents() {
this.events = [];
}
/**
* Dispatch event to configured outputs
*/
async dispatch(event) {
const formatted = this.format(event);
// Console output
if (this.config.console) {
this.logToConsole(event, formatted);
}
// Network output
if (this.config.network?.enabled && this.config.network.endpoint) {
await this.sendToNetwork(event, formatted);
}
}
/**
* Format event for output
*/
format(event) {
switch (this.config.format) {
case 'json':
return JSON.stringify(event);
case 'verbose':
return this.formatVerbose(event);
case 'minimal':
default:
return this.formatMinimal(event);
}
}
formatMinimal(event) {
switch (event.type) {
case 'step':
return `[IO:step ${event.stepNumber}] ${event.prettyPrint}`;
case 'complete':
return `[IO:complete] ${event.totalSteps} steps → ${event.prettyPrint}`;
case 'error':
return `[IO:error] ${event.message}`;
}
}
formatVerbose(event) {
const ts = event.timestamp.toISOString();
switch (event.type) {
case 'step':
return [
`┌─ IO Effect: Reduction Step ${event.stepNumber}`,
`│ Time: ${ts}`,
`│ Hash: ${event.termHash.substring(0, 16)}...`,
`│ Term: ${event.prettyPrint}`,
`└─────────────────────────────────────────`
].join('\n');
case 'complete':
return [
`╔═══════════════════════════════════════════`,
`║ IO Effect: Normalization Complete`,
`╠═══════════════════════════════════════════`,
`║ Time: ${ts}`,
`║ Total Steps: ${event.totalSteps}`,
`║ Normal Form: ${event.prettyPrint}`,
`║ Hash: ${event.normalForm.substring(0, 16)}...`,
`╚═══════════════════════════════════════════`
].join('\n');
case 'error':
return [
`╔═══════════════════════════════════════════`,
`║ IO Effect: ERROR`,
`╠═══════════════════════════════════════════`,
`║ Time: ${ts}`,
`║ Message: ${event.message}`,
`║ Partial Steps: ${event.partialSteps}`,
`╚═══════════════════════════════════════════`
].join('\n');
}
}
logToConsole(event, formatted) {
const prefix = '\x1b[36m'; // Cyan color
const reset = '\x1b[0m';
switch (event.type) {
case 'step':
console.log(`${prefix}${formatted}${reset}`);
break;
case 'complete':
console.log(`${prefix}${formatted}${reset}`);
break;
case 'error':
console.error(`\x1b[31m${formatted}${reset}`); // Red for errors
break;
}
}
async sendToNetwork(event, formatted) {
if (!this.config.network?.endpoint)
return;
try {
const response = await fetch(this.config.network.endpoint, {
method: this.config.network.method || 'POST',
headers: {
'Content-Type': 'application/json',
...this.config.network.headers
},
body: JSON.stringify({
event: event.type,
data: event,
formatted
})
});
if (!response.ok) {
console.warn(`IO network effect failed: ${response.status}`);
}
}
catch (err) {
console.warn(`IO network effect error: ${err}`);
}
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Factory and Utilities
// ─────────────────────────────────────────────────────────────────────────────
/**
* Create an IO effects handler from CLM config
*/
export function createIOHandler(config) {
if (!config || !config.io_effects) {
return new IOEffectsHandler({ enabled: false });
}
const ioConfig = config.io_effects;
return new IOEffectsHandler({
enabled: ioConfig.enabled ?? false,
console: ioConfig.console ?? true,
network: ioConfig.network,
onStep: ioConfig.on_step ?? ioConfig.onStep ?? false,
onComplete: ioConfig.on_complete ?? ioConfig.onComplete ?? true,
onError: ioConfig.on_error ?? ioConfig.onError ?? true,
format: ioConfig.format ?? 'minimal'
});
}
/**
* No-op handler for when IO effects are disabled
*/
export const noopIOHandler = new IOEffectsHandler({ enabled: false });
//# sourceMappingURL=IOEffects.js.map