UNPKG

@synet/signal

Version:

Experimental Fractal Architecture pattern for Synet development

490 lines (489 loc) 17.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Signal = void 0; const signal_plugin_registry_1 = require("./signal-plugin-registry"); const node_crypto_1 = __importDefault(require("node:crypto")); /** * Signal is a self-aware data container that tracks its journey through layers */ class Signal { constructor(value, error, trace = [], metadata = {}) { this._trace = []; this._metadata = {}; this._value = value; this._error = error; this._trace = [...trace]; this._metadata = metadata; this._id = typeof metadata.id === "string" ? metadata.id : node_crypto_1.default.randomUUID(); } get id() { return this._id; } // Core properties and methods get isSuccess() { return this._error === undefined; } get isFailure() { return !this.isSuccess; } get value() { return this._value; } get error() { return this._error; } get traceEntries() { return [...this._trace]; } static open(layer) { return new Signal(undefined, // Cast to U to support both same type and explicit transformations undefined, [ { layer: layer || "Signal Open", timestamp: Date.now(), status: undefined, }, ]); } // Static methods static success(value, layer) { return new Signal(value, undefined, [], { layer }); } static failure(error, layer) { const err = typeof error === "string" ? new Error(error) : error; return new Signal(undefined, err, [], { layer }); } // Alias for backward compatibility static fail(error, layer) { return Signal.failure(error, layer); } /** * Executes the given callback if this is a success result * @param fn Function to execute with the success value * @returns This result, for method chaining */ onSuccess(fn) { if (this.isSuccess && this._value) { fn(this._value); } return this; } /** * Executes the given callback if this is a failure result * @param fn Function to execute with the error details * @returns This result, for method chaining */ onFailure(fn) { if (this.isFailure && this._error) { fn(this._error); } return this; } /** * Create a new signal with a potentially different value type while preserving the trace history * @param original The original signal whose trace will be copied * @param transformer Optional function to transform the value */ static extend(original, transformer) { const value = transformer ? transformer(original._value) : original._value; return new Signal(value, original._error, [...original._trace]); } /** * Add a reflection to the trace * @param message Message to add to the trace * @param context Optional context object to add to the trace * @param component Optional component name to add to the trace * */ reflect(message, context, component) { const methodData = component ? { methodName: component } : this._getMethodName(); // If there are no trace entries, create a default one first if (!this._trace || this._trace.length === 0) { // Create a default trace entry return new Signal(this._value, this._error, [ { layer: methodData?.methodName || "default", timestamp: Date.now(), reflections: [ { message, method: methodData?.methodName, class: methodData?.className, context, timestamp: Date.now(), }, ], }, ]); } // Add reflection to the last trace entry const lastEntry = this._trace[this._trace.length - 1]; const reflections = lastEntry.reflections || []; // Create a new trace array with the updated last entry const newTrace = [ ...this._trace.slice(0, -1), { ...lastEntry, reflections: [ ...reflections, { method: methodData?.methodName, class: methodData?.className, message, context, timestamp: Date.now(), }, ], }, ]; return new Signal(this._value, this._error, newTrace); } /** * Add a new layer to the trace */ layer(name, context) { return new Signal(this._value, // Cast to U to support both same type and explicit transformations this._error, [ ...this._trace, { layer: name, timestamp: Date.now(), reflections: context ? [ { message: "Layer created", context, timestamp: Date.now(), }, ] : undefined, }, ]); } /** * Transform this signal into a failure signal */ fail(error, layer) { const err = typeof error === "string" ? new Error(error) : error; return new Signal(undefined, err, [ ...this._trace, { layer: layer || "failure", timestamp: Date.now(), error: err, reflections: [ { message: "Signal converted to failure", timestamp: Date.now(), }, ], }, ]); } success(value) { if (this._trace.length === 0) { // Create a default trace entry if none exists return new Signal(value, undefined, [ { layer: "default", timestamp: Date.now(), status: "success", reflections: [], }, ]); } // Preserve the entire trace array const newTrace = this._trace.map((entry, index) => { // Only update the status of the last trace entry if (index === this._trace.length - 1) { return { ...entry, status: "success", }; } return entry; }); return new Signal(value, undefined, newTrace); } // Alias for consistency failure(error, layer) { return this.fail(error, layer); } // Regular map - no trace by default map(fn) { return this._map(fn, { trace: false }); } // Traced map - adds trace entry tracedMap(fn, layer) { return this._map(fn, { trace: true, layer }); } _map(fn, options) { if (this.isFailure) { return new Signal(undefined, this._error, this._trace); } try { const result = fn(this._value); // Only add a trace entry if explicitly requested if (options?.trace) { return new Signal(result, undefined, [ ...this._trace, { layer: options?.layer || "map", timestamp: Date.now(), reflections: [ { message: "Value transformed", timestamp: Date.now(), }, ], }, ]); } // Just return a new signal with the transformed value but same trace return new Signal(result, undefined, this._trace); } catch (error) { // Always trace errors return new Signal(undefined, error instanceof Error ? error : new Error(String(error)), [ ...this._trace, { layer: options?.layer || "map", timestamp: Date.now(), error: error instanceof Error ? error : new Error(String(error)), }, ]); } } /** * Apply a function that returns a Signal, flattening the result * Preserves the parent-child relationship for tracing */ // Regular map - no trace by default flatMap(fn, layer) { return this._flatMap(fn, { trace: false }); } // Traced map - adds trace entry tracedFlatMap(fn, layer) { return this._flatMap(fn, { trace: true, layer }); } _flatMap(fn, options, layer) { if (this.isFailure || this._value === undefined) { // If this signal is a failure, propagate the failure return new Signal(undefined, this._error, [...this._trace]); } try { // Apply the function which returns a new signal const nextSignal = fn(this._value); // Create a new trace array with all previous entries plus a layer for this operation if (options?.trace) { const newTrace = [ ...this._trace, { layer: layer || "flatMap", timestamp: Date.now(), status: nextSignal.isSuccess ? "success" : "error", error: nextSignal._error, reflections: [ { message: `Applied operation ${layer || "flatMap"}`, timestamp: Date.now(), }, ], }, ]; return new Signal(nextSignal._value, nextSignal._error, newTrace); } // Return a new signal with the next value and combined trace return new Signal(nextSignal._value, nextSignal._error, this._trace); } catch (error) { const err = error instanceof Error ? error : new Error(String(error)); return new Signal(undefined, err, [ ...this._trace, { layer: layer || "flatMap", timestamp: Date.now(), status: "error", error: err, reflections: [ { message: `Operation failed: ${err.message}`, timestamp: Date.now(), }, ], }, ]); } } /** * Format trace for logging */ trace() { let result = ""; for (const entry of this._trace) { const time = new Date(entry.timestamp).toISOString(); const status = entry.error ? "ERROR" : "OK"; result += `[${time}] ${entry.layer} (${status})\n`; if (entry.error) { result += ` Error: ${entry.error.message}\n`; } if (entry.reflections && entry.reflections.length > 0) { for (const ref of entry.reflections) { const refTime = new Date(ref.timestamp).toISOString(); const method = ref.method ? `${ref.method}: ` : ""; result += ` [${refTime}] ${method}${ref.message || ""}\n`; if (ref.context) { result += ` Context: ${JSON.stringify(ref.context)}\n`; } } } } return result; } traceData() { // Return all trace entries, not just the first one return this._trace.map((entry) => ({ layer: entry.layer, timestamp: new Date(entry.timestamp).toISOString(), status: entry.error ? "error" : "success", error: entry.error?.message, reflections: entry.reflections ?.filter((r) => r.message !== undefined) .map((r) => ({ message: r.message || "", method: r.method, class: r.class, timestamp: new Date(r.timestamp).toISOString(), context: r.context, })), })); } // Helper methods /** * Helper method to extract the calling method name from the stack trace */ _getMethodName() { //if (!obj) return undefined; //const obj = this; // Try to find the calling method name from stack trace try { const stack = new Error().stack || ""; if (!stack) return undefined; const matches = /at \w+\.(\w+)/.exec(stack.split("\n")[3]); if (matches) { const methodName = matches[1]; // This will be undefined for standalone functions const className = matches[0].split(" ")[1].split(".")[0]; return { className, methodName }; } if (this.constructor?.name) { return { methodName: `${this.constructor.name}` }; } } catch (error) { // Silently fail - method name detection is non-critical console.debug("Error detecting method name:", error); } // Last resort: just use the object's constructor name return this.constructor ? { methodName: this.constructor.name } : { methodName: "unknown" }; } ensure(condition, message) { if (!this.isSuccess || this._value === undefined) { return this; } return condition(this._value) ? this : Signal.fail(message); } // Add metadata setter/getter withMeta(key, value) { const newMetadata = { ...this._metadata, [key]: value }; return new Signal(this._value, this._error, this._trace, newMetadata); } // Get metadata meta(key) { return this._metadata[key]; } resolve() { if (this.isFailure) { throw this._error; } return this._value; } /** * Create a static helper for tracing the call chain through layers */ static chain(value, initialLayer) { return Signal.success(value, initialLayer); } /** * Record when execution flow takes a specific branch */ branch(branchName, condition) { return new Signal(this._value, this._error, [ ...this._trace, { layer: "branch", timestamp: Date.now(), status: this.isSuccess ? "success" : "error", reflections: [ { message: `Branch condition: ${condition}`, timestamp: Date.now(), }, ], }, ]); } /** * Plugin system */ static registerPlugin(plugin) { if (!plugin || typeof plugin !== "object" || !plugin.id) { console.error("Invalid plugin provided to registerPlugin"); return; } //console.info(`Registering plugin: ${plugin.id}`); if (Signal.plugins.has(plugin.id)) { // console.warn(`Plugin '${plugin.id}' is already registered. Skipping registration.`); return; } // Add the plugin to the registry Signal.plugins.set(plugin.id, plugin); if (typeof plugin.init === "function") { try { plugin.init(); } catch (error) { // console.error(`Error initializing plugin '${plugin.id}':`, error); } } //console.info(`Plugin ${plugin.id} registered successfully. Total plugins: ${Signal.plugins.size}`); } static unregisterPlugin(pluginId) { signal_plugin_registry_1.SignalPluginRegistry.getInstance().unregister(pluginId); } with(pluginConfig) { const pluginId = typeof pluginConfig === "string" ? pluginConfig : pluginConfig.id; const options = typeof pluginConfig === "string" ? undefined : pluginConfig.options; // Use class name instead of this const plugin = Signal.plugins.get(pluginId); if (!plugin) { console.warn(`Plugin '${pluginId}' not found. Returning original signal.`); return this; } // Execute the plugin and return the result return plugin.execute(this, options); } } exports.Signal = Signal; Signal.plugins = new Map();