@zubridge/electron
Version:
A streamlined state management library for Electron applications using Zustand.
1,477 lines (1,459 loc) • 70.1 kB
JavaScript
let electron = require("electron");
let zod = require("zod");
//#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/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/batching/ActionBatcher.ts
const uuidv4$1 = () => {
return self.crypto.randomUUID();
};
var ActionBatcher = class {
/**
* Hard limit for queue size to prevent DoS attacks.
* Set to 4x maxBatchSize to allow some buffering while preventing unbounded growth.
* If queue exceeds this limit, new actions are rejected immediately.
*/
HARD_QUEUE_LIMIT;
queue = [];
activeBatch = [];
flushTimeoutId = null;
isFlushing = false;
flushingPromise = null;
flushResultWaiters = /* @__PURE__ */ new Set();
isDestroyed = false;
pendingForceFlush = false;
stats = {
totalBatches: 0,
totalActions: 0,
rejectedActions: 0
};
constructor(config, sendBatch) {
this.config = config;
this.sendBatch = sendBatch;
this.HARD_QUEUE_LIMIT = Math.max(this.config.maxBatchSize * 4, 100);
debugLog("batching", `ActionBatcher initialized with hard queue limit: ${this.HARD_QUEUE_LIMIT}`);
}
enqueue(action, resolve, reject, priority, parentId) {
if (this.isDestroyed) {
reject(/* @__PURE__ */ new Error("ActionBatcher is destroyed"));
return "";
}
const id = action.__id || uuidv4$1();
if (this.queue.length >= this.HARD_QUEUE_LIMIT) {
const error = /* @__PURE__ */ new Error(`ActionBatcher queue exceeded hard limit (${this.HARD_QUEUE_LIMIT}). This may indicate a memory leak or DoS attack. Current queue size: ${this.queue.length}`);
this.stats.rejectedActions++;
debugLog("batching:error", `Action ${action.type} rejected: queue at hard limit (${this.queue.length}/${this.HARD_QUEUE_LIMIT})`);
reject(error);
return id;
}
if (this.shouldFlushNow(priority)) {
debugLog("batching", `Immediate flush triggered for high-priority action ${action.type}`);
this.queue.unshift({
action,
resolve,
reject,
priority,
id,
parentId
});
if (this.isFlushing) this.pendingForceFlush = true;
else this.flush(true);
return id;
}
if (this.queue.length >= this.config.maxBatchSize) {
debugLog("batching", `Queue full, flushing before adding action ${action.type}`);
if (this.isFlushing) this.pendingForceFlush = true;
else this.flush(true);
}
this.addToQueue(action, resolve, reject, priority, id, parentId);
if (!this.flushTimeoutId && !this.isFlushing) this.scheduleFlush();
return id;
}
shouldFlushNow(priority) {
return priority >= this.config.priorityFlushThreshold;
}
scheduleFlush() {
if (this.flushTimeoutId) return;
this.flushTimeoutId = setTimeout(() => {
this.flushTimeoutId = null;
if (this.queue.length > 0) this.flush();
}, this.config.windowMs);
}
async flush(force = false) {
if (this.isFlushing) {
debugLog("batching", "Flush already in progress, deferring");
if (force) this.pendingForceFlush = true;
return;
}
if (this.queue.length === 0) {
debugLog("batching", "Queue empty, nothing to flush");
return;
}
if (this.flushTimeoutId) {
clearTimeout(this.flushTimeoutId);
this.flushTimeoutId = null;
}
this.isFlushing = true;
const doFlush = async () => {
let batchId = "";
try {
this.activeBatch = this.prepareBatch();
batchId = uuidv4$1();
const actionIds = this.activeBatch.map((item) => item.id);
debugLog("batching", `Flushing batch ${batchId} with ${this.activeBatch.length} actions`);
const payload = {
batchId,
actions: this.activeBatch.map((item) => ({
action: item.action,
id: item.id,
parentId: item.parentId
}))
};
this.stats.totalBatches++;
this.stats.totalActions += this.activeBatch.length;
const ackPayload = await this.sendBatch(payload);
if (!this.isDestroyed) {
const resultMap = /* @__PURE__ */ new Map();
if (ackPayload?.results) for (const result$1 of ackPayload.results) resultMap.set(result$1.actionId, result$1);
for (const item of this.activeBatch) {
const result$1 = resultMap.get(item.id);
if (!result$1) item.reject(/* @__PURE__ */ new Error(`No result received for action ${item.id}`));
else if (!result$1.success) item.reject(new Error(result$1.error || `Action ${item.id} failed`));
else item.resolve(item.action);
}
}
const result = {
batchId,
actionsSent: actionIds.length,
actionIds
};
for (const resolve of this.flushResultWaiters) resolve(result);
this.flushResultWaiters.clear();
} catch (error) {
debugLog("batching:error", `Batch ${batchId || "<unknown>"} failed:`, error);
if (!this.isDestroyed) this.activeBatch.forEach((item) => {
item.reject(error);
});
const errorResult = {
batchId,
actionsSent: 0,
actionIds: []
};
for (const resolve of this.flushResultWaiters) resolve(errorResult);
this.flushResultWaiters.clear();
} finally {
this.activeBatch = [];
this.isFlushing = false;
this.flushingPromise = null;
if (this.flushResultWaiters.size > 0) {
const fallback = {
batchId: "",
actionsSent: 0,
actionIds: []
};
for (const resolve of this.flushResultWaiters) resolve(fallback);
this.flushResultWaiters.clear();
}
if (!this.isDestroyed) {
if (this.pendingForceFlush) {
this.pendingForceFlush = false;
if (this.queue.length > 0) this.flush(true);
} else if (this.queue.length > 0) this.scheduleFlush();
}
}
};
this.flushingPromise = doFlush();
await this.flushingPromise;
}
/**
* Flush pending actions and return result with batch stats.
* This is used for manual flush from thunks.
*
* Results are delivered exclusively via flushResultWaiters — doFlush resolves
* all registered waiters, so there is no stale result window between
* scheduled flushes.
*/
async flushWithResult(force = false) {
const emptyResult = {
batchId: "",
actionsSent: 0,
actionIds: []
};
if (this.isDestroyed) return emptyResult;
if (this.isFlushing) return new Promise((resolve) => {
this.flushResultWaiters.add(resolve);
});
if (this.queue.length === 0) return emptyResult;
const resultPromise = new Promise((resolve) => {
this.flushResultWaiters.add(resolve);
});
await this.flush(force);
return resultPromise;
}
prepareBatch() {
return this.queue.splice(0, this.config.maxBatchSize);
}
addToQueue(action, resolve, reject, priority, id, parentId) {
this.queue.push({
action,
resolve,
reject,
priority,
id,
parentId
});
debugLog("batching", `Added action ${action.type} to queue (size: ${this.queue.length})`);
}
removeAction(actionId) {
const index = this.queue.findIndex((item) => item.id === actionId);
if (index !== -1) {
this.queue.splice(index, 1)[0].reject(/* @__PURE__ */ new Error(`Action ${actionId} was cancelled`));
debugLog("batching", `Removed action ${actionId} from queue`);
return true;
}
return false;
}
getStats() {
return {
totalBatches: this.stats.totalBatches,
totalActions: this.stats.totalActions,
averageBatchSize: this.stats.totalBatches > 0 ? this.stats.totalActions / this.stats.totalBatches : 0,
currentQueueSize: this.queue.length,
isFlushing: this.isFlushing,
rejectedActions: this.stats.rejectedActions,
queueLimit: this.HARD_QUEUE_LIMIT
};
}
destroy() {
this.isDestroyed = true;
if (this.flushTimeoutId) {
clearTimeout(this.flushTimeoutId);
this.flushTimeoutId = null;
}
this.pendingForceFlush = false;
this.queue.forEach((item) => {
item.reject(/* @__PURE__ */ new Error("ActionBatcher destroyed"));
});
this.queue = [];
this.activeBatch.forEach((item) => {
item.reject(/* @__PURE__ */ new Error("ActionBatcher destroyed"));
});
this.activeBatch = [];
const emptyResult = {
batchId: "",
actionsSent: 0,
actionIds: []
};
for (const resolve of this.flushResultWaiters) resolve(emptyResult);
this.flushResultWaiters.clear();
debugLog("batching", "ActionBatcher destroyed");
}
};
/**
* Calculate the priority for an action based on its flags.
* Uses centralized PRIORITY_LEVELS constants for consistency across the system.
*
* Priority rules:
* - Actions with __immediate get IMMEDIATE priority (100)
* - Actions with __thunkParentId get ROOT_THUNK_ACTION priority (70)
* - All other actions get NORMAL_THUNK_ACTION priority (50)
*
* Note: This function is used in the renderer process (ActionBatcher).
* The renderer does not have access to the active root thunk context;
* without __thunkParentId on the action, all renderer thunk actions
* default to NORMAL_THUNK_ACTION priority (50).
* The main process (ActionScheduler) distinguishes between root and
* nested thunks, assigning NORMAL_THUNK_ACTION (50) to non-root thunks.
*/
function calculatePriority(action) {
if (action.__immediate) return PRIORITY_LEVELS.IMMEDIATE;
if (action.__thunkParentId) return PRIORITY_LEVELS.ROOT_THUNK_ACTION;
return PRIORITY_LEVELS.NORMAL_THUNK_ACTION;
}
//#endregion
//#region src/bridge/ipc/validation.ts
/**
* Validation schemas for IPC payloads using Zod.
*
* These schemas provide:
* - Type-safe validation of all IPC inputs from renderer processes
* - Protection against injection attacks and DoS via length limits
* - Consistent validation across single and batch dispatch
* - Better error messages for debugging
*/
/**
* Maximum lengths for string fields to prevent DoS attacks
*/
const MAX_STRING_LENGTH = {
ACTION_TYPE: 200,
ACTION_ID: 100,
BATCH_ID: 100,
PARENT_ID: 100
};
/**
* Maximum batch size to prevent DoS attacks.
*
* This is an absolute hard limit that cannot be overridden by configuration.
*
* Rationale for 200:
* - ActionBatcher sends ~50 actions/batch in normal operation (BATCHING_DEFAULTS.maxBatchSize)
* - ActionBatcher queue limit is maxBatchSize * 4 = 200 (proven safe in production)
* - This limit provides 4x safety margin while preventing abuse from compromised renderers
* - Normal operation never approaches this limit
* - Consistent with existing architecture (4x multiplier used in ActionBatcher)
*
* Security model:
* - Normal operation: ≤50 actions/batch (ActionBatcher controlled)
* - Hard security cap: ≤200 actions/batch (Zod validation - catches malicious/buggy batches)
*/
const MAX_BATCH_SIZE = 200;
/**
* Schema for validating Action payloads from renderer
* Matches the Action type from @zubridge/types but enforces runtime validation
*/
const ActionPayloadSchema = zod.z.object({
type: zod.z.string().min(1).max(MAX_STRING_LENGTH.ACTION_TYPE),
payload: zod.z.unknown().optional(),
__id: zod.z.string().max(MAX_STRING_LENGTH.ACTION_ID).optional(),
__bypassAccessControl: zod.z.boolean().optional(),
__immediate: zod.z.boolean().optional(),
__startsThunk: zod.z.boolean().optional(),
__sourceWindowId: zod.z.number().optional()
}).strict();
/**
* Known fields in ActionPayloadSchema - these are kept for validation.
* Fields starting with __ that are NOT in this list will be stripped.
*/
const KNOWN_ACTION_FIELDS = new Set([
"type",
"payload",
"__id",
"__bypassAccessControl",
"__immediate",
"__startsThunk",
"__sourceWindowId"
]);
/**
* Strips internal __-prefixed fields from an action object before validation.
* Only strips fields that start with __ but are NOT in the known action fields list.
* Fields in the schema (like __id, __immediate) are kept for validation.
* @param action - The action object to sanitize (must be a plain object)
* @returns Action object with unknown internal fields removed, or original if not an object
*/
function stripInternalFields(action) {
if (!action || typeof action !== "object" || Array.isArray(action)) return action;
const result = {};
for (const [key, value] of Object.entries(action)) if (!key.startsWith("__") || KNOWN_ACTION_FIELDS.has(key)) result[key] = value;
return result;
}
/**
* Schema for single action dispatch payload
* Used by handleDispatch
*/
const SingleDispatchPayloadSchema = zod.z.object({
action: ActionPayloadSchema,
parentId: zod.z.string().max(MAX_STRING_LENGTH.PARENT_ID).optional()
}).strict();
/**
* Schema for individual actions within a batch
*/
const BatchActionItemSchema = zod.z.object({
action: ActionPayloadSchema,
id: zod.z.string().min(1).max(MAX_STRING_LENGTH.ACTION_ID),
parentId: zod.z.string().max(MAX_STRING_LENGTH.PARENT_ID).optional()
}).strict();
/**
* Schema for batch dispatch payload
* Used by handleBatchDispatch
*/
const BatchDispatchPayloadSchema = zod.z.object({
batchId: zod.z.string().min(1).max(MAX_STRING_LENGTH.BATCH_ID),
actions: zod.z.array(BatchActionItemSchema).min(1).max(MAX_BATCH_SIZE)
}).strict();
/**
* Validates a single action dispatch payload
* @param data - Unknown data from IPC channel
* @returns Validation result with typed data or error message
*/
function validateSingleDispatch(data) {
if (data && typeof data === "object" && "action" in data) {
const sanitizedData = {
...data,
action: stripInternalFields(data.action)
};
const result$1 = SingleDispatchPayloadSchema.safeParse(sanitizedData);
if (result$1.success) return {
success: true,
data: result$1.data
};
return {
success: false,
error: formatZodError(result$1.error),
details: result$1.error
};
}
const result = SingleDispatchPayloadSchema.safeParse(data);
if (result.success) return {
success: true,
data: result.data
};
return {
success: false,
error: formatZodError(result.error),
details: result.error
};
}
/**
* Formats Zod validation errors into human-readable messages
* @param error - Zod validation error
* @returns Formatted error message
*/
function formatZodError(error) {
if (error.issues.length === 0) return "Invalid payload structure";
const uniqueIssues = [...new Set(error.issues.map((i) => i.path.length ? `${i.path.join(".")}: ${i.message}` : i.message))];
const summary = uniqueIssues.slice(0, 3).join("; ");
return uniqueIssues.length > 3 ? `${summary} (+${uniqueIssues.length - 3} more)` : summary;
}
/**
* Get the renderer validation level from environment
* Defaults to 'warn' in development, 'off' in production
*/
function getRendererValidationLevel() {
const envLevel = process.env.ZUBRIDGE_RENDERER_VALIDATION;
if (envLevel === "off" || envLevel === "warn" || envLevel === "error") return envLevel;
return process.env.NODE_ENV === "development" ? "warn" : "off";
}
/**
* Validates an action in the renderer process
* Used by preload script to validate actions before sending to main
*
* @param action - The action to validate
* @param parentId - Optional parent thunk ID
* @param level - Validation level (defaults to environment-based)
*/
function validateActionInRenderer(action, parentId, level = getRendererValidationLevel()) {
if (level === "off") return;
const result = validateSingleDispatch({
action,
parentId
});
if (!result.success) {
const message = `[Zubridge] Invalid action dispatch: ${result.error}`;
const details = {
action,
parentId,
error: result.error,
validationDetails: result.details
};
if (level === "error") {
console.error(message, details);
throw new Error(message);
}
console.warn(message, details);
}
}
//#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;
}({});
//#endregion
//#region src/deltas/DeltaMerger.ts
var DeltaMerger = class {
merge(currentState, delta) {
const hasChanges = delta.changed && Object.keys(delta.changed).length > 0;
const hasRemovals = delta.removed && delta.removed.length > 0;
if (delta.type === "full" || !hasChanges && !hasRemovals) {
if (delta.fullState && Object.keys(delta.fullState).length > 0) return structuredClone(delta.fullState);
return { ...currentState };
}
const result = this.cloneWithStructuralSharing(currentState);
if (delta.changed) for (const [keyPath, value] of Object.entries(delta.changed).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) this.setDeepWithStructuralSharing(result, currentState, keyPath, value);
if (delta.removed) for (const keyPath of delta.removed) this.deleteDeep(result, currentState, keyPath);
return result;
}
cloneWithStructuralSharing(obj) {
return { ...obj };
}
setDeepWithStructuralSharing(result, original, path, value) {
const keys = path.split(".");
if (keys.length === 1) {
result[keys[0]] = this.cloneValue(value);
return;
}
const pathToParent = keys.slice(0, -1);
const finalKey = keys[keys.length - 1];
let current = result;
let originalCurrent = original;
for (let i = 0; i < pathToParent.length; i++) {
const key = pathToParent[i];
const existingInResult = current[key];
const originalValue = originalCurrent[key];
if (existingInResult && typeof existingInResult === "object" && existingInResult !== originalValue) {
current = existingInResult;
originalCurrent = originalValue ?? existingInResult;
} else if (originalValue !== null && typeof originalValue === "object") {
const cloned = Array.isArray(originalValue) ? [...originalValue] : { ...originalValue };
current[key] = cloned;
current = cloned;
originalCurrent = originalValue;
} else {
const newObj = {};
current[key] = newObj;
current = newObj;
originalCurrent = newObj;
}
}
current[finalKey] = this.cloneValue(value);
}
cloneValue(value) {
if (value === null || value === void 0 || typeof value !== "object") return value;
return structuredClone(value);
}
deleteDeep(obj, original, path) {
const keys = path.split(".");
if (keys.length === 1) {
delete obj[keys[0]];
return;
}
let current = obj;
let originalCurrent = original;
for (let i = 0; i < keys.length - 1; i++) {
const next = current[keys[i]];
if (next == null || typeof next !== "object") return;
const originalValue = originalCurrent[keys[i]];
if (next !== originalValue) {
current = next;
originalCurrent = originalValue ?? next;
} else {
const cloned = Array.isArray(next) ? [...next] : { ...next };
current[keys[i]] = cloned;
current = cloned;
originalCurrent = originalValue ?? next;
}
}
delete current[keys[keys.length - 1]];
}
};
//#endregion
//#region src/renderer/preloadListeners.ts
var CleanupRegistry = class {
cleanups = [];
add(cleanup) {
this.cleanups.push(cleanup);
}
remove(cleanup) {
const index = this.cleanups.indexOf(cleanup);
if (index !== -1) this.cleanups.splice(index, 1);
}
async cleanupAll() {
(await Promise.allSettled(this.cleanups.map((cleanup) => cleanup()))).forEach((result, index) => {
if (result.status === "rejected") debugLog("cleanup:error", `Cleanup ${index} failed:`, result.reason);
});
this.cleanups.length = 0;
}
};
function createIPCManager({ ipcRenderer: ipcRenderer$1 }) {
const ipcListeners = /* @__PURE__ */ new Map();
const ipcCleanupFunctions = /* @__PURE__ */ new Map();
const cleanupRegistry = {
ipc: new CleanupRegistry(),
dom: new CleanupRegistry(),
async cleanupAll() {
await Promise.all([this.ipc.cleanupAll(), this.dom.cleanupAll()]);
}
};
const registerIpcListener = (channel, listener) => {
try {
const existingListener = ipcListeners.get(channel);
const existingCleanup = ipcCleanupFunctions.get(channel);
if (existingCleanup) {
cleanupRegistry.ipc.remove(existingCleanup);
ipcCleanupFunctions.delete(channel);
}
ipcRenderer$1.on(channel, listener);
if (existingListener) ipcRenderer$1.removeListener(channel, existingListener);
ipcListeners.set(channel, listener);
const cleanupFn = () => {
ipcRenderer$1.removeListener(channel, listener);
ipcListeners.delete(channel);
ipcCleanupFunctions.delete(channel);
};
ipcCleanupFunctions.set(channel, cleanupFn);
cleanupRegistry.ipc.add(cleanupFn);
} catch (error) {
debugLog("ipc:error", `Failed to register IPC listener for channel ${channel}:`, error);
}
};
return {
ipcListeners: {
get: (channel) => ipcListeners.get(channel),
delete: (channel) => {
const listener = ipcListeners.get(channel);
if (listener) ipcRenderer$1.removeListener(channel, listener);
const cleanup = ipcCleanupFunctions.get(channel);
if (cleanup) {
cleanupRegistry.ipc.remove(cleanup);
ipcCleanupFunctions.delete(channel);
}
ipcListeners.delete(channel);
}
},
cleanupRegistry,
registerIpcListener
};
}
//#endregion
//#region ../../node_modules/.pnpm/unenv@2.0.0-rc.24/node_modules/unenv/dist/runtime/node/internal/crypto/web.mjs
const subtle = globalThis.crypto?.subtle;
const randomUUID = () => {
return globalThis.crypto?.randomUUID();
};
//#endregion
//#region src/types/errors.ts
/**
* Error thrown when the action queue exceeds its maximum size
*
* This error indicates that the application is generating actions faster than
* they can be processed, which could lead to memory issues or performance degradation.
*
* @example
* ```typescript
* import { QueueOverflowError } from '@zubridge/electron';
*
* try {
* await dispatch(someAction);
* } catch (error) {
* if (error instanceof QueueOverflowError) {
* debug('queue:error', 'Action queue is full:', error.message);
* // Handle overflow - maybe wait and retry, or warn user
* }
* }
* ```
*/
var QueueOverflowError = class extends Error {
queueSize;
maxSize;
constructor(queueSize, maxSize) {
super(`Action queue overflow: ${queueSize} actions pending, maximum allowed is ${maxSize}`);
this.name = "QueueOverflowError";
this.queueSize = queueSize;
this.maxSize = maxSize;
}
};
//#endregion
//#region src/thunk/shared/BaseThunkProcessor.ts
/**
* Base class for thunk processors that handles common functionality
* like action completion tracking, timeouts, and queue management
*/
var BaseThunkProcessor = class {
actionCompletionTimeoutMs;
maxQueueSize;
actionCompletionCallbacks = /* @__PURE__ */ new Map();
actionTimeouts = /* @__PURE__ */ new Map();
constructor(options, logPrefix) {
this.logPrefix = logPrefix;
this.actionCompletionTimeoutMs = options.actionCompletionTimeoutMs;
this.maxQueueSize = options.maxQueueSize;
debugLog("core", `[${this.logPrefix}] Initialized with timeout: ${this.actionCompletionTimeoutMs}ms, maxQueueSize: ${this.maxQueueSize}`);
}
/**
* Generate a unique action ID if one doesn't exist
*/
ensureActionId(action, payload) {
const actionObj = typeof action === "string" ? {
type: action,
payload,
__id: randomUUID()
} : {
...action,
__id: action.__id || randomUUID()
};
if (!actionObj.__id) actionObj.__id = randomUUID();
return actionObj;
}
/**
* Check if queue has capacity and throw QueueOverflowError if not
*/
checkQueueCapacity(currentSize) {
if (currentSize >= this.maxQueueSize) {
const error = new QueueOverflowError(currentSize, this.maxQueueSize);
debugLog("core:error", `[${this.logPrefix}] Queue overflow: ${error.message}`);
throw error;
}
}
/**
* Set up action completion tracking with timeout
*/
setupActionCompletion(actionId, callback, timeoutCallback) {
this.actionCompletionCallbacks.set(actionId, callback);
debugLog("core", `[${this.logPrefix}] Set completion callback for action ${actionId}`);
const safetyTimeout = setTimeout(() => {
if (this.actionCompletionCallbacks.has(actionId)) {
debugLog("core", `[${this.logPrefix}] Safety timeout triggered for action ${actionId} after ${this.actionCompletionTimeoutMs}ms`);
if (timeoutCallback) timeoutCallback();
else this.completeActionInternal(actionId, { __timeout: true });
}
}, this.actionCompletionTimeoutMs);
this.actionTimeouts.set(actionId, safetyTimeout);
}
/**
* Complete an action and call its callback
*/
completeActionInternal(actionId, result) {
const callback = this.actionCompletionCallbacks.get(actionId);
if (!callback) {
debugLog("core", `[${this.logPrefix}] No completion callback found for action ${actionId}`);
return false;
}
const timeout = this.actionTimeouts.get(actionId);
if (timeout) {
clearTimeout(timeout);
this.actionTimeouts.delete(actionId);
}
try {
debugLog("core", `[${this.logPrefix}] Action ${actionId} result: ${JSON.stringify(result)}`);
} catch {
debugLog("core", `[${this.logPrefix}] Action ${actionId} result: [Non-serializable result]`);
}
try {
callback(result);
debugLog("core", `[${this.logPrefix}] Completion callback executed for action ${actionId}`);
} catch (callbackError) {
debugLog("core:error", `[${this.logPrefix}] Error in completion callback for action ${actionId}: ${callbackError}`);
}
this.actionCompletionCallbacks.delete(actionId);
return true;
}
/**
* Handle action completion with error checking
*/
completeAction(actionId, result) {
debugLog("core", `[${this.logPrefix}] Action completed: ${actionId}`);
const { error: errorString } = result;
if (errorString) debugLog("core:error", `[${this.logPrefix}] Action ${actionId} completed with error: ${errorString}`);
this.completeActionInternal(actionId, result);
}
/**
* Force cleanup of expired actions and timeouts
* This prevents memory leaks from stale actions
*/
forceCleanupExpiredActions() {
debugLog("core", `[${this.logPrefix}] Force cleaning up expired actions and timeouts`);
for (const [actionId, timeout] of this.actionTimeouts) {
debugLog("core", `[${this.logPrefix}] Force clearing timeout for action ${actionId}`);
clearTimeout(timeout);
}
const clearedTimeouts = this.actionTimeouts.size;
const clearedCallbacks = this.actionCompletionCallbacks.size;
this.actionTimeouts.clear();
this.actionCompletionCallbacks.clear();
debugLog("core", `[${this.logPrefix}] Force cleaned up ${clearedTimeouts} timeouts, ${clearedCallbacks} callbacks`);
}
/**
* Test method to check queue capacity (for testing purposes)
*/
testCheckQueueCapacity(currentSize) {
this.checkQueueCapacity(currentSize);
}
/**
* Destroy and cleanup all resources
*/
destroy() {
debugLog("core", `[${this.logPrefix}] Destroying processor instance`);
this.forceCleanupExpiredActions();
debugLog("core", `[${this.logPrefix}] Processor instance destroyed`);
}
};
//#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/utils/configuration.ts
/**
* Base default configuration values for thunk processors
*/
const THUNK_PROCESSOR_DEFAULTS$1 = {
maxQueueSize: 100,
actionCompletionTimeoutMs: process.platform === "linux" ? 6e4 : 3e4
};
/**
* Merge user options with thunk processor defaults
*/
function getThunkProcessorOptions$1(userOptions) {
return {
maxQueueSize: userOptions?.maxQueueSize ?? THUNK_PROCESSOR_DEFAULTS$1.maxQueueSize,
actionCompletionTimeoutMs: userOptions?.actionCompletionTimeoutMs ?? THUNK_PROCESSOR_DEFAULTS$1.actionCompletionTimeoutMs
};
}
//#endregion
//#region src/renderer/rendererThunkProcessor.ts
/**
* Handles thunk execution in the renderer process
*/
var RendererThunkProcessor = class extends BaseThunkProcessor {
currentWindowId;
actionSender;
batchFlusher;
thunkRegistrar;
thunkCompleter;
stateProvider;
pendingDispatches = /* @__PURE__ */ new Set();
constructor(options) {
const config = getThunkProcessorOptions$1(options);
super(config, "RENDERER_THUNK");
}
/**
* Initialize the processor with all required dependencies
*/
initialize(options) {
debugLog("ipc", "[RENDERER_THUNK] Initializing with options:", options);
this.currentWindowId = options.windowId;
this.actionSender = options.actionSender;
this.batchFlusher = options.batchFlusher;
this.thunkRegistrar = options.thunkRegistrar;
this.thunkCompleter = options.thunkCompleter;
if (options.actionCompletionTimeoutMs !== void 0) {
this.actionCompletionTimeoutMs = options.actionCompletionTimeoutMs;
debugLog("ipc", "[RENDERER_THUNK] Updated timeout:", this.actionCompletionTimeoutMs);
}
debugLog("ipc", "[RENDERER_THUNK] Action sender:", this.actionSender);
debugLog("ipc", `[RENDERER_THUNK] Initialized with window ID ${options.windowId}`);
}
/**
* Set a custom state provider function
* This allows explicitly registering a way to get state after initialization
*/
setStateProvider(provider) {
this.stateProvider = provider;
debugLog("ipc", "[RENDERER_THUNK] Custom state provider registered");
}
/**
* Handle action completion notification
* This should be called when an action acknowledgment is received from the main process
*/
completeAction(actionId, result) {
debugLog("ipc", `[RENDERER_THUNK] Action completed: ${actionId}`);
super.completeAction(actionId, result);
this.pendingDispatches.delete(actionId);
debugLog("ipc", `[RENDERER_THUNK] Removed ${actionId} from pending dispatches, remaining: ${this.pendingDispatches.size}`);
if (this.pendingDispatches.size > 0) debugLog("ipc", `[RENDERER_THUNK] Remaining dispatch IDs: ${Array.from(this.pendingDispatches).join(", ")}`);
}
/**
* Execute a thunk function
*/
async executeThunk(thunkFn, options, parentId) {
const thunk = new Thunk({
sourceWindowId: this.currentWindowId ?? 0,
source: "renderer",
parentId,
bypassAccessControl: options?.bypassAccessControl ?? false,
immediate: options?.immediate ?? false
});
debugLog("ipc", `[RENDERER_THUNK] Executing thunk ${thunk.id} (immediate=${thunk.immediate})`);
let isFirstAction = true;
debugLog("ipc", `[RENDERER_THUNK] Thunk ${thunk.id} immediate: ${thunk.immediate}`);
if (this.thunkRegistrar && this.currentWindowId) try {
debugLog("ipc", `[RENDERER_THUNK] Registering thunk ${thunk.id} with main process (immediate=${thunk.immediate})`);
await this.thunkRegistrar(thunk.id, parentId, options?.immediate, options?.bypassAccessControl);
debugLog("ipc", `[RENDERER_THUNK] Thunk ${thunk.id} registered successfully`);
} catch (error) {
debugLog("ipc:error", `[RENDERER_THUNK] Error registering thunk: ${error}`);
}
try {
const getState = async (getStateOptions) => {
debugLog("ipc", `[RENDERER_THUNK] getState called for thunk ${thunk.id}`);
if (this.stateProvider) {
debugLog("ipc", `[RENDERER_THUNK] Using registered state provider for thunk ${thunk.id}`);
const bypassFlag = getStateOptions?.bypassAccessControl ?? thunk.bypassAccessControl;
return await this.stateProvider({ bypassAccessControl: bypassFlag });
}
throw new Error("No state provider available");
};
const baseDispatch = async (action, payloadOrOptions, optionsIfString) => {
let payload;
let dispatchOptions;
if (typeof action === "string") {
payload = payloadOrOptions;
dispatchOptions = optionsIfString;
} else dispatchOptions = payloadOrOptions;
const isBatched = dispatchOptions?.batch === true;
debugLog("ipc", `[RENDERER_THUNK] [${thunk.id}] Dispatch called (immediate=${thunk.immediate}, batch=${isBatched})`, action);
if (typeof action === "function") {
debugLog("ipc", `[RENDERER_THUNK] Handling nested thunk in ${thunk.id}`);
return this.executeThunk(action, dispatchOptions, thunk.id);
}
const actionObj = this.ensureActionId(action, payload);
if (dispatchOptions?.immediate ?? thunk.immediate) actionObj.__immediate = true;
if (dispatchOptions?.bypassAccessControl ?? thunk.bypassAccessControl) actionObj.__bypassAccessControl = true;
const actionId = actionObj.__id;
debugLog("ipc", `[RENDERER_THUNK] Thunk ${thunk.id} dispatching action ${actionObj.type} (${actionId})`);
if (isFirstAction) {
debugLog("ipc", `[RENDERER_THUNK] Marking action ${actionId} as starting thunk ${thunk.id}`);
actionObj.__startsThunk = true;
isFirstAction = false;
}
this.checkQueueCapacity(this.pendingDispatches.size);
this.pendingDispatches.add(actionId);
debugLog("ipc", `[RENDERER_THUNK] Added ${actionId} to pending dispatches, now pending: ${this.pendingDispatches.size}/${this.maxQueueSize}`);
const actionPromise = new Promise((resolve, reject) => {
this.setupActionCompletion(actionId, (result) => {
const { error: errorString } = result;
debugLog("ipc", `[RENDERER_THUNK] Action ${actionId} completion callback called with result`, result);
if (errorString) {
debugLog("ipc:error", `[RENDERER_THUNK] Rejecting promise for action ${actionId} with error: ${errorString}`);
reject(new Error(errorString));
} else resolve(result || actionObj);
}, () => {
debugLog("ipc", `[RENDERER_THUNK] Safety timeout triggered for action ${actionId}`);
this.completeAction(actionId, actionObj);
});
});
if (this.actionSender) try {
debugLog("ipc", `[RENDERER_THUNK] Sending action ${actionId} to main process`);
await this.actionSender(actionObj, thunk.id, { batch: isBatched });
debugLog("ipc", `[RENDERER_THUNK] Action ${actionId} sent to main process`);
} catch (error) {
this.completeAction(actionId, { error: String(error) });
this.pendingDispatches.delete(actionId);
debugLog("ipc:error", `[RENDERER_THUNK] Error sending action ${actionId}:`, error);
throw error;
}
else {
debugLog("ipc:error", `[RENDERER_THUNK] No action sender configured, cannot send action ${actionId}`);
throw new Error("Action sender not configured for renderer thunk processor");
}
return actionPromise;
};
const dispatch = Object.assign(baseDispatch, {
batch: async (action, payloadOrOpts, optsIfString) => {
if (typeof action === "string") await baseDispatch(action, payloadOrOpts, {
...optsIfString,
batch: true
});
else await baseDispatch(action, {
...payloadOrOpts ?? {},
batch: true
});
},
flush: async () => {
debugLog("ipc", `[RENDERER_THUNK] [${thunk.id}] Flushing pending batched actions`);
if (!this.batchFlusher) {
debugLog("ipc", "[RENDERER_THUNK] No batch flusher available");
return {
batchId: "",
actionsSent: 0,
actionIds: []
};
}
return this.batchFlusher();
}
});
debugLog("ipc", `[RENDERER_THUNK] Executing thunk function for ${thunk.id}`);
try {
const result = await thunkFn(getState, dispatch);
debugLog("ipc", `[RENDERER_THUNK] Thunk ${thunk.id} execution completed, result:`, result);
return result;
} catch (thunkError) {
debugLog("ipc:error", `[RENDERER_THUNK] Thunk ${thunk.id} threw error:`, thunkError);
throw thunkError;
}
} catch (error) {
debugLog("ipc:error", `[RENDERER_THUNK] Error executing thunk ${thunk.id}:`, error);
throw error;
} finally {
if (this.thunkCompleter && this.currentWindowId) try {
debugLog("ipc", `[RENDERER_THUNK] Notifying main process of thunk ${thunk.id} completion`);
await this.thunkCompleter(thunk.id);
debugLog("ipc", `[RENDERER_THUNK] Thunk ${thunk.id} completion notified`);
} catch (e) {
debugLog("ipc:error", `[RENDERER_THUNK] Error notifying thunk completion: ${e}`);
}
}
}
/**
* Dispatch an action to the main process (for non-thunk scenarios)
*/
async dispatchAction(action, payload, parentId) {
debugLog("ipc", "[RENDERER_THUNK] dispatchAction called with:", {
action,
payload,
parentId
});
if (typeof window !== "undefined" && window.zubridge?.dispatch) {
debugLog("ipc", "[RENDERER_THUNK] Using window.zubridge.dispatch for action");
try {
if (typeof action === "string") await window.zubridge.dispatch(action, payload);
else await window.zubridge.dispatch(action);
return;
} catch (error) {
debugLog("ipc:error", "[RENDERER_THUNK] Error dispatching through window.zubridge:", error);
}
}
if (!this.actionSender) {
debugLog("ipc:error", "[RENDERER_THUNK] dispatchAction: No action sender configured, cannot dispatch.");
throw new Error("Action sender not configured for direct dispatch.");
}
const actionObj = this.ensureActionId(action, payload);
const actionId = actionObj.__id;
return new Promise((resolve, reject) => {
try {
this.checkQueueCapacity(this.pendingDispatches.size);
} catch (error) {
reject(error);
return;
}
this.pendingDispatches.add(actionId);
debugLog("ipc", `[RENDERER_THUNK] Added ${actionId} to pending dispatches, now pending: ${this.pendingDispatches.size}/${this.maxQueueSize}`);
this.setupActionCompletion(actionId, (result) => {
const { error: errorString } = result;
debugLog("ipc", `[RENDERER_THUNK] Action ${actionId} completion callback called with result:`, result);
if (errorString) {
debugLog("ipc:error", `[RENDERER_THUNK] Rejecting promise for action ${actionId} with error: ${errorString}`);
reject(new Error(errorString));
} else resolve();
}, () => {
debugLog("ipc", `[RENDERER_THUNK] Safety timeout triggered for action ${actionId}`);
this.completeAction(actionId, actionObj);
});
debugLog("ipc", `[RENDERER_THUNK] dispatchAction: Sending action ${actionObj.type} (${actionObj.__id})`);
this.actionSender?.(actionObj, parentId).then(() => {
debugLog("ipc", `[RENDERER_THUNK] dispatchAction: Action ${actionObj.__id} sent.`);
}).catch((error) => {
this.completeAction(actionId, { error: error.message });
this.pendingDispatches.delete(actionId);
debugLog("ipc:error", `[RENDERER_THUNK] Error sending action ${actionId}:`, error);
reject(error);
});
});
}
/**
* Force cleanup of expired timeouts and callbacks
* This prevents memory leaks from stale actions
*/
forceCleanupExpiredActions() {
debugLog("ipc", "[RENDERER_THUNK] Force cleaning up expired actions and timeouts");
super.forceCleanupExpiredActions();
const clearedDispatches = this.pendingDispatches.size;
this.pendingDispatches.clear();
debugLog("ipc", `[RENDERER_THUNK] Force cleaned up ${clearedDispatches} pending dispatches`);
}
/**
* Destroy the processor and cleanup all resources
*/
destroy() {
debugLog("ipc", "[RENDERER_THUNK] Destroying RendererThunkProcessor instance");
this.forceCleanupExpiredActions();
this.actionSender = void 0;
this.thunkRegistrar = void 0;
this.thunkCompleter = void 0;
this.stateProvider = void 0;
this.currentWindowId = void 0;
super.destroy();
}
};
//#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 configuration and validation
*/
var ConfigurationError = class extends ZubridgeError {
configPath;
expectedType;
actualType;
constructor(message, context) {
super(message, context);
this.configPath = context?.configPath;
this.expectedType = context?.expectedType;
this.actualType = context?.actualType;
}
};
//#endregion
//#region src/utils/errorHandling.ts
/**
* Enhanced error logging that preserves Error instances
*/
function logZubridgeError(error, additionalInfo) {
debugLog(`${error.constructor.name.replace("Error", "").toLowerCase()}:error`, `${error.name}:`, {
...error.getDetails(),
...additionalInfo
});
}
//#endregion
//#region src/utils/globalErrorHandlers.ts
/**
* Sets up error handlers for the renderer process
*/
function setupRendererErrorHandlers() {
if (typeof window !== "undefined") {
window.addEventListener("unhandledrejection", (event) => {
logZubridgeError(new ConfigurationError("Unhandled promise rejection in renderer process", {
reason: event.reason instanceof Error ? event.reason.message : String(event.reason),
originalReason: event.reason
}));
debugLog("renderer:error", "Unhandled promise rejection detected and logged");
event.preventDefault();
});
window.addEventListener("error", (event) => {
logZubridgeError(new ConfigurationError("Uncaught error in renderer process", {
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
originalError: event.error
}));
debugLog("renderer:error", "Uncaught error detected and logged");
});
debugLog("renderer", "Global error handlers setup for renderer process");
}
}
//#endregion
//#region src/utils/thunkProcessor.ts
/**
* Base default configuration values for thunk processors
*/
const THUNK_PROCESSOR_DEFAULTS = {
maxQueueSize: 100,
actionCompletionTimeoutMs: process.platform === "linux" ? 6e4 : 3e4
};
/**
* Merge user options with thunk processor defaults
*/
function getThunkProcessorOptions(userOptions) {
return {
maxQueueSize: userOptions?.maxQueueSize ?? THUNK_PROCESSOR_DEFAULTS.maxQueueSize,
actionCompletionTimeoutMs: userOptions?.actionCompletionTimeoutMs ?? THUNK_PROCESSOR_DEFAULTS.actionCompletionTimeoutMs
};
}
//#endregion
//#region src/utils/preloadOptions.ts
/**
* Get batching configuration with defaults
*/
function getBatchingConfig(userConfig) {
return {
windowMs: userConfig?.windowMs ?? BATCHING_DEFAULTS.windowMs,
maxBatchSize: userConfig?.maxBatchSize ?? BATCHING_DEFAULTS.maxBatchSize,
priorityFlushThreshold: userConfig?.priorityFlushThreshold ?? BATCHING_DEFAULTS.priorityFlushThreshold,
ackTimeoutMs: userCo