@zubridge/electron
Version:
A streamlined state management library for Electron applications using Zustand.
279 lines (278 loc) • 11.9 kB
JavaScript
// Debug logger with timestamp
const debugWindows = (message, ...args) => {
const timestamp = new Date().toISOString();
console.log(`[WINDOWS ${timestamp}] ${message}`, ...args);
};
/**
* Type guard to check if an object is an Electron WebContents
*/
export const isWebContents = (wrapperOrWebContents) => {
const result = wrapperOrWebContents && typeof wrapperOrWebContents === 'object' && 'id' in wrapperOrWebContents;
if (result) {
debugWindows(`isWebContents: TRUE for id ${wrapperOrWebContents.id}`);
}
else {
debugWindows('isWebContents: FALSE', wrapperOrWebContents);
}
return result;
};
/**
* Type guard to check if an object is a WebContentsWrapper
*/
export const isWrapper = (wrapperOrWebContents) => {
const result = wrapperOrWebContents && typeof wrapperOrWebContents === 'object' && 'webContents' in wrapperOrWebContents;
if (result) {
debugWindows(`isWrapper: TRUE for id ${wrapperOrWebContents.webContents?.id}`);
}
else {
debugWindows('isWrapper: FALSE', wrapperOrWebContents);
}
return result;
};
/**
* Get the WebContents object from either a WebContentsWrapper or WebContents
*/
export const getWebContents = (wrapperOrWebContents) => {
// Create a more readable description of the input for logging
let description = 'Invalid input';
if (wrapperOrWebContents && typeof wrapperOrWebContents === 'object') {
if ('id' in wrapperOrWebContents) {
description = `WebContents ID: ${wrapperOrWebContents.id}`;
}
else if ('webContents' in wrapperOrWebContents) {
description = `Wrapper with WebContents ID: ${wrapperOrWebContents.webContents?.id}`;
}
else {
description = 'Unknown object type';
}
}
debugWindows(`getWebContents called with: ${description}`);
if (isWebContents(wrapperOrWebContents)) {
debugWindows(`getWebContents: Returning direct WebContents with ID: ${wrapperOrWebContents.id}`);
return wrapperOrWebContents;
}
if (isWrapper(wrapperOrWebContents)) {
const webContents = wrapperOrWebContents.webContents;
debugWindows(`getWebContents: Extracting from wrapper, ID: ${webContents?.id || 'undefined'}`);
return webContents;
}
debugWindows('getWebContents: Could not extract WebContents, returning undefined');
return undefined;
};
/**
* Check if a WebContents is destroyed
*/
export const isDestroyed = (webContents) => {
try {
if (typeof webContents.isDestroyed === 'function') {
const destroyed = webContents.isDestroyed();
debugWindows(`isDestroyed check for WebContents ID ${webContents.id}: ${destroyed}`);
return destroyed;
}
debugWindows(`isDestroyed: WebContents ID ${webContents?.id} has no isDestroyed function`);
return false;
}
catch (error) {
debugWindows(`isDestroyed: Exception while checking ID ${webContents?.id}`, error);
return true;
}
};
/**
* Safely send a message to a WebContents
*/
export const safelySendToWindow = (webContents, channel, data) => {
try {
debugWindows(`safelySendToWindow: Attempting to send to WebContents ID ${webContents?.id}, channel: ${channel}`);
if (!webContents || isDestroyed(webContents)) {
debugWindows(`safelySendToWindow: WebContents is undefined or destroyed, aborting send`);
return false;
}
// Type check for WebContents API
const hasWebContentsAPI = typeof webContents.send === 'function';
if (!hasWebContentsAPI) {
debugWindows(`safelySendToWindow: WebContents ID ${webContents.id} missing 'send' function`);
return false;
}
// Check if isLoading is a function before calling it
const isLoading = typeof webContents.isLoading === 'function' ? webContents.isLoading() : false;
debugWindows(`safelySendToWindow: WebContents ID ${webContents.id} isLoading: ${isLoading}`);
if (isLoading) {
debugWindows(`safelySendToWindow: WebContents ID ${webContents.id} is loading, queueing message for later`);
webContents.once('did-finish-load', () => {
try {
if (!webContents.isDestroyed()) {
debugWindows(`safelySendToWindow: Now sending delayed message to WebContents ID ${webContents.id}`);
webContents.send(channel, data);
}
else {
debugWindows(`safelySendToWindow: WebContents ID ${webContents.id} was destroyed before load finished`);
}
}
catch (e) {
debugWindows(`safelySendToWindow: Error sending delayed message to WebContents ID ${webContents.id}`, e);
}
});
return true;
}
debugWindows(`safelySendToWindow: Sending message immediately to WebContents ID ${webContents.id}`);
webContents.send(channel, data);
return true;
}
catch (error) {
debugWindows(`safelySendToWindow: Exception while sending to WebContents ID ${webContents?.id}`, error);
return false;
}
};
/**
* Set up cleanup when WebContents is destroyed
*/
export const setupDestroyListener = (webContents, cleanup) => {
try {
debugWindows(`setupDestroyListener: Setting up cleanup for WebContents ID ${webContents?.id}`);
if (typeof webContents.once === 'function') {
webContents.once('destroyed', () => {
debugWindows(`WebContents ID ${webContents.id} destroyed, running cleanup`);
cleanup();
});
}
else {
debugWindows(`setupDestroyListener: WebContents ID ${webContents.id} missing 'once' function`);
}
}
catch (e) {
debugWindows(`setupDestroyListener: Exception for WebContents ID ${webContents?.id}`, e);
}
};
/**
* Creates a WebContents tracker that uses WeakMap for automatic garbage collection
* but maintains a set of active IDs for tracking purposes
*/
export const createWebContentsTracker = () => {
debugWindows('Creating new WebContentsTracker');
// WeakMap for the primary storage - won't prevent garbage collection
const webContentsTracker = new WeakMap();
// Set to track active subscription IDs (not object references)
const activeIds = new Set();
// Strong reference map of WebContents by ID - we need this to retrieve active WebContents
// This will be maintained alongside the WeakMap
const webContentsById = new Map();
const logTrackerState = () => {
debugWindows(`WebContentsTracker state: ${activeIds.size} active IDs, ${webContentsById.size} tracked WebContents`);
debugWindows(`Active IDs: ${[...activeIds].join(', ')}`);
};
return {
track: (webContents) => {
if (!webContents) {
debugWindows('track: Called with undefined WebContents');
return false;
}
if (isDestroyed(webContents)) {
debugWindows(`track: WebContents ID ${webContents.id} is already destroyed`);
return false;
}
const id = webContents.id;
debugWindows(`track: Adding WebContents ID ${id} to tracker`);
webContentsTracker.set(webContents, { id });
activeIds.add(id);
webContentsById.set(id, webContents);
// Set up the destroyed listener for cleanup
setupDestroyListener(webContents, () => {
debugWindows(`track: Cleanup handler for WebContents ID ${id} triggered`);
activeIds.delete(id);
webContentsById.delete(id);
});
logTrackerState();
return true;
},
untrack: (webContents) => {
if (!webContents) {
debugWindows('untrack: Called with undefined WebContents');
return;
}
const id = webContents.id;
debugWindows(`untrack: Removing WebContents ID ${id} from tracker`);
// Explicitly delete from all tracking structures
webContentsTracker.delete(webContents);
activeIds.delete(id);
webContentsById.delete(id);
logTrackerState();
},
untrackById: (id) => {
debugWindows(`untrackById: Removing ID ${id} from tracker`);
activeIds.delete(id);
const webContents = webContentsById.get(id);
if (webContents) {
debugWindows(`untrackById: Found and removing WebContents for ID ${id}`);
webContentsTracker.delete(webContents);
}
webContentsById.delete(id);
logTrackerState();
},
isTracked: (webContents) => {
if (!webContents) {
debugWindows('isTracked: Called with undefined WebContents');
return false;
}
const tracked = webContents && webContentsTracker.has(webContents) && activeIds.has(webContents.id);
debugWindows(`isTracked: WebContents ID ${webContents.id} tracked: ${tracked}`);
return tracked;
},
hasId: (id) => {
const has = activeIds.has(id);
debugWindows(`hasId: ID ${id} in tracker: ${has}`);
return has;
},
getActiveIds: () => {
const ids = [...activeIds];
debugWindows(`getActiveIds: Returning ${ids.length} active IDs: ${ids.join(', ')}`);
return ids;
},
getActiveWebContents: () => {
debugWindows('getActiveWebContents: Collecting active WebContents');
const result = [];
// Filter out any destroyed WebContents that might still be in our map
for (const [id, webContents] of webContentsById.entries()) {
if (!isDestroyed(webContents)) {
debugWindows(`getActiveWebContents: Adding active WebContents ID ${id}`);
result.push(webContents);
}
else {
// Clean up any destroyed WebContents we find
debugWindows(`getActiveWebContents: Found destroyed WebContents ID ${id}, cleaning up`);
activeIds.delete(id);
webContentsById.delete(id);
}
}
debugWindows(`getActiveWebContents: Returning ${result.length} active WebContents`);
return result;
},
cleanup: () => {
debugWindows(`cleanup: Clearing all tracked WebContents (${activeIds.size} IDs)`);
activeIds.clear();
webContentsById.clear();
},
};
};
/**
* Prepare WebContents objects from an array of wrappers or WebContents
*/
export const prepareWebContents = (wrappers) => {
if (!wrappers || !Array.isArray(wrappers)) {
debugWindows('prepareWebContents: No wrappers provided or invalid input, returning empty array');
return [];
}
debugWindows(`prepareWebContents: Processing ${wrappers.length} wrappers/WebContents`);
const result = [];
for (const wrapper of wrappers) {
const webContents = getWebContents(wrapper);
if (webContents && !isDestroyed(webContents)) {
debugWindows(`prepareWebContents: Adding WebContents ID ${webContents.id} to result`);
result.push(webContents);
}
else {
debugWindows('prepareWebContents: Skipping undefined or destroyed WebContents');
}
}
debugWindows(`prepareWebContents: Returning ${result.length} valid WebContents objects`);
return result;
};