@synet/signal
Version:
Experimental Fractal Architecture pattern for Synet development
490 lines (489 loc) • 17.3 kB
JavaScript
"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();