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

@diablo-oss/electron-store

v0.1.0

Published

Effect-native Electron store plugin with a Tauri-style API and SuperJSON serialization

Readme

@diablo-oss/electron-store

An Effect-native Electron store plugin with a Tauri-style API.

It gives you typed, key-value persistence that works the same way from the main process and from every renderer. Persistence and IPC both use SuperJSON, so values like Date, BigInt, Map, Set, RegExp, URL, undefined, typed arrays, and special number values (NaN, Infinity) survive the round-trip instead of being flattened to JSON.

You can use it with plain Promises (no Effect knowledge required) or with Effect layers, streams, and schemas.

Why this package

  • Tauri-style plugin shape — install once in main, expose in preload, call from the renderer.
  • Rich serialization — SuperJSON end-to-end, not plain JSON.
  • Two renderer APIs — Promise client for everyday use, Effect API for apps already on Effect.
  • Safe by default — store paths are jailed under a root directory; resource IDs are bound to the renderer that created them.
  • Testable without Electron — in-memory client and Effect layer for unit tests.

Requirements

| Package | Role | | ------------------ | ------------------ | | electron >= 28 | peer dependency | | effect ^3.14 | peer dependency | | superjson | bundled dependency |

Installation

npm install @diablo-oss/electron-store effect

Entry points:

| Import | Process | | ------------------------------------------- | ---------------- | | @diablo-oss/electron-store/main | Main | | @diablo-oss/electron-store/preload | Preload | | @diablo-oss/electron-store/renderer | Renderer / tests |

Quick start

Wire the three Electron sides once:

Main

import { installStorePlugin } from "@diablo-oss/electron-store/main";

// Waits for app.whenReady() internally — safe at top level
const stores = await installStorePlugin();

Preload

import { exposeStorePlugin } from "@diablo-oss/electron-store/preload";

exposeStorePlugin();

Renderer (Promise API)

import { createStoreClient } from "@diablo-oss/electron-store/renderer";

const stores = createStoreClient();

const settings = stores.lazy("settings.store", {
  defaults: { theme: "light", lastOpened: new Date() },
  autoSave: 250,
});

await settings.set("theme", "dark");
const theme = await settings.get<"light" | "dark">("theme");

Main process

installStorePlugin(options?)

Registers IPC handlers and returns a StorePlugin. It always awaits app.whenReady() first, so you can call it at the top of your main entry file.

import { installStorePlugin } from "@diablo-oss/electron-store/main";

const stores = await installStorePlugin({
  // Defaults to app.getPath("userData")
  root: undefined,

  // Deny paths that should stay main-only
  scope: (storePath) => !storePath.startsWith("private/"),

  // Defaults to console.error
  onError: (cause) => console.error(cause),
});

| Option | Type | Description | | --------- | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | root | string? | Directory that stores are resolved under. Defaults to app.getPath("userData"). Paths that escape this root are rejected. | | scope | (storePath, event) => boolean | Per-request access check. Return false to deny a renderer. | | onError | (cause) => void | Called for background errors (for example failed auto-save or quit flush). |

Pinning a store from main

stores.open(path, options?) loads a store and pins it: it stays in memory even after every renderer handle is closed. Changes still broadcast to renderers that hold a handle.

const settings = await stores.open("settings.store", {
  defaults: { theme: "light", lastOpened: new Date() },
  autoSave: true,
});

settings.set("theme", "dark");

Other plugin methods:

  • saveAll() — flush every open store to disk
  • dispose() — remove IPC handlers, close stores, and detach the before-quit listener

On before-quit, the plugin prevents quit once, flushes all dirty stores, then allows the app to exit.

Preload

import { exposeStorePlugin } from "@diablo-oss/electron-store/preload";

// Default global: window.electronStore
exposeStorePlugin();

// Or a custom name (must match the renderer client)
exposeStorePlugin("myStoreBridge");

exposeStorePlugin puts a small bridge on globalThis via contextBridge. The bridge only transports SuperJSON-encoded strings (invoke + onChange).

If the BrowserWindow uses sandbox: true, bundle the preload script (esbuild, Vite, etc.). Sandboxed preload cannot require npm packages from node_modules.

Renderer — Promise API

No Effect knowledge required. The client is constructed synchronously; I/O happens when you call methods.

import { createStoreClient } from "@diablo-oss/electron-store/renderer";

const stores = createStoreClient();
// Or: createStoreClient({ globalName: "myStoreBridge" })

Loading stores

// Eager — opens immediately
const settings = await stores.load("settings.store", {
  defaults: { theme: "light" },
  autoSave: 250,
});

// Lazy — handle is sync; load runs on first method call (or init())
const prefs = stores.lazy("prefs.store", { defaults: { locale: "en" } });
await prefs.init(); // optional explicit load

// Attach only if main (or another renderer) already has it open
const existing = await stores.getStore("settings.store");

Reading and writing

await settings.set("theme", "dark");
await settings.set("lastOpened", new Date());

const theme = await settings.get<"light" | "dark">("theme");
const exists = await settings.has("theme");

await settings.delete("theme");
await settings.clear(); // empty the store
await settings.reset(); // restore defaults

const keys = await settings.keys();
const values = await settings.values();
const entries = await settings.entries();
const count = await settings.length();

await settings.save(); // flush now
await settings.reload(); // re-read from disk
await settings.reload({ ignoreDefaults: true });

Change listeners

const stopAll = await settings.onChange((key, value) => {
  console.log(key, value);
});

const stopTheme = await settings.onKeyChange<"light" | "dark">("theme", (value) =>
  console.log("theme:", value),
);

stopTheme();
stopAll();

Listeners resolve only after the subscription is active, so events are not lost between calling onChange / onKeyChange and the first callback.

Cleanup

await settings.close();
await stores.dispose();

Closing a store releases that renderer’s resource ID. Disposing the client closes its scope and tears down the ManagedRuntime.

Renderer — Effect API

import { Console, Effect, Option, Schema, Stream } from "effect";
import { layerBridge, load } from "@diablo-oss/electron-store/renderer";

const Theme = Schema.Literal("light", "dark");

const program = Effect.gen(function* () {
  const store = yield* load("settings.store", {
    defaults: { theme: "light" },
  });

  const themeChanges = yield* store.changesOf("theme");
  yield* themeChanges.pipe(
    Stream.runForEach((value) => Console.log(value)),
    Effect.forkScoped,
  );

  yield* store.set("theme", "dark");

  const theme = yield* store.getSchema("theme", Theme);

  if (Option.isSome(theme)) {
    yield* Console.log(theme.value);
  }
}).pipe(Effect.scoped, Effect.provide(layerBridge()));

await Effect.runPromise(program);

Useful pieces:

| Export | Role | | ----------------------------- | --------------------------------------------- | | layerBridge(globalName?) | StoreIpc layer over the preload bridge | | load(path, options?) | Open a store; closes on scope finalizer | | getStore(path) | Option of an already-open store | | store.get / getSchema | Read as Option or decode with Effect Schema | | store.changes / changesOf | Scoped effects that yield change streams | | StoreBridgeUnavailableError | Thrown when the preload global is missing | | StoreIpcError | Wraps failed invoke / change decode errors |

Pass a matching globalName if you customized exposeStorePlugin:

Effect.provide(layerBridge("myStoreBridge"));

Store options

Shared by main open / renderer load / lazy:

| Option | Type | Default | Description | | ------------------ | ------------------------- | --------------- | --------------------------------------------------------------------------------------------------------------------------- | | defaults | Record<string, unknown> | {} | Initial values when the file is missing or a key is absent. | | autoSave | boolean \| number | true (100 ms) | true or omitted → 100 ms debounce. A number sets the debounce in ms. false disables auto-save (call save() yourself). | | createNew | boolean | false | Ignore any existing file and start from defaults. | | overrideDefaults | boolean | false | When loading from disk, prefer defaults over stored values for overlapping keys. |

Reload options:

| Option | Description | | ---------------- | ----------------------------------------- | | ignoreDefaults | Reload the file without merging defaults. |

Testing without Electron

Use the in-memory client or Effect layer — same renderer API, no IPC and no disk.

Promise

import { createMemoryStoreClient } from "@diablo-oss/electron-store/renderer";

const stores = createMemoryStoreClient();
const store = await stores.load("test.store");

await store.set("date", new Date());
const date = await store.get<Date>("date");

Effect

import { Effect } from "effect";
import { layerMemory, load } from "@diablo-oss/electron-store/renderer";

const program = Effect.gen(function* () {
  const store = yield* load("test.store");
  yield* store.set("n", 1);
}).pipe(Effect.scoped, Effect.provide(layerMemory));

Behavior and guarantees

  • Path jail — store paths resolve under root (default userData). Relative segments that escape (..) or equal the root are rejected.
  • Resource ownership — each load / getStore returns a resource ID owned by that renderer’s WebContents. Other windows cannot use or close it. Destroyed windows release their handles automatically.
  • Change fan-out — mutations broadcast only to windows that hold a handle for that store.
  • Pinning — main-process open() keeps the store loaded after the last renderer handle closes. Unpinned stores close when the last handle is released.
  • Quit flushbefore-quit waits for pending writes before exit.
  • Serializable values only — anything you set must be SuperJSON- serializable.

Project scripts

npm test          # vitest
npm run typecheck
npm run build     # vite + tsc declarations