@vielzeug/ripple 1.2.0 → 1.2.2
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.
- package/dist/devtools.d.ts +12 -1
- package/dist/devtools.d.ts.map +1 -1
- package/dist/ripple.cjs +1 -1
- package/dist/ripple.cjs.map +1 -1
- package/dist/ripple.iife.js +1 -1
- package/dist/ripple.iife.js.map +1 -1
- package/dist/ripple.js +1 -1
- package/dist/ripple.js.map +1 -1
- package/dist/src/devtools.cjs.map +1 -1
- package/dist/src/devtools.js.map +1 -1
- package/dist/store.cjs +1 -1
- package/dist/store.cjs.map +1 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +3 -2
- package/dist/store.js.map +1 -1
- package/dist/tracking.cjs.map +1 -1
- package/dist/tracking.d.ts.map +1 -1
- package/dist/tracking.js.map +1 -1
- package/dist/utilities.cjs.map +1 -1
- package/dist/utilities.d.ts +3 -2
- package/dist/utilities.d.ts.map +1 -1
- package/dist/utilities.js.map +1 -1
- package/package.json +6 -3
package/dist/store.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("./devtools-hook.cjs"),t=require("./errors.cjs"),n=require("./scheduling.cjs"),r=require("./symbols.cjs"),i=require("./computed.cjs"),a=require("./signal.cjs");var o=e=>{if(e===`__proto__`||e===`constructor`||e===`prototype`)throw new t.RippleInvalidStoreError(`Unsafe key "${e}" rejected to prevent prototype pollution.`)},s=(e,t)=>{let n=e;for(let e of t){if(typeof n!=`object`||!n)return;n=n[e]}return n},c=(e,n,r)=>{if(n.length===0)return r;if(typeof e!=`object`||!e)throw new t.RippleInvalidStoreError(`Cannot write to a nested path through a null or non-object intermediate value.`);let[i,...a]=n,o=e;return{...o,[i]:c(o[i],a,r)}},l=class{[r.IS_SIGNAL]=!0;source_;write_;evict_;disposeSource_;name_;get name(){return this.name_??this.source_.name}constructor(e){this.source_=e.source,this.write_=e.write,this.evict_=e.evict,this.disposeSource_=e.disposeSource,this.name_=e.name}get value(){return this.source_.value}set value(e){this.write_(e)}peek(){return this.source_.peek()}subscribe(e){return this.source_.subscribe(e)}get disposed(){return this.source_.disposed}dispose(){this.evict_(),this.disposeSource_?.()}[Symbol.dispose](){this.dispose()}},u=class{[r.IS_SIGNAL]=!0;[r.IS_STORE]=!0;name;current_;disposed_;initial_;lensCache_;propSignals_;readonlyProxy_;version_;constructor(e,n){this.name=n,this.current_=structuredClone(e),this.disposed_=!1,this.initial_=structuredClone(e),this.lensCache_=new Map,this.propSignals_=new Map,this.version_=new a.SignalImpl(0,void 0,n?`${n}.version`:void 0),this.readonlyProxy_=new Proxy(this.current_,{deleteProperty(e,n){throw new t.RippleInvalidStoreError(`Direct deletion from store.value is not allowed. Use store.patch() or store.lens(). (key: "${String(n)}")`)},set(e,n){throw new t.RippleInvalidStoreError(`Direct mutation of store.value is not allowed. Use store.patch(), store.lens(), or store.replace(). (key: "${String(n)}")`)}})}propSignalFor_(e){let t=this.propSignals_.get(e);return t===void 0&&(t=new a.SignalImpl(this.current_[e],void 0,this.name===void 0?e:`${this.name}.${e}`),this.propSignals_.set(e,t)),t}setPath_(e,t){let[n,...r]=e,i=this.current_[n],a=c(i,r,t);this.applyTopLevelChange_(n,a)}applyTopLevelChange_(e,t){o(e);let n=this.current_[e];Object.is(n,t)||(this.current_[e]=t,this.propSignalFor_(e).value=t,this.version_.value=this.version_.peek()+1)}deleteTopLevelKey_(e){Object.hasOwn(this.current_,e)&&(delete this.current_[e],this.propSignalFor_(e).value=void 0,this.version_.value=this.version_.peek()+1)}applyFullState_(e){let t=this.current_,r=new Set([...Object.keys(t),...Object.keys(e)]);for(let e of r)o(e);n.batch(()=>{for(let t of r)Object.hasOwn(e,t)?this.applyTopLevelChange_(t,e[t]):this.deleteTopLevelKey_(t)})}get value(){return this.version_.value,this.readonlyProxy_}peek(){return Object.freeze(structuredClone(this.current_))}subscribe=e=>this.version_.subscribe(e);patch(r){if(typeof r!=`object`||!r||Array.isArray(r))throw new t.RippleInvalidStoreError(`store.patch() requires a plain object partial.`);let i=Object.keys(r);if(i.length!==0){for(let e of i)o(e);n.batch(()=>{for(let e of i)this.applyTopLevelChange_(e,r[e])}),e.getDevToolsHook()?.mutate?.({kind:`patch`,name:this.name})}}replace(t){let n=structuredClone(this.current_),r=t(n);r!==n&&(this.applyFullState_(r),e.getDevToolsHook()?.mutate?.({kind:`replace`,name:this.name}))}reset(){this.applyFullState_(this.initial_),e.getDevToolsHook()?.mutate?.({kind:`reset`,name:this.name})}lens(r){let a=this.lensCache_.get(r);if(a!==void 0)return a;let o=()=>{this.lensCache_.delete(r)},c=r.split(`.`);if(c.length>32)throw new t.RippleInvalidStoreError(`Lens path exceeds maximum depth of 32 segments: "${r}".`);for(let e of c){if(e===``)throw new t.RippleInvalidStoreError(`Empty path segment in lens path "${r}". Check for consecutive dots.`);if(e===`__proto__`||e===`constructor`||e===`prototype`)throw new t.RippleInvalidStoreError(`Unsafe path segment "${e}" in lens path "${r}".`)}let u;if(c.length===1)u=new l({evict:o,source:this.propSignalFor_(c[0]),write:t=>{n.batch(()=>this.applyTopLevelChange_(c[0],t)),e.getDevToolsHook()?.mutate?.({kind:`lens`,name:this.name,path:r})}});else{let t=i.computed(()=>s(this.propSignalFor_(c[0]).value,c.slice(1)));u=new l({disposeSource:()=>t.dispose(),evict:o,name:this.name?`${this.name}.${r}`:r,source:t,write:i=>{let a=t.peek();Object.is(a,i)||(n.batch(()=>this.setPath_(c,i)),e.getDevToolsHook()?.mutate?.({kind:`lens`,name:this.name,path:r}))}})}return this.lensCache_.set(r,u),u}get disposed(){return this.disposed_}dispose(){if(!this.disposed_){this.disposed_=!0;for(let e of[...this.lensCache_.values()])e.dispose();this.lensCache_.clear();for(let e of this.propSignals_.values())e.dispose();this.propSignals_.clear(),this.version_.dispose(),e.getDevToolsHook()?.dispose?.({kind:`store`,name:this.name})}}[Symbol.dispose](){this.dispose()}},d=(e,n)=>{if(typeof e!=`object`||!e||Array.isArray(e))throw new t.RippleInvalidStoreError(`store() requires a plain object initial state.`);for(let t of Object.keys(e))o(t);return new u(e,n?.name)};exports.StoreImpl=u,exports.store=d;
|
|
1
|
+
const e=require("./devtools-hook.cjs"),t=require("./errors.cjs"),n=require("./scheduling.cjs"),r=require("./symbols.cjs"),i=require("./computed.cjs"),a=require("./signal.cjs");var o=e=>{if(e===`__proto__`||e===`constructor`||e===`prototype`)throw new t.RippleInvalidStoreError(`Unsafe key "${e}" rejected to prevent prototype pollution.`)},s=(e,t)=>{let n=e;for(let e of t){if(typeof n!=`object`||!n)return;n=n[e]}return n},c=(e,n,r)=>{if(n.length===0)return r;if(typeof e!=`object`||!e)throw new t.RippleInvalidStoreError(`Cannot write to a nested path through a null or non-object intermediate value.`);let[i,...a]=n,o=e;return{...o,[i]:c(o[i],a,r)}},l=class{[r.IS_SIGNAL]=!0;source_;write_;evict_;disposeSource_;name_;disposed_=!1;get name(){return this.name_??this.source_.name}constructor(e){this.source_=e.source,this.write_=e.write,this.evict_=e.evict,this.disposeSource_=e.disposeSource,this.name_=e.name}get value(){return this.source_.value}set value(e){this.write_(e)}peek(){return this.source_.peek()}subscribe(e){return this.source_.subscribe(e)}get disposed(){return this.disposed_||this.source_.disposed}dispose(){this.disposed_=!0,this.evict_(),this.disposeSource_?.()}[Symbol.dispose](){this.dispose()}},u=class{[r.IS_SIGNAL]=!0;[r.IS_STORE]=!0;name;current_;disposed_;initial_;lensCache_;propSignals_;readonlyProxy_;version_;constructor(e,n){this.name=n,this.current_=structuredClone(e),this.disposed_=!1,this.initial_=structuredClone(e),this.lensCache_=new Map,this.propSignals_=new Map,this.version_=new a.SignalImpl(0,void 0,n?`${n}.version`:void 0),this.readonlyProxy_=new Proxy(this.current_,{deleteProperty(e,n){throw new t.RippleInvalidStoreError(`Direct deletion from store.value is not allowed. Use store.patch() or store.lens(). (key: "${String(n)}")`)},set(e,n){throw new t.RippleInvalidStoreError(`Direct mutation of store.value is not allowed. Use store.patch(), store.lens(), or store.replace(). (key: "${String(n)}")`)}})}propSignalFor_(e){let t=this.propSignals_.get(e);return t===void 0&&(t=new a.SignalImpl(this.current_[e],void 0,this.name===void 0?e:`${this.name}.${e}`),this.propSignals_.set(e,t)),t}setPath_(e,t){let[n,...r]=e,i=this.current_[n],a=c(i,r,t);this.applyTopLevelChange_(n,a)}applyTopLevelChange_(e,t){o(e);let n=this.current_[e];Object.is(n,t)||(this.current_[e]=t,this.propSignalFor_(e).value=t,this.version_.value=this.version_.peek()+1)}deleteTopLevelKey_(e){Object.hasOwn(this.current_,e)&&(delete this.current_[e],this.propSignalFor_(e).value=void 0,this.version_.value=this.version_.peek()+1)}applyFullState_(e){let t=this.current_,r=new Set([...Object.keys(t),...Object.keys(e)]);for(let e of r)o(e);n.batch(()=>{for(let t of r)Object.hasOwn(e,t)?this.applyTopLevelChange_(t,e[t]):this.deleteTopLevelKey_(t)})}get value(){return this.version_.value,this.readonlyProxy_}peek(){return Object.freeze(structuredClone(this.current_))}subscribe=e=>this.version_.subscribe(e);patch(r){if(typeof r!=`object`||!r||Array.isArray(r))throw new t.RippleInvalidStoreError(`store.patch() requires a plain object partial.`);let i=Object.keys(r);if(i.length!==0){for(let e of i)o(e);n.batch(()=>{for(let e of i)this.applyTopLevelChange_(e,r[e])}),e.getDevToolsHook()?.mutate?.({kind:`patch`,name:this.name})}}replace(t){let n=structuredClone(this.current_),r=t(n);r!==n&&(this.applyFullState_(r),e.getDevToolsHook()?.mutate?.({kind:`replace`,name:this.name}))}reset(){this.applyFullState_(this.initial_),e.getDevToolsHook()?.mutate?.({kind:`reset`,name:this.name})}lens(r){let a=this.lensCache_.get(r);if(a!==void 0)return a;let o=()=>{this.lensCache_.delete(r)},c=r.split(`.`);if(c.length>32)throw new t.RippleInvalidStoreError(`Lens path exceeds maximum depth of 32 segments: "${r}".`);for(let e of c){if(e===``)throw new t.RippleInvalidStoreError(`Empty path segment in lens path "${r}". Check for consecutive dots.`);if(e===`__proto__`||e===`constructor`||e===`prototype`)throw new t.RippleInvalidStoreError(`Unsafe path segment "${e}" in lens path "${r}".`)}let u;if(c.length===1)u=new l({evict:o,source:this.propSignalFor_(c[0]),write:t=>{n.batch(()=>this.applyTopLevelChange_(c[0],t)),e.getDevToolsHook()?.mutate?.({kind:`lens`,name:this.name,path:r})}});else{let t=i.computed(()=>s(this.propSignalFor_(c[0]).value,c.slice(1)));u=new l({disposeSource:()=>t.dispose(),evict:o,name:this.name?`${this.name}.${r}`:r,source:t,write:i=>{let a=t.peek();Object.is(a,i)||(n.batch(()=>this.setPath_(c,i)),e.getDevToolsHook()?.mutate?.({kind:`lens`,name:this.name,path:r}))}})}return this.lensCache_.set(r,u),u}get disposed(){return this.disposed_}dispose(){if(!this.disposed_){this.disposed_=!0;for(let e of[...this.lensCache_.values()])e.dispose();this.lensCache_.clear();for(let e of this.propSignals_.values())e.dispose();this.propSignals_.clear(),this.version_.dispose(),e.getDevToolsHook()?.dispose?.({kind:`store`,name:this.name})}}[Symbol.dispose](){this.dispose()}},d=(e,n)=>{if(typeof e!=`object`||!e||Array.isArray(e))throw new t.RippleInvalidStoreError(`store() requires a plain object initial state.`);for(let t of Object.keys(e))o(t);return new u(e,n?.name)};exports.StoreImpl=u,exports.store=d;
|
|
2
2
|
//# sourceMappingURL=store.cjs.map
|
package/dist/store.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.cjs","names":[],"sources":["../src/store.ts"],"sourcesContent":["import type { PathValue, Readable, Signal, Store, Subscription } from './types';\n\nimport { computed } from './computed';\nimport { getDevToolsHook } from './devtools-hook';\nimport { RippleInvalidStoreError } from './errors';\nimport { batch } from './scheduling';\nimport { SignalImpl } from './signal';\nimport { IS_SIGNAL, IS_STORE } from './symbols';\n\n// ── Key safety ────────────────────────────────────────────────────────────────\n\n/**\n * Rejects prototype-pollution-prone top-level keys. Called both as an upfront\n * validation pass (patch/replace — so a later unsafe key never leaves earlier,\n * safe keys partially applied) and defensively inside applyTopLevelChange_.\n */\nconst assertSafeKey = (key: string): void => {\n if (key === '__proto__' || key === 'constructor' || key === 'prototype') {\n throw new RippleInvalidStoreError(`Unsafe key \"${key}\" rejected to prevent prototype pollution.`);\n }\n};\n\n// ── Nested path helpers ───────────────────────────────────────────────────────\n\nconst getNestedValue = (obj: unknown, parts: string[]): unknown => {\n let current = obj;\n\n for (const key of parts) {\n if (current == null || typeof current !== 'object') return undefined;\n\n current = (current as Record<string, unknown>)[key];\n }\n\n return current;\n};\n\nconst setNestedValue = <T>(obj: T, parts: string[], value: unknown): T => {\n if (parts.length === 0) return value as T;\n\n if (obj === null || typeof obj !== 'object') {\n throw new RippleInvalidStoreError('Cannot write to a nested path through a null or non-object intermediate value.');\n }\n\n const [key, ...rest] = parts;\n const currentObj = obj as Record<string, unknown>;\n\n return { ...currentObj, [key!]: setNestedValue(currentObj[key!], rest, value) } as T;\n};\n\n// ── LensSignal ───────────────────────────────────────────────────────────────\n//\n// A unified writable Signal that delegates reads to an inner Reactive<V>\n// and writes to an injected callback. Replaces the old TopLevelLens +\n// NestedLens pair — same interface, one class to update when Signal<T> changes.\n\ntype LensOptions<V> = {\n disposeSource?: () => void;\n evict: () => void;\n name?: string;\n source: Readable<V>;\n write: (v: V) => void;\n};\n\nclass LensSignal<V> implements Signal<V> {\n [IS_SIGNAL] = true as const;\n\n private source_: Readable<V>;\n private write_: (v: V) => void;\n private evict_: () => void;\n private disposeSource_: (() => void) | undefined;\n private name_: string | undefined;\n\n get name(): string | undefined {\n return this.name_ ?? this.source_.name;\n }\n\n constructor(opts: LensOptions<V>) {\n this.source_ = opts.source;\n this.write_ = opts.write;\n this.evict_ = opts.evict;\n this.disposeSource_ = opts.disposeSource;\n this.name_ = opts.name;\n }\n\n get value(): V {\n return this.source_.value;\n }\n\n set value(next: V) {\n this.write_(next);\n }\n\n peek(): V {\n return this.source_.peek();\n }\n\n subscribe(listener: () => void): Subscription {\n return this.source_.subscribe(listener);\n }\n\n get disposed(): boolean {\n return this.source_.disposed;\n }\n\n dispose(): void {\n this.evict_();\n this.disposeSource_?.();\n }\n\n [Symbol.dispose](): void {\n this.dispose();\n }\n}\n\n// ── StoreImpl ─────────────────────────────────────────────────────────────────\n//\n// Fine-grained reactive store.\n//\n// Internal layout:\n// - current_: mutable backing object — NEVER exposed directly, internal writes only\n// - readonlyProxy_: Proxy over current_ that throws on set/delete — returned by .peek()\n// - propSignals_: per-top-level-key SignalImpl, lazily created on first lens/read\n// - version_: whole-store monotonic counter (bumped on any change, used by subscribe())\n//\n// StoreImpl is a standalone class — does not extend ReactiveBase.\n// applyTopLevelChange_() has NO batch() wrapper — callers that need atomicity\n// (patch, replace, reset, and both lens write paths) wrap with batch() at their\n// own call sites; the method itself stays free of any scheduling concern.\n\nexport class StoreImpl<T extends object> implements Store<T> {\n [IS_SIGNAL] = true as const;\n [IS_STORE] = true as const;\n\n readonly name: string | undefined;\n\n private current_: T;\n private disposed_: boolean;\n private readonly initial_: T;\n private readonly lensCache_: Map<string, LensSignal<unknown>>;\n private readonly propSignals_: Map<string, SignalImpl<unknown>>;\n private readonly readonlyProxy_: Readonly<T>;\n private readonly version_: SignalImpl<number>;\n\n constructor(initial: T, name?: string) {\n this.name = name;\n this.current_ = structuredClone(initial);\n this.disposed_ = false;\n this.initial_ = structuredClone(initial);\n this.lensCache_ = new Map();\n this.propSignals_ = new Map();\n this.version_ = new SignalImpl(0, undefined, name ? `${name}.version` : undefined);\n\n // Shallow proxy: throws on any top-level mutation attempt.\n // All external reads go through this proxy; internal writes use this.current_ directly.\n // NOTE: nested objects are plain references — mutations on nested properties\n // bypass reactivity. Use store.lens('a.b') or store.replace() to update nested state.\n this.readonlyProxy_ = new Proxy(this.current_, {\n deleteProperty(_target, key): never {\n throw new RippleInvalidStoreError(\n `Direct deletion from store.value is not allowed. Use store.patch() or store.lens(). (key: \"${String(key)}\")`,\n );\n },\n set(_target, key): never {\n throw new RippleInvalidStoreError(\n `Direct mutation of store.value is not allowed. Use store.patch(), store.lens(), or store.replace(). (key: \"${String(key)}\")`,\n );\n },\n }) as Readonly<T>;\n }\n\n /** Returns (or lazily creates) the per-property signal for a top-level key. */\n propSignalFor_(key: string): SignalImpl<unknown> {\n let sig = this.propSignals_.get(key);\n\n if (sig === undefined) {\n sig = new SignalImpl(\n (this.current_ as Record<string, unknown>)[key],\n undefined,\n this.name !== undefined ? `${this.name}.${key}` : key,\n );\n this.propSignals_.set(key, sig);\n }\n\n return sig;\n }\n\n /** @internal Called by nested lens write to update the store through a path. */\n setPath_(parts: string[], value: unknown): void {\n const [rootKey, ...rest] = parts;\n const rootVal = (this.current_ as Record<string, unknown>)[rootKey!];\n const newRootVal = setNestedValue(rootVal, rest, value);\n\n this.applyTopLevelChange_(rootKey!, newRootVal);\n }\n\n /**\n * @internal Apply a change to a top-level key and propagate to prop signal + version.\n * R4: No batch() wrapper — callers that need atomicity must wrap themselves.\n */\n applyTopLevelChange_(key: string, newValue: unknown): void {\n assertSafeKey(key);\n\n const current = (this.current_ as Record<string, unknown>)[key];\n\n if (Object.is(current, newValue)) return;\n\n (this.current_ as Record<string, unknown>)[key] = newValue;\n this.propSignalFor_(key).value = newValue as never;\n this.version_.value = this.version_.peek() + 1;\n }\n\n /**\n * @internal Removes a top-level key entirely (as opposed to applyTopLevelChange_,\n * which can only ever assign a value — including `undefined` — never delete the key).\n * Companion used by applyFullState_ for keys absent from the target state.\n */\n deleteTopLevelKey_(key: string): void {\n if (!Object.hasOwn(this.current_ as object, key)) return;\n\n delete (this.current_ as Record<string, unknown>)[key];\n this.propSignalFor_(key).value = undefined as never;\n this.version_.value = this.version_.peek() + 1;\n }\n\n /**\n * @internal Applies `next` as the authoritative full top-level state: keys present in\n * `next` are set via applyTopLevelChange_, keys present in current_ but absent from\n * `next` are removed via deleteTopLevelKey_. Shared by replace() and reset() so both\n * can actually remove a key, not just null it out — see B1 in the ripple improvement plan.\n */\n applyFullState_(next: Record<string, unknown>): void {\n const current = this.current_ as Record<string, unknown>;\n const allKeys = new Set([...Object.keys(current), ...Object.keys(next)]);\n\n for (const key of allKeys) assertSafeKey(key);\n\n batch(() => {\n for (const key of allKeys) {\n if (Object.hasOwn(next, key)) {\n this.applyTopLevelChange_(key, next[key]);\n } else {\n this.deleteTopLevelKey_(key);\n }\n }\n });\n }\n\n /**\n * Returns the current state as a tracked read — registers the store as a dependency\n * inside `effect()` / `computed()`. Any mutation re-runs the subscriber.\n * For untracked one-off reads, use `peek()` instead.\n */\n get value(): Readonly<T> {\n void this.version_.value;\n\n return this.readonlyProxy_;\n }\n\n /**\n * Returns a frozen deep snapshot of the current state.\n * The returned object reflects state at call time — it does not update as the store mutates.\n * Nested objects are cloned (not aliased), so mutating nested properties on the snapshot\n * does not affect the live store state.\n * Use for serialization or one-off reads outside reactive contexts.\n * For reactive reads, use `store.lens(path)` instead.\n */\n peek(): Readonly<T> {\n return Object.freeze(structuredClone(this.current_)) as Readonly<T>;\n }\n\n readonly subscribe = (listener: () => void): Subscription => {\n return this.version_.subscribe(listener);\n };\n\n patch(partial: Partial<T>): void {\n if (typeof partial !== 'object' || partial === null || Array.isArray(partial)) {\n throw new RippleInvalidStoreError('store.patch() requires a plain object partial.');\n }\n\n const keys = Object.keys(partial) as Array<keyof T & string>;\n\n if (keys.length === 0) return;\n\n // Validate every key before mutating any — a key rejected mid-loop must not\n // leave earlier, safe keys partially applied (and un-notified — see batch()'s\n // error path, which clears the pending flush queue rather than replaying it).\n for (const key of keys) assertSafeKey(key);\n\n batch(() => {\n for (const key of keys) {\n this.applyTopLevelChange_(key, (partial as Record<string, unknown>)[key]);\n }\n });\n\n getDevToolsHook()?.mutate?.({ kind: 'patch', name: this.name });\n }\n\n replace(fn: (state: Readonly<T>) => T): void {\n const snapshot = structuredClone(this.current_) as Readonly<T>;\n const next = fn(snapshot);\n\n if (next === snapshot) return;\n\n this.applyFullState_(next as Record<string, unknown>);\n\n getDevToolsHook()?.mutate?.({ kind: 'replace', name: this.name });\n }\n\n reset(): void {\n this.applyFullState_(this.initial_ as Record<string, unknown>);\n\n getDevToolsHook()?.mutate?.({ kind: 'reset', name: this.name });\n }\n\n lens<P extends string>(path: P): Signal<PathValue<T, P>> {\n const cached = this.lensCache_.get(path);\n\n if (cached !== undefined) return cached as unknown as Signal<PathValue<T, P>>;\n\n const evict = (): void => {\n this.lensCache_.delete(path);\n };\n\n const parts = path.split('.');\n\n if (parts.length > 32) {\n throw new RippleInvalidStoreError(`Lens path exceeds maximum depth of 32 segments: \"${path}\".`);\n }\n\n for (const part of parts) {\n if (part === '') {\n throw new RippleInvalidStoreError(`Empty path segment in lens path \"${path}\". Check for consecutive dots.`);\n }\n\n if (part === '__proto__' || part === 'constructor' || part === 'prototype') {\n throw new RippleInvalidStoreError(`Unsafe path segment \"${part}\" in lens path \"${path}\".`);\n }\n }\n\n let lens: LensSignal<unknown>;\n\n if (parts.length === 1) {\n // Top-level lens: backed directly by the per-property signal.\n const propSig = this.propSignalFor_(parts[0]!) as SignalImpl<PathValue<T, P>>;\n\n lens = new LensSignal<PathValue<T, P>>({\n evict,\n // propSig lifecycle is owned by the store — do not dispose it\n source: propSig,\n write: (v) => {\n batch(() => this.applyTopLevelChange_(parts[0]!, v));\n getDevToolsHook()?.mutate?.({ kind: 'lens', name: this.name, path });\n },\n }) as unknown as LensSignal<unknown>;\n } else {\n // Nested lens: backed by a derived computed over the root property signal.\n // Write path uses batch() here so nested lens writes remain atomic.\n const readComputed = computed(\n () => getNestedValue(this.propSignalFor_(parts[0]!).value, parts.slice(1)) as PathValue<T, P>,\n );\n\n lens = new LensSignal<PathValue<T, P>>({\n disposeSource: () => readComputed.dispose(),\n evict,\n name: this.name ? `${this.name}.${path}` : path,\n source: readComputed,\n write: (v) => {\n const current = readComputed.peek();\n\n if (Object.is(current, v)) return;\n\n batch(() => this.setPath_(parts, v));\n getDevToolsHook()?.mutate?.({ kind: 'lens', name: this.name, path });\n },\n }) as unknown as LensSignal<unknown>;\n }\n\n this.lensCache_.set(path, lens);\n\n return lens as unknown as Signal<PathValue<T, P>>;\n }\n\n get disposed(): boolean {\n return this.disposed_;\n }\n\n dispose(): void {\n if (this.disposed_) return;\n\n this.disposed_ = true;\n\n for (const lens of [...this.lensCache_.values()]) {\n lens.dispose();\n }\n\n this.lensCache_.clear();\n\n for (const sig of this.propSignals_.values()) {\n sig.dispose();\n }\n\n this.propSignals_.clear();\n this.version_.dispose();\n getDevToolsHook()?.dispose?.({ kind: 'store', name: this.name });\n }\n\n [Symbol.dispose](): void {\n this.dispose();\n }\n}\n\nexport const store = <T extends object>(initial: T, options?: { name?: string }): Store<T> => {\n if (typeof initial !== 'object' || initial === null || Array.isArray(initial)) {\n throw new RippleInvalidStoreError('store() requires a plain object initial state.');\n }\n\n // Same guard as patch()/replace() — reject an unsafe top-level key (e.g. a real\n // own \"__proto__\" property from JSON.parse) before it ever enters store state.\n for (const key of Object.keys(initial)) assertSafeKey(key);\n\n return new StoreImpl(initial, options?.name) as unknown as Store<T>;\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"store.cjs","names":[],"sources":["../src/store.ts"],"sourcesContent":["import type { PathValue, Readable, Signal, Store, Subscription } from './types';\n\nimport { computed } from './computed';\nimport { getDevToolsHook } from './devtools-hook';\nimport { RippleInvalidStoreError } from './errors';\nimport { batch } from './scheduling';\nimport { SignalImpl } from './signal';\nimport { IS_SIGNAL, IS_STORE } from './symbols';\n\n// ── Key safety ────────────────────────────────────────────────────────────────\n\n/**\n * Rejects prototype-pollution-prone top-level keys. Called both as an upfront\n * validation pass (patch/replace — so a later unsafe key never leaves earlier,\n * safe keys partially applied) and defensively inside applyTopLevelChange_.\n */\nconst assertSafeKey = (key: string): void => {\n if (key === '__proto__' || key === 'constructor' || key === 'prototype') {\n throw new RippleInvalidStoreError(`Unsafe key \"${key}\" rejected to prevent prototype pollution.`);\n }\n};\n\n// ── Nested path helpers ───────────────────────────────────────────────────────\n\nconst getNestedValue = (obj: unknown, parts: string[]): unknown => {\n let current = obj;\n\n for (const key of parts) {\n if (current == null || typeof current !== 'object') return undefined;\n\n current = (current as Record<string, unknown>)[key];\n }\n\n return current;\n};\n\nconst setNestedValue = <T>(obj: T, parts: string[], value: unknown): T => {\n if (parts.length === 0) return value as T;\n\n if (obj === null || typeof obj !== 'object') {\n throw new RippleInvalidStoreError('Cannot write to a nested path through a null or non-object intermediate value.');\n }\n\n const [key, ...rest] = parts;\n const currentObj = obj as Record<string, unknown>;\n\n return { ...currentObj, [key!]: setNestedValue(currentObj[key!], rest, value) } as T;\n};\n\n// ── LensSignal ───────────────────────────────────────────────────────────────\n//\n// A unified writable Signal that delegates reads to an inner Reactive<V>\n// and writes to an injected callback. Replaces the old TopLevelLens +\n// NestedLens pair — same interface, one class to update when Signal<T> changes.\n\ntype LensOptions<V> = {\n disposeSource?: () => void;\n evict: () => void;\n name?: string;\n source: Readable<V>;\n write: (v: V) => void;\n};\n\nclass LensSignal<V> implements Signal<V> {\n [IS_SIGNAL] = true as const;\n\n private source_: Readable<V>;\n private write_: (v: V) => void;\n private evict_: () => void;\n private disposeSource_: (() => void) | undefined;\n private name_: string | undefined;\n // Own flag — top-level lenses don't own their source (the store does), so\n // `dispose()` can't rely on `source_.disposed` alone to reflect \"was this\n // handle disposed?\". Nested lenses still cascade into `source_.disposed`\n // via `disposeSource_`, so the getter below ORs both signals.\n private disposed_ = false;\n\n get name(): string | undefined {\n return this.name_ ?? this.source_.name;\n }\n\n constructor(opts: LensOptions<V>) {\n this.source_ = opts.source;\n this.write_ = opts.write;\n this.evict_ = opts.evict;\n this.disposeSource_ = opts.disposeSource;\n this.name_ = opts.name;\n }\n\n get value(): V {\n return this.source_.value;\n }\n\n set value(next: V) {\n this.write_(next);\n }\n\n peek(): V {\n return this.source_.peek();\n }\n\n subscribe(listener: () => void): Subscription {\n return this.source_.subscribe(listener);\n }\n\n get disposed(): boolean {\n return this.disposed_ || this.source_.disposed;\n }\n\n dispose(): void {\n this.disposed_ = true;\n this.evict_();\n this.disposeSource_?.();\n }\n\n [Symbol.dispose](): void {\n this.dispose();\n }\n}\n\n// ── StoreImpl ─────────────────────────────────────────────────────────────────\n//\n// Fine-grained reactive store.\n//\n// Internal layout:\n// - current_: mutable backing object — NEVER exposed directly, internal writes only\n// - readonlyProxy_: Proxy over current_ that throws on set/delete — returned by .peek()\n// - propSignals_: per-top-level-key SignalImpl, lazily created on first lens/read\n// - version_: whole-store monotonic counter (bumped on any change, used by subscribe())\n//\n// StoreImpl is a standalone class — does not extend ReactiveBase.\n// applyTopLevelChange_() has NO batch() wrapper — callers that need atomicity\n// (patch, replace, reset, and both lens write paths) wrap with batch() at their\n// own call sites; the method itself stays free of any scheduling concern.\n\nexport class StoreImpl<T extends object> implements Store<T> {\n [IS_SIGNAL] = true as const;\n [IS_STORE] = true as const;\n\n readonly name: string | undefined;\n\n private current_: T;\n private disposed_: boolean;\n private readonly initial_: T;\n private readonly lensCache_: Map<string, LensSignal<unknown>>;\n private readonly propSignals_: Map<string, SignalImpl<unknown>>;\n private readonly readonlyProxy_: Readonly<T>;\n private readonly version_: SignalImpl<number>;\n\n constructor(initial: T, name?: string) {\n this.name = name;\n this.current_ = structuredClone(initial);\n this.disposed_ = false;\n this.initial_ = structuredClone(initial);\n this.lensCache_ = new Map();\n this.propSignals_ = new Map();\n this.version_ = new SignalImpl(0, undefined, name ? `${name}.version` : undefined);\n\n // Shallow proxy: throws on any top-level mutation attempt.\n // All external reads go through this proxy; internal writes use this.current_ directly.\n // NOTE: nested objects are plain references — mutations on nested properties\n // bypass reactivity. Use store.lens('a.b') or store.replace() to update nested state.\n this.readonlyProxy_ = new Proxy(this.current_, {\n deleteProperty(_target, key): never {\n throw new RippleInvalidStoreError(\n `Direct deletion from store.value is not allowed. Use store.patch() or store.lens(). (key: \"${String(key)}\")`,\n );\n },\n set(_target, key): never {\n throw new RippleInvalidStoreError(\n `Direct mutation of store.value is not allowed. Use store.patch(), store.lens(), or store.replace(). (key: \"${String(key)}\")`,\n );\n },\n }) as Readonly<T>;\n }\n\n /** Returns (or lazily creates) the per-property signal for a top-level key. */\n propSignalFor_(key: string): SignalImpl<unknown> {\n let sig = this.propSignals_.get(key);\n\n if (sig === undefined) {\n sig = new SignalImpl(\n (this.current_ as Record<string, unknown>)[key],\n undefined,\n this.name !== undefined ? `${this.name}.${key}` : key,\n );\n this.propSignals_.set(key, sig);\n }\n\n return sig;\n }\n\n /** @internal Called by nested lens write to update the store through a path. */\n setPath_(parts: string[], value: unknown): void {\n const [rootKey, ...rest] = parts;\n const rootVal = (this.current_ as Record<string, unknown>)[rootKey!];\n const newRootVal = setNestedValue(rootVal, rest, value);\n\n this.applyTopLevelChange_(rootKey!, newRootVal);\n }\n\n /**\n * @internal Apply a change to a top-level key and propagate to prop signal + version.\n * R4: No batch() wrapper — callers that need atomicity must wrap themselves.\n */\n applyTopLevelChange_(key: string, newValue: unknown): void {\n assertSafeKey(key);\n\n const current = (this.current_ as Record<string, unknown>)[key];\n\n if (Object.is(current, newValue)) return;\n\n (this.current_ as Record<string, unknown>)[key] = newValue;\n this.propSignalFor_(key).value = newValue as never;\n this.version_.value = this.version_.peek() + 1;\n }\n\n /**\n * @internal Removes a top-level key entirely (as opposed to applyTopLevelChange_,\n * which can only ever assign a value — including `undefined` — never delete the key).\n * Companion used by applyFullState_ for keys absent from the target state.\n */\n deleteTopLevelKey_(key: string): void {\n if (!Object.hasOwn(this.current_ as object, key)) return;\n\n delete (this.current_ as Record<string, unknown>)[key];\n this.propSignalFor_(key).value = undefined as never;\n this.version_.value = this.version_.peek() + 1;\n }\n\n /**\n * @internal Applies `next` as the authoritative full top-level state: keys present in\n * `next` are set via applyTopLevelChange_, keys present in current_ but absent from\n * `next` are removed via deleteTopLevelKey_. Shared by replace() and reset() so both\n * can actually remove a key, not just null it out — see B1 in the ripple improvement plan.\n */\n applyFullState_(next: Record<string, unknown>): void {\n const current = this.current_ as Record<string, unknown>;\n const allKeys = new Set([...Object.keys(current), ...Object.keys(next)]);\n\n for (const key of allKeys) assertSafeKey(key);\n\n batch(() => {\n for (const key of allKeys) {\n if (Object.hasOwn(next, key)) {\n this.applyTopLevelChange_(key, next[key]);\n } else {\n this.deleteTopLevelKey_(key);\n }\n }\n });\n }\n\n /**\n * Returns the current state as a tracked read — registers the store as a dependency\n * inside `effect()` / `computed()`. Any mutation re-runs the subscriber.\n * For untracked one-off reads, use `peek()` instead.\n */\n get value(): Readonly<T> {\n void this.version_.value;\n\n return this.readonlyProxy_;\n }\n\n /**\n * Returns a frozen deep snapshot of the current state.\n * The returned object reflects state at call time — it does not update as the store mutates.\n * Nested objects are cloned (not aliased), so mutating nested properties on the snapshot\n * does not affect the live store state.\n * Use for serialization or one-off reads outside reactive contexts.\n * For reactive reads, use `store.lens(path)` instead.\n */\n peek(): Readonly<T> {\n return Object.freeze(structuredClone(this.current_)) as Readonly<T>;\n }\n\n readonly subscribe = (listener: () => void): Subscription => {\n return this.version_.subscribe(listener);\n };\n\n patch(partial: Partial<T>): void {\n if (typeof partial !== 'object' || partial === null || Array.isArray(partial)) {\n throw new RippleInvalidStoreError('store.patch() requires a plain object partial.');\n }\n\n const keys = Object.keys(partial) as Array<keyof T & string>;\n\n if (keys.length === 0) return;\n\n // Validate every key before mutating any — a key rejected mid-loop must not\n // leave earlier, safe keys partially applied (and un-notified — see batch()'s\n // error path, which clears the pending flush queue rather than replaying it).\n for (const key of keys) assertSafeKey(key);\n\n batch(() => {\n for (const key of keys) {\n this.applyTopLevelChange_(key, (partial as Record<string, unknown>)[key]);\n }\n });\n\n getDevToolsHook()?.mutate?.({ kind: 'patch', name: this.name });\n }\n\n replace(fn: (state: Readonly<T>) => T): void {\n const snapshot = structuredClone(this.current_) as Readonly<T>;\n const next = fn(snapshot);\n\n if (next === snapshot) return;\n\n this.applyFullState_(next as Record<string, unknown>);\n\n getDevToolsHook()?.mutate?.({ kind: 'replace', name: this.name });\n }\n\n reset(): void {\n this.applyFullState_(this.initial_ as Record<string, unknown>);\n\n getDevToolsHook()?.mutate?.({ kind: 'reset', name: this.name });\n }\n\n lens<P extends string>(path: P): Signal<PathValue<T, P>> {\n const cached = this.lensCache_.get(path);\n\n if (cached !== undefined) return cached as unknown as Signal<PathValue<T, P>>;\n\n const evict = (): void => {\n this.lensCache_.delete(path);\n };\n\n const parts = path.split('.');\n\n if (parts.length > 32) {\n throw new RippleInvalidStoreError(`Lens path exceeds maximum depth of 32 segments: \"${path}\".`);\n }\n\n for (const part of parts) {\n if (part === '') {\n throw new RippleInvalidStoreError(`Empty path segment in lens path \"${path}\". Check for consecutive dots.`);\n }\n\n if (part === '__proto__' || part === 'constructor' || part === 'prototype') {\n throw new RippleInvalidStoreError(`Unsafe path segment \"${part}\" in lens path \"${path}\".`);\n }\n }\n\n let lens: LensSignal<unknown>;\n\n if (parts.length === 1) {\n // Top-level lens: backed directly by the per-property signal.\n const propSig = this.propSignalFor_(parts[0]!) as SignalImpl<PathValue<T, P>>;\n\n lens = new LensSignal<PathValue<T, P>>({\n evict,\n // propSig lifecycle is owned by the store — do not dispose it\n source: propSig,\n write: (v) => {\n batch(() => this.applyTopLevelChange_(parts[0]!, v));\n getDevToolsHook()?.mutate?.({ kind: 'lens', name: this.name, path });\n },\n }) as unknown as LensSignal<unknown>;\n } else {\n // Nested lens: backed by a derived computed over the root property signal.\n // Write path uses batch() here so nested lens writes remain atomic.\n const readComputed = computed(\n () => getNestedValue(this.propSignalFor_(parts[0]!).value, parts.slice(1)) as PathValue<T, P>,\n );\n\n lens = new LensSignal<PathValue<T, P>>({\n disposeSource: () => readComputed.dispose(),\n evict,\n name: this.name ? `${this.name}.${path}` : path,\n source: readComputed,\n write: (v) => {\n const current = readComputed.peek();\n\n if (Object.is(current, v)) return;\n\n batch(() => this.setPath_(parts, v));\n getDevToolsHook()?.mutate?.({ kind: 'lens', name: this.name, path });\n },\n }) as unknown as LensSignal<unknown>;\n }\n\n this.lensCache_.set(path, lens);\n\n return lens as unknown as Signal<PathValue<T, P>>;\n }\n\n get disposed(): boolean {\n return this.disposed_;\n }\n\n dispose(): void {\n if (this.disposed_) return;\n\n this.disposed_ = true;\n\n for (const lens of [...this.lensCache_.values()]) {\n lens.dispose();\n }\n\n this.lensCache_.clear();\n\n for (const sig of this.propSignals_.values()) {\n sig.dispose();\n }\n\n this.propSignals_.clear();\n this.version_.dispose();\n getDevToolsHook()?.dispose?.({ kind: 'store', name: this.name });\n }\n\n [Symbol.dispose](): void {\n this.dispose();\n }\n}\n\nexport const store = <T extends object>(initial: T, options?: { name?: string }): Store<T> => {\n if (typeof initial !== 'object' || initial === null || Array.isArray(initial)) {\n throw new RippleInvalidStoreError('store() requires a plain object initial state.');\n }\n\n // Same guard as patch()/replace() — reject an unsafe top-level key (e.g. a real\n // own \"__proto__\" property from JSON.parse) before it ever enters store state.\n for (const key of Object.keys(initial)) assertSafeKey(key);\n\n return new StoreImpl(initial, options?.name) as unknown as Store<T>;\n};\n"],"mappings":"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"}
|
package/dist/store.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAY,MAAM,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAMhF,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAY,MAAM,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAMhF,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAgIhD,qBAAa,SAAS,CAAC,CAAC,SAAS,MAAM,CAAE,YAAW,KAAK,CAAC,CAAC,CAAC;IAC1D,CAAC,SAAS,CAAC,EAAG,IAAI,CAAU;IAC5B,CAAC,QAAQ,CAAC,EAAG,IAAI,CAAU;IAE3B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IAElC,OAAO,CAAC,QAAQ,CAAI;IACpB,OAAO,CAAC,SAAS,CAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAI;IAC7B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAmC;IAC9D,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAmC;IAChE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAc;IAC7C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqB;gBAElC,OAAO,EAAE,CAAC,EAAE,IAAI,CAAC,EAAE,MAAM;IA2BrC,+EAA+E;IAC/E,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAAC,OAAO,CAAC;IA4EhD;;;;OAIG;IACH,IAAI,KAAK,IAAI,QAAQ,CAAC,CAAC,CAAC,CAIvB;IAED;;;;;;;OAOG;IACH,IAAI,IAAI,QAAQ,CAAC,CAAC,CAAC;IAInB,QAAQ,CAAC,SAAS,GAAI,UAAU,MAAM,IAAI,KAAG,YAAY,CAEvD;IAEF,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI;IAuBhC,OAAO,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,IAAI;IAW5C,KAAK,IAAI,IAAI;IAMb,IAAI,CAAC,CAAC,SAAS,MAAM,EAAE,IAAI,EAAE,CAAC,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAoExD,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED,OAAO,IAAI,IAAI;IAoBf,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI;CAGzB;AAED,eAAO,MAAM,KAAK,GAAI,CAAC,SAAS,MAAM,EAAE,SAAS,CAAC,EAAE,UAAU;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,KAAG,KAAK,CAAC,CAAC,CAUxF,CAAC"}
|
package/dist/store.js
CHANGED
|
@@ -29,6 +29,7 @@ var s = (e) => {
|
|
|
29
29
|
evict_;
|
|
30
30
|
disposeSource_;
|
|
31
31
|
name_;
|
|
32
|
+
disposed_ = !1;
|
|
32
33
|
get name() {
|
|
33
34
|
return this.name_ ?? this.source_.name;
|
|
34
35
|
}
|
|
@@ -48,10 +49,10 @@ var s = (e) => {
|
|
|
48
49
|
return this.source_.subscribe(e);
|
|
49
50
|
}
|
|
50
51
|
get disposed() {
|
|
51
|
-
return this.source_.disposed;
|
|
52
|
+
return this.disposed_ || this.source_.disposed;
|
|
52
53
|
}
|
|
53
54
|
dispose() {
|
|
54
|
-
this.evict_(), this.disposeSource_?.();
|
|
55
|
+
this.disposed_ = !0, this.evict_(), this.disposeSource_?.();
|
|
55
56
|
}
|
|
56
57
|
[Symbol.dispose]() {
|
|
57
58
|
this.dispose();
|
package/dist/store.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.js","names":[],"sources":["../src/store.ts"],"sourcesContent":["import type { PathValue, Readable, Signal, Store, Subscription } from './types';\n\nimport { computed } from './computed';\nimport { getDevToolsHook } from './devtools-hook';\nimport { RippleInvalidStoreError } from './errors';\nimport { batch } from './scheduling';\nimport { SignalImpl } from './signal';\nimport { IS_SIGNAL, IS_STORE } from './symbols';\n\n// ── Key safety ────────────────────────────────────────────────────────────────\n\n/**\n * Rejects prototype-pollution-prone top-level keys. Called both as an upfront\n * validation pass (patch/replace — so a later unsafe key never leaves earlier,\n * safe keys partially applied) and defensively inside applyTopLevelChange_.\n */\nconst assertSafeKey = (key: string): void => {\n if (key === '__proto__' || key === 'constructor' || key === 'prototype') {\n throw new RippleInvalidStoreError(`Unsafe key \"${key}\" rejected to prevent prototype pollution.`);\n }\n};\n\n// ── Nested path helpers ───────────────────────────────────────────────────────\n\nconst getNestedValue = (obj: unknown, parts: string[]): unknown => {\n let current = obj;\n\n for (const key of parts) {\n if (current == null || typeof current !== 'object') return undefined;\n\n current = (current as Record<string, unknown>)[key];\n }\n\n return current;\n};\n\nconst setNestedValue = <T>(obj: T, parts: string[], value: unknown): T => {\n if (parts.length === 0) return value as T;\n\n if (obj === null || typeof obj !== 'object') {\n throw new RippleInvalidStoreError('Cannot write to a nested path through a null or non-object intermediate value.');\n }\n\n const [key, ...rest] = parts;\n const currentObj = obj as Record<string, unknown>;\n\n return { ...currentObj, [key!]: setNestedValue(currentObj[key!], rest, value) } as T;\n};\n\n// ── LensSignal ───────────────────────────────────────────────────────────────\n//\n// A unified writable Signal that delegates reads to an inner Reactive<V>\n// and writes to an injected callback. Replaces the old TopLevelLens +\n// NestedLens pair — same interface, one class to update when Signal<T> changes.\n\ntype LensOptions<V> = {\n disposeSource?: () => void;\n evict: () => void;\n name?: string;\n source: Readable<V>;\n write: (v: V) => void;\n};\n\nclass LensSignal<V> implements Signal<V> {\n [IS_SIGNAL] = true as const;\n\n private source_: Readable<V>;\n private write_: (v: V) => void;\n private evict_: () => void;\n private disposeSource_: (() => void) | undefined;\n private name_: string | undefined;\n\n get name(): string | undefined {\n return this.name_ ?? this.source_.name;\n }\n\n constructor(opts: LensOptions<V>) {\n this.source_ = opts.source;\n this.write_ = opts.write;\n this.evict_ = opts.evict;\n this.disposeSource_ = opts.disposeSource;\n this.name_ = opts.name;\n }\n\n get value(): V {\n return this.source_.value;\n }\n\n set value(next: V) {\n this.write_(next);\n }\n\n peek(): V {\n return this.source_.peek();\n }\n\n subscribe(listener: () => void): Subscription {\n return this.source_.subscribe(listener);\n }\n\n get disposed(): boolean {\n return this.source_.disposed;\n }\n\n dispose(): void {\n this.evict_();\n this.disposeSource_?.();\n }\n\n [Symbol.dispose](): void {\n this.dispose();\n }\n}\n\n// ── StoreImpl ─────────────────────────────────────────────────────────────────\n//\n// Fine-grained reactive store.\n//\n// Internal layout:\n// - current_: mutable backing object — NEVER exposed directly, internal writes only\n// - readonlyProxy_: Proxy over current_ that throws on set/delete — returned by .peek()\n// - propSignals_: per-top-level-key SignalImpl, lazily created on first lens/read\n// - version_: whole-store monotonic counter (bumped on any change, used by subscribe())\n//\n// StoreImpl is a standalone class — does not extend ReactiveBase.\n// applyTopLevelChange_() has NO batch() wrapper — callers that need atomicity\n// (patch, replace, reset, and both lens write paths) wrap with batch() at their\n// own call sites; the method itself stays free of any scheduling concern.\n\nexport class StoreImpl<T extends object> implements Store<T> {\n [IS_SIGNAL] = true as const;\n [IS_STORE] = true as const;\n\n readonly name: string | undefined;\n\n private current_: T;\n private disposed_: boolean;\n private readonly initial_: T;\n private readonly lensCache_: Map<string, LensSignal<unknown>>;\n private readonly propSignals_: Map<string, SignalImpl<unknown>>;\n private readonly readonlyProxy_: Readonly<T>;\n private readonly version_: SignalImpl<number>;\n\n constructor(initial: T, name?: string) {\n this.name = name;\n this.current_ = structuredClone(initial);\n this.disposed_ = false;\n this.initial_ = structuredClone(initial);\n this.lensCache_ = new Map();\n this.propSignals_ = new Map();\n this.version_ = new SignalImpl(0, undefined, name ? `${name}.version` : undefined);\n\n // Shallow proxy: throws on any top-level mutation attempt.\n // All external reads go through this proxy; internal writes use this.current_ directly.\n // NOTE: nested objects are plain references — mutations on nested properties\n // bypass reactivity. Use store.lens('a.b') or store.replace() to update nested state.\n this.readonlyProxy_ = new Proxy(this.current_, {\n deleteProperty(_target, key): never {\n throw new RippleInvalidStoreError(\n `Direct deletion from store.value is not allowed. Use store.patch() or store.lens(). (key: \"${String(key)}\")`,\n );\n },\n set(_target, key): never {\n throw new RippleInvalidStoreError(\n `Direct mutation of store.value is not allowed. Use store.patch(), store.lens(), or store.replace(). (key: \"${String(key)}\")`,\n );\n },\n }) as Readonly<T>;\n }\n\n /** Returns (or lazily creates) the per-property signal for a top-level key. */\n propSignalFor_(key: string): SignalImpl<unknown> {\n let sig = this.propSignals_.get(key);\n\n if (sig === undefined) {\n sig = new SignalImpl(\n (this.current_ as Record<string, unknown>)[key],\n undefined,\n this.name !== undefined ? `${this.name}.${key}` : key,\n );\n this.propSignals_.set(key, sig);\n }\n\n return sig;\n }\n\n /** @internal Called by nested lens write to update the store through a path. */\n setPath_(parts: string[], value: unknown): void {\n const [rootKey, ...rest] = parts;\n const rootVal = (this.current_ as Record<string, unknown>)[rootKey!];\n const newRootVal = setNestedValue(rootVal, rest, value);\n\n this.applyTopLevelChange_(rootKey!, newRootVal);\n }\n\n /**\n * @internal Apply a change to a top-level key and propagate to prop signal + version.\n * R4: No batch() wrapper — callers that need atomicity must wrap themselves.\n */\n applyTopLevelChange_(key: string, newValue: unknown): void {\n assertSafeKey(key);\n\n const current = (this.current_ as Record<string, unknown>)[key];\n\n if (Object.is(current, newValue)) return;\n\n (this.current_ as Record<string, unknown>)[key] = newValue;\n this.propSignalFor_(key).value = newValue as never;\n this.version_.value = this.version_.peek() + 1;\n }\n\n /**\n * @internal Removes a top-level key entirely (as opposed to applyTopLevelChange_,\n * which can only ever assign a value — including `undefined` — never delete the key).\n * Companion used by applyFullState_ for keys absent from the target state.\n */\n deleteTopLevelKey_(key: string): void {\n if (!Object.hasOwn(this.current_ as object, key)) return;\n\n delete (this.current_ as Record<string, unknown>)[key];\n this.propSignalFor_(key).value = undefined as never;\n this.version_.value = this.version_.peek() + 1;\n }\n\n /**\n * @internal Applies `next` as the authoritative full top-level state: keys present in\n * `next` are set via applyTopLevelChange_, keys present in current_ but absent from\n * `next` are removed via deleteTopLevelKey_. Shared by replace() and reset() so both\n * can actually remove a key, not just null it out — see B1 in the ripple improvement plan.\n */\n applyFullState_(next: Record<string, unknown>): void {\n const current = this.current_ as Record<string, unknown>;\n const allKeys = new Set([...Object.keys(current), ...Object.keys(next)]);\n\n for (const key of allKeys) assertSafeKey(key);\n\n batch(() => {\n for (const key of allKeys) {\n if (Object.hasOwn(next, key)) {\n this.applyTopLevelChange_(key, next[key]);\n } else {\n this.deleteTopLevelKey_(key);\n }\n }\n });\n }\n\n /**\n * Returns the current state as a tracked read — registers the store as a dependency\n * inside `effect()` / `computed()`. Any mutation re-runs the subscriber.\n * For untracked one-off reads, use `peek()` instead.\n */\n get value(): Readonly<T> {\n void this.version_.value;\n\n return this.readonlyProxy_;\n }\n\n /**\n * Returns a frozen deep snapshot of the current state.\n * The returned object reflects state at call time — it does not update as the store mutates.\n * Nested objects are cloned (not aliased), so mutating nested properties on the snapshot\n * does not affect the live store state.\n * Use for serialization or one-off reads outside reactive contexts.\n * For reactive reads, use `store.lens(path)` instead.\n */\n peek(): Readonly<T> {\n return Object.freeze(structuredClone(this.current_)) as Readonly<T>;\n }\n\n readonly subscribe = (listener: () => void): Subscription => {\n return this.version_.subscribe(listener);\n };\n\n patch(partial: Partial<T>): void {\n if (typeof partial !== 'object' || partial === null || Array.isArray(partial)) {\n throw new RippleInvalidStoreError('store.patch() requires a plain object partial.');\n }\n\n const keys = Object.keys(partial) as Array<keyof T & string>;\n\n if (keys.length === 0) return;\n\n // Validate every key before mutating any — a key rejected mid-loop must not\n // leave earlier, safe keys partially applied (and un-notified — see batch()'s\n // error path, which clears the pending flush queue rather than replaying it).\n for (const key of keys) assertSafeKey(key);\n\n batch(() => {\n for (const key of keys) {\n this.applyTopLevelChange_(key, (partial as Record<string, unknown>)[key]);\n }\n });\n\n getDevToolsHook()?.mutate?.({ kind: 'patch', name: this.name });\n }\n\n replace(fn: (state: Readonly<T>) => T): void {\n const snapshot = structuredClone(this.current_) as Readonly<T>;\n const next = fn(snapshot);\n\n if (next === snapshot) return;\n\n this.applyFullState_(next as Record<string, unknown>);\n\n getDevToolsHook()?.mutate?.({ kind: 'replace', name: this.name });\n }\n\n reset(): void {\n this.applyFullState_(this.initial_ as Record<string, unknown>);\n\n getDevToolsHook()?.mutate?.({ kind: 'reset', name: this.name });\n }\n\n lens<P extends string>(path: P): Signal<PathValue<T, P>> {\n const cached = this.lensCache_.get(path);\n\n if (cached !== undefined) return cached as unknown as Signal<PathValue<T, P>>;\n\n const evict = (): void => {\n this.lensCache_.delete(path);\n };\n\n const parts = path.split('.');\n\n if (parts.length > 32) {\n throw new RippleInvalidStoreError(`Lens path exceeds maximum depth of 32 segments: \"${path}\".`);\n }\n\n for (const part of parts) {\n if (part === '') {\n throw new RippleInvalidStoreError(`Empty path segment in lens path \"${path}\". Check for consecutive dots.`);\n }\n\n if (part === '__proto__' || part === 'constructor' || part === 'prototype') {\n throw new RippleInvalidStoreError(`Unsafe path segment \"${part}\" in lens path \"${path}\".`);\n }\n }\n\n let lens: LensSignal<unknown>;\n\n if (parts.length === 1) {\n // Top-level lens: backed directly by the per-property signal.\n const propSig = this.propSignalFor_(parts[0]!) as SignalImpl<PathValue<T, P>>;\n\n lens = new LensSignal<PathValue<T, P>>({\n evict,\n // propSig lifecycle is owned by the store — do not dispose it\n source: propSig,\n write: (v) => {\n batch(() => this.applyTopLevelChange_(parts[0]!, v));\n getDevToolsHook()?.mutate?.({ kind: 'lens', name: this.name, path });\n },\n }) as unknown as LensSignal<unknown>;\n } else {\n // Nested lens: backed by a derived computed over the root property signal.\n // Write path uses batch() here so nested lens writes remain atomic.\n const readComputed = computed(\n () => getNestedValue(this.propSignalFor_(parts[0]!).value, parts.slice(1)) as PathValue<T, P>,\n );\n\n lens = new LensSignal<PathValue<T, P>>({\n disposeSource: () => readComputed.dispose(),\n evict,\n name: this.name ? `${this.name}.${path}` : path,\n source: readComputed,\n write: (v) => {\n const current = readComputed.peek();\n\n if (Object.is(current, v)) return;\n\n batch(() => this.setPath_(parts, v));\n getDevToolsHook()?.mutate?.({ kind: 'lens', name: this.name, path });\n },\n }) as unknown as LensSignal<unknown>;\n }\n\n this.lensCache_.set(path, lens);\n\n return lens as unknown as Signal<PathValue<T, P>>;\n }\n\n get disposed(): boolean {\n return this.disposed_;\n }\n\n dispose(): void {\n if (this.disposed_) return;\n\n this.disposed_ = true;\n\n for (const lens of [...this.lensCache_.values()]) {\n lens.dispose();\n }\n\n this.lensCache_.clear();\n\n for (const sig of this.propSignals_.values()) {\n sig.dispose();\n }\n\n this.propSignals_.clear();\n this.version_.dispose();\n getDevToolsHook()?.dispose?.({ kind: 'store', name: this.name });\n }\n\n [Symbol.dispose](): void {\n this.dispose();\n }\n}\n\nexport const store = <T extends object>(initial: T, options?: { name?: string }): Store<T> => {\n if (typeof initial !== 'object' || initial === null || Array.isArray(initial)) {\n throw new RippleInvalidStoreError('store() requires a plain object initial state.');\n }\n\n // Same guard as patch()/replace() — reject an unsafe top-level key (e.g. a real\n // own \"__proto__\" property from JSON.parse) before it ever enters store state.\n for (const key of Object.keys(initial)) assertSafeKey(key);\n\n return new StoreImpl(initial, options?.name) as unknown as Store<T>;\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"store.js","names":[],"sources":["../src/store.ts"],"sourcesContent":["import type { PathValue, Readable, Signal, Store, Subscription } from './types';\n\nimport { computed } from './computed';\nimport { getDevToolsHook } from './devtools-hook';\nimport { RippleInvalidStoreError } from './errors';\nimport { batch } from './scheduling';\nimport { SignalImpl } from './signal';\nimport { IS_SIGNAL, IS_STORE } from './symbols';\n\n// ── Key safety ────────────────────────────────────────────────────────────────\n\n/**\n * Rejects prototype-pollution-prone top-level keys. Called both as an upfront\n * validation pass (patch/replace — so a later unsafe key never leaves earlier,\n * safe keys partially applied) and defensively inside applyTopLevelChange_.\n */\nconst assertSafeKey = (key: string): void => {\n if (key === '__proto__' || key === 'constructor' || key === 'prototype') {\n throw new RippleInvalidStoreError(`Unsafe key \"${key}\" rejected to prevent prototype pollution.`);\n }\n};\n\n// ── Nested path helpers ───────────────────────────────────────────────────────\n\nconst getNestedValue = (obj: unknown, parts: string[]): unknown => {\n let current = obj;\n\n for (const key of parts) {\n if (current == null || typeof current !== 'object') return undefined;\n\n current = (current as Record<string, unknown>)[key];\n }\n\n return current;\n};\n\nconst setNestedValue = <T>(obj: T, parts: string[], value: unknown): T => {\n if (parts.length === 0) return value as T;\n\n if (obj === null || typeof obj !== 'object') {\n throw new RippleInvalidStoreError('Cannot write to a nested path through a null or non-object intermediate value.');\n }\n\n const [key, ...rest] = parts;\n const currentObj = obj as Record<string, unknown>;\n\n return { ...currentObj, [key!]: setNestedValue(currentObj[key!], rest, value) } as T;\n};\n\n// ── LensSignal ───────────────────────────────────────────────────────────────\n//\n// A unified writable Signal that delegates reads to an inner Reactive<V>\n// and writes to an injected callback. Replaces the old TopLevelLens +\n// NestedLens pair — same interface, one class to update when Signal<T> changes.\n\ntype LensOptions<V> = {\n disposeSource?: () => void;\n evict: () => void;\n name?: string;\n source: Readable<V>;\n write: (v: V) => void;\n};\n\nclass LensSignal<V> implements Signal<V> {\n [IS_SIGNAL] = true as const;\n\n private source_: Readable<V>;\n private write_: (v: V) => void;\n private evict_: () => void;\n private disposeSource_: (() => void) | undefined;\n private name_: string | undefined;\n // Own flag — top-level lenses don't own their source (the store does), so\n // `dispose()` can't rely on `source_.disposed` alone to reflect \"was this\n // handle disposed?\". Nested lenses still cascade into `source_.disposed`\n // via `disposeSource_`, so the getter below ORs both signals.\n private disposed_ = false;\n\n get name(): string | undefined {\n return this.name_ ?? this.source_.name;\n }\n\n constructor(opts: LensOptions<V>) {\n this.source_ = opts.source;\n this.write_ = opts.write;\n this.evict_ = opts.evict;\n this.disposeSource_ = opts.disposeSource;\n this.name_ = opts.name;\n }\n\n get value(): V {\n return this.source_.value;\n }\n\n set value(next: V) {\n this.write_(next);\n }\n\n peek(): V {\n return this.source_.peek();\n }\n\n subscribe(listener: () => void): Subscription {\n return this.source_.subscribe(listener);\n }\n\n get disposed(): boolean {\n return this.disposed_ || this.source_.disposed;\n }\n\n dispose(): void {\n this.disposed_ = true;\n this.evict_();\n this.disposeSource_?.();\n }\n\n [Symbol.dispose](): void {\n this.dispose();\n }\n}\n\n// ── StoreImpl ─────────────────────────────────────────────────────────────────\n//\n// Fine-grained reactive store.\n//\n// Internal layout:\n// - current_: mutable backing object — NEVER exposed directly, internal writes only\n// - readonlyProxy_: Proxy over current_ that throws on set/delete — returned by .peek()\n// - propSignals_: per-top-level-key SignalImpl, lazily created on first lens/read\n// - version_: whole-store monotonic counter (bumped on any change, used by subscribe())\n//\n// StoreImpl is a standalone class — does not extend ReactiveBase.\n// applyTopLevelChange_() has NO batch() wrapper — callers that need atomicity\n// (patch, replace, reset, and both lens write paths) wrap with batch() at their\n// own call sites; the method itself stays free of any scheduling concern.\n\nexport class StoreImpl<T extends object> implements Store<T> {\n [IS_SIGNAL] = true as const;\n [IS_STORE] = true as const;\n\n readonly name: string | undefined;\n\n private current_: T;\n private disposed_: boolean;\n private readonly initial_: T;\n private readonly lensCache_: Map<string, LensSignal<unknown>>;\n private readonly propSignals_: Map<string, SignalImpl<unknown>>;\n private readonly readonlyProxy_: Readonly<T>;\n private readonly version_: SignalImpl<number>;\n\n constructor(initial: T, name?: string) {\n this.name = name;\n this.current_ = structuredClone(initial);\n this.disposed_ = false;\n this.initial_ = structuredClone(initial);\n this.lensCache_ = new Map();\n this.propSignals_ = new Map();\n this.version_ = new SignalImpl(0, undefined, name ? `${name}.version` : undefined);\n\n // Shallow proxy: throws on any top-level mutation attempt.\n // All external reads go through this proxy; internal writes use this.current_ directly.\n // NOTE: nested objects are plain references — mutations on nested properties\n // bypass reactivity. Use store.lens('a.b') or store.replace() to update nested state.\n this.readonlyProxy_ = new Proxy(this.current_, {\n deleteProperty(_target, key): never {\n throw new RippleInvalidStoreError(\n `Direct deletion from store.value is not allowed. Use store.patch() or store.lens(). (key: \"${String(key)}\")`,\n );\n },\n set(_target, key): never {\n throw new RippleInvalidStoreError(\n `Direct mutation of store.value is not allowed. Use store.patch(), store.lens(), or store.replace(). (key: \"${String(key)}\")`,\n );\n },\n }) as Readonly<T>;\n }\n\n /** Returns (or lazily creates) the per-property signal for a top-level key. */\n propSignalFor_(key: string): SignalImpl<unknown> {\n let sig = this.propSignals_.get(key);\n\n if (sig === undefined) {\n sig = new SignalImpl(\n (this.current_ as Record<string, unknown>)[key],\n undefined,\n this.name !== undefined ? `${this.name}.${key}` : key,\n );\n this.propSignals_.set(key, sig);\n }\n\n return sig;\n }\n\n /** @internal Called by nested lens write to update the store through a path. */\n setPath_(parts: string[], value: unknown): void {\n const [rootKey, ...rest] = parts;\n const rootVal = (this.current_ as Record<string, unknown>)[rootKey!];\n const newRootVal = setNestedValue(rootVal, rest, value);\n\n this.applyTopLevelChange_(rootKey!, newRootVal);\n }\n\n /**\n * @internal Apply a change to a top-level key and propagate to prop signal + version.\n * R4: No batch() wrapper — callers that need atomicity must wrap themselves.\n */\n applyTopLevelChange_(key: string, newValue: unknown): void {\n assertSafeKey(key);\n\n const current = (this.current_ as Record<string, unknown>)[key];\n\n if (Object.is(current, newValue)) return;\n\n (this.current_ as Record<string, unknown>)[key] = newValue;\n this.propSignalFor_(key).value = newValue as never;\n this.version_.value = this.version_.peek() + 1;\n }\n\n /**\n * @internal Removes a top-level key entirely (as opposed to applyTopLevelChange_,\n * which can only ever assign a value — including `undefined` — never delete the key).\n * Companion used by applyFullState_ for keys absent from the target state.\n */\n deleteTopLevelKey_(key: string): void {\n if (!Object.hasOwn(this.current_ as object, key)) return;\n\n delete (this.current_ as Record<string, unknown>)[key];\n this.propSignalFor_(key).value = undefined as never;\n this.version_.value = this.version_.peek() + 1;\n }\n\n /**\n * @internal Applies `next` as the authoritative full top-level state: keys present in\n * `next` are set via applyTopLevelChange_, keys present in current_ but absent from\n * `next` are removed via deleteTopLevelKey_. Shared by replace() and reset() so both\n * can actually remove a key, not just null it out — see B1 in the ripple improvement plan.\n */\n applyFullState_(next: Record<string, unknown>): void {\n const current = this.current_ as Record<string, unknown>;\n const allKeys = new Set([...Object.keys(current), ...Object.keys(next)]);\n\n for (const key of allKeys) assertSafeKey(key);\n\n batch(() => {\n for (const key of allKeys) {\n if (Object.hasOwn(next, key)) {\n this.applyTopLevelChange_(key, next[key]);\n } else {\n this.deleteTopLevelKey_(key);\n }\n }\n });\n }\n\n /**\n * Returns the current state as a tracked read — registers the store as a dependency\n * inside `effect()` / `computed()`. Any mutation re-runs the subscriber.\n * For untracked one-off reads, use `peek()` instead.\n */\n get value(): Readonly<T> {\n void this.version_.value;\n\n return this.readonlyProxy_;\n }\n\n /**\n * Returns a frozen deep snapshot of the current state.\n * The returned object reflects state at call time — it does not update as the store mutates.\n * Nested objects are cloned (not aliased), so mutating nested properties on the snapshot\n * does not affect the live store state.\n * Use for serialization or one-off reads outside reactive contexts.\n * For reactive reads, use `store.lens(path)` instead.\n */\n peek(): Readonly<T> {\n return Object.freeze(structuredClone(this.current_)) as Readonly<T>;\n }\n\n readonly subscribe = (listener: () => void): Subscription => {\n return this.version_.subscribe(listener);\n };\n\n patch(partial: Partial<T>): void {\n if (typeof partial !== 'object' || partial === null || Array.isArray(partial)) {\n throw new RippleInvalidStoreError('store.patch() requires a plain object partial.');\n }\n\n const keys = Object.keys(partial) as Array<keyof T & string>;\n\n if (keys.length === 0) return;\n\n // Validate every key before mutating any — a key rejected mid-loop must not\n // leave earlier, safe keys partially applied (and un-notified — see batch()'s\n // error path, which clears the pending flush queue rather than replaying it).\n for (const key of keys) assertSafeKey(key);\n\n batch(() => {\n for (const key of keys) {\n this.applyTopLevelChange_(key, (partial as Record<string, unknown>)[key]);\n }\n });\n\n getDevToolsHook()?.mutate?.({ kind: 'patch', name: this.name });\n }\n\n replace(fn: (state: Readonly<T>) => T): void {\n const snapshot = structuredClone(this.current_) as Readonly<T>;\n const next = fn(snapshot);\n\n if (next === snapshot) return;\n\n this.applyFullState_(next as Record<string, unknown>);\n\n getDevToolsHook()?.mutate?.({ kind: 'replace', name: this.name });\n }\n\n reset(): void {\n this.applyFullState_(this.initial_ as Record<string, unknown>);\n\n getDevToolsHook()?.mutate?.({ kind: 'reset', name: this.name });\n }\n\n lens<P extends string>(path: P): Signal<PathValue<T, P>> {\n const cached = this.lensCache_.get(path);\n\n if (cached !== undefined) return cached as unknown as Signal<PathValue<T, P>>;\n\n const evict = (): void => {\n this.lensCache_.delete(path);\n };\n\n const parts = path.split('.');\n\n if (parts.length > 32) {\n throw new RippleInvalidStoreError(`Lens path exceeds maximum depth of 32 segments: \"${path}\".`);\n }\n\n for (const part of parts) {\n if (part === '') {\n throw new RippleInvalidStoreError(`Empty path segment in lens path \"${path}\". Check for consecutive dots.`);\n }\n\n if (part === '__proto__' || part === 'constructor' || part === 'prototype') {\n throw new RippleInvalidStoreError(`Unsafe path segment \"${part}\" in lens path \"${path}\".`);\n }\n }\n\n let lens: LensSignal<unknown>;\n\n if (parts.length === 1) {\n // Top-level lens: backed directly by the per-property signal.\n const propSig = this.propSignalFor_(parts[0]!) as SignalImpl<PathValue<T, P>>;\n\n lens = new LensSignal<PathValue<T, P>>({\n evict,\n // propSig lifecycle is owned by the store — do not dispose it\n source: propSig,\n write: (v) => {\n batch(() => this.applyTopLevelChange_(parts[0]!, v));\n getDevToolsHook()?.mutate?.({ kind: 'lens', name: this.name, path });\n },\n }) as unknown as LensSignal<unknown>;\n } else {\n // Nested lens: backed by a derived computed over the root property signal.\n // Write path uses batch() here so nested lens writes remain atomic.\n const readComputed = computed(\n () => getNestedValue(this.propSignalFor_(parts[0]!).value, parts.slice(1)) as PathValue<T, P>,\n );\n\n lens = new LensSignal<PathValue<T, P>>({\n disposeSource: () => readComputed.dispose(),\n evict,\n name: this.name ? `${this.name}.${path}` : path,\n source: readComputed,\n write: (v) => {\n const current = readComputed.peek();\n\n if (Object.is(current, v)) return;\n\n batch(() => this.setPath_(parts, v));\n getDevToolsHook()?.mutate?.({ kind: 'lens', name: this.name, path });\n },\n }) as unknown as LensSignal<unknown>;\n }\n\n this.lensCache_.set(path, lens);\n\n return lens as unknown as Signal<PathValue<T, P>>;\n }\n\n get disposed(): boolean {\n return this.disposed_;\n }\n\n dispose(): void {\n if (this.disposed_) return;\n\n this.disposed_ = true;\n\n for (const lens of [...this.lensCache_.values()]) {\n lens.dispose();\n }\n\n this.lensCache_.clear();\n\n for (const sig of this.propSignals_.values()) {\n sig.dispose();\n }\n\n this.propSignals_.clear();\n this.version_.dispose();\n getDevToolsHook()?.dispose?.({ kind: 'store', name: this.name });\n }\n\n [Symbol.dispose](): void {\n this.dispose();\n }\n}\n\nexport const store = <T extends object>(initial: T, options?: { name?: string }): Store<T> => {\n if (typeof initial !== 'object' || initial === null || Array.isArray(initial)) {\n throw new RippleInvalidStoreError('store() requires a plain object initial state.');\n }\n\n // Same guard as patch()/replace() — reject an unsafe top-level key (e.g. a real\n // own \"__proto__\" property from JSON.parse) before it ever enters store state.\n for (const key of Object.keys(initial)) assertSafeKey(key);\n\n return new StoreImpl(initial, options?.name) as unknown as Store<T>;\n};\n"],"mappings":"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"}
|
package/dist/tracking.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tracking.cjs","names":[],"sources":["../src/tracking.ts"],"sourcesContent":["import type { ComputedBase, ReactiveBase } from './reactive-base';\nimport type { CleanupFn, Subscriber } from './types';\n\n// ── Global revision clock ─────────────────────────────────────────────────────\n//\n// Monotonically-increasing counter incremented on every signal write.\n// ComputedImpl records the clock value after each successful recompute and uses\n// it as an O(1) \"nothing has changed globally\" fast-path check in refreshIfDirty.\n\nlet globalRevision = 0;\n\nexport const tickRevision = (): number => ++globalRevision;\nexport const getRevision = (): number => globalRevision;\n\n// ── Internal types ────────────────────────────────────────────────────────────\n\nexport type DepEntry = {\n source: ReactiveBase<unknown>;\n version: number;\n};\n\n/** Called by trackSource to observe each reactive source accessed during a tracked run. */\nexport type SourceObserver = (source: ReactiveBase<unknown>) => void;\n\n// TrackingCtx carries both dep-collection state AND an optional sourceObserver.\n// Putting the observer inside the context (R12) scopes it to the specific effect/computed\n// run rather than making it a process-wide global — nested computed recomputes use their\n// own context (no observer), so the debugEffect pattern works without identity checks.\n// sourceObserver is shared across both branches and lives outside the discriminant union.\nexport type TrackingCtx = {\n sourceObserver?: SourceObserver;\n} & (\n | {\n computed: ComputedBase<unknown>;\n depCollector: DepEntry[];\n kind: 'computed';\n }\n | {\n cleanups: CleanupFn[];\n deps: Map<ReactiveBase<unknown>, number>;\n effect: Subscriber;\n kind: 'effect';\n subscriptions: Set<CleanupFn>;\n }\n);\n\n// ── Scheduling state ────────────────────────────────────────────────────────\n//\n// Colocated here (rather than in scheduling.ts) so it can be a field of\n// ExecutionContext without a runtime circular import between tracking.ts and\n// scheduling.ts — scheduling.ts only needs `getSchedulingState()` and the\n// `SchedulingState` type (type-only import, erased at compile time).\n//\n// Isolating this per-request (via the SSR hook) closes the documented gap where\n// concurrent SSR requests share one flush queue: `batchDepth`, `pendingSubscribers`,\n// and the dirty-computed sets are the only scheduling fields that can observably\n// leak across requests (they persist across `await` boundaries via `batch()`\n// spanning shared module-level signals). The propagation epoch/stack in\n// scheduling.ts stay module-level scratch memory — they're only touched within a\n// single synchronous call stack, which JS's run-to-completion model already isolates.\n\nexport type SchedulingState = {\n activeDirty: 'a' | 'b';\n batchDepth: number;\n readonly dirtyWithEffectSubsA: Set<ComputedBase<unknown>>;\n readonly dirtyWithEffectSubsB: Set<ComputedBase<unknown>>;\n readonly pendingSubscribers: Set<Subscriber>;\n};\n\nexport const createSchedulingState = (): SchedulingState => ({\n activeDirty: 'a',\n batchDepth: 0,\n dirtyWithEffectSubsA: new Set(),\n dirtyWithEffectSubsB: new Set(),\n pendingSubscribers: new Set(),\n});\n\n// ── ExecutionContext ──────────────────────────────────────────────────────────\n//\n// Unified context replaces two separate globals (_tracking and _scopeStack).\n// Both the dep-tracking context and the innermost scope's cleanup array live here.\n// The SSR hook overrides the entire context per-request — fixing the prior bug\n// where scope cleanups were always a process-wide singleton even with the hook.\n\nexport type ExecutionContext = {\n readonly scheduling: SchedulingState;\n readonly scopeCleanups: CleanupFn[] | null;\n readonly tracking: TrackingCtx | null;\n};\n\nexport type ContextHook = {\n get(): ExecutionContext;\n run<T>(ctx: ExecutionContext, fn: () => T): T;\n};\n\nlet _ctx: ExecutionContext = { scheduling: createSchedulingState(), scopeCleanups: null, tracking: null };\nlet _hook: ContextHook | null = null;\n\nconst getCtx = (): ExecutionContext => (_hook !== null ? _hook.get() : _ctx);\n\n/** Returns the scheduling-state bucket for the active execution context (module-level singleton outside SSR). */\nexport const getSchedulingState = (): SchedulingState => getCtx().scheduling;\n\n/** `true` once an SSR context hook is installed — scheduling state is then request-isolated. */\nexport const hasContextHook = (): boolean => _hook !== null;\n\n// ── Tracking ──────────────────────────────────────────────────────────────────\n\nexport const getTracking = (): TrackingCtx | null => getCtx().tracking;\n\nexport const withTracking = <T>(tracking: TrackingCtx | null, fn: () => T): T => {\n if (_hook !== null) {\n const current = _hook.get();\n\n return _hook.run({ ...current, tracking }, fn);\n }\n\n const prev = _ctx;\n\n _ctx = { ..._ctx, tracking };\n\n try {\n return fn();\n } finally {\n _ctx = prev;\n }\n};\n\n// ── Scope cleanup ─────────────────────────────────────────────────────────────\n\n/**\n * Pushes `cleanups` as the active scope cleanup array for the duration of `fn`.\n * Any `onCleanup()` calls inside `fn` will register into these cleanups.\n * Effects and computed values created inside `fn` are also auto-registered.\n */\nexport const withScopeCleanups = <T>(cleanups: CleanupFn[], fn: () => T): T => {\n if (_hook !== null) {\n const current = _hook.get();\n\n return _hook.run({ ...current, scopeCleanups: cleanups }, fn);\n }\n\n const prev = _ctx;\n\n _ctx = { ..._ctx, scopeCleanups: cleanups };\n\n try {\n return fn();\n } finally {\n _ctx = prev;\n }\n};\n\n/**\n * Returns the cleanup array of the innermost active scope, or `null` if not inside a scope.\n */\nexport const getScopeCleanups = (): CleanupFn[] | null => getCtx().scopeCleanups;\n\n// ── Auto-disposal ───────────────────────────────────────────────────────────────\n//\n// Shared by every primitive that ties its own lifetime to the context it was\n// created in (computed(), resource()): dispose when the enclosing effect re-runs\n// or disposes, or — if not inside an effect — when the enclosing scope disposes.\n// No-op if neither is active (the caller owns disposal directly).\n\n/**\n * Registers `dispose` to run when the enclosing effect re-runs/disposes, or —\n * if there is no enclosing effect — when the enclosing scope disposes.\n */\nexport const autoRegisterDisposal = (dispose: CleanupFn): void => {\n const ctx = getTracking();\n\n if (ctx?.kind === 'effect') {\n ctx.cleanups.push(dispose);\n } else {\n getScopeCleanups()?.push(dispose);\n }\n};\n\n// ── Context hook (for SSR) ────────────────────────────────────────────────────\n\n/**\n * @internal Used only by `/ripple/ssr`.\n * Installs a context hook that overrides both tracking and scope-cleanup access.\n * Returns the previous hook so callers can restore it.\n */\nexport const _installContextHook = (hook: ContextHook | null): ContextHook | null => {\n const prev = _hook;\n\n _hook = hook;\n\n return prev;\n};\n\n// ── Source observer ───────────────────────────────────────────────────────────\n//\n// Observer is now scoped to the active TrackingCtx instead of a process-wide global.\n// This means it only fires for direct deps of the current effect/computed — nested\n// computed recomputes use their own context (no observer) so no identity check needed.\n\n/**\n * Runs `fn` while calling `observer` for every reactive source directly accessed.\n * Used by `debugEffect` to detect which sources changed between runs.\n * No-op if called outside an active tracking context.\n */\nexport const withSourceObserver = <T>(observer: SourceObserver, fn: () => T): T => {\n const current = getTracking();\n\n if (current === null) return fn();\n\n return withTracking({ ...current, sourceObserver: observer }, fn);\n};\n\n// ── untrack ───────────────────────────────────────────────────────────────────\n\n/**\n * Runs `fn` without recording any reactive reads as dependencies of the enclosing\n * effect or computed. Useful when you need to read reactive state \"silently\".\n *\n * @example\n * ```ts\n * effect(() => {\n * const a = count.value; // tracked\n * const b = untrack(() => name.value); // NOT tracked — no re-run when name changes\n * });\n * ```\n */\nexport const untrack = <T>(fn: () => T): T => withTracking(null, fn);\n\n// ── trackSource ───────────────────────────────────────────────────────────────\n//\n// Records `source` as a dependency of the currently active tracking context.\n// For computed contexts: only records the dep entry — addComputedSub is called\n// later by updateDeps() in computed.ts (only for genuinely NEW deps, preventing\n// duplicate WeakRef entries).\n// For effect contexts: immediately subscribes the effect to the source and\n// records the dep for getDependencies().\n\nexport const trackSource = (source: ReactiveBase<unknown>): void => {\n const ctx = getTracking();\n\n if (ctx === null) return;\n\n ctx.sourceObserver?.(source);\n\n if (ctx.kind === 'computed') {\n ctx.depCollector.push({ source, version: source.version });\n } else if (ctx.kind === 'effect') {\n const owner = ctx.effect;\n\n source.addEffectSub(owner);\n ctx.subscriptions.add(() => source.removeEffectSub(owner));\n ctx.deps.set(source, source.version);\n }\n};\n"],"mappings":"AASA,IAAI,EAAiB,EAER,MAA6B,EAAE,EAC/B,MAA4B,EAyD5B,OAAgD,CAC3D,YAAa,IACb,WAAY,EACZ,qBAAsB,IAAI,IAC1B,qBAAsB,IAAI,IAC1B,mBAAoB,IAAI,GAC1B,GAoBI,EAAyB,CAAE,WAAY,EAAsB,EAAG,cAAe,KAAM,SAAU,IAAK,EACpG,EAA4B,KAE1B,MAAkC,IAAU,KAAqB,EAAd,EAAM,IAAI,EAGtD,MAA4C,EAAO,CAAC,CAAC,WAGrD,MAAgC,IAAU,KAI1C,MAAwC,EAAO,CAAC,CAAC,SAEjD,GAAmB,EAA8B,IAAmB,CAC/E,GAAI,IAAU,KAAM,CAClB,IAAM,EAAU,EAAM,IAAI,EAE1B,OAAO,EAAM,IAAI,CAAE,GAAG,EAAS,UAAS,EAAG,CAAE,CAC/C,CAEA,IAAM,EAAO,EAEb,EAAO,CAAE,GAAG,EAAM,UAAS,EAE3B,GAAI,CACF,OAAO,EAAG,CACZ,QAAU,CACR,EAAO,CACT,CACF,EASa,GAAwB,EAAuB,IAAmB,CAC7E,GAAI,IAAU,KAAM,CAClB,IAAM,EAAU,EAAM,IAAI,EAE1B,OAAO,EAAM,IAAI,CAAE,GAAG,EAAS,cAAe,CAAS,EAAG,CAAE,CAC9D,CAEA,IAAM,EAAO,EAEb,EAAO,CAAE,GAAG,EAAM,cAAe,CAAS,EAE1C,GAAI,CACF,OAAO,EAAG,CACZ,QAAU,CACR,EAAO,CACT,CACF,EAKa,MAA6C,EAAO,CAAC,CAAC,cAatD,EAAwB,GAA6B,CAChE,IAAM,EAAM,EAAY,EAEpB,GAAK,OAAS,SAChB,EAAI,SAAS,KAAK,CAAO,EAEzB,EAAiB,CAAC,EAAE,KAAK,CAAO,CAEpC,EASa,EAAuB,GAAiD,CACnF,IAAM,EAAO,EAIb,MAFA,GAAQ,EAED,CACT,EAaa,GAAyB,EAA0B,IAAmB,CACjF,IAAM,EAAU,EAAY,EAI5B,OAFI,IAAY,KAAa,EAAG,EAEzB,EAAa,CAAE,GAAG,EAAS,eAAgB,CAAS,EAAG,CAAE,CAClE,EAgBa,EAAc,GAAmB,EAAa,KAAM,CAAE,EAWtD,EAAe,GAAwC,CAClE,IAAM,EAAM,EAAY,EAEpB,OAAQ,KAIZ,IAFA,EAAI,iBAAiB,CAAM,EAEvB,EAAI,OAAS,WACf,EAAI,aAAa,KAAK,CAAE,SAAQ,QAAS,EAAO,OAAQ,CAAC,OACpD,GAAI,EAAI,OAAS,SAAU,CAChC,IAAM,EAAQ,EAAI,OAElB,EAAO,aAAa,CAAK,EACzB,EAAI,cAAc,QAAU,EAAO,gBAAgB,CAAK,CAAC,EACzD,EAAI,KAAK,IAAI,EAAQ,EAAO,OAAO,CACrC,EACF"}
|
|
1
|
+
{"version":3,"file":"tracking.cjs","names":[],"sources":["../src/tracking.ts"],"sourcesContent":["import type { ComputedBase, ReactiveBase } from './reactive-base';\nimport type { CleanupFn, Subscriber } from './types';\n\n// ── Global revision clock ─────────────────────────────────────────────────────\n//\n// Monotonically-increasing counter incremented on every signal write.\n// ComputedImpl records the clock value after each successful recompute and uses\n// it as an O(1) \"nothing has changed globally\" fast-path check in refreshIfDirty.\n\nlet globalRevision = 0;\n\nexport const tickRevision = (): number => ++globalRevision;\nexport const getRevision = (): number => globalRevision;\n\n// ── Internal types ────────────────────────────────────────────────────────────\n\nexport type DepEntry = {\n source: ReactiveBase<unknown>;\n version: number;\n};\n\n/** Called by trackSource to observe each reactive source accessed during a tracked run. */\nexport type SourceObserver = (source: ReactiveBase<unknown>) => void;\n\n// TrackingCtx carries both dep-collection state AND an optional sourceObserver.\n// Putting the observer inside the context (R12) scopes it to the specific effect/computed\n// run rather than making it a process-wide global — nested computed recomputes use their\n// own context (no observer), so the debugEffect pattern works without identity checks.\n// sourceObserver is shared across both branches and lives outside the discriminant union.\nexport type TrackingCtx = {\n sourceObserver?: SourceObserver;\n} & (\n | {\n computed: ComputedBase<unknown>;\n depCollector: DepEntry[];\n kind: 'computed';\n }\n | {\n cleanups: CleanupFn[];\n deps: Map<ReactiveBase<unknown>, number>;\n effect: Subscriber;\n kind: 'effect';\n subscriptions: Set<CleanupFn>;\n }\n);\n\n// ── Scheduling state ────────────────────────────────────────────────────────\n//\n// Colocated here (rather than in scheduling.ts) so it can be a field of\n// ExecutionContext without a runtime circular import between tracking.ts and\n// scheduling.ts — scheduling.ts only needs `getSchedulingState()` and the\n// `SchedulingState` type (type-only import, erased at compile time).\n//\n// Isolating this per-request (via the SSR hook) closes the documented gap where\n// concurrent SSR requests share one flush queue: `batchDepth`, `pendingSubscribers`,\n// and the dirty-computed sets are the only scheduling fields that can observably\n// leak across requests (they persist across `await` boundaries via `batch()`\n// spanning shared module-level signals). The propagation epoch/stack in\n// scheduling.ts stay module-level scratch memory — they're only touched within a\n// single synchronous call stack, which JS's run-to-completion model already isolates.\n\nexport type SchedulingState = {\n activeDirty: 'a' | 'b';\n batchDepth: number;\n readonly dirtyWithEffectSubsA: Set<ComputedBase<unknown>>;\n readonly dirtyWithEffectSubsB: Set<ComputedBase<unknown>>;\n readonly pendingSubscribers: Set<Subscriber>;\n};\n\nexport const createSchedulingState = (): SchedulingState => ({\n activeDirty: 'a',\n batchDepth: 0,\n dirtyWithEffectSubsA: new Set(),\n dirtyWithEffectSubsB: new Set(),\n pendingSubscribers: new Set(),\n});\n\n// ── ExecutionContext ──────────────────────────────────────────────────────────\n//\n// Unified context replaces two separate globals (_tracking and _scopeStack).\n// Both the dep-tracking context and the innermost scope's cleanup array live here.\n// The SSR hook overrides the entire context per-request — fixing the prior bug\n// where scope cleanups were always a process-wide singleton even with the hook.\n\nexport type ExecutionContext = {\n readonly scheduling: SchedulingState;\n readonly scopeCleanups: CleanupFn[] | null;\n readonly tracking: TrackingCtx | null;\n};\n\nexport type ContextHook = {\n get(): ExecutionContext;\n run<T>(ctx: ExecutionContext, fn: () => T): T;\n};\n\nlet _ctx: ExecutionContext = { scheduling: createSchedulingState(), scopeCleanups: null, tracking: null };\nlet _hook: ContextHook | null = null;\n\nconst getCtx = (): ExecutionContext => (_hook !== null ? _hook.get() : _ctx);\n\n/** Returns the scheduling-state bucket for the active execution context (module-level singleton outside SSR). */\nexport const getSchedulingState = (): SchedulingState => getCtx().scheduling;\n\n/** `true` once an SSR context hook is installed — scheduling state is then request-isolated. */\nexport const hasContextHook = (): boolean => _hook !== null;\n\n// ── Tracking ──────────────────────────────────────────────────────────────────\n\nexport const getTracking = (): TrackingCtx | null => getCtx().tracking;\n\nexport const withTracking = <T>(tracking: TrackingCtx | null, fn: () => T): T => {\n if (_hook !== null) {\n const current = _hook.get();\n\n return _hook.run({ ...current, tracking }, fn);\n }\n\n const prev = _ctx;\n\n _ctx = { ..._ctx, tracking };\n\n try {\n return fn();\n } finally {\n _ctx = prev;\n }\n};\n\n// ── Scope cleanup ─────────────────────────────────────────────────────────────\n\n/**\n * Pushes `cleanups` as the active scope cleanup array for the duration of `fn`.\n * Any `onCleanup()` calls inside `fn` will register into these cleanups.\n * Effects and computed values created inside `fn` are also auto-registered.\n */\nexport const withScopeCleanups = <T>(cleanups: CleanupFn[], fn: () => T): T => {\n if (_hook !== null) {\n const current = _hook.get();\n\n return _hook.run({ ...current, scopeCleanups: cleanups }, fn);\n }\n\n const prev = _ctx;\n\n _ctx = { ..._ctx, scopeCleanups: cleanups };\n\n try {\n return fn();\n } finally {\n _ctx = prev;\n }\n};\n\n/**\n * Returns the cleanup array of the innermost active scope, or `null` if not inside a scope.\n */\nexport const getScopeCleanups = (): CleanupFn[] | null => getCtx().scopeCleanups;\n\n// ── Auto-disposal ───────────────────────────────────────────────────────────────\n//\n// Shared by every primitive that ties its own lifetime to the context it was\n// created in (computed(), resource()): dispose when the enclosing effect re-runs\n// or disposes, or — if not inside an effect — when the enclosing scope disposes.\n// No-op if neither is active (the caller owns disposal directly).\n//\n// Why signal() and store() don't call this: computed()/resource() are *derived*\n// computations — recreating one on every effect re-run is normal and cheap, so\n// tying their lifetime to the enclosing context avoids leaking a fresh graph\n// node per run. signal()/store() hold raw, caller-owned state that is typically\n// created once and read across many effect re-runs; auto-disposing it on the\n// first re-run would silently break any effect that reads a signal it declared\n// itself. Callers who genuinely want scope-scoped state should use\n// `scope.add(() => mySignal.dispose())` explicitly.\n\n/**\n * Registers `dispose` to run when the enclosing effect re-runs/disposes, or —\n * if there is no enclosing effect — when the enclosing scope disposes.\n */\nexport const autoRegisterDisposal = (dispose: CleanupFn): void => {\n const ctx = getTracking();\n\n if (ctx?.kind === 'effect') {\n ctx.cleanups.push(dispose);\n } else {\n getScopeCleanups()?.push(dispose);\n }\n};\n\n// ── Context hook (for SSR) ────────────────────────────────────────────────────\n\n/**\n * @internal Used only by `/ripple/ssr`.\n * Installs a context hook that overrides both tracking and scope-cleanup access.\n * Returns the previous hook so callers can restore it.\n */\nexport const _installContextHook = (hook: ContextHook | null): ContextHook | null => {\n const prev = _hook;\n\n _hook = hook;\n\n return prev;\n};\n\n// ── Source observer ───────────────────────────────────────────────────────────\n//\n// Observer is now scoped to the active TrackingCtx instead of a process-wide global.\n// This means it only fires for direct deps of the current effect/computed — nested\n// computed recomputes use their own context (no observer) so no identity check needed.\n\n/**\n * Runs `fn` while calling `observer` for every reactive source directly accessed.\n * Used by `debugEffect` to detect which sources changed between runs.\n * No-op if called outside an active tracking context.\n */\nexport const withSourceObserver = <T>(observer: SourceObserver, fn: () => T): T => {\n const current = getTracking();\n\n if (current === null) return fn();\n\n return withTracking({ ...current, sourceObserver: observer }, fn);\n};\n\n// ── untrack ───────────────────────────────────────────────────────────────────\n\n/**\n * Runs `fn` without recording any reactive reads as dependencies of the enclosing\n * effect or computed. Useful when you need to read reactive state \"silently\".\n *\n * @example\n * ```ts\n * effect(() => {\n * const a = count.value; // tracked\n * const b = untrack(() => name.value); // NOT tracked — no re-run when name changes\n * });\n * ```\n */\nexport const untrack = <T>(fn: () => T): T => withTracking(null, fn);\n\n// ── trackSource ───────────────────────────────────────────────────────────────\n//\n// Records `source` as a dependency of the currently active tracking context.\n// For computed contexts: only records the dep entry — addComputedSub is called\n// later by updateDeps() in computed.ts (only for genuinely NEW deps, preventing\n// duplicate WeakRef entries).\n// For effect contexts: immediately subscribes the effect to the source and\n// records the dep for getDependencies().\n\nexport const trackSource = (source: ReactiveBase<unknown>): void => {\n const ctx = getTracking();\n\n if (ctx === null) return;\n\n ctx.sourceObserver?.(source);\n\n if (ctx.kind === 'computed') {\n ctx.depCollector.push({ source, version: source.version });\n } else if (ctx.kind === 'effect') {\n const owner = ctx.effect;\n\n source.addEffectSub(owner);\n ctx.subscriptions.add(() => source.removeEffectSub(owner));\n ctx.deps.set(source, source.version);\n }\n};\n"],"mappings":"AASA,IAAI,EAAiB,EAER,MAA6B,EAAE,EAC/B,MAA4B,EAyD5B,OAAgD,CAC3D,YAAa,IACb,WAAY,EACZ,qBAAsB,IAAI,IAC1B,qBAAsB,IAAI,IAC1B,mBAAoB,IAAI,GAC1B,GAoBI,EAAyB,CAAE,WAAY,EAAsB,EAAG,cAAe,KAAM,SAAU,IAAK,EACpG,EAA4B,KAE1B,MAAkC,IAAU,KAAqB,EAAd,EAAM,IAAI,EAGtD,MAA4C,EAAO,CAAC,CAAC,WAGrD,MAAgC,IAAU,KAI1C,MAAwC,EAAO,CAAC,CAAC,SAEjD,GAAmB,EAA8B,IAAmB,CAC/E,GAAI,IAAU,KAAM,CAClB,IAAM,EAAU,EAAM,IAAI,EAE1B,OAAO,EAAM,IAAI,CAAE,GAAG,EAAS,UAAS,EAAG,CAAE,CAC/C,CAEA,IAAM,EAAO,EAEb,EAAO,CAAE,GAAG,EAAM,UAAS,EAE3B,GAAI,CACF,OAAO,EAAG,CACZ,QAAU,CACR,EAAO,CACT,CACF,EASa,GAAwB,EAAuB,IAAmB,CAC7E,GAAI,IAAU,KAAM,CAClB,IAAM,EAAU,EAAM,IAAI,EAE1B,OAAO,EAAM,IAAI,CAAE,GAAG,EAAS,cAAe,CAAS,EAAG,CAAE,CAC9D,CAEA,IAAM,EAAO,EAEb,EAAO,CAAE,GAAG,EAAM,cAAe,CAAS,EAE1C,GAAI,CACF,OAAO,EAAG,CACZ,QAAU,CACR,EAAO,CACT,CACF,EAKa,MAA6C,EAAO,CAAC,CAAC,cAsBtD,EAAwB,GAA6B,CAChE,IAAM,EAAM,EAAY,EAEpB,GAAK,OAAS,SAChB,EAAI,SAAS,KAAK,CAAO,EAEzB,EAAiB,CAAC,EAAE,KAAK,CAAO,CAEpC,EASa,EAAuB,GAAiD,CACnF,IAAM,EAAO,EAIb,MAFA,GAAQ,EAED,CACT,EAaa,GAAyB,EAA0B,IAAmB,CACjF,IAAM,EAAU,EAAY,EAI5B,OAFI,IAAY,KAAa,EAAG,EAEzB,EAAa,CAAE,GAAG,EAAS,eAAgB,CAAS,EAAG,CAAE,CAClE,EAgBa,EAAc,GAAmB,EAAa,KAAM,CAAE,EAWtD,EAAe,GAAwC,CAClE,IAAM,EAAM,EAAY,EAEpB,OAAQ,KAIZ,IAFA,EAAI,iBAAiB,CAAM,EAEvB,EAAI,OAAS,WACf,EAAI,aAAa,KAAK,CAAE,SAAQ,QAAS,EAAO,OAAQ,CAAC,OACpD,GAAI,EAAI,OAAS,SAAU,CAChC,IAAM,EAAQ,EAAI,OAElB,EAAO,aAAa,CAAK,EACzB,EAAI,cAAc,QAAU,EAAO,gBAAgB,CAAK,CAAC,EACzD,EAAI,KAAK,IAAI,EAAQ,EAAO,OAAO,CACrC,EACF"}
|
package/dist/tracking.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tracking.d.ts","sourceRoot":"","sources":["../src/tracking.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAClE,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAUrD,eAAO,MAAM,YAAY,QAAO,MAA0B,CAAC;AAC3D,eAAO,MAAM,WAAW,QAAO,MAAwB,CAAC;AAIxD,MAAM,MAAM,QAAQ,GAAG;IACrB,MAAM,EAAE,YAAY,CAAC,OAAO,CAAC,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,2FAA2F;AAC3F,MAAM,MAAM,cAAc,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;AAOrE,MAAM,MAAM,WAAW,GAAG;IACxB,cAAc,CAAC,EAAE,cAAc,CAAC;CACjC,GAAG,CACA;IACE,QAAQ,EAAE,YAAY,CAAC,OAAO,CAAC,CAAC;IAChC,YAAY,EAAE,QAAQ,EAAE,CAAC;IACzB,IAAI,EAAE,UAAU,CAAC;CAClB,GACD;IACE,QAAQ,EAAE,SAAS,EAAE,CAAC;IACtB,IAAI,EAAE,GAAG,CAAC,YAAY,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,CAAC;IACzC,MAAM,EAAE,UAAU,CAAC;IACnB,IAAI,EAAE,QAAQ,CAAC;IACf,aAAa,EAAE,GAAG,CAAC,SAAS,CAAC,CAAC;CAC/B,CACJ,CAAC;AAiBF,MAAM,MAAM,eAAe,GAAG;IAC5B,WAAW,EAAE,GAAG,GAAG,GAAG,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,oBAAoB,EAAE,GAAG,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC;IAC1D,QAAQ,CAAC,oBAAoB,EAAE,GAAG,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC;IAC1D,QAAQ,CAAC,kBAAkB,EAAE,GAAG,CAAC,UAAU,CAAC,CAAC;CAC9C,CAAC;AAEF,eAAO,MAAM,qBAAqB,QAAO,eAMvC,CAAC;AASH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,UAAU,EAAE,eAAe,CAAC;IACrC,QAAQ,CAAC,aAAa,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC;IAC3C,QAAQ,CAAC,QAAQ,EAAE,WAAW,GAAG,IAAI,CAAC;CACvC,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,GAAG,IAAI,gBAAgB,CAAC;IACxB,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,gBAAgB,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;CAC/C,CAAC;AAOF,iHAAiH;AACjH,eAAO,MAAM,kBAAkB,QAAO,eAAsC,CAAC;AAE7E,gGAAgG;AAChG,eAAO,MAAM,cAAc,QAAO,OAAyB,CAAC;AAI5D,eAAO,MAAM,WAAW,QAAO,WAAW,GAAG,IAAyB,CAAC;AAEvE,eAAO,MAAM,YAAY,GAAI,CAAC,EAAE,UAAU,WAAW,GAAG,IAAI,EAAE,IAAI,MAAM,CAAC,KAAG,CAgB3E,CAAC;AAIF;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAAI,CAAC,EAAE,UAAU,SAAS,EAAE,EAAE,IAAI,MAAM,CAAC,KAAG,CAgBzE,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,gBAAgB,QAAO,SAAS,EAAE,GAAG,IAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"tracking.d.ts","sourceRoot":"","sources":["../src/tracking.ts"],"names":[],"mappings":"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"}
|
package/dist/tracking.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tracking.js","names":[],"sources":["../src/tracking.ts"],"sourcesContent":["import type { ComputedBase, ReactiveBase } from './reactive-base';\nimport type { CleanupFn, Subscriber } from './types';\n\n// ── Global revision clock ─────────────────────────────────────────────────────\n//\n// Monotonically-increasing counter incremented on every signal write.\n// ComputedImpl records the clock value after each successful recompute and uses\n// it as an O(1) \"nothing has changed globally\" fast-path check in refreshIfDirty.\n\nlet globalRevision = 0;\n\nexport const tickRevision = (): number => ++globalRevision;\nexport const getRevision = (): number => globalRevision;\n\n// ── Internal types ────────────────────────────────────────────────────────────\n\nexport type DepEntry = {\n source: ReactiveBase<unknown>;\n version: number;\n};\n\n/** Called by trackSource to observe each reactive source accessed during a tracked run. */\nexport type SourceObserver = (source: ReactiveBase<unknown>) => void;\n\n// TrackingCtx carries both dep-collection state AND an optional sourceObserver.\n// Putting the observer inside the context (R12) scopes it to the specific effect/computed\n// run rather than making it a process-wide global — nested computed recomputes use their\n// own context (no observer), so the debugEffect pattern works without identity checks.\n// sourceObserver is shared across both branches and lives outside the discriminant union.\nexport type TrackingCtx = {\n sourceObserver?: SourceObserver;\n} & (\n | {\n computed: ComputedBase<unknown>;\n depCollector: DepEntry[];\n kind: 'computed';\n }\n | {\n cleanups: CleanupFn[];\n deps: Map<ReactiveBase<unknown>, number>;\n effect: Subscriber;\n kind: 'effect';\n subscriptions: Set<CleanupFn>;\n }\n);\n\n// ── Scheduling state ────────────────────────────────────────────────────────\n//\n// Colocated here (rather than in scheduling.ts) so it can be a field of\n// ExecutionContext without a runtime circular import between tracking.ts and\n// scheduling.ts — scheduling.ts only needs `getSchedulingState()` and the\n// `SchedulingState` type (type-only import, erased at compile time).\n//\n// Isolating this per-request (via the SSR hook) closes the documented gap where\n// concurrent SSR requests share one flush queue: `batchDepth`, `pendingSubscribers`,\n// and the dirty-computed sets are the only scheduling fields that can observably\n// leak across requests (they persist across `await` boundaries via `batch()`\n// spanning shared module-level signals). The propagation epoch/stack in\n// scheduling.ts stay module-level scratch memory — they're only touched within a\n// single synchronous call stack, which JS's run-to-completion model already isolates.\n\nexport type SchedulingState = {\n activeDirty: 'a' | 'b';\n batchDepth: number;\n readonly dirtyWithEffectSubsA: Set<ComputedBase<unknown>>;\n readonly dirtyWithEffectSubsB: Set<ComputedBase<unknown>>;\n readonly pendingSubscribers: Set<Subscriber>;\n};\n\nexport const createSchedulingState = (): SchedulingState => ({\n activeDirty: 'a',\n batchDepth: 0,\n dirtyWithEffectSubsA: new Set(),\n dirtyWithEffectSubsB: new Set(),\n pendingSubscribers: new Set(),\n});\n\n// ── ExecutionContext ──────────────────────────────────────────────────────────\n//\n// Unified context replaces two separate globals (_tracking and _scopeStack).\n// Both the dep-tracking context and the innermost scope's cleanup array live here.\n// The SSR hook overrides the entire context per-request — fixing the prior bug\n// where scope cleanups were always a process-wide singleton even with the hook.\n\nexport type ExecutionContext = {\n readonly scheduling: SchedulingState;\n readonly scopeCleanups: CleanupFn[] | null;\n readonly tracking: TrackingCtx | null;\n};\n\nexport type ContextHook = {\n get(): ExecutionContext;\n run<T>(ctx: ExecutionContext, fn: () => T): T;\n};\n\nlet _ctx: ExecutionContext = { scheduling: createSchedulingState(), scopeCleanups: null, tracking: null };\nlet _hook: ContextHook | null = null;\n\nconst getCtx = (): ExecutionContext => (_hook !== null ? _hook.get() : _ctx);\n\n/** Returns the scheduling-state bucket for the active execution context (module-level singleton outside SSR). */\nexport const getSchedulingState = (): SchedulingState => getCtx().scheduling;\n\n/** `true` once an SSR context hook is installed — scheduling state is then request-isolated. */\nexport const hasContextHook = (): boolean => _hook !== null;\n\n// ── Tracking ──────────────────────────────────────────────────────────────────\n\nexport const getTracking = (): TrackingCtx | null => getCtx().tracking;\n\nexport const withTracking = <T>(tracking: TrackingCtx | null, fn: () => T): T => {\n if (_hook !== null) {\n const current = _hook.get();\n\n return _hook.run({ ...current, tracking }, fn);\n }\n\n const prev = _ctx;\n\n _ctx = { ..._ctx, tracking };\n\n try {\n return fn();\n } finally {\n _ctx = prev;\n }\n};\n\n// ── Scope cleanup ─────────────────────────────────────────────────────────────\n\n/**\n * Pushes `cleanups` as the active scope cleanup array for the duration of `fn`.\n * Any `onCleanup()` calls inside `fn` will register into these cleanups.\n * Effects and computed values created inside `fn` are also auto-registered.\n */\nexport const withScopeCleanups = <T>(cleanups: CleanupFn[], fn: () => T): T => {\n if (_hook !== null) {\n const current = _hook.get();\n\n return _hook.run({ ...current, scopeCleanups: cleanups }, fn);\n }\n\n const prev = _ctx;\n\n _ctx = { ..._ctx, scopeCleanups: cleanups };\n\n try {\n return fn();\n } finally {\n _ctx = prev;\n }\n};\n\n/**\n * Returns the cleanup array of the innermost active scope, or `null` if not inside a scope.\n */\nexport const getScopeCleanups = (): CleanupFn[] | null => getCtx().scopeCleanups;\n\n// ── Auto-disposal ───────────────────────────────────────────────────────────────\n//\n// Shared by every primitive that ties its own lifetime to the context it was\n// created in (computed(), resource()): dispose when the enclosing effect re-runs\n// or disposes, or — if not inside an effect — when the enclosing scope disposes.\n// No-op if neither is active (the caller owns disposal directly).\n\n/**\n * Registers `dispose` to run when the enclosing effect re-runs/disposes, or —\n * if there is no enclosing effect — when the enclosing scope disposes.\n */\nexport const autoRegisterDisposal = (dispose: CleanupFn): void => {\n const ctx = getTracking();\n\n if (ctx?.kind === 'effect') {\n ctx.cleanups.push(dispose);\n } else {\n getScopeCleanups()?.push(dispose);\n }\n};\n\n// ── Context hook (for SSR) ────────────────────────────────────────────────────\n\n/**\n * @internal Used only by `/ripple/ssr`.\n * Installs a context hook that overrides both tracking and scope-cleanup access.\n * Returns the previous hook so callers can restore it.\n */\nexport const _installContextHook = (hook: ContextHook | null): ContextHook | null => {\n const prev = _hook;\n\n _hook = hook;\n\n return prev;\n};\n\n// ── Source observer ───────────────────────────────────────────────────────────\n//\n// Observer is now scoped to the active TrackingCtx instead of a process-wide global.\n// This means it only fires for direct deps of the current effect/computed — nested\n// computed recomputes use their own context (no observer) so no identity check needed.\n\n/**\n * Runs `fn` while calling `observer` for every reactive source directly accessed.\n * Used by `debugEffect` to detect which sources changed between runs.\n * No-op if called outside an active tracking context.\n */\nexport const withSourceObserver = <T>(observer: SourceObserver, fn: () => T): T => {\n const current = getTracking();\n\n if (current === null) return fn();\n\n return withTracking({ ...current, sourceObserver: observer }, fn);\n};\n\n// ── untrack ───────────────────────────────────────────────────────────────────\n\n/**\n * Runs `fn` without recording any reactive reads as dependencies of the enclosing\n * effect or computed. Useful when you need to read reactive state \"silently\".\n *\n * @example\n * ```ts\n * effect(() => {\n * const a = count.value; // tracked\n * const b = untrack(() => name.value); // NOT tracked — no re-run when name changes\n * });\n * ```\n */\nexport const untrack = <T>(fn: () => T): T => withTracking(null, fn);\n\n// ── trackSource ───────────────────────────────────────────────────────────────\n//\n// Records `source` as a dependency of the currently active tracking context.\n// For computed contexts: only records the dep entry — addComputedSub is called\n// later by updateDeps() in computed.ts (only for genuinely NEW deps, preventing\n// duplicate WeakRef entries).\n// For effect contexts: immediately subscribes the effect to the source and\n// records the dep for getDependencies().\n\nexport const trackSource = (source: ReactiveBase<unknown>): void => {\n const ctx = getTracking();\n\n if (ctx === null) return;\n\n ctx.sourceObserver?.(source);\n\n if (ctx.kind === 'computed') {\n ctx.depCollector.push({ source, version: source.version });\n } else if (ctx.kind === 'effect') {\n const owner = ctx.effect;\n\n source.addEffectSub(owner);\n ctx.subscriptions.add(() => source.removeEffectSub(owner));\n ctx.deps.set(source, source.version);\n }\n};\n"],"mappings":";AASA,IAAI,IAAiB,GAER,UAA6B,EAAE,GAC/B,UAA4B,GAyD5B,WAAgD;CAC3D,aAAa;CACb,YAAY;CACZ,sCAAsB,IAAI,IAAI;CAC9B,sCAAsB,IAAI,IAAI;CAC9B,oCAAoB,IAAI,IAAI;AAC9B,IAoBI,IAAyB;CAAE,YAAY,EAAsB;CAAG,eAAe;CAAM,UAAU;AAAK,GACpG,IAA4B,MAE1B,UAAkC,MAAU,OAAqB,IAAd,EAAM,IAAI,GAGtD,UAA4C,EAAO,CAAC,CAAC,YAGrD,UAAgC,MAAU,MAI1C,UAAwC,EAAO,CAAC,CAAC,UAEjD,KAAmB,GAA8B,MAAmB;CAC/E,IAAI,MAAU,MAAM;EAClB,IAAM,IAAU,EAAM,IAAI;EAE1B,OAAO,EAAM,IAAI;GAAE,GAAG;GAAS;EAAS,GAAG,CAAE;CAC/C;CAEA,IAAM,IAAO;CAEb,IAAO;EAAE,GAAG;EAAM;CAAS;CAE3B,IAAI;EACF,OAAO,EAAG;CACZ,UAAU;EACR,IAAO;CACT;AACF,GASa,KAAwB,GAAuB,MAAmB;CAC7E,IAAI,MAAU,MAAM;EAClB,IAAM,IAAU,EAAM,IAAI;EAE1B,OAAO,EAAM,IAAI;GAAE,GAAG;GAAS,eAAe;EAAS,GAAG,CAAE;CAC9D;CAEA,IAAM,IAAO;CAEb,IAAO;EAAE,GAAG;EAAM,eAAe;CAAS;CAE1C,IAAI;EACF,OAAO,EAAG;CACZ,UAAU;EACR,IAAO;CACT;AACF,GAKa,UAA6C,EAAO,CAAC,CAAC,eAatD,KAAwB,MAA6B;CAChE,IAAM,IAAM,EAAY;CAExB,AAAI,GAAK,SAAS,WAChB,EAAI,SAAS,KAAK,CAAO,IAEzB,EAAiB,CAAC,EAAE,KAAK,CAAO;AAEpC,GASa,KAAuB,MAAiD;CACnF,IAAM,IAAO;CAIb,OAFA,IAAQ,GAED;AACT,GAaa,KAAyB,GAA0B,MAAmB;CACjF,IAAM,IAAU,EAAY;CAI5B,OAFI,MAAY,OAAa,EAAG,IAEzB,EAAa;EAAE,GAAG;EAAS,gBAAgB;CAAS,GAAG,CAAE;AAClE,GAgBa,KAAc,MAAmB,EAAa,MAAM,CAAE,GAWtD,KAAe,MAAwC;CAClE,IAAM,IAAM,EAAY;CAEpB,UAAQ,MAIZ;MAFA,EAAI,iBAAiB,CAAM,GAEvB,EAAI,SAAS,YACf,EAAI,aAAa,KAAK;GAAE;GAAQ,SAAS,EAAO;EAAQ,CAAC;OACpD,IAAI,EAAI,SAAS,UAAU;GAChC,IAAM,IAAQ,EAAI;GAIlB,AAFA,EAAO,aAAa,CAAK,GACzB,EAAI,cAAc,UAAU,EAAO,gBAAgB,CAAK,CAAC,GACzD,EAAI,KAAK,IAAI,GAAQ,EAAO,OAAO;EACrC;;AACF"}
|
|
1
|
+
{"version":3,"file":"tracking.js","names":[],"sources":["../src/tracking.ts"],"sourcesContent":["import type { ComputedBase, ReactiveBase } from './reactive-base';\nimport type { CleanupFn, Subscriber } from './types';\n\n// ── Global revision clock ─────────────────────────────────────────────────────\n//\n// Monotonically-increasing counter incremented on every signal write.\n// ComputedImpl records the clock value after each successful recompute and uses\n// it as an O(1) \"nothing has changed globally\" fast-path check in refreshIfDirty.\n\nlet globalRevision = 0;\n\nexport const tickRevision = (): number => ++globalRevision;\nexport const getRevision = (): number => globalRevision;\n\n// ── Internal types ────────────────────────────────────────────────────────────\n\nexport type DepEntry = {\n source: ReactiveBase<unknown>;\n version: number;\n};\n\n/** Called by trackSource to observe each reactive source accessed during a tracked run. */\nexport type SourceObserver = (source: ReactiveBase<unknown>) => void;\n\n// TrackingCtx carries both dep-collection state AND an optional sourceObserver.\n// Putting the observer inside the context (R12) scopes it to the specific effect/computed\n// run rather than making it a process-wide global — nested computed recomputes use their\n// own context (no observer), so the debugEffect pattern works without identity checks.\n// sourceObserver is shared across both branches and lives outside the discriminant union.\nexport type TrackingCtx = {\n sourceObserver?: SourceObserver;\n} & (\n | {\n computed: ComputedBase<unknown>;\n depCollector: DepEntry[];\n kind: 'computed';\n }\n | {\n cleanups: CleanupFn[];\n deps: Map<ReactiveBase<unknown>, number>;\n effect: Subscriber;\n kind: 'effect';\n subscriptions: Set<CleanupFn>;\n }\n);\n\n// ── Scheduling state ────────────────────────────────────────────────────────\n//\n// Colocated here (rather than in scheduling.ts) so it can be a field of\n// ExecutionContext without a runtime circular import between tracking.ts and\n// scheduling.ts — scheduling.ts only needs `getSchedulingState()` and the\n// `SchedulingState` type (type-only import, erased at compile time).\n//\n// Isolating this per-request (via the SSR hook) closes the documented gap where\n// concurrent SSR requests share one flush queue: `batchDepth`, `pendingSubscribers`,\n// and the dirty-computed sets are the only scheduling fields that can observably\n// leak across requests (they persist across `await` boundaries via `batch()`\n// spanning shared module-level signals). The propagation epoch/stack in\n// scheduling.ts stay module-level scratch memory — they're only touched within a\n// single synchronous call stack, which JS's run-to-completion model already isolates.\n\nexport type SchedulingState = {\n activeDirty: 'a' | 'b';\n batchDepth: number;\n readonly dirtyWithEffectSubsA: Set<ComputedBase<unknown>>;\n readonly dirtyWithEffectSubsB: Set<ComputedBase<unknown>>;\n readonly pendingSubscribers: Set<Subscriber>;\n};\n\nexport const createSchedulingState = (): SchedulingState => ({\n activeDirty: 'a',\n batchDepth: 0,\n dirtyWithEffectSubsA: new Set(),\n dirtyWithEffectSubsB: new Set(),\n pendingSubscribers: new Set(),\n});\n\n// ── ExecutionContext ──────────────────────────────────────────────────────────\n//\n// Unified context replaces two separate globals (_tracking and _scopeStack).\n// Both the dep-tracking context and the innermost scope's cleanup array live here.\n// The SSR hook overrides the entire context per-request — fixing the prior bug\n// where scope cleanups were always a process-wide singleton even with the hook.\n\nexport type ExecutionContext = {\n readonly scheduling: SchedulingState;\n readonly scopeCleanups: CleanupFn[] | null;\n readonly tracking: TrackingCtx | null;\n};\n\nexport type ContextHook = {\n get(): ExecutionContext;\n run<T>(ctx: ExecutionContext, fn: () => T): T;\n};\n\nlet _ctx: ExecutionContext = { scheduling: createSchedulingState(), scopeCleanups: null, tracking: null };\nlet _hook: ContextHook | null = null;\n\nconst getCtx = (): ExecutionContext => (_hook !== null ? _hook.get() : _ctx);\n\n/** Returns the scheduling-state bucket for the active execution context (module-level singleton outside SSR). */\nexport const getSchedulingState = (): SchedulingState => getCtx().scheduling;\n\n/** `true` once an SSR context hook is installed — scheduling state is then request-isolated. */\nexport const hasContextHook = (): boolean => _hook !== null;\n\n// ── Tracking ──────────────────────────────────────────────────────────────────\n\nexport const getTracking = (): TrackingCtx | null => getCtx().tracking;\n\nexport const withTracking = <T>(tracking: TrackingCtx | null, fn: () => T): T => {\n if (_hook !== null) {\n const current = _hook.get();\n\n return _hook.run({ ...current, tracking }, fn);\n }\n\n const prev = _ctx;\n\n _ctx = { ..._ctx, tracking };\n\n try {\n return fn();\n } finally {\n _ctx = prev;\n }\n};\n\n// ── Scope cleanup ─────────────────────────────────────────────────────────────\n\n/**\n * Pushes `cleanups` as the active scope cleanup array for the duration of `fn`.\n * Any `onCleanup()` calls inside `fn` will register into these cleanups.\n * Effects and computed values created inside `fn` are also auto-registered.\n */\nexport const withScopeCleanups = <T>(cleanups: CleanupFn[], fn: () => T): T => {\n if (_hook !== null) {\n const current = _hook.get();\n\n return _hook.run({ ...current, scopeCleanups: cleanups }, fn);\n }\n\n const prev = _ctx;\n\n _ctx = { ..._ctx, scopeCleanups: cleanups };\n\n try {\n return fn();\n } finally {\n _ctx = prev;\n }\n};\n\n/**\n * Returns the cleanup array of the innermost active scope, or `null` if not inside a scope.\n */\nexport const getScopeCleanups = (): CleanupFn[] | null => getCtx().scopeCleanups;\n\n// ── Auto-disposal ───────────────────────────────────────────────────────────────\n//\n// Shared by every primitive that ties its own lifetime to the context it was\n// created in (computed(), resource()): dispose when the enclosing effect re-runs\n// or disposes, or — if not inside an effect — when the enclosing scope disposes.\n// No-op if neither is active (the caller owns disposal directly).\n//\n// Why signal() and store() don't call this: computed()/resource() are *derived*\n// computations — recreating one on every effect re-run is normal and cheap, so\n// tying their lifetime to the enclosing context avoids leaking a fresh graph\n// node per run. signal()/store() hold raw, caller-owned state that is typically\n// created once and read across many effect re-runs; auto-disposing it on the\n// first re-run would silently break any effect that reads a signal it declared\n// itself. Callers who genuinely want scope-scoped state should use\n// `scope.add(() => mySignal.dispose())` explicitly.\n\n/**\n * Registers `dispose` to run when the enclosing effect re-runs/disposes, or —\n * if there is no enclosing effect — when the enclosing scope disposes.\n */\nexport const autoRegisterDisposal = (dispose: CleanupFn): void => {\n const ctx = getTracking();\n\n if (ctx?.kind === 'effect') {\n ctx.cleanups.push(dispose);\n } else {\n getScopeCleanups()?.push(dispose);\n }\n};\n\n// ── Context hook (for SSR) ────────────────────────────────────────────────────\n\n/**\n * @internal Used only by `/ripple/ssr`.\n * Installs a context hook that overrides both tracking and scope-cleanup access.\n * Returns the previous hook so callers can restore it.\n */\nexport const _installContextHook = (hook: ContextHook | null): ContextHook | null => {\n const prev = _hook;\n\n _hook = hook;\n\n return prev;\n};\n\n// ── Source observer ───────────────────────────────────────────────────────────\n//\n// Observer is now scoped to the active TrackingCtx instead of a process-wide global.\n// This means it only fires for direct deps of the current effect/computed — nested\n// computed recomputes use their own context (no observer) so no identity check needed.\n\n/**\n * Runs `fn` while calling `observer` for every reactive source directly accessed.\n * Used by `debugEffect` to detect which sources changed between runs.\n * No-op if called outside an active tracking context.\n */\nexport const withSourceObserver = <T>(observer: SourceObserver, fn: () => T): T => {\n const current = getTracking();\n\n if (current === null) return fn();\n\n return withTracking({ ...current, sourceObserver: observer }, fn);\n};\n\n// ── untrack ───────────────────────────────────────────────────────────────────\n\n/**\n * Runs `fn` without recording any reactive reads as dependencies of the enclosing\n * effect or computed. Useful when you need to read reactive state \"silently\".\n *\n * @example\n * ```ts\n * effect(() => {\n * const a = count.value; // tracked\n * const b = untrack(() => name.value); // NOT tracked — no re-run when name changes\n * });\n * ```\n */\nexport const untrack = <T>(fn: () => T): T => withTracking(null, fn);\n\n// ── trackSource ───────────────────────────────────────────────────────────────\n//\n// Records `source` as a dependency of the currently active tracking context.\n// For computed contexts: only records the dep entry — addComputedSub is called\n// later by updateDeps() in computed.ts (only for genuinely NEW deps, preventing\n// duplicate WeakRef entries).\n// For effect contexts: immediately subscribes the effect to the source and\n// records the dep for getDependencies().\n\nexport const trackSource = (source: ReactiveBase<unknown>): void => {\n const ctx = getTracking();\n\n if (ctx === null) return;\n\n ctx.sourceObserver?.(source);\n\n if (ctx.kind === 'computed') {\n ctx.depCollector.push({ source, version: source.version });\n } else if (ctx.kind === 'effect') {\n const owner = ctx.effect;\n\n source.addEffectSub(owner);\n ctx.subscriptions.add(() => source.removeEffectSub(owner));\n ctx.deps.set(source, source.version);\n }\n};\n"],"mappings":";AASA,IAAI,IAAiB,GAER,UAA6B,EAAE,GAC/B,UAA4B,GAyD5B,WAAgD;CAC3D,aAAa;CACb,YAAY;CACZ,sCAAsB,IAAI,IAAI;CAC9B,sCAAsB,IAAI,IAAI;CAC9B,oCAAoB,IAAI,IAAI;AAC9B,IAoBI,IAAyB;CAAE,YAAY,EAAsB;CAAG,eAAe;CAAM,UAAU;AAAK,GACpG,IAA4B,MAE1B,UAAkC,MAAU,OAAqB,IAAd,EAAM,IAAI,GAGtD,UAA4C,EAAO,CAAC,CAAC,YAGrD,UAAgC,MAAU,MAI1C,UAAwC,EAAO,CAAC,CAAC,UAEjD,KAAmB,GAA8B,MAAmB;CAC/E,IAAI,MAAU,MAAM;EAClB,IAAM,IAAU,EAAM,IAAI;EAE1B,OAAO,EAAM,IAAI;GAAE,GAAG;GAAS;EAAS,GAAG,CAAE;CAC/C;CAEA,IAAM,IAAO;CAEb,IAAO;EAAE,GAAG;EAAM;CAAS;CAE3B,IAAI;EACF,OAAO,EAAG;CACZ,UAAU;EACR,IAAO;CACT;AACF,GASa,KAAwB,GAAuB,MAAmB;CAC7E,IAAI,MAAU,MAAM;EAClB,IAAM,IAAU,EAAM,IAAI;EAE1B,OAAO,EAAM,IAAI;GAAE,GAAG;GAAS,eAAe;EAAS,GAAG,CAAE;CAC9D;CAEA,IAAM,IAAO;CAEb,IAAO;EAAE,GAAG;EAAM,eAAe;CAAS;CAE1C,IAAI;EACF,OAAO,EAAG;CACZ,UAAU;EACR,IAAO;CACT;AACF,GAKa,UAA6C,EAAO,CAAC,CAAC,eAsBtD,KAAwB,MAA6B;CAChE,IAAM,IAAM,EAAY;CAExB,AAAI,GAAK,SAAS,WAChB,EAAI,SAAS,KAAK,CAAO,IAEzB,EAAiB,CAAC,EAAE,KAAK,CAAO;AAEpC,GASa,KAAuB,MAAiD;CACnF,IAAM,IAAO;CAIb,OAFA,IAAQ,GAED;AACT,GAaa,KAAyB,GAA0B,MAAmB;CACjF,IAAM,IAAU,EAAY;CAI5B,OAFI,MAAY,OAAa,EAAG,IAEzB,EAAa;EAAE,GAAG;EAAS,gBAAgB;CAAS,GAAG,CAAE;AAClE,GAgBa,KAAc,MAAmB,EAAa,MAAM,CAAE,GAWtD,KAAe,MAAwC;CAClE,IAAM,IAAM,EAAY;CAEpB,UAAQ,MAIZ;MAFA,EAAI,iBAAiB,CAAM,GAEvB,EAAI,SAAS,YACf,EAAI,aAAa,KAAK;GAAE;GAAQ,SAAS,EAAO;EAAQ,CAAC;OACpD,IAAI,EAAI,SAAS,UAAU;GAChC,IAAM,IAAQ,EAAI;GAIlB,AAFA,EAAO,aAAa,CAAK,GACzB,EAAI,cAAc,UAAU,EAAO,gBAAgB,CAAK,CAAC,GACzD,EAAI,KAAK,IAAI,GAAQ,EAAO,OAAO;EACrC;;AACF"}
|
package/dist/utilities.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utilities.cjs","names":[],"sources":["../src/utilities.ts"],"sourcesContent":["import type { Computed, Readable, Signal } from './types';\n\nimport { IS_COMPUTED, IS_SIGNAL, IS_STORE } from './symbols';\nimport { untrack } from './tracking';\n\nexport { untrack };\n\n// ── Readonly wrapper ──────────────────────────────────────────────────────────\n//\n// readonly() returns Readable<T> — no dispose() because the wrapper does not\n// own the source. Callers retain ownership and must dispose the source themselves.\n// The [IS_SIGNAL] brand is set so isReactive(readonly(s)) and isSignal(readonly(s))\n// both return true (it is reactive, not IS_COMPUTED, not IS_STORE).\n// [IS_COMPUTED] is intentionally NOT set — the wrapper is not a Computed<T>\n// and does not have dispose().\n\n/**\n * Wraps a reactive value to produce a structurally read-only view.\n * The `value` setter is hidden. Delegates reads directly to the source —\n * no extra reactive graph node, no extra subscription.\n *\n * The caller retains ownership of the source and is responsible for disposing it.\n *\n * @example\n * ```ts\n * const count = signal(0);\n * const readCount = readonly(count);\n * readCount.value; // fine — tracks dep on count\n * // readCount.value = 1; // TypeScript error — Readable has no setter\n * ```\n */\nexport const readonly = <T>(source: Readable<T>): Readable<T> => {\n const wrapper = {\n get disposed(): boolean {\n return source.disposed;\n },\n [IS_SIGNAL]: true as const,\n get name(): string | undefined {\n return source.name;\n },\n peek(): T {\n return source.peek();\n },\n subscribe(listener: () => void) {\n return source.subscribe(listener);\n },\n get value(): T {\n return source.value;\n },\n };\n\n return wrapper as unknown as Readable<T>;\n};\n\n// ── Type guards ───────────────────────────────────────────────────────────────\n\n/**\n * Returns `true` for any reactive value that implements `Readable<T>` — including\n * `signal()`, `computed()`, `store()`, and `readonly()` wrappers.\n *\n * This is the broad \"is this value reactive?\" check. Use `isSignal` for writable-only\n * signals, or `isComputed` to narrow to computed values.\n *\n * @example\n * ```ts\n * isReactive(signal(0)) // true\n * isReactive(computed(() => 1)) // true\n * isReactive(store({ x: 0 })) // true\n * isReactive(readonly(signal(0))) // true\n * isReactive({}) // false\n * ```\n */\nexport const isReactive = <T = unknown>(value: unknown): value is Readable<T> =>\n typeof value === 'object' && value !== null && !!(value as Record<typeof IS_SIGNAL, unknown>)[IS_SIGNAL];\n\n/**\n * Returns `true` only for writable `Signal<T>` values — excludes `computed()
|
|
1
|
+
{"version":3,"file":"utilities.cjs","names":[],"sources":["../src/utilities.ts"],"sourcesContent":["import type { Computed, Readable, Signal } from './types';\n\nimport { IS_COMPUTED, IS_SIGNAL, IS_STORE } from './symbols';\nimport { untrack } from './tracking';\n\nexport { untrack };\n\n// ── Readonly wrapper ──────────────────────────────────────────────────────────\n//\n// readonly() returns Readable<T> — no dispose() because the wrapper does not\n// own the source. Callers retain ownership and must dispose the source themselves.\n// The [IS_SIGNAL] brand is set so isReactive(readonly(s)) and isSignal(readonly(s))\n// both return true (it is reactive, not IS_COMPUTED, not IS_STORE).\n// [IS_COMPUTED] is intentionally NOT set — the wrapper is not a Computed<T>\n// and does not have dispose().\n\n/**\n * Wraps a reactive value to produce a structurally read-only view.\n * The `value` setter is hidden. Delegates reads directly to the source —\n * no extra reactive graph node, no extra subscription.\n *\n * The caller retains ownership of the source and is responsible for disposing it.\n *\n * @example\n * ```ts\n * const count = signal(0);\n * const readCount = readonly(count);\n * readCount.value; // fine — tracks dep on count\n * // readCount.value = 1; // TypeScript error — Readable has no setter\n * ```\n */\nexport const readonly = <T>(source: Readable<T>): Readable<T> => {\n const wrapper = {\n get disposed(): boolean {\n return source.disposed;\n },\n [IS_SIGNAL]: true as const,\n get name(): string | undefined {\n return source.name;\n },\n peek(): T {\n return source.peek();\n },\n subscribe(listener: () => void) {\n return source.subscribe(listener);\n },\n get value(): T {\n return source.value;\n },\n };\n\n return wrapper as unknown as Readable<T>;\n};\n\n// ── Type guards ───────────────────────────────────────────────────────────────\n\n/**\n * Returns `true` for any reactive value that implements `Readable<T>` — including\n * `signal()`, `computed()`, `store()`, and `readonly()` wrappers.\n *\n * This is the broad \"is this value reactive?\" check. Use `isSignal` for writable-only\n * signals, or `isComputed` to narrow to computed values.\n *\n * @example\n * ```ts\n * isReactive(signal(0)) // true\n * isReactive(computed(() => 1)) // true\n * isReactive(store({ x: 0 })) // true\n * isReactive(readonly(signal(0))) // true\n * isReactive({}) // false\n * ```\n */\nexport const isReactive = <T = unknown>(value: unknown): value is Readable<T> =>\n typeof value === 'object' && value !== null && !!(value as Record<typeof IS_SIGNAL, unknown>)[IS_SIGNAL];\n\n/**\n * Returns `true` only for writable `Signal<T>` values — excludes `computed()` and `store()`.\n * `readonly()` wrappers of a signal still report `true` since they carry the same\n * `IS_SIGNAL` brand as their source (they are not `IS_COMPUTED`/`IS_STORE`).\n *\n * Use `isReactive` for the broad \"is this reactive?\" check.\n *\n * @example\n * ```ts\n * isSignal(signal(0)) // true\n * isSignal(computed(() => 1)) // false — computed excluded\n * isSignal(store({ x: 0 })) // false — store excluded\n * isSignal(readonly(signal(0))) // true — readonly has IS_SIGNAL, not IS_COMPUTED/IS_STORE\n * isSignal({}) // false\n * ```\n */\nexport const isSignal = <T = unknown>(value: unknown): value is Signal<T> =>\n isReactive(value) &&\n !(value as unknown as Record<typeof IS_COMPUTED, unknown>)[IS_COMPUTED] &&\n !(value as unknown as Record<typeof IS_STORE, unknown>)[IS_STORE];\n\nexport const isComputed = <T = unknown>(value: unknown): value is Computed<T> =>\n typeof value === 'object' && value !== null && !!(value as Record<typeof IS_COMPUTED, unknown>)[IS_COMPUTED];\n\nexport const isStore = <T extends object = object>(value: unknown): value is import('./types').Store<T> =>\n typeof value === 'object' && value !== null && !!(value as Record<typeof IS_STORE, unknown>)[IS_STORE];\n"],"mappings":"6DA+BA,IAAa,EAAe,IAoBnB,CAlBL,IAAI,UAAoB,CACtB,OAAO,EAAO,QAChB,GACC,EAAA,WAAY,GACb,IAAI,MAA2B,CAC7B,OAAO,EAAO,IAChB,EACA,MAAU,CACR,OAAO,EAAO,KAAK,CACrB,EACA,UAAU,EAAsB,CAC9B,OAAO,EAAO,UAAU,CAAQ,CAClC,EACA,IAAI,OAAW,CACb,OAAO,EAAO,KAChB,CAGK,GAqBI,EAA2B,GACtC,OAAO,GAAU,YAAY,GAAkB,CAAC,CAAE,EAA4C,EAAA,WAkBnF,EAAyB,GACpC,EAAW,CAAK,GAChB,CAAE,EAAyD,EAAA,cAC3D,CAAE,EAAsD,EAAA,UAE7C,EAA2B,GACtC,OAAO,GAAU,YAAY,GAAkB,CAAC,CAAE,EAA8C,EAAA,aAErF,EAAsC,GACjD,OAAO,GAAU,YAAY,GAAkB,CAAC,CAAE,EAA2C,EAAA"}
|
package/dist/utilities.d.ts
CHANGED
|
@@ -35,8 +35,9 @@ export declare const readonly: <T>(source: Readable<T>) => Readable<T>;
|
|
|
35
35
|
*/
|
|
36
36
|
export declare const isReactive: <T = unknown>(value: unknown) => value is Readable<T>;
|
|
37
37
|
/**
|
|
38
|
-
* Returns `true` only for writable `Signal<T>` values — excludes `computed()
|
|
39
|
-
*
|
|
38
|
+
* Returns `true` only for writable `Signal<T>` values — excludes `computed()` and `store()`.
|
|
39
|
+
* `readonly()` wrappers of a signal still report `true` since they carry the same
|
|
40
|
+
* `IS_SIGNAL` brand as their source (they are not `IS_COMPUTED`/`IS_STORE`).
|
|
40
41
|
*
|
|
41
42
|
* Use `isReactive` for the broad "is this reactive?" check.
|
|
42
43
|
*
|
package/dist/utilities.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAG1D,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAErC,OAAO,EAAE,OAAO,EAAE,CAAC;AAWnB;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,QAAQ,GAAI,CAAC,EAAE,QAAQ,QAAQ,CAAC,CAAC,CAAC,KAAG,QAAQ,CAAC,CAAC,CAqB3D,CAAC;AAIF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,UAAU,GAAI,CAAC,GAAG,OAAO,EAAE,OAAO,OAAO,KAAG,KAAK,IAAI,QAAQ,CAAC,CAAC,CAC8B,CAAC;AAE3G
|
|
1
|
+
{"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAG1D,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAErC,OAAO,EAAE,OAAO,EAAE,CAAC;AAWnB;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,QAAQ,GAAI,CAAC,EAAE,QAAQ,QAAQ,CAAC,CAAC,CAAC,KAAG,QAAQ,CAAC,CAAC,CAqB3D,CAAC;AAIF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,UAAU,GAAI,CAAC,GAAG,OAAO,EAAE,OAAO,OAAO,KAAG,KAAK,IAAI,QAAQ,CAAC,CAAC,CAC8B,CAAC;AAE3G;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,QAAQ,GAAI,CAAC,GAAG,OAAO,EAAE,OAAO,OAAO,KAAG,KAAK,IAAI,MAAM,CAAC,CAAC,CAGL,CAAC;AAEpE,eAAO,MAAM,UAAU,GAAI,CAAC,GAAG,OAAO,EAAE,OAAO,OAAO,KAAG,KAAK,IAAI,QAAQ,CAAC,CAAC,CACkC,CAAC;AAE/G,eAAO,MAAM,OAAO,GAAI,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,OAAO,OAAO,KAAG,KAAK,IAAI,OAAO,SAAS,EAAE,KAAK,CAAC,CAAC,CACE,CAAC"}
|
package/dist/utilities.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utilities.js","names":[],"sources":["../src/utilities.ts"],"sourcesContent":["import type { Computed, Readable, Signal } from './types';\n\nimport { IS_COMPUTED, IS_SIGNAL, IS_STORE } from './symbols';\nimport { untrack } from './tracking';\n\nexport { untrack };\n\n// ── Readonly wrapper ──────────────────────────────────────────────────────────\n//\n// readonly() returns Readable<T> — no dispose() because the wrapper does not\n// own the source. Callers retain ownership and must dispose the source themselves.\n// The [IS_SIGNAL] brand is set so isReactive(readonly(s)) and isSignal(readonly(s))\n// both return true (it is reactive, not IS_COMPUTED, not IS_STORE).\n// [IS_COMPUTED] is intentionally NOT set — the wrapper is not a Computed<T>\n// and does not have dispose().\n\n/**\n * Wraps a reactive value to produce a structurally read-only view.\n * The `value` setter is hidden. Delegates reads directly to the source —\n * no extra reactive graph node, no extra subscription.\n *\n * The caller retains ownership of the source and is responsible for disposing it.\n *\n * @example\n * ```ts\n * const count = signal(0);\n * const readCount = readonly(count);\n * readCount.value; // fine — tracks dep on count\n * // readCount.value = 1; // TypeScript error — Readable has no setter\n * ```\n */\nexport const readonly = <T>(source: Readable<T>): Readable<T> => {\n const wrapper = {\n get disposed(): boolean {\n return source.disposed;\n },\n [IS_SIGNAL]: true as const,\n get name(): string | undefined {\n return source.name;\n },\n peek(): T {\n return source.peek();\n },\n subscribe(listener: () => void) {\n return source.subscribe(listener);\n },\n get value(): T {\n return source.value;\n },\n };\n\n return wrapper as unknown as Readable<T>;\n};\n\n// ── Type guards ───────────────────────────────────────────────────────────────\n\n/**\n * Returns `true` for any reactive value that implements `Readable<T>` — including\n * `signal()`, `computed()`, `store()`, and `readonly()` wrappers.\n *\n * This is the broad \"is this value reactive?\" check. Use `isSignal` for writable-only\n * signals, or `isComputed` to narrow to computed values.\n *\n * @example\n * ```ts\n * isReactive(signal(0)) // true\n * isReactive(computed(() => 1)) // true\n * isReactive(store({ x: 0 })) // true\n * isReactive(readonly(signal(0))) // true\n * isReactive({}) // false\n * ```\n */\nexport const isReactive = <T = unknown>(value: unknown): value is Readable<T> =>\n typeof value === 'object' && value !== null && !!(value as Record<typeof IS_SIGNAL, unknown>)[IS_SIGNAL];\n\n/**\n * Returns `true` only for writable `Signal<T>` values — excludes `computed()
|
|
1
|
+
{"version":3,"file":"utilities.js","names":[],"sources":["../src/utilities.ts"],"sourcesContent":["import type { Computed, Readable, Signal } from './types';\n\nimport { IS_COMPUTED, IS_SIGNAL, IS_STORE } from './symbols';\nimport { untrack } from './tracking';\n\nexport { untrack };\n\n// ── Readonly wrapper ──────────────────────────────────────────────────────────\n//\n// readonly() returns Readable<T> — no dispose() because the wrapper does not\n// own the source. Callers retain ownership and must dispose the source themselves.\n// The [IS_SIGNAL] brand is set so isReactive(readonly(s)) and isSignal(readonly(s))\n// both return true (it is reactive, not IS_COMPUTED, not IS_STORE).\n// [IS_COMPUTED] is intentionally NOT set — the wrapper is not a Computed<T>\n// and does not have dispose().\n\n/**\n * Wraps a reactive value to produce a structurally read-only view.\n * The `value` setter is hidden. Delegates reads directly to the source —\n * no extra reactive graph node, no extra subscription.\n *\n * The caller retains ownership of the source and is responsible for disposing it.\n *\n * @example\n * ```ts\n * const count = signal(0);\n * const readCount = readonly(count);\n * readCount.value; // fine — tracks dep on count\n * // readCount.value = 1; // TypeScript error — Readable has no setter\n * ```\n */\nexport const readonly = <T>(source: Readable<T>): Readable<T> => {\n const wrapper = {\n get disposed(): boolean {\n return source.disposed;\n },\n [IS_SIGNAL]: true as const,\n get name(): string | undefined {\n return source.name;\n },\n peek(): T {\n return source.peek();\n },\n subscribe(listener: () => void) {\n return source.subscribe(listener);\n },\n get value(): T {\n return source.value;\n },\n };\n\n return wrapper as unknown as Readable<T>;\n};\n\n// ── Type guards ───────────────────────────────────────────────────────────────\n\n/**\n * Returns `true` for any reactive value that implements `Readable<T>` — including\n * `signal()`, `computed()`, `store()`, and `readonly()` wrappers.\n *\n * This is the broad \"is this value reactive?\" check. Use `isSignal` for writable-only\n * signals, or `isComputed` to narrow to computed values.\n *\n * @example\n * ```ts\n * isReactive(signal(0)) // true\n * isReactive(computed(() => 1)) // true\n * isReactive(store({ x: 0 })) // true\n * isReactive(readonly(signal(0))) // true\n * isReactive({}) // false\n * ```\n */\nexport const isReactive = <T = unknown>(value: unknown): value is Readable<T> =>\n typeof value === 'object' && value !== null && !!(value as Record<typeof IS_SIGNAL, unknown>)[IS_SIGNAL];\n\n/**\n * Returns `true` only for writable `Signal<T>` values — excludes `computed()` and `store()`.\n * `readonly()` wrappers of a signal still report `true` since they carry the same\n * `IS_SIGNAL` brand as their source (they are not `IS_COMPUTED`/`IS_STORE`).\n *\n * Use `isReactive` for the broad \"is this reactive?\" check.\n *\n * @example\n * ```ts\n * isSignal(signal(0)) // true\n * isSignal(computed(() => 1)) // false — computed excluded\n * isSignal(store({ x: 0 })) // false — store excluded\n * isSignal(readonly(signal(0))) // true — readonly has IS_SIGNAL, not IS_COMPUTED/IS_STORE\n * isSignal({}) // false\n * ```\n */\nexport const isSignal = <T = unknown>(value: unknown): value is Signal<T> =>\n isReactive(value) &&\n !(value as unknown as Record<typeof IS_COMPUTED, unknown>)[IS_COMPUTED] &&\n !(value as unknown as Record<typeof IS_STORE, unknown>)[IS_STORE];\n\nexport const isComputed = <T = unknown>(value: unknown): value is Computed<T> =>\n typeof value === 'object' && value !== null && !!(value as Record<typeof IS_COMPUTED, unknown>)[IS_COMPUTED];\n\nexport const isStore = <T extends object = object>(value: unknown): value is import('./types').Store<T> =>\n typeof value === 'object' && value !== null && !!(value as Record<typeof IS_STORE, unknown>)[IS_STORE];\n"],"mappings":";;;AA+BA,IAAa,KAAe,OAoBnB;CAlBL,IAAI,WAAoB;EACtB,OAAO,EAAO;CAChB;EACC,IAAY;CACb,IAAI,OAA2B;EAC7B,OAAO,EAAO;CAChB;CACA,OAAU;EACR,OAAO,EAAO,KAAK;CACrB;CACA,UAAU,GAAsB;EAC9B,OAAO,EAAO,UAAU,CAAQ;CAClC;CACA,IAAI,QAAW;EACb,OAAO,EAAO;CAChB;AAGK,IAqBI,KAA2B,MACtC,OAAO,KAAU,cAAY,KAAkB,CAAC,CAAE,EAA4C,IAkBnF,KAAyB,MACpC,EAAW,CAAK,KAChB,CAAE,EAAyD,MAC3D,CAAE,EAAsD,IAE7C,KAA2B,MACtC,OAAO,KAAU,cAAY,KAAkB,CAAC,CAAE,EAA8C,IAErF,KAAsC,MACjD,OAAO,KAAU,cAAY,KAAkB,CAAC,CAAE,EAA2C"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vielzeug/ripple",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
@@ -43,10 +43,13 @@
|
|
|
43
43
|
"access": "public",
|
|
44
44
|
"registry": "https://registry.npmjs.org/"
|
|
45
45
|
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=18"
|
|
48
|
+
},
|
|
46
49
|
"devDependencies": {
|
|
47
50
|
"@types/node": "^26.1.0",
|
|
48
|
-
"typescript": "
|
|
51
|
+
"typescript": "^6.0.3",
|
|
49
52
|
"vite": "^8.1.3",
|
|
50
|
-
"vitest": "^4.1.
|
|
53
|
+
"vitest": "^4.1.10"
|
|
51
54
|
}
|
|
52
55
|
}
|