osa-bridge
Version:
Cross-platform Apple Events bridge for Node & Electron applications
193 lines (192 loc) • 6.47 kB
JavaScript
import os from "os";
import bindings from "bindings";
/** Internal registry */
const _registry = new Map();
// Platform detection and native module loading
let native = null;
let isSupported = false;
let initializationError = null;
/**
* Safely load the native module with proper error handling
*/
async function loadNativeModule() {
try {
if (os.platform() !== "darwin") {
initializationError = `Apple Events not supported on ${os.platform()}`;
return;
}
// Use bindings to load the native module
native = bindings("osa_bridge");
if (native && typeof native.addHandler === "function") {
isSupported = true;
// Mark dispatch as ready and set it up immediately
native._dispatchReady = true;
if (typeof native.setDispatch === "function") {
native.setDispatch(_dispatch);
}
}
else {
throw new Error("Native module loaded but required functions not available");
}
}
catch (error) {
initializationError = `Failed to load native module: ${error instanceof Error ? error.message : String(error)}`;
// Don't console.warn here - let the application decide how to handle this
}
}
// Initialize the module
loadNativeModule().catch(() => {
// Error already captured in initializationError
});
/** Register a handler for (suite,event). Wild-cards allowed. */
export function on(suite, event, handler) {
if (!isSupported) {
// Store handlers even on unsupported platforms for future compatibility
_registry.set(`${suite}${event}`, handler);
return;
}
try {
native.addHandler(suite, event); // install native hook if not present
_registry.set(`${suite}${event}`, handler);
}
catch (error) {
throw new Error(`Failed to register handler for ${suite}.${event}: ${error instanceof Error ? error.message : String(error)}`);
}
}
/** Check if Apple Events are supported on this platform */
export function isAppleEventsSupported() {
return isSupported;
}
/** Get current platform information */
export function getPlatformInfo() {
return {
platform: os.platform(),
supported: isSupported,
...(initializationError && { error: initializationError }),
architecture: os.arch(),
nodeVersion: process.version,
};
}
/** Get all registered handlers (useful for debugging) */
export function getRegisteredHandlers() {
return Array.from(_registry.keys());
}
/** Remove a handler */
export function off(suite, event) {
const key = `${suite}${event}`;
return _registry.delete(key);
}
/** Remove all handlers */
export function removeAllHandlers() {
_registry.clear();
}
/** Called from native when an Apple event arrives */
const _dispatch = async (ae, done) => {
const key = `${ae.suite}${ae.event}`;
const h = _registry.get(key) ||
_registry.get(`${ae.suite}****`) ||
_registry.get(`****${ae.event}`);
if (!h) {
return done(`No handler registered for ${ae.suite}.${ae.event}`);
}
try {
const result = await h(ae);
done(null, result);
}
catch (e) {
done(e instanceof Error ? e.message : String(e));
}
};
// For Electron compatibility - ensure module works in both main and renderer processes
if (typeof window !== "undefined" &&
window.process &&
window.process.type === "renderer") {
console.warn("osa-bridge: Running in Electron renderer process. Apple Events only work in the main process.");
}
/**
* Helper function to get a human-readable representation of an Apple Event
* Useful for debugging and logging
*/
export function formatAEEvent(evt) {
const parts = [`${evt.suite}.${evt.event}`];
if (evt.targetApp) {
parts.push(`target: ${evt.targetApp}`);
}
if (evt.sourceApp) {
parts.push(`source: ${evt.sourceApp}`);
}
if (evt.transactionID !== undefined) {
parts.push(`txn: ${evt.transactionID}`);
}
const paramCount = Object.keys(evt.params).length;
if (paramCount > 0) {
parts.push(`params: ${paramCount}`);
}
return parts.join(", ");
}
/**
* Helper function to extract commonly used parameter types
* from Apple Event parameters
*/
export function extractCommonParams(params) {
return {
directObject: params["----"], // Direct object parameter
subject: params["subj"], // Subject parameter
result: params["----"], // Result parameter (same as direct object)
data: params["data"], // Data parameter
text: params["ctxt"], // Text parameter
file: params["file"], // File parameter
url: params["url "], // URL parameter (note the space)
};
}
/**
* Type guard to check if a parameter is an object specifier
*/
export function isObjectSpecifier(param) {
return (typeof param === "object" &&
param !== null &&
!Array.isArray(param) &&
"type" in param &&
param.type === "objectSpecifier");
}
/**
* Extract the human-readable representation from an object specifier
* Falls back to building it from the components if not available
*/
export function getObjectSpecifierDescription(param) {
if (!isObjectSpecifier(param)) {
return String(param);
}
// Use the native-generated human-readable string if available
if (param.humanReadable) {
return param.humanReadable;
}
// Fallback: build description from components
let description = `${param.class} ${param.keyData}`;
if (param.container && isObjectSpecifier(param.container)) {
description += ` of ${getObjectSpecifierDescription(param.container)}`;
}
return description;
}
/**
* Extract all object specifiers from event parameters recursively
*/
export function extractObjectSpecifiers(params) {
const specifiers = [];
function findSpecifiers(param) {
if (isObjectSpecifier(param)) {
specifiers.push(param);
if (param.container) {
findSpecifiers(param.container);
}
}
else if (Array.isArray(param)) {
param.forEach(findSpecifiers);
}
else if (typeof param === "object" && param !== null) {
Object.values(param).forEach(findSpecifiers);
}
}
Object.values(params).forEach(findSpecifiers);
return specifiers;
}