@vielzeug/ripple 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/README.md +101 -0
  2. package/dist/_dev.cjs +2 -0
  3. package/dist/_dev.cjs.map +1 -0
  4. package/dist/_dev.d.ts +2 -0
  5. package/dist/_dev.d.ts.map +1 -0
  6. package/dist/_dev.js +9 -0
  7. package/dist/_dev.js.map +1 -0
  8. package/dist/_error-utils.cjs +2 -0
  9. package/dist/_error-utils.cjs.map +1 -0
  10. package/dist/_error-utils.d.ts +24 -0
  11. package/dist/_error-utils.d.ts.map +1 -0
  12. package/dist/_error-utils.js +20 -0
  13. package/dist/_error-utils.js.map +1 -0
  14. package/dist/_virtual/_rolldown/runtime.js +7 -0
  15. package/dist/async-computed.cjs +2 -0
  16. package/dist/async-computed.cjs.map +1 -0
  17. package/dist/async-computed.d.ts +36 -0
  18. package/dist/async-computed.d.ts.map +1 -0
  19. package/dist/async-computed.js +58 -0
  20. package/dist/async-computed.js.map +1 -0
  21. package/dist/computed.cjs +2 -0
  22. package/dist/computed.cjs.map +1 -0
  23. package/dist/computed.d.ts +26 -0
  24. package/dist/computed.d.ts.map +1 -0
  25. package/dist/computed.js +113 -0
  26. package/dist/computed.js.map +1 -0
  27. package/dist/devtools-hook.cjs +2 -0
  28. package/dist/devtools-hook.cjs.map +1 -0
  29. package/dist/devtools-hook.d.ts +4 -0
  30. package/dist/devtools-hook.d.ts.map +1 -0
  31. package/dist/devtools-hook.js +8 -0
  32. package/dist/devtools-hook.js.map +1 -0
  33. package/dist/devtools.d.ts +39 -0
  34. package/dist/devtools.d.ts.map +1 -0
  35. package/dist/effect.cjs +2 -0
  36. package/dist/effect.cjs.map +1 -0
  37. package/dist/effect.d.ts +104 -0
  38. package/dist/effect.d.ts.map +1 -0
  39. package/dist/effect.js +173 -0
  40. package/dist/effect.js.map +1 -0
  41. package/dist/errors.cjs +2 -0
  42. package/dist/errors.cjs.map +1 -0
  43. package/dist/errors.d.ts +24 -0
  44. package/dist/errors.d.ts.map +1 -0
  45. package/dist/errors.js +13 -0
  46. package/dist/errors.js.map +1 -0
  47. package/dist/index.cjs +1 -0
  48. package/dist/index.d.ts +12 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +12 -0
  51. package/dist/reactive-base.cjs +2 -0
  52. package/dist/reactive-base.cjs.map +1 -0
  53. package/dist/reactive-base.d.ts +60 -0
  54. package/dist/reactive-base.d.ts.map +1 -0
  55. package/dist/reactive-base.js +56 -0
  56. package/dist/reactive-base.js.map +1 -0
  57. package/dist/ripple.cjs +2 -0
  58. package/dist/ripple.cjs.map +1 -0
  59. package/dist/ripple.iife.js +2 -0
  60. package/dist/ripple.iife.js.map +1 -0
  61. package/dist/ripple.js +2 -0
  62. package/dist/ripple.js.map +1 -0
  63. package/dist/scheduling.cjs +2 -0
  64. package/dist/scheduling.cjs.map +1 -0
  65. package/dist/scheduling.d.ts +5 -0
  66. package/dist/scheduling.d.ts.map +1 -0
  67. package/dist/scheduling.js +57 -0
  68. package/dist/scheduling.js.map +1 -0
  69. package/dist/signal.cjs +2 -0
  70. package/dist/signal.cjs.map +1 -0
  71. package/dist/signal.d.ts +24 -0
  72. package/dist/signal.d.ts.map +1 -0
  73. package/dist/signal.js +54 -0
  74. package/dist/signal.js.map +1 -0
  75. package/dist/src/devtools.cjs +2 -0
  76. package/dist/src/devtools.cjs.map +1 -0
  77. package/dist/src/devtools.js +36 -0
  78. package/dist/src/devtools.js.map +1 -0
  79. package/dist/src/ssr/index.cjs +2 -0
  80. package/dist/src/ssr/index.cjs.map +1 -0
  81. package/dist/src/ssr/index.js +38 -0
  82. package/dist/src/ssr/index.js.map +1 -0
  83. package/dist/ssr/index.d.ts +56 -0
  84. package/dist/ssr/index.d.ts.map +1 -0
  85. package/dist/store-history.cjs +2 -0
  86. package/dist/store-history.cjs.map +1 -0
  87. package/dist/store-history.d.ts +42 -0
  88. package/dist/store-history.d.ts.map +1 -0
  89. package/dist/store-history.js +60 -0
  90. package/dist/store-history.js.map +1 -0
  91. package/dist/store.cjs +2 -0
  92. package/dist/store.cjs.map +1 -0
  93. package/dist/store.d.ts +45 -0
  94. package/dist/store.d.ts.map +1 -0
  95. package/dist/store.js +204 -0
  96. package/dist/store.js.map +1 -0
  97. package/dist/subscription.cjs +2 -0
  98. package/dist/subscription.cjs.map +1 -0
  99. package/dist/subscription.d.ts +22 -0
  100. package/dist/subscription.d.ts.map +1 -0
  101. package/dist/subscription.js +47 -0
  102. package/dist/subscription.js.map +1 -0
  103. package/dist/symbols.cjs +2 -0
  104. package/dist/symbols.cjs.map +1 -0
  105. package/dist/symbols.d.ts +6 -0
  106. package/dist/symbols.d.ts.map +1 -0
  107. package/dist/symbols.js +6 -0
  108. package/dist/symbols.js.map +1 -0
  109. package/dist/tracking.cjs +2 -0
  110. package/dist/tracking.cjs.map +1 -0
  111. package/dist/tracking.d.ts +82 -0
  112. package/dist/tracking.d.ts.map +1 -0
  113. package/dist/tracking.js +76 -0
  114. package/dist/tracking.js.map +1 -0
  115. package/dist/types.d.ts +315 -0
  116. package/dist/types.d.ts.map +1 -0
  117. package/dist/utilities.cjs +2 -0
  118. package/dist/utilities.cjs.map +1 -0
  119. package/dist/utilities.d.ts +55 -0
  120. package/dist/utilities.d.ts.map +1 -0
  121. package/dist/utilities.js +25 -0
  122. package/dist/utilities.js.map +1 -0
  123. package/package.json +52 -0
@@ -0,0 +1,2 @@
1
+ const e=require("./devtools-hook.cjs"),t=require("./_error-utils.cjs"),n=require("./errors.cjs"),r=require("./tracking.cjs");require("./scheduling.cjs");const i=require("./subscription.cjs"),a=require("./symbols.cjs");var o=(e,t)=>{if(t===`sync`||t===void 0)return e;let n=!1;return()=>{n||(n=!0,queueMicrotask(()=>{n=!1,e()}))}},s=(s,c)=>{let l=c?.scheduler??`sync`,u=c?.name,d=[],f=new Set,p=new Map,m=!1,h=!1,g=!1,_=()=>{if(p.clear(),d.length===0&&f.size===0)return;let e=[...d,...f];d.length=0,f.clear(),t.runAll(e,`effect teardown errors`)},v=()=>{if(!g){if(m){h=!0;return}m=!0;try{let i=0;do{if(++i>100)throw new n.RippleInfiniteLoopError(`infinite effect loop (> 100 iterations)${u?` in "${u}"`:``}`);h=!1,_(),e.getDevToolsHook()?.run?.({name:u});let a;try{a=r.withTracking({cleanups:d,deps:p,effect:y,kind:`effect`,subscriptions:f},s)}catch(e){let n=[...t.collectErrors(f),...t.collectErrors(d)];d.length=0,f.clear(),t.rethrowWith(e,n,`effect failure with cleanup errors`)}if(a!==void 0&&typeof a!=`function`)throw new n.RippleInvalidCleanupError(`effect() returned ${typeof a} — expected a cleanup function or void.`);typeof a==`function`&&d.push(a)}while(h&&!g)}finally{m=!1}}},y=o(v,l);v();let b=new i.SubscriptionImpl(()=>{g||(g=!0,_(),e.getDevToolsHook()?.dispose?.({kind:`effect`,name:u}))});return r.getScopeCleanups()?.push(()=>b.dispose()),{dispose:()=>b.dispose(),get disposed(){return b.disposed},getDependencies(){return[...p.keys()].map(e=>({kind:a.IS_COMPUTED in e?`computed`:`signal`,name:e.name}))},[Symbol.dispose](){b.dispose()}}},c=(e,t)=>{let n=null,r=null,a=null,o=t?.onError??(e=>{queueMicrotask(()=>{throw e})});return new i.AsyncSubscriptionImpl(s(()=>{n?.abort(),r?.(),r=null,n=new AbortController;let{signal:t}=n,i=u(),s;return a=s=(async()=>{try{let n=await e(t,i);typeof n==`function`&&(t.aborted?n():r=n)}catch(e){t.aborted||o(e)}finally{i.dispose(),a===s&&(a=null)}})(),()=>{n?.abort(),r?.(),r=null}},{name:t?.name}),async()=>{let e=a;e&&await e},()=>a)},l=e=>{let t=r.getTracking();if(t?.kind===`effect`){t.cleanups.push(e);return}let i=r.getScopeCleanups();if(i!==null){i.push(e);return}throw new n.RippleInvalidCleanupError(`onCleanup() must be called from within an active effect or scope.`)},u=e=>{let i=[],a=new AbortController,o=!1,s=e=>{if(o)throw new n.RippleDisposedScopeError(`Cannot register cleanup on a disposed scope.`);i.push(e)},c=e=>{if(o)throw new n.RippleDisposedScopeError(`Cannot run inside a disposed scope.`);return r.withScopeCleanups(i,e)},l=()=>{o||(o=!0,a.abort(),t.runAll([...i].reverse(),`scope cleanup errors`),i.length=0)},u={add:s,get disposalSignal(){return a.signal},dispose:l,get disposed(){return o},run:c,[Symbol.dispose]:l};return e&&c(e),u};function d(e,t,a){let o=a?.equals??Object.is,c,l=(e,r)=>{c?.(),c=void 0;let i=t(e,r);if(i!==void 0&&typeof i!=`function`)throw new n.RippleInvalidCleanupError(`watch() callback returned ${typeof i} — expected a cleanup function or void.`);typeof i==`function`&&(c=i)},u;if(typeof e==`function`){let t,n=!0,r=s(()=>{let r=e();if(n){n=!1,t=r;return}if(!o(t,r)){let e=t;t=r,l(r,e),a?.once&&u.dispose()}},{name:a?.name});u=new i.SubscriptionImpl(()=>{c?.(),c=void 0,r.dispose()}),a?.immediate&&(l(t,void 0),a.once&&u.dispose())}else{let t=e.peek(),n=e.subscribe(()=>{let n=e.peek();o(t,n)||(l(n,t),t=n,a?.once&&u.dispose())});u=new i.SubscriptionImpl(()=>{c?.(),c=void 0,n.dispose()}),a?.immediate&&(l(t,void 0),a.once&&u.dispose())}return r.getScopeCleanups()?.push(()=>u.dispose()),u}exports.effect=s,exports.effectAsync=c,exports.onCleanup=l,exports.scope=u,exports.watch=d;
2
+ //# sourceMappingURL=effect.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"effect.cjs","names":[],"sources":["../src/effect.ts"],"sourcesContent":["import type { ReactiveBase } from './reactive-base';\nimport type {\n AsyncEffectCallback,\n AsyncSubscription,\n CleanupFn,\n DepInfo,\n EffectAsyncOptions,\n EffectCallback,\n EffectHandle,\n EffectOptions,\n EqualityFn,\n Readable,\n Scope,\n Subscriber,\n Subscription,\n WatchOptions,\n} from './types';\n\nimport { collectErrors, rethrowWith, runAll } from './_error-utils';\nimport { getDevToolsHook } from './devtools-hook';\nimport { RippleDisposedScopeError, RippleInfiniteLoopError, RippleInvalidCleanupError } from './errors';\nimport { DEFAULT_MAX_ITERATIONS } from './scheduling';\nimport { AsyncSubscriptionImpl, SubscriptionImpl } from './subscription';\nimport { IS_COMPUTED } from './symbols';\nimport { getScopeCleanups, getTracking, withScopeCleanups, withTracking } from './tracking';\n\n/**\n * Wraps a core `run` function with a microtask-based scheduler that coalesces rapid re-runs.\n * The `subscriber` returned here is what gets registered in signal subscriber sets —\n * so notifications call the deferred wrapper, not `run` directly.\n *\n * The scheduler identity is stable for the lifetime of the effect.\n * Never replace `subscriber` with a new function after creation — doing so would break\n * the removeEffectSub calls that rely on reference identity.\n */\nconst withScheduler = (run: Subscriber, scheduler: EffectOptions['scheduler']): Subscriber => {\n if (scheduler === 'sync' || scheduler === undefined) return run;\n\n // 'microtask'\n let scheduled = false;\n\n return (): void => {\n if (scheduled) return;\n\n scheduled = true;\n queueMicrotask(() => {\n scheduled = false;\n run();\n });\n };\n};\n\n/**\n * Creates a reactive side-effect.\n * Runs immediately, re-runs whenever tracked dependencies change.\n * The callback may return a cleanup function that runs before the next re-run and on dispose.\n *\n * @example\n * ```ts\n * const stop = effect(() => {\n * document.title = count.value.toString();\n * return () => { document.title = ''; }; // optional cleanup\n * });\n *\n * stop.dispose(); // or: using stop = effect(...)\n * console.log(stop.getDependencies()); // reactive graph introspection\n * ```\n */\nexport const effect = (fn: EffectCallback, options?: EffectOptions): EffectHandle => {\n const scheduler = options?.scheduler ?? 'sync';\n const effectName = options?.name;\n\n const runCleanups: CleanupFn[] = [];\n const subscriptions = new Set<CleanupFn>();\n const currentDeps = new Map<ReactiveBase<unknown>, number>();\n let isRunning = false;\n let isDirty = false;\n let isDisposed = false;\n\n const teardown = (): void => {\n currentDeps.clear();\n\n if (runCleanups.length === 0 && subscriptions.size === 0) return;\n\n const toRun = [...runCleanups, ...subscriptions];\n\n runCleanups.length = 0;\n subscriptions.clear();\n\n runAll(toRun, 'effect teardown errors');\n };\n\n // The inner do-while handles self-cascade: when the effect body writes to a signal it reads,\n // the recursive flush sets isDirty via the isRunning guard.\n const run: Subscriber = (): void => {\n if (isDisposed) return;\n\n if (isRunning) {\n isDirty = true;\n\n return;\n }\n\n isRunning = true;\n\n try {\n let iterations = 0;\n\n do {\n if (++iterations > DEFAULT_MAX_ITERATIONS) {\n const label = effectName ? ` in \"${effectName}\"` : '';\n\n throw new RippleInfiniteLoopError(`infinite effect loop (> ${DEFAULT_MAX_ITERATIONS} iterations)${label}`);\n }\n\n isDirty = false;\n teardown();\n\n getDevToolsHook()?.run?.({ name: effectName });\n\n let returnedCleanup: CleanupFn | void = undefined;\n\n try {\n returnedCleanup = withTracking(\n { cleanups: runCleanups, deps: currentDeps, effect: subscriber, kind: 'effect', subscriptions },\n fn,\n );\n } catch (error) {\n const cleanupErrors = [...collectErrors(subscriptions), ...collectErrors(runCleanups)];\n\n runCleanups.length = 0;\n subscriptions.clear();\n rethrowWith(error, cleanupErrors, 'effect failure with cleanup errors');\n }\n\n if (returnedCleanup !== undefined && typeof returnedCleanup !== 'function') {\n throw new RippleInvalidCleanupError(\n `effect() returned ${typeof returnedCleanup} — expected a cleanup function or void.`,\n );\n }\n\n if (typeof returnedCleanup === 'function') runCleanups.push(returnedCleanup);\n } while (isDirty && !isDisposed);\n } finally {\n isRunning = false;\n }\n };\n\n // subscriber is what gets registered in signal subscriber Sets.\n // For 'sync' it is the same as run; for deferred schedulers it's the debouncing wrapper.\n const subscriber = withScheduler(run, scheduler);\n\n // Always run synchronously on creation to establish initial tracking.\n run();\n\n const sub = new SubscriptionImpl(() => {\n if (isDisposed) return;\n\n isDisposed = true;\n teardown();\n getDevToolsHook()?.dispose?.({ kind: 'effect', name: effectName });\n });\n\n // Auto-register disposal into the enclosing scope (if any).\n // Uses the separate scope stack — orthogonal to the effect/computed tracking context.\n getScopeCleanups()?.push(() => sub.dispose());\n\n const handle: EffectHandle = {\n dispose: () => sub.dispose(),\n get disposed() {\n return sub.disposed;\n },\n getDependencies(): ReadonlyArray<DepInfo> {\n return [...currentDeps.keys()].map((src) => ({\n kind: IS_COMPUTED in (src as object) ? 'computed' : 'signal',\n name: src.name,\n }));\n },\n [Symbol.dispose]() {\n sub.dispose();\n },\n };\n\n return handle;\n};\n\n/**\n * Like `effect()`, but the callback is async and receives an AbortSignal that fires when the\n * effect re-runs or is disposed. Read reactive dependencies synchronously before the first\n * `await` to register them as tracked.\n *\n * Returns an {@link AsyncSubscription} — call `await stop[Symbol.asyncDispose]()` to await\n * full teardown, or `stop.run()` to await just the current in-flight run.\n *\n * @example\n * ```ts\n * const userId = signal('u1');\n * const stop = effectAsync(async (signal) => {\n * const id = userId.value; // sync dep — tracked\n * const data = await fetchUser(id, { signal }); // aborted automatically if id changes\n * renderUser(data);\n * });\n *\n * await stop[Symbol.asyncDispose]();\n * ```\n */\nexport const effectAsync = (fn: AsyncEffectCallback, options?: EffectAsyncOptions): AsyncSubscription => {\n let controller: AbortController | null = null;\n let asyncCleanup: CleanupFn | null = null;\n let currentRunPromise: Promise<void> | null = null;\n\n const onError =\n options?.onError ??\n ((err: unknown) => {\n queueMicrotask(() => {\n throw err;\n });\n });\n\n const syncStop = effect(\n () => {\n controller?.abort();\n asyncCleanup?.();\n asyncCleanup = null;\n\n controller = new AbortController();\n\n const { signal } = controller;\n const runOwner = scope(); // per-run owner scope for async body resources\n\n let theRun!: Promise<void>;\n\n currentRunPromise = theRun = (async () => {\n try {\n const returned = await fn(signal, runOwner);\n\n if (typeof returned === 'function') {\n if (signal.aborted) {\n // Aborted while async fn was running — run cleanup immediately.\n returned();\n } else {\n asyncCleanup = returned;\n }\n }\n } catch (err) {\n if (!signal.aborted) onError(err);\n } finally {\n runOwner.dispose();\n\n if (currentRunPromise === theRun) currentRunPromise = null;\n }\n })();\n\n return () => {\n controller?.abort();\n asyncCleanup?.();\n asyncCleanup = null;\n };\n },\n { name: options?.name },\n );\n\n return new AsyncSubscriptionImpl(\n syncStop,\n async () => {\n const runningPromise = currentRunPromise;\n\n if (runningPromise) await runningPromise;\n },\n () => currentRunPromise,\n );\n};\n\nexport const onCleanup = (fn: CleanupFn): void => {\n const ctx = getTracking();\n\n if (ctx?.kind === 'effect') {\n ctx.cleanups.push(fn);\n\n return;\n }\n\n const scopeCleanups = getScopeCleanups();\n\n if (scopeCleanups !== null) {\n scopeCleanups.push(fn);\n\n return;\n }\n\n throw new RippleInvalidCleanupError('onCleanup() must be called from within an active effect or scope.');\n};\n\n/**\n * Creates a lifecycle scope that collects `onCleanup()` registrations and runs\n * them in reverse order when `dispose()` is called.\n *\n * The optional `setup` callback is run immediately inside the scope so that\n * `onCleanup()` calls in setup are captured without requiring a separate\n * `scope.run(setup)` call.\n *\n * Use `scope.add(fn)` to explicitly register a cleanup into this scope from\n * anywhere — including from inside an effect body where `onCleanup()` would otherwise\n * go to the effect's own cleanup queue.\n *\n * @example\n * ```ts\n * const s = scope(() => {\n * onCleanup(() => expensiveCleanup()); // captured by scope\n * });\n *\n * const stop = effect(() => {\n * void count.value;\n * s.add(() => doWork()); // explicit: goes to scope, not effect\n * onCleanup(() => cheapCleanup()); // goes to effect\n * });\n *\n * // later:\n * s.dispose(); // or: using s = scope(...)\n * ```\n */\nexport const scope = (setup?: () => void): Scope => {\n const cleanups: CleanupFn[] = [];\n const ac = new AbortController();\n let disposed = false;\n\n const scopeOnCleanup = (fn: CleanupFn): void => {\n if (disposed) throw new RippleDisposedScopeError('Cannot register cleanup on a disposed scope.');\n\n cleanups.push(fn);\n };\n\n const run = <T>(fn: () => T): T => {\n if (disposed) throw new RippleDisposedScopeError('Cannot run inside a disposed scope.');\n\n return withScopeCleanups(cleanups, fn);\n };\n\n const dispose = (): void => {\n if (disposed) return;\n\n disposed = true;\n ac.abort();\n runAll([...cleanups].reverse(), 'scope cleanup errors');\n cleanups.length = 0;\n };\n\n const api: Scope = {\n add: scopeOnCleanup,\n get disposalSignal() {\n return ac.signal;\n },\n dispose,\n get disposed() {\n return disposed;\n },\n run,\n [Symbol.dispose]: dispose,\n };\n\n if (setup) run(setup);\n\n return api;\n};\n\n// ── watch() ───────────────────────────────────────────────────────────────────\n\n/**\n * Observes a reactive source and calls `callback` whenever its value changes.\n * Does NOT invoke `callback` on creation (unless `immediate: true` is passed).\n *\n * `source` may be a single `Readable<T>` (signal, computed, or store lens), or a\n * function that reads multiple reactive sources — the function form tracks every\n * dependency read inside it, like `computed()`, without needing an intermediate\n * `computed()` node.\n *\n * When called without `immediate`, `prev` is always `T` — the previous value.\n * When `immediate: true` is passed, `prev` is `T | undefined` on the first call.\n *\n * @example\n * ```ts\n * const stop = watch(count, (next, prev) => {\n * console.log(`count changed: ${prev} → ${next}`);\n * });\n *\n * stop.dispose();\n * ```\n *\n * @example\n * ```ts\n * // Function form — tracks every signal read inside, re-runs when any changes.\n * const stopSum = watch(\n * () => a.value + b.value,\n * (sum, prevSum) => console.log(`sum: ${prevSum} → ${sum}`),\n * );\n *\n * stopSum.dispose();\n * ```\n */\nexport function watch<T>(\n source: Readable<T>,\n callback: (value: T, prev: T | undefined) => CleanupFn | void,\n options?: WatchOptions<T>,\n): Subscription;\nexport function watch<T>(\n source: () => T,\n callback: (value: T, prev: T | undefined) => CleanupFn | void,\n options?: WatchOptions<T>,\n): Subscription;\nexport function watch<T>(\n source: Readable<T> | (() => T),\n callback: (value: T, prev: T | undefined) => CleanupFn | void,\n options?: WatchOptions<T>,\n): Subscription {\n const equals: EqualityFn<T> = options?.equals ?? Object.is;\n let pendingCleanup: CleanupFn | undefined;\n const invokeCallback = (next: T, p: T | undefined): void => {\n pendingCleanup?.();\n pendingCleanup = undefined;\n\n const returned = callback(next, p);\n\n if (returned !== undefined && typeof returned !== 'function') {\n throw new RippleInvalidCleanupError(\n `watch() callback returned ${typeof returned} — expected a cleanup function or void.`,\n );\n }\n\n if (typeof returned === 'function') pendingCleanup = returned;\n };\n\n // Declared before the subscriber/effect closures so both they and the immediate\n // path can call watchSub.dispose() when once:true. Subscribers/effects always\n // (re-)run after watchSub is assigned, since it's assigned synchronously right after.\n\n let watchSub!: SubscriptionImpl;\n\n if (typeof source === 'function') {\n // Function form — track every dependency read inside `source` via a real effect,\n // mirroring computed()'s dependency collection. `first` suppresses the callback\n // on the initial (tracking-establishing) run — handled explicitly below (after\n // watchSub exists) instead of inside this synchronous first run, since effect()\n // runs its body immediately and watchSub isn't assigned until after it returns.\n let prev!: T;\n let first = true;\n\n const stopEffect = effect(\n () => {\n const next = source();\n\n if (first) {\n first = false;\n prev = next;\n\n return;\n }\n\n if (!equals(prev, next)) {\n const p = prev;\n\n prev = next;\n invokeCallback(next, p);\n\n if (options?.once) watchSub.dispose();\n }\n },\n { name: options?.name },\n );\n\n watchSub = new SubscriptionImpl(() => {\n pendingCleanup?.();\n pendingCleanup = undefined;\n stopEffect.dispose();\n });\n\n if (options?.immediate) {\n invokeCallback(prev, undefined);\n\n if (options.once) watchSub.dispose();\n }\n } else {\n let prev: T = source.peek();\n\n const innerSub = source.subscribe(() => {\n const next = source.peek();\n\n if (!equals(prev, next)) {\n invokeCallback(next, prev);\n prev = next;\n\n if (options?.once) watchSub.dispose();\n }\n });\n\n watchSub = new SubscriptionImpl(() => {\n pendingCleanup?.();\n pendingCleanup = undefined;\n innerSub.dispose();\n });\n\n if (options?.immediate) {\n invokeCallback(prev, undefined);\n\n if (options.once) watchSub.dispose();\n }\n }\n\n // Auto-register disposal into the enclosing scope (if any) — matches effect() behaviour.\n getScopeCleanups()?.push(() => watchSub.dispose());\n\n return watchSub;\n}\n"],"mappings":"0NAmCA,IAAM,GAAiB,EAAiB,IAAsD,CAC5F,GAAI,IAAc,QAAU,IAAc,IAAA,GAAW,OAAO,EAG5D,IAAI,EAAY,GAEhB,UAAmB,CACb,IAEJ,EAAY,GACZ,mBAAqB,CACnB,EAAY,GACZ,EAAI,CACN,CAAC,EACH,CACF,EAkBa,GAAU,EAAoB,IAA0C,CACnF,IAAM,EAAY,GAAS,WAAa,OAClC,EAAa,GAAS,KAEtB,EAA2B,CAAC,EAC5B,EAAgB,IAAI,IACpB,EAAc,IAAI,IACpB,EAAY,GACZ,EAAU,GACV,EAAa,GAEX,MAAuB,CAG3B,GAFA,EAAY,MAAM,EAEd,EAAY,SAAW,GAAK,EAAc,OAAS,EAAG,OAE1D,IAAM,EAAQ,CAAC,GAAG,EAAa,GAAG,CAAa,EAE/C,EAAY,OAAS,EACrB,EAAc,MAAM,EAEpB,EAAA,OAAO,EAAO,wBAAwB,CACxC,EAIM,MAA8B,CAC9B,MAEJ,IAAI,EAAW,CACb,EAAU,GAEV,MACF,CAEA,EAAY,GAEZ,GAAI,CACF,IAAI,EAAa,EAEjB,EAAG,CACD,GAAI,EAAE,EAAA,IAGJ,MAAM,IAAI,EAAA,wBAAwB,0CAFpB,EAAa,QAAQ,EAAW,GAAK,IAEsD,EAG3G,EAAU,GACV,EAAS,EAET,EAAA,gBAAgB,CAAC,EAAE,MAAM,CAAE,KAAM,CAAW,CAAC,EAE7C,IAAI,EAEJ,GAAI,CACF,EAAkB,EAAA,aAChB,CAAE,SAAU,EAAa,KAAM,EAAa,OAAQ,EAAY,KAAM,SAAU,eAAc,EAC9F,CACF,CACF,OAAS,EAAO,CACd,IAAM,EAAgB,CAAC,GAAG,EAAA,cAAc,CAAa,EAAG,GAAG,EAAA,cAAc,CAAW,CAAC,EAErF,EAAY,OAAS,EACrB,EAAc,MAAM,EACpB,EAAA,YAAY,EAAO,EAAe,oCAAoC,CACxE,CAEA,GAAI,IAAoB,IAAA,IAAa,OAAO,GAAoB,WAC9D,MAAM,IAAI,EAAA,0BACR,qBAAqB,OAAO,EAAgB,wCAC9C,EAGE,OAAO,GAAoB,YAAY,EAAY,KAAK,CAAe,CAC7E,OAAS,GAAW,CAAC,EACvB,QAAU,CACR,EAAY,EACd,CA5CA,CA6CF,EAIM,EAAa,EAAc,EAAK,CAAS,EAG/C,EAAI,EAEJ,IAAM,EAAM,IAAI,EAAA,qBAAuB,CACjC,IAEJ,EAAa,GACb,EAAS,EACT,EAAA,gBAAgB,CAAC,EAAE,UAAU,CAAE,KAAM,SAAU,KAAM,CAAW,CAAC,EACnE,CAAC,EAsBD,OAlBA,EAAA,iBAAiB,CAAC,EAAE,SAAW,EAAI,QAAQ,CAAC,EAkBrC,CAfL,YAAe,EAAI,QAAQ,EAC3B,IAAI,UAAW,CACb,OAAO,EAAI,QACb,EACA,iBAA0C,CACxC,MAAO,CAAC,GAAG,EAAY,KAAK,CAAC,CAAC,CAAC,IAAK,IAAS,CAC3C,KAAM,EAAA,eAAgB,EAAiB,WAAa,SACpD,KAAM,EAAI,IACZ,EAAE,CACJ,EACA,CAAC,OAAO,UAAW,CACjB,EAAI,QAAQ,CACd,CAGK,CACT,EAsBa,GAAe,EAAyB,IAAoD,CACvG,IAAI,EAAqC,KACrC,EAAiC,KACjC,EAA0C,KAExC,EACJ,GAAS,UACP,GAAiB,CACjB,mBAAqB,CACnB,MAAM,CACR,CAAC,CACH,GA6CF,OAAO,IAAI,EAAA,sBA3CM,MACT,CACJ,GAAY,MAAM,EAClB,IAAe,EACf,EAAe,KAEf,EAAa,IAAI,gBAEjB,GAAM,CAAE,UAAW,EACb,EAAW,EAAM,EAEnB,EAuBJ,MArBA,GAAoB,GAAU,SAAY,CACxC,GAAI,CACF,IAAM,EAAW,MAAM,EAAG,EAAQ,CAAQ,EAEtC,OAAO,GAAa,aAClB,EAAO,QAET,EAAS,EAET,EAAe,EAGrB,OAAS,EAAK,CACP,EAAO,SAAS,EAAQ,CAAG,CAClC,QAAU,CACR,EAAS,QAAQ,EAEb,IAAsB,IAAQ,EAAoB,KACxD,CACF,EAAA,CAAG,MAEU,CACX,GAAY,MAAM,EAClB,IAAe,EACf,EAAe,IACjB,CACF,EACA,CAAE,KAAM,GAAS,IAAK,CAItB,EACA,SAAY,CACV,IAAM,EAAiB,EAEnB,GAAgB,MAAM,CAC5B,MACM,CACR,CACF,EAEa,EAAa,GAAwB,CAChD,IAAM,EAAM,EAAA,YAAY,EAExB,GAAI,GAAK,OAAS,SAAU,CAC1B,EAAI,SAAS,KAAK,CAAE,EAEpB,MACF,CAEA,IAAM,EAAgB,EAAA,iBAAiB,EAEvC,GAAI,IAAkB,KAAM,CAC1B,EAAc,KAAK,CAAE,EAErB,MACF,CAEA,MAAM,IAAI,EAAA,0BAA0B,mEAAmE,CACzG,EA8Ba,EAAS,GAA8B,CAClD,IAAM,EAAwB,CAAC,EACzB,EAAK,IAAI,gBACX,EAAW,GAET,EAAkB,GAAwB,CAC9C,GAAI,EAAU,MAAM,IAAI,EAAA,yBAAyB,8CAA8C,EAE/F,EAAS,KAAK,CAAE,CAClB,EAEM,EAAU,GAAmB,CACjC,GAAI,EAAU,MAAM,IAAI,EAAA,yBAAyB,qCAAqC,EAEtF,OAAO,EAAA,kBAAkB,EAAU,CAAE,CACvC,EAEM,MAAsB,CACtB,IAEJ,EAAW,GACX,EAAG,MAAM,EACT,EAAA,OAAO,CAAC,GAAG,CAAQ,CAAC,CAAC,QAAQ,EAAG,sBAAsB,EACtD,EAAS,OAAS,EACpB,EAEM,EAAa,CACjB,IAAK,EACL,IAAI,gBAAiB,CACnB,OAAO,EAAG,MACZ,EACA,UACA,IAAI,UAAW,CACb,OAAO,CACT,EACA,OACC,OAAO,SAAU,CACpB,EAIA,OAFI,GAAO,EAAI,CAAK,EAEb,CACT,EA8CA,SAAgB,EACd,EACA,EACA,EACc,CACd,IAAM,EAAwB,GAAS,QAAU,OAAO,GACpD,EACE,GAAkB,EAAS,IAA2B,CAC1D,IAAiB,EACjB,EAAiB,IAAA,GAEjB,IAAM,EAAW,EAAS,EAAM,CAAC,EAEjC,GAAI,IAAa,IAAA,IAAa,OAAO,GAAa,WAChD,MAAM,IAAI,EAAA,0BACR,6BAA6B,OAAO,EAAS,wCAC/C,EAGE,OAAO,GAAa,aAAY,EAAiB,EACvD,EAMI,EAEJ,GAAI,OAAO,GAAW,WAAY,CAMhC,IAAI,EACA,EAAQ,GAEN,EAAa,MACX,CACJ,IAAM,EAAO,EAAO,EAEpB,GAAI,EAAO,CACT,EAAQ,GACR,EAAO,EAEP,MACF,CAEA,GAAI,CAAC,EAAO,EAAM,CAAI,EAAG,CACvB,IAAM,EAAI,EAEV,EAAO,EACP,EAAe,EAAM,CAAC,EAElB,GAAS,MAAM,EAAS,QAAQ,CACtC,CACF,EACA,CAAE,KAAM,GAAS,IAAK,CACxB,EAEA,EAAW,IAAI,EAAA,qBAAuB,CACpC,IAAiB,EACjB,EAAiB,IAAA,GACjB,EAAW,QAAQ,CACrB,CAAC,EAEG,GAAS,YACX,EAAe,EAAM,IAAA,EAAS,EAE1B,EAAQ,MAAM,EAAS,QAAQ,EAEvC,KAAO,CACL,IAAI,EAAU,EAAO,KAAK,EAEpB,EAAW,EAAO,cAAgB,CACtC,IAAM,EAAO,EAAO,KAAK,EAEpB,EAAO,EAAM,CAAI,IACpB,EAAe,EAAM,CAAI,EACzB,EAAO,EAEH,GAAS,MAAM,EAAS,QAAQ,EAExC,CAAC,EAED,EAAW,IAAI,EAAA,qBAAuB,CACpC,IAAiB,EACjB,EAAiB,IAAA,GACjB,EAAS,QAAQ,CACnB,CAAC,EAEG,GAAS,YACX,EAAe,EAAM,IAAA,EAAS,EAE1B,EAAQ,MAAM,EAAS,QAAQ,EAEvC,CAKA,OAFA,EAAA,iBAAiB,CAAC,EAAE,SAAW,EAAS,QAAQ,CAAC,EAE1C,CACT"}
@@ -0,0 +1,104 @@
1
+ import type { AsyncEffectCallback, AsyncSubscription, CleanupFn, EffectAsyncOptions, EffectCallback, EffectHandle, EffectOptions, Readable, Scope, Subscription, WatchOptions } from './types';
2
+ /**
3
+ * Creates a reactive side-effect.
4
+ * Runs immediately, re-runs whenever tracked dependencies change.
5
+ * The callback may return a cleanup function that runs before the next re-run and on dispose.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const stop = effect(() => {
10
+ * document.title = count.value.toString();
11
+ * return () => { document.title = ''; }; // optional cleanup
12
+ * });
13
+ *
14
+ * stop.dispose(); // or: using stop = effect(...)
15
+ * console.log(stop.getDependencies()); // reactive graph introspection
16
+ * ```
17
+ */
18
+ export declare const effect: (fn: EffectCallback, options?: EffectOptions) => EffectHandle;
19
+ /**
20
+ * Like `effect()`, but the callback is async and receives an AbortSignal that fires when the
21
+ * effect re-runs or is disposed. Read reactive dependencies synchronously before the first
22
+ * `await` to register them as tracked.
23
+ *
24
+ * Returns an {@link AsyncSubscription} — call `await stop[Symbol.asyncDispose]()` to await
25
+ * full teardown, or `stop.run()` to await just the current in-flight run.
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * const userId = signal('u1');
30
+ * const stop = effectAsync(async (signal) => {
31
+ * const id = userId.value; // sync dep — tracked
32
+ * const data = await fetchUser(id, { signal }); // aborted automatically if id changes
33
+ * renderUser(data);
34
+ * });
35
+ *
36
+ * await stop[Symbol.asyncDispose]();
37
+ * ```
38
+ */
39
+ export declare const effectAsync: (fn: AsyncEffectCallback, options?: EffectAsyncOptions) => AsyncSubscription;
40
+ export declare const onCleanup: (fn: CleanupFn) => void;
41
+ /**
42
+ * Creates a lifecycle scope that collects `onCleanup()` registrations and runs
43
+ * them in reverse order when `dispose()` is called.
44
+ *
45
+ * The optional `setup` callback is run immediately inside the scope so that
46
+ * `onCleanup()` calls in setup are captured without requiring a separate
47
+ * `scope.run(setup)` call.
48
+ *
49
+ * Use `scope.add(fn)` to explicitly register a cleanup into this scope from
50
+ * anywhere — including from inside an effect body where `onCleanup()` would otherwise
51
+ * go to the effect's own cleanup queue.
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * const s = scope(() => {
56
+ * onCleanup(() => expensiveCleanup()); // captured by scope
57
+ * });
58
+ *
59
+ * const stop = effect(() => {
60
+ * void count.value;
61
+ * s.add(() => doWork()); // explicit: goes to scope, not effect
62
+ * onCleanup(() => cheapCleanup()); // goes to effect
63
+ * });
64
+ *
65
+ * // later:
66
+ * s.dispose(); // or: using s = scope(...)
67
+ * ```
68
+ */
69
+ export declare const scope: (setup?: () => void) => Scope;
70
+ /**
71
+ * Observes a reactive source and calls `callback` whenever its value changes.
72
+ * Does NOT invoke `callback` on creation (unless `immediate: true` is passed).
73
+ *
74
+ * `source` may be a single `Readable<T>` (signal, computed, or store lens), or a
75
+ * function that reads multiple reactive sources — the function form tracks every
76
+ * dependency read inside it, like `computed()`, without needing an intermediate
77
+ * `computed()` node.
78
+ *
79
+ * When called without `immediate`, `prev` is always `T` — the previous value.
80
+ * When `immediate: true` is passed, `prev` is `T | undefined` on the first call.
81
+ *
82
+ * @example
83
+ * ```ts
84
+ * const stop = watch(count, (next, prev) => {
85
+ * console.log(`count changed: ${prev} → ${next}`);
86
+ * });
87
+ *
88
+ * stop.dispose();
89
+ * ```
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * // Function form — tracks every signal read inside, re-runs when any changes.
94
+ * const stopSum = watch(
95
+ * () => a.value + b.value,
96
+ * (sum, prevSum) => console.log(`sum: ${prevSum} → ${sum}`),
97
+ * );
98
+ *
99
+ * stopSum.dispose();
100
+ * ```
101
+ */
102
+ export declare function watch<T>(source: Readable<T>, callback: (value: T, prev: T | undefined) => CleanupFn | void, options?: WatchOptions<T>): Subscription;
103
+ export declare function watch<T>(source: () => T, callback: (value: T, prev: T | undefined) => CleanupFn | void, options?: WatchOptions<T>): Subscription;
104
+ //# sourceMappingURL=effect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"effect.d.ts","sourceRoot":"","sources":["../src/effect.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,mBAAmB,EACnB,iBAAiB,EACjB,SAAS,EAET,kBAAkB,EAClB,cAAc,EACd,YAAY,EACZ,aAAa,EAEb,QAAQ,EACR,KAAK,EAEL,YAAY,EACZ,YAAY,EACb,MAAM,SAAS,CAAC;AAoCjB;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,MAAM,GAAI,IAAI,cAAc,EAAE,UAAU,aAAa,KAAG,YAoHpE,CAAC;AAEF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,WAAW,GAAI,IAAI,mBAAmB,EAAE,UAAU,kBAAkB,KAAG,iBAiEnF,CAAC;AAEF,eAAO,MAAM,SAAS,GAAI,IAAI,SAAS,KAAG,IAkBzC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,eAAO,MAAM,KAAK,GAAI,QAAQ,MAAM,IAAI,KAAG,KA0C1C,CAAC;AAIF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,KAAK,CAAC,CAAC,EACrB,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,EACnB,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,GAAG,SAAS,KAAK,SAAS,GAAG,IAAI,EAC7D,OAAO,CAAC,EAAE,YAAY,CAAC,CAAC,CAAC,GACxB,YAAY,CAAC;AAChB,wBAAgB,KAAK,CAAC,CAAC,EACrB,MAAM,EAAE,MAAM,CAAC,EACf,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,GAAG,SAAS,KAAK,SAAS,GAAG,IAAI,EAC7D,OAAO,CAAC,EAAE,YAAY,CAAC,CAAC,CAAC,GACxB,YAAY,CAAC"}
package/dist/effect.js ADDED
@@ -0,0 +1,173 @@
1
+ import { getDevToolsHook as e } from "./devtools-hook.js";
2
+ import { collectErrors as t, rethrowWith as n, runAll as r } from "./_error-utils.js";
3
+ import { RippleDisposedScopeError as i, RippleInfiniteLoopError as a, RippleInvalidCleanupError as o } from "./errors.js";
4
+ import { getScopeCleanups as s, getTracking as c, withScopeCleanups as l, withTracking as u } from "./tracking.js";
5
+ import "./scheduling.js";
6
+ import { AsyncSubscriptionImpl as d, SubscriptionImpl as f } from "./subscription.js";
7
+ import { IS_COMPUTED as p } from "./symbols.js";
8
+ //#region src/effect.ts
9
+ var m = (e, t) => {
10
+ if (t === "sync" || t === void 0) return e;
11
+ let n = !1;
12
+ return () => {
13
+ n || (n = !0, queueMicrotask(() => {
14
+ n = !1, e();
15
+ }));
16
+ };
17
+ }, h = (i, c) => {
18
+ let l = c?.scheduler ?? "sync", d = c?.name, h = [], g = /* @__PURE__ */ new Set(), _ = /* @__PURE__ */ new Map(), v = !1, y = !1, b = !1, x = () => {
19
+ if (_.clear(), h.length === 0 && g.size === 0) return;
20
+ let e = [...h, ...g];
21
+ h.length = 0, g.clear(), r(e, "effect teardown errors");
22
+ }, S = () => {
23
+ if (!b) {
24
+ if (v) {
25
+ y = !0;
26
+ return;
27
+ }
28
+ v = !0;
29
+ try {
30
+ let r = 0;
31
+ do {
32
+ if (++r > 100) throw new a(`infinite effect loop (> 100 iterations)${d ? ` in "${d}"` : ""}`);
33
+ y = !1, x(), e()?.run?.({ name: d });
34
+ let s;
35
+ try {
36
+ s = u({
37
+ cleanups: h,
38
+ deps: _,
39
+ effect: C,
40
+ kind: "effect",
41
+ subscriptions: g
42
+ }, i);
43
+ } catch (e) {
44
+ let r = [...t(g), ...t(h)];
45
+ h.length = 0, g.clear(), n(e, r, "effect failure with cleanup errors");
46
+ }
47
+ if (s !== void 0 && typeof s != "function") throw new o(`effect() returned ${typeof s} — expected a cleanup function or void.`);
48
+ typeof s == "function" && h.push(s);
49
+ } while (y && !b);
50
+ } finally {
51
+ v = !1;
52
+ }
53
+ }
54
+ }, C = m(S, l);
55
+ S();
56
+ let w = new f(() => {
57
+ b || (b = !0, x(), e()?.dispose?.({
58
+ kind: "effect",
59
+ name: d
60
+ }));
61
+ });
62
+ return s()?.push(() => w.dispose()), {
63
+ dispose: () => w.dispose(),
64
+ get disposed() {
65
+ return w.disposed;
66
+ },
67
+ getDependencies() {
68
+ return [..._.keys()].map((e) => ({
69
+ kind: p in e ? "computed" : "signal",
70
+ name: e.name
71
+ }));
72
+ },
73
+ [Symbol.dispose]() {
74
+ w.dispose();
75
+ }
76
+ };
77
+ }, g = (e, t) => {
78
+ let n = null, r = null, i = null, a = t?.onError ?? ((e) => {
79
+ queueMicrotask(() => {
80
+ throw e;
81
+ });
82
+ });
83
+ return new d(h(() => {
84
+ n?.abort(), r?.(), r = null, n = new AbortController();
85
+ let { signal: t } = n, o = v(), s;
86
+ return i = s = (async () => {
87
+ try {
88
+ let n = await e(t, o);
89
+ typeof n == "function" && (t.aborted ? n() : r = n);
90
+ } catch (e) {
91
+ t.aborted || a(e);
92
+ } finally {
93
+ o.dispose(), i === s && (i = null);
94
+ }
95
+ })(), () => {
96
+ n?.abort(), r?.(), r = null;
97
+ };
98
+ }, { name: t?.name }), async () => {
99
+ let e = i;
100
+ e && await e;
101
+ }, () => i);
102
+ }, _ = (e) => {
103
+ let t = c();
104
+ if (t?.kind === "effect") {
105
+ t.cleanups.push(e);
106
+ return;
107
+ }
108
+ let n = s();
109
+ if (n !== null) {
110
+ n.push(e);
111
+ return;
112
+ }
113
+ throw new o("onCleanup() must be called from within an active effect or scope.");
114
+ }, v = (e) => {
115
+ let t = [], n = new AbortController(), a = !1, o = (e) => {
116
+ if (a) throw new i("Cannot register cleanup on a disposed scope.");
117
+ t.push(e);
118
+ }, s = (e) => {
119
+ if (a) throw new i("Cannot run inside a disposed scope.");
120
+ return l(t, e);
121
+ }, c = () => {
122
+ a || (a = !0, n.abort(), r([...t].reverse(), "scope cleanup errors"), t.length = 0);
123
+ }, u = {
124
+ add: o,
125
+ get disposalSignal() {
126
+ return n.signal;
127
+ },
128
+ dispose: c,
129
+ get disposed() {
130
+ return a;
131
+ },
132
+ run: s,
133
+ [Symbol.dispose]: c
134
+ };
135
+ return e && s(e), u;
136
+ };
137
+ function y(e, t, n) {
138
+ let r = n?.equals ?? Object.is, i, a = (e, n) => {
139
+ i?.(), i = void 0;
140
+ let r = t(e, n);
141
+ if (r !== void 0 && typeof r != "function") throw new o(`watch() callback returned ${typeof r} — expected a cleanup function or void.`);
142
+ typeof r == "function" && (i = r);
143
+ }, c;
144
+ if (typeof e == "function") {
145
+ let t, o = !0, s = h(() => {
146
+ let i = e();
147
+ if (o) {
148
+ o = !1, t = i;
149
+ return;
150
+ }
151
+ if (!r(t, i)) {
152
+ let e = t;
153
+ t = i, a(i, e), n?.once && c.dispose();
154
+ }
155
+ }, { name: n?.name });
156
+ c = new f(() => {
157
+ i?.(), i = void 0, s.dispose();
158
+ }), n?.immediate && (a(t, void 0), n.once && c.dispose());
159
+ } else {
160
+ let t = e.peek(), o = e.subscribe(() => {
161
+ let i = e.peek();
162
+ r(t, i) || (a(i, t), t = i, n?.once && c.dispose());
163
+ });
164
+ c = new f(() => {
165
+ i?.(), i = void 0, o.dispose();
166
+ }), n?.immediate && (a(t, void 0), n.once && c.dispose());
167
+ }
168
+ return s()?.push(() => c.dispose()), c;
169
+ }
170
+ //#endregion
171
+ export { h as effect, g as effectAsync, _ as onCleanup, v as scope, y as watch };
172
+
173
+ //# sourceMappingURL=effect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"effect.js","names":[],"sources":["../src/effect.ts"],"sourcesContent":["import type { ReactiveBase } from './reactive-base';\nimport type {\n AsyncEffectCallback,\n AsyncSubscription,\n CleanupFn,\n DepInfo,\n EffectAsyncOptions,\n EffectCallback,\n EffectHandle,\n EffectOptions,\n EqualityFn,\n Readable,\n Scope,\n Subscriber,\n Subscription,\n WatchOptions,\n} from './types';\n\nimport { collectErrors, rethrowWith, runAll } from './_error-utils';\nimport { getDevToolsHook } from './devtools-hook';\nimport { RippleDisposedScopeError, RippleInfiniteLoopError, RippleInvalidCleanupError } from './errors';\nimport { DEFAULT_MAX_ITERATIONS } from './scheduling';\nimport { AsyncSubscriptionImpl, SubscriptionImpl } from './subscription';\nimport { IS_COMPUTED } from './symbols';\nimport { getScopeCleanups, getTracking, withScopeCleanups, withTracking } from './tracking';\n\n/**\n * Wraps a core `run` function with a microtask-based scheduler that coalesces rapid re-runs.\n * The `subscriber` returned here is what gets registered in signal subscriber sets —\n * so notifications call the deferred wrapper, not `run` directly.\n *\n * The scheduler identity is stable for the lifetime of the effect.\n * Never replace `subscriber` with a new function after creation — doing so would break\n * the removeEffectSub calls that rely on reference identity.\n */\nconst withScheduler = (run: Subscriber, scheduler: EffectOptions['scheduler']): Subscriber => {\n if (scheduler === 'sync' || scheduler === undefined) return run;\n\n // 'microtask'\n let scheduled = false;\n\n return (): void => {\n if (scheduled) return;\n\n scheduled = true;\n queueMicrotask(() => {\n scheduled = false;\n run();\n });\n };\n};\n\n/**\n * Creates a reactive side-effect.\n * Runs immediately, re-runs whenever tracked dependencies change.\n * The callback may return a cleanup function that runs before the next re-run and on dispose.\n *\n * @example\n * ```ts\n * const stop = effect(() => {\n * document.title = count.value.toString();\n * return () => { document.title = ''; }; // optional cleanup\n * });\n *\n * stop.dispose(); // or: using stop = effect(...)\n * console.log(stop.getDependencies()); // reactive graph introspection\n * ```\n */\nexport const effect = (fn: EffectCallback, options?: EffectOptions): EffectHandle => {\n const scheduler = options?.scheduler ?? 'sync';\n const effectName = options?.name;\n\n const runCleanups: CleanupFn[] = [];\n const subscriptions = new Set<CleanupFn>();\n const currentDeps = new Map<ReactiveBase<unknown>, number>();\n let isRunning = false;\n let isDirty = false;\n let isDisposed = false;\n\n const teardown = (): void => {\n currentDeps.clear();\n\n if (runCleanups.length === 0 && subscriptions.size === 0) return;\n\n const toRun = [...runCleanups, ...subscriptions];\n\n runCleanups.length = 0;\n subscriptions.clear();\n\n runAll(toRun, 'effect teardown errors');\n };\n\n // The inner do-while handles self-cascade: when the effect body writes to a signal it reads,\n // the recursive flush sets isDirty via the isRunning guard.\n const run: Subscriber = (): void => {\n if (isDisposed) return;\n\n if (isRunning) {\n isDirty = true;\n\n return;\n }\n\n isRunning = true;\n\n try {\n let iterations = 0;\n\n do {\n if (++iterations > DEFAULT_MAX_ITERATIONS) {\n const label = effectName ? ` in \"${effectName}\"` : '';\n\n throw new RippleInfiniteLoopError(`infinite effect loop (> ${DEFAULT_MAX_ITERATIONS} iterations)${label}`);\n }\n\n isDirty = false;\n teardown();\n\n getDevToolsHook()?.run?.({ name: effectName });\n\n let returnedCleanup: CleanupFn | void = undefined;\n\n try {\n returnedCleanup = withTracking(\n { cleanups: runCleanups, deps: currentDeps, effect: subscriber, kind: 'effect', subscriptions },\n fn,\n );\n } catch (error) {\n const cleanupErrors = [...collectErrors(subscriptions), ...collectErrors(runCleanups)];\n\n runCleanups.length = 0;\n subscriptions.clear();\n rethrowWith(error, cleanupErrors, 'effect failure with cleanup errors');\n }\n\n if (returnedCleanup !== undefined && typeof returnedCleanup !== 'function') {\n throw new RippleInvalidCleanupError(\n `effect() returned ${typeof returnedCleanup} — expected a cleanup function or void.`,\n );\n }\n\n if (typeof returnedCleanup === 'function') runCleanups.push(returnedCleanup);\n } while (isDirty && !isDisposed);\n } finally {\n isRunning = false;\n }\n };\n\n // subscriber is what gets registered in signal subscriber Sets.\n // For 'sync' it is the same as run; for deferred schedulers it's the debouncing wrapper.\n const subscriber = withScheduler(run, scheduler);\n\n // Always run synchronously on creation to establish initial tracking.\n run();\n\n const sub = new SubscriptionImpl(() => {\n if (isDisposed) return;\n\n isDisposed = true;\n teardown();\n getDevToolsHook()?.dispose?.({ kind: 'effect', name: effectName });\n });\n\n // Auto-register disposal into the enclosing scope (if any).\n // Uses the separate scope stack — orthogonal to the effect/computed tracking context.\n getScopeCleanups()?.push(() => sub.dispose());\n\n const handle: EffectHandle = {\n dispose: () => sub.dispose(),\n get disposed() {\n return sub.disposed;\n },\n getDependencies(): ReadonlyArray<DepInfo> {\n return [...currentDeps.keys()].map((src) => ({\n kind: IS_COMPUTED in (src as object) ? 'computed' : 'signal',\n name: src.name,\n }));\n },\n [Symbol.dispose]() {\n sub.dispose();\n },\n };\n\n return handle;\n};\n\n/**\n * Like `effect()`, but the callback is async and receives an AbortSignal that fires when the\n * effect re-runs or is disposed. Read reactive dependencies synchronously before the first\n * `await` to register them as tracked.\n *\n * Returns an {@link AsyncSubscription} — call `await stop[Symbol.asyncDispose]()` to await\n * full teardown, or `stop.run()` to await just the current in-flight run.\n *\n * @example\n * ```ts\n * const userId = signal('u1');\n * const stop = effectAsync(async (signal) => {\n * const id = userId.value; // sync dep — tracked\n * const data = await fetchUser(id, { signal }); // aborted automatically if id changes\n * renderUser(data);\n * });\n *\n * await stop[Symbol.asyncDispose]();\n * ```\n */\nexport const effectAsync = (fn: AsyncEffectCallback, options?: EffectAsyncOptions): AsyncSubscription => {\n let controller: AbortController | null = null;\n let asyncCleanup: CleanupFn | null = null;\n let currentRunPromise: Promise<void> | null = null;\n\n const onError =\n options?.onError ??\n ((err: unknown) => {\n queueMicrotask(() => {\n throw err;\n });\n });\n\n const syncStop = effect(\n () => {\n controller?.abort();\n asyncCleanup?.();\n asyncCleanup = null;\n\n controller = new AbortController();\n\n const { signal } = controller;\n const runOwner = scope(); // per-run owner scope for async body resources\n\n let theRun!: Promise<void>;\n\n currentRunPromise = theRun = (async () => {\n try {\n const returned = await fn(signal, runOwner);\n\n if (typeof returned === 'function') {\n if (signal.aborted) {\n // Aborted while async fn was running — run cleanup immediately.\n returned();\n } else {\n asyncCleanup = returned;\n }\n }\n } catch (err) {\n if (!signal.aborted) onError(err);\n } finally {\n runOwner.dispose();\n\n if (currentRunPromise === theRun) currentRunPromise = null;\n }\n })();\n\n return () => {\n controller?.abort();\n asyncCleanup?.();\n asyncCleanup = null;\n };\n },\n { name: options?.name },\n );\n\n return new AsyncSubscriptionImpl(\n syncStop,\n async () => {\n const runningPromise = currentRunPromise;\n\n if (runningPromise) await runningPromise;\n },\n () => currentRunPromise,\n );\n};\n\nexport const onCleanup = (fn: CleanupFn): void => {\n const ctx = getTracking();\n\n if (ctx?.kind === 'effect') {\n ctx.cleanups.push(fn);\n\n return;\n }\n\n const scopeCleanups = getScopeCleanups();\n\n if (scopeCleanups !== null) {\n scopeCleanups.push(fn);\n\n return;\n }\n\n throw new RippleInvalidCleanupError('onCleanup() must be called from within an active effect or scope.');\n};\n\n/**\n * Creates a lifecycle scope that collects `onCleanup()` registrations and runs\n * them in reverse order when `dispose()` is called.\n *\n * The optional `setup` callback is run immediately inside the scope so that\n * `onCleanup()` calls in setup are captured without requiring a separate\n * `scope.run(setup)` call.\n *\n * Use `scope.add(fn)` to explicitly register a cleanup into this scope from\n * anywhere — including from inside an effect body where `onCleanup()` would otherwise\n * go to the effect's own cleanup queue.\n *\n * @example\n * ```ts\n * const s = scope(() => {\n * onCleanup(() => expensiveCleanup()); // captured by scope\n * });\n *\n * const stop = effect(() => {\n * void count.value;\n * s.add(() => doWork()); // explicit: goes to scope, not effect\n * onCleanup(() => cheapCleanup()); // goes to effect\n * });\n *\n * // later:\n * s.dispose(); // or: using s = scope(...)\n * ```\n */\nexport const scope = (setup?: () => void): Scope => {\n const cleanups: CleanupFn[] = [];\n const ac = new AbortController();\n let disposed = false;\n\n const scopeOnCleanup = (fn: CleanupFn): void => {\n if (disposed) throw new RippleDisposedScopeError('Cannot register cleanup on a disposed scope.');\n\n cleanups.push(fn);\n };\n\n const run = <T>(fn: () => T): T => {\n if (disposed) throw new RippleDisposedScopeError('Cannot run inside a disposed scope.');\n\n return withScopeCleanups(cleanups, fn);\n };\n\n const dispose = (): void => {\n if (disposed) return;\n\n disposed = true;\n ac.abort();\n runAll([...cleanups].reverse(), 'scope cleanup errors');\n cleanups.length = 0;\n };\n\n const api: Scope = {\n add: scopeOnCleanup,\n get disposalSignal() {\n return ac.signal;\n },\n dispose,\n get disposed() {\n return disposed;\n },\n run,\n [Symbol.dispose]: dispose,\n };\n\n if (setup) run(setup);\n\n return api;\n};\n\n// ── watch() ───────────────────────────────────────────────────────────────────\n\n/**\n * Observes a reactive source and calls `callback` whenever its value changes.\n * Does NOT invoke `callback` on creation (unless `immediate: true` is passed).\n *\n * `source` may be a single `Readable<T>` (signal, computed, or store lens), or a\n * function that reads multiple reactive sources — the function form tracks every\n * dependency read inside it, like `computed()`, without needing an intermediate\n * `computed()` node.\n *\n * When called without `immediate`, `prev` is always `T` — the previous value.\n * When `immediate: true` is passed, `prev` is `T | undefined` on the first call.\n *\n * @example\n * ```ts\n * const stop = watch(count, (next, prev) => {\n * console.log(`count changed: ${prev} → ${next}`);\n * });\n *\n * stop.dispose();\n * ```\n *\n * @example\n * ```ts\n * // Function form — tracks every signal read inside, re-runs when any changes.\n * const stopSum = watch(\n * () => a.value + b.value,\n * (sum, prevSum) => console.log(`sum: ${prevSum} → ${sum}`),\n * );\n *\n * stopSum.dispose();\n * ```\n */\nexport function watch<T>(\n source: Readable<T>,\n callback: (value: T, prev: T | undefined) => CleanupFn | void,\n options?: WatchOptions<T>,\n): Subscription;\nexport function watch<T>(\n source: () => T,\n callback: (value: T, prev: T | undefined) => CleanupFn | void,\n options?: WatchOptions<T>,\n): Subscription;\nexport function watch<T>(\n source: Readable<T> | (() => T),\n callback: (value: T, prev: T | undefined) => CleanupFn | void,\n options?: WatchOptions<T>,\n): Subscription {\n const equals: EqualityFn<T> = options?.equals ?? Object.is;\n let pendingCleanup: CleanupFn | undefined;\n const invokeCallback = (next: T, p: T | undefined): void => {\n pendingCleanup?.();\n pendingCleanup = undefined;\n\n const returned = callback(next, p);\n\n if (returned !== undefined && typeof returned !== 'function') {\n throw new RippleInvalidCleanupError(\n `watch() callback returned ${typeof returned} — expected a cleanup function or void.`,\n );\n }\n\n if (typeof returned === 'function') pendingCleanup = returned;\n };\n\n // Declared before the subscriber/effect closures so both they and the immediate\n // path can call watchSub.dispose() when once:true. Subscribers/effects always\n // (re-)run after watchSub is assigned, since it's assigned synchronously right after.\n\n let watchSub!: SubscriptionImpl;\n\n if (typeof source === 'function') {\n // Function form — track every dependency read inside `source` via a real effect,\n // mirroring computed()'s dependency collection. `first` suppresses the callback\n // on the initial (tracking-establishing) run — handled explicitly below (after\n // watchSub exists) instead of inside this synchronous first run, since effect()\n // runs its body immediately and watchSub isn't assigned until after it returns.\n let prev!: T;\n let first = true;\n\n const stopEffect = effect(\n () => {\n const next = source();\n\n if (first) {\n first = false;\n prev = next;\n\n return;\n }\n\n if (!equals(prev, next)) {\n const p = prev;\n\n prev = next;\n invokeCallback(next, p);\n\n if (options?.once) watchSub.dispose();\n }\n },\n { name: options?.name },\n );\n\n watchSub = new SubscriptionImpl(() => {\n pendingCleanup?.();\n pendingCleanup = undefined;\n stopEffect.dispose();\n });\n\n if (options?.immediate) {\n invokeCallback(prev, undefined);\n\n if (options.once) watchSub.dispose();\n }\n } else {\n let prev: T = source.peek();\n\n const innerSub = source.subscribe(() => {\n const next = source.peek();\n\n if (!equals(prev, next)) {\n invokeCallback(next, prev);\n prev = next;\n\n if (options?.once) watchSub.dispose();\n }\n });\n\n watchSub = new SubscriptionImpl(() => {\n pendingCleanup?.();\n pendingCleanup = undefined;\n innerSub.dispose();\n });\n\n if (options?.immediate) {\n invokeCallback(prev, undefined);\n\n if (options.once) watchSub.dispose();\n }\n }\n\n // Auto-register disposal into the enclosing scope (if any) — matches effect() behaviour.\n getScopeCleanups()?.push(() => watchSub.dispose());\n\n return watchSub;\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ var e=class e extends Error{constructor(e,t){super(e,t),this.name=new.target.name,Object.setPrototypeOf(this,new.target.prototype)}static is(t){return t instanceof e}},t=class extends e{},n=class extends e{},r=class extends e{},i=class extends e{},a=class extends e{},o=class extends e{};exports.RippleComputedCycleError=t,exports.RippleDisposedScopeError=n,exports.RippleEnvironmentError=r,exports.RippleError=e,exports.RippleInfiniteLoopError=i,exports.RippleInvalidCleanupError=a,exports.RippleInvalidStoreError=o;
2
+ //# sourceMappingURL=errors.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.cjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["// ── RippleError ─────────────────────────────────────────────────────────────\n\n/** Base class for all ripple errors. Use `instanceof RippleError` to catch any ripple-originated error. */\nexport class RippleError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n\n static is(err: unknown): err is RippleError {\n return err instanceof RippleError;\n }\n}\n\n/** Thrown when a computed value reads itself, creating a circular dependency. */\nexport class RippleComputedCycleError extends RippleError {}\n\n/** Thrown when a method is called on a scope that has already been disposed. */\nexport class RippleDisposedScopeError extends RippleError {}\n\n/** Thrown when the current runtime environment does not support the requested feature (e.g. SSR APIs in the browser). */\nexport class RippleEnvironmentError extends RippleError {}\n\n/** Thrown when an effect or flush loop exceeds the maximum iteration count. */\nexport class RippleInfiniteLoopError extends RippleError {}\n\n/** Thrown when `onCleanup()` is called outside an active effect or scope. */\nexport class RippleInvalidCleanupError extends RippleError {}\n\n/** Thrown when a store is created or mutated with an invalid value or path. */\nexport class RippleInvalidStoreError extends RippleError {}\n"],"mappings":"AAGA,IAAa,EAAb,MAAa,UAAoB,KAAM,CACrC,YAAY,EAAiB,EAAqB,CAChD,MAAM,EAAS,CAAI,EACnB,KAAK,KAAO,IAAI,OAAO,KACvB,OAAO,eAAe,KAAM,IAAI,OAAO,SAAS,CAClD,CAEA,OAAO,GAAG,EAAkC,CAC1C,OAAO,aAAe,CACxB,CACF,EAGa,EAAb,cAA8C,CAAY,CAAC,EAG9C,EAAb,cAA8C,CAAY,CAAC,EAG9C,EAAb,cAA4C,CAAY,CAAC,EAG5C,EAAb,cAA6C,CAAY,CAAC,EAG7C,EAAb,cAA+C,CAAY,CAAC,EAG/C,EAAb,cAA6C,CAAY,CAAC"}
@@ -0,0 +1,24 @@
1
+ /** Base class for all ripple errors. Use `instanceof RippleError` to catch any ripple-originated error. */
2
+ export declare class RippleError extends Error {
3
+ constructor(message: string, opts?: ErrorOptions);
4
+ static is(err: unknown): err is RippleError;
5
+ }
6
+ /** Thrown when a computed value reads itself, creating a circular dependency. */
7
+ export declare class RippleComputedCycleError extends RippleError {
8
+ }
9
+ /** Thrown when a method is called on a scope that has already been disposed. */
10
+ export declare class RippleDisposedScopeError extends RippleError {
11
+ }
12
+ /** Thrown when the current runtime environment does not support the requested feature (e.g. SSR APIs in the browser). */
13
+ export declare class RippleEnvironmentError extends RippleError {
14
+ }
15
+ /** Thrown when an effect or flush loop exceeds the maximum iteration count. */
16
+ export declare class RippleInfiniteLoopError extends RippleError {
17
+ }
18
+ /** Thrown when `onCleanup()` is called outside an active effect or scope. */
19
+ export declare class RippleInvalidCleanupError extends RippleError {
20
+ }
21
+ /** Thrown when a store is created or mutated with an invalid value or path. */
22
+ export declare class RippleInvalidStoreError extends RippleError {
23
+ }
24
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAEA,2GAA2G;AAC3G,qBAAa,WAAY,SAAQ,KAAK;gBACxB,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,YAAY;IAMhD,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,WAAW;CAG5C;AAED,iFAAiF;AACjF,qBAAa,wBAAyB,SAAQ,WAAW;CAAG;AAE5D,gFAAgF;AAChF,qBAAa,wBAAyB,SAAQ,WAAW;CAAG;AAE5D,yHAAyH;AACzH,qBAAa,sBAAuB,SAAQ,WAAW;CAAG;AAE1D,+EAA+E;AAC/E,qBAAa,uBAAwB,SAAQ,WAAW;CAAG;AAE3D,6EAA6E;AAC7E,qBAAa,yBAA0B,SAAQ,WAAW;CAAG;AAE7D,+EAA+E;AAC/E,qBAAa,uBAAwB,SAAQ,WAAW;CAAG"}
package/dist/errors.js ADDED
@@ -0,0 +1,13 @@
1
+ //#region src/errors.ts
2
+ var e = class e extends Error {
3
+ constructor(e, t) {
4
+ super(e, t), this.name = new.target.name, Object.setPrototypeOf(this, new.target.prototype);
5
+ }
6
+ static is(t) {
7
+ return t instanceof e;
8
+ }
9
+ }, t = class extends e {}, n = class extends e {}, r = class extends e {}, i = class extends e {}, a = class extends e {}, o = class extends e {};
10
+ //#endregion
11
+ export { t as RippleComputedCycleError, n as RippleDisposedScopeError, r as RippleEnvironmentError, e as RippleError, i as RippleInfiniteLoopError, a as RippleInvalidCleanupError, o as RippleInvalidStoreError };
12
+
13
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","names":[],"sources":["../src/errors.ts"],"sourcesContent":["// ── RippleError ─────────────────────────────────────────────────────────────\n\n/** Base class for all ripple errors. Use `instanceof RippleError` to catch any ripple-originated error. */\nexport class RippleError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n\n static is(err: unknown): err is RippleError {\n return err instanceof RippleError;\n }\n}\n\n/** Thrown when a computed value reads itself, creating a circular dependency. */\nexport class RippleComputedCycleError extends RippleError {}\n\n/** Thrown when a method is called on a scope that has already been disposed. */\nexport class RippleDisposedScopeError extends RippleError {}\n\n/** Thrown when the current runtime environment does not support the requested feature (e.g. SSR APIs in the browser). */\nexport class RippleEnvironmentError extends RippleError {}\n\n/** Thrown when an effect or flush loop exceeds the maximum iteration count. */\nexport class RippleInfiniteLoopError extends RippleError {}\n\n/** Thrown when `onCleanup()` is called outside an active effect or scope. */\nexport class RippleInvalidCleanupError extends RippleError {}\n\n/** Thrown when a store is created or mutated with an invalid value or path. */\nexport class RippleInvalidStoreError extends RippleError {}\n"],"mappings":";AAGA,IAAa,IAAb,MAAa,UAAoB,MAAM;CACrC,YAAY,GAAiB,GAAqB;EAGhD,AAFA,MAAM,GAAS,CAAI,GACnB,KAAK,OAAO,IAAI,OAAO,MACvB,OAAO,eAAe,MAAM,IAAI,OAAO,SAAS;CAClD;CAEA,OAAO,GAAG,GAAkC;EAC1C,OAAO,aAAe;CACxB;AACF,GAGa,IAAb,cAA8C,EAAY,CAAC,GAG9C,IAAb,cAA8C,EAAY,CAAC,GAG9C,IAAb,cAA4C,EAAY,CAAC,GAG5C,IAAb,cAA6C,EAAY,CAAC,GAG7C,IAAb,cAA+C,EAAY,CAAC,GAG/C,IAAb,cAA6C,EAAY,CAAC"}
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./devtools-hook.cjs"),t=require("./errors.cjs"),n=require("./tracking.cjs"),r=require("./scheduling.cjs"),i=require("./effect.cjs"),a=require("./computed.cjs"),o=require("./signal.cjs"),s=require("./store.cjs"),c=require("./utilities.cjs"),l=require("./async-computed.cjs"),u=require("./store-history.cjs");exports.RippleComputedCycleError=t.RippleComputedCycleError,exports.RippleDisposedScopeError=t.RippleDisposedScopeError,exports.RippleEnvironmentError=t.RippleEnvironmentError,exports.RippleError=t.RippleError,exports.RippleInfiniteLoopError=t.RippleInfiniteLoopError,exports.RippleInvalidCleanupError=t.RippleInvalidCleanupError,exports.RippleInvalidStoreError=t.RippleInvalidStoreError,exports.batch=r.batch,exports.computed=a.computed,exports.effect=i.effect,exports.effectAsync=i.effectAsync,exports.getDevToolsHook=e.getDevToolsHook,exports.isComputed=c.isComputed,exports.isReactive=c.isReactive,exports.isSignal=c.isSignal,exports.isStore=c.isStore,exports.onCleanup=i.onCleanup,exports.readonly=c.readonly,exports.resource=l.resource,exports.scope=i.scope,exports.signal=o.signal,exports.store=s.store,exports.storeWithHistory=u.storeWithHistory,exports.untrack=n.untrack,exports.watch=i.watch;
@@ -0,0 +1,12 @@
1
+ export type { AsyncEffectCallback, AsyncSubscription, CleanupFn, Computed, ComputedOptions, DepInfo, EffectAsyncOptions, EffectCallback, EffectHandle, EffectOptions, EffectScheduler, EqualityFn, HistoryEntry, PathValue, Readable, Resource, ResourceOptions, ResourceState, Scope, Signal, SignalOptions, Store, StoreWithHistory, Subscription, WatchOptions, } from './types';
2
+ export { RippleComputedCycleError, RippleDisposedScopeError, RippleEnvironmentError, RippleError, RippleInfiniteLoopError, RippleInvalidCleanupError, RippleInvalidStoreError, } from './errors';
3
+ export { computed } from './computed';
4
+ export { signal } from './signal';
5
+ export { store } from './store';
6
+ export { batch } from './scheduling';
7
+ export { effect, effectAsync, onCleanup, scope, watch } from './effect';
8
+ export { isComputed, isReactive, isSignal, isStore, readonly, untrack } from './utilities';
9
+ export { resource } from './async-computed';
10
+ export { storeWithHistory } from './store-history';
11
+ export { getDevToolsHook } from './devtools-hook';
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,YAAY,EACV,mBAAmB,EACnB,iBAAiB,EACjB,SAAS,EACT,QAAQ,EACR,eAAe,EACf,OAAO,EACP,kBAAkB,EAClB,cAAc,EACd,YAAY,EACZ,aAAa,EACb,eAAe,EACf,UAAU,EACV,YAAY,EACZ,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,eAAe,EACf,aAAa,EACb,KAAK,EACL,MAAM,EACN,aAAa,EACb,KAAK,EACL,gBAAgB,EAChB,YAAY,EACZ,YAAY,GACb,MAAM,SAAS,CAAC;AAEjB,OAAO,EACL,wBAAwB,EACxB,wBAAwB,EACxB,sBAAsB,EACtB,WAAW,EACX,uBAAuB,EACvB,yBAAyB,EACzB,uBAAuB,GACxB,MAAM,UAAU,CAAC;AAGlB,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAGhC,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AACrC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAGxE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAG3F,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAG5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAGnD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ import { getDevToolsHook as e } from "./devtools-hook.js";
2
+ import { RippleComputedCycleError as t, RippleDisposedScopeError as n, RippleEnvironmentError as r, RippleError as i, RippleInfiniteLoopError as a, RippleInvalidCleanupError as o, RippleInvalidStoreError as s } from "./errors.js";
3
+ import { untrack as c } from "./tracking.js";
4
+ import { batch as l } from "./scheduling.js";
5
+ import { effect as u, effectAsync as d, onCleanup as f, scope as p, watch as m } from "./effect.js";
6
+ import { computed as h } from "./computed.js";
7
+ import { signal as g } from "./signal.js";
8
+ import { store as _ } from "./store.js";
9
+ import { isComputed as v, isReactive as y, isSignal as b, isStore as x, readonly as S } from "./utilities.js";
10
+ import { resource as C } from "./async-computed.js";
11
+ import { storeWithHistory as w } from "./store-history.js";
12
+ export { t as RippleComputedCycleError, n as RippleDisposedScopeError, r as RippleEnvironmentError, i as RippleError, a as RippleInfiniteLoopError, o as RippleInvalidCleanupError, s as RippleInvalidStoreError, l as batch, h as computed, u as effect, d as effectAsync, e as getDevToolsHook, v as isComputed, y as isReactive, b as isSignal, x as isStore, f as onCleanup, S as readonly, C as resource, p as scope, g as signal, _ as store, w as storeWithHistory, c as untrack, m as watch };
@@ -0,0 +1,2 @@
1
+ const e=require("./symbols.cjs");var t=new FinalizationRegistry(({key:e,map:t})=>{t.delete(e)}),n=class{version=0;name;[e.IS_SIGNAL]=!0;computedSubs_=new Map;effectSubs_=new Set;constructor(e){this.name=e}addComputedSub(e){let n=new WeakRef(e);this.computedSubs_.set(e,n),t.register(e,{key:e,map:this.computedSubs_},n)}removeComputedSub(e){let n=this.computedSubs_.get(e);n!==void 0&&(this.computedSubs_.delete(e),t.unregister(n))}addEffectSub(e){this.effectSubs_.add(e)}removeEffectSub(e){this.effectSubs_.delete(e)}clearSubscribers(){for(let e of this.computedSubs_.values())t.unregister(e);this.computedSubs_.clear(),this.effectSubs_.clear()}hasSubscribers(){if(this.effectSubs_.size>0)return!0;for(let e of this.computedSubs_.values())if(e.deref()!==void 0)return!0;return!1}*computedSubs(){for(let[e,n]of this.computedSubs_){let r=n.deref();r===void 0?(this.computedSubs_.delete(e),t.unregister(n)):yield r}}effectSubs(){return this.effectSubs_}},r=class extends n{[e.IS_COMPUTED]=!0;lastPropEpoch_=0};exports.ComputedBase=r,exports.ReactiveBase=n;
2
+ //# sourceMappingURL=reactive-base.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reactive-base.cjs","names":[],"sources":["../src/reactive-base.ts"],"sourcesContent":["import type { Subscriber } from './types';\n\nimport { IS_COMPUTED, IS_SIGNAL } from './symbols';\n\n// ── Global FinalizationRegistry (F1) ──────────────────────────────────────────\n//\n// When a ComputedBase node becomes garbage-collectible, the registry fires and\n// removes the entry from its source's computedSubs_ map. This lets orphaned\n// computeds (created outside a scope/effect and never explicitly disposed) be\n// reclaimed by the GC without manual lifecycle management.\n\ntype ComputedWeakEntry = {\n readonly key: ComputedBase<unknown>;\n readonly map: Map<ComputedBase<unknown>, WeakRef<ComputedBase<unknown>>>;\n};\n\nconst computedSubRegistry = new FinalizationRegistry<ComputedWeakEntry>(({ key, map }) => {\n map.delete(key);\n});\n\n// ── ReactiveBase ──────────────────────────────────────────────────────────────\n\n/**\n * Abstract base shared by SignalImpl and ComputedImpl.\n *\n * Centralises:\n * - Subscriber-set management (computed subs via WeakRef, effect subs directly)\n * - Version counter (used for dep-version cache invalidation in computed)\n * - The [IS_SIGNAL] brand marker\n */\nexport abstract class ReactiveBase<T> {\n version = 0;\n readonly name: string | undefined;\n [IS_SIGNAL] = true;\n\n // A single Map<computed, WeakRef> replaces the previous Set<WeakRef> + WeakMap pair.\n // O(1) add, remove, and lookup without keeping two structures in sync.\n private readonly computedSubs_ = new Map<ComputedBase<unknown>, WeakRef<ComputedBase<unknown>>>();\n private readonly effectSubs_ = new Set<Subscriber>();\n\n constructor(name?: string) {\n this.name = name;\n }\n\n /**\n * Registers a computed as a downstream subscriber. Callers (trackSource in\n * tracking.ts) must ensure they only call this for genuinely NEW deps — i.e.\n * deps not already present in the previous dep list. This guarantees each\n * computed is registered exactly once per source, keeping the WeakRef set\n * clean without needing a dedup scan.\n */\n addComputedSub(c: ComputedBase<unknown>): void {\n const ref = new WeakRef(c);\n\n this.computedSubs_.set(c, ref);\n computedSubRegistry.register(c, { key: c, map: this.computedSubs_ }, ref);\n }\n\n removeComputedSub(c: ComputedBase<unknown>): void {\n const ref = this.computedSubs_.get(c);\n\n if (ref === undefined) return;\n\n this.computedSubs_.delete(c);\n computedSubRegistry.unregister(ref);\n }\n\n addEffectSub(subscriber: Subscriber): void {\n this.effectSubs_.add(subscriber);\n }\n\n removeEffectSub(subscriber: Subscriber): void {\n this.effectSubs_.delete(subscriber);\n }\n\n protected clearSubscribers(): void {\n for (const ref of this.computedSubs_.values()) {\n computedSubRegistry.unregister(ref);\n }\n\n this.computedSubs_.clear();\n this.effectSubs_.clear();\n }\n\n hasSubscribers(): boolean {\n if (this.effectSubs_.size > 0) return true;\n\n for (const ref of this.computedSubs_.values()) {\n if (ref.deref() !== undefined) return true;\n }\n\n return false;\n }\n\n /**\n * Yields live downstream computed subscribers.\n * Dead WeakRefs (GC'd computeds) are pruned eagerly from the map on each traversal,\n * keeping the subscriber set compact without requiring manual dispose calls.\n */\n *computedSubs(): Generator<ComputedBase<unknown>> {\n for (const [key, ref] of this.computedSubs_) {\n const node = ref.deref();\n\n if (node !== undefined) {\n yield node;\n } else {\n this.computedSubs_.delete(key);\n computedSubRegistry.unregister(ref);\n }\n }\n }\n\n effectSubs(): ReadonlySet<Subscriber> {\n return this.effectSubs_;\n }\n\n abstract get value(): T;\n abstract peek(): T;\n}\n\n// ── Refreshable ──────────────────────────────────────────────────────────────\n\n/**\n * Structural interface for lazy nodes that support dirty-flagging and\n * on-demand recompute. Used by tracking.ts to pull computed deps without\n * importing the concrete ComputedImpl class.\n */\nexport interface Refreshable {\n refreshIfDirty(): boolean;\n}\n\n// ── ComputedBase ──────────────────────────────────────────────────────────────\n\n/**\n * Extension of ReactiveBase for derived (lazy) nodes that can be marked dirty\n * and refreshed on demand. Exposes the two methods scheduling.ts needs without\n * importing the concrete ComputedImpl class (breaks the potential circular dep).\n */\nexport abstract class ComputedBase<T> extends ReactiveBase<T> implements Refreshable {\n [IS_COMPUTED] = true;\n lastPropEpoch_ = 0;\n\n abstract markDirty(): boolean;\n abstract refreshIfDirty(): boolean;\n}\n"],"mappings":"iCAgBA,IAAM,EAAsB,IAAI,sBAAyC,CAAE,MAAK,SAAU,CACxF,EAAI,OAAO,CAAG,CAChB,CAAC,EAYqB,EAAtB,KAAsC,CACpC,QAAU,EACV,KACA,CAAC,EAAA,WAAa,GAId,cAAiC,IAAI,IACrC,YAA+B,IAAI,IAEnC,YAAY,EAAe,CACzB,KAAK,KAAO,CACd,CASA,eAAe,EAAgC,CAC7C,IAAM,EAAM,IAAI,QAAQ,CAAC,EAEzB,KAAK,cAAc,IAAI,EAAG,CAAG,EAC7B,EAAoB,SAAS,EAAG,CAAE,IAAK,EAAG,IAAK,KAAK,aAAc,EAAG,CAAG,CAC1E,CAEA,kBAAkB,EAAgC,CAChD,IAAM,EAAM,KAAK,cAAc,IAAI,CAAC,EAEhC,IAAQ,IAAA,KAEZ,KAAK,cAAc,OAAO,CAAC,EAC3B,EAAoB,WAAW,CAAG,EACpC,CAEA,aAAa,EAA8B,CACzC,KAAK,YAAY,IAAI,CAAU,CACjC,CAEA,gBAAgB,EAA8B,CAC5C,KAAK,YAAY,OAAO,CAAU,CACpC,CAEA,kBAAmC,CACjC,IAAK,IAAM,KAAO,KAAK,cAAc,OAAO,EAC1C,EAAoB,WAAW,CAAG,EAGpC,KAAK,cAAc,MAAM,EACzB,KAAK,YAAY,MAAM,CACzB,CAEA,gBAA0B,CACxB,GAAI,KAAK,YAAY,KAAO,EAAG,MAAO,GAEtC,IAAK,IAAM,KAAO,KAAK,cAAc,OAAO,EAC1C,GAAI,EAAI,MAAM,IAAM,IAAA,GAAW,MAAO,GAGxC,MAAO,EACT,CAOA,CAAC,cAAiD,CAChD,IAAK,GAAM,CAAC,EAAK,KAAQ,KAAK,cAAe,CAC3C,IAAM,EAAO,EAAI,MAAM,EAEnB,IAAS,IAAA,IAGX,KAAK,cAAc,OAAO,CAAG,EAC7B,EAAoB,WAAW,CAAG,GAHlC,MAAM,CAKV,CACF,CAEA,YAAsC,CACpC,OAAO,KAAK,WACd,CAIF,EAoBsB,EAAtB,cAA8C,CAAuC,CACnF,CAAC,EAAA,aAAe,GAChB,eAAiB,CAInB"}
@@ -0,0 +1,60 @@
1
+ import type { Subscriber } from './types';
2
+ import { IS_COMPUTED, IS_SIGNAL } from './symbols';
3
+ /**
4
+ * Abstract base shared by SignalImpl and ComputedImpl.
5
+ *
6
+ * Centralises:
7
+ * - Subscriber-set management (computed subs via WeakRef, effect subs directly)
8
+ * - Version counter (used for dep-version cache invalidation in computed)
9
+ * - The [IS_SIGNAL] brand marker
10
+ */
11
+ export declare abstract class ReactiveBase<T> {
12
+ version: number;
13
+ readonly name: string | undefined;
14
+ [IS_SIGNAL]: boolean;
15
+ private readonly computedSubs_;
16
+ private readonly effectSubs_;
17
+ constructor(name?: string);
18
+ /**
19
+ * Registers a computed as a downstream subscriber. Callers (trackSource in
20
+ * tracking.ts) must ensure they only call this for genuinely NEW deps — i.e.
21
+ * deps not already present in the previous dep list. This guarantees each
22
+ * computed is registered exactly once per source, keeping the WeakRef set
23
+ * clean without needing a dedup scan.
24
+ */
25
+ addComputedSub(c: ComputedBase<unknown>): void;
26
+ removeComputedSub(c: ComputedBase<unknown>): void;
27
+ addEffectSub(subscriber: Subscriber): void;
28
+ removeEffectSub(subscriber: Subscriber): void;
29
+ protected clearSubscribers(): void;
30
+ hasSubscribers(): boolean;
31
+ /**
32
+ * Yields live downstream computed subscribers.
33
+ * Dead WeakRefs (GC'd computeds) are pruned eagerly from the map on each traversal,
34
+ * keeping the subscriber set compact without requiring manual dispose calls.
35
+ */
36
+ computedSubs(): Generator<ComputedBase<unknown>>;
37
+ effectSubs(): ReadonlySet<Subscriber>;
38
+ abstract get value(): T;
39
+ abstract peek(): T;
40
+ }
41
+ /**
42
+ * Structural interface for lazy nodes that support dirty-flagging and
43
+ * on-demand recompute. Used by tracking.ts to pull computed deps without
44
+ * importing the concrete ComputedImpl class.
45
+ */
46
+ export interface Refreshable {
47
+ refreshIfDirty(): boolean;
48
+ }
49
+ /**
50
+ * Extension of ReactiveBase for derived (lazy) nodes that can be marked dirty
51
+ * and refreshed on demand. Exposes the two methods scheduling.ts needs without
52
+ * importing the concrete ComputedImpl class (breaks the potential circular dep).
53
+ */
54
+ export declare abstract class ComputedBase<T> extends ReactiveBase<T> implements Refreshable {
55
+ [IS_COMPUTED]: boolean;
56
+ lastPropEpoch_: number;
57
+ abstract markDirty(): boolean;
58
+ abstract refreshIfDirty(): boolean;
59
+ }
60
+ //# sourceMappingURL=reactive-base.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reactive-base.d.ts","sourceRoot":"","sources":["../src/reactive-base.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1C,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAoBnD;;;;;;;GAOG;AACH,8BAAsB,YAAY,CAAC,CAAC;IAClC,OAAO,SAAK;IACZ,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,CAAC,SAAS,CAAC,UAAQ;IAInB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAoE;IAClG,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAyB;gBAEzC,IAAI,CAAC,EAAE,MAAM;IAIzB;;;;;;OAMG;IACH,cAAc,CAAC,CAAC,EAAE,YAAY,CAAC,OAAO,CAAC,GAAG,IAAI;IAO9C,iBAAiB,CAAC,CAAC,EAAE,YAAY,CAAC,OAAO,CAAC,GAAG,IAAI;IASjD,YAAY,CAAC,UAAU,EAAE,UAAU,GAAG,IAAI;IAI1C,eAAe,CAAC,UAAU,EAAE,UAAU,GAAG,IAAI;IAI7C,SAAS,CAAC,gBAAgB,IAAI,IAAI;IASlC,cAAc,IAAI,OAAO;IAUzB;;;;OAIG;IACF,YAAY,IAAI,SAAS,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;IAajD,UAAU,IAAI,WAAW,CAAC,UAAU,CAAC;IAIrC,QAAQ,KAAK,KAAK,IAAI,CAAC,CAAC;IACxB,QAAQ,CAAC,IAAI,IAAI,CAAC;CACnB;AAID;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,cAAc,IAAI,OAAO,CAAC;CAC3B;AAID;;;;GAIG;AACH,8BAAsB,YAAY,CAAC,CAAC,CAAE,SAAQ,YAAY,CAAC,CAAC,CAAE,YAAW,WAAW;IAClF,CAAC,WAAW,CAAC,UAAQ;IACrB,cAAc,SAAK;IAEnB,QAAQ,CAAC,SAAS,IAAI,OAAO;IAC7B,QAAQ,CAAC,cAAc,IAAI,OAAO;CACnC"}
@@ -0,0 +1,56 @@
1
+ import { IS_COMPUTED as e, IS_SIGNAL as t } from "./symbols.js";
2
+ //#region src/reactive-base.ts
3
+ var n = new FinalizationRegistry(({ key: e, map: t }) => {
4
+ t.delete(e);
5
+ }), r = class {
6
+ version = 0;
7
+ name;
8
+ [t] = !0;
9
+ computedSubs_ = /* @__PURE__ */ new Map();
10
+ effectSubs_ = /* @__PURE__ */ new Set();
11
+ constructor(e) {
12
+ this.name = e;
13
+ }
14
+ addComputedSub(e) {
15
+ let t = new WeakRef(e);
16
+ this.computedSubs_.set(e, t), n.register(e, {
17
+ key: e,
18
+ map: this.computedSubs_
19
+ }, t);
20
+ }
21
+ removeComputedSub(e) {
22
+ let t = this.computedSubs_.get(e);
23
+ t !== void 0 && (this.computedSubs_.delete(e), n.unregister(t));
24
+ }
25
+ addEffectSub(e) {
26
+ this.effectSubs_.add(e);
27
+ }
28
+ removeEffectSub(e) {
29
+ this.effectSubs_.delete(e);
30
+ }
31
+ clearSubscribers() {
32
+ for (let e of this.computedSubs_.values()) n.unregister(e);
33
+ this.computedSubs_.clear(), this.effectSubs_.clear();
34
+ }
35
+ hasSubscribers() {
36
+ if (this.effectSubs_.size > 0) return !0;
37
+ for (let e of this.computedSubs_.values()) if (e.deref() !== void 0) return !0;
38
+ return !1;
39
+ }
40
+ *computedSubs() {
41
+ for (let [e, t] of this.computedSubs_) {
42
+ let r = t.deref();
43
+ r === void 0 ? (this.computedSubs_.delete(e), n.unregister(t)) : yield r;
44
+ }
45
+ }
46
+ effectSubs() {
47
+ return this.effectSubs_;
48
+ }
49
+ }, i = class extends r {
50
+ [e] = !0;
51
+ lastPropEpoch_ = 0;
52
+ };
53
+ //#endregion
54
+ export { i as ComputedBase, r as ReactiveBase };
55
+
56
+ //# sourceMappingURL=reactive-base.js.map