UNPKG

@zubridge/electron

Version:

A streamlined state management library for Electron applications using Zustand.

1,504 lines (1,494 loc) 87 kB
// Node.js build with bundled dependencies import { randomUUID } from "node:crypto"; import { EventEmitter } from "node:events"; //#region ../core/dist/utils/debug.js /** * Debug logging utility for Zubridge packages * Supports optional weald and @wdio/logger dependencies that are loaded dynamically * Falls back to console logging in sandbox or when dependencies are unavailable */ const isWDIOTest = () => { if (typeof process === "undefined") return false; return typeof globalThis.browser !== "undefined" || process.env.WDIO === "true" || !!process.env.WDIO_LOG_LEVEL; }; const isDebugEnabled = () => { if (typeof process !== "undefined" && process.env?.DEBUG) { const debugPattern = process.env.DEBUG; return debugPattern === "*" || debugPattern.includes("zubridge"); } if (typeof localStorage !== "undefined") try { const debug = localStorage.getItem("debug"); return debug === "*" || debug?.includes("zubridge") || false; } catch { return false; } return false; }; let wdioLogger = null; let wealdDebug = null; let loggersInitialized = false; /** * Attempt to load optional logging dependencies dynamically */ async function initializeOptionalLoggers() { if (loggersInitialized) return; loggersInitialized = true; if (isWDIOTest()) try { const wdioModule = await import("@wdio/logger"); wdioLogger = (wdioModule.default || wdioModule)("zubridge"); } catch {} if (!isWDIOTest()) try { const wealdModule = await import("weald"); wealdDebug = wealdModule.default || wealdModule; } catch {} } initializeOptionalLoggers(); /** * Create a debug function for a specific namespace */ function createDebugger(namespace) { return (...args) => { if (!isDebugEnabled()) return; if (wdioLogger) { if (namespace.endsWith(":error")) wdioLogger.error(namespace, ...args); else if (namespace.endsWith(":warn")) wdioLogger.warn(namespace, ...args); else if (namespace.endsWith(":info")) wdioLogger.info(namespace, ...args); else wdioLogger.debug(namespace, ...args); return; } if (wealdDebug) { wealdDebug(namespace)(...args); return; } console.log(`[${namespace}]`, ...args); }; } const debuggers = { core: createDebugger("zubridge:core"), ipc: createDebugger("zubridge:ipc"), store: createDebugger("zubridge:store"), adapters: createDebugger("zubridge:adapters"), windows: createDebugger("zubridge:windows"), serialization: createDebugger("zubridge:serialization") }; const dynamicDebuggers = /* @__PURE__ */ new Map(); /** * Get or create a debugger for the specified area */ function getDebugger(area) { if (area in debuggers) return debuggers[area]; if (!dynamicDebuggers.has(area)) dynamicDebuggers.set(area, createDebugger(`zubridge:${area}`)); const debugFn = dynamicDebuggers.get(area); if (!debugFn) throw new Error(`Failed to create debugger for area: ${area}`); return debugFn; } /** * Log a debug message */ function debugLog(area, ...args) { getDebugger(area)(...args); } //#endregion //#region src/errors/index.ts /** * Custom Error classes for different areas of the Zubridge application * Provides better type safety, debugging, and error handling */ /** * Base class for all Zubridge errors */ var ZubridgeError = class extends Error { timestamp; context; constructor(message, context) { super(message); this.name = this.constructor.name; this.timestamp = Date.now(); this.context = context; if (Error.captureStackTrace) Error.captureStackTrace(this, this.constructor); } /** * Get error details for logging */ getDetails() { return { name: this.name, message: this.message, timestamp: this.timestamp, context: this.context, stack: this.stack }; } }; /** * Errors related to IPC communication between main and renderer processes */ var IpcCommunicationError = class extends ZubridgeError { channel; windowId; constructor(message, context) { super(message, context); this.channel = context?.channel; this.windowId = context?.windowId; } }; /** * Errors related to action processing in adapters */ var ActionProcessingError = class extends ZubridgeError { actionType; adapter; handlerName; constructor(message, actionType, adapter, context) { super(message, context); this.actionType = actionType; this.adapter = adapter; this.handlerName = context?.handlerName; } }; /** * Errors related to resource management and cleanup */ var ResourceManagementError = class extends ZubridgeError { resourceType; operation; constructor(message, resourceType, operation, context) { super(message, context); this.resourceType = resourceType; this.operation = operation; } }; /** * Type guard to check if an error is a ZubridgeError */ function isZubridgeError(error) { return error instanceof ZubridgeError; } /** * Convert any error to a ZubridgeError if it's not already one */ function ensureZubridgeError(error, fallbackMessage = "Unknown error") { if (isZubridgeError(error)) return error; if (error instanceof Error) { const originalError = error; return new class extends ZubridgeError { constructor() { super(originalError.message); this.name = originalError.name; this.stack = originalError.stack; } }(); } const errorMessage = typeof error === "string" ? error : fallbackMessage; return new class extends ZubridgeError { constructor() { super(errorMessage, { originalError: error, originalType: typeof error }); } }(); } //#endregion //#region src/batching/types.ts const BATCHING_DEFAULTS = { windowMs: 16, maxBatchSize: 50, priorityFlushThreshold: 80, ackTimeoutMs: typeof process !== "undefined" && process.platform === "linux" ? 6e4 : 3e4 }; /** * Priority levels for action scheduling and batching. * Higher values indicate higher priority. * * Priority assignment rules: * - IMMEDIATE (100): Actions with __immediate flag, always execute immediately * - ROOT_THUNK_ACTION (70): Actions belonging to the active root thunk * - NORMAL_THUNK_ACTION (50): Regular thunk-dispatched actions (renderer default) * - NORMAL_ACTION (0): Regular actions without special flags (main process default) * * Note: ActionBatcher (renderer) uses NORMAL_THUNK_ACTION as default, * while ActionScheduler (main) uses NORMAL_ACTION. This distinction * allows different priority handling in renderer vs main process. */ const PRIORITY_LEVELS = { IMMEDIATE: 100, ROOT_THUNK_ACTION: 70, NORMAL_THUNK_ACTION: 50, NORMAL_ACTION: 0 }; //#endregion //#region src/action/ActionScheduler.ts /** * Events emitted by ActionScheduler */ let ActionSchedulerEvents = /* @__PURE__ */ function(ActionSchedulerEvents$1) { ActionSchedulerEvents$1["ACTION_ENQUEUED"] = "action:enqueued"; ActionSchedulerEvents$1["ACTION_STARTED"] = "action:started"; ActionSchedulerEvents$1["ACTION_COMPLETED"] = "action:completed"; ActionSchedulerEvents$1["ACTION_FAILED"] = "action:failed"; return ActionSchedulerEvents$1; }({}); /** * ActionScheduler is responsible for scheduling action execution with proper concurrency control. * It works with ThunkManager to determine when actions can be executed. */ var ActionScheduler = class extends EventEmitter { /** * Queue of actions waiting to be processed */ queue = []; /** * Maximum queue size before overflow handling */ maxQueueSize = 1e3; /** * Number of actions dropped due to overflow */ droppedActionsCount = 0; /** * Set of action IDs currently being processed */ runningActions = /* @__PURE__ */ new Set(); /** * Flag to prevent recursive queue processing */ processing = false; /** * Flag to indicate queue needs sorting before processing * This deferred sorting approach avoids O(n log n) sort on every enqueue, * instead sorting only once when processing the queue. * Performance improvement: O(n log n) per process vs O(n² log n) for n enqueues */ needsSort = false; /** * ThunkManager reference for concurrency decisions */ thunkManager; /** * Function to process actions */ actionProcessor; /** * Create a new ActionScheduler */ constructor(thunkManager$2) { super(); this.thunkManager = thunkManager$2; debugLog("scheduler", "ActionScheduler initialized"); this.thunkManager.on("thunk:root:completed", () => { debugLog("scheduler", "Root thunk completed, processing queue"); this.processQueue(); }); } /** * Set the action processor function */ setActionProcessor(processor) { debugLog("scheduler", "Setting action processor"); this.actionProcessor = processor; } /** * Enqueue an action for execution * Returns true if the action was executed immediately, false if it was queued */ enqueueAction(action, options) { const { sourceWindowId, onComplete } = options; if (!action.__id) action.__id = randomUUID(); debugLog("scheduler", `Enqueueing action: ${action.type} (id: ${action.__id}) from window ${sourceWindowId}${action.__thunkParentId ? `, parent thunk: ${action.__thunkParentId}` : ""}`); action.__sourceWindowId = sourceWindowId; if (this.canExecuteImmediately(action)) { debugLog("scheduler", `Action ${action.type} (${action.__id}) can execute immediately`); this.executeAction(action, sourceWindowId, onComplete); return true; } if (this.queue.length >= this.maxQueueSize) { if (!this.handleQueueOverflow(action)) { debugLog("scheduler", `Action ${action.type} (${action.__id}) rejected due to queue overflow`); onComplete?.(new ResourceManagementError("Action queue overflow", "action_queue", "enqueue", { queueSize: this.queue.length, maxSize: this.maxQueueSize, actionType: action.type })); return false; } } debugLog("scheduler", `Action ${action.type} (${action.__id}) queued for later execution`); this.queue.push({ action, sourceWindowId, receivedTime: Date.now(), priority: this.getPriorityForAction(action), onComplete }); this.needsSort = true; this.emit(ActionSchedulerEvents.ACTION_ENQUEUED, action); return false; } /** * Check if an action can be executed immediately based on concurrency rules */ canExecuteImmediately(action) { debugLog("scheduler-debug", `[DECISION] Evaluating if action ${action.type} (${action.__id}) can execute immediately`); debugLog("scheduler-debug", `[DECISION] Action details: parentThunkId=${action.__thunkParentId}, immediate=${action.__immediate}`); if (action.__immediate) { debugLog("scheduler", `Action ${action.type} (${action.__id}) has immediate, can execute immediately`); return true; } const rootThunkId = this.thunkManager.getRootThunkId(); const hasActiveThunk = rootThunkId && this.thunkManager.isThunkActive(rootThunkId); debugLog("scheduler-debug", `[DECISION] Root thunk: ${rootThunkId || "none"}, active: ${hasActiveThunk}`); if (hasActiveThunk && !action.__thunkParentId) { debugLog("scheduler", `Active thunk ${rootThunkId} exists and ${action.type} (${action.__id}) is not a thunk action, must wait`); return false; } if (action.__thunkParentId && hasActiveThunk) { const belongsToRootThunk = action.__thunkParentId === rootThunkId; debugLog("scheduler-debug", `[DECISION] Action belongs to root thunk: ${belongsToRootThunk}`); if (!belongsToRootThunk) { debugLog("scheduler", `Thunk action ${action.type} (${action.__id}) belongs to thunk ${action.__thunkParentId}, not root thunk ${rootThunkId}, must wait`); return false; } } if (!hasActiveThunk) { debugLog("scheduler", `No active thunk, action ${action.type} (${action.__id}) can execute immediately`); return true; } const runningTasks = this.getScheduler().getRunningTasks(); debugLog("scheduler-debug", `[DECISION] Running tasks: ${runningTasks.length}`); if (runningTasks.length === 0) { debugLog("scheduler", `No running tasks, action ${action.type} (${action.__id}) can execute immediately`); return true; } const hasBlockingTask = runningTasks.some((task) => !task.canRunConcurrently); debugLog("scheduler-debug", `[DECISION] Has blocking tasks: ${hasBlockingTask}`); if (!hasBlockingTask) { debugLog("scheduler", `No blocking tasks running, action ${action.type} (${action.__id}) can execute immediately`); return true; } if (action.__thunkParentId) { const belongsToRunningThunk = runningTasks.some((task) => task.thunkId === action.__thunkParentId); debugLog("scheduler-debug", `[DECISION] Action belongs to running thunk: ${belongsToRunningThunk}`); if (belongsToRunningThunk) { debugLog("scheduler", `Action ${action.type} (${action.__id}) belongs to running thunk ${action.__thunkParentId}, can execute immediately`); return true; } debugLog("scheduler", `Thunk action ${action.type} (${action.__id}) must wait for current thunk to complete`); return false; } debugLog("scheduler", `Action ${action.type} (${action.__id}) must wait due to blocking tasks running`); return false; } /** * Process the queue, attempting to execute any pending actions * that can now be executed based on concurrency rules */ processQueue() { if (this.processing || !this.actionProcessor) return; this.processing = true; try { debugLog("scheduler", `Processing queue with ${this.queue.length} actions`); if (this.needsSort) { this.sortQueue(); this.needsSort = false; } const executableActions = []; const remainingActions = []; for (const queuedAction of this.queue) if (this.canExecuteImmediately(queuedAction.action)) executableActions.push(queuedAction); else remainingActions.push(queuedAction); this.queue = remainingActions; for (const queuedAction of executableActions) this.executeAction(queuedAction.action, queuedAction.sourceWindowId, queuedAction.onComplete); debugLog("scheduler", `Executed ${executableActions.length} actions, ${this.queue.length} remaining in queue`); } finally { this.processing = false; } } /** * Execute a single action with the action processor */ async executeAction(action, sourceWindowId, onComplete) { if (!this.actionProcessor) { debugLog("scheduler", "No action processor set, cannot execute action"); onComplete?.(/* @__PURE__ */ new Error("No action processor set")); return; } const actionId = action.__id; this.runningActions.add(actionId); this.emit(ActionSchedulerEvents.ACTION_STARTED, action); debugLog("scheduler", `Executing action ${action.type} (${actionId}) from window ${sourceWindowId}`); try { const result = await this.actionProcessor(action); debugLog("scheduler", `Action ${action.type} (${actionId}) completed successfully`); this.runningActions.delete(actionId); if (action.__thunkParentId) this.thunkManager.handleActionComplete(actionId); try { onComplete?.(null); } catch (callbackError) { debugLog("scheduler", `Error in action completion callback: ${callbackError}`); } this.emit(ActionSchedulerEvents.ACTION_COMPLETED, action, result); this.processQueue(); } catch (error) { debugLog("scheduler", `Action ${action.type} (${actionId}) failed: ${error}`); this.runningActions.delete(actionId); try { onComplete?.(error instanceof Error ? error : new Error(String(error))); } catch (callbackError) { debugLog("scheduler", `Error in action error callback: ${callbackError}`); } this.emit(ActionSchedulerEvents.ACTION_FAILED, action, error); this.processQueue(); } } /** * Sort the queue by priority (highest first) and then by received time (earliest first) */ sortQueue() { this.queue.sort((a, b) => { if (a.priority !== b.priority) return b.priority - a.priority; return a.receivedTime - b.receivedTime; }); } /** * Notify the scheduler that a thunk has completed * This may allow queued actions to be processed */ onThunkCompleted(thunkId) { debugLog("scheduler", `Thunk ${thunkId} completed, processing queue`); this.processQueue(); } /** * Notify the scheduler that an action has completed * This may allow dependent actions to be processed */ onActionCompleted(actionId) { debugLog("scheduler", `Action ${actionId} completed, processing queue`); this.runningActions.delete(actionId); this.processQueue(); } /** * Get the priority for an action * Higher priority actions are executed first * * This method uses PRIORITY_LEVELS constants for consistency with ActionBatcher. * However, ActionScheduler has additional context-aware logic: * - It can determine if a thunk action belongs to the active root thunk * - It distinguishes between thunk actions with and without bypass flags * * Priority assignment: * - IMMEDIATE (100): Actions with __immediate flag * - ROOT_THUNK_ACTION (70): Actions belonging to the active root thunk * - NORMAL_THUNK_ACTION (50): Other thunk-dispatched actions * - NORMAL_ACTION (0): Regular actions */ getPriorityForAction(action) { if (action.__immediate) return PRIORITY_LEVELS.IMMEDIATE; const rootThunkId = this.thunkManager.getRootThunkId(); if (rootThunkId && action.__thunkParentId === rootThunkId) return PRIORITY_LEVELS.ROOT_THUNK_ACTION; if (action.__thunkParentId) return PRIORITY_LEVELS.NORMAL_THUNK_ACTION; return PRIORITY_LEVELS.NORMAL_ACTION; } /** * Handle queue overflow by dropping low-priority actions * Returns true if the new action should be accepted, false if rejected */ handleQueueOverflow(newAction) { debugLog("scheduler:overflow", `Queue overflow detected (${this.queue.length}/${this.maxQueueSize})`); const newPriority = this.getPriorityForAction(newAction); const droppableActions = this.queue.filter((queuedAction) => queuedAction.priority < PRIORITY_LEVELS.NORMAL_THUNK_ACTION).sort((a, b) => a.priority - b.priority || a.receivedTime - b.receivedTime); if (droppableActions.length === 0) { if (newPriority < PRIORITY_LEVELS.NORMAL_THUNK_ACTION) { debugLog("scheduler:overflow", "No droppable actions found and new action is low priority, rejecting"); return false; } debugLog("scheduler:overflow", "No droppable actions found but new action is high priority, forcing acceptance"); const oldestAction = this.queue.reduce((oldest, current) => current.receivedTime < oldest.receivedTime ? current : oldest); this.removeActionFromQueue(oldestAction); this.droppedActionsCount++; return true; } const actionToDrop = droppableActions[0]; this.removeActionFromQueue(actionToDrop); this.droppedActionsCount++; debugLog("scheduler:overflow", `Dropped action ${actionToDrop.action.type} (priority ${actionToDrop.priority}) to make room`); actionToDrop.onComplete?.(new ResourceManagementError("Action dropped due to queue overflow", "action_queue", "overflow", { droppedActionType: actionToDrop.action.type, newActionType: newAction.type, queueSize: this.queue.length })); return true; } /** * Remove an action from the queue */ removeActionFromQueue(actionToRemove) { const index = this.queue.indexOf(actionToRemove); if (index !== -1) this.queue.splice(index, 1); } /** * Get queue statistics */ getQueueStats() { const priorityDistribution = {}; for (const queuedAction of this.queue) { const priority = queuedAction.priority; const priorityName = priority >= PRIORITY_LEVELS.ROOT_THUNK_ACTION ? "High" : priority >= PRIORITY_LEVELS.NORMAL_THUNK_ACTION ? "Normal" : "Low"; priorityDistribution[priorityName] = (priorityDistribution[priorityName] || 0) + 1; } return { currentSize: this.queue.length, maxSize: this.maxQueueSize, droppedActionsCount: this.droppedActionsCount, priorityDistribution }; } /** * Get the ThunkScheduler instance */ getScheduler() { return this.thunkManager.getScheduler(); } }; let actionSchedulerInstance; /** * Initialize the global ActionScheduler */ function initActionScheduler(thunkManager$2) { actionSchedulerInstance = new ActionScheduler(thunkManager$2); return actionSchedulerInstance; } //#endregion //#region src/constants.ts /** * Constants used for IPC communication between main and renderer processes. * These are internal to the Zubridge electron implementation. */ let IpcChannel = /* @__PURE__ */ function(IpcChannel$1) { /** * Used by renderer to send actions to main process */ IpcChannel$1["DISPATCH"] = "zubridge:dispatch"; /** * Used by main process to acknowledge action receipt */ IpcChannel$1["DISPATCH_ACK"] = "zubridge:dispatch-ack"; /** * Used by renderer to get state from main process */ IpcChannel$1["GET_STATE"] = "zubridge:get-state"; /** * Used by main process to send state updates to renderer */ IpcChannel$1["SUBSCRIBE"] = "zubridge:subscribe"; /** * Used by main process to send state updates to renderer with tracking */ IpcChannel$1["STATE_UPDATE"] = "zubridge:state-update"; /** * Used by renderer to acknowledge receipt of state update */ IpcChannel$1["STATE_UPDATE_ACK"] = "zubridge:state-update-ack"; /** * Used by renderer to register a thunk with main process */ IpcChannel$1["REGISTER_THUNK"] = "zubridge:register-thunk"; /** * Used by main process to acknowledge thunk registration */ IpcChannel$1["REGISTER_THUNK_ACK"] = "zubridge:register-thunk-ack"; /** * Used by renderer to notify main process of thunk completion */ IpcChannel$1["COMPLETE_THUNK"] = "zubridge:complete-thunk"; /** * Used by renderer to get window ID from main process */ IpcChannel$1["GET_WINDOW_ID"] = "zubridge:get-window-id"; /** * Used by renderer to get current thunk state from main process */ IpcChannel$1["GET_THUNK_STATE"] = "zubridge:get-thunk-state"; /** * Used by renderer to track action dispatch for performance metrics */ IpcChannel$1["TRACK_ACTION_DISPATCH"] = "zubridge:track-action-dispatch"; /** * Used by renderer to get window subscriptions from main process */ IpcChannel$1["GET_WINDOW_SUBSCRIPTIONS"] = "zubridge:get-window-subscriptions"; /** * Used by renderer to send batched actions to main process */ IpcChannel$1["BATCH_DISPATCH"] = "zubridge:batch-dispatch"; /** * Used by main process to acknowledge batch dispatch */ IpcChannel$1["BATCH_ACK"] = "zubridge:batch-ack"; return IpcChannel$1; }({}); /** * Defines priority levels for thunks in the scheduler * Higher numbers indicate higher priority */ let ThunkPriority = /* @__PURE__ */ function(ThunkPriority$1) { /** * Highest priority thunks that can run concurrently with other thunks * Used for bypass thunks that should not be blocked */ ThunkPriority$1[ThunkPriority$1["BYPASS"] = 100] = "BYPASS"; /** * High priority thunks * Reserved for future use (high priority but not bypass) */ ThunkPriority$1[ThunkPriority$1["HIGH"] = 75] = "HIGH"; /** * Standard priority for most thunks * This is the default priority level */ ThunkPriority$1[ThunkPriority$1["NORMAL"] = 50] = "NORMAL"; /** * Lower priority thunks * Used for background tasks that can wait */ ThunkPriority$1[ThunkPriority$1["LOW"] = 25] = "LOW"; /** * Lowest priority * Only runs when nothing else is running */ ThunkPriority$1[ThunkPriority$1["IDLE"] = 0] = "IDLE"; return ThunkPriority$1; }({}); /** * Events emitted by the ThunkScheduler */ const ThunkSchedulerEvents = { TASK_QUEUED: "task:queued", TASK_STARTED: "task:started", TASK_COMPLETED: "task:completed", TASK_FAILED: "task:failed", QUEUE_CHANGED: "queue:changed" }; //#endregion //#region src/thunk/scheduling/ThunkScheduler.ts /** * Priority queue-based implementation of ThunkScheduler */ var ThunkScheduler = class extends EventEmitter { /** * Queue of tasks waiting to be executed * Sorted by priority (highest first) and then by start time (earliest first) */ queue = []; /** * Tasks currently executing */ runningTasks = /* @__PURE__ */ new Map(); /** * Whether the scheduler is actively processing the queue */ isProcessing = false; /** * Callback to notify when a thunk action task completes * Used to trigger ActionScheduler.processQueue() after each thunk action */ onTaskCompletedCallback; constructor() { super(); debugLog("scheduler", "ThunkScheduler initialized"); } /** * Set a callback to be called when a task completes * This allows ActionScheduler to re-process its queue after each thunk action */ setOnTaskCompletedCallback(callback) { this.onTaskCompletedCallback = callback; } /** * Enqueue a task for execution * Task will be executed when it reaches the front of the queue and has no conflicts */ enqueue(task) { debugLog("scheduler", `Enqueuing task ${task.id} for thunk ${task.thunkId} (priority: ${task.priority}, canRunConcurrently: ${task.canRunConcurrently})`); debugLog("scheduler", "Task details:", task); debugLog("scheduler", "Current running tasks:", Array.from(this.runningTasks.values()).map((t) => `${t.id} (thunk: ${t.thunkId}, canRunConcurrently: ${t.canRunConcurrently})`)); this.queue.push(task); this.sortQueue(); this.processQueue(); debugLog("scheduler-debug", `Queue state after enqueueing task ${task.id}: ${JSON.stringify(this.getQueueStatus())}`); debugLog("scheduler-debug", `Queue items: ${this.queue.length}, Running tasks: ${this.runningTasks.size}`); } /** * Get all tasks currently running */ getRunningTasks() { return Array.from(this.runningTasks.values()); } /** * Get the status of the task queue */ getQueueStatus() { const queuedTasks = this.queue.length; const runningTasks = this.runningTasks.size; return { queuedTasks, runningTasks, highestPriorityQueued: queuedTasks > 0 ? this.queue[0].priority : -1, isIdle: queuedTasks === 0 && runningTasks === 0 }; } /** * Process the task queue * This will execute any tasks that have no conflicts */ processQueue() { if (this.isProcessing) { debugLog("scheduler-debug", "Already processing queue, returning"); return; } this.isProcessing = true; try { debugLog("scheduler-debug", `Processing queue with ${this.queue.length} tasks`); let tasksStarted = 0; let i = 0; while (i < this.queue.length) { const task = this.queue[i]; debugLog("scheduler-debug", `Checking task ${task.id} (thunk: ${task.thunkId}, canRunConcurrently: ${task.canRunConcurrently})`); if (this.hasConflicts(task)) { debugLog("scheduler-debug", `Task ${task.id} has conflicts with running tasks, skipping`); i++; continue; } debugLog("scheduler", `Starting task ${task.id} for thunk ${task.thunkId}`); this.queue.splice(i, 1); this.runningTasks.set(task.id, task); tasksStarted++; this.emit(ThunkSchedulerEvents.TASK_STARTED, task); this.executeTask(task); } debugLog("scheduler-debug", `Queue processing complete, started ${tasksStarted} tasks`); debugLog("scheduler-debug", `Queue state after processing: ${JSON.stringify(this.getQueueStatus())}`); if (tasksStarted > 0) { debugLog("scheduler-debug", "Running tasks:"); for (const [id, task] of this.runningTasks.entries()) debugLog("scheduler-debug", ` ${id}: thunk=${task.thunkId}, canRunConcurrently=${task.canRunConcurrently}`); } } finally { this.isProcessing = false; } } /** * Check if a task has conflicts with any running tasks * A task has conflicts if it can't run concurrently and any running task * affects the same keys */ hasConflicts(task) { debugLog("scheduler-debug", `Checking conflicts for task ${task.id} (canRunConcurrently: ${task.canRunConcurrently})`); debugLog("scheduler-debug", `Currently running tasks: [${Array.from(this.runningTasks.values()).map((t) => `${t.id} (thunk: ${t.thunkId}, canRunConcurrently: ${t.canRunConcurrently})`).join(", ")}]`); if (this.runningTasks.size === 0) { debugLog("scheduler-debug", `No running tasks, task ${task.id} has no conflicts`); return false; } if (task.canRunConcurrently) { debugLog("scheduler-debug", `Task ${task.id} can run concurrently, no conflicts`); return false; } for (const runningTask of this.runningTasks.values()) { if (runningTask.canRunConcurrently) continue; debugLog("scheduler-debug", `Checking for conflicts between task ${task.id} and running task ${runningTask.id}`); debugLog("scheduler-debug", `Conflict: task ${task.id} cannot run concurrently with task ${runningTask.id}`); return true; } debugLog("scheduler-debug", `Task ${task.id} has no conflicts with running tasks`); return false; } /** * Execute a task asynchronously */ executeTask(task) { debugLog("scheduler", `Starting execution of task ${task.id} (thunk: ${task.thunkId}, canRunConcurrently: ${task.canRunConcurrently})`); task.handler().then(() => { debugLog("scheduler", `Task ${task.id} completed successfully (thunk: ${task.thunkId}, canRunConcurrently: ${task.canRunConcurrently})`); this.runningTasks.delete(task.id); this.emit(ThunkSchedulerEvents.TASK_COMPLETED, task); this.processQueue(); if (this.onTaskCompletedCallback) this.onTaskCompletedCallback(); debugLog("scheduler-debug", `Queue state after task ${task.id} completed: ${JSON.stringify(this.getQueueStatus())}`); }).catch((error) => { debugLog("scheduler", `Task ${task.id} failed with error: ${error}`); this.runningTasks.delete(task.id); this.emit(ThunkSchedulerEvents.TASK_FAILED, task, error); this.processQueue(); if (this.onTaskCompletedCallback) this.onTaskCompletedCallback(); debugLog("scheduler-debug", `Queue state after task ${task.id} failed: ${JSON.stringify(this.getQueueStatus())}`); }); } /** * Sort queue by priority (highest first) and then by creation time (earliest first) */ sortQueue() { this.queue.sort((a, b) => { if (a.priority !== b.priority) return b.priority - a.priority; return a.createdAt - b.createdAt; }); } /** * Remove all tasks for a specific thunk */ removeTasks(thunkId) { debugLog("scheduler", `Removing all tasks for thunk ${thunkId}`); this.queue = this.queue.filter((task) => task.thunkId !== thunkId); const runningTaskIds = []; for (const [taskId, task] of this.runningTasks.entries()) if (task.thunkId === thunkId) runningTaskIds.push(taskId); debugLog("scheduler", `Removed ${runningTaskIds.length} running tasks for thunk ${thunkId}`); } }; //#endregion //#region src/thunk/Thunk.ts let ThunkState = /* @__PURE__ */ function(ThunkState$1) { ThunkState$1["PENDING"] = "pending"; ThunkState$1["EXECUTING"] = "executing"; ThunkState$1["COMPLETED"] = "completed"; ThunkState$1["FAILED"] = "failed"; return ThunkState$1; }({}); /** * Base Thunk class that works in both main and renderer processes */ var Thunk = class { /** Unique identifier for this thunk */ id; /** ID of the window that dispatched this thunk */ _sourceWindowId; /** Parent thunk ID if this is a nested thunk */ parentId; /** Thunk source - the process that dispatched this thunk */ source; /** Current state of the thunk */ _state; /** Time when the thunk was created */ startTime; /** Set of child thunk IDs */ children; /** Keys this thunk will affect (for key-based locking) */ keys; /** Flag for immediate execution, bypassing all queues */ immediate; /** Flag for access control bypass */ bypassAccessControl; /** ID of the linked execution context */ _contextId; constructor(options) { this.id = options.id || randomUUID(); this._sourceWindowId = options.sourceWindowId; this.parentId = options.parentId; this.source = options.source; this._state = ThunkState.PENDING; this.startTime = Date.now(); this.children = /* @__PURE__ */ new Set(); this.keys = options.keys; this.immediate = options.immediate; this.bypassAccessControl = options.bypassAccessControl; this._contextId = options.contextId; debugLog("thunk", `Created thunk ${this.id} (type: ${this.source}, immediate: ${this.immediate})`); } /** * Get the source window ID */ get sourceWindowId() { return this._sourceWindowId; } /** * Set the source window ID */ set sourceWindowId(windowId) { this._sourceWindowId = windowId; } /** * Get the current state of the thunk */ get state() { return this._state; } /** * Get the execution context ID */ get contextId() { return this._contextId; } /** * Set the execution context ID */ set contextId(contextId) { this._contextId = contextId; } /** * Mark the thunk as active (processing) */ activate() { this._state = ThunkState.EXECUTING; } /** * Mark the thunk as completed */ complete() { this._state = ThunkState.COMPLETED; } /** * Mark the thunk as failed */ fail() { this._state = ThunkState.FAILED; } /** * Add a child thunk to this thunk */ addChild(childId) { this.children.add(childId); } /** * Get all child thunk IDs */ getChildren() { return Array.from(this.children); } /** * Check if the thunk is in a terminal state (completed or failed) */ isComplete() { return this.state === ThunkState.COMPLETED || this.state === ThunkState.FAILED; } }; //#endregion //#region src/thunk/lifecycle/ThunkLifecycleManager.ts /** * Events emitted by ThunkLifecycleManager */ let ThunkManagerEvent = /* @__PURE__ */ function(ThunkManagerEvent$1) { ThunkManagerEvent$1["THUNK_REGISTERED"] = "thunk:registered"; ThunkManagerEvent$1["THUNK_STARTED"] = "thunk:started"; ThunkManagerEvent$1["THUNK_COMPLETED"] = "thunk:completed"; ThunkManagerEvent$1["THUNK_FAILED"] = "thunk:failed"; ThunkManagerEvent$1["ROOT_THUNK_CHANGED"] = "thunk:root:changed"; ThunkManagerEvent$1["ROOT_THUNK_COMPLETED"] = "thunk:root:completed"; return ThunkManagerEvent$1; }({}); /** * Manages thunk lifecycle: registration, execution, and completion */ var ThunkLifecycleManager = class extends EventEmitter { /** * Map of registered thunks by ID */ thunks = /* @__PURE__ */ new Map(); /** * Current active root thunk ID */ rootThunkId; /** * Tracked tasks for each thunk */ thunkTasks = /* @__PURE__ */ new Map(); /** * Thunk results by ID */ thunkResults = /* @__PURE__ */ new Map(); /** * Thunk errors by ID */ thunkErrors = /* @__PURE__ */ new Map(); constructor(scheduler$1, actionProcessor, stateUpdateTracker) { super(); this.scheduler = scheduler$1; this.actionProcessor = actionProcessor; this.stateUpdateTracker = stateUpdateTracker; this.actionProcessor.on("actionComplete", (actionId) => { const completedThunkIds = this.actionProcessor.handleActionComplete(actionId, this.thunks); for (const thunkId of completedThunkIds) { const thunk = this.thunks.get(thunkId); if (thunk && thunk.source === "main") this.completeThunk(thunkId); } }); } /** * Register a thunk for future execution */ registerThunk(thunkAction, task, _priority = ThunkPriority.NORMAL) { const thunk = new Thunk({ id: thunkAction.__id, sourceWindowId: 0, source: "main", parentId: thunkAction.parentId }); this.thunks.set(thunk.id, thunk); this.emit(ThunkManagerEvent.THUNK_REGISTERED, thunk); if (task) this.thunkTasks.set(thunk.id, { canRunConcurrently: task.canRunConcurrently ?? false }); return { id: thunk.id }; } /** * Mark a thunk as executing */ executeThunk(thunkId) { const thunk = this.thunks.get(thunkId); if (!thunk) { debugLog("thunk", `Thunk ${thunkId} not found`); return; } if (!this.rootThunkId) { this.rootThunkId = thunkId; this.emit(ThunkManagerEvent.ROOT_THUNK_CHANGED, thunkId); debugLog("thunk", `Root thunk changed to: ${thunkId}`); } thunk.activate(); this.emit(ThunkManagerEvent.THUNK_STARTED, thunk); debugLog("thunk", `Thunk ${thunkId} started executing`); } /** * Directly complete a thunk when all actions are finished */ completeThunk(thunkId, result) { const thunk = this.thunks.get(thunkId); if (!thunk) { debugLog("thunk", `Thunk ${thunkId} not found for completion`); return; } if (thunk.state === ThunkState.COMPLETED) { debugLog("thunk", `Thunk ${thunkId} is already completed`); return; } if (result !== void 0) this.thunkResults.set(thunkId, result); const pendingActions = this.actionProcessor.getPendingActions(thunkId); if (!pendingActions || pendingActions.size === 0) { debugLog("thunk", `Thunk ${thunkId} has no pending actions, completing immediately`); this.finalizeThunkCompletion(thunkId); } else { debugLog("thunk", `Thunk ${thunkId} still has ${pendingActions.size} pending actions`); this.thunkResults.set(`${thunkId}:pendingCompletion`, true); } } /** * Actually complete the thunk after all its actions are done */ finalizeThunkCompletion(thunkId) { debugLog("thunk", `Finalizing completion for thunk ${thunkId}`); const thunk = this.thunks.get(thunkId); if (!thunk) { debugLog("thunk", `Thunk ${thunkId} not found during finalization`); return; } const pendingActions = this.actionProcessor.getPendingActions(thunkId); if (pendingActions && pendingActions.size > 0) { debugLog("thunk", `Thunk ${thunkId} still has ${pendingActions.size} pending actions, deferring completion`); return; } thunk.complete(); this.emit(ThunkManagerEvent.THUNK_COMPLETED, thunk); this.thunkResults.delete(`${thunkId}:pendingCompletion`); this.actionProcessor.cleanupThunkActions(thunkId); if (this.rootThunkId === thunkId) { debugLog("thunk", `Root thunk ${thunkId} completed`); this.rootThunkId = void 0; this.emit(ThunkManagerEvent.ROOT_THUNK_COMPLETED, thunk); this.emit(ThunkManagerEvent.ROOT_THUNK_CHANGED, void 0); } this.scheduler.processQueue(); this.tryFinalCleanup(thunkId); } /** * Try to do final cleanup of a completed thunk if it has no pending state updates */ tryFinalCleanup(thunkId) { const thunk = this.thunks.get(thunkId); if (!thunk || thunk.state !== ThunkState.COMPLETED) return; if (!this.stateUpdateTracker.hasPendingStateUpdates(thunkId)) setImmediate(() => { if (!this.stateUpdateTracker.hasPendingStateUpdates(thunkId)) { debugLog("thunk", `Final cleanup for thunk ${thunkId} - no pending state updates after defer`); this.cleanupThunk(thunkId); } else debugLog("thunk", `Thunk ${thunkId} now has pending state updates, cleanup canceled`); }); else debugLog("thunk", `Thunk ${thunkId} still has pending state updates, deferring cleanup`); } /** * Mark a thunk as failed */ failThunk(thunkId, error) { debugLog("thunk", `Failing thunk ${thunkId}: ${error?.message || "Unknown error"}`); const thunk = this.thunks.get(thunkId); if (!thunk) { debugLog("thunk", `Thunk ${thunkId} not found`); return; } thunk.fail(); if (error) { this.thunkErrors.set(thunkId, error); this.emit(ThunkManagerEvent.THUNK_FAILED, thunk, error); } else this.emit(ThunkManagerEvent.THUNK_FAILED, thunk); this.actionProcessor.cleanupThunkActions(thunkId); if (this.rootThunkId === thunkId) { debugLog("thunk", `Root thunk ${thunkId} failed`); this.emit(ThunkManagerEvent.ROOT_THUNK_COMPLETED, thunk); this.rootThunkId = void 0; this.emit(ThunkManagerEvent.ROOT_THUNK_CHANGED, void 0); } } /** * Get all active thunks for broadcasting */ getActiveThunksSummary() { return Array.from(this.thunks.values()).filter((thunk) => thunk.state === ThunkState.EXECUTING).map((thunk) => ({ id: thunk.id, state: thunk.state, task: void 0 })); } /** * Check if we can process an action immediately or need to queue it */ canProcessActionImmediately(action) { if (action.__immediate) return true; const isIdle = this.scheduler.getQueueStatus().isIdle; debugLog("thunk", `Checking if action can be processed immediately. Scheduler idle: ${isIdle}`); return isIdle; } /** * Get the current root thunk ID */ getCurrentRootThunkId() { return this.rootThunkId; } /** * Check if a thunk exists with the given ID */ hasThunk(thunkId) { return this.thunks.has(thunkId); } /** * Check if a thunk is currently active */ isThunkActive(thunkId) { const thunk = this.thunks.get(thunkId); return thunk ? thunk.state === ThunkState.EXECUTING : false; } /** * Check if a thunk is fully complete (including all state updates acknowledged) */ isThunkFullyComplete(thunkId) { const thunk = this.thunks.get(thunkId); if (!thunk) { debugLog("thunk", `Thunk ${thunkId} not found`); return false; } if (thunk.state !== ThunkState.COMPLETED) return false; if (this.stateUpdateTracker.hasPendingStateUpdates(thunkId)) { debugLog("thunk", `Thunk ${thunkId} has pending state updates, not fully complete`); return false; } return true; } /** * Get thunk by ID */ getThunk(thunkId) { return this.thunks.get(thunkId); } /** * Get thunk result */ getThunkResult(thunkId) { return this.thunkResults.get(thunkId); } /** * Get thunk error */ getThunkError(thunkId) { return this.thunkErrors.get(thunkId); } /** * Clean up a specific thunk */ cleanupThunk(thunkId) { debugLog("thunk", `Cleaning up thunk ${thunkId}`); this.thunks.delete(thunkId); this.thunkTasks.delete(thunkId); this.thunkResults.delete(thunkId); this.thunkErrors.delete(thunkId); this.actionProcessor.cleanupThunkActions(thunkId); } /** * Force cleanup of completed thunks for memory management * This is a safety method to prevent unbounded growth */ forceCleanupCompletedThunks() { debugLog("thunk", "Forcing cleanup of all completed thunks"); const completedThunkIds = []; for (const [thunkId, thunk] of this.thunks) if (thunk.state === ThunkState.COMPLETED || thunk.state === ThunkState.FAILED) completedThunkIds.push(thunkId); for (const thunkId of completedThunkIds) this.cleanupThunk(thunkId); debugLog("thunk", `Cleaned up ${completedThunkIds.length} completed thunks`); } /** * Clear all thunks and state */ clear() { this.thunks.clear(); this.thunkTasks.clear(); this.thunkResults.clear(); this.thunkErrors.clear(); this.rootThunkId = void 0; this.actionProcessor.clear(); } }; //#endregion //#region src/thunk/processing/ActionProcessor.ts /** * Processes thunk actions and manages action lifecycle */ var ActionProcessor = class extends EventEmitter { /** * Tracked actions for each thunk */ thunkActions = /* @__PURE__ */ new Map(); /** * Currently processing thunk action ID (for state update tracking) */ currentThunkActionId; stateManager; constructor(scheduler$1) { super(); this.scheduler = scheduler$1; } /** * Set the state manager for processing actions */ setStateManager(stateManager) { this.stateManager = stateManager || void 0; } /** * Process a thunk action */ async processAction(thunkId, action, thunk, onActionComplete) { if (!thunkId) throw new Error("thunkId is required for processAction"); debugLog("thunk", `Processing action ${action.type} for thunk ${thunkId}`); if (!action.__id) action.__id = `${thunkId}_action_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const thunk_ = thunk; if (!thunk_) { debugLog("thunk", `Thunk ${thunkId} not found, cannot process action`); return; } if (thunk_.state === ThunkState.COMPLETED || thunk_.state === ThunkState.FAILED) { debugLog("thunk", `Thunk ${thunkId} is already completed/failed, ignoring action`); return; } if (!this.stateManager) throw new Error("State manager not set"); let thunkActionSet = this.thunkActions.get(thunkId); if (!thunkActionSet) { thunkActionSet = /* @__PURE__ */ new Set(); this.thunkActions.set(thunkId, thunkActionSet); } this.currentThunkActionId = action.__id; try { const result = this.stateManager.processAction(action); if (result && typeof result === "object") { if ("completion" in result) { const completableResult = result; const completion = completableResult.completion; if (completableResult.completion && typeof completion.then === "function") await completion; } else if (typeof result.then === "function") await result; } if (action.__id) { thunkActionSet.add(action.__id); onActionComplete(action.__id); } } catch (error) { debugLog("thunk", `Error processing action for thunk ${thunkId}:`, error); if (action.__id) { thunkActionSet.add(action.__id); onActionComplete(action.__id); } throw error; } finally { this.currentThunkActionId = void 0; } } /** * Handle a completed action * This helps track when all actions for a thunk have completed */ handleActionComplete(actionId, thunks) { const completedThunkIds = []; for (const [thunkId, actions] of this.thunkActions.entries()) if (actions.has(actionId)) { debugLog("thunk", `Action ${actionId} completed for thunk ${thunkId}`); actions.delete(actionId); if (actions.size === 0) { const thunk = thunks.get(thunkId); if (thunk && thunk.state === ThunkState.EXECUTING) { debugLog("thunk", `All actions completed for thunk ${thunkId}, marking as completable`); completedThunkIds.push(thunkId); } } break; } return completedThunkIds; } /** * Check if an action requires queue or can run immediately */ requiresQueue(action) { return !action.__immediate; } /** * Get the currently processing thunk action ID */ getCurrentThunkActionId() { return this.currentThunkActionId; } /** * Set the currently processing thunk action ID */ setCurrentThunkActionId(actionId) { this.currentThunkActionId = actionId; } /** * Clean up actions for a thunk */ cleanupThunkActions(thunkId) { this.thunkActions.delete(thunkId); } /** * Get pending actions for a thunk */ getPendingActions(thunkId) { return this.thunkActions.get(thunkId); } /** * Check if a thunk has pending actions */ hasPendingActions(thunkId) { const actions = this.thunkActions.get(thunkId); return actions ? actions.size > 0 : false; } /** * Clear all action tracking data */ clear() { this.thunkActions.clear(); this.currentThunkActionId = void 0; } /** * Get the scheduler instance */ getScheduler() { return this.scheduler; } }; //#endregion //#region src/thunk/tracking/StateUpdateTracker.ts /** * Manages state update tracking and acknowledgments from renderers */ var StateUpdateTracker = class { /** * Tracks state updates pending acknowledgment from renderers */ pendingStateUpdates = /* @__PURE__ */ new Map(); /** * Maps thunk ID to set of pending update IDs for that thunk */ thunkPendingUpdates = /* @__PURE__ */ new Map(); /** * Track a state update for thunk completion * The thunk will not be considered complete until all renderers acknowledge this update */ trackStateUpdateForThunk(thunkId, updateId, subscribedRendererIds) { debugLog("thunk", `Tracking state update ${updateId} for thunk ${thunkId}`); this.pendingStateUpdates.set(updateId, { updateId, thunkId, subscribedRenderers: new Set(subscribedRendererIds), acknowledgedBy: /* @__PURE__ */ new Set(), timestamp: Date.now() }); if (!this.thunkPendingUpdates.has(thunkId)) this.thunkPendingUpdates.set(thunkId, /* @__PURE__ */ new Set()); const thunkUpdates = this.thunkPendingUpdates.get(thunkId); if (thunkUpdates) thunkUpdates.add(updateId); } /** * Mark a renderer as having acknowledged a state update * Returns true if all renderers have now acknowledged this update */ acknowledgeStateUpdate(updateId, rendererId) { const update = this.pendingStateUpdates.get(updateId); if (!update) { debugLog("thunk", `No pending state update found for ID: ${updateId}`); return true; } if (!update.subscribedRenderers.has(rendererId)) { debugLog("thunk", `Renderer ${rendererId} not subscribed to state update ${updateId}, ignoring ACK`); return false; } update.acknowledgedBy.add(rendererId); if (update.acknowledgedBy.size >= update.subscribedRenderers.size) { debugLog("thunk", `All renderers acknowledged state update ${updateId}`); this.pendingStateUpdates.delete(updateId); const thunkUpdates = this.thunkPendingUpdates.get(update.thunkId); if (thunkUpdates) { thunkUpdates.delete(updateId); if (thunkUpdates.size === 0) this.thunkPendingUpdates.delete(update.thunkId); } return true; } return false; } /** * Remove a dead renderer from all pending state updates * This should be called when a window is destroyed to prevent hanging acknowledgments */ cleanupDeadRenderer(rendererId) { debugLog("thunk", `Cleaning up dead renderer ${rendererId} from pending state updates`); const updatesToCheck = Array.from(this.pendingStateUpdates.values()); for (const update of updatesToCheck) if (update.subscribedRenderers.has(rendererId)) { update.subscribedRenderers.delete(rendererId); if (update.acknowledgedBy.size >= update.subscribedRenderers.size) { debugLog("thunk", `State update ${update.updateId} now fully acknowledged after cleaning up dead renderer`); this.pendingStateUpdates.delete(update.updateId); const thunkUpdates = this.thunkPendingUpdates.get(update.thunkId); if (thunkUpdates) { thunkUpdates.delete(update.updateId); if (thunkUpdates.size === 0) this.thunkPendingUpdates.delete(update.thunkId); } } } } /** * Check if a thunk has pending state updates */ hasPendingStateUpdates(thunkId) { const pendingUpdates = this.thunkPendingUpdates.get(thunkId); return pendingUpdates ? pendingUpdates.size > 0 : false; } /** * Clean up expired pending state updates (prevent memory leaks) */ cleanupExpiredUpdates(maxAgeMs = 3e4) { const now = Date.now(); const expiredUpdates = []; for (const [updateId, update] of this.pendingStateUpdates) if (now - update.timestamp > maxAgeMs) expiredUpdates.push(updateId); for (const updateId of expiredUpdates) { const update = this.pendingStateUpdates.get(updateId); if (update) { debugLog("thunk", `Cleaning up expired state update ${updateId} for thunk ${update.thunkId}`); this.pendingStateUpdates.delete(updateId); const thunkUpdates = this.thunkPendingUpdates.get(update.thunkId); if (thunkUpdates) { thunkUpdates.delete(updateId); if (thunkUpdates.size === 0) this.thunkPendingUpdates.delete(update.thunkId); } } } } /** * Clear all tracking data */ clear() { this.pendingStateUpdates.clear(); this.thunkPendingUpdates.clear(); } /** * Get the count of pending upd