@pyreon/rx 0.11.4 → 0.11.6

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/README.md CHANGED
@@ -13,21 +13,21 @@ bun add @pyreon/rx
13
13
  ## Usage
14
14
 
15
15
  ```ts
16
- import { rx } from "@pyreon/rx"
17
- import { signal } from "@pyreon/reactivity"
16
+ import { rx } from '@pyreon/rx'
17
+ import { signal } from '@pyreon/reactivity'
18
18
 
19
19
  const users = signal<User[]>([])
20
20
 
21
- const active = rx.filter(users, u => u.active) // Computed<User[]>
22
- const sorted = rx.sortBy(active, "name") // Computed<User[]>
23
- const top10 = rx.take(sorted, 10) // Computed<User[]>
21
+ const active = rx.filter(users, (u) => u.active) // Computed<User[]>
22
+ const sorted = rx.sortBy(active, 'name') // Computed<User[]>
23
+ const top10 = rx.take(sorted, 10) // Computed<User[]>
24
24
 
25
25
  // Pipe chains
26
26
  const result = rx.pipe(
27
27
  users,
28
- items => items.filter(u => u.active),
29
- items => items.sort((a, b) => a.name.localeCompare(b.name)),
30
- items => items.slice(0, 10),
28
+ (items) => items.filter((u) => u.active),
29
+ (items) => items.sort((a, b) => a.name.localeCompare(b.name)),
30
+ (items) => items.slice(0, 10),
31
31
  )
32
32
  ```
33
33
 
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["reactive"],"sources":["../src/types.ts","../src/aggregation.ts","../src/collections.ts","../src/operators.ts","../src/pipe.ts","../src/search.ts","../src/timing.ts","../src/index.ts"],"sourcesContent":["import type { computed, signal } from \"@pyreon/reactivity\"\n\n/** A readable signal — any callable that returns a value and tracks subscribers. */\nexport type ReadableSignal<T> = (() => T) & { peek?: () => T }\n\n/** Result of a signal-aware transform — Computed when input is signal, plain when not. */\nexport type ReactiveResult<TInput, TOutput> =\n TInput extends ReadableSignal<any> ? ReturnType<typeof computed<TOutput>> : TOutput\n\n/** Key extractor — string key name or function. */\nexport type KeyOf<T> = keyof T | ((item: T) => string | number)\n\n/** Resolve a key extractor to a function. */\nexport function resolveKey<T>(key: KeyOf<T>): (item: T) => string | number {\n return typeof key === \"function\" ? key : (item: T) => String(item[key])\n}\n\n/** Check if a value is a signal (callable function with .set or .peek). */\nexport function isSignal<T>(value: unknown): value is ReadableSignal<T> {\n return typeof value === \"function\"\n}\n","import { computed } from \"@pyreon/reactivity\"\nimport type { KeyOf, ReadableSignal } from \"./types\"\nimport { isSignal, resolveKey } from \"./types\"\n\nfunction reactive<TIn, TOut>(source: TIn, fn: (val: any) => TOut): any {\n if (isSignal(source)) return computed(() => fn((source as ReadableSignal<any>)()))\n return fn(source)\n}\n\n/** Count items in collection. */\nexport function count<T>(source: ReadableSignal<T[]>): ReturnType<typeof computed<number>>\nexport function count<T>(source: T[]): number\nexport function count<T>(source: ReadableSignal<T[]> | T[]): any {\n return reactive(source, (arr: T[]) => arr.length)\n}\n\n/** Sum numeric values. Optionally by key. */\nexport function sum<T>(\n source: ReadableSignal<T[]>,\n key?: KeyOf<T>,\n): ReturnType<typeof computed<number>>\nexport function sum<T>(source: T[], key?: KeyOf<T>): number\nexport function sum<T>(source: ReadableSignal<T[]> | T[], key?: KeyOf<T>): any {\n const getVal = key ? resolveKey(key) : (item: T) => item as unknown as number\n return reactive(source, (arr: T[]) => arr.reduce((acc, item) => acc + Number(getVal(item)), 0))\n}\n\n/** Find minimum item. Optionally by key. */\nexport function min<T>(\n source: ReadableSignal<T[]>,\n key?: KeyOf<T>,\n): ReturnType<typeof computed<T | undefined>>\nexport function min<T>(source: T[], key?: KeyOf<T>): T | undefined\nexport function min<T>(source: ReadableSignal<T[]> | T[], key?: KeyOf<T>): any {\n const getVal = key ? resolveKey(key) : (item: T) => item as unknown as number\n return reactive(source, (arr: T[]) => {\n if (arr.length === 0) return undefined\n let result = arr[0] as T\n let minVal = Number(getVal(result))\n for (let i = 1; i < arr.length; i++) {\n const val = Number(getVal(arr[i] as T))\n if (val < minVal) {\n minVal = val\n result = arr[i] as T\n }\n }\n return result\n })\n}\n\n/** Find maximum item. Optionally by key. */\nexport function max<T>(\n source: ReadableSignal<T[]>,\n key?: KeyOf<T>,\n): ReturnType<typeof computed<T | undefined>>\nexport function max<T>(source: T[], key?: KeyOf<T>): T | undefined\nexport function max<T>(source: ReadableSignal<T[]> | T[], key?: KeyOf<T>): any {\n const getVal = key ? resolveKey(key) : (item: T) => item as unknown as number\n return reactive(source, (arr: T[]) => {\n if (arr.length === 0) return undefined\n let result = arr[0] as T\n let maxVal = Number(getVal(result))\n for (let i = 1; i < arr.length; i++) {\n const val = Number(getVal(arr[i] as T))\n if (val > maxVal) {\n maxVal = val\n result = arr[i] as T\n }\n }\n return result\n })\n}\n\n/** Average of numeric values. Optionally by key. */\nexport function average<T>(\n source: ReadableSignal<T[]>,\n key?: KeyOf<T>,\n): ReturnType<typeof computed<number>>\nexport function average<T>(source: T[], key?: KeyOf<T>): number\nexport function average<T>(source: ReadableSignal<T[]> | T[], key?: KeyOf<T>): any {\n const getVal = key ? resolveKey(key) : (item: T) => item as unknown as number\n return reactive(source, (arr: T[]) => {\n if (arr.length === 0) return 0\n return arr.reduce((acc, item) => acc + Number(getVal(item)), 0) / arr.length\n })\n}\n","import { computed } from \"@pyreon/reactivity\"\nimport type { KeyOf, ReadableSignal } from \"./types\"\nimport { isSignal, resolveKey } from \"./types\"\n\n// ─── Helpers ────────────────────────────────────────────────────────────────\n\nfunction reactive<TIn, TOut>(\n source: TIn,\n fn: (val: any) => TOut,\n): TIn extends ReadableSignal<any> ? ReturnType<typeof computed<TOut>> : TOut {\n if (isSignal(source)) {\n return computed(() => fn((source as ReadableSignal<any>)())) as any\n }\n return fn(source) as any\n}\n\n// ─── Collection transforms ──────────────────────────────────────────────────\n\n/** Filter items by predicate. Signal in → Computed out. */\nexport function filter<T>(\n source: ReadableSignal<T[]>,\n predicate: (item: T, index: number) => boolean,\n): ReturnType<typeof computed<T[]>>\nexport function filter<T>(source: T[], predicate: (item: T, index: number) => boolean): T[]\nexport function filter<T>(\n source: ReadableSignal<T[]> | T[],\n predicate: (item: T, index: number) => boolean,\n): any {\n return reactive(source, (arr: T[]) => arr.filter(predicate))\n}\n\n/** Map items to a new type. */\nexport function map<T, U>(\n source: ReadableSignal<T[]>,\n fn: (item: T, index: number) => U,\n): ReturnType<typeof computed<U[]>>\nexport function map<T, U>(source: T[], fn: (item: T, index: number) => U): U[]\nexport function map<T, U>(\n source: ReadableSignal<T[]> | T[],\n fn: (item: T, index: number) => U,\n): any {\n return reactive(source, (arr: T[]) => arr.map(fn))\n}\n\n/** Sort items by key or comparator. */\nexport function sortBy<T>(\n source: ReadableSignal<T[]>,\n key: KeyOf<T>,\n): ReturnType<typeof computed<T[]>>\nexport function sortBy<T>(source: T[], key: KeyOf<T>): T[]\nexport function sortBy<T>(source: ReadableSignal<T[]> | T[], key: KeyOf<T>): any {\n const getKey = resolveKey(key)\n return reactive(source, (arr: T[]) =>\n [...arr].sort((a, b) => {\n const ka = getKey(a)\n const kb = getKey(b)\n return ka < kb ? -1 : ka > kb ? 1 : 0\n }),\n )\n}\n\n/** Group items by key. Returns Record<string, T[]>. */\nexport function groupBy<T>(\n source: ReadableSignal<T[]>,\n key: KeyOf<T>,\n): ReturnType<typeof computed<Record<string, T[]>>>\nexport function groupBy<T>(source: T[], key: KeyOf<T>): Record<string, T[]>\nexport function groupBy<T>(source: ReadableSignal<T[]> | T[], key: KeyOf<T>): any {\n const getKey = resolveKey(key)\n return reactive(source, (arr: T[]) => {\n const result: Record<string, T[]> = {}\n for (const item of arr) {\n const k = String(getKey(item))\n let group = result[k]\n if (!group) {\n group = []\n result[k] = group\n }\n group.push(item)\n }\n return result\n })\n}\n\n/** Index items by key. Returns Record<string, T> (last wins on collision). */\nexport function keyBy<T>(\n source: ReadableSignal<T[]>,\n key: KeyOf<T>,\n): ReturnType<typeof computed<Record<string, T>>>\nexport function keyBy<T>(source: T[], key: KeyOf<T>): Record<string, T>\nexport function keyBy<T>(source: ReadableSignal<T[]> | T[], key: KeyOf<T>): any {\n const getKey = resolveKey(key)\n return reactive(source, (arr: T[]) => {\n const result: Record<string, T> = {}\n for (const item of arr) result[String(getKey(item))] = item\n return result\n })\n}\n\n/** Deduplicate items by key. */\nexport function uniqBy<T>(\n source: ReadableSignal<T[]>,\n key: KeyOf<T>,\n): ReturnType<typeof computed<T[]>>\nexport function uniqBy<T>(source: T[], key: KeyOf<T>): T[]\nexport function uniqBy<T>(source: ReadableSignal<T[]> | T[], key: KeyOf<T>): any {\n const getKey = resolveKey(key)\n return reactive(source, (arr: T[]) => {\n const seen = new Set<string | number>()\n return arr.filter((item) => {\n const k = getKey(item)\n if (seen.has(k)) return false\n seen.add(k)\n return true\n })\n })\n}\n\n/** Take the first n items. */\nexport function take<T>(source: ReadableSignal<T[]>, n: number): ReturnType<typeof computed<T[]>>\nexport function take<T>(source: T[], n: number): T[]\nexport function take<T>(source: ReadableSignal<T[]> | T[], n: number): any {\n return reactive(source, (arr: T[]) => arr.slice(0, n))\n}\n\n/** Split into chunks of given size. */\nexport function chunk<T>(\n source: ReadableSignal<T[]>,\n size: number,\n): ReturnType<typeof computed<T[][]>>\nexport function chunk<T>(source: T[], size: number): T[][]\nexport function chunk<T>(source: ReadableSignal<T[]> | T[], size: number): any {\n return reactive(source, (arr: T[]) => {\n const result: T[][] = []\n for (let i = 0; i < arr.length; i += size) result.push(arr.slice(i, i + size))\n return result\n })\n}\n\n/** Flatten one level of nesting. */\nexport function flatten<T>(source: ReadableSignal<T[][]>): ReturnType<typeof computed<T[]>>\nexport function flatten<T>(source: T[][]): T[]\nexport function flatten<T>(source: ReadableSignal<T[][]> | T[][]): any {\n return reactive(source, (arr: T[][]) => arr.flat())\n}\n\n/** Find the first item matching a predicate. */\nexport function find<T>(\n source: ReadableSignal<T[]>,\n predicate: (item: T) => boolean,\n): ReturnType<typeof computed<T | undefined>>\nexport function find<T>(source: T[], predicate: (item: T) => boolean): T | undefined\nexport function find<T>(source: ReadableSignal<T[]> | T[], predicate: (item: T) => boolean): any {\n return reactive(source, (arr: T[]) => arr.find(predicate))\n}\n\n/** Skip the first n items. */\nexport function skip<T>(source: ReadableSignal<T[]>, n: number): ReturnType<typeof computed<T[]>>\nexport function skip<T>(source: T[], n: number): T[]\nexport function skip<T>(source: ReadableSignal<T[]> | T[], n: number): any {\n return reactive(source, (arr: T[]) => arr.slice(n))\n}\n\n/** Take the last n items. */\nexport function last<T>(source: ReadableSignal<T[]>, n: number): ReturnType<typeof computed<T[]>>\nexport function last<T>(source: T[], n: number): T[]\nexport function last<T>(source: ReadableSignal<T[]> | T[], n: number): any {\n return reactive(source, (arr: T[]) => arr.slice(-n))\n}\n\n/** Map over values of a Record. Useful after groupBy. */\nexport function mapValues<T, U>(\n source: ReadableSignal<Record<string, T>>,\n fn: (value: T, key: string) => U,\n): ReturnType<typeof computed<Record<string, U>>>\nexport function mapValues<T, U>(\n source: Record<string, T>,\n fn: (value: T, key: string) => U,\n): Record<string, U>\nexport function mapValues<T, U>(\n source: ReadableSignal<Record<string, T>> | Record<string, T>,\n fn: (value: T, key: string) => U,\n): any {\n return reactive(source, (obj: Record<string, T>) => {\n const result: Record<string, U> = {}\n for (const key of Object.keys(obj)) result[key] = fn(obj[key] as T, key)\n return result\n })\n}\n","import { computed, effect, signal } from \"@pyreon/reactivity\"\nimport type { ReadableSignal } from \"./types\"\n\n/**\n * Distinct — skip consecutive duplicate values from a signal.\n * Uses `Object.is` by default, or a custom equality function.\n */\nexport function distinct<T>(\n source: ReadableSignal<T>,\n equals: (a: T, b: T) => boolean = Object.is,\n): ReadableSignal<T> {\n const result = signal(source())\n\n effect(() => {\n const val = source()\n if (!equals(val, result.peek())) {\n result.set(val)\n }\n })\n\n return result\n}\n\n/**\n * Scan — running accumulator over signal changes.\n * Like Array.reduce but emits the accumulated value on each source change.\n *\n * @example\n * ```ts\n * const clicks = signal(0)\n * const total = rx.scan(clicks, (acc, val) => acc + val, 0)\n * // clicks: 1 → total: 1\n * // clicks: 3 → total: 4\n * // clicks: 2 → total: 6\n * ```\n */\nexport function scan<T, U>(\n source: ReadableSignal<T>,\n reducer: (acc: U, value: T) => U,\n initial: U,\n): ReadableSignal<U> {\n const result = signal(initial)\n\n effect(() => {\n const val = source()\n result.set(reducer(result.peek(), val))\n })\n\n return result\n}\n\n/**\n * Combine multiple signals into a single computed value.\n *\n * @example\n * ```ts\n * const fullName = rx.combine(firstName, lastName, (f, l) => `${f} ${l}`)\n * ```\n */\nexport function combine<A, B, R>(\n a: ReadableSignal<A>,\n b: ReadableSignal<B>,\n fn: (a: A, b: B) => R,\n): ReturnType<typeof computed<R>>\nexport function combine<A, B, C, R>(\n a: ReadableSignal<A>,\n b: ReadableSignal<B>,\n c: ReadableSignal<C>,\n fn: (a: A, b: B, c: C) => R,\n): ReturnType<typeof computed<R>>\nexport function combine(...args: any[]): any {\n const fn = args[args.length - 1] as (...vals: any[]) => any\n const sources = args.slice(0, -1) as ReadableSignal<any>[]\n return computed(() => fn(...sources.map((s) => s())))\n}\n","import { computed } from \"@pyreon/reactivity\"\nimport type { ReadableSignal } from \"./types\"\nimport { isSignal } from \"./types\"\n\n/**\n * Pipe a signal through a chain of transform functions.\n * Each transform receives the resolved value (not the signal) and returns a new value.\n * The entire chain is wrapped in a single `computed()`.\n *\n * @example\n * ```ts\n * const topRisks = rx.pipe(\n * findings,\n * (items) => items.filter(f => f.severity === \"critical\"),\n * (items) => items.sort((a, b) => b.score - a.score),\n * (items) => items.slice(0, 10),\n * )\n * // topRisks() → reactive, type-safe\n * ```\n */\nexport function pipe<A, B>(\n source: ReadableSignal<A>,\n f1: (a: A) => B,\n): ReturnType<typeof computed<B>>\nexport function pipe<A, B, C>(\n source: ReadableSignal<A>,\n f1: (a: A) => B,\n f2: (b: B) => C,\n): ReturnType<typeof computed<C>>\nexport function pipe<A, B, C, D>(\n source: ReadableSignal<A>,\n f1: (a: A) => B,\n f2: (b: B) => C,\n f3: (c: C) => D,\n): ReturnType<typeof computed<D>>\nexport function pipe<A, B, C, D, E>(\n source: ReadableSignal<A>,\n f1: (a: A) => B,\n f2: (b: B) => C,\n f3: (c: C) => D,\n f4: (d: D) => E,\n): ReturnType<typeof computed<E>>\nexport function pipe<A, B, C, D, E, F>(\n source: ReadableSignal<A>,\n f1: (a: A) => B,\n f2: (b: B) => C,\n f3: (c: C) => D,\n f4: (d: D) => E,\n f5: (e: E) => F,\n): ReturnType<typeof computed<F>>\n// Plain value overloads\nexport function pipe<A, B>(source: A, f1: (a: A) => B): B\nexport function pipe<A, B, C>(source: A, f1: (a: A) => B, f2: (b: B) => C): C\nexport function pipe<A, B, C, D>(source: A, f1: (a: A) => B, f2: (b: B) => C, f3: (c: C) => D): D\nexport function pipe<A, B, C, D, E>(\n source: A,\n f1: (a: A) => B,\n f2: (b: B) => C,\n f3: (c: C) => D,\n f4: (d: D) => E,\n): E\nexport function pipe<A, B, C, D, E, F>(\n source: A,\n f1: (a: A) => B,\n f2: (b: B) => C,\n f3: (c: C) => D,\n f4: (d: D) => E,\n f5: (e: E) => F,\n): F\nexport function pipe(source: any, ...fns: Array<(v: any) => any>): any {\n if (isSignal(source)) {\n return computed(() => {\n let val = (source as ReadableSignal<any>)()\n for (const fn of fns) val = fn(val)\n return val\n })\n }\n let val = source\n for (const fn of fns) val = fn(val)\n return val\n}\n","import { computed } from \"@pyreon/reactivity\"\nimport type { ReadableSignal } from \"./types\"\nimport { isSignal } from \"./types\"\n\n/**\n * Search items by substring matching across specified keys.\n * Case-insensitive. Signal-aware — reactive when either source or query is a signal.\n *\n * @example\n * ```ts\n * const results = rx.search(users, searchQuery, [\"name\", \"email\"])\n * ```\n */\nexport function search<T>(\n source: ReadableSignal<T[]> | T[],\n query: ReadableSignal<string> | string,\n keys: (keyof T)[],\n): any {\n const isReactive = isSignal(source) || isSignal(query)\n\n const doSearch = (): T[] => {\n const arr = isSignal(source) ? (source as ReadableSignal<T[]>)() : source\n const q = (isSignal(query) ? (query as ReadableSignal<string>)() : query).toLowerCase().trim()\n if (!q) return arr\n return arr.filter((item) =>\n keys.some((key) => {\n const val = item[key]\n return typeof val === \"string\" && val.toLowerCase().includes(q)\n }),\n )\n }\n\n return isReactive ? computed(doSearch) : doSearch()\n}\n","import { effect, signal } from \"@pyreon/reactivity\"\nimport type { ReadableSignal } from \"./types\"\n\n/**\n * Debounce a signal — emits the latest value after `ms` of silence.\n * Returns a new signal that updates only after the source stops changing.\n *\n * Works both inside and outside component context.\n * The returned signal has a `.dispose()` method to stop tracking.\n */\nexport function debounce<T>(\n source: ReadableSignal<T>,\n ms: number,\n): ReadableSignal<T> & { dispose: () => void } {\n const debounced = signal(source())\n let timer: ReturnType<typeof setTimeout> | undefined\n\n const fx = effect(() => {\n const val = source()\n if (timer) clearTimeout(timer)\n timer = setTimeout(() => debounced.set(val), ms)\n })\n\n const dispose = () => {\n if (timer) clearTimeout(timer)\n fx.dispose()\n }\n\n return Object.assign(debounced as ReadableSignal<T>, { dispose })\n}\n\n/**\n * Throttle a signal — emits at most once every `ms` milliseconds.\n * Immediately emits on first change, then waits for the interval.\n *\n * Works both inside and outside component context.\n * The returned signal has a `.dispose()` method to stop tracking.\n */\nexport function throttle<T>(\n source: ReadableSignal<T>,\n ms: number,\n): ReadableSignal<T> & { dispose: () => void } {\n const throttled = signal(source())\n let lastEmit = 0\n let timer: ReturnType<typeof setTimeout> | undefined\n\n const fx = effect(() => {\n const val = source()\n const now = Date.now()\n const elapsed = now - lastEmit\n\n if (elapsed >= ms) {\n throttled.set(val)\n lastEmit = now\n } else {\n if (timer) clearTimeout(timer)\n timer = setTimeout(() => {\n throttled.set(val)\n lastEmit = Date.now()\n }, ms - elapsed)\n }\n })\n\n const dispose = () => {\n if (timer) clearTimeout(timer)\n fx.dispose()\n }\n\n return Object.assign(throttled as ReadableSignal<T>, { dispose })\n}\n","import { average, count, max, min, sum } from \"./aggregation\"\nimport {\n chunk,\n filter,\n find,\n flatten,\n groupBy,\n keyBy,\n last,\n map,\n mapValues,\n skip,\n sortBy,\n take,\n uniqBy,\n} from \"./collections\"\nimport { combine, distinct, scan } from \"./operators\"\nimport { pipe } from \"./pipe\"\nimport { search } from \"./search\"\nimport { debounce, throttle } from \"./timing\"\n\nexport type { KeyOf, ReadableSignal } from \"./types\"\n\n/**\n * Signal-aware reactive transforms.\n *\n * Every function is overloaded:\n * - `Signal<T[]>` input → returns `Computed<R>` (reactive)\n * - `T[]` input → returns `R` (static)\n *\n * @example\n * ```ts\n * import { rx } from \"@pyreon/rx\"\n *\n * const users = signal<User[]>([])\n * const active = rx.filter(users, u => u.active) // Computed<User[]>\n * const sorted = rx.sortBy(active, \"name\") // Computed<User[]>\n * const top10 = rx.take(sorted, 10) // Computed<User[]>\n *\n * // Or pipe:\n * const result = rx.pipe(users,\n * items => items.filter(u => u.active),\n * items => items.sort((a, b) => a.name.localeCompare(b.name)),\n * items => items.slice(0, 10),\n * )\n * ```\n */\nexport const rx = {\n // Collections\n filter,\n map,\n sortBy,\n groupBy,\n keyBy,\n uniqBy,\n take,\n skip,\n last,\n chunk,\n flatten,\n find,\n mapValues,\n\n // Aggregation\n count,\n sum,\n min,\n max,\n average,\n\n // Operators\n distinct,\n scan,\n combine,\n\n // Timing\n debounce,\n throttle,\n\n // Search\n search,\n\n // Pipe\n pipe,\n} as const\n\n// Also export individual functions for tree-shaking\nexport {\n average,\n chunk,\n combine,\n count,\n debounce,\n distinct,\n filter,\n find,\n flatten,\n groupBy,\n keyBy,\n last,\n map,\n mapValues,\n max,\n min,\n pipe,\n scan,\n search,\n skip,\n sortBy,\n sum,\n take,\n throttle,\n uniqBy,\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["reactive"],"sources":["../src/types.ts","../src/aggregation.ts","../src/collections.ts","../src/operators.ts","../src/pipe.ts","../src/search.ts","../src/timing.ts","../src/index.ts"],"sourcesContent":["import type { computed, signal } from '@pyreon/reactivity'\n\n/** A readable signal — any callable that returns a value and tracks subscribers. */\nexport type ReadableSignal<T> = (() => T) & { peek?: () => T }\n\n/** Result of a signal-aware transform — Computed when input is signal, plain when not. */\nexport type ReactiveResult<TInput, TOutput> =\n TInput extends ReadableSignal<any> ? ReturnType<typeof computed<TOutput>> : TOutput\n\n/** Key extractor — string key name or function. */\nexport type KeyOf<T> = keyof T | ((item: T) => string | number)\n\n/** Resolve a key extractor to a function. */\nexport function resolveKey<T>(key: KeyOf<T>): (item: T) => string | number {\n return typeof key === 'function' ? key : (item: T) => String(item[key])\n}\n\n/** Check if a value is a signal (callable function with .set or .peek). */\nexport function isSignal<T>(value: unknown): value is ReadableSignal<T> {\n return typeof value === 'function'\n}\n","import { computed } from '@pyreon/reactivity'\nimport type { KeyOf, ReadableSignal } from './types'\nimport { isSignal, resolveKey } from './types'\n\nfunction reactive<TIn, TOut>(source: TIn, fn: (val: any) => TOut): any {\n if (isSignal(source)) return computed(() => fn((source as ReadableSignal<any>)()))\n return fn(source)\n}\n\n/** Count items in collection. */\nexport function count<T>(source: ReadableSignal<T[]>): ReturnType<typeof computed<number>>\nexport function count<T>(source: T[]): number\nexport function count<T>(source: ReadableSignal<T[]> | T[]): any {\n return reactive(source, (arr: T[]) => arr.length)\n}\n\n/** Sum numeric values. Optionally by key. */\nexport function sum<T>(\n source: ReadableSignal<T[]>,\n key?: KeyOf<T>,\n): ReturnType<typeof computed<number>>\nexport function sum<T>(source: T[], key?: KeyOf<T>): number\nexport function sum<T>(source: ReadableSignal<T[]> | T[], key?: KeyOf<T>): any {\n const getVal = key ? resolveKey(key) : (item: T) => item as unknown as number\n return reactive(source, (arr: T[]) => arr.reduce((acc, item) => acc + Number(getVal(item)), 0))\n}\n\n/** Find minimum item. Optionally by key. */\nexport function min<T>(\n source: ReadableSignal<T[]>,\n key?: KeyOf<T>,\n): ReturnType<typeof computed<T | undefined>>\nexport function min<T>(source: T[], key?: KeyOf<T>): T | undefined\nexport function min<T>(source: ReadableSignal<T[]> | T[], key?: KeyOf<T>): any {\n const getVal = key ? resolveKey(key) : (item: T) => item as unknown as number\n return reactive(source, (arr: T[]) => {\n if (arr.length === 0) return undefined\n let result = arr[0] as T\n let minVal = Number(getVal(result))\n for (let i = 1; i < arr.length; i++) {\n const val = Number(getVal(arr[i] as T))\n if (val < minVal) {\n minVal = val\n result = arr[i] as T\n }\n }\n return result\n })\n}\n\n/** Find maximum item. Optionally by key. */\nexport function max<T>(\n source: ReadableSignal<T[]>,\n key?: KeyOf<T>,\n): ReturnType<typeof computed<T | undefined>>\nexport function max<T>(source: T[], key?: KeyOf<T>): T | undefined\nexport function max<T>(source: ReadableSignal<T[]> | T[], key?: KeyOf<T>): any {\n const getVal = key ? resolveKey(key) : (item: T) => item as unknown as number\n return reactive(source, (arr: T[]) => {\n if (arr.length === 0) return undefined\n let result = arr[0] as T\n let maxVal = Number(getVal(result))\n for (let i = 1; i < arr.length; i++) {\n const val = Number(getVal(arr[i] as T))\n if (val > maxVal) {\n maxVal = val\n result = arr[i] as T\n }\n }\n return result\n })\n}\n\n/** Average of numeric values. Optionally by key. */\nexport function average<T>(\n source: ReadableSignal<T[]>,\n key?: KeyOf<T>,\n): ReturnType<typeof computed<number>>\nexport function average<T>(source: T[], key?: KeyOf<T>): number\nexport function average<T>(source: ReadableSignal<T[]> | T[], key?: KeyOf<T>): any {\n const getVal = key ? resolveKey(key) : (item: T) => item as unknown as number\n return reactive(source, (arr: T[]) => {\n if (arr.length === 0) return 0\n return arr.reduce((acc, item) => acc + Number(getVal(item)), 0) / arr.length\n })\n}\n","import { computed } from '@pyreon/reactivity'\nimport type { KeyOf, ReadableSignal } from './types'\nimport { isSignal, resolveKey } from './types'\n\n// ─── Helpers ────────────────────────────────────────────────────────────────\n\nfunction reactive<TIn, TOut>(\n source: TIn,\n fn: (val: any) => TOut,\n): TIn extends ReadableSignal<any> ? ReturnType<typeof computed<TOut>> : TOut {\n if (isSignal(source)) {\n return computed(() => fn((source as ReadableSignal<any>)())) as any\n }\n return fn(source) as any\n}\n\n// ─── Collection transforms ──────────────────────────────────────────────────\n\n/** Filter items by predicate. Signal in → Computed out. */\nexport function filter<T>(\n source: ReadableSignal<T[]>,\n predicate: (item: T, index: number) => boolean,\n): ReturnType<typeof computed<T[]>>\nexport function filter<T>(source: T[], predicate: (item: T, index: number) => boolean): T[]\nexport function filter<T>(\n source: ReadableSignal<T[]> | T[],\n predicate: (item: T, index: number) => boolean,\n): any {\n return reactive(source, (arr: T[]) => arr.filter(predicate))\n}\n\n/** Map items to a new type. */\nexport function map<T, U>(\n source: ReadableSignal<T[]>,\n fn: (item: T, index: number) => U,\n): ReturnType<typeof computed<U[]>>\nexport function map<T, U>(source: T[], fn: (item: T, index: number) => U): U[]\nexport function map<T, U>(\n source: ReadableSignal<T[]> | T[],\n fn: (item: T, index: number) => U,\n): any {\n return reactive(source, (arr: T[]) => arr.map(fn))\n}\n\n/** Sort items by key or comparator. */\nexport function sortBy<T>(\n source: ReadableSignal<T[]>,\n key: KeyOf<T>,\n): ReturnType<typeof computed<T[]>>\nexport function sortBy<T>(source: T[], key: KeyOf<T>): T[]\nexport function sortBy<T>(source: ReadableSignal<T[]> | T[], key: KeyOf<T>): any {\n const getKey = resolveKey(key)\n return reactive(source, (arr: T[]) =>\n [...arr].sort((a, b) => {\n const ka = getKey(a)\n const kb = getKey(b)\n return ka < kb ? -1 : ka > kb ? 1 : 0\n }),\n )\n}\n\n/** Group items by key. Returns Record<string, T[]>. */\nexport function groupBy<T>(\n source: ReadableSignal<T[]>,\n key: KeyOf<T>,\n): ReturnType<typeof computed<Record<string, T[]>>>\nexport function groupBy<T>(source: T[], key: KeyOf<T>): Record<string, T[]>\nexport function groupBy<T>(source: ReadableSignal<T[]> | T[], key: KeyOf<T>): any {\n const getKey = resolveKey(key)\n return reactive(source, (arr: T[]) => {\n const result: Record<string, T[]> = {}\n for (const item of arr) {\n const k = String(getKey(item))\n let group = result[k]\n if (!group) {\n group = []\n result[k] = group\n }\n group.push(item)\n }\n return result\n })\n}\n\n/** Index items by key. Returns Record<string, T> (last wins on collision). */\nexport function keyBy<T>(\n source: ReadableSignal<T[]>,\n key: KeyOf<T>,\n): ReturnType<typeof computed<Record<string, T>>>\nexport function keyBy<T>(source: T[], key: KeyOf<T>): Record<string, T>\nexport function keyBy<T>(source: ReadableSignal<T[]> | T[], key: KeyOf<T>): any {\n const getKey = resolveKey(key)\n return reactive(source, (arr: T[]) => {\n const result: Record<string, T> = {}\n for (const item of arr) result[String(getKey(item))] = item\n return result\n })\n}\n\n/** Deduplicate items by key. */\nexport function uniqBy<T>(\n source: ReadableSignal<T[]>,\n key: KeyOf<T>,\n): ReturnType<typeof computed<T[]>>\nexport function uniqBy<T>(source: T[], key: KeyOf<T>): T[]\nexport function uniqBy<T>(source: ReadableSignal<T[]> | T[], key: KeyOf<T>): any {\n const getKey = resolveKey(key)\n return reactive(source, (arr: T[]) => {\n const seen = new Set<string | number>()\n return arr.filter((item) => {\n const k = getKey(item)\n if (seen.has(k)) return false\n seen.add(k)\n return true\n })\n })\n}\n\n/** Take the first n items. */\nexport function take<T>(source: ReadableSignal<T[]>, n: number): ReturnType<typeof computed<T[]>>\nexport function take<T>(source: T[], n: number): T[]\nexport function take<T>(source: ReadableSignal<T[]> | T[], n: number): any {\n return reactive(source, (arr: T[]) => arr.slice(0, n))\n}\n\n/** Split into chunks of given size. */\nexport function chunk<T>(\n source: ReadableSignal<T[]>,\n size: number,\n): ReturnType<typeof computed<T[][]>>\nexport function chunk<T>(source: T[], size: number): T[][]\nexport function chunk<T>(source: ReadableSignal<T[]> | T[], size: number): any {\n return reactive(source, (arr: T[]) => {\n const result: T[][] = []\n for (let i = 0; i < arr.length; i += size) result.push(arr.slice(i, i + size))\n return result\n })\n}\n\n/** Flatten one level of nesting. */\nexport function flatten<T>(source: ReadableSignal<T[][]>): ReturnType<typeof computed<T[]>>\nexport function flatten<T>(source: T[][]): T[]\nexport function flatten<T>(source: ReadableSignal<T[][]> | T[][]): any {\n return reactive(source, (arr: T[][]) => arr.flat())\n}\n\n/** Find the first item matching a predicate. */\nexport function find<T>(\n source: ReadableSignal<T[]>,\n predicate: (item: T) => boolean,\n): ReturnType<typeof computed<T | undefined>>\nexport function find<T>(source: T[], predicate: (item: T) => boolean): T | undefined\nexport function find<T>(source: ReadableSignal<T[]> | T[], predicate: (item: T) => boolean): any {\n return reactive(source, (arr: T[]) => arr.find(predicate))\n}\n\n/** Skip the first n items. */\nexport function skip<T>(source: ReadableSignal<T[]>, n: number): ReturnType<typeof computed<T[]>>\nexport function skip<T>(source: T[], n: number): T[]\nexport function skip<T>(source: ReadableSignal<T[]> | T[], n: number): any {\n return reactive(source, (arr: T[]) => arr.slice(n))\n}\n\n/** Take the last n items. */\nexport function last<T>(source: ReadableSignal<T[]>, n: number): ReturnType<typeof computed<T[]>>\nexport function last<T>(source: T[], n: number): T[]\nexport function last<T>(source: ReadableSignal<T[]> | T[], n: number): any {\n return reactive(source, (arr: T[]) => arr.slice(-n))\n}\n\n/** Map over values of a Record. Useful after groupBy. */\nexport function mapValues<T, U>(\n source: ReadableSignal<Record<string, T>>,\n fn: (value: T, key: string) => U,\n): ReturnType<typeof computed<Record<string, U>>>\nexport function mapValues<T, U>(\n source: Record<string, T>,\n fn: (value: T, key: string) => U,\n): Record<string, U>\nexport function mapValues<T, U>(\n source: ReadableSignal<Record<string, T>> | Record<string, T>,\n fn: (value: T, key: string) => U,\n): any {\n return reactive(source, (obj: Record<string, T>) => {\n const result: Record<string, U> = {}\n for (const key of Object.keys(obj)) result[key] = fn(obj[key] as T, key)\n return result\n })\n}\n","import { computed, effect, signal } from '@pyreon/reactivity'\nimport type { ReadableSignal } from './types'\n\n/**\n * Distinct — skip consecutive duplicate values from a signal.\n * Uses `Object.is` by default, or a custom equality function.\n */\nexport function distinct<T>(\n source: ReadableSignal<T>,\n equals: (a: T, b: T) => boolean = Object.is,\n): ReadableSignal<T> {\n const result = signal(source())\n\n effect(() => {\n const val = source()\n if (!equals(val, result.peek())) {\n result.set(val)\n }\n })\n\n return result\n}\n\n/**\n * Scan — running accumulator over signal changes.\n * Like Array.reduce but emits the accumulated value on each source change.\n *\n * @example\n * ```ts\n * const clicks = signal(0)\n * const total = rx.scan(clicks, (acc, val) => acc + val, 0)\n * // clicks: 1 → total: 1\n * // clicks: 3 → total: 4\n * // clicks: 2 → total: 6\n * ```\n */\nexport function scan<T, U>(\n source: ReadableSignal<T>,\n reducer: (acc: U, value: T) => U,\n initial: U,\n): ReadableSignal<U> {\n const result = signal(initial)\n\n effect(() => {\n const val = source()\n result.set(reducer(result.peek(), val))\n })\n\n return result\n}\n\n/**\n * Combine multiple signals into a single computed value.\n *\n * @example\n * ```ts\n * const fullName = rx.combine(firstName, lastName, (f, l) => `${f} ${l}`)\n * ```\n */\nexport function combine<A, B, R>(\n a: ReadableSignal<A>,\n b: ReadableSignal<B>,\n fn: (a: A, b: B) => R,\n): ReturnType<typeof computed<R>>\nexport function combine<A, B, C, R>(\n a: ReadableSignal<A>,\n b: ReadableSignal<B>,\n c: ReadableSignal<C>,\n fn: (a: A, b: B, c: C) => R,\n): ReturnType<typeof computed<R>>\nexport function combine(...args: any[]): any {\n const fn = args[args.length - 1] as (...vals: any[]) => any\n const sources = args.slice(0, -1) as ReadableSignal<any>[]\n return computed(() => fn(...sources.map((s) => s())))\n}\n","import { computed } from '@pyreon/reactivity'\nimport type { ReadableSignal } from './types'\nimport { isSignal } from './types'\n\n/**\n * Pipe a signal through a chain of transform functions.\n * Each transform receives the resolved value (not the signal) and returns a new value.\n * The entire chain is wrapped in a single `computed()`.\n *\n * @example\n * ```ts\n * const topRisks = rx.pipe(\n * findings,\n * (items) => items.filter(f => f.severity === \"critical\"),\n * (items) => items.sort((a, b) => b.score - a.score),\n * (items) => items.slice(0, 10),\n * )\n * // topRisks() → reactive, type-safe\n * ```\n */\nexport function pipe<A, B>(\n source: ReadableSignal<A>,\n f1: (a: A) => B,\n): ReturnType<typeof computed<B>>\nexport function pipe<A, B, C>(\n source: ReadableSignal<A>,\n f1: (a: A) => B,\n f2: (b: B) => C,\n): ReturnType<typeof computed<C>>\nexport function pipe<A, B, C, D>(\n source: ReadableSignal<A>,\n f1: (a: A) => B,\n f2: (b: B) => C,\n f3: (c: C) => D,\n): ReturnType<typeof computed<D>>\nexport function pipe<A, B, C, D, E>(\n source: ReadableSignal<A>,\n f1: (a: A) => B,\n f2: (b: B) => C,\n f3: (c: C) => D,\n f4: (d: D) => E,\n): ReturnType<typeof computed<E>>\nexport function pipe<A, B, C, D, E, F>(\n source: ReadableSignal<A>,\n f1: (a: A) => B,\n f2: (b: B) => C,\n f3: (c: C) => D,\n f4: (d: D) => E,\n f5: (e: E) => F,\n): ReturnType<typeof computed<F>>\n// Plain value overloads\nexport function pipe<A, B>(source: A, f1: (a: A) => B): B\nexport function pipe<A, B, C>(source: A, f1: (a: A) => B, f2: (b: B) => C): C\nexport function pipe<A, B, C, D>(source: A, f1: (a: A) => B, f2: (b: B) => C, f3: (c: C) => D): D\nexport function pipe<A, B, C, D, E>(\n source: A,\n f1: (a: A) => B,\n f2: (b: B) => C,\n f3: (c: C) => D,\n f4: (d: D) => E,\n): E\nexport function pipe<A, B, C, D, E, F>(\n source: A,\n f1: (a: A) => B,\n f2: (b: B) => C,\n f3: (c: C) => D,\n f4: (d: D) => E,\n f5: (e: E) => F,\n): F\nexport function pipe(source: any, ...fns: Array<(v: any) => any>): any {\n if (isSignal(source)) {\n return computed(() => {\n let val = (source as ReadableSignal<any>)()\n for (const fn of fns) val = fn(val)\n return val\n })\n }\n let val = source\n for (const fn of fns) val = fn(val)\n return val\n}\n","import { computed } from '@pyreon/reactivity'\nimport type { ReadableSignal } from './types'\nimport { isSignal } from './types'\n\n/**\n * Search items by substring matching across specified keys.\n * Case-insensitive. Signal-aware — reactive when either source or query is a signal.\n *\n * @example\n * ```ts\n * const results = rx.search(users, searchQuery, [\"name\", \"email\"])\n * ```\n */\nexport function search<T>(\n source: ReadableSignal<T[]> | T[],\n query: ReadableSignal<string> | string,\n keys: (keyof T)[],\n): any {\n const isReactive = isSignal(source) || isSignal(query)\n\n const doSearch = (): T[] => {\n const arr = isSignal(source) ? (source as ReadableSignal<T[]>)() : source\n const q = (isSignal(query) ? (query as ReadableSignal<string>)() : query).toLowerCase().trim()\n if (!q) return arr\n return arr.filter((item) =>\n keys.some((key) => {\n const val = item[key]\n return typeof val === 'string' && val.toLowerCase().includes(q)\n }),\n )\n }\n\n return isReactive ? computed(doSearch) : doSearch()\n}\n","import { effect, signal } from '@pyreon/reactivity'\nimport type { ReadableSignal } from './types'\n\n/**\n * Debounce a signal — emits the latest value after `ms` of silence.\n * Returns a new signal that updates only after the source stops changing.\n *\n * Works both inside and outside component context.\n * The returned signal has a `.dispose()` method to stop tracking.\n */\nexport function debounce<T>(\n source: ReadableSignal<T>,\n ms: number,\n): ReadableSignal<T> & { dispose: () => void } {\n const debounced = signal(source())\n let timer: ReturnType<typeof setTimeout> | undefined\n\n const fx = effect(() => {\n const val = source()\n if (timer) clearTimeout(timer)\n timer = setTimeout(() => debounced.set(val), ms)\n })\n\n const dispose = () => {\n if (timer) clearTimeout(timer)\n fx.dispose()\n }\n\n return Object.assign(debounced as ReadableSignal<T>, { dispose })\n}\n\n/**\n * Throttle a signal — emits at most once every `ms` milliseconds.\n * Immediately emits on first change, then waits for the interval.\n *\n * Works both inside and outside component context.\n * The returned signal has a `.dispose()` method to stop tracking.\n */\nexport function throttle<T>(\n source: ReadableSignal<T>,\n ms: number,\n): ReadableSignal<T> & { dispose: () => void } {\n const throttled = signal(source())\n let lastEmit = 0\n let timer: ReturnType<typeof setTimeout> | undefined\n\n const fx = effect(() => {\n const val = source()\n const now = Date.now()\n const elapsed = now - lastEmit\n\n if (elapsed >= ms) {\n throttled.set(val)\n lastEmit = now\n } else {\n if (timer) clearTimeout(timer)\n timer = setTimeout(() => {\n throttled.set(val)\n lastEmit = Date.now()\n }, ms - elapsed)\n }\n })\n\n const dispose = () => {\n if (timer) clearTimeout(timer)\n fx.dispose()\n }\n\n return Object.assign(throttled as ReadableSignal<T>, { dispose })\n}\n","import { average, count, max, min, sum } from './aggregation'\nimport {\n chunk,\n filter,\n find,\n flatten,\n groupBy,\n keyBy,\n last,\n map,\n mapValues,\n skip,\n sortBy,\n take,\n uniqBy,\n} from './collections'\nimport { combine, distinct, scan } from './operators'\nimport { pipe } from './pipe'\nimport { search } from './search'\nimport { debounce, throttle } from './timing'\n\nexport type { KeyOf, ReadableSignal } from './types'\n\n/**\n * Signal-aware reactive transforms.\n *\n * Every function is overloaded:\n * - `Signal<T[]>` input → returns `Computed<R>` (reactive)\n * - `T[]` input → returns `R` (static)\n *\n * @example\n * ```ts\n * import { rx } from \"@pyreon/rx\"\n *\n * const users = signal<User[]>([])\n * const active = rx.filter(users, u => u.active) // Computed<User[]>\n * const sorted = rx.sortBy(active, \"name\") // Computed<User[]>\n * const top10 = rx.take(sorted, 10) // Computed<User[]>\n *\n * // Or pipe:\n * const result = rx.pipe(users,\n * items => items.filter(u => u.active),\n * items => items.sort((a, b) => a.name.localeCompare(b.name)),\n * items => items.slice(0, 10),\n * )\n * ```\n */\nexport const rx = {\n // Collections\n filter,\n map,\n sortBy,\n groupBy,\n keyBy,\n uniqBy,\n take,\n skip,\n last,\n chunk,\n flatten,\n find,\n mapValues,\n\n // Aggregation\n count,\n sum,\n min,\n max,\n average,\n\n // Operators\n distinct,\n scan,\n combine,\n\n // Timing\n debounce,\n throttle,\n\n // Search\n search,\n\n // Pipe\n pipe,\n} as const\n\n// Also export individual functions for tree-shaking\nexport {\n average,\n chunk,\n combine,\n count,\n debounce,\n distinct,\n filter,\n find,\n flatten,\n groupBy,\n keyBy,\n last,\n map,\n mapValues,\n max,\n min,\n pipe,\n scan,\n search,\n skip,\n sortBy,\n sum,\n take,\n throttle,\n uniqBy,\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,19 +1,16 @@
1
1
  {
2
2
  "name": "@pyreon/rx",
3
- "version": "0.11.4",
3
+ "version": "0.11.6",
4
4
  "description": "Signal-aware reactive transforms — filter, map, sort, group, pipe for Pyreon signals",
5
+ "bugs": {
6
+ "url": "https://github.com/pyreon/pyreon/issues"
7
+ },
5
8
  "license": "MIT",
6
9
  "repository": {
7
10
  "type": "git",
8
11
  "url": "https://github.com/pyreon/pyreon.git",
9
12
  "directory": "packages/fundamentals/rx"
10
13
  },
11
- "bugs": {
12
- "url": "https://github.com/pyreon/pyreon/issues"
13
- },
14
- "publishConfig": {
15
- "access": "public"
16
- },
17
14
  "files": [
18
15
  "lib",
19
16
  "src",
@@ -29,17 +26,20 @@
29
26
  "types": "./lib/types/index.d.ts"
30
27
  }
31
28
  },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
32
  "scripts": {
33
33
  "build": "vl_rolldown_build",
34
34
  "dev": "vl_rolldown_build-watch",
35
35
  "test": "vitest run",
36
36
  "typecheck": "tsc --noEmit",
37
- "lint": "biome check ."
38
- },
39
- "peerDependencies": {
40
- "@pyreon/reactivity": "^0.11.4"
37
+ "lint": "oxlint ."
41
38
  },
42
39
  "devDependencies": {
43
- "@pyreon/typescript": "^0.11.4"
40
+ "@pyreon/typescript": "^0.11.6"
41
+ },
42
+ "peerDependencies": {
43
+ "@pyreon/reactivity": "^0.11.6"
44
44
  }
45
45
  }
@@ -1,6 +1,6 @@
1
- import { computed } from "@pyreon/reactivity"
2
- import type { KeyOf, ReadableSignal } from "./types"
3
- import { isSignal, resolveKey } from "./types"
1
+ import { computed } from '@pyreon/reactivity'
2
+ import type { KeyOf, ReadableSignal } from './types'
3
+ import { isSignal, resolveKey } from './types'
4
4
 
5
5
  function reactive<TIn, TOut>(source: TIn, fn: (val: any) => TOut): any {
6
6
  if (isSignal(source)) return computed(() => fn((source as ReadableSignal<any>)()))
@@ -1,6 +1,6 @@
1
- import { computed } from "@pyreon/reactivity"
2
- import type { KeyOf, ReadableSignal } from "./types"
3
- import { isSignal, resolveKey } from "./types"
1
+ import { computed } from '@pyreon/reactivity'
2
+ import type { KeyOf, ReadableSignal } from './types'
3
+ import { isSignal, resolveKey } from './types'
4
4
 
5
5
  // ─── Helpers ────────────────────────────────────────────────────────────────
6
6
 
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { average, count, max, min, sum } from "./aggregation"
1
+ import { average, count, max, min, sum } from './aggregation'
2
2
  import {
3
3
  chunk,
4
4
  filter,
@@ -13,13 +13,13 @@ import {
13
13
  sortBy,
14
14
  take,
15
15
  uniqBy,
16
- } from "./collections"
17
- import { combine, distinct, scan } from "./operators"
18
- import { pipe } from "./pipe"
19
- import { search } from "./search"
20
- import { debounce, throttle } from "./timing"
16
+ } from './collections'
17
+ import { combine, distinct, scan } from './operators'
18
+ import { pipe } from './pipe'
19
+ import { search } from './search'
20
+ import { debounce, throttle } from './timing'
21
21
 
22
- export type { KeyOf, ReadableSignal } from "./types"
22
+ export type { KeyOf, ReadableSignal } from './types'
23
23
 
24
24
  /**
25
25
  * Signal-aware reactive transforms.
package/src/operators.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { computed, effect, signal } from "@pyreon/reactivity"
2
- import type { ReadableSignal } from "./types"
1
+ import { computed, effect, signal } from '@pyreon/reactivity'
2
+ import type { ReadableSignal } from './types'
3
3
 
4
4
  /**
5
5
  * Distinct — skip consecutive duplicate values from a signal.
package/src/pipe.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { computed } from "@pyreon/reactivity"
2
- import type { ReadableSignal } from "./types"
3
- import { isSignal } from "./types"
1
+ import { computed } from '@pyreon/reactivity'
2
+ import type { ReadableSignal } from './types'
3
+ import { isSignal } from './types'
4
4
 
5
5
  /**
6
6
  * Pipe a signal through a chain of transform functions.
package/src/search.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { computed } from "@pyreon/reactivity"
2
- import type { ReadableSignal } from "./types"
3
- import { isSignal } from "./types"
1
+ import { computed } from '@pyreon/reactivity'
2
+ import type { ReadableSignal } from './types'
3
+ import { isSignal } from './types'
4
4
 
5
5
  /**
6
6
  * Search items by substring matching across specified keys.
@@ -25,7 +25,7 @@ export function search<T>(
25
25
  return arr.filter((item) =>
26
26
  keys.some((key) => {
27
27
  const val = item[key]
28
- return typeof val === "string" && val.toLowerCase().includes(q)
28
+ return typeof val === 'string' && val.toLowerCase().includes(q)
29
29
  }),
30
30
  )
31
31
  }
@@ -1,53 +1,53 @@
1
- import { signal } from "@pyreon/reactivity"
2
- import { describe, expect, it } from "vitest"
3
- import { average, count, max, min, sum } from "../aggregation"
1
+ import { signal } from '@pyreon/reactivity'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { average, count, max, min, sum } from '../aggregation'
4
4
 
5
- describe("aggregation — plain values", () => {
6
- it("count", () => {
5
+ describe('aggregation — plain values', () => {
6
+ it('count', () => {
7
7
  expect(count([1, 2, 3])).toBe(3)
8
8
  expect(count([])).toBe(0)
9
9
  })
10
10
 
11
- it("sum", () => {
11
+ it('sum', () => {
12
12
  expect(sum([1, 2, 3])).toBe(6)
13
13
  })
14
14
 
15
- it("sum with key", () => {
15
+ it('sum with key', () => {
16
16
  const items = [{ v: 10 }, { v: 20 }, { v: 30 }]
17
- expect(sum(items, "v")).toBe(60)
17
+ expect(sum(items, 'v')).toBe(60)
18
18
  })
19
19
 
20
- it("min", () => {
20
+ it('min', () => {
21
21
  const items = [{ v: 3 }, { v: 1 }, { v: 2 }]
22
- expect(min(items, "v")?.v).toBe(1)
22
+ expect(min(items, 'v')?.v).toBe(1)
23
23
  })
24
24
 
25
- it("max", () => {
25
+ it('max', () => {
26
26
  const items = [{ v: 3 }, { v: 1 }, { v: 2 }]
27
- expect(max(items, "v")?.v).toBe(3)
27
+ expect(max(items, 'v')?.v).toBe(3)
28
28
  })
29
29
 
30
- it("min/max empty array", () => {
30
+ it('min/max empty array', () => {
31
31
  expect(min([])).toBeUndefined()
32
32
  expect(max([])).toBeUndefined()
33
33
  })
34
34
 
35
- it("average", () => {
35
+ it('average', () => {
36
36
  expect(average([2, 4, 6])).toBe(4)
37
37
  })
38
38
 
39
- it("average with key", () => {
39
+ it('average with key', () => {
40
40
  const items = [{ v: 10 }, { v: 20 }, { v: 30 }]
41
- expect(average(items, "v")).toBe(20)
41
+ expect(average(items, 'v')).toBe(20)
42
42
  })
43
43
 
44
- it("average empty array returns 0", () => {
44
+ it('average empty array returns 0', () => {
45
45
  expect(average([])).toBe(0)
46
46
  })
47
47
  })
48
48
 
49
- describe("aggregation — signal values", () => {
50
- it("count returns computed", () => {
49
+ describe('aggregation — signal values', () => {
50
+ it('count returns computed', () => {
51
51
  const src = signal([1, 2, 3])
52
52
  const c = count(src)
53
53
  expect(c()).toBe(3)
@@ -55,7 +55,7 @@ describe("aggregation — signal values", () => {
55
55
  expect(c()).toBe(1)
56
56
  })
57
57
 
58
- it("sum returns computed", () => {
58
+ it('sum returns computed', () => {
59
59
  const src = signal([1, 2, 3])
60
60
  const s = sum(src)
61
61
  expect(s()).toBe(6)
@@ -63,7 +63,7 @@ describe("aggregation — signal values", () => {
63
63
  expect(s()).toBe(30)
64
64
  })
65
65
 
66
- it("average returns computed", () => {
66
+ it('average returns computed', () => {
67
67
  const src = signal([2, 4, 6])
68
68
  const avg = average(src)
69
69
  expect(avg()).toBe(4)
@@ -72,9 +72,9 @@ describe("aggregation — signal values", () => {
72
72
  expect(avg()).toBe(15)
73
73
  })
74
74
 
75
- it("average with key returns computed", () => {
75
+ it('average with key returns computed', () => {
76
76
  const src = signal([{ v: 10 }, { v: 20 }, { v: 30 }])
77
- const avg = average(src, "v")
77
+ const avg = average(src, 'v')
78
78
  expect(avg()).toBe(20)
79
79
 
80
80
  src.set([{ v: 100 }])
@@ -1,5 +1,5 @@
1
- import { signal } from "@pyreon/reactivity"
2
- import { describe, expect, it } from "vitest"
1
+ import { signal } from '@pyreon/reactivity'
2
+ import { describe, expect, it } from 'vitest'
3
3
  import {
4
4
  chunk,
5
5
  filter,
@@ -14,131 +14,131 @@ import {
14
14
  sortBy,
15
15
  take,
16
16
  uniqBy,
17
- } from "../collections"
17
+ } from '../collections'
18
18
 
19
19
  type User = { id: number; name: string; role: string; active: boolean }
20
20
  const users: User[] = [
21
- { id: 1, name: "Alice", role: "admin", active: true },
22
- { id: 2, name: "Bob", role: "viewer", active: false },
23
- { id: 3, name: "Charlie", role: "admin", active: true },
24
- { id: 4, name: "Diana", role: "editor", active: true },
21
+ { id: 1, name: 'Alice', role: 'admin', active: true },
22
+ { id: 2, name: 'Bob', role: 'viewer', active: false },
23
+ { id: 3, name: 'Charlie', role: 'admin', active: true },
24
+ { id: 4, name: 'Diana', role: 'editor', active: true },
25
25
  ]
26
26
 
27
- describe("collections — plain values", () => {
28
- it("filter", () => {
27
+ describe('collections — plain values', () => {
28
+ it('filter', () => {
29
29
  expect(filter(users, (u) => u.active)).toHaveLength(3)
30
30
  })
31
31
 
32
- it("map", () => {
33
- expect(map(users, (u) => u.name)).toEqual(["Alice", "Bob", "Charlie", "Diana"])
32
+ it('map', () => {
33
+ expect(map(users, (u) => u.name)).toEqual(['Alice', 'Bob', 'Charlie', 'Diana'])
34
34
  })
35
35
 
36
- it("sortBy string key", () => {
37
- const sorted = sortBy(users, "name")
38
- expect(sorted[0]!.name).toBe("Alice")
39
- expect(sorted[3]!.name).toBe("Diana")
36
+ it('sortBy string key', () => {
37
+ const sorted = sortBy(users, 'name')
38
+ expect(sorted[0]!.name).toBe('Alice')
39
+ expect(sorted[3]!.name).toBe('Diana')
40
40
  })
41
41
 
42
- it("sortBy function", () => {
42
+ it('sortBy function', () => {
43
43
  const sorted = sortBy(users, (u) => u.id)
44
44
  expect(sorted[0]!.id).toBe(1)
45
45
  })
46
46
 
47
- it("groupBy", () => {
48
- const groups = groupBy(users, "role")
47
+ it('groupBy', () => {
48
+ const groups = groupBy(users, 'role')
49
49
  expect(groups.admin).toHaveLength(2)
50
50
  expect(groups.viewer).toHaveLength(1)
51
51
  })
52
52
 
53
- it("keyBy", () => {
54
- const indexed = keyBy(users, "id")
55
- expect(indexed["1"]!.name).toBe("Alice")
56
- expect(indexed["3"]!.name).toBe("Charlie")
53
+ it('keyBy', () => {
54
+ const indexed = keyBy(users, 'id')
55
+ expect(indexed['1']!.name).toBe('Alice')
56
+ expect(indexed['3']!.name).toBe('Charlie')
57
57
  })
58
58
 
59
- it("uniqBy", () => {
60
- const result = uniqBy(users, "role")
59
+ it('uniqBy', () => {
60
+ const result = uniqBy(users, 'role')
61
61
  expect(result).toHaveLength(3) // admin, viewer, editor
62
62
  })
63
63
 
64
- it("take", () => {
64
+ it('take', () => {
65
65
  expect(take(users, 2)).toHaveLength(2)
66
- expect(take(users, 2)[0]!.name).toBe("Alice")
66
+ expect(take(users, 2)[0]!.name).toBe('Alice')
67
67
  })
68
68
 
69
- it("chunk", () => {
69
+ it('chunk', () => {
70
70
  const chunks = chunk(users, 2)
71
71
  expect(chunks).toHaveLength(2)
72
72
  expect(chunks[0]).toHaveLength(2)
73
73
  })
74
74
 
75
- it("flatten", () => {
75
+ it('flatten', () => {
76
76
  const nested = [[1, 2], [3, 4], [5]]
77
77
  expect(flatten(nested)).toEqual([1, 2, 3, 4, 5])
78
78
  })
79
79
 
80
- it("find", () => {
81
- expect(find(users, (u) => u.name === "Bob")?.id).toBe(2)
82
- expect(find(users, (u) => u.name === "Nobody")).toBeUndefined()
80
+ it('find', () => {
81
+ expect(find(users, (u) => u.name === 'Bob')?.id).toBe(2)
82
+ expect(find(users, (u) => u.name === 'Nobody')).toBeUndefined()
83
83
  })
84
84
 
85
- it("skip", () => {
85
+ it('skip', () => {
86
86
  const result = skip(users, 2)
87
87
  expect(result).toHaveLength(2)
88
- expect(result[0]!.name).toBe("Charlie")
89
- expect(result[1]!.name).toBe("Diana")
88
+ expect(result[0]!.name).toBe('Charlie')
89
+ expect(result[1]!.name).toBe('Diana')
90
90
  })
91
91
 
92
- it("last", () => {
92
+ it('last', () => {
93
93
  const result = last(users, 2)
94
94
  expect(result).toHaveLength(2)
95
- expect(result[0]!.name).toBe("Charlie")
96
- expect(result[1]!.name).toBe("Diana")
95
+ expect(result[0]!.name).toBe('Charlie')
96
+ expect(result[1]!.name).toBe('Diana')
97
97
  })
98
98
 
99
- it("mapValues", () => {
99
+ it('mapValues', () => {
100
100
  const record: Record<string, number> = { a: 1, b: 2, c: 3 }
101
101
  const result = mapValues(record, (v) => v * 10)
102
102
  expect(result).toEqual({ a: 10, b: 20, c: 30 })
103
103
  })
104
104
 
105
- it("mapValues with key argument", () => {
105
+ it('mapValues with key argument', () => {
106
106
  const record: Record<string, number> = { x: 1, y: 2 }
107
107
  const result = mapValues(record, (v, k) => `${k}=${v}`)
108
- expect(result).toEqual({ x: "x=1", y: "y=2" })
108
+ expect(result).toEqual({ x: 'x=1', y: 'y=2' })
109
109
  })
110
110
  })
111
111
 
112
- describe("collections — signal values (reactive)", () => {
113
- it("filter returns computed that tracks signal", () => {
112
+ describe('collections — signal values (reactive)', () => {
113
+ it('filter returns computed that tracks signal', () => {
114
114
  const src = signal(users)
115
115
  const active = filter(src, (u) => u.active)
116
116
  expect(active()).toHaveLength(3)
117
117
 
118
118
  // Mutate source
119
- src.set([...users, { id: 5, name: "Eve", role: "admin", active: true }])
119
+ src.set([...users, { id: 5, name: 'Eve', role: 'admin', active: true }])
120
120
  expect(active()).toHaveLength(4)
121
121
  })
122
122
 
123
- it("map returns computed", () => {
123
+ it('map returns computed', () => {
124
124
  const src = signal(users)
125
125
  const names = map(src, (u) => u.name)
126
- expect(names()).toEqual(["Alice", "Bob", "Charlie", "Diana"])
126
+ expect(names()).toEqual(['Alice', 'Bob', 'Charlie', 'Diana'])
127
127
  })
128
128
 
129
- it("sortBy returns computed", () => {
130
- const src = signal([{ n: "B" }, { n: "A" }, { n: "C" }])
131
- const sorted = sortBy(src, "n")
132
- expect(sorted()[0]!.n).toBe("A")
129
+ it('sortBy returns computed', () => {
130
+ const src = signal([{ n: 'B' }, { n: 'A' }, { n: 'C' }])
131
+ const sorted = sortBy(src, 'n')
132
+ expect(sorted()[0]!.n).toBe('A')
133
133
  })
134
134
 
135
- it("groupBy returns computed", () => {
135
+ it('groupBy returns computed', () => {
136
136
  const src = signal(users)
137
- const groups = groupBy(src, "role")
137
+ const groups = groupBy(src, 'role')
138
138
  expect(groups().admin).toHaveLength(2)
139
139
  })
140
140
 
141
- it("take returns computed", () => {
141
+ it('take returns computed', () => {
142
142
  const src = signal(users)
143
143
  const first2 = take(src, 2)
144
144
  expect(first2()).toHaveLength(2)
@@ -147,35 +147,35 @@ describe("collections — signal values (reactive)", () => {
147
147
  expect(first2()).toHaveLength(1)
148
148
  })
149
149
 
150
- it("does not mutate original array", () => {
151
- const original = [{ n: "B" }, { n: "A" }]
150
+ it('does not mutate original array', () => {
151
+ const original = [{ n: 'B' }, { n: 'A' }]
152
152
  const src = signal(original)
153
- sortBy(src, "n")()
154
- expect(original[0]!.n).toBe("B") // original untouched
153
+ sortBy(src, 'n')()
154
+ expect(original[0]!.n).toBe('B') // original untouched
155
155
  })
156
156
 
157
- it("skip returns computed", () => {
157
+ it('skip returns computed', () => {
158
158
  const src = signal(users)
159
159
  const skipped = skip(src, 3)
160
160
  expect(skipped()).toHaveLength(1)
161
- expect(skipped()[0]!.name).toBe("Diana")
161
+ expect(skipped()[0]!.name).toBe('Diana')
162
162
 
163
- src.set([...users, { id: 5, name: "Eve", role: "admin", active: true }])
163
+ src.set([...users, { id: 5, name: 'Eve', role: 'admin', active: true }])
164
164
  expect(skipped()).toHaveLength(2)
165
165
  })
166
166
 
167
- it("last returns computed", () => {
167
+ it('last returns computed', () => {
168
168
  const src = signal(users)
169
169
  const lastTwo = last(src, 2)
170
170
  expect(lastTwo()).toHaveLength(2)
171
- expect(lastTwo()[0]!.name).toBe("Charlie")
171
+ expect(lastTwo()[0]!.name).toBe('Charlie')
172
172
 
173
173
  src.set([users[0]!])
174
174
  expect(lastTwo()).toHaveLength(1)
175
- expect(lastTwo()[0]!.name).toBe("Alice")
175
+ expect(lastTwo()[0]!.name).toBe('Alice')
176
176
  })
177
177
 
178
- it("mapValues returns computed", () => {
178
+ it('mapValues returns computed', () => {
179
179
  const src = signal<Record<string, number>>({ a: 1, b: 2 })
180
180
  const doubled = mapValues(src, (v) => v * 2)
181
181
  expect(doubled()).toEqual({ a: 2, b: 4 })
@@ -184,25 +184,25 @@ describe("collections — signal values (reactive)", () => {
184
184
  expect(doubled()).toEqual({ x: 20 })
185
185
  })
186
186
 
187
- it("mapValues with signal tracks key argument reactively", () => {
187
+ it('mapValues with signal tracks key argument reactively', () => {
188
188
  const src = signal<Record<string, number[]>>({
189
189
  admin: [1, 2, 3],
190
190
  viewer: [4, 5],
191
191
  })
192
192
  const counts = mapValues(src, (arr, key) => ({ role: key, count: arr.length }))
193
193
  expect(counts()).toEqual({
194
- admin: { role: "admin", count: 3 },
195
- viewer: { role: "viewer", count: 2 },
194
+ admin: { role: 'admin', count: 3 },
195
+ viewer: { role: 'viewer', count: 2 },
196
196
  })
197
197
 
198
198
  src.set({ admin: [1], editor: [2, 3, 4, 5] })
199
199
  expect(counts()).toEqual({
200
- admin: { role: "admin", count: 1 },
201
- editor: { role: "editor", count: 4 },
200
+ admin: { role: 'admin', count: 1 },
201
+ editor: { role: 'editor', count: 4 },
202
202
  })
203
203
  })
204
204
 
205
- it("mapValues with signal handles empty record", () => {
205
+ it('mapValues with signal handles empty record', () => {
206
206
  const src = signal<Record<string, number>>({})
207
207
  const doubled = mapValues(src, (v) => v * 2)
208
208
  expect(doubled()).toEqual({})
@@ -1,9 +1,9 @@
1
- import { signal } from "@pyreon/reactivity"
2
- import { describe, expect, it } from "vitest"
3
- import { combine, distinct, scan } from "../operators"
1
+ import { signal } from '@pyreon/reactivity'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { combine, distinct, scan } from '../operators'
4
4
 
5
- describe("distinct", () => {
6
- it("skips consecutive duplicate values", () => {
5
+ describe('distinct', () => {
6
+ it('skips consecutive duplicate values', () => {
7
7
  const src = signal(1)
8
8
  const d = distinct(src)
9
9
  expect(d()).toBe(1)
@@ -25,38 +25,38 @@ describe("distinct", () => {
25
25
  expect(d()).toBe(3)
26
26
  })
27
27
 
28
- it("supports custom equality function", () => {
29
- const src = signal({ id: 1, name: "Alice" })
28
+ it('supports custom equality function', () => {
29
+ const src = signal({ id: 1, name: 'Alice' })
30
30
  const d = distinct(src, (a, b) => a.id === b.id)
31
- expect(d().name).toBe("Alice")
31
+ expect(d().name).toBe('Alice')
32
32
 
33
33
  // Same id, different name — should be considered equal, skip
34
- src.set({ id: 1, name: "Updated Alice" })
35
- expect(d().name).toBe("Alice")
34
+ src.set({ id: 1, name: 'Updated Alice' })
35
+ expect(d().name).toBe('Alice')
36
36
 
37
37
  // Different id — should emit
38
- src.set({ id: 2, name: "Bob" })
39
- expect(d().name).toBe("Bob")
38
+ src.set({ id: 2, name: 'Bob' })
39
+ expect(d().name).toBe('Bob')
40
40
  })
41
41
 
42
- it("works reactively with signal source", () => {
43
- const src = signal("a")
42
+ it('works reactively with signal source', () => {
43
+ const src = signal('a')
44
44
  const d = distinct(src)
45
- expect(d()).toBe("a")
45
+ expect(d()).toBe('a')
46
46
 
47
- src.set("b")
48
- expect(d()).toBe("b")
47
+ src.set('b')
48
+ expect(d()).toBe('b')
49
49
 
50
- src.set("b")
51
- expect(d()).toBe("b")
50
+ src.set('b')
51
+ expect(d()).toBe('b')
52
52
 
53
- src.set("c")
54
- expect(d()).toBe("c")
53
+ src.set('c')
54
+ expect(d()).toBe('c')
55
55
  })
56
56
  })
57
57
 
58
- describe("scan", () => {
59
- it("accumulates values over signal changes", () => {
58
+ describe('scan', () => {
59
+ it('accumulates values over signal changes', () => {
60
60
  const src = signal(0)
61
61
  const total = scan(src, (acc, val) => acc + val, 0)
62
62
 
@@ -73,21 +73,21 @@ describe("scan", () => {
73
73
  expect(total()).toBe(6)
74
74
  })
75
75
 
76
- it("works with non-numeric accumulation", () => {
77
- const src = signal("start")
76
+ it('works with non-numeric accumulation', () => {
77
+ const src = signal('start')
78
78
  const log = scan(src, (acc, val) => [...acc, val], [] as string[])
79
79
 
80
80
  // Effect runs immediately with initial value
81
- expect(log()).toEqual(["start"])
81
+ expect(log()).toEqual(['start'])
82
82
 
83
- src.set("hello")
84
- expect(log()).toEqual(["start", "hello"])
83
+ src.set('hello')
84
+ expect(log()).toEqual(['start', 'hello'])
85
85
 
86
- src.set("world")
87
- expect(log()).toEqual(["start", "hello", "world"])
86
+ src.set('world')
87
+ expect(log()).toEqual(['start', 'hello', 'world'])
88
88
  })
89
89
 
90
- it("is signal-reactive", () => {
90
+ it('is signal-reactive', () => {
91
91
  const src = signal(10)
92
92
  const running = scan(src, (acc, val) => acc + val, 0)
93
93
  expect(running()).toBe(10)
@@ -101,16 +101,16 @@ describe("scan", () => {
101
101
  })
102
102
  })
103
103
 
104
- describe("combine", () => {
105
- it("combines 2 signals", () => {
106
- const firstName = signal("John")
107
- const lastName = signal("Doe")
104
+ describe('combine', () => {
105
+ it('combines 2 signals', () => {
106
+ const firstName = signal('John')
107
+ const lastName = signal('Doe')
108
108
  const fullName = combine(firstName, lastName, (f, l) => `${f} ${l}`)
109
109
 
110
- expect(fullName()).toBe("John Doe")
110
+ expect(fullName()).toBe('John Doe')
111
111
  })
112
112
 
113
- it("combines 3 signals", () => {
113
+ it('combines 3 signals', () => {
114
114
  const a = signal(1)
115
115
  const b = signal(2)
116
116
  const c = signal(3)
@@ -119,21 +119,21 @@ describe("combine", () => {
119
119
  expect(total()).toBe(6)
120
120
  })
121
121
 
122
- it("reacts to updates from any source", () => {
123
- const firstName = signal("John")
124
- const lastName = signal("Doe")
122
+ it('reacts to updates from any source', () => {
123
+ const firstName = signal('John')
124
+ const lastName = signal('Doe')
125
125
  const fullName = combine(firstName, lastName, (f, l) => `${f} ${l}`)
126
126
 
127
- expect(fullName()).toBe("John Doe")
127
+ expect(fullName()).toBe('John Doe')
128
128
 
129
- firstName.set("Jane")
130
- expect(fullName()).toBe("Jane Doe")
129
+ firstName.set('Jane')
130
+ expect(fullName()).toBe('Jane Doe')
131
131
 
132
- lastName.set("Smith")
133
- expect(fullName()).toBe("Jane Smith")
132
+ lastName.set('Smith')
133
+ expect(fullName()).toBe('Jane Smith')
134
134
  })
135
135
 
136
- it("reacts to updates from all 3 sources", () => {
136
+ it('reacts to updates from all 3 sources', () => {
137
137
  const a = signal(1)
138
138
  const b = signal(10)
139
139
  const c = signal(100)
@@ -1,9 +1,9 @@
1
- import { signal } from "@pyreon/reactivity"
2
- import { describe, expect, it } from "vitest"
3
- import { pipe } from "../pipe"
1
+ import { signal } from '@pyreon/reactivity'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { pipe } from '../pipe'
4
4
 
5
- describe("pipe — plain values", () => {
6
- it("chains transforms", () => {
5
+ describe('pipe — plain values', () => {
6
+ it('chains transforms', () => {
7
7
  const result = pipe(
8
8
  [3, 1, 4, 1, 5, 9],
9
9
  (arr) => arr.filter((n) => n > 2),
@@ -13,13 +13,13 @@ describe("pipe — plain values", () => {
13
13
  expect(result).toEqual([3, 4, 5])
14
14
  })
15
15
 
16
- it("single transform", () => {
16
+ it('single transform', () => {
17
17
  expect(pipe([1, 2, 3], (arr) => arr.length)).toBe(3)
18
18
  })
19
19
  })
20
20
 
21
- describe("pipe — signal values", () => {
22
- it("returns computed that tracks source", () => {
21
+ describe('pipe — signal values', () => {
22
+ it('returns computed that tracks source', () => {
23
23
  const src = signal([3, 1, 4, 1, 5, 9])
24
24
  const result = pipe(
25
25
  src,
@@ -32,12 +32,12 @@ describe("pipe — signal values", () => {
32
32
  expect(result()).toEqual([10])
33
33
  })
34
34
 
35
- it("supports type narrowing across steps", () => {
35
+ it('supports type narrowing across steps', () => {
36
36
  type User = { name: string; score: number }
37
37
  const users = signal<User[]>([
38
- { name: "A", score: 5 },
39
- { name: "B", score: 10 },
40
- { name: "C", score: 3 },
38
+ { name: 'A', score: 5 },
39
+ { name: 'B', score: 10 },
40
+ { name: 'C', score: 3 },
41
41
  ])
42
42
 
43
43
  const topNames = pipe(
@@ -46,10 +46,10 @@ describe("pipe — signal values", () => {
46
46
  (items) => items.slice(0, 2),
47
47
  (items) => items.map((u) => u.name),
48
48
  )
49
- expect(topNames()).toEqual(["B", "A"])
49
+ expect(topNames()).toEqual(['B', 'A'])
50
50
  })
51
51
 
52
- it("supports 4 transforms", () => {
52
+ it('supports 4 transforms', () => {
53
53
  const src = signal([10, 5, 20, 3, 15, 8])
54
54
  const result = pipe(
55
55
  src,
@@ -67,7 +67,7 @@ describe("pipe — signal values", () => {
67
67
  expect(result()).toBe(150)
68
68
  })
69
69
 
70
- it("supports 5 transforms", () => {
70
+ it('supports 5 transforms', () => {
71
71
  const src = signal([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
72
72
  const result = pipe(
73
73
  src,
@@ -77,17 +77,17 @@ describe("pipe — signal values", () => {
77
77
  (arr) => arr.reduce((sum, n) => sum + n, 0), // 180
78
78
  (n) => `Total: ${n}`,
79
79
  )
80
- expect(result()).toBe("Total: 180")
80
+ expect(result()).toBe('Total: 180')
81
81
  })
82
82
 
83
- it("4+ transforms with plain values (non-signal)", () => {
83
+ it('4+ transforms with plain values (non-signal)', () => {
84
84
  const result = pipe(
85
85
  [5, 3, 8, 1, 9, 2],
86
86
  (arr) => arr.filter((n) => n > 3),
87
87
  (arr) => arr.sort((a, b) => b - a),
88
88
  (arr) => arr.slice(0, 2),
89
- (arr) => arr.join("-"),
89
+ (arr) => arr.join('-'),
90
90
  )
91
- expect(result).toBe("9-8")
91
+ expect(result).toBe('9-8')
92
92
  })
93
93
  })
@@ -1,109 +1,109 @@
1
- import { signal } from "@pyreon/reactivity"
2
- import { describe, expect, it } from "vitest"
3
- import { search } from "../search"
1
+ import { signal } from '@pyreon/reactivity'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { search } from '../search'
4
4
 
5
5
  type User = { id: number; name: string; email: string }
6
6
 
7
7
  const users: User[] = [
8
- { id: 1, name: "Alice", email: "alice@example.com" },
9
- { id: 2, name: "Bob", email: "bob@test.com" },
10
- { id: 3, name: "Charlie", email: "charlie@example.com" },
11
- { id: 4, name: "Diana", email: "diana@test.com" },
8
+ { id: 1, name: 'Alice', email: 'alice@example.com' },
9
+ { id: 2, name: 'Bob', email: 'bob@test.com' },
10
+ { id: 3, name: 'Charlie', email: 'charlie@example.com' },
11
+ { id: 4, name: 'Diana', email: 'diana@test.com' },
12
12
  ]
13
13
 
14
- describe("search — plain values", () => {
15
- it("returns all items when query is empty", () => {
16
- const result = search(users, "", ["name"])
14
+ describe('search — plain values', () => {
15
+ it('returns all items when query is empty', () => {
16
+ const result = search(users, '', ['name'])
17
17
  expect(result).toHaveLength(4)
18
18
  })
19
19
 
20
- it("filters by single key", () => {
21
- const result = search(users, "alice", ["name"])
20
+ it('filters by single key', () => {
21
+ const result = search(users, 'alice', ['name'])
22
22
  expect(result).toHaveLength(1)
23
- expect(result[0]!.name).toBe("Alice")
23
+ expect(result[0]!.name).toBe('Alice')
24
24
  })
25
25
 
26
- it("filters across multiple keys", () => {
27
- const result = search(users, "example", ["name", "email"])
26
+ it('filters across multiple keys', () => {
27
+ const result = search(users, 'example', ['name', 'email'])
28
28
  expect(result).toHaveLength(2)
29
- expect(result.map((u: (typeof users)[number]) => u.name)).toEqual(["Alice", "Charlie"])
29
+ expect(result.map((u: (typeof users)[number]) => u.name)).toEqual(['Alice', 'Charlie'])
30
30
  })
31
31
 
32
- it("is case-insensitive", () => {
33
- const result = search(users, "BOB", ["name"])
32
+ it('is case-insensitive', () => {
33
+ const result = search(users, 'BOB', ['name'])
34
34
  expect(result).toHaveLength(1)
35
- expect(result[0]!.name).toBe("Bob")
35
+ expect(result[0]!.name).toBe('Bob')
36
36
  })
37
37
 
38
- it("trims whitespace from query", () => {
39
- const result = search(users, " alice ", ["name"])
38
+ it('trims whitespace from query', () => {
39
+ const result = search(users, ' alice ', ['name'])
40
40
  expect(result).toHaveLength(1)
41
41
  })
42
42
 
43
- it("returns empty array when no matches", () => {
44
- const result = search(users, "zzz", ["name", "email"])
43
+ it('returns empty array when no matches', () => {
44
+ const result = search(users, 'zzz', ['name', 'email'])
45
45
  expect(result).toHaveLength(0)
46
46
  })
47
47
 
48
- it("only matches string values (skips non-string fields)", () => {
48
+ it('only matches string values (skips non-string fields)', () => {
49
49
  // id is a number — should not be matched
50
- const result = search(users, "1", ["id" as keyof User, "name"])
50
+ const result = search(users, '1', ['id' as keyof User, 'name'])
51
51
  expect(result).toHaveLength(0)
52
52
  })
53
53
  })
54
54
 
55
- describe("search — signal source (reactive)", () => {
56
- it("returns computed when source is a signal", () => {
55
+ describe('search — signal source (reactive)', () => {
56
+ it('returns computed when source is a signal', () => {
57
57
  const src = signal(users)
58
- const result = search(src, "alice", ["name"])
59
- expect(typeof result).toBe("function")
58
+ const result = search(src, 'alice', ['name'])
59
+ expect(typeof result).toBe('function')
60
60
  expect(result()).toHaveLength(1)
61
61
  })
62
62
 
63
- it("reacts to source changes", () => {
63
+ it('reacts to source changes', () => {
64
64
  const src = signal(users)
65
- const result = search(src, "test", ["email"])
65
+ const result = search(src, 'test', ['email'])
66
66
  expect(result()).toHaveLength(2) // bob + diana
67
67
 
68
- src.set([...users, { id: 5, name: "Eve", email: "eve@test.com" }])
68
+ src.set([...users, { id: 5, name: 'Eve', email: 'eve@test.com' }])
69
69
  expect(result()).toHaveLength(3)
70
70
  })
71
71
 
72
- it("returns computed when query is a signal", () => {
73
- const query = signal("")
74
- const result = search(users, query, ["name"])
72
+ it('returns computed when query is a signal', () => {
73
+ const query = signal('')
74
+ const result = search(users, query, ['name'])
75
75
 
76
- expect(typeof result).toBe("function")
76
+ expect(typeof result).toBe('function')
77
77
  expect(result()).toHaveLength(4) // empty query returns all
78
78
 
79
- query.set("ali")
79
+ query.set('ali')
80
80
  expect(result()).toHaveLength(1)
81
81
 
82
- query.set("a") // Alice, Charlie, Diana
82
+ query.set('a') // Alice, Charlie, Diana
83
83
  expect(result()).toHaveLength(3)
84
84
  })
85
85
 
86
- it("reacts to both source and query signal changes", () => {
86
+ it('reacts to both source and query signal changes', () => {
87
87
  const src = signal(users)
88
- const query = signal("")
89
- const result = search(src, query, ["name"])
88
+ const query = signal('')
89
+ const result = search(src, query, ['name'])
90
90
 
91
91
  expect(result()).toHaveLength(4)
92
92
 
93
- query.set("bob")
93
+ query.set('bob')
94
94
  expect(result()).toHaveLength(1)
95
95
 
96
96
  // Remove Bob from source
97
- src.set(users.filter((u) => u.name !== "Bob"))
97
+ src.set(users.filter((u) => u.name !== 'Bob'))
98
98
  expect(result()).toHaveLength(0)
99
99
 
100
- query.set("")
100
+ query.set('')
101
101
  expect(result()).toHaveLength(3) // all remaining users
102
102
  })
103
103
 
104
- it("returns empty when signal query matches nothing", () => {
105
- const query = signal("nonexistent")
106
- const result = search(users, query, ["name", "email"])
104
+ it('returns empty when signal query matches nothing', () => {
105
+ const query = signal('nonexistent')
106
+ const result = search(users, query, ['name', 'email'])
107
107
  expect(result()).toHaveLength(0)
108
108
  })
109
109
  })
@@ -1,13 +1,13 @@
1
- import { signal } from "@pyreon/reactivity"
2
- import { afterEach, describe, expect, it, vi } from "vitest"
3
- import { debounce, throttle } from "../timing"
1
+ import { signal } from '@pyreon/reactivity'
2
+ import { afterEach, describe, expect, it, vi } from 'vitest'
3
+ import { debounce, throttle } from '../timing'
4
4
 
5
- describe("debounce", () => {
5
+ describe('debounce', () => {
6
6
  afterEach(() => {
7
7
  vi.useRealTimers()
8
8
  })
9
9
 
10
- it("dispose() stops tracking source changes", () => {
10
+ it('dispose() stops tracking source changes', () => {
11
11
  vi.useFakeTimers()
12
12
  const src = signal(0)
13
13
  const debounced = debounce(src, 100)
@@ -26,14 +26,14 @@ describe("debounce", () => {
26
26
  expect(debounced()).toBe(1) // still 1, not 2
27
27
  })
28
28
 
29
- it("dispose() clears pending timer so debounced value stays frozen", () => {
29
+ it('dispose() clears pending timer so debounced value stays frozen', () => {
30
30
  vi.useFakeTimers()
31
- const src = signal("a")
31
+ const src = signal('a')
32
32
  const debounced = debounce(src, 200)
33
- expect(debounced()).toBe("a")
33
+ expect(debounced()).toBe('a')
34
34
 
35
35
  // Start a debounce cycle
36
- src.set("b")
36
+ src.set('b')
37
37
  vi.advanceTimersByTime(50) // not yet debounced
38
38
 
39
39
  // Dispose mid-cycle — pending timer should be cleared
@@ -42,10 +42,10 @@ describe("debounce", () => {
42
42
  // Advance well past the debounce window
43
43
  vi.advanceTimersByTime(500)
44
44
  // Value should remain "a" — the pending "b" was cancelled
45
- expect(debounced()).toBe("a")
45
+ expect(debounced()).toBe('a')
46
46
  })
47
47
 
48
- it("debounces rapid updates to only emit the latest", () => {
48
+ it('debounces rapid updates to only emit the latest', () => {
49
49
  vi.useFakeTimers()
50
50
  const src = signal(0)
51
51
  const debounced = debounce(src, 100)
@@ -62,7 +62,7 @@ describe("debounce", () => {
62
62
  expect(debounced()).toBe(3)
63
63
  })
64
64
 
65
- it("multiple dispose calls do not throw", () => {
65
+ it('multiple dispose calls do not throw', () => {
66
66
  vi.useFakeTimers()
67
67
  const src = signal(0)
68
68
  const debounced = debounce(src, 100)
@@ -72,12 +72,12 @@ describe("debounce", () => {
72
72
  })
73
73
  })
74
74
 
75
- describe("throttle", () => {
75
+ describe('throttle', () => {
76
76
  afterEach(() => {
77
77
  vi.useRealTimers()
78
78
  })
79
79
 
80
- it("dispose() stops tracking source changes", () => {
80
+ it('dispose() stops tracking source changes', () => {
81
81
  vi.useFakeTimers()
82
82
  const src = signal(0)
83
83
  const throttled = throttle(src, 100)
@@ -98,7 +98,7 @@ describe("throttle", () => {
98
98
  expect(throttled()).toBe(1) // still 1, not 2
99
99
  })
100
100
 
101
- it("dispose() clears pending trailing timer", () => {
101
+ it('dispose() clears pending trailing timer', () => {
102
102
  vi.useFakeTimers()
103
103
  const src = signal(0)
104
104
  const throttled = throttle(src, 200)
@@ -120,7 +120,7 @@ describe("throttle", () => {
120
120
  expect(throttled()).toBe(1) // trailing update never fires
121
121
  })
122
122
 
123
- it("emits immediately on first change after window", () => {
123
+ it('emits immediately on first change after window', () => {
124
124
  vi.useFakeTimers()
125
125
  const src = signal(0)
126
126
  const throttled = throttle(src, 100)
@@ -133,7 +133,7 @@ describe("throttle", () => {
133
133
  expect(throttled()).toBe(42)
134
134
  })
135
135
 
136
- it("trailing timer fires the latest value after throttle window", () => {
136
+ it('trailing timer fires the latest value after throttle window', () => {
137
137
  vi.useFakeTimers()
138
138
  const src = signal(0)
139
139
  const throttled = throttle(src, 100)
@@ -152,7 +152,7 @@ describe("throttle", () => {
152
152
  expect(throttled()).toBe(3) // latest value
153
153
  })
154
154
 
155
- it("multiple dispose calls do not throw", () => {
155
+ it('multiple dispose calls do not throw', () => {
156
156
  vi.useFakeTimers()
157
157
  const src = signal(0)
158
158
  const throttled = throttle(src, 100)
package/src/timing.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { effect, signal } from "@pyreon/reactivity"
2
- import type { ReadableSignal } from "./types"
1
+ import { effect, signal } from '@pyreon/reactivity'
2
+ import type { ReadableSignal } from './types'
3
3
 
4
4
  /**
5
5
  * Debounce a signal — emits the latest value after `ms` of silence.
package/src/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { computed, signal } from "@pyreon/reactivity"
1
+ import type { computed, signal } from '@pyreon/reactivity'
2
2
 
3
3
  /** A readable signal — any callable that returns a value and tracks subscribers. */
4
4
  export type ReadableSignal<T> = (() => T) & { peek?: () => T }
@@ -12,10 +12,10 @@ export type KeyOf<T> = keyof T | ((item: T) => string | number)
12
12
 
13
13
  /** Resolve a key extractor to a function. */
14
14
  export function resolveKey<T>(key: KeyOf<T>): (item: T) => string | number {
15
- return typeof key === "function" ? key : (item: T) => String(item[key])
15
+ return typeof key === 'function' ? key : (item: T) => String(item[key])
16
16
  }
17
17
 
18
18
  /** Check if a value is a signal (callable function with .set or .peek). */
19
19
  export function isSignal<T>(value: unknown): value is ReadableSignal<T> {
20
- return typeof value === "function"
20
+ return typeof value === 'function'
21
21
  }