@pyreon/rx 0.11.5 → 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 +8 -8
- package/lib/index.js.map +1 -1
- package/package.json +12 -12
- package/src/aggregation.ts +3 -3
- package/src/collections.ts +3 -3
- package/src/index.ts +7 -7
- package/src/operators.ts +2 -2
- package/src/pipe.ts +3 -3
- package/src/search.ts +4 -4
- package/src/tests/aggregation.test.ts +23 -23
- package/src/tests/collections.test.ts +69 -69
- package/src/tests/operators.test.ts +46 -46
- package/src/tests/pipe.test.ts +19 -19
- package/src/tests/search.test.ts +47 -47
- package/src/tests/timing.test.ts +18 -18
- package/src/timing.ts +2 -2
- package/src/types.ts +3 -3
package/README.md
CHANGED
|
@@ -13,21 +13,21 @@ bun add @pyreon/rx
|
|
|
13
13
|
## Usage
|
|
14
14
|
|
|
15
15
|
```ts
|
|
16
|
-
import { rx } from
|
|
17
|
-
import { signal } from
|
|
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)
|
|
22
|
-
const sorted = rx.sortBy(active,
|
|
23
|
-
const top10 = rx.take(sorted, 10)
|
|
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.
|
|
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": "
|
|
38
|
-
},
|
|
39
|
-
"peerDependencies": {
|
|
40
|
-
"@pyreon/reactivity": "^0.11.5"
|
|
37
|
+
"lint": "oxlint ."
|
|
41
38
|
},
|
|
42
39
|
"devDependencies": {
|
|
43
|
-
"@pyreon/typescript": "^0.11.
|
|
40
|
+
"@pyreon/typescript": "^0.11.6"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"@pyreon/reactivity": "^0.11.6"
|
|
44
44
|
}
|
|
45
45
|
}
|
package/src/aggregation.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { computed } from
|
|
2
|
-
import type { KeyOf, ReadableSignal } from
|
|
3
|
-
import { isSignal, resolveKey } from
|
|
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>)()))
|
package/src/collections.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { computed } from
|
|
2
|
-
import type { KeyOf, ReadableSignal } from
|
|
3
|
-
import { isSignal, resolveKey } from
|
|
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
|
|
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
|
|
17
|
-
import { combine, distinct, scan } from
|
|
18
|
-
import { pipe } from
|
|
19
|
-
import { search } from
|
|
20
|
-
import { debounce, throttle } from
|
|
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
|
|
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
|
|
2
|
-
import type { ReadableSignal } from
|
|
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
|
|
2
|
-
import type { ReadableSignal } from
|
|
3
|
-
import { isSignal } from
|
|
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
|
|
2
|
-
import type { ReadableSignal } from
|
|
3
|
-
import { isSignal } from
|
|
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 ===
|
|
28
|
+
return typeof val === 'string' && val.toLowerCase().includes(q)
|
|
29
29
|
}),
|
|
30
30
|
)
|
|
31
31
|
}
|
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
import { signal } from
|
|
2
|
-
import { describe, expect, it } from
|
|
3
|
-
import { average, count, max, min, sum } from
|
|
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(
|
|
6
|
-
it(
|
|
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(
|
|
11
|
+
it('sum', () => {
|
|
12
12
|
expect(sum([1, 2, 3])).toBe(6)
|
|
13
13
|
})
|
|
14
14
|
|
|
15
|
-
it(
|
|
15
|
+
it('sum with key', () => {
|
|
16
16
|
const items = [{ v: 10 }, { v: 20 }, { v: 30 }]
|
|
17
|
-
expect(sum(items,
|
|
17
|
+
expect(sum(items, 'v')).toBe(60)
|
|
18
18
|
})
|
|
19
19
|
|
|
20
|
-
it(
|
|
20
|
+
it('min', () => {
|
|
21
21
|
const items = [{ v: 3 }, { v: 1 }, { v: 2 }]
|
|
22
|
-
expect(min(items,
|
|
22
|
+
expect(min(items, 'v')?.v).toBe(1)
|
|
23
23
|
})
|
|
24
24
|
|
|
25
|
-
it(
|
|
25
|
+
it('max', () => {
|
|
26
26
|
const items = [{ v: 3 }, { v: 1 }, { v: 2 }]
|
|
27
|
-
expect(max(items,
|
|
27
|
+
expect(max(items, 'v')?.v).toBe(3)
|
|
28
28
|
})
|
|
29
29
|
|
|
30
|
-
it(
|
|
30
|
+
it('min/max empty array', () => {
|
|
31
31
|
expect(min([])).toBeUndefined()
|
|
32
32
|
expect(max([])).toBeUndefined()
|
|
33
33
|
})
|
|
34
34
|
|
|
35
|
-
it(
|
|
35
|
+
it('average', () => {
|
|
36
36
|
expect(average([2, 4, 6])).toBe(4)
|
|
37
37
|
})
|
|
38
38
|
|
|
39
|
-
it(
|
|
39
|
+
it('average with key', () => {
|
|
40
40
|
const items = [{ v: 10 }, { v: 20 }, { v: 30 }]
|
|
41
|
-
expect(average(items,
|
|
41
|
+
expect(average(items, 'v')).toBe(20)
|
|
42
42
|
})
|
|
43
43
|
|
|
44
|
-
it(
|
|
44
|
+
it('average empty array returns 0', () => {
|
|
45
45
|
expect(average([])).toBe(0)
|
|
46
46
|
})
|
|
47
47
|
})
|
|
48
48
|
|
|
49
|
-
describe(
|
|
50
|
-
it(
|
|
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(
|
|
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(
|
|
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(
|
|
75
|
+
it('average with key returns computed', () => {
|
|
76
76
|
const src = signal([{ v: 10 }, { v: 20 }, { v: 30 }])
|
|
77
|
-
const avg = average(src,
|
|
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
|
|
2
|
-
import { describe, expect, it } from
|
|
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
|
|
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:
|
|
22
|
-
{ id: 2, name:
|
|
23
|
-
{ id: 3, name:
|
|
24
|
-
{ id: 4, name:
|
|
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(
|
|
28
|
-
it(
|
|
27
|
+
describe('collections — plain values', () => {
|
|
28
|
+
it('filter', () => {
|
|
29
29
|
expect(filter(users, (u) => u.active)).toHaveLength(3)
|
|
30
30
|
})
|
|
31
31
|
|
|
32
|
-
it(
|
|
33
|
-
expect(map(users, (u) => u.name)).toEqual([
|
|
32
|
+
it('map', () => {
|
|
33
|
+
expect(map(users, (u) => u.name)).toEqual(['Alice', 'Bob', 'Charlie', 'Diana'])
|
|
34
34
|
})
|
|
35
35
|
|
|
36
|
-
it(
|
|
37
|
-
const sorted = sortBy(users,
|
|
38
|
-
expect(sorted[0]!.name).toBe(
|
|
39
|
-
expect(sorted[3]!.name).toBe(
|
|
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(
|
|
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(
|
|
48
|
-
const groups = groupBy(users,
|
|
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(
|
|
54
|
-
const indexed = keyBy(users,
|
|
55
|
-
expect(indexed[
|
|
56
|
-
expect(indexed[
|
|
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(
|
|
60
|
-
const result = uniqBy(users,
|
|
59
|
+
it('uniqBy', () => {
|
|
60
|
+
const result = uniqBy(users, 'role')
|
|
61
61
|
expect(result).toHaveLength(3) // admin, viewer, editor
|
|
62
62
|
})
|
|
63
63
|
|
|
64
|
-
it(
|
|
64
|
+
it('take', () => {
|
|
65
65
|
expect(take(users, 2)).toHaveLength(2)
|
|
66
|
-
expect(take(users, 2)[0]!.name).toBe(
|
|
66
|
+
expect(take(users, 2)[0]!.name).toBe('Alice')
|
|
67
67
|
})
|
|
68
68
|
|
|
69
|
-
it(
|
|
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(
|
|
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(
|
|
81
|
-
expect(find(users, (u) => u.name ===
|
|
82
|
-
expect(find(users, (u) => u.name ===
|
|
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(
|
|
85
|
+
it('skip', () => {
|
|
86
86
|
const result = skip(users, 2)
|
|
87
87
|
expect(result).toHaveLength(2)
|
|
88
|
-
expect(result[0]!.name).toBe(
|
|
89
|
-
expect(result[1]!.name).toBe(
|
|
88
|
+
expect(result[0]!.name).toBe('Charlie')
|
|
89
|
+
expect(result[1]!.name).toBe('Diana')
|
|
90
90
|
})
|
|
91
91
|
|
|
92
|
-
it(
|
|
92
|
+
it('last', () => {
|
|
93
93
|
const result = last(users, 2)
|
|
94
94
|
expect(result).toHaveLength(2)
|
|
95
|
-
expect(result[0]!.name).toBe(
|
|
96
|
-
expect(result[1]!.name).toBe(
|
|
95
|
+
expect(result[0]!.name).toBe('Charlie')
|
|
96
|
+
expect(result[1]!.name).toBe('Diana')
|
|
97
97
|
})
|
|
98
98
|
|
|
99
|
-
it(
|
|
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(
|
|
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:
|
|
108
|
+
expect(result).toEqual({ x: 'x=1', y: 'y=2' })
|
|
109
109
|
})
|
|
110
110
|
})
|
|
111
111
|
|
|
112
|
-
describe(
|
|
113
|
-
it(
|
|
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:
|
|
119
|
+
src.set([...users, { id: 5, name: 'Eve', role: 'admin', active: true }])
|
|
120
120
|
expect(active()).toHaveLength(4)
|
|
121
121
|
})
|
|
122
122
|
|
|
123
|
-
it(
|
|
123
|
+
it('map returns computed', () => {
|
|
124
124
|
const src = signal(users)
|
|
125
125
|
const names = map(src, (u) => u.name)
|
|
126
|
-
expect(names()).toEqual([
|
|
126
|
+
expect(names()).toEqual(['Alice', 'Bob', 'Charlie', 'Diana'])
|
|
127
127
|
})
|
|
128
128
|
|
|
129
|
-
it(
|
|
130
|
-
const src = signal([{ n:
|
|
131
|
-
const sorted = sortBy(src,
|
|
132
|
-
expect(sorted()[0]!.n).toBe(
|
|
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(
|
|
135
|
+
it('groupBy returns computed', () => {
|
|
136
136
|
const src = signal(users)
|
|
137
|
-
const groups = groupBy(src,
|
|
137
|
+
const groups = groupBy(src, 'role')
|
|
138
138
|
expect(groups().admin).toHaveLength(2)
|
|
139
139
|
})
|
|
140
140
|
|
|
141
|
-
it(
|
|
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(
|
|
151
|
-
const original = [{ n:
|
|
150
|
+
it('does not mutate original array', () => {
|
|
151
|
+
const original = [{ n: 'B' }, { n: 'A' }]
|
|
152
152
|
const src = signal(original)
|
|
153
|
-
sortBy(src,
|
|
154
|
-
expect(original[0]!.n).toBe(
|
|
153
|
+
sortBy(src, 'n')()
|
|
154
|
+
expect(original[0]!.n).toBe('B') // original untouched
|
|
155
155
|
})
|
|
156
156
|
|
|
157
|
-
it(
|
|
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(
|
|
161
|
+
expect(skipped()[0]!.name).toBe('Diana')
|
|
162
162
|
|
|
163
|
-
src.set([...users, { id: 5, name:
|
|
163
|
+
src.set([...users, { id: 5, name: 'Eve', role: 'admin', active: true }])
|
|
164
164
|
expect(skipped()).toHaveLength(2)
|
|
165
165
|
})
|
|
166
166
|
|
|
167
|
-
it(
|
|
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(
|
|
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(
|
|
175
|
+
expect(lastTwo()[0]!.name).toBe('Alice')
|
|
176
176
|
})
|
|
177
177
|
|
|
178
|
-
it(
|
|
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(
|
|
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:
|
|
195
|
-
viewer: { role:
|
|
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:
|
|
201
|
-
editor: { role:
|
|
200
|
+
admin: { role: 'admin', count: 1 },
|
|
201
|
+
editor: { role: 'editor', count: 4 },
|
|
202
202
|
})
|
|
203
203
|
})
|
|
204
204
|
|
|
205
|
-
it(
|
|
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
|
|
2
|
-
import { describe, expect, it } from
|
|
3
|
-
import { combine, distinct, scan } from
|
|
1
|
+
import { signal } from '@pyreon/reactivity'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { combine, distinct, scan } from '../operators'
|
|
4
4
|
|
|
5
|
-
describe(
|
|
6
|
-
it(
|
|
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(
|
|
29
|
-
const src = signal({ id: 1, name:
|
|
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(
|
|
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:
|
|
35
|
-
expect(d().name).toBe(
|
|
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:
|
|
39
|
-
expect(d().name).toBe(
|
|
38
|
+
src.set({ id: 2, name: 'Bob' })
|
|
39
|
+
expect(d().name).toBe('Bob')
|
|
40
40
|
})
|
|
41
41
|
|
|
42
|
-
it(
|
|
43
|
-
const src = signal(
|
|
42
|
+
it('works reactively with signal source', () => {
|
|
43
|
+
const src = signal('a')
|
|
44
44
|
const d = distinct(src)
|
|
45
|
-
expect(d()).toBe(
|
|
45
|
+
expect(d()).toBe('a')
|
|
46
46
|
|
|
47
|
-
src.set(
|
|
48
|
-
expect(d()).toBe(
|
|
47
|
+
src.set('b')
|
|
48
|
+
expect(d()).toBe('b')
|
|
49
49
|
|
|
50
|
-
src.set(
|
|
51
|
-
expect(d()).toBe(
|
|
50
|
+
src.set('b')
|
|
51
|
+
expect(d()).toBe('b')
|
|
52
52
|
|
|
53
|
-
src.set(
|
|
54
|
-
expect(d()).toBe(
|
|
53
|
+
src.set('c')
|
|
54
|
+
expect(d()).toBe('c')
|
|
55
55
|
})
|
|
56
56
|
})
|
|
57
57
|
|
|
58
|
-
describe(
|
|
59
|
-
it(
|
|
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(
|
|
77
|
-
const src = signal(
|
|
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([
|
|
81
|
+
expect(log()).toEqual(['start'])
|
|
82
82
|
|
|
83
|
-
src.set(
|
|
84
|
-
expect(log()).toEqual([
|
|
83
|
+
src.set('hello')
|
|
84
|
+
expect(log()).toEqual(['start', 'hello'])
|
|
85
85
|
|
|
86
|
-
src.set(
|
|
87
|
-
expect(log()).toEqual([
|
|
86
|
+
src.set('world')
|
|
87
|
+
expect(log()).toEqual(['start', 'hello', 'world'])
|
|
88
88
|
})
|
|
89
89
|
|
|
90
|
-
it(
|
|
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(
|
|
105
|
-
it(
|
|
106
|
-
const firstName = signal(
|
|
107
|
-
const lastName = signal(
|
|
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(
|
|
110
|
+
expect(fullName()).toBe('John Doe')
|
|
111
111
|
})
|
|
112
112
|
|
|
113
|
-
it(
|
|
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(
|
|
123
|
-
const firstName = signal(
|
|
124
|
-
const lastName = signal(
|
|
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(
|
|
127
|
+
expect(fullName()).toBe('John Doe')
|
|
128
128
|
|
|
129
|
-
firstName.set(
|
|
130
|
-
expect(fullName()).toBe(
|
|
129
|
+
firstName.set('Jane')
|
|
130
|
+
expect(fullName()).toBe('Jane Doe')
|
|
131
131
|
|
|
132
|
-
lastName.set(
|
|
133
|
-
expect(fullName()).toBe(
|
|
132
|
+
lastName.set('Smith')
|
|
133
|
+
expect(fullName()).toBe('Jane Smith')
|
|
134
134
|
})
|
|
135
135
|
|
|
136
|
-
it(
|
|
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)
|
package/src/tests/pipe.test.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { signal } from
|
|
2
|
-
import { describe, expect, it } from
|
|
3
|
-
import { pipe } from
|
|
1
|
+
import { signal } from '@pyreon/reactivity'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { pipe } from '../pipe'
|
|
4
4
|
|
|
5
|
-
describe(
|
|
6
|
-
it(
|
|
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(
|
|
16
|
+
it('single transform', () => {
|
|
17
17
|
expect(pipe([1, 2, 3], (arr) => arr.length)).toBe(3)
|
|
18
18
|
})
|
|
19
19
|
})
|
|
20
20
|
|
|
21
|
-
describe(
|
|
22
|
-
it(
|
|
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(
|
|
35
|
+
it('supports type narrowing across steps', () => {
|
|
36
36
|
type User = { name: string; score: number }
|
|
37
37
|
const users = signal<User[]>([
|
|
38
|
-
{ name:
|
|
39
|
-
{ name:
|
|
40
|
-
{ name:
|
|
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([
|
|
49
|
+
expect(topNames()).toEqual(['B', 'A'])
|
|
50
50
|
})
|
|
51
51
|
|
|
52
|
-
it(
|
|
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(
|
|
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(
|
|
80
|
+
expect(result()).toBe('Total: 180')
|
|
81
81
|
})
|
|
82
82
|
|
|
83
|
-
it(
|
|
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(
|
|
91
|
+
expect(result).toBe('9-8')
|
|
92
92
|
})
|
|
93
93
|
})
|
package/src/tests/search.test.ts
CHANGED
|
@@ -1,109 +1,109 @@
|
|
|
1
|
-
import { signal } from
|
|
2
|
-
import { describe, expect, it } from
|
|
3
|
-
import { search } from
|
|
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:
|
|
9
|
-
{ id: 2, name:
|
|
10
|
-
{ id: 3, name:
|
|
11
|
-
{ id: 4, name:
|
|
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(
|
|
15
|
-
it(
|
|
16
|
-
const result = search(users,
|
|
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(
|
|
21
|
-
const result = search(users,
|
|
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(
|
|
23
|
+
expect(result[0]!.name).toBe('Alice')
|
|
24
24
|
})
|
|
25
25
|
|
|
26
|
-
it(
|
|
27
|
-
const result = search(users,
|
|
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([
|
|
29
|
+
expect(result.map((u: (typeof users)[number]) => u.name)).toEqual(['Alice', 'Charlie'])
|
|
30
30
|
})
|
|
31
31
|
|
|
32
|
-
it(
|
|
33
|
-
const result = search(users,
|
|
32
|
+
it('is case-insensitive', () => {
|
|
33
|
+
const result = search(users, 'BOB', ['name'])
|
|
34
34
|
expect(result).toHaveLength(1)
|
|
35
|
-
expect(result[0]!.name).toBe(
|
|
35
|
+
expect(result[0]!.name).toBe('Bob')
|
|
36
36
|
})
|
|
37
37
|
|
|
38
|
-
it(
|
|
39
|
-
const result = search(users,
|
|
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(
|
|
44
|
-
const result = search(users,
|
|
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(
|
|
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,
|
|
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(
|
|
56
|
-
it(
|
|
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,
|
|
59
|
-
expect(typeof result).toBe(
|
|
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(
|
|
63
|
+
it('reacts to source changes', () => {
|
|
64
64
|
const src = signal(users)
|
|
65
|
-
const result = search(src,
|
|
65
|
+
const result = search(src, 'test', ['email'])
|
|
66
66
|
expect(result()).toHaveLength(2) // bob + diana
|
|
67
67
|
|
|
68
|
-
src.set([...users, { id: 5, name:
|
|
68
|
+
src.set([...users, { id: 5, name: 'Eve', email: 'eve@test.com' }])
|
|
69
69
|
expect(result()).toHaveLength(3)
|
|
70
70
|
})
|
|
71
71
|
|
|
72
|
-
it(
|
|
73
|
-
const query = signal(
|
|
74
|
-
const result = search(users, query, [
|
|
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(
|
|
76
|
+
expect(typeof result).toBe('function')
|
|
77
77
|
expect(result()).toHaveLength(4) // empty query returns all
|
|
78
78
|
|
|
79
|
-
query.set(
|
|
79
|
+
query.set('ali')
|
|
80
80
|
expect(result()).toHaveLength(1)
|
|
81
81
|
|
|
82
|
-
query.set(
|
|
82
|
+
query.set('a') // Alice, Charlie, Diana
|
|
83
83
|
expect(result()).toHaveLength(3)
|
|
84
84
|
})
|
|
85
85
|
|
|
86
|
-
it(
|
|
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, [
|
|
88
|
+
const query = signal('')
|
|
89
|
+
const result = search(src, query, ['name'])
|
|
90
90
|
|
|
91
91
|
expect(result()).toHaveLength(4)
|
|
92
92
|
|
|
93
|
-
query.set(
|
|
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 !==
|
|
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(
|
|
105
|
-
const query = signal(
|
|
106
|
-
const result = search(users, query, [
|
|
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
|
})
|
package/src/tests/timing.test.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { signal } from
|
|
2
|
-
import { afterEach, describe, expect, it, vi } from
|
|
3
|
-
import { debounce, throttle } from
|
|
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(
|
|
5
|
+
describe('debounce', () => {
|
|
6
6
|
afterEach(() => {
|
|
7
7
|
vi.useRealTimers()
|
|
8
8
|
})
|
|
9
9
|
|
|
10
|
-
it(
|
|
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(
|
|
29
|
+
it('dispose() clears pending timer so debounced value stays frozen', () => {
|
|
30
30
|
vi.useFakeTimers()
|
|
31
|
-
const src = signal(
|
|
31
|
+
const src = signal('a')
|
|
32
32
|
const debounced = debounce(src, 200)
|
|
33
|
-
expect(debounced()).toBe(
|
|
33
|
+
expect(debounced()).toBe('a')
|
|
34
34
|
|
|
35
35
|
// Start a debounce cycle
|
|
36
|
-
src.set(
|
|
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(
|
|
45
|
+
expect(debounced()).toBe('a')
|
|
46
46
|
})
|
|
47
47
|
|
|
48
|
-
it(
|
|
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(
|
|
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(
|
|
75
|
+
describe('throttle', () => {
|
|
76
76
|
afterEach(() => {
|
|
77
77
|
vi.useRealTimers()
|
|
78
78
|
})
|
|
79
79
|
|
|
80
|
-
it(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
2
|
-
import type { ReadableSignal } from
|
|
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
|
|
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 ===
|
|
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 ===
|
|
20
|
+
return typeof value === 'function'
|
|
21
21
|
}
|