{"_id":"@diablo-oss/electron-store","name":"@diablo-oss/electron-store","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@diablo-oss/electron-store","version":"0.1.0","description":"Effect-native Electron store plugin with a Tauri-style API and SuperJSON serialization","license":"MIT","repository":{"type":"git","url":"git+https://github.com/levischd/electron-store.git"},"homepage":"https://github.com/levischd/electron-store#readme","bugs":{"url":"https://github.com/levischd/electron-store/issues"},"publishConfig":{"access":"public"},"type":"module","sideEffects":false,"exports":{"./main":{"types":"./dist/main/index.d.ts","import":"./dist/main/index.js","require":"./dist/main/index.cjs"},"./preload":{"types":"./dist/preload/index.d.ts","import":"./dist/preload/index.js","require":"./dist/preload/index.cjs"},"./renderer":{"types":"./dist/renderer/index.d.ts","import":"./dist/renderer/index.js","require":"./dist/renderer/index.cjs"}},"scripts":{"build":"vite build && tsc -p tsconfig.build.json","typecheck":"tsc -p tsconfig.json","test":"vitest run","test:watch":"vitest","prepack":"npm run build"},"dependencies":{"superjson":"^2.2.6"},"devDependencies":{"@effect/language-service":"^0.87.0","@types/node":"^26.1.1","effect":"^3.22.0","electron":"^43.1.1","typescript":"^7.0.2","vite":"^8.1.5","vitest":"^4.1.10"},"peerDependencies":{"effect":"^3.14.21","electron":">=28"},"gitHead":"1f6b49236ac6ebefdee093f79360f6a5bad232fb","_id":"@diablo-oss/electron-store@0.1.0","_nodeVersion":"25.6.0","_npmVersion":"11.8.0","dist":{"integrity":"sha512-dmylqi3ZTktd/ivfsF4qrTDt9pz4jIe/7BqjA0s9q90CfmMJFA5gcJwOL0dI54M5h0ZkgYZq+t0yxAZHtUibZQ==","shasum":"78aad4d17c520fd0b4226e9cdc5b1ea00156bb00","tarball":"https://registry.npmjs.org/@diablo-oss/electron-store/-/electron-store-0.1.0.tgz","fileCount":38,"unpackedSize":199615,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCkyOBW3EUMh3lmIWwtqKRr6+bCPRMsrcDQegX+0EAVEAIhAKMJqTZG1t/Q+CoOCsXw/g5RYediC9pUA4KZqtk0X2dW"}]},"_npmUser":{"name":"lescd","email":"clamp.yowl0d@icloud.com"},"directories":{},"maintainers":[{"name":"lescd","email":"clamp.yowl0d@icloud.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/electron-store_0.1.0_1784384710646_0.8712461957872275"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-18T14:25:10.481Z","0.1.0":"2026-07-18T14:25:10.779Z","modified":"2026-07-18T14:25:10.962Z"},"maintainers":[{"name":"lescd","email":"clamp.yowl0d@icloud.com"}],"description":"Effect-native Electron store plugin with a Tauri-style API and SuperJSON serialization","homepage":"https://github.com/levischd/electron-store#readme","repository":{"type":"git","url":"git+https://github.com/levischd/electron-store.git"},"bugs":{"url":"https://github.com/levischd/electron-store/issues"},"license":"MIT","readme":"# @diablo-oss/electron-store\n\nAn Effect-native Electron store plugin with a Tauri-style API.\n\nIt 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](https://github.com/blitz-js/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.\n\nYou can use it with plain Promises (no Effect knowledge required) or with Effect layers, streams, and schemas.\n\n## Why this package\n\n- **Tauri-style plugin shape** — install once in main, expose in preload, call from the renderer.\n- **Rich serialization** — SuperJSON end-to-end, not plain JSON.\n- **Two renderer APIs** — Promise client for everyday use, Effect API for apps already on Effect.\n- **Safe by default** — store paths are jailed under a root directory; resource IDs are bound to the renderer that created them.\n- **Testable without Electron** — in-memory client and Effect layer for unit tests.\n\n## Requirements\n\n| Package            | Role               |\n| ------------------ | ------------------ |\n| `electron` `>= 28` | peer dependency    |\n| `effect` `^3.14`   | peer dependency    |\n| `superjson`        | bundled dependency |\n\n## Installation\n\n```bash\nnpm install @diablo-oss/electron-store effect\n```\n\nEntry points:\n\n| Import                                      | Process          |\n| ------------------------------------------- | ---------------- |\n| `@diablo-oss/electron-store/main`           | Main             |\n| `@diablo-oss/electron-store/preload`        | Preload          |\n| `@diablo-oss/electron-store/renderer`       | Renderer / tests |\n\n## Quick start\n\nWire the three Electron sides once:\n\n**Main**\n\n```ts\nimport { installStorePlugin } from \"@diablo-oss/electron-store/main\";\n\n// Waits for app.whenReady() internally — safe at top level\nconst stores = await installStorePlugin();\n```\n\n**Preload**\n\n```ts\nimport { exposeStorePlugin } from \"@diablo-oss/electron-store/preload\";\n\nexposeStorePlugin();\n```\n\n**Renderer (Promise API)**\n\n```ts\nimport { createStoreClient } from \"@diablo-oss/electron-store/renderer\";\n\nconst stores = createStoreClient();\n\nconst settings = stores.lazy(\"settings.store\", {\n  defaults: { theme: \"light\", lastOpened: new Date() },\n  autoSave: 250,\n});\n\nawait settings.set(\"theme\", \"dark\");\nconst theme = await settings.get<\"light\" | \"dark\">(\"theme\");\n```\n\n## Main process\n\n### `installStorePlugin(options?)`\n\nRegisters 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.\n\n```ts\nimport { installStorePlugin } from \"@diablo-oss/electron-store/main\";\n\nconst stores = await installStorePlugin({\n  // Defaults to app.getPath(\"userData\")\n  root: undefined,\n\n  // Deny paths that should stay main-only\n  scope: (storePath) => !storePath.startsWith(\"private/\"),\n\n  // Defaults to console.error\n  onError: (cause) => console.error(cause),\n});\n```\n\n| Option    | Type                            | Description                                                                                                                |\n| --------- | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |\n| `root`    | `string?`                       | Directory that stores are resolved under. Defaults to `app.getPath(\"userData\")`. Paths that escape this root are rejected. |\n| `scope`   | `(storePath, event) => boolean` | Per-request access check. Return `false` to deny a renderer.                                                               |\n| `onError` | `(cause) => void`               | Called for background errors (for example failed auto-save or quit flush).                                                 |\n\n### Pinning a store from main\n\n`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.\n\n```ts\nconst settings = await stores.open(\"settings.store\", {\n  defaults: { theme: \"light\", lastOpened: new Date() },\n  autoSave: true,\n});\n\nsettings.set(\"theme\", \"dark\");\n```\n\nOther plugin methods:\n\n- `saveAll()` — flush every open store to disk\n- `dispose()` — remove IPC handlers, close stores, and detach the `before-quit` listener\n\nOn `before-quit`, the plugin prevents quit once, flushes all dirty stores, then allows the app to exit.\n\n## Preload\n\n```ts\nimport { exposeStorePlugin } from \"@diablo-oss/electron-store/preload\";\n\n// Default global: window.electronStore\nexposeStorePlugin();\n\n// Or a custom name (must match the renderer client)\nexposeStorePlugin(\"myStoreBridge\");\n```\n\n`exposeStorePlugin` puts a small bridge on `globalThis` via `contextBridge`. The bridge only transports SuperJSON-encoded strings (`invoke` + `onChange`).\n\nIf the BrowserWindow uses `sandbox: true`, bundle the preload script (esbuild, Vite, etc.). Sandboxed preload cannot `require` npm packages from `node_modules`.\n\n## Renderer — Promise API\n\nNo Effect knowledge required. The client is constructed synchronously; I/O happens when you call methods.\n\n```ts\nimport { createStoreClient } from \"@diablo-oss/electron-store/renderer\";\n\nconst stores = createStoreClient();\n// Or: createStoreClient({ globalName: \"myStoreBridge\" })\n```\n\n### Loading stores\n\n```ts\n// Eager — opens immediately\nconst settings = await stores.load(\"settings.store\", {\n  defaults: { theme: \"light\" },\n  autoSave: 250,\n});\n\n// Lazy — handle is sync; load runs on first method call (or init())\nconst prefs = stores.lazy(\"prefs.store\", { defaults: { locale: \"en\" } });\nawait prefs.init(); // optional explicit load\n\n// Attach only if main (or another renderer) already has it open\nconst existing = await stores.getStore(\"settings.store\");\n```\n\n### Reading and writing\n\n```ts\nawait settings.set(\"theme\", \"dark\");\nawait settings.set(\"lastOpened\", new Date());\n\nconst theme = await settings.get<\"light\" | \"dark\">(\"theme\");\nconst exists = await settings.has(\"theme\");\n\nawait settings.delete(\"theme\");\nawait settings.clear(); // empty the store\nawait settings.reset(); // restore defaults\n\nconst keys = await settings.keys();\nconst values = await settings.values();\nconst entries = await settings.entries();\nconst count = await settings.length();\n\nawait settings.save(); // flush now\nawait settings.reload(); // re-read from disk\nawait settings.reload({ ignoreDefaults: true });\n```\n\n### Change listeners\n\n```ts\nconst stopAll = await settings.onChange((key, value) => {\n  console.log(key, value);\n});\n\nconst stopTheme = await settings.onKeyChange<\"light\" | \"dark\">(\"theme\", (value) =>\n  console.log(\"theme:\", value),\n);\n\nstopTheme();\nstopAll();\n```\n\nListeners resolve only after the subscription is active, so events are not lost between calling `onChange` / `onKeyChange` and the first callback.\n\n### Cleanup\n\n```ts\nawait settings.close();\nawait stores.dispose();\n```\n\nClosing a store releases that renderer’s resource ID. Disposing the client closes its scope and tears down the ManagedRuntime.\n\n## Renderer — Effect API\n\n```ts\nimport { Console, Effect, Option, Schema, Stream } from \"effect\";\nimport { layerBridge, load } from \"@diablo-oss/electron-store/renderer\";\n\nconst Theme = Schema.Literal(\"light\", \"dark\");\n\nconst program = Effect.gen(function* () {\n  const store = yield* load(\"settings.store\", {\n    defaults: { theme: \"light\" },\n  });\n\n  const themeChanges = yield* store.changesOf(\"theme\");\n  yield* themeChanges.pipe(\n    Stream.runForEach((value) => Console.log(value)),\n    Effect.forkScoped,\n  );\n\n  yield* store.set(\"theme\", \"dark\");\n\n  const theme = yield* store.getSchema(\"theme\", Theme);\n\n  if (Option.isSome(theme)) {\n    yield* Console.log(theme.value);\n  }\n}).pipe(Effect.scoped, Effect.provide(layerBridge()));\n\nawait Effect.runPromise(program);\n```\n\nUseful pieces:\n\n| Export                        | Role                                          |\n| ----------------------------- | --------------------------------------------- |\n| `layerBridge(globalName?)`    | `StoreIpc` layer over the preload bridge      |\n| `load(path, options?)`        | Open a store; closes on scope finalizer       |\n| `getStore(path)`              | `Option` of an already-open store             |\n| `store.get` / `getSchema`     | Read as `Option` or decode with Effect Schema |\n| `store.changes` / `changesOf` | Scoped effects that yield change streams      |\n| `StoreBridgeUnavailableError` | Thrown when the preload global is missing     |\n| `StoreIpcError`               | Wraps failed invoke / change decode errors    |\n\nPass a matching `globalName` if you customized `exposeStorePlugin`:\n\n```ts\nEffect.provide(layerBridge(\"myStoreBridge\"));\n```\n\n## Store options\n\nShared by main `open` / renderer `load` / `lazy`:\n\n| Option             | Type                      | Default         | Description                                                                                                                 |\n| ------------------ | ------------------------- | --------------- | --------------------------------------------------------------------------------------------------------------------------- |\n| `defaults`         | `Record<string, unknown>` | `{}`            | Initial values when the file is missing or a key is absent.                                                                 |\n| `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). |\n| `createNew`        | `boolean`                 | `false`         | Ignore any existing file and start from defaults.                                                                           |\n| `overrideDefaults` | `boolean`                 | `false`         | When loading from disk, prefer defaults over stored values for overlapping keys.                                            |\n\nReload options:\n\n| Option           | Description                               |\n| ---------------- | ----------------------------------------- |\n| `ignoreDefaults` | Reload the file without merging defaults. |\n\n## Testing without Electron\n\nUse the in-memory client or Effect layer — same renderer API, no IPC and no disk.\n\n**Promise**\n\n```ts\nimport { createMemoryStoreClient } from \"@diablo-oss/electron-store/renderer\";\n\nconst stores = createMemoryStoreClient();\nconst store = await stores.load(\"test.store\");\n\nawait store.set(\"date\", new Date());\nconst date = await store.get<Date>(\"date\");\n```\n\n**Effect**\n\n```ts\nimport { Effect } from \"effect\";\nimport { layerMemory, load } from \"@diablo-oss/electron-store/renderer\";\n\nconst program = Effect.gen(function* () {\n  const store = yield* load(\"test.store\");\n  yield* store.set(\"n\", 1);\n}).pipe(Effect.scoped, Effect.provide(layerMemory));\n```\n\n## Behavior and guarantees\n\n- **Path jail** — store paths resolve under `root` (default `userData`). Relative segments that escape (`..`) or equal the root are rejected.\n- **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.\n- **Change fan-out** — mutations broadcast only to windows that hold a handle for that store.\n- **Pinning** — main-process `open()` keeps the store loaded after the last renderer handle closes. Unpinned stores close when the last handle is released.\n- **Quit flush** — `before-quit` waits for pending writes before exit.\n- **Serializable values only** — anything you `set` must be SuperJSON- serializable.\n\n## Project scripts\n\n```bash\nnpm test          # vitest\nnpm run typecheck\nnpm run build     # vite + tsc declarations\n```\n","readmeFilename":"README.md","_rev":"1-f47a3ba40fd687047d1e5ab2e8cbedae"}