tiny-electron-essentials
Version:
A lightweight and modular utility library for Electron apps, offering simplified window management, tray support, IPC channels, and custom frameless window styling.
293 lines (256 loc) • 11.7 kB
JavaScript
;
var electron = require('electron');
var events = require('events');
var Events = require('../global/Events.cjs');
var Utils = require('../global/Utils.cjs');
/**
* @typedef {(...args: any[]) => void} ListenerCallback
* A generic callback function used for event listeners.
*/
/**
* @typedef {Object} NotificationInstance
* @property {() => boolean} isSupported - Indicates whether the feature is supported.
* @property {() => Promise<void>} show - Shows the notification.
* @property {() => Promise<void>} close - Closes the notification.
* @property {(event: string|symbol, callback: ListenerCallback) => void} on - Registers an event listener.
* @property {(event: string|symbol, callback: ListenerCallback) => void} off - Removes an event listener.
* @property {(event: string|symbol, callback: ListenerCallback) => void} once - Registers a one-time event listener.
* @property {(event: string|symbol, callback: ListenerCallback) => void} addListener - Alias for `on`.
* @property {(event: string|symbol, callback: ListenerCallback) => void} removeListener - Alias for `off`.
* @property {(event: string|symbol, callback: ListenerCallback) => void} prependListener - Adds listener to the beginning.
* @property {(event: string|symbol, callback: ListenerCallback) => void} prependOnceListener - Adds one-time listener to the beginning.
* @property {(count: number) => void} setMaxListeners - Sets the maximum number of listeners.
* @property {() => number} getMaxListeners - Gets the maximum number of listeners.
* @property {(event: string|symbol) => number} listenerCount - Returns the number of listeners for the event.
* @property {(event: string|symbol) => Function[]} listeners - Returns an array of listeners.
* @property {(event: string|symbol) => Function[]} rawListeners - Returns raw array of listeners.
* @property {() => (string|symbol)[]} eventNames - Returns registered event names.
*/
/**
* Provides an interface to manage notifications with event handling
* through Electron's native IPC.
*
* @beta This API is experimental and may change in future versions.
*/
class TinyElectronNotification {
#exposeInMainWorld = '';
#maxListeners = Infinity;
/** @type {Map<string, { resolve: Function, reject: Function, event?: EventEmitter }>} */
#notifications = new Map();
#Events = Events.NotificationEvents;
/**
* Exposes the API securely to the main window context.
*
* @param {string} [apiName='newElectronNotification'] - The name under which the API will be exposed.
*/
installWinScript(apiName = 'newElectronNotification') {
if (this.#exposeInMainWorld.length > 0)
throw new Error(`[installWinScript] API '${this.#exposeInMainWorld}' is already exposed.`);
if (typeof apiName !== 'string' || apiName.trim() === '')
throw new Error('[installWinScript] apiName must be a non-empty string.');
this.#exposeInMainWorld = apiName;
electron.contextBridge.exposeInMainWorld(
apiName,
/** @param {Electron.NotificationConstructorOptions & { tag: string }} args */ (args) =>
this.create(args),
);
}
/**
* Creates a new notification instance and sets up event bridging.
*
* @param {Electron.NotificationConstructorOptions & { tag: string }} arg - Notification configuration.
* @returns {Promise<NotificationInstance>} Resolves to the notification instance methods.
*/
create(arg) {
return new Promise((resolve, reject) => {
if (typeof arg !== 'object' || arg === null)
throw new TypeError('Argument "arg" must be a non-null object.');
if (!('tag' in arg)) throw new Error('Notification "tag" is required.');
if (typeof arg.tag !== 'string' || arg.tag.trim() === '')
throw new Error('Notification "tag" must be a non-empty string.');
if (this.#notifications.has(arg.tag))
throw new Error(`Notification with tag "${arg.tag}" already exists.`);
const notiConfig = { resolve, reject, event: new events.EventEmitter() };
this.#notifications.set(arg.tag, notiConfig);
notiConfig.event.setMaxListeners(this.#maxListeners);
// Using native ipcRenderer.invoke to call handle from main process
electron.ipcRenderer
.invoke(this.#Events.Create, arg)
.then((response) => {
notiConfig.resolve({
/**
* Indicates whether the feature is supported.
* @returns {boolean} True if supported, false otherwise.
*/
isSupported: () => response.isSupported,
/**
* Shows the notification.
* Sends an IPC request to show.
*/
show: () => electron.ipcRenderer.invoke(this.#Events.Show, arg.tag),
/**
* Closes the notification.
* Sends an IPC request to close.
*/
close: () => electron.ipcRenderer.invoke(this.#Events.Close, arg.tag),
// EventEmitter bindings
/**
* Registers an event listener.
* @param {string|symbol} event The event name.
* @param {ListenerCallback} callback The event handler function.
*/
on: (event, callback) => {
notiConfig.event?.on(event, callback);
},
/**
* Removes an event listener.
* @param {string|symbol} event The event name.
* @param {ListenerCallback} callback The event handler function.
*/
off: (event, callback) => {
notiConfig.event?.off(event, callback);
},
/**
* Registers a one-time event listener.
* @param {string|symbol} event The event name.
* @param {ListenerCallback} callback The event handler function.
*/
once: (event, callback) => {
notiConfig.event?.once(event, callback);
},
/**
* Alias for `on`. Registers an event listener.
* @param {string|symbol} event The event name.
* @param {ListenerCallback} callback The event handler function.
*/
addListener: (event, callback) => {
notiConfig.event?.addListener(event, callback);
},
/**
* Alias for `off`. Removes an event listener.
* @param {string|symbol} event The event name.
* @param {ListenerCallback} callback The event handler function.
*/
removeListener: (event, callback) => {
notiConfig.event?.removeListener(event, callback);
},
/**
* Adds an event listener to the beginning of the listeners array.
* @param {string|symbol} event The event name.
* @param {ListenerCallback} callback The event handler function.
*/
prependListener: (event, callback) => {
notiConfig.event?.prependListener(event, callback);
},
/**
* Adds a one-time event listener to the beginning of the listeners array.
* @param {string|symbol} event The event name.
* @param {ListenerCallback} callback The event handler function.
*/
prependOnceListener: (event, callback) => {
notiConfig.event?.prependOnceListener(event, callback);
},
/**
* Sets the maximum number of listeners for the EventEmitter instance.
* @param {number} count The maximum number of listeners.
*/
setMaxListeners: (count) => {
notiConfig.event?.setMaxListeners(count);
},
/**
* Gets the maximum number of listeners for the EventEmitter instance.
* @returns {number} The maximum number of listeners.
*/
getMaxListeners: () => notiConfig.event?.getMaxListeners(),
/**
* Returns the number of listeners listening to the specified event.
* @param {string|symbol} event The event name.
* @returns {number} The number of listeners.
*/
listenerCount: (event) => notiConfig.event?.listenerCount(event),
/**
* Returns a copy of the array of listeners for the specified event.
* @param {string|symbol} event The event name.
* @returns {Function[]} Array of listener functions.
*/
listeners: (event) => notiConfig.event?.listeners(event),
/**
* Returns a copy of the array of listeners for the specified event,
* including wrappers for once listeners.
* @param {string|symbol} event The event name.
* @returns {Function[]} Array of listener functions (raw).
*/
rawListeners: (event) => notiConfig.event?.rawListeners(event),
/**
* Returns an array listing the events for which the emitter has registered listeners.
* @returns {(string|symbol)[]} Array of event names.
*/
eventNames: () => notiConfig.event?.eventNames() || [],
});
})
.catch(reject);
});
}
/**
* Creates a new TinyElectronNotification instance and binds renderer IPC listeners.
*
* @param {Object} [settings={}] - Configuration settings for the notifications.
* @param {NotificationEvents} [settings.eventNames=this.#Events] - Set of event names for internal messaging.
*/
constructor({ eventNames = this.#Events } = {}) {
Utils.checkEventsList(eventNames, this.#Events);
/** @param {string} tag */
const clearNotification = (tag) => {
if (this.#notifications.has(tag)) this.#notifications.delete(tag);
};
/**
* @param {string} eventName
* @param {string} tag
* @param {any} payload
*/
const emitEvent = (eventName, tag, payload) => {
const notiConfig = this.#notifications.get(tag);
if (notiConfig?.event) {
notiConfig.event.emit(eventName, payload);
}
return notiConfig;
};
// Bind native IPC listeners for incoming events from Main
// Close
electron.ipcRenderer.on(this.#Events.Close, (_event, { arg } = {}) => {
const config = emitEvent('close', arg.tag, arg.event);
if (config) delete config.event;
clearNotification(arg.tag);
});
// All
electron.ipcRenderer.on(this.#Events.All, (_event, { arg, type } = {}) => {
emitEvent('all', arg.tag, type);
});
// Show
electron.ipcRenderer.on(this.#Events.Show, (_event, { arg } = {}) =>
emitEvent('show', arg.tag, arg.event),
);
// Click
electron.ipcRenderer.on(this.#Events.Click, (_event, { arg } = {}) => {
emitEvent('click', arg.tag, arg.event);
clearNotification(arg.tag);
});
// Reply
electron.ipcRenderer.on(this.#Events.Reply, (_event, { arg } = {}) => {
emitEvent('reply', arg.tag, arg.reply);
clearNotification(arg.tag);
});
// Action
electron.ipcRenderer.on(this.#Events.Action, (_event, { arg } = {}) => {
emitEvent('action', arg.tag, arg.index);
clearNotification(arg.tag);
});
// Failed
electron.ipcRenderer.on(this.#Events.Failed, (_event, { arg } = {}) => {
const notiConfig = this.#notifications.get(arg.tag);
if (notiConfig?.event) notiConfig.event.emit('failed', new Error(arg.error));
clearNotification(arg.tag);
});
}
}
module.exports = TinyElectronNotification;