npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@electrojs/runtime

v1.0.8

Published

Electron main-process runtime for ElectroJS with DI, lifecycle hooks, typed IPC, and jobs

Readme

@electrojs/runtime

The main-process runtime for ElectroJS applications.

Documentation: https://electrojs.myraxbyte.dev/guide/getting-started

@electrojs/runtime manages the full lifecycle of an Electron main process: scanning decorator metadata, building the module graph, wiring dependency injection, running lifecycle hooks, exposing a typed IPC bridge, dispatching signals, and scheduling background jobs. It is the layer between your application code and the Electron APIs.


Installation

npm install @electrojs/runtime

Peer dependency: @electrojs/common must be installed alongside.


Quick start

import { Module, Injectable, command, query } from "@electrojs/common";
import { AppKernel, createConsoleLogger } from "@electrojs/runtime";

@Injectable()
class GreetingService {
    @query()
    hello() {
        return "world";
    }

    @command()
    setLocale(locale: string) {
        // ...
    }
}

@Module({ providers: [GreetingService] })
class AppModule {}

const kernel = AppKernel.create(AppModule, {
    logger: createConsoleLogger(),
});
await kernel.initialize();
await kernel.start();

AppKernel.initialize() scans AppModule, validates the module graph, creates the DI container, instantiates module declarations, installs capabilities, and runs onInit. kernel.start() then runs onStart and onReady. Calling start() directly from idle still performs initialization automatically for backward compatibility.

Modules, providers, windows, and views also receive this.logger through the authoring API, so app-level logs can use the same runtime logger contract and be redirected to a file, Sentry, or any other sink by passing a custom logger into AppKernel.create(...).


Modules

Every feature lives inside a module. A module declares what it owns and what it shares:

@Module({
    imports: [DatabaseModule],
    providers: [UserService],
    views: [UserView],
    windows: [UserWindow],
    exports: [UserService, UserWindow],
})
class UserModule {}
  • imports -- modules this module depends on. Their exported declarations become available for injection.
  • providers -- service and infrastructure classes managed by this module.
  • views -- renderer view classes owned by this module.
  • windows -- window host classes owned by this module.
  • exports -- declarations shared with modules that import this one.

The framework builds a directed acyclic graph from imports, detects cycles at startup, and instantiates modules in dependency-first (topological) order.


Providers and dependency injection

Providers are classes decorated with @Injectable(). They hold business logic, repositories, and infrastructure. Views and windows are declared separately in @Module({ views, windows }).

Inject dependencies with the inject() function:

import { inject, Injector } from "@electrojs/runtime";

@Injectable()
class AuthService {
    private readonly db = inject(DatabaseService);
    private readonly config = inject(ConfigService);

    @query()
    getMe() {
        return this.db.findUser(this.config.get("userId"));
    }
}

inject() works inside:

  • Property initializers -- during construction of framework-managed classes
  • Lifecycle hooks -- onInit, onStart, onReady, onShutdown, onDispose
  • Capability handlers -- methods decorated with @command, @query, @signal, @job

Calling it outside these scopes throws a DIError.

Scopes

  • singleton (default) -- one instance per module injector
  • transient -- a new instance every time the token is resolved
@Injectable({ scope: "transient" })
class RequestContext {}

Lifecycle hooks

Modules and providers can implement lifecycle hooks to participate in startup and shutdown:

@Injectable()
class DatabaseService {
    async onInit() {
        await this.pool.connect();
    }

    async onStart() {
        await this.attachWindowListeners();
    }

    async onReady() {
        await this.runMigrations();
    }

    async onShutdown() {
        await this.pool.end();
    }

    onDispose() {
        this.logger.flush();
    }
}

| Hook | Phase | Purpose | | ------------ | ---------- | ------------------------------------------------------------- | | onInit | Initialize | Prepare bridge-safe state, handlers, and runtime dependencies | | onStart | Startup | Start windows, jobs, network bootstrapping, launch flows | | onReady | Startup | Final coordination before the kernel becomes started | | onShutdown | Shutdown | Release resources gracefully | | onDispose | Shutdown | Final cleanup (file handles, timers) |

Ordering: onInit, onStart, and onReady all run per-module in dependency-first order. Within each module, provider hooks run before the module's own hook. Shutdown runs in the reverse order -- module first, then its providers.

If a startup hook throws, the framework automatically rolls back already-initialized modules by calling their shutdown hooks.


Bridge (IPC)

The bridge connects the main process to renderer views with typed channels. Decorate methods with @command() or @query() to expose them to the renderer:

@Injectable()
class FileService {
    @query()
    async listFiles(directory: string): Promise<string[]> {
        return fs.readdir(directory);
    }

    @command()
    async deleteFile(path: string): Promise<void> {
        await fs.unlink(path);
    }
}
  • Queries are read-only operations -- they return data without side effects.
  • Commands perform mutations.

Channel names are derived as moduleId:methodName (e.g. file:listFiles). Views declare which channels they can access:

@View({
    source: "view:main",
    access: ["file:listFiles", "file:deleteFile"],
})
class MainView extends ViewProvider {}

Calls from a view to channels not listed in access are rejected by the BridgeAccessGuard.


Signals

Signals provide fire-and-forget cross-module communication. Any provider can publish; any provider can subscribe.

Declarative handlers

Use the @signal() decorator to subscribe a method at bootstrap time:

@Injectable()
class NotificationService {
    @signal({ id: "user-logged-in" })
    onUserLogin(payload: { userId: string }) {
        this.showWelcome(payload.userId);
    }
}

Programmatic API

Use SignalBus directly for runtime subscriptions:

@Injectable()
class AuditService {
    private readonly bus = inject(SignalBus);

    onInit() {
        this.bus.subscribe("user-logged-in", (payload) => {
            this.log("login", payload);
        });
    }
}

Publishing a signal:

this.bus.publish("user-logged-in", { userId: "42" });

Handlers run asynchronously via microtasks. Each handler retains the injection context from the point where it was subscribed, so inject() works correctly inside handlers.


Jobs

Jobs are background tasks that can run on a cron schedule or be triggered manually. Decorate a method with @job():

@Injectable()
class SyncService {
    @job({ cron: "0 * * * *" })
    async syncUsers(context: JobContext) {
        const users = await this.fetchRemoteUsers();

        for (const [i, user] of users.entries()) {
            if (context.isCancelled) break;
            context.reportProgress(((i + 1) / users.length) * 100);
            await this.upsert(user);
        }
    }
}

The first argument is always a JobContext, which provides:

  • isCancelled -- check whether the job was cancelled
  • reportProgress(percent) -- report 0-100 progress

Jobs without a cron option are manual-only. Use JobRegistry to control them at runtime:

const jobs = inject(JobRegistry);
await jobs.run("sync:syncUsers"); // trigger manually
jobs.cancel("sync:syncUsers"); // cancel a running job
const state = jobs.getState("sync:syncUsers"); // { status, progress, lastRunAt }

Desktop

Windows and views are first-class providers. Extend the base classes WindowProvider and ViewProvider to manage Electron windows and their content.

Windows

@Window({ id: "main", configuration: { width: 1280, height: 800 } })
class MainWindow extends WindowProvider {
    private readonly mainView = inject(MainView);

    onStart() {
        this.create();
        this.mount(this.mainView);
        this.show();
    }
}

WindowProvider gives you: create(), mount(view), show(), hide(), focus(), close(), getBounds().

Views

@View({
    source: "view:main",
    access: ["file:listFiles", "file:deleteFile"],
    signals: ["user-logged-in"],
})
class MainView extends ViewProvider {
    async onStart() {
        await this.load();
        this.setBounds({ x: 0, y: 0, width: 1280, height: 800 });
        this.setBackgroundColor("#00000000");
    }
}

ViewProvider gives you: load(), setBounds(rect), setBackgroundColor(color), focus(). Views are created with strict security defaults: sandbox: true, contextIsolation: true, nodeIntegration: false.


Exports

// App
import { AppKernel } from "@electrojs/runtime";

// Container
import { inject, InjectionContext, Injector } from "@electrojs/runtime";

// Modules
import { ModuleRef, ProviderRef, ModuleRegistry } from "@electrojs/runtime";
import { scanModules, validateAppDefinition } from "@electrojs/runtime";

// Signals
import { SignalBus, SignalContext } from "@electrojs/runtime";

// Jobs
import { JobContext, JobRegistry } from "@electrojs/runtime";

// Desktop
import { WindowProvider, ViewProvider, WindowManager, ViewManager, RendererRegistry, RendererSession } from "@electrojs/runtime";

// Bridge
import { BridgeAccessGuard, BridgeDispatcher, BridgeHandler, serializeBridgeError } from "@electrojs/runtime";

// Errors
import { RuntimeError, BootstrapError, DIError, LifecycleError, BridgeError, SignalError, JobError } from "@electrojs/runtime";

Type-only imports:

import type {
    AppDefinition,
    ModuleDefinition,
    ProviderDefinition,
    ViewDefinition,
    WindowDefinition,
    BridgeMethodDefinition,
    SignalHandlerDefinition,
    JobDefinition,
    KernelState,
    ModuleStatus,
    LifecycleTarget,
    ModuleSnapshot,
    ProviderSnapshot,
    JobStatus,
    JobRuntimeState,
    BridgeRequest,
    BridgeResponse,
    SignalHandler,
    SignalListener,
    ContextualSignalHandler,
    ProviderKind,
    BridgeMethodKind,
} from "@electrojs/runtime";

Package layering

@electrojs/common        ← decorators, metadata, DI primitives
    ↑
@electrojs/runtime       ← this package: DI container, lifecycle, bridge, signals, jobs, desktop
    ↑
@electrojs/renderer      ← renderer-side bridge client, signal subscriptions

@electrojs/common defines the shared vocabulary. @electrojs/runtime implements the main-process engine. @electrojs/renderer provides the renderer-side counterpart.