@mantiq/helpers 0.0.1

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/src/async.ts ADDED
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Async utility functions — retry, sleep, parallel, timeout, debounce, throttle.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * await sleep('2s')
7
+ * const data = await retry(3, () => fetchApi(), '500ms')
8
+ * const results = await parallel([fn1, fn2, fn3], { concurrency: 2 })
9
+ * const result = await timeout(fetchData(), '5s')
10
+ * ```
11
+ */
12
+
13
+ /** Parse a human-readable duration string to milliseconds */
14
+ export function parseDuration(duration: string | number): number {
15
+ if (typeof duration === 'number') return duration
16
+ const match = duration.match(/^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)?$/i)
17
+ if (!match) throw new Error(`Invalid duration: ${duration}`)
18
+ const value = parseFloat(match[1]!)
19
+ const unit = (match[2] ?? 'ms').toLowerCase()
20
+ switch (unit) {
21
+ case 'ms': return value
22
+ case 's': return value * 1000
23
+ case 'm': return value * 60_000
24
+ case 'h': return value * 3_600_000
25
+ case 'd': return value * 86_400_000
26
+ default: return value
27
+ }
28
+ }
29
+
30
+ /** Sleep for a given duration */
31
+ export function sleep(duration: string | number): Promise<void> {
32
+ const ms = parseDuration(duration)
33
+ return new Promise((resolve) => setTimeout(resolve, ms))
34
+ }
35
+
36
+ /**
37
+ * Retry a function up to N times with optional delay between attempts.
38
+ * Supports exponential backoff via callback delay.
39
+ */
40
+ export async function retry<T>(
41
+ times: number,
42
+ callback: (attempt: number) => T | Promise<T>,
43
+ delay?: string | number | ((attempt: number) => string | number),
44
+ ): Promise<T> {
45
+ let lastError: Error | undefined
46
+ for (let attempt = 1; attempt <= times; attempt++) {
47
+ try {
48
+ return await callback(attempt)
49
+ } catch (e) {
50
+ lastError = e instanceof Error ? e : new Error(String(e))
51
+ if (attempt < times && delay !== undefined) {
52
+ const d = typeof delay === 'function' ? delay(attempt) : delay
53
+ await sleep(d)
54
+ }
55
+ }
56
+ }
57
+ throw lastError
58
+ }
59
+
60
+ /**
61
+ * Run multiple async functions with optional concurrency limit.
62
+ * Like Promise.all but with a concurrency pool.
63
+ */
64
+ export async function parallel<T>(
65
+ tasks: Array<() => Promise<T>>,
66
+ options?: { concurrency?: number },
67
+ ): Promise<T[]> {
68
+ const concurrency = options?.concurrency ?? Infinity
69
+
70
+ if (concurrency >= tasks.length) {
71
+ return Promise.all(tasks.map((fn) => fn()))
72
+ }
73
+
74
+ const results: T[] = new Array(tasks.length)
75
+ let nextIndex = 0
76
+
77
+ async function runNext(): Promise<void> {
78
+ while (nextIndex < tasks.length) {
79
+ const idx = nextIndex++
80
+ results[idx] = await tasks[idx]!()
81
+ }
82
+ }
83
+
84
+ const workers = Array.from(
85
+ { length: Math.min(concurrency, tasks.length) },
86
+ () => runNext(),
87
+ )
88
+
89
+ await Promise.all(workers)
90
+ return results
91
+ }
92
+
93
+ /** Wrap a promise with a timeout — rejects if it doesn't resolve in time */
94
+ export function timeout<T>(
95
+ promise: Promise<T>,
96
+ duration: string | number,
97
+ message?: string,
98
+ ): Promise<T> {
99
+ const ms = parseDuration(duration)
100
+ return new Promise<T>((resolve, reject) => {
101
+ const timer = setTimeout(() => {
102
+ reject(new Error(message ?? `Operation timed out after ${ms}ms`))
103
+ }, ms)
104
+
105
+ promise.then(
106
+ (value) => { clearTimeout(timer); resolve(value) },
107
+ (error) => { clearTimeout(timer); reject(error) },
108
+ )
109
+ })
110
+ }
111
+
112
+ /**
113
+ * Create a debounced version of a function.
114
+ * The function will only execute after `wait` ms of no calls.
115
+ */
116
+ export function debounce<T extends (...args: any[]) => any>(
117
+ fn: T,
118
+ wait: string | number,
119
+ ): T & { cancel(): void; flush(): void } {
120
+ const ms = parseDuration(wait)
121
+ let timer: ReturnType<typeof setTimeout> | null = null
122
+ let lastArgs: any[] | null = null
123
+ let lastThis: any = null
124
+
125
+ const debounced = function (this: any, ...args: any[]) {
126
+ lastArgs = args
127
+ lastThis = this
128
+ if (timer) clearTimeout(timer)
129
+ timer = setTimeout(() => {
130
+ timer = null
131
+ fn.apply(lastThis, lastArgs!)
132
+ lastArgs = null
133
+ }, ms)
134
+ } as any
135
+
136
+ debounced.cancel = () => {
137
+ if (timer) clearTimeout(timer)
138
+ timer = null
139
+ lastArgs = null
140
+ }
141
+
142
+ debounced.flush = () => {
143
+ if (timer && lastArgs) {
144
+ clearTimeout(timer)
145
+ timer = null
146
+ fn.apply(lastThis, lastArgs)
147
+ lastArgs = null
148
+ }
149
+ }
150
+
151
+ return debounced
152
+ }
153
+
154
+ /**
155
+ * Create a throttled version of a function.
156
+ * The function will execute at most once per `wait` ms.
157
+ */
158
+ export function throttle<T extends (...args: any[]) => any>(
159
+ fn: T,
160
+ wait: string | number,
161
+ ): T & { cancel(): void } {
162
+ const ms = parseDuration(wait)
163
+ let lastCall = 0
164
+ let timer: ReturnType<typeof setTimeout> | null = null
165
+
166
+ const throttled = function (this: any, ...args: any[]) {
167
+ const now = Date.now()
168
+ const remaining = ms - (now - lastCall)
169
+
170
+ if (remaining <= 0) {
171
+ if (timer) { clearTimeout(timer); timer = null }
172
+ lastCall = now
173
+ fn.apply(this, args)
174
+ } else if (!timer) {
175
+ timer = setTimeout(() => {
176
+ lastCall = Date.now()
177
+ timer = null
178
+ fn.apply(this, args)
179
+ }, remaining)
180
+ }
181
+ } as any
182
+
183
+ throttled.cancel = () => {
184
+ if (timer) clearTimeout(timer)
185
+ timer = null
186
+ lastCall = 0
187
+ }
188
+
189
+ return throttled
190
+ }
191
+
192
+ /**
193
+ * Run tasks in a waterfall: each task receives the result of the previous.
194
+ */
195
+ export async function waterfall<T>(
196
+ initial: T,
197
+ tasks: Array<(value: T) => T | Promise<T>>,
198
+ ): Promise<T> {
199
+ let result = initial
200
+ for (const task of tasks) {
201
+ result = await task(result)
202
+ }
203
+ return result
204
+ }
205
+
206
+ /** Defer a function to the next microtask */
207
+ export function defer(fn: () => void): void {
208
+ queueMicrotask(fn)
209
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Function utilities — tap, pipe, pipeline, once, memoize, benchmark.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * const user = tap(new User(), (u) => { u.name = 'John' })
7
+ * const result = pipe(5, double, addOne, toString) // '11'
8
+ * const getValue = once(() => expensiveComputation())
9
+ * const cachedFetch = memoize(fetchUser, { ttl: 60_000 })
10
+ * ```
11
+ */
12
+
13
+ import { parseDuration } from './async.ts'
14
+
15
+ /** Pass a value through a callback and return the original value */
16
+ export function tap<T>(value: T, callback: (value: T) => void): T {
17
+ callback(value)
18
+ return value
19
+ }
20
+
21
+ /**
22
+ * Pipe a value through a sequence of functions (left to right).
23
+ * Each function receives the return value of the previous.
24
+ */
25
+ export function pipe<A, B>(value: A, fn1: (a: A) => B): B
26
+ export function pipe<A, B, C>(value: A, fn1: (a: A) => B, fn2: (b: B) => C): C
27
+ export function pipe<A, B, C, D>(value: A, fn1: (a: A) => B, fn2: (b: B) => C, fn3: (c: C) => D): D
28
+ export function pipe<A, B, C, D, E>(value: A, fn1: (a: A) => B, fn2: (b: B) => C, fn3: (c: C) => D, fn4: (d: D) => E): E
29
+ export function pipe(value: any, ...fns: Array<(arg: any) => any>): any {
30
+ return fns.reduce((acc, fn) => fn(acc), value)
31
+ }
32
+
33
+ /**
34
+ * Pipeline: pass a value through an array of functions.
35
+ * Same as pipe but takes an array instead of variadic args.
36
+ */
37
+ export function pipeline<T>(value: T, fns: Array<(value: any) => any>): any {
38
+ return fns.reduce((acc, fn) => fn(acc), value as any)
39
+ }
40
+
41
+ /** Compose functions right to left */
42
+ export function compose<T>(...fns: Array<(arg: any) => any>): (arg: T) => any {
43
+ return (arg: T) => fns.reduceRight((acc, fn) => fn(acc), arg as any)
44
+ }
45
+
46
+ /**
47
+ * Create a function that executes only once.
48
+ * Subsequent calls return the first result.
49
+ */
50
+ export function once<T extends (...args: any[]) => any>(fn: T): T {
51
+ let called = false
52
+ let result: any
53
+
54
+ return ((...args: any[]) => {
55
+ if (called) return result
56
+ called = true
57
+ result = fn(...args)
58
+ return result
59
+ }) as T
60
+ }
61
+
62
+ /**
63
+ * Memoize a function with optional TTL.
64
+ * Cache key is derived from arguments (JSON serialized).
65
+ */
66
+ export function memoize<T extends (...args: any[]) => any>(
67
+ fn: T,
68
+ options?: { ttl?: string | number; maxSize?: number },
69
+ ): T & { cache: Map<string, any>; clear(): void } {
70
+ const ttl = options?.ttl ? parseDuration(options.ttl) : null
71
+ const maxSize = options?.maxSize ?? 1000
72
+ const cache = new Map<string, { value: any; expiresAt: number | null }>()
73
+
74
+ const memoized = ((...args: any[]) => {
75
+ const key = JSON.stringify(args)
76
+ const entry = cache.get(key)
77
+
78
+ if (entry) {
79
+ if (entry.expiresAt === null || Date.now() < entry.expiresAt) {
80
+ return entry.value
81
+ }
82
+ cache.delete(key)
83
+ }
84
+
85
+ const result = fn(...args)
86
+ const expiresAt = ttl ? Date.now() + ttl : null
87
+
88
+ // Evict oldest if at capacity
89
+ if (cache.size >= maxSize) {
90
+ const firstKey = cache.keys().next().value
91
+ if (firstKey !== undefined) cache.delete(firstKey)
92
+ }
93
+
94
+ cache.set(key, { value: result, expiresAt })
95
+ return result
96
+ }) as any
97
+
98
+ memoized.cache = cache
99
+ memoized.clear = () => cache.clear()
100
+
101
+ return memoized
102
+ }
103
+
104
+ /**
105
+ * Benchmark a function's execution time.
106
+ * Returns [result, durationMs].
107
+ */
108
+ export async function benchmark<T>(
109
+ fn: () => T | Promise<T>,
110
+ ): Promise<[T, number]> {
111
+ const start = performance.now()
112
+ const result = await fn()
113
+ const duration = performance.now() - start
114
+ return [result, duration]
115
+ }
116
+
117
+ /** No-op function */
118
+ export function noop(): void {}
119
+
120
+ /** Identity function — returns its argument unchanged */
121
+ export function identity<T>(value: T): T {
122
+ return value
123
+ }
124
+
125
+ /**
126
+ * Create a function that always returns the same value.
127
+ */
128
+ export function constant<T>(value: T): () => T {
129
+ return () => value
130
+ }
131
+
132
+ /**
133
+ * Wrap a value in a callback — useful for lazy evaluation.
134
+ */
135
+ export function lazy<T>(fn: () => T): { value: T } {
136
+ let computed = false
137
+ let result: T
138
+
139
+ return {
140
+ get value(): T {
141
+ if (!computed) {
142
+ result = fn()
143
+ computed = true
144
+ }
145
+ return result
146
+ },
147
+ }
148
+ }
149
+
150
+ /** Times: execute a callback N times, collect results */
151
+ export function times<T>(n: number, fn: (index: number) => T): T[] {
152
+ return Array.from({ length: n }, (_, i) => fn(i))
153
+ }
package/src/index.ts ADDED
@@ -0,0 +1,65 @@
1
+ // @mantiq/helpers — public API exports
2
+
3
+ // ── Laravel-equivalent utilities ─────────────────────────────────
4
+ export { Str, Stringable } from './Str.ts'
5
+ export { Arr } from './Arr.ts'
6
+ export { Num } from './Num.ts'
7
+ export { Collection, LazyCollection, collect, lazy, generate, range } from './Collection.ts'
8
+
9
+ // ── Beyond-Laravel utilities ─────────────────────────────────────
10
+ export { Result } from './Result.ts'
11
+ export type { Result as ResultType } from './Result.ts'
12
+ export { match } from './match.ts'
13
+ export { Duration } from './Duration.ts'
14
+ export { is } from './is.ts'
15
+
16
+ // ── HTTP client ──────────────────────────────────────────────────
17
+ export { Http, PendingRequest } from './Http.ts'
18
+ export type { HttpResponse, HttpError, HttpMiddleware, RetryConfig } from './Http.ts'
19
+ export { HttpFake } from './HttpFake.ts'
20
+ export type { StubResponse, StubHandler } from './HttpFake.ts'
21
+
22
+ // ── Async utilities ──────────────────────────────────────────────
23
+ export {
24
+ parseDuration,
25
+ sleep,
26
+ retry,
27
+ parallel,
28
+ timeout,
29
+ debounce,
30
+ throttle,
31
+ waterfall,
32
+ defer,
33
+ } from './async.ts'
34
+
35
+ // ── Object utilities ─────────────────────────────────────────────
36
+ export {
37
+ deepClone,
38
+ deepMerge,
39
+ deepFreeze,
40
+ deepEqual,
41
+ pick,
42
+ omit,
43
+ diff,
44
+ mapValues,
45
+ mapKeys,
46
+ filterObject,
47
+ invert,
48
+ isPlainObject,
49
+ } from './objects.ts'
50
+
51
+ // ── Function utilities ───────────────────────────────────────────
52
+ export {
53
+ tap,
54
+ pipe,
55
+ pipeline,
56
+ compose,
57
+ once,
58
+ memoize,
59
+ benchmark,
60
+ noop,
61
+ identity,
62
+ constant,
63
+ lazy as lazyValue,
64
+ times,
65
+ } from './functions.ts'
package/src/is.ts ADDED
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Runtime type guards and predicates.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * is.string('hello') // true
7
+ * is.empty([]) // true
8
+ * is.plainObject({}) // true
9
+ * is.between(5, 1, 10) // true
10
+ * is.email('user@example.com') // true
11
+ * ```
12
+ */
13
+ export const is = {
14
+ // ── Type checks ─────────────────────────────────────────────────
15
+
16
+ string(value: unknown): value is string {
17
+ return typeof value === 'string'
18
+ },
19
+
20
+ number(value: unknown): value is number {
21
+ return typeof value === 'number' && !Number.isNaN(value)
22
+ },
23
+
24
+ integer(value: unknown): value is number {
25
+ return typeof value === 'number' && Number.isInteger(value)
26
+ },
27
+
28
+ float(value: unknown): value is number {
29
+ return typeof value === 'number' && !Number.isInteger(value) && !Number.isNaN(value)
30
+ },
31
+
32
+ boolean(value: unknown): value is boolean {
33
+ return typeof value === 'boolean'
34
+ },
35
+
36
+ function(value: unknown): value is Function {
37
+ return typeof value === 'function'
38
+ },
39
+
40
+ symbol(value: unknown): value is symbol {
41
+ return typeof value === 'symbol'
42
+ },
43
+
44
+ bigint(value: unknown): value is bigint {
45
+ return typeof value === 'bigint'
46
+ },
47
+
48
+ array(value: unknown): value is any[] {
49
+ return Array.isArray(value)
50
+ },
51
+
52
+ object(value: unknown): value is object {
53
+ return value !== null && typeof value === 'object'
54
+ },
55
+
56
+ plainObject(value: unknown): value is Record<string, any> {
57
+ if (value === null || typeof value !== 'object') return false
58
+ const proto = Object.getPrototypeOf(value)
59
+ return proto === Object.prototype || proto === null
60
+ },
61
+
62
+ date(value: unknown): value is Date {
63
+ return value instanceof Date && !isNaN(value.getTime())
64
+ },
65
+
66
+ regExp(value: unknown): value is RegExp {
67
+ return value instanceof RegExp
68
+ },
69
+
70
+ promise(value: unknown): value is Promise<any> {
71
+ return value instanceof Promise || (
72
+ value !== null && typeof value === 'object' && typeof (value as any).then === 'function'
73
+ )
74
+ },
75
+
76
+ map(value: unknown): value is Map<any, any> {
77
+ return value instanceof Map
78
+ },
79
+
80
+ set(value: unknown): value is Set<any> {
81
+ return value instanceof Set
82
+ },
83
+
84
+ error(value: unknown): value is Error {
85
+ return value instanceof Error
86
+ },
87
+
88
+ // ── Nullish checks ──────────────────────────────────────────────
89
+
90
+ null(value: unknown): value is null {
91
+ return value === null
92
+ },
93
+
94
+ undefined(value: unknown): value is undefined {
95
+ return value === undefined
96
+ },
97
+
98
+ nullish(value: unknown): value is null | undefined {
99
+ return value === null || value === undefined
100
+ },
101
+
102
+ defined<T>(value: T | null | undefined): value is T {
103
+ return value !== null && value !== undefined
104
+ },
105
+
106
+ // ── Emptiness ───────────────────────────────────────────────────
107
+
108
+ /** Check if a value is "empty" (null, undefined, '', [], {}, Map(0), Set(0)) */
109
+ empty(value: unknown): boolean {
110
+ if (value === null || value === undefined) return true
111
+ if (typeof value === 'string') return value.length === 0
112
+ if (Array.isArray(value)) return value.length === 0
113
+ if (value instanceof Map || value instanceof Set) return value.size === 0
114
+ if (typeof value === 'object') return Object.keys(value).length === 0
115
+ return false
116
+ },
117
+
118
+ /** Opposite of empty */
119
+ notEmpty(value: unknown): boolean {
120
+ return !is.empty(value)
121
+ },
122
+
123
+ // ── String format checks ────────────────────────────────────────
124
+
125
+ email(value: string): boolean {
126
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
127
+ },
128
+
129
+ url(value: string): boolean {
130
+ try { new URL(value); return true } catch { return false }
131
+ },
132
+
133
+ uuid(value: string): boolean {
134
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)
135
+ },
136
+
137
+ json(value: string): boolean {
138
+ try { JSON.parse(value); return true } catch { return false }
139
+ },
140
+
141
+ numeric(value: string): boolean {
142
+ return /^-?\d+(\.\d+)?$/.test(value)
143
+ },
144
+
145
+ alpha(value: string): boolean {
146
+ return /^[a-zA-Z]+$/.test(value)
147
+ },
148
+
149
+ alphanumeric(value: string): boolean {
150
+ return /^[a-zA-Z0-9]+$/.test(value)
151
+ },
152
+
153
+ ip(value: string): boolean {
154
+ // IPv4
155
+ if (/^(\d{1,3}\.){3}\d{1,3}$/.test(value)) {
156
+ return value.split('.').every((n) => parseInt(n, 10) <= 255)
157
+ }
158
+ // IPv6 (simplified check)
159
+ return /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/.test(value)
160
+ },
161
+
162
+ // ── Number checks ───────────────────────────────────────────────
163
+
164
+ positive(value: number): boolean {
165
+ return value > 0
166
+ },
167
+
168
+ negative(value: number): boolean {
169
+ return value < 0
170
+ },
171
+
172
+ zero(value: number): boolean {
173
+ return value === 0
174
+ },
175
+
176
+ between(value: number, min: number, max: number): boolean {
177
+ return value >= min && value <= max
178
+ },
179
+
180
+ even(value: number): boolean {
181
+ return value % 2 === 0
182
+ },
183
+
184
+ odd(value: number): boolean {
185
+ return value % 2 !== 0
186
+ },
187
+
188
+ finite(value: number): boolean {
189
+ return Number.isFinite(value)
190
+ },
191
+
192
+ nan(value: unknown): boolean {
193
+ return Number.isNaN(value)
194
+ },
195
+ }
package/src/match.ts ADDED
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Functional pattern matching — an expressive alternative to switch/if-else chains.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * const label = match(statusCode)
7
+ * .when(200, 'OK')
8
+ * .when(404, 'Not Found')
9
+ * .when((code) => code >= 500, 'Server Error')
10
+ * .otherwise('Unknown')
11
+ *
12
+ * const result = match(user.role)
13
+ * .when('admin', () => getAdminDashboard())
14
+ * .when('editor', () => getEditorPanel())
15
+ * .when(['viewer', 'guest'], () => getPublicView())
16
+ * .otherwise(() => redirect('/login'))
17
+ * ```
18
+ */
19
+
20
+ type MatchPredicate<T> = T | T[] | ((value: T) => boolean)
21
+ type MatchResult<R> = R | (() => R)
22
+
23
+ interface MatchBuilder<T, R = never> {
24
+ /** Match against a value, array of values, or predicate function */
25
+ when<U>(predicate: MatchPredicate<T>, result: MatchResult<U>): MatchBuilder<T, R | U>
26
+ /** Provide a default value if nothing matched */
27
+ otherwise<U>(result: MatchResult<U>): R | U
28
+ /** Execute without a default — throws if nothing matched */
29
+ exhaustive(): R
30
+ }
31
+
32
+ class MatchImpl<T, R> implements MatchBuilder<T, R> {
33
+ private arms: Array<{ predicate: MatchPredicate<T>; result: MatchResult<any> }> = []
34
+ private matched = false
35
+ private matchedResult: any = undefined
36
+
37
+ constructor(private readonly value: T) {}
38
+
39
+ when<U>(predicate: MatchPredicate<T>, result: MatchResult<U>): MatchBuilder<T, R | U> {
40
+ if (this.matched) return this as any
41
+
42
+ if (this.test(predicate)) {
43
+ this.matched = true
44
+ this.matchedResult = typeof result === 'function' ? (result as () => U)() : result
45
+ } else {
46
+ this.arms.push({ predicate, result })
47
+ }
48
+
49
+ return this as any
50
+ }
51
+
52
+ otherwise<U>(result: MatchResult<U>): R | U {
53
+ if (this.matched) return this.matchedResult
54
+ return typeof result === 'function' ? (result as () => U)() : result
55
+ }
56
+
57
+ exhaustive(): R {
58
+ if (this.matched) return this.matchedResult
59
+ throw new Error(`No match found for value: ${JSON.stringify(this.value)}`)
60
+ }
61
+
62
+ private test(predicate: MatchPredicate<T>): boolean {
63
+ if (typeof predicate === 'function') {
64
+ return (predicate as (value: T) => boolean)(this.value)
65
+ }
66
+ if (Array.isArray(predicate)) {
67
+ return predicate.includes(this.value)
68
+ }
69
+ return Object.is(this.value, predicate)
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Start a pattern match on a value.
75
+ */
76
+ export function match<T>(value: T): MatchBuilder<T> {
77
+ return new MatchImpl(value)
78
+ }