@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.
- package/LICENSE +21 -0
- package/README.md +50 -0
- package/lib/analysis/index.js.html +5406 -0
- package/lib/index.js +343 -0
- package/lib/index.js.map +1 -0
- package/lib/types/index.d.ts +214 -0
- package/lib/types/index.d.ts.map +1 -0
- package/package.json +45 -0
- package/src/aggregation.ts +86 -0
- package/src/collections.ts +189 -0
- package/src/index.ts +114 -0
- package/src/operators.ts +75 -0
- package/src/pipe.ts +66 -0
- package/src/search.ts +34 -0
- package/src/tests/aggregation.test.ts +83 -0
- package/src/tests/collections.test.ts +186 -0
- package/src/tests/operators.test.ts +153 -0
- package/src/tests/pipe.test.ts +51 -0
- package/src/tests/timing.test.ts +55 -0
- package/src/timing.ts +70 -0
- package/src/types.ts +21 -0
|
@@ -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
|
+
}
|
package/src/operators.ts
ADDED
|
@@ -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
|
+
})
|