@zubridge/electron
Version:
A streamlined state management library for Electron applications using Zustand.
1,504 lines (1,494 loc) • 87 kB
JavaScript
// 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