{"_id":"@dmytromykhailiuk/preact-signal-effects","_rev":"2-f7358d7d0023cd77244e194e887b7b1b","name":"@dmytromykhailiuk/preact-signal-effects","dist-tags":{"latest":"1.0.0"},"versions":{"0.1.0":{"name":"@dmytromykhailiuk/preact-signal-effects","version":"0.1.0","keywords":["preact","preact-signals","signals","side-effects","effects","middleware","redux","redux-middleware","actions","reactive","typescript","typed"],"author":{"name":"Dmytro Mykhailiuk","email":"dimamykhayluk@gmail.com"},"license":"MIT","_id":"@dmytromykhailiuk/preact-signal-effects@0.1.0","maintainers":[{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"}],"homepage":"https://github.com/dmytromykhailiuk/preact-signal-effects#readme","bugs":{"url":"https://github.com/dmytromykhailiuk/preact-signal-effects/issues"},"dist":{"shasum":"63c5aeed25d97f77349648b5edb27d1ede85bfd3","tarball":"https://registry.npmjs.org/@dmytromykhailiuk/preact-signal-effects/-/preact-signal-effects-0.1.0.tgz","fileCount":9,"integrity":"sha512-yC/moSjCEC6MTXZFH8YGtG087ws2cuMsEyLlfpIaGHMeSA94LvKI0mnvrZEBB7BSSG+KwUAMP/L1f0QrJpc94g==","signatures":[{"sig":"MEYCIQC7clAtj4BDrPp/Dcu+ltv6NlPwrbEgNdrgCGkkez6AFwIhAJQ1frErYTCscQRYjdRt2j5gtLtTkS65zQj0I1nLnVrO","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":52602},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"gitHead":"908b0ea20fe92643362d17e44159ed8c9b23d048","scripts":{"dev":"tsup --watch","lint":"biome check .","test":"vitest run","build":"tsup","format":"biome format --write .","lint:fix":"biome check --write .","typecheck":"tsc --noEmit","playground":"vite --config vite.playground.config.ts","test:watch":"vitest","prepublishOnly":"npm run build"},"_npmUser":{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"},"repository":{"url":"git+https://github.com/dmytromykhailiuk/preact-signal-effects.git","type":"git"},"_npmVersion":"11.6.2","description":"Signal side effects for @preact/signals — declarative createSideEffect over any signals, a lifecycle-managed runner, and a redux-compatible effects middleware with typed createActionEffect. Standalone, framework-agnostic.","directories":{},"sideEffects":false,"_nodeVersion":"24.12.0","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.5","vite":"^5.4.11","jsdom":"^25.0.1","preact":"^10.25.4","vitest":"^2.1.8","typescript":"^5.7.3","@types/node":"^22.10.5","@biomejs/biome":"^1.9.4","@preact/signals":"^2.0.1","@preact/preset-vite":"^2.10.1","@preact/signals-core":"^1.8.0","@dmytromykhailiuk/preact-signal-redux":"file:../preact-signal-redux"},"peerDependencies":{"@preact/signals-core":"^1.6.0"},"_npmOperationalInternal":{"tmp":"tmp/preact-signal-effects_0.1.0_1785097541541_0.5654647950387042","host":"s3://npm-registry-packages-npm-production"}},"1.0.0":{"name":"@dmytromykhailiuk/preact-signal-effects","version":"1.0.0","description":"Signal side effects for @preact/signals — declarative createSideEffect over any signals, a lifecycle-managed runner, and a redux-compatible effects middleware with typed createActionEffect. Standalone, framework-agnostic.","type":"module","sideEffects":false,"author":{"name":"Dmytro Mykhailiuk","email":"dimamykhayluk@gmail.com"},"license":"MIT","keywords":["preact","preact-signals","signals","side-effects","effects","middleware","redux","redux-middleware","actions","reactive","typescript","typed"],"main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"scripts":{"build":"tsup","dev":"tsup --watch","playground":"vite --config vite.playground.config.ts","typecheck":"tsc --noEmit","test":"vitest run","test:watch":"vitest","lint":"biome check .","lint:fix":"biome check --write .","format":"biome format --write .","prepublishOnly":"npm run build"},"engines":{"node":">=18"},"peerDependencies":{"@preact/signals-core":"^1.6.0"},"devDependencies":{"@biomejs/biome":"^1.9.4","@dmytromykhailiuk/preact-signal-redux":"file:../preact-signal-redux","@preact/preset-vite":"^2.10.1","@preact/signals":"^2.0.1","@preact/signals-core":"^1.8.0","@types/node":"^22.10.5","jsdom":"^25.0.1","preact":"^10.25.4","tsup":"^8.3.5","typescript":"^5.7.3","vite":"^5.4.11","vitest":"^2.1.8"},"repository":{"type":"git","url":"git+https://github.com/dmytromykhailiuk/preact-signal-effects.git"},"bugs":{"url":"https://github.com/dmytromykhailiuk/preact-signal-effects/issues"},"homepage":"https://dmytromykhailiuk.github.io/preact-signal-effects/","gitHead":"908b0ea20fe92643362d17e44159ed8c9b23d048","_id":"@dmytromykhailiuk/preact-signal-effects@1.0.0","_nodeVersion":"24.12.0","_npmVersion":"11.6.2","dist":{"integrity":"sha512-MM7Ny37++FaWQQeZR/ub8dEoNo7fkwa8+nJ/XnECK/VyXBvSVQeK2Om2ulaVAVVC8ksTiFgVuEOheLeYwkj9sQ==","shasum":"16a60c5fd5e36aec70a8cdb1493116b37f72fda6","tarball":"https://registry.npmjs.org/@dmytromykhailiuk/preact-signal-effects/-/preact-signal-effects-1.0.0.tgz","fileCount":9,"unpackedSize":52595,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCv8GWkp2lewg5vCURqQgIv7Yfm0L2fl5W5e6OqRwHlVAIhAMd97EafyovlW79NoB9E/nHbFAR+zst627gvZDBAXixk"}]},"_npmUser":{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"},"directories":{},"maintainers":[{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/preact-signal-effects_1.0.0_1786638789577_0.3183676524429122"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-26T20:25:41.327Z","modified":"2026-08-13T16:33:09.851Z","0.1.0":"2026-07-26T20:25:41.685Z","1.0.0":"2026-08-13T16:33:09.725Z"},"bugs":{"url":"https://github.com/dmytromykhailiuk/preact-signal-effects/issues"},"author":{"name":"Dmytro Mykhailiuk","email":"dimamykhayluk@gmail.com"},"license":"MIT","homepage":"https://dmytromykhailiuk.github.io/preact-signal-effects/","keywords":["preact","preact-signals","signals","side-effects","effects","middleware","redux","redux-middleware","actions","reactive","typescript","typed"],"repository":{"type":"git","url":"git+https://github.com/dmytromykhailiuk/preact-signal-effects.git"},"description":"Signal side effects for @preact/signals — declarative createSideEffect over any signals, a lifecycle-managed runner, and a redux-compatible effects middleware with typed createActionEffect. Standalone, framework-agnostic.","maintainers":[{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"}],"readme":"# @dmytromykhailiuk/preact-signal-effects\n\nSignal side effects for [@preact/signals](https://preactjs.com/guide/v10/signals/): a declarative `createSideEffect` over any signals, a lifecycle-managed runner, and a **redux-compatible effects middleware** with a typed `createActionEffect`. Standalone and framework-agnostic — works with [`@dmytromykhailiuk/preact-signal-redux`](https://github.com/dmytromykhailiuk/preact-signal-redux), plain redux / redux-toolkit, or no store at all.\n\n> **Docs:** the full documentation lives in [`index.html`](https://dmytromykhailiuk.github.io/preact-signal-effects/) — this README is the short form.\n\n**Why this exists.** Signals are great at deriving state, but real apps also need *effects*: persist to IndexedDB, call an API, retry a failed upload. This package gives those effects a first-class lifecycle (`run`/`stop`, grouped runners) and — when you use an action-driven store — a clean bridge from dispatched actions to typed effect handlers, without any global mutable `actions$`.\n\n- `createSideEffect(...signals, fn)` — subscribe to N signals, get their values as a typed tuple, microtask-deferred.\n- `createSideEffectsRunner()` — start/stop groups of effects together (per feature, per page).\n- `createEffectsMiddleware()` — a classic curried redux middleware that publishes every dispatched action into a **local** `actions$` signal.\n- `createActionEffect(actions$, creators, handler)` — effects filtered by action creator(s), with fully typed payloads.\n\n## Install\n\n```sh\nnpm install @dmytromykhailiuk/preact-signal-effects @preact/signals-core\n```\n\nThe only peer dependency is `@preact/signals-core` (the primitives `@preact/signals` itself is built on). No dependency on any store library — redux compatibility is purely structural.\n\n## Quick start\n\nA side effect over plain signals — no store involved:\n\n```ts\nimport { signal } from \"@preact/signals\";\nimport { createSideEffect } from \"@dmytromykhailiuk/preact-signal-effects\";\n\nconst user$ = signal({ name: \"Ada\" });\nconst theme$ = signal<\"light\" | \"dark\">(\"light\");\n\nconst persistPrefs = createSideEffect(user$, theme$, (user, theme) => {\n  localStorage.setItem(\"prefs\", JSON.stringify({ user, theme }));\n});\n\nconst stop = persistPrefs.run(); // fires now with current values, then on every change\n// ...\nstop();                          // or persistPrefs.stop()\n```\n\n## Concepts\n\n### Lifecycle\n\n`createSideEffect` returns a `SideEffect`: `{ run(options?), stop(), isRunning }`.\n\n- `run()` subscribes and returns the `stop` function. Calling `run()` while running is a no-op (returns `stop`).\n- `stop()` unsubscribes **and cancels invocations that are scheduled but not yet flushed**.\n- `run()` / `stop()` can cycle any number of times.\n\n### Microtask deferral & batching\n\nSignal values are captured **synchronously** (so the tuple is always consistent), but your callback runs in a **microtask**:\n\n- Writes inside `batch()` collapse into a single invocation.\n- N separate synchronous writes produce N invocations, each with the values captured at write time.\n- Your callback never runs in the middle of a signal write.\n\n### First emission\n\nBy default `run()` fires the callback once immediately (microtask-deferred) with the current values — the right default for \"sync this somewhere\" effects. Pass `run({ immediate: false })` to skip the first emission and only react to subsequent changes (subscriptions are still established by reading every signal).\n\n## `createSideEffectsRunner`\n\nGroup effects and manage them together:\n\n```ts\nimport { createSideEffectsRunner } from \"@dmytromykhailiuk/preact-signal-effects\";\n\nexport const imageSideEffects = createSideEffectsRunner();\nimageSideEffects.register(uploadEffect, retryEffect, persistEffect);\n\nimageSideEffects.run();        // start everything (idempotent)\nimageSideEffects.stop();       // stop everything\nimageSideEffects.unregister(retryEffect); // stop + remove one effect\n```\n\n- `run(options?)` forwards `RunOptions` to every effect and only starts effects that are not already running.\n- Registering while \"running\" does **not** auto-start the new effect — call `run()` again.\n\n## `createEffectsMiddleware`\n\nThe bridge between an action-driven store and signal effects. Each call creates its **own** local `actions$` — no global state:\n\n```ts\nimport { createEffectsMiddleware } from \"@dmytromykhailiuk/preact-signal-effects\";\nimport { createSignalStore } from \"@dmytromykhailiuk/preact-signal-redux\";\n\nconst { middleware, actions$ } = createEffectsMiddleware<State>();\n\nconst store$ = createSignalStore(reducer, initialState, {\n  middlewares: [thunk, logger, middleware], // recommended: LAST in the chain\n});\n```\n\nOrdering guarantees:\n\n- The action is published **after `next(action)` returns** — i.e. after the reducer ran. Combined with the microtask deferral, effect handlers always observe **post-reducer state** via `store.peek()` / `getState()`.\n- The **original action** is published, not `next`'s return value.\n- Non-plain actions (thunk functions, promises) pass through unpublished.\n- Placed last, it only sees actions that survived the outer middleware.\n\nOne caveat: signals skip identity-equal writes, so dispatching the *same action object* twice will not re-emit. Action creators produce fresh objects on every call, so this never bites in practice.\n\n## `createActionEffect`\n\nReplaces the manual `if (action?.type !== someAction.type) return;` boilerplate with typed filtering:\n\n```ts\nimport { createActionEffect, createSideEffectsRunner } from \"@dmytromykhailiuk/preact-signal-effects\";\n\n// single creator — payload fully typed from the creator:\nconst uploadEffect = createActionEffect(actions$, tryUploadImage, async (action) => {\n  const { key, blob } = action.payload;\n  try {\n    await api.upload(key, blob);\n    store$.dispatch(imageUploaded({ key }));\n  } catch {\n    store$.dispatch(imageUploadFailed({ key, blob }));\n  }\n});\n\n// several creators — the action is a typed union:\nconst persistEffect = createActionEffect(\n  actions$,\n  [imageUploaded, imageUploadFailed],\n  async (action) => {\n    const image = store$.peek().images[action.payload.key];\n    if (image) await idb.put(image);\n  },\n);\n\nconst runner = createSideEffectsRunner();\nrunner.register(uploadEffect, persistEffect);\nrunner.run();\n```\n\n- Matching uses `creator.match(action)` when available (preact-signal-redux and redux-toolkit creators both have it), falling back to `creator.type === action.type` — bare `{ type: \"...\" }` objects work too.\n- The handler's `action` parameter is inferred from the creator's call signature (union across an array of creators).\n- The result is a regular `SideEffect` — register it in a runner, `run`/`stop` it like any other.\n- The initial `null` value of `actions$` never triggers a handler.\n\n## Using with preact-signal-redux\n\nThe full action-driven pipeline (see the [playground](src/playground/main.tsx) for a live version):\n\n```ts\nconst { middleware, actions$ } = createEffectsMiddleware<UploadState>();\n\nconst store$ = createSignalStore(reducer, { items: {} }, {\n  middlewares: [createDevToolsMiddleware({ name: \"uploads\" }), middleware],\n});\n\nconst uploadEffect = createActionEffect(actions$, tryUpload, async (action) => {\n  try {\n    await fakeUpload(action.payload.key);\n    store$.dispatch(uploadSucceeded({ key: action.payload.key }));\n  } catch {\n    store$.dispatch(uploadFailed({ key: action.payload.key }));\n  }\n});\n\nconst retryEffect = createActionEffect(actions$, uploadFailed, async (action) => {\n  await delay(1000);\n  if (store$.peek().items[action.payload.key]) {\n    store$.dispatch(tryUpload({ key: action.payload.key }));\n  }\n});\n\nconst runner = createSideEffectsRunner();\nrunner.register(uploadEffect, retryEffect);\nrunner.run();\n```\n\nNote: Redux DevTools time travel on the store performs *silent* state writes that bypass middleware — effects do **not** re-fire while you scrub through history. Replaying the past must not replay its side effects.\n\n## Using with plain redux / redux-toolkit\n\n`createEffectsMiddleware().middleware` is a classic curried middleware, typed structurally — no imports from redux required:\n\n```ts\nimport { configureStore } from \"@reduxjs/toolkit\";\nimport { createEffectsMiddleware, createActionEffect } from \"@dmytromykhailiuk/preact-signal-effects\";\n\nconst { middleware, actions$ } = createEffectsMiddleware<RootState>();\n\nconst store = configureStore({\n  reducer,\n  middleware: (getDefault) => getDefault().concat(middleware),\n});\n\n// RTK createAction creators have .match — createActionEffect uses it:\nconst effect = createActionEffect(actions$, todoAdded, (action) => {\n  console.log(\"todo added:\", action.payload);\n});\neffect.run();\n```\n\n## TypeScript\n\nSignal tuples (`createSideEffect(a$, b$, (a, b) => ...)`), action payloads and creator unions are inferred end-to-end. The package ships `.d.ts` for ESM and `.d.cts` for CJS. Requires TypeScript 5+.\n\n```ts\nimport type {\n  ActionCreatorLike, ActionLike, CompatibleMiddleware, DispatchedAction,\n  EffectsMiddleware, RunOptions, SideEffect, SideEffectsRunner,\n} from \"@dmytromykhailiuk/preact-signal-effects\";\n```\n\n## License\n\nMIT © Dmytro Mykhailiuk\n","readmeFilename":"README.md"}