@pyreon/rx 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,86 @@
1
+ import { computed } from "@pyreon/reactivity"
2
+ import type { KeyOf, ReadableSignal } from "./types"
3
+ import { isSignal, resolveKey } from "./types"
4
+
5
+ function reactive<TIn, TOut>(source: TIn, fn: (val: any) => TOut): any {
6
+ if (isSignal(source)) return computed(() => fn((source as ReadableSignal<any>)()))
7
+ return fn(source)
8
+ }
9
+
10
+ /** Count items in collection. */
11
+ export function count<T>(source: ReadableSignal<T[]>): ReturnType<typeof computed<number>>
12
+ export function count<T>(source: T[]): number
13
+ export function count<T>(source: ReadableSignal<T[]> | T[]): any {
14
+ return reactive(source, (arr: T[]) => arr.length)
15
+ }
16
+
17
+ /** Sum numeric values. Optionally by key. */
18
+ export function sum<T>(
19
+ source: ReadableSignal<T[]>,
20
+ key?: KeyOf<T>,
21
+ ): ReturnType<typeof computed<number>>
22
+ export function sum<T>(source: T[], key?: KeyOf<T>): number
23
+ export function sum<T>(source: ReadableSignal<T[]> | T[], key?: KeyOf<T>): any {
24
+ const getVal = key ? resolveKey(key) : (item: T) => item as unknown as number
25
+ return reactive(source, (arr: T[]) => arr.reduce((acc, item) => acc + Number(getVal(item)), 0))
26
+ }
27
+
28
+ /** Find minimum item. Optionally by key. */
29
+ export function min<T>(
30
+ source: ReadableSignal<T[]>,
31
+ key?: KeyOf<T>,
32
+ ): ReturnType<typeof computed<T | undefined>>
33
+ export function min<T>(source: T[], key?: KeyOf<T>): T | undefined
34
+ export function min<T>(source: ReadableSignal<T[]> | T[], key?: KeyOf<T>): any {
35
+ const getVal = key ? resolveKey(key) : (item: T) => item as unknown as number
36
+ return reactive(source, (arr: T[]) => {
37
+ if (arr.length === 0) return undefined
38
+ let result = arr[0] as T
39
+ let minVal = Number(getVal(result))
40
+ for (let i = 1; i < arr.length; i++) {
41
+ const val = Number(getVal(arr[i] as T))
42
+ if (val < minVal) {
43
+ minVal = val
44
+ result = arr[i] as T
45
+ }
46
+ }
47
+ return result
48
+ })
49
+ }
50
+
51
+ /** Find maximum item. Optionally by key. */
52
+ export function max<T>(
53
+ source: ReadableSignal<T[]>,
54
+ key?: KeyOf<T>,
55
+ ): ReturnType<typeof computed<T | undefined>>
56
+ export function max<T>(source: T[], key?: KeyOf<T>): T | undefined
57
+ export function max<T>(source: ReadableSignal<T[]> | T[], key?: KeyOf<T>): any {
58
+ const getVal = key ? resolveKey(key) : (item: T) => item as unknown as number
59
+ return reactive(source, (arr: T[]) => {
60
+ if (arr.length === 0) return undefined
61
+ let result = arr[0] as T
62
+ let maxVal = Number(getVal(result))
63
+ for (let i = 1; i < arr.length; i++) {
64
+ const val = Number(getVal(arr[i] as T))
65
+ if (val > maxVal) {
66
+ maxVal = val
67
+ result = arr[i] as T
68
+ }
69
+ }
70
+ return result
71
+ })
72
+ }
73
+
74
+ /** Average of numeric values. Optionally by key. */
75
+ export function average<T>(
76
+ source: ReadableSignal<T[]>,
77
+ key?: KeyOf<T>,
78
+ ): ReturnType<typeof computed<number>>
79
+ export function average<T>(source: T[], key?: KeyOf<T>): number
80
+ export function average<T>(source: ReadableSignal<T[]> | T[], key?: KeyOf<T>): any {
81
+ const getVal = key ? resolveKey(key) : (item: T) => item as unknown as number
82
+ return reactive(source, (arr: T[]) => {
83
+ if (arr.length === 0) return 0
84
+ return arr.reduce((acc, item) => acc + Number(getVal(item)), 0) / arr.length
85
+ })
86
+ }
@@ -0,0 +1,189 @@
1
+ import { computed } from "@pyreon/reactivity"
2
+ import type { KeyOf, ReadableSignal } from "./types"
3
+ import { isSignal, resolveKey } from "./types"
4
+
5
+ // ─── Helpers ────────────────────────────────────────────────────────────────
6
+
7
+ function reactive<TIn, TOut>(
8
+ source: TIn,
9
+ fn: (val: any) => TOut,
10
+ ): TIn extends ReadableSignal<any> ? ReturnType<typeof computed<TOut>> : TOut {
11
+ if (isSignal(source)) {
12
+ return computed(() => fn((source as ReadableSignal<any>)())) as any
13
+ }
14
+ return fn(source) as any
15
+ }
16
+
17
+ // ─── Collection transforms ──────────────────────────────────────────────────
18
+
19
+ /** Filter items by predicate. Signal in → Computed out. */
20
+ export function filter<T>(
21
+ source: ReadableSignal<T[]>,
22
+ predicate: (item: T, index: number) => boolean,
23
+ ): ReturnType<typeof computed<T[]>>
24
+ export function filter<T>(source: T[], predicate: (item: T, index: number) => boolean): T[]
25
+ export function filter<T>(
26
+ source: ReadableSignal<T[]> | T[],
27
+ predicate: (item: T, index: number) => boolean,
28
+ ): any {
29
+ return reactive(source, (arr: T[]) => arr.filter(predicate))
30
+ }
31
+
32
+ /** Map items to a new type. */
33
+ export function map<T, U>(
34
+ source: ReadableSignal<T[]>,
35
+ fn: (item: T, index: number) => U,
36
+ ): ReturnType<typeof computed<U[]>>
37
+ export function map<T, U>(source: T[], fn: (item: T, index: number) => U): U[]
38
+ export function map<T, U>(
39
+ source: ReadableSignal<T[]> | T[],
40
+ fn: (item: T, index: number) => U,
41
+ ): any {
42
+ return reactive(source, (arr: T[]) => arr.map(fn))
43
+ }
44
+
45
+ /** Sort items by key or comparator. */
46
+ export function sortBy<T>(
47
+ source: ReadableSignal<T[]>,
48
+ key: KeyOf<T>,
49
+ ): ReturnType<typeof computed<T[]>>
50
+ export function sortBy<T>(source: T[], key: KeyOf<T>): T[]
51
+ export function sortBy<T>(source: ReadableSignal<T[]> | T[], key: KeyOf<T>): any {
52
+ const getKey = resolveKey(key)
53
+ return reactive(source, (arr: T[]) =>
54
+ [...arr].sort((a, b) => {
55
+ const ka = getKey(a)
56
+ const kb = getKey(b)
57
+ return ka < kb ? -1 : ka > kb ? 1 : 0
58
+ }),
59
+ )
60
+ }
61
+
62
+ /** Group items by key. Returns Record<string, T[]>. */
63
+ export function groupBy<T>(
64
+ source: ReadableSignal<T[]>,
65
+ key: KeyOf<T>,
66
+ ): ReturnType<typeof computed<Record<string, T[]>>>
67
+ export function groupBy<T>(source: T[], key: KeyOf<T>): Record<string, T[]>
68
+ export function groupBy<T>(source: ReadableSignal<T[]> | T[], key: KeyOf<T>): any {
69
+ const getKey = resolveKey(key)
70
+ return reactive(source, (arr: T[]) => {
71
+ const result: Record<string, T[]> = {}
72
+ for (const item of arr) {
73
+ const k = String(getKey(item))
74
+ let group = result[k]
75
+ if (!group) {
76
+ group = []
77
+ result[k] = group
78
+ }
79
+ group.push(item)
80
+ }
81
+ return result
82
+ })
83
+ }
84
+
85
+ /** Index items by key. Returns Record<string, T> (last wins on collision). */
86
+ export function keyBy<T>(
87
+ source: ReadableSignal<T[]>,
88
+ key: KeyOf<T>,
89
+ ): ReturnType<typeof computed<Record<string, T>>>
90
+ export function keyBy<T>(source: T[], key: KeyOf<T>): Record<string, T>
91
+ export function keyBy<T>(source: ReadableSignal<T[]> | T[], key: KeyOf<T>): any {
92
+ const getKey = resolveKey(key)
93
+ return reactive(source, (arr: T[]) => {
94
+ const result: Record<string, T> = {}
95
+ for (const item of arr) result[String(getKey(item))] = item
96
+ return result
97
+ })
98
+ }
99
+
100
+ /** Deduplicate items by key. */
101
+ export function uniqBy<T>(
102
+ source: ReadableSignal<T[]>,
103
+ key: KeyOf<T>,
104
+ ): ReturnType<typeof computed<T[]>>
105
+ export function uniqBy<T>(source: T[], key: KeyOf<T>): T[]
106
+ export function uniqBy<T>(source: ReadableSignal<T[]> | T[], key: KeyOf<T>): any {
107
+ const getKey = resolveKey(key)
108
+ return reactive(source, (arr: T[]) => {
109
+ const seen = new Set<string | number>()
110
+ return arr.filter((item) => {
111
+ const k = getKey(item)
112
+ if (seen.has(k)) return false
113
+ seen.add(k)
114
+ return true
115
+ })
116
+ })
117
+ }
118
+
119
+ /** Take the first n items. */
120
+ export function take<T>(source: ReadableSignal<T[]>, n: number): ReturnType<typeof computed<T[]>>
121
+ export function take<T>(source: T[], n: number): T[]
122
+ export function take<T>(source: ReadableSignal<T[]> | T[], n: number): any {
123
+ return reactive(source, (arr: T[]) => arr.slice(0, n))
124
+ }
125
+
126
+ /** Split into chunks of given size. */
127
+ export function chunk<T>(
128
+ source: ReadableSignal<T[]>,
129
+ size: number,
130
+ ): ReturnType<typeof computed<T[][]>>
131
+ export function chunk<T>(source: T[], size: number): T[][]
132
+ export function chunk<T>(source: ReadableSignal<T[]> | T[], size: number): any {
133
+ return reactive(source, (arr: T[]) => {
134
+ const result: T[][] = []
135
+ for (let i = 0; i < arr.length; i += size) result.push(arr.slice(i, i + size))
136
+ return result
137
+ })
138
+ }
139
+
140
+ /** Flatten one level of nesting. */
141
+ export function flatten<T>(source: ReadableSignal<T[][]>): ReturnType<typeof computed<T[]>>
142
+ export function flatten<T>(source: T[][]): T[]
143
+ export function flatten<T>(source: ReadableSignal<T[][]> | T[][]): any {
144
+ return reactive(source, (arr: T[][]) => arr.flat())
145
+ }
146
+
147
+ /** Find the first item matching a predicate. */
148
+ export function find<T>(
149
+ source: ReadableSignal<T[]>,
150
+ predicate: (item: T) => boolean,
151
+ ): ReturnType<typeof computed<T | undefined>>
152
+ export function find<T>(source: T[], predicate: (item: T) => boolean): T | undefined
153
+ export function find<T>(source: ReadableSignal<T[]> | T[], predicate: (item: T) => boolean): any {
154
+ return reactive(source, (arr: T[]) => arr.find(predicate))
155
+ }
156
+
157
+ /** Skip the first n items. */
158
+ export function skip<T>(source: ReadableSignal<T[]>, n: number): ReturnType<typeof computed<T[]>>
159
+ export function skip<T>(source: T[], n: number): T[]
160
+ export function skip<T>(source: ReadableSignal<T[]> | T[], n: number): any {
161
+ return reactive(source, (arr: T[]) => arr.slice(n))
162
+ }
163
+
164
+ /** Take the last n items. */
165
+ export function last<T>(source: ReadableSignal<T[]>, n: number): ReturnType<typeof computed<T[]>>
166
+ export function last<T>(source: T[], n: number): T[]
167
+ export function last<T>(source: ReadableSignal<T[]> | T[], n: number): any {
168
+ return reactive(source, (arr: T[]) => arr.slice(-n))
169
+ }
170
+
171
+ /** Map over values of a Record. Useful after groupBy. */
172
+ export function mapValues<T, U>(
173
+ source: ReadableSignal<Record<string, T>>,
174
+ fn: (value: T, key: string) => U,
175
+ ): ReturnType<typeof computed<Record<string, U>>>
176
+ export function mapValues<T, U>(
177
+ source: Record<string, T>,
178
+ fn: (value: T, key: string) => U,
179
+ ): Record<string, U>
180
+ export function mapValues<T, U>(
181
+ source: ReadableSignal<Record<string, T>> | Record<string, T>,
182
+ fn: (value: T, key: string) => U,
183
+ ): any {
184
+ return reactive(source, (obj: Record<string, T>) => {
185
+ const result: Record<string, U> = {}
186
+ for (const key of Object.keys(obj)) result[key] = fn(obj[key] as T, key)
187
+ return result
188
+ })
189
+ }
package/src/index.ts ADDED
@@ -0,0 +1,114 @@
1
+ import { average, count, max, min, sum } from "./aggregation"
2
+ import {
3
+ chunk,
4
+ filter,
5
+ find,
6
+ flatten,
7
+ groupBy,
8
+ keyBy,
9
+ last,
10
+ map,
11
+ mapValues,
12
+ skip,
13
+ sortBy,
14
+ take,
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"
21
+
22
+ export type { KeyOf, ReadableSignal } from "./types"
23
+
24
+ /**
25
+ * Signal-aware reactive transforms.
26
+ *
27
+ * Every function is overloaded:
28
+ * - `Signal<T[]>` input → returns `Computed<R>` (reactive)
29
+ * - `T[]` input → returns `R` (static)
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * import { rx } from "@pyreon/rx"
34
+ *
35
+ * const users = signal<User[]>([])
36
+ * const active = rx.filter(users, u => u.active) // Computed<User[]>
37
+ * const sorted = rx.sortBy(active, "name") // Computed<User[]>
38
+ * const top10 = rx.take(sorted, 10) // Computed<User[]>
39
+ *
40
+ * // Or pipe:
41
+ * const result = rx.pipe(users,
42
+ * items => items.filter(u => u.active),
43
+ * items => items.sort((a, b) => a.name.localeCompare(b.name)),
44
+ * items => items.slice(0, 10),
45
+ * )
46
+ * ```
47
+ */
48
+ export const rx = {
49
+ // Collections
50
+ filter,
51
+ map,
52
+ sortBy,
53
+ groupBy,
54
+ keyBy,
55
+ uniqBy,
56
+ take,
57
+ skip,
58
+ last,
59
+ chunk,
60
+ flatten,
61
+ find,
62
+ mapValues,
63
+
64
+ // Aggregation
65
+ count,
66
+ sum,
67
+ min,
68
+ max,
69
+ average,
70
+
71
+ // Operators
72
+ distinct,
73
+ scan,
74
+ combine,
75
+
76
+ // Timing
77
+ debounce,
78
+ throttle,
79
+
80
+ // Search
81
+ search,
82
+
83
+ // Pipe
84
+ pipe,
85
+ } as const
86
+
87
+ // Also export individual functions for tree-shaking
88
+ export {
89
+ average,
90
+ chunk,
91
+ combine,
92
+ count,
93
+ debounce,
94
+ distinct,
95
+ filter,
96
+ find,
97
+ flatten,
98
+ groupBy,
99
+ keyBy,
100
+ last,
101
+ map,
102
+ mapValues,
103
+ max,
104
+ min,
105
+ pipe,
106
+ scan,
107
+ search,
108
+ skip,
109
+ sortBy,
110
+ sum,
111
+ take,
112
+ throttle,
113
+ uniqBy,
114
+ }
@@ -0,0 +1,75 @@
1
+ import { computed, effect, signal } from "@pyreon/reactivity"
2
+ import type { ReadableSignal } from "./types"
3
+
4
+ /**
5
+ * Distinct — skip consecutive duplicate values from a signal.
6
+ * Uses `Object.is` by default, or a custom equality function.
7
+ */
8
+ export function distinct<T>(
9
+ source: ReadableSignal<T>,
10
+ equals: (a: T, b: T) => boolean = Object.is,
11
+ ): ReadableSignal<T> {
12
+ const result = signal(source())
13
+
14
+ effect(() => {
15
+ const val = source()
16
+ if (!equals(val, result.peek())) {
17
+ result.set(val)
18
+ }
19
+ })
20
+
21
+ return result
22
+ }
23
+
24
+ /**
25
+ * Scan — running accumulator over signal changes.
26
+ * Like Array.reduce but emits the accumulated value on each source change.
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * const clicks = signal(0)
31
+ * const total = rx.scan(clicks, (acc, val) => acc + val, 0)
32
+ * // clicks: 1 → total: 1
33
+ * // clicks: 3 → total: 4
34
+ * // clicks: 2 → total: 6
35
+ * ```
36
+ */
37
+ export function scan<T, U>(
38
+ source: ReadableSignal<T>,
39
+ reducer: (acc: U, value: T) => U,
40
+ initial: U,
41
+ ): ReadableSignal<U> {
42
+ const result = signal(initial)
43
+
44
+ effect(() => {
45
+ const val = source()
46
+ result.set(reducer(result.peek(), val))
47
+ })
48
+
49
+ return result
50
+ }
51
+
52
+ /**
53
+ * Combine multiple signals into a single computed value.
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * const fullName = rx.combine(firstName, lastName, (f, l) => `${f} ${l}`)
58
+ * ```
59
+ */
60
+ export function combine<A, B, R>(
61
+ a: ReadableSignal<A>,
62
+ b: ReadableSignal<B>,
63
+ fn: (a: A, b: B) => R,
64
+ ): ReturnType<typeof computed<R>>
65
+ export function combine<A, B, C, R>(
66
+ a: ReadableSignal<A>,
67
+ b: ReadableSignal<B>,
68
+ c: ReadableSignal<C>,
69
+ fn: (a: A, b: B, c: C) => R,
70
+ ): ReturnType<typeof computed<R>>
71
+ export function combine(...args: any[]): any {
72
+ const fn = args[args.length - 1] as (...vals: any[]) => any
73
+ const sources = args.slice(0, -1) as ReadableSignal<any>[]
74
+ return computed(() => fn(...sources.map((s) => s())))
75
+ }
package/src/pipe.ts ADDED
@@ -0,0 +1,66 @@
1
+ import { computed } from "@pyreon/reactivity"
2
+ import type { ReadableSignal } from "./types"
3
+ import { isSignal } from "./types"
4
+
5
+ /**
6
+ * Pipe a signal through a chain of transform functions.
7
+ * Each transform receives the resolved value (not the signal) and returns a new value.
8
+ * The entire chain is wrapped in a single `computed()`.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * const topRisks = rx.pipe(
13
+ * findings,
14
+ * (items) => items.filter(f => f.severity === "critical"),
15
+ * (items) => items.sort((a, b) => b.score - a.score),
16
+ * (items) => items.slice(0, 10),
17
+ * )
18
+ * // topRisks() → reactive, type-safe
19
+ * ```
20
+ */
21
+ export function pipe<A, B>(
22
+ source: ReadableSignal<A>,
23
+ f1: (a: A) => B,
24
+ ): ReturnType<typeof computed<B>>
25
+ export function pipe<A, B, C>(
26
+ source: ReadableSignal<A>,
27
+ f1: (a: A) => B,
28
+ f2: (b: B) => C,
29
+ ): ReturnType<typeof computed<C>>
30
+ export function pipe<A, B, C, D>(
31
+ source: ReadableSignal<A>,
32
+ f1: (a: A) => B,
33
+ f2: (b: B) => C,
34
+ f3: (c: C) => D,
35
+ ): ReturnType<typeof computed<D>>
36
+ export function pipe<A, B, C, D, E>(
37
+ source: ReadableSignal<A>,
38
+ f1: (a: A) => B,
39
+ f2: (b: B) => C,
40
+ f3: (c: C) => D,
41
+ f4: (d: D) => E,
42
+ ): ReturnType<typeof computed<E>>
43
+ export function pipe<A, B, C, D, E, F>(
44
+ source: ReadableSignal<A>,
45
+ f1: (a: A) => B,
46
+ f2: (b: B) => C,
47
+ f3: (c: C) => D,
48
+ f4: (d: D) => E,
49
+ f5: (e: E) => F,
50
+ ): ReturnType<typeof computed<F>>
51
+ // Plain value overloads
52
+ export function pipe<A, B>(source: A, f1: (a: A) => B): B
53
+ export function pipe<A, B, C>(source: A, f1: (a: A) => B, f2: (b: B) => C): C
54
+ export function pipe<A, B, C, D>(source: A, f1: (a: A) => B, f2: (b: B) => C, f3: (c: C) => D): D
55
+ export function pipe(source: any, ...fns: Array<(v: any) => any>): any {
56
+ if (isSignal(source)) {
57
+ return computed(() => {
58
+ let val = (source as ReadableSignal<any>)()
59
+ for (const fn of fns) val = fn(val)
60
+ return val
61
+ })
62
+ }
63
+ let val = source
64
+ for (const fn of fns) val = fn(val)
65
+ return val
66
+ }
package/src/search.ts ADDED
@@ -0,0 +1,34 @@
1
+ import { computed } from "@pyreon/reactivity"
2
+ import type { ReadableSignal } from "./types"
3
+ import { isSignal } from "./types"
4
+
5
+ /**
6
+ * Search items by substring matching across specified keys.
7
+ * Case-insensitive. Signal-aware — reactive when either source or query is a signal.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * const results = rx.search(users, searchQuery, ["name", "email"])
12
+ * ```
13
+ */
14
+ export function search<T>(
15
+ source: ReadableSignal<T[]> | T[],
16
+ query: ReadableSignal<string> | string,
17
+ keys: (keyof T)[],
18
+ ): any {
19
+ const isReactive = isSignal(source) || isSignal(query)
20
+
21
+ const doSearch = (): T[] => {
22
+ const arr = isSignal(source) ? (source as ReadableSignal<T[]>)() : source
23
+ const q = (isSignal(query) ? (query as ReadableSignal<string>)() : query).toLowerCase().trim()
24
+ if (!q) return arr
25
+ return arr.filter((item) =>
26
+ keys.some((key) => {
27
+ const val = item[key]
28
+ return typeof val === "string" && val.toLowerCase().includes(q)
29
+ }),
30
+ )
31
+ }
32
+
33
+ return isReactive ? computed(doSearch) : doSearch()
34
+ }
@@ -0,0 +1,83 @@
1
+ import { signal } from "@pyreon/reactivity"
2
+ import { describe, expect, it } from "vitest"
3
+ import { average, count, max, min, sum } from "../aggregation"
4
+
5
+ describe("aggregation — plain values", () => {
6
+ it("count", () => {
7
+ expect(count([1, 2, 3])).toBe(3)
8
+ expect(count([])).toBe(0)
9
+ })
10
+
11
+ it("sum", () => {
12
+ expect(sum([1, 2, 3])).toBe(6)
13
+ })
14
+
15
+ it("sum with key", () => {
16
+ const items = [{ v: 10 }, { v: 20 }, { v: 30 }]
17
+ expect(sum(items, "v")).toBe(60)
18
+ })
19
+
20
+ it("min", () => {
21
+ const items = [{ v: 3 }, { v: 1 }, { v: 2 }]
22
+ expect(min(items, "v")?.v).toBe(1)
23
+ })
24
+
25
+ it("max", () => {
26
+ const items = [{ v: 3 }, { v: 1 }, { v: 2 }]
27
+ expect(max(items, "v")?.v).toBe(3)
28
+ })
29
+
30
+ it("min/max empty array", () => {
31
+ expect(min([])).toBeUndefined()
32
+ expect(max([])).toBeUndefined()
33
+ })
34
+
35
+ it("average", () => {
36
+ expect(average([2, 4, 6])).toBe(4)
37
+ })
38
+
39
+ it("average with key", () => {
40
+ const items = [{ v: 10 }, { v: 20 }, { v: 30 }]
41
+ expect(average(items, "v")).toBe(20)
42
+ })
43
+
44
+ it("average empty array returns 0", () => {
45
+ expect(average([])).toBe(0)
46
+ })
47
+ })
48
+
49
+ describe("aggregation — signal values", () => {
50
+ it("count returns computed", () => {
51
+ const src = signal([1, 2, 3])
52
+ const c = count(src)
53
+ expect(c()).toBe(3)
54
+ src.set([1])
55
+ expect(c()).toBe(1)
56
+ })
57
+
58
+ it("sum returns computed", () => {
59
+ const src = signal([1, 2, 3])
60
+ const s = sum(src)
61
+ expect(s()).toBe(6)
62
+ src.set([10, 20])
63
+ expect(s()).toBe(30)
64
+ })
65
+
66
+ it("average returns computed", () => {
67
+ const src = signal([2, 4, 6])
68
+ const avg = average(src)
69
+ expect(avg()).toBe(4)
70
+
71
+ src.set([10, 20])
72
+ expect(avg()).toBe(15)
73
+ })
74
+
75
+ it("average with key returns computed", () => {
76
+ const src = signal([{ v: 10 }, { v: 20 }, { v: 30 }])
77
+ const avg = average(src, "v")
78
+ expect(avg()).toBe(20)
79
+
80
+ src.set([{ v: 100 }])
81
+ expect(avg()).toBe(100)
82
+ })
83
+ })