UNPKG

@cadenza.io/core

Version:

This is a framework for building asynchronous graphs and flows of tasks and signals.

1 lines 680 kB
{"version":3,"sources":["../src/utils/tools.ts","../src/engine/SignalBroker.ts","../node_modules/lodash-es/isObject.js","../node_modules/lodash-es/_freeGlobal.js","../node_modules/lodash-es/_root.js","../node_modules/lodash-es/now.js","../node_modules/lodash-es/_trimmedEndIndex.js","../node_modules/lodash-es/_baseTrim.js","../node_modules/lodash-es/_Symbol.js","../node_modules/lodash-es/_getRawTag.js","../node_modules/lodash-es/_objectToString.js","../node_modules/lodash-es/_baseGetTag.js","../node_modules/lodash-es/isObjectLike.js","../node_modules/lodash-es/isSymbol.js","../node_modules/lodash-es/toNumber.js","../node_modules/lodash-es/debounce.js","../src/utils/promise.ts","../node_modules/lodash-es/_listCacheClear.js","../node_modules/lodash-es/eq.js","../node_modules/lodash-es/_assocIndexOf.js","../node_modules/lodash-es/_listCacheDelete.js","../node_modules/lodash-es/_listCacheGet.js","../node_modules/lodash-es/_listCacheHas.js","../node_modules/lodash-es/_listCacheSet.js","../node_modules/lodash-es/_ListCache.js","../node_modules/lodash-es/_stackClear.js","../node_modules/lodash-es/_stackDelete.js","../node_modules/lodash-es/_stackGet.js","../node_modules/lodash-es/_stackHas.js","../node_modules/lodash-es/isFunction.js","../node_modules/lodash-es/_coreJsData.js","../node_modules/lodash-es/_isMasked.js","../node_modules/lodash-es/_toSource.js","../node_modules/lodash-es/_baseIsNative.js","../node_modules/lodash-es/_getValue.js","../node_modules/lodash-es/_getNative.js","../node_modules/lodash-es/_Map.js","../node_modules/lodash-es/_nativeCreate.js","../node_modules/lodash-es/_hashClear.js","../node_modules/lodash-es/_hashDelete.js","../node_modules/lodash-es/_hashGet.js","../node_modules/lodash-es/_hashHas.js","../node_modules/lodash-es/_hashSet.js","../node_modules/lodash-es/_Hash.js","../node_modules/lodash-es/_mapCacheClear.js","../node_modules/lodash-es/_isKeyable.js","../node_modules/lodash-es/_getMapData.js","../node_modules/lodash-es/_mapCacheDelete.js","../node_modules/lodash-es/_mapCacheGet.js","../node_modules/lodash-es/_mapCacheHas.js","../node_modules/lodash-es/_mapCacheSet.js","../node_modules/lodash-es/_MapCache.js","../node_modules/lodash-es/_stackSet.js","../node_modules/lodash-es/_Stack.js","../node_modules/lodash-es/_defineProperty.js","../node_modules/lodash-es/_baseAssignValue.js","../node_modules/lodash-es/_assignMergeValue.js","../node_modules/lodash-es/_createBaseFor.js","../node_modules/lodash-es/_baseFor.js","../node_modules/lodash-es/_cloneBuffer.js","../node_modules/lodash-es/_Uint8Array.js","../node_modules/lodash-es/_cloneArrayBuffer.js","../node_modules/lodash-es/_cloneTypedArray.js","../node_modules/lodash-es/_copyArray.js","../node_modules/lodash-es/_baseCreate.js","../node_modules/lodash-es/_overArg.js","../node_modules/lodash-es/_getPrototype.js","../node_modules/lodash-es/_isPrototype.js","../node_modules/lodash-es/_initCloneObject.js","../node_modules/lodash-es/_baseIsArguments.js","../node_modules/lodash-es/isArguments.js","../node_modules/lodash-es/isArray.js","../node_modules/lodash-es/isLength.js","../node_modules/lodash-es/isArrayLike.js","../node_modules/lodash-es/isArrayLikeObject.js","../node_modules/lodash-es/stubFalse.js","../node_modules/lodash-es/isBuffer.js","../node_modules/lodash-es/isPlainObject.js","../node_modules/lodash-es/_baseIsTypedArray.js","../node_modules/lodash-es/_baseUnary.js","../node_modules/lodash-es/_nodeUtil.js","../node_modules/lodash-es/isTypedArray.js","../node_modules/lodash-es/_safeGet.js","../node_modules/lodash-es/_assignValue.js","../node_modules/lodash-es/_copyObject.js","../node_modules/lodash-es/_baseTimes.js","../node_modules/lodash-es/_isIndex.js","../node_modules/lodash-es/_arrayLikeKeys.js","../node_modules/lodash-es/_nativeKeysIn.js","../node_modules/lodash-es/_baseKeysIn.js","../node_modules/lodash-es/keysIn.js","../node_modules/lodash-es/toPlainObject.js","../node_modules/lodash-es/_baseMergeDeep.js","../node_modules/lodash-es/_baseMerge.js","../node_modules/lodash-es/identity.js","../node_modules/lodash-es/_apply.js","../node_modules/lodash-es/_overRest.js","../node_modules/lodash-es/constant.js","../node_modules/lodash-es/_baseSetToString.js","../node_modules/lodash-es/_shortOut.js","../node_modules/lodash-es/_setToString.js","../node_modules/lodash-es/_baseRest.js","../node_modules/lodash-es/_isIterateeCall.js","../node_modules/lodash-es/_createAssigner.js","../node_modules/lodash-es/merge.js","../src/engine/GraphRunner.ts","../src/engine/GraphRun.ts","../src/utils/ColorRandomizer.ts","../src/engine/exporters/vue-flow/VueFlowExportVisitor.ts","../src/engine/exporters/vue-flow/VueFlowExporter.ts","../src/graph/execution/GraphNode.ts","../src/graph/context/GraphContext.ts","../src/graph/iterators/GraphNodeIterator.ts","../src/interfaces/SignalEmitter.ts","../src/graph/definition/GraphRoutine.ts","../src/graph/definition/Task.ts","../src/graph/iterators/TaskIterator.ts","../src/actors/Actor.ts","../src/tools/definitions.ts","../src/registry/GraphRegistry.ts","../src/graph/definition/DebounceTask.ts","../src/graph/definition/EphemeralTask.ts","../src/interfaces/ExecutionChain.ts","../src/graph/iterators/GraphLayerIterator.ts","../src/interfaces/GraphLayer.ts","../src/graph/execution/SyncGraphLayer.ts","../src/interfaces/GraphBuilder.ts","../src/engine/builders/GraphBreadthFirstBuilder.ts","../src/interfaces/GraphRunStrategy.ts","../src/engine/ThrottleEngine.ts","../src/graph/execution/AsyncGraphLayer.ts","../src/engine/builders/GraphAsyncQueueBuilder.ts","../src/engine/strategy/GraphAsyncRun.ts","../src/engine/strategy/GraphStandardRun.ts","../src/engine/InquiryBroker.ts","../src/runtime/RuntimeDefinitionRegistry.ts","../src/runtime/sandbox.ts","../src/runtime/sanitize.ts","../src/Cadenza.ts","../src/runtime/RuntimeSubscriptionManager.ts","../src/runtime/RuntimeHost.ts","../src/index.ts"],"sourcesContent":["/**\n * Creates a deep clone of the given input object or array, while allowing specific keys to be filtered out based on a provided criteria.\n *\n * @param {T} input The input data to be cloned. It can be an object or an array.\n * @param {(key: string) => boolean} [filterOut] A callback function to determine which keys should be excluded from the cloned structure. It receives the key as a parameter and should return `true` to exclude the key, or `false` to include it. Default is a function that includes all keys (`() => false`).\n * @return {T} Returns a deep clone of the input object or array with the specified keys filtered out.\n */\nexport function deepCloneFilter<T>(\n input: T,\n filterOut: (key: string) => boolean = () => false,\n): T {\n if (input === null || typeof input !== \"object\") {\n return input;\n }\n\n const visited = new WeakMap<any, any>(); // For cycle detection\n\n const stack: Array<{ source: any; target: any; key?: string }> = [];\n const output = Array.isArray(input) ? [] : {};\n\n stack.push({ source: input, target: output });\n visited.set(input, output);\n\n while (stack.length) {\n const { source, target, key } = stack.pop()!;\n const currentTarget = key !== undefined ? target[key] : target;\n\n if (\n // TODO Should probably not be done like this...\n key === \"taskInstance\" ||\n key === \"routineInstance\" ||\n key === \"task\" ||\n key === \"routine\" ||\n key === \"tasks\" ||\n key === \"routines\" ||\n key === \"httpServer\" ||\n key === \"httpsServer\"\n ) {\n target[key] = source;\n continue;\n }\n\n for (const [k, value] of Object.entries(source)) {\n if (filterOut(k)) continue;\n\n if (value && typeof value === \"object\") {\n if (visited.has(value)) {\n currentTarget[k] = visited.get(value); // Cycle: link to existing clone\n continue;\n }\n\n const clonedValue = Array.isArray(value) ? [] : {};\n currentTarget[k] = clonedValue;\n visited.set(value, clonedValue);\n\n stack.push({ source: value, target: currentTarget, key: k });\n } else {\n currentTarget[k] = value;\n }\n }\n }\n\n return output as T;\n}\n\n/**\n * Converts a given timestamp to an ISO 8601 formatted string.\n *\n * @param {number} timestamp - The timestamp in milliseconds to be formatted.\n * @return {string} The ISO 8601 formatted date string.\n */\nexport function formatTimestamp(timestamp: number) {\n return new Date(timestamp).toISOString();\n}\n","import GraphRunner from \"./GraphRunner\";\nimport { AnyObject, ThrottleHandle } from \"../types/global\";\nimport Task from \"../graph/definition/Task\";\nimport GraphRoutine from \"../graph/definition/GraphRoutine\";\nimport { formatTimestamp } from \"../utils/tools\";\nimport { v4 as uuid } from \"uuid\";\nimport debounce from \"lodash-es/debounce\";\nimport { sleep } from \"../utils/promise\";\nimport Cadenza from \"../Cadenza\";\nimport merge from \"lodash-es/merge\";\n\nexport interface EmitOptions {\n squash?: boolean;\n squashId?: string | null;\n groupId?: string | null;\n mergeFunction?:\n | ((oldContext: AnyObject, ...newContext: AnyObject[]) => AnyObject)\n | null;\n debounce?: boolean;\n throttle?: boolean;\n delayMs?: number;\n schedule?: boolean;\n exactDateTime?: Date | null;\n throttleBatch?: number;\n flushStrategy?: string; // New: choose which flush interval bucket to use\n}\n\nexport type SignalDeliveryMode = \"single\" | \"broadcast\";\n\nexport type SignalReceiverFilter = {\n serviceNames?: string[];\n serviceInstanceIds?: string[];\n origins?: string[];\n roles?: string[];\n protocols?: string[];\n runtimeStates?: string[];\n};\n\nexport type SignalMetadata = {\n deliveryMode?: SignalDeliveryMode;\n broadcastFilter?: SignalReceiverFilter | null;\n};\n\nexport type PassiveSignalListener = (\n signal: string,\n context: AnyObject,\n metadata: SignalMetadata | null,\n) => void;\n\nexport type SignalDefinitionInput =\n | string\n | ({\n name: string;\n } & SignalMetadata);\n\ntype FlushStrategyName = string;\n\ninterface FlushStrategy {\n intervalMs: number;\n maxBatchSize: number;\n}\n\n/**\n * This class manages signals and observers, enabling communication across different parts of an application.\n * It follows a singleton design pattern, allowing for centralized signal management.\n */\nexport default class SignalBroker {\n static instance_: SignalBroker;\n\n static get instance(): SignalBroker {\n if (!this.instance_) {\n this.instance_ = new SignalBroker();\n }\n return this.instance_;\n }\n\n debug: boolean = false;\n verbose: boolean = false;\n\n setDebug(value: boolean) {\n this.debug = value;\n }\n\n setVerbose(value: boolean) {\n this.verbose = value;\n }\n\n runner: GraphRunner | undefined;\n metaRunner: GraphRunner | undefined;\n\n throttleEmitters: Map<string, any> = new Map();\n throttleQueues: Map<string, any> = new Map();\n\n public getSignalsTask: Task | undefined;\n public registerSignalTask: Task | undefined;\n\n // TODO: Signals should be a class with a the observers, registered flag and other data.\n signalObservers: Map<\n string,\n {\n fn: (\n runner: GraphRunner,\n tasks: (Task | GraphRoutine)[],\n context: AnyObject,\n ) => void;\n tasks: Set<Task | GraphRoutine>;\n registered: boolean;\n }\n > = new Map();\n\n emittedSignalsRegistry: Set<string> = new Set();\n signalMetadataRegistry: Map<string, SignalMetadata> = new Map();\n passiveSignalListeners: Map<string, PassiveSignalListener> = new Map();\n\n // ── Flush Strategy Management ───────────────────────────────────────\n private flushStrategies = new Map<FlushStrategyName, FlushStrategy>();\n private strategyData = new Map<\n FlushStrategyName,\n Map<string, { signal: string; contexts: AnyObject[] }>\n >();\n private strategyTimers = new Map<FlushStrategyName, NodeJS.Timeout | null>();\n private isStrategyFlushing = new Map<FlushStrategyName, boolean>();\n\n private readonly defaultStrategyName = \"default\";\n\n constructor() {\n this.addSignal(\"meta.signal_broker.added\");\n // Register default strategy\n this.setFlushStrategy(this.defaultStrategyName, {\n intervalMs: 350,\n maxBatchSize: 80,\n });\n }\n\n private resolveSignalMetadataKey(signal: unknown): string {\n const normalizedSignal = typeof signal === \"string\" ? signal.trim() : \"\";\n if (!normalizedSignal) {\n return \"\";\n }\n\n return normalizedSignal.split(\":\")[0] ?? normalizedSignal;\n }\n\n private normalizeSignalMetadata(\n metadata?: SignalMetadata | null,\n ): SignalMetadata | undefined {\n if (!metadata) {\n return undefined;\n }\n\n const deliveryMode =\n metadata.deliveryMode === \"broadcast\" ? \"broadcast\" : \"single\";\n const broadcastFilter =\n metadata.broadcastFilter &&\n typeof metadata.broadcastFilter === \"object\" &&\n !Array.isArray(metadata.broadcastFilter)\n ? merge({}, metadata.broadcastFilter)\n : null;\n\n return {\n deliveryMode,\n broadcastFilter,\n };\n }\n\n setSignalMetadata(signal: string, metadata?: SignalMetadata | null): void {\n const metadataKey = this.resolveSignalMetadataKey(signal);\n if (!metadataKey) {\n return;\n }\n\n const normalized = this.normalizeSignalMetadata(metadata);\n if (!normalized) {\n this.signalMetadataRegistry.delete(metadataKey);\n return;\n }\n\n this.signalMetadataRegistry.set(metadataKey, normalized);\n }\n\n getSignalMetadata(signal: string): SignalMetadata | undefined {\n const metadataKey = this.resolveSignalMetadataKey(signal);\n if (!metadataKey) {\n return undefined;\n }\n\n const metadata = this.signalMetadataRegistry.get(metadataKey);\n if (!metadata) {\n return undefined;\n }\n\n return merge({}, metadata);\n }\n\n // Dor debugging\n public logMemoryFootprint(label = \"current\") {\n console.log(`[${label}] SignalBroker state sizes:`);\n console.log(` • signalObservers entries: ${this.signalObservers.size}`);\n console.log(\n ` • emittedSignalsRegistry size: ${this.emittedSignalsRegistry.size}`,\n );\n\n const exactSignalEntries = Array.from(this.signalObservers.entries())\n .filter(([signal]) => signal.includes(\":\"))\n .sort((left, right) => {\n const taskDelta = right[1].tasks.size - left[1].tasks.size;\n if (taskDelta !== 0) {\n return taskDelta;\n }\n return left[0].localeCompare(right[0]);\n });\n console.log(` • exact signal observer entries: ${exactSignalEntries.length}`);\n for (const [signal, observer] of exactSignalEntries.slice(0, 8)) {\n const sampleTaskNames = Array.from(observer.tasks)\n .slice(0, 3)\n .map((task) => task.name);\n console.log(\n ` - ${signal} (${observer.tasks.size} tasks) sample=${sampleTaskNames.join(\", \")}`,\n );\n }\n\n let totalSquashContexts = 0;\n let totalSquashGroups = 0;\n for (const groups of this.strategyData.values()) {\n totalSquashGroups += groups.size;\n for (const data of groups.values()) {\n totalSquashContexts += data.contexts.length;\n }\n }\n console.log(` • Active squash groups: ${totalSquashGroups}`);\n console.log(` • Total queued squash contexts: ${totalSquashContexts}`);\n\n let totalDebouncers = this.debouncedEmitters.size;\n console.log(` • Active debouncers: ${totalDebouncers}`);\n\n let totalThrottleQueues = 0;\n for (const q of this.throttleQueues.values()) {\n totalThrottleQueues += q.length;\n }\n console.log(` • Total items in throttle queues: ${totalThrottleQueues}`);\n\n let totalScheduled = 0;\n for (const bucket of this.scheduledBuckets.values()) {\n totalScheduled += bucket.length;\n }\n console.log(` • Pending scheduled items: ${totalScheduled}`);\n }\n\n /**\n * Validates the provided signal name string to ensure it adheres to specific formatting rules.\n * Throws an error if any of the validation checks fail.\n *\n * @param {string} signalName - The signal name to be validated.\n * @return {void} - Returns nothing if the signal name is valid.\n * @throws {Error} - Throws an error if the signal name is longer than 100 characters, contains spaces,\n * contains backslashes, or contains uppercase letters in restricted parts of the name.\n */\n validateSignalName(signalName: string): void {\n if (signalName.length > 100) {\n throw new Error(\n `Signal name must be less than 100 characters: ${signalName}`,\n );\n }\n\n if (signalName.includes(\" \")) {\n throw new Error(`Signal name must not contain spaces: ${signalName}\"`);\n }\n\n if (signalName.includes(\"\\\\\")) {\n throw new Error(\n `Signal name must not contain backslashes: ${signalName}`,\n );\n }\n\n if (/[A-Z]/.test(signalName.split(\":\")[0].split(\".\").slice(1).join(\".\"))) {\n throw new Error(\n `Signal name must not contain uppercase letters in the middle of the signal name. It is only allowed in the first part of the signal name: ${signalName}`,\n );\n }\n }\n\n /**\n * Initializes with runners.\n * @param runner Standard runner for user signals.\n * @param metaRunner Meta runner for 'meta.' signals (suppresses further meta-emits).\n */\n bootstrap(runner: GraphRunner, metaRunner: GraphRunner): void {\n this.runner = runner;\n this.metaRunner = metaRunner;\n }\n\n /**\n * Initializes and sets up the various tasks for managing and processing signals.\n *\n * @return {void} This method does not return a value.\n */\n init(): void {\n this.getSignalsTask = Cadenza.createMetaTask(\"Get signals\", (ctx) => {\n const uniqueSignals = Array.from(\n new Set([\n ...this.signalObservers.keys(),\n ...this.emittedSignalsRegistry,\n ]),\n ).filter((s) => !s.includes(\":\"));\n\n const processedSignals = uniqueSignals.map((signal) => ({\n signal,\n data: {\n registered: this.signalObservers.get(signal)?.registered ?? false,\n metadata: this.getSignalMetadata(signal) ?? null,\n },\n }));\n\n return {\n signals: processedSignals,\n ...ctx,\n };\n });\n\n this.registerSignalTask = Cadenza.createMetaTask(\n \"Register signal\",\n (ctx) => {\n const { signalName } = ctx;\n if (!this.signalObservers.has(signalName)) {\n this.addSignal(signalName);\n }\n\n this.signalObservers.get(signalName)!.registered = true;\n },\n ).doOn(\"meta.signal.registered\");\n }\n\n setFlushStrategy(\n name: FlushStrategyName,\n config: { intervalMs: number; maxBatchSize?: number },\n ): void {\n if (config.intervalMs < 50) {\n throw new Error(\"intervalMs must be >= 50ms for performance reasons\");\n }\n\n this.flushStrategies.set(name, {\n intervalMs: config.intervalMs,\n maxBatchSize: config.maxBatchSize ?? 80,\n });\n\n if (!this.strategyData.has(name)) {\n this.strategyData.set(name, new Map());\n this.strategyTimers.set(name, null);\n this.isStrategyFlushing.set(name, false);\n }\n }\n\n updateFlushStrategy(name: FlushStrategyName, config: FlushStrategy): void {\n if (!this.flushStrategies.get(name)) {\n return this.setFlushStrategy(name, config);\n }\n\n this.flushStrategies.set(name, config);\n }\n\n removeFlushStrategy(name: FlushStrategyName): void {\n if (name === this.defaultStrategyName) {\n throw new Error(\"Cannot remove default strategy\");\n }\n const timer = this.strategyTimers.get(name);\n if (timer) clearTimeout(timer);\n this.strategyTimers.delete(name);\n this.strategyData.delete(name);\n this.isStrategyFlushing.delete(name);\n this.flushStrategies.delete(name);\n }\n\n getFlushStrategies(): Record<FlushStrategyName, FlushStrategy> {\n return Object.fromEntries(this.flushStrategies);\n }\n\n // ── Squash (Multi-strategy) ─────────────────────────────────────────\n private readonly MAX_FLUSH_DURATION_MS = 120;\n\n squash(\n signal: string,\n context: AnyObject,\n options: EmitOptions = { squash: true },\n ): void {\n if (!options.squash) {\n this.emit(signal, context, options);\n return;\n }\n\n const strategyName = options.flushStrategy ?? this.defaultStrategyName;\n const strategy = this.flushStrategies.get(strategyName);\n\n if (!strategy) {\n console.warn(\n `Unknown flush strategy '${strategyName}', falling back to default`,\n );\n return this.squash(signal, context, {\n ...options,\n flushStrategy: this.defaultStrategyName,\n });\n }\n\n const squashId = options.squashId ?? signal;\n\n let groups = this.strategyData.get(strategyName)!;\n let data = groups.get(squashId);\n\n if (!data) {\n data = { signal, contexts: [] };\n groups.set(squashId, data);\n }\n\n data.contexts.push(context);\n\n if (data.contexts.length >= strategy.maxBatchSize) {\n this.flushGroup(strategyName, squashId, options);\n return;\n }\n\n if (\n !this.strategyTimers.get(strategyName) &&\n !this.isStrategyFlushing.get(strategyName)\n ) {\n this.strategyTimers.set(\n strategyName,\n setTimeout(() => this.flushStrategy(strategyName), strategy.intervalMs),\n );\n }\n }\n\n private flushGroup(\n strategyName: FlushStrategyName,\n squashId: string,\n options: EmitOptions,\n ): void {\n const groups = this.strategyData.get(strategyName);\n if (!groups) return;\n\n const data = groups.get(squashId);\n if (!data || data.contexts.length === 0) return;\n\n const start = performance.now();\n\n const merged = options.mergeFunction\n ? options.mergeFunction(data.contexts[0], ...data.contexts.slice(1))\n : merge({}, ...data.contexts);\n\n groups.delete(squashId);\n\n this.emit(data.signal, merged, { ...options, squash: false });\n\n const duration = performance.now() - start;\n if (duration > this.MAX_FLUSH_DURATION_MS) {\n console.warn(\n `Squash flush for ${data.signal} in group ${squashId} took ${duration.toFixed(1)}ms`,\n );\n }\n }\n\n private flushStrategy(strategyName: FlushStrategyName): void {\n this.strategyTimers.set(strategyName, null);\n this.isStrategyFlushing.set(strategyName, true);\n\n const groups = this.strategyData.get(strategyName);\n if (!groups || groups.size === 0) {\n this.isStrategyFlushing.set(strategyName, false);\n return;\n }\n\n const keys = Array.from(groups.keys());\n\n for (const squashId of keys) {\n if (groups.has(squashId)) {\n this.flushGroup(strategyName, squashId, {});\n }\n }\n\n this.isStrategyFlushing.set(strategyName, false);\n\n if (groups.size > 0) {\n const strategy = this.flushStrategies.get(strategyName)!;\n this.strategyTimers.set(\n strategyName,\n setTimeout(() => this.flushStrategy(strategyName), strategy.intervalMs),\n );\n }\n }\n\n public clearSquashState(): void {\n for (const [name, timer] of this.strategyTimers.entries()) {\n if (timer) clearTimeout(timer);\n }\n this.strategyTimers.clear();\n this.strategyData.clear();\n this.isStrategyFlushing.clear();\n\n // Keep configured strategies valid after reset/shutdown.\n for (const [name] of this.flushStrategies.entries()) {\n this.strategyData.set(name, new Map());\n this.strategyTimers.set(name, null);\n this.isStrategyFlushing.set(name, false);\n }\n }\n\n private clearScheduledState(): void {\n if (this.scheduleTimer) {\n clearTimeout(this.scheduleTimer);\n this.scheduleTimer = null;\n }\n this.scheduledBuckets.clear();\n }\n\n // ── Schedule (Bucketed) ─────────────────────────────────────────────\n private scheduledBuckets = new Map<\n number,\n Array<{ signal: string; context: AnyObject }>\n >();\n private scheduleTimer: NodeJS.Timeout | null = null;\n\n schedule(\n signal: string,\n context: AnyObject,\n options: EmitOptions = { delayMs: 60_000 },\n ): AbortController {\n let delay = options.delayMs ?? 0;\n if (options.exactDateTime) {\n delay = options.exactDateTime.getTime() - Date.now();\n }\n delay = Math.max(0, delay);\n\n // Bucket by 100ms granularity, but based on an absolute due time\n const dueAt = Date.now() + delay;\n const bucketKey = Math.ceil(dueAt / 100) * 100;\n\n let bucket = this.scheduledBuckets.get(bucketKey);\n if (!bucket) {\n bucket = [];\n this.scheduledBuckets.set(bucketKey, bucket);\n }\n\n bucket.push({ signal, context });\n\n const controller = new AbortController();\n\n if (!this.scheduleTimer) {\n this.scheduleTimer = setTimeout(() => this.flushScheduled(), 50);\n }\n\n return controller;\n }\n\n private flushScheduled(): void {\n this.scheduleTimer = null;\n\n const now = Date.now();\n const toProcess: [number, Array<{ signal: string; context: AnyObject }>][] =\n [];\n\n for (const [bucketKey, items] of this.scheduledBuckets.entries()) {\n const bucketTime = bucketKey; // bucketKey is now an absolute due timestamp\n if (now >= bucketTime - 150) {\n // tolerance\n toProcess.push([bucketKey, items]);\n }\n }\n\n for (const [key, items] of toProcess) {\n this.scheduledBuckets.delete(key);\n for (const item of items) {\n this.emit(item.signal, item.context);\n }\n }\n\n if (this.scheduledBuckets.size > 0) {\n this.scheduleTimer = setTimeout(() => this.flushScheduled(), 50);\n }\n }\n\n // ── Debounce (with cap) ─────────────────────────────────────────────\n private debouncedEmitters = new Map<\n string,\n {\n debouncedFn: ReturnType<typeof debounce>;\n idleTimeout: NodeJS.Timeout | null;\n }\n >();\n\n private readonly MAX_DEBOUNCERS = 5000;\n\n private clearDebounceState(): void {\n for (const entry of this.debouncedEmitters.values()) {\n entry.debouncedFn.cancel();\n if (entry.idleTimeout) {\n clearTimeout(entry.idleTimeout);\n }\n }\n this.debouncedEmitters.clear();\n }\n\n private clearThrottleState(): void {\n for (const queue of this.throttleQueues.values()) {\n queue.length = 0;\n }\n this.throttleQueues.clear();\n this.throttleEmitters.clear();\n }\n\n debounce(signal: string, context: any, options = { delayMs: 500 }) {\n if (this.debouncedEmitters.size > this.MAX_DEBOUNCERS) {\n console.warn(\"Max debouncers reached - evicting oldest\");\n const oldestKey = this.debouncedEmitters.keys().next().value;\n if (oldestKey) {\n const entry = this.debouncedEmitters.get(oldestKey)!;\n entry.debouncedFn.flush();\n this.debouncedEmitters.delete(oldestKey);\n }\n }\n\n const delay = options.delayMs ?? 300;\n const key = signal;\n\n let entry = this.debouncedEmitters.get(key);\n\n if (!entry) {\n const debouncedFn = debounce(\n (ctx: any) => {\n this.emit(signal, ctx);\n entry!.idleTimeout = setTimeout(() => {\n this.debouncedEmitters.delete(key);\n }, delay * 4);\n },\n delay,\n { leading: false, trailing: true },\n );\n\n entry = { debouncedFn, idleTimeout: null };\n this.debouncedEmitters.set(key, entry);\n }\n\n if (entry.idleTimeout) {\n clearTimeout(entry.idleTimeout);\n entry.idleTimeout = null;\n }\n\n entry.debouncedFn(context);\n }\n\n // ── Existing throttle, interval, etc. remain unchanged ──────────────\n throttle(\n signal: string,\n context: any,\n options: EmitOptions = { delayMs: 1000 },\n ) {\n let { groupId, delayMs = 300 } = options;\n if (!groupId) {\n groupId = signal;\n }\n\n if (!this.throttleQueues.has(groupId)) {\n this.throttleQueues.set(groupId, []);\n }\n\n const queue = this.throttleQueues.get(groupId);\n queue.push([signal, context]);\n\n if (!this.throttleEmitters.has(groupId)) {\n this.throttleEmitters.set(groupId, async () => {\n while (queue.length > 0) {\n let batchSize = options.throttleBatch ?? 1;\n if (batchSize > queue.length) {\n batchSize = queue.length;\n }\n for (let i = 0; i < batchSize; i++) {\n const [nextSignal, nextContext] = queue.shift();\n this.emit(nextSignal, nextContext);\n }\n await sleep(delayMs);\n }\n this.throttleEmitters.delete(groupId);\n this.throttleQueues.delete(groupId);\n });\n\n this.throttleEmitters.get(groupId)!();\n }\n }\n\n /**\n * Emits `signal` repeatedly with a fixed interval.\n *\n * @param signal\n * @param context\n * @param intervalMs\n * @param leading If true, emits immediately (unless a startDateTime is given and we are before it).\n * @param startDateTime Optional absolute Date when the *first* emission after `leading` should occur.\n * @returns a handle with `clear()` to stop the loop.\n */\n interval(\n signal: string,\n context: AnyObject,\n intervalMs: number = 60_000,\n leading = false,\n startDateTime?: Date,\n ): ThrottleHandle {\n if (intervalMs <= 0) {\n throw new Error(\"intervalMs must be a positive number\");\n }\n\n const emit = () => this.emit(signal, context);\n\n if (leading) {\n const now = Date.now();\n const start = startDateTime?.getTime();\n\n // If we have a startDateTime and we are already past it → fire now\n if (!start || start <= now) {\n emit();\n }\n }\n\n let firstDelay = intervalMs;\n if (startDateTime) {\n // Find the *next* slot that is >= now\n let slot = startDateTime.getTime();\n const now = Date.now();\n\n while (slot < now) {\n slot += intervalMs;\n }\n firstDelay = slot - now;\n }\n\n let timer: NodeJS.Timeout | null = null;\n let stopped = false;\n\n const scheduleNext = () => {\n if (stopped) return;\n emit();\n timer = setTimeout(scheduleNext, intervalMs);\n };\n\n timer = setTimeout(scheduleNext, firstDelay);\n\n return {\n clear() {\n stopped = true;\n if (timer !== null) clearTimeout(timer);\n },\n };\n }\n\n /**\n * Emits a signal with the specified context, triggering any associated handlers for that signal.\n *\n * @param {string} signal - The name of the signal to emit.\n * @param {AnyObject} [context={}] - An optional context object containing additional information or metadata\n * associated with the signal. If the context includes a `__routineExecId`, it will be handled accordingly.\n * @param options\n * @return {void} This method does not return a value.\n */\n emit(\n signal: string,\n context: AnyObject = {},\n options: EmitOptions = {},\n ): void {\n delete context.__routineExecId;\n if (options.squash) {\n this.squash(signal, context, options);\n return;\n }\n\n if (options.debounce) {\n this.debounce(signal, context);\n return;\n }\n\n if (options.schedule) {\n this.schedule(signal, context, options);\n return;\n }\n\n this.validateSignalName(signal);\n this.execute(signal, context);\n }\n\n /**\n * Executes a signal by emitting events, updating context, and invoking listeners.\n * Creates a new execution trace if necessary and updates the context with relevant metadata.\n * Handles specific, hierarchy-based, and wildcard signals.\n *\n * @param {string} signal - The signal name to be executed, potentially including namespaces or tags (e.g., \"meta.*\" or \"signal:type\").\n * @param {AnyObject} context - An object containing relevant metadata and execution details used for handling the signal.\n * @return {boolean} Returns true if any listeners were successfully executed, otherwise false.\n */\n execute(signal: string, context: AnyObject): boolean {\n const isMeta = signal.includes(\"meta.\");\n const isSubMeta = signal.includes(\"sub_meta.\") || context.__isSubMeta;\n const isReceivedSignalTransmission =\n context.__receivedSignalTransmission === true;\n const isMetric = context.__signalEmission?.isMetric;\n const isNewTrace =\n !context.__signalEmission?.executionTraceId &&\n !context.__metadata?.__executionTraceId &&\n !context.__executionTraceId;\n\n const executionTraceId =\n context.__signalEmission?.executionTraceId ??\n context.__metadata?.__executionTraceId ??\n context.__executionTraceId ??\n uuid();\n\n if (!isSubMeta && (!isMeta || this.debug)) {\n if (isNewTrace) {\n this.emit(\"sub_meta.signal_broker.new_trace\", {\n data: {\n uuid: executionTraceId,\n issuer_type: \"service\", // TODO: Add issuer type\n issuer_id:\n context.__metadata?.__issuerId ?? context.__issuerId ?? null,\n issued_at: formatTimestamp(Date.now()),\n intent: context.__metadata?.__intent ?? context.__intent ?? null,\n context: {\n id: uuid(),\n context: context,\n },\n is_meta: isMeta,\n },\n metadata: {\n __executionTraceId: executionTraceId,\n },\n });\n }\n\n context.__metadata = {\n ...context.__metadata,\n __executionTraceId: executionTraceId,\n };\n\n const emittedAt = Date.now();\n\n const signalParts = signal.split(\":\");\n const signalName = signalParts[0];\n const signalTag = signalParts.length > 1 ? signalParts[1] : null;\n const existingSignalEmission =\n context.__signalEmission && typeof context.__signalEmission === \"object\"\n ? context.__signalEmission\n : {};\n context.__signalEmission = {\n ...existingSignalEmission,\n fullSignalName: existingSignalEmission.fullSignalName ?? signal,\n uuid: existingSignalEmission.uuid ?? uuid(),\n executionTraceId:\n existingSignalEmission.executionTraceId ?? executionTraceId,\n signalName: existingSignalEmission.signalName ?? signalName,\n signalTag: existingSignalEmission.signalTag ?? signalTag,\n emittedAt:\n existingSignalEmission.emittedAt ?? formatTimestamp(emittedAt),\n consumed: existingSignalEmission.consumed ?? false,\n consumedBy: existingSignalEmission.consumedBy ?? null,\n isMeta: existingSignalEmission.isMeta ?? isMeta,\n __traceCreatedBySignalBroker:\n existingSignalEmission.__traceCreatedBySignalBroker ?? isNewTrace,\n };\n\n if (!isReceivedSignalTransmission) {\n if (isNewTrace) {\n context.__traceCreatedBySignalBroker = true;\n }\n\n this.emit(\"sub_meta.signal_broker.emitting_signal\", { ...context });\n\n if (isNewTrace) {\n delete context.__traceCreatedBySignalBroker;\n }\n }\n } else if (isSubMeta) {\n context.__isSubMeta = true;\n }\n\n delete context.__receivedSignalTransmission;\n\n context.__metadata = {\n ...context.__metadata,\n __executionTraceId: executionTraceId,\n };\n\n this.notifyPassiveSignalListeners(signal, context);\n\n if (this.debug && ((!isMetric && !isSubMeta) || this.verbose)) {\n console.log(\n `EMITTING ${signal} to listeners ${this.signalObservers.get(signal)?.tasks.size ?? 0} with context ${this.verbose ? JSON.stringify(context) : JSON.stringify(context).slice(0, 100)}`,\n );\n }\n\n let executed;\n executed = this.executeListener(signal, context); // Exact signal\n\n if (!isSubMeta) {\n const parts = signal\n .slice(0, Math.max(signal.lastIndexOf(\":\"), signal.lastIndexOf(\".\")))\n .split(\".\");\n for (let i = parts.length; i > -1; i--) {\n const parent = parts.slice(0, i).join(\".\");\n executed = this.executeListener(parent + \".*\", context) || executed; // Wildcard\n }\n }\n\n return executed;\n }\n\n /**\n * Executes the tasks associated with a given signal and context.\n * It processes both normal and meta tasks depending on the signal type\n * and the availability of the appropriate runner.\n *\n * @param {string} signal - The signal identifier that determines which tasks to execute.\n * @param {AnyObject} context - The context object passed to the task execution function.\n * @return {boolean} - Returns true if tasks were executed; otherwise, false.\n */\n executeListener(signal: string, context: AnyObject): boolean {\n const obs = this.signalObservers.get(signal);\n if (!obs || obs.tasks.size === 0) {\n return false;\n }\n\n const isMeta = signal.startsWith(\"meta\");\n if (!isMeta) {\n const tasks: Task[] = [];\n const metaTasks: Task[] = [];\n\n obs.tasks.forEach(\n (\n task, // @ts-ignore\n ) => (task.isMeta ? metaTasks.push(task) : tasks.push(task)),\n );\n\n if (tasks.length && this.runner) {\n obs.fn(this.runner, tasks, context);\n }\n\n if (metaTasks.length && this.metaRunner) {\n obs.fn(this.metaRunner, metaTasks, context);\n }\n\n return true;\n } else if (this.metaRunner) {\n obs.fn(this.metaRunner, Array.from(obs.tasks), context);\n return true;\n }\n\n return false;\n }\n\n /**\n * Adds a signal to the signalObservers for tracking and execution.\n * Performs validation on the signal name and emits a meta signal event when added.\n * If the signal contains a namespace (denoted by a colon \":\"), its base signal is\n * also added if it doesn't already exist.\n *\n * @param {string} signal - The name of the signal to be added.\n * @return {void} This method does not return any value.\n */\n addSignal(signal: string, metadata?: SignalMetadata | null): void {\n let _signal = signal;\n if (metadata) {\n this.setSignalMetadata(signal, metadata);\n }\n if (!this.signalObservers.has(_signal)) {\n this.validateSignalName(_signal);\n this.signalObservers.set(_signal, {\n fn: (\n runner: GraphRunner,\n tasks: (Task | GraphRoutine)[],\n context: AnyObject,\n ) => runner.run(tasks, context),\n tasks: new Set(),\n registered: false,\n });\n\n const sections = _signal.split(\":\");\n if (sections.length === 2) {\n _signal = sections[0];\n\n if (!this.signalObservers.has(sections[0])) {\n this.signalObservers.set(_signal, {\n fn: (\n runner: GraphRunner,\n tasks: (Task | GraphRoutine)[],\n context: AnyObject,\n ) => runner.run(tasks, context),\n tasks: new Set(),\n registered: false,\n });\n } else {\n return;\n }\n }\n\n this.emit(\"meta.signal_broker.added\", { signalName: _signal });\n }\n }\n\n /**\n * Observes a signal with a routine/task.\n * @param signal The signal (e.g., 'domain.action', 'domain.*' for wildcards).\n * @param routineOrTask The observer.\n * @edge Duplicates ignored; supports wildcards for broad listening.\n */\n observe(\n signal: string,\n routineOrTask: Task | GraphRoutine,\n metadata?: SignalMetadata | null,\n ): void {\n this.addSignal(signal, metadata);\n this.signalObservers.get(signal)!.tasks.add(routineOrTask);\n }\n\n addPassiveSignalListener(listener: PassiveSignalListener): () => void {\n const listenerId = uuid();\n this.passiveSignalListeners.set(listenerId, listener);\n\n return () => {\n this.passiveSignalListeners.delete(listenerId);\n };\n }\n\n private notifyPassiveSignalListeners(\n signal: string,\n context: AnyObject,\n ): void {\n if (this.passiveSignalListeners.size === 0) {\n return;\n }\n\n const metadata = this.getSignalMetadata(signal) ?? null;\n for (const listener of this.passiveSignalListeners.values()) {\n try {\n listener(signal, context, metadata);\n } catch (error) {\n if (this.debug) {\n console.error(\"Passive signal listener failed\", error);\n }\n }\n }\n }\n\n registerEmittedSignal(signal: string, metadata?: SignalMetadata | null): void {\n const signalKey = this.resolveSignalMetadataKey(signal);\n if (!signalKey) {\n return;\n }\n\n if (metadata) {\n this.setSignalMetadata(signalKey, metadata);\n }\n this.emittedSignalsRegistry.add(signalKey);\n }\n\n /**\n * Unsubscribes a routine/task from a signal.\n * @param signal The signal.\n * @param routineOrTask The observer.\n * @edge Removes all instances if duplicate; deletes if empty.\n */\n unsubscribe(signal: string, routineOrTask: Task | GraphRoutine): void {\n const obs = this.signalObservers.get(signal);\n if (obs) {\n obs.tasks.delete(routineOrTask);\n if (obs.tasks.size === 0) {\n this.signalObservers.delete(signal);\n }\n }\n }\n\n /**\n * Lists all observed signals.\n * @returns Array of signals.\n */\n listObservedSignals(): string[] {\n return Array.from(this.signalObservers.keys());\n }\n\n listEmittedSignals(): string[] {\n return Array.from(this.emittedSignalsRegistry);\n }\n\n reset() {\n this.clearSquashState();\n this.clearScheduledState();\n this.clearDebounceState();\n this.clearThrottleState();\n this.signalObservers.clear();\n this.emittedSignalsRegistry.clear();\n this.signalMetadataRegistry.clear();\n }\n\n public shutdown(): void {\n this.reset();\n }\n}\n","/**\n * Checks if `value` is the\n * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types)\n * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`)\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is an object, else `false`.\n * @example\n *\n * _.isObject({});\n * // => true\n *\n * _.isObject([1, 2, 3]);\n * // => true\n *\n * _.isObject(_.noop);\n * // => true\n *\n * _.isObject(null);\n * // => false\n */\nfunction isObject(value) {\n var type = typeof value;\n return value != null && (type == 'object' || type == 'function');\n}\n\nexport default isObject;\n","/** Detect free variable `global` from Node.js. */\nvar freeGlobal = typeof global == 'object' && global && global.Object === Object && global;\n\nexport default freeGlobal;\n","import freeGlobal from './_freeGlobal.js';\n\n/** Detect free variable `self`. */\nvar freeSelf = typeof self == 'object' && self && self.Object === Object && self;\n\n/** Used as a reference to the global object. */\nvar root = freeGlobal || freeSelf || Function('return this')();\n\nexport default root;\n","import root from './_root.js';\n\n/**\n * Gets the timestamp of the number of milliseconds that have elapsed since\n * the Unix epoch (1 January 1970 00:00:00 UTC).\n *\n * @static\n * @memberOf _\n * @since 2.4.0\n * @category Date\n * @returns {number} Returns the timestamp.\n * @example\n *\n * _.defer(function(stamp) {\n * console.log(_.now() - stamp);\n * }, _.now());\n * // => Logs the number of milliseconds it took for the deferred invocation.\n */\nvar now = function() {\n return root.Date.now();\n};\n\nexport default now;\n","/** Used to match a single whitespace character. */\nvar reWhitespace = /\\s/;\n\n/**\n * Used by `_.trim` and `_.trimEnd` to get the index of the last non-whitespace\n * character of `string`.\n *\n * @private\n * @param {string} string The string to inspect.\n * @returns {number} Returns the index of the last non-whitespace character.\n */\nfunction trimmedEndIndex(string) {\n var index = string.length;\n\n while (index-- && reWhitespace.test(string.charAt(index))) {}\n return index;\n}\n\nexport default trimmedEndIndex;\n","import trimmedEndIndex from './_trimmedEndIndex.js';\n\n/** Used to match leading whitespace. */\nvar reTrimStart = /^\\s+/;\n\n/**\n * The base implementation of `_.trim`.\n *\n * @private\n * @param {string} string The string to trim.\n * @returns {string} Returns the trimmed string.\n */\nfunction baseTrim(string) {\n return string\n ? string.slice(0, trimmedEndIndex(string) + 1).replace(reTrimStart, '')\n : string;\n}\n\nexport default baseTrim;\n","import root from './_root.js';\n\n/** Built-in value references. */\nvar Symbol = root.Symbol;\n\nexport default Symbol;\n","import Symbol from './_Symbol.js';\n\n/** Used for built-in method references. */\nvar objectProto = Object.prototype;\n\n/** Used to check objects for own properties. */\nvar hasOwnProperty = objectProto.hasOwnProperty;\n\n/**\n * Used to resolve the\n * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)\n * of values.\n */\nvar nativeObjectToString = objectProto.toString;\n\n/** Built-in value references. */\nvar symToStringTag = Symbol ? Symbol.toStringTag : undefined;\n\n/**\n * A specialized version of `baseGetTag` which ignores `Symbol.toStringTag` values.\n *\n * @private\n * @param {*} value The value to query.\n * @returns {string} Returns the raw `toStringTag`.\n */\nfunction getRawTag(value) {\n var isOwn = hasOwnProperty.call(value, symToStringTag),\n tag = value[symToStringTag];\n\n try {\n value[symToStringTag] = undefined;\n var unmasked = true;\n } catch (e) {}\n\n var result = nativeObjectToString.call(value);\n if (unmasked) {\n if (isOwn) {\n value[symToStringTag] = tag;\n } else {\n delete value[symToStringTag];\n }\n }\n return result;\n}\n\nexport default getRawTag;\n","/** Used for built-in method references. */\nvar objectProto = Object.prototype;\n\n/**\n * Used to resolve the\n * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)\n * of values.\n */\nvar nativeObjectToString = objectProto.toString;\n\n/**\n * Converts `value` to a string using `Object.prototype.toString`.\n *\n * @private\n * @param {*} value The value to convert.\n * @returns {string} Returns the converted string.\n */\nfunction objectToString(value) {\n return nativeObjectToString.call(value);\n}\n\nexport default objectToString;\n","import Symbol from './_Symbol.js';\nimport getRawTag from './_getRawTag.js';\nimport objectToString from './_objectToString.js';\n\n/** `Object#toString` result references. */\nvar nullTag = '[object Null]',\n undefinedTag = '[object Undefined]';\n\n/** Built-in value references. */\nvar symToStringTag = Symbol ? Symbol.toStringTag : undefined;\n\n/**\n * The base implementation of `getTag` without fallbacks for buggy environments.\n *\n * @private\n * @param {*} value The value to query.\n * @returns {string} Returns the `toStringTag`.\n */\nfunction baseGetTag(value) {\n if (value == null) {\n return value === undefined ? undefinedTag : nullTag;\n }\n return (symToStringTag && symToStringTag in Object(value))\n ? getRawTag(value)\n : objectToString(value);\n}\n\nexport default baseGetTag;\n","/**\n * Checks if `value` is object-like. A value is object-like if it's not `null`\n * and has a `typeof` result of \"object\".\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is object-like, else `false`.\n * @example\n *\n * _.isObjectLike({});\n * // => true\n *\n * _.isObjectLike([1, 2, 3]);\n * // => true\n *\n * _.isObjectLike(_.noop);\n * // => false\n *\n * _.isObjectLike(null);\n * // => false\n */\nfunction isObjectLike(value) {\n return value != null && typeof value == 'object';\n}\n\nexport default isObjectLike;\n","import baseGetTag from './_baseGetTag.js';\nimport isObjectLike from './isObjectLike.js';\n\n/** `Object#toString` result references. */\nvar symbolTag = '[object Symbol]';\n\n/**\n * Checks if `value` is classified as a `Symbol` primitive or object.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is a symbol, else `false`.\n * @example\n *\n * _.isSymbol(Symbol.iterator);\n * // => true\n *\n * _.isSymbol('abc');\n * // => false\n */\nfunction isSymbol(value) {\n return typeof value == 'symbol' ||\n (isObjectLike(value) && baseGetTag(value) == symbolTag);\n}\n\nexport default isSymbol;\n","import baseTrim from './_baseTrim.js';\nimport isObject from './isObject.js';\nimport isSymbol from './isSymbol.js';\n\n/** Used as references for various `Number` constants. */\nvar NAN = 0 / 0;\n\n/** Used to detect bad signed hexadecimal string values. */\nvar reIsBadHex = /^[-+]0x[0-9a-f]+$/i;\n\n/** Used to detect binary string values. */\nvar reIsBinary = /^0b[01]+$/i;\n\n/** Used to detect octal string values. */\nvar reIsOctal = /^0o[0-7]+$/i;\n\n/** Built-in method references without a dependency on `root`. */\nvar freeParseInt = parseInt;\n\n/**\n * Converts `value` to a number.\n *\n * @static\n * @memberOf _\n * @since 4.0.0\n * @category Lang\n * @param {*} value The value to process.\n * @returns {number} Returns the number.\n * @example\n *\n * _.toNumber(3.2);\n * // => 3.2\n *\n * _.toNumber(Number.MIN_VALUE);\n * // => 5e-324\n *\n * _.toNumber(Infinity);\n * // => Infinity\n *\n * _