@nmtjs/common 0.15.0-beta.3 → 0.15.0-beta.5
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/package.json +2 -1
- package/src/abortSignal.ts +61 -0
- package/src/constants.ts +3 -0
- package/src/index.ts +5 -0
- package/src/streams.ts +52 -0
- package/src/types.ts +80 -0
- package/src/utils.ts +174 -0
package/package.json
CHANGED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Combines multiple AbortSignals into one that aborts when any of the source signals abort.
|
|
3
|
+
*
|
|
4
|
+
* This is a custom implementation to work around memory leaks in Node.js's AbortSignal.any().
|
|
5
|
+
* Bun's implementation is fine, so we use the native version there.
|
|
6
|
+
*
|
|
7
|
+
* @see https://github.com/nodejs/node/issues/54614
|
|
8
|
+
*/
|
|
9
|
+
export function anyAbortSignal(
|
|
10
|
+
...signals: (AbortSignal | undefined)[]
|
|
11
|
+
): AbortSignal {
|
|
12
|
+
const filtered = signals.filter(Boolean) as AbortSignal[]
|
|
13
|
+
|
|
14
|
+
if (filtered.length === 0) {
|
|
15
|
+
return new AbortController().signal
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (filtered.length === 1) {
|
|
19
|
+
return filtered[0]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Use native implementation on Bun (no memory leak there)
|
|
23
|
+
if ('Bun' in globalThis) {
|
|
24
|
+
return AbortSignal.any(filtered)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Custom implementation for Node.js to avoid memory leaks
|
|
28
|
+
const controller = new AbortController()
|
|
29
|
+
|
|
30
|
+
// Check if any signal is already aborted
|
|
31
|
+
for (const signal of filtered) {
|
|
32
|
+
if (signal.aborted) {
|
|
33
|
+
controller.abort()
|
|
34
|
+
return controller.signal
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Track cleanup functions
|
|
39
|
+
const cleanups: (() => void)[] = []
|
|
40
|
+
|
|
41
|
+
const onAbort = () => {
|
|
42
|
+
controller.abort()
|
|
43
|
+
// Clean up all listeners immediately after abort
|
|
44
|
+
cleanup()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const cleanup = () => {
|
|
48
|
+
for (const fn of cleanups) {
|
|
49
|
+
fn()
|
|
50
|
+
}
|
|
51
|
+
cleanups.length = 0
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Attach listeners to all signals
|
|
55
|
+
for (const signal of filtered) {
|
|
56
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
57
|
+
cleanups.push(() => signal.removeEventListener('abort', onAbort))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return controller.signal
|
|
61
|
+
}
|
package/src/constants.ts
ADDED
package/src/index.ts
ADDED
package/src/streams.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// TODO: add proper queueing/backpressure strategy support
|
|
2
|
+
|
|
3
|
+
import type { Async } from './types.ts'
|
|
4
|
+
|
|
5
|
+
export interface DuplexStreamOptions<O = unknown, I = O> {
|
|
6
|
+
start?: (controller: ReadableStreamDefaultController<O>) => void
|
|
7
|
+
pull?: (controller: ReadableStreamDefaultController<O>) => Async<void>
|
|
8
|
+
cancel?: (reason: unknown) => void
|
|
9
|
+
transform?: (chunk: I) => O
|
|
10
|
+
close?: () => void
|
|
11
|
+
readableStrategy?: QueuingStrategy<O>
|
|
12
|
+
writableStrategy?: QueuingStrategy<I>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class DuplexStream<O = unknown, I = O> {
|
|
16
|
+
readonly readable: ReadableStream<O>
|
|
17
|
+
readonly writable!: WritableStream<I>
|
|
18
|
+
|
|
19
|
+
constructor(options: DuplexStreamOptions<O, I> = {}) {
|
|
20
|
+
this.readable = new ReadableStream<O>(
|
|
21
|
+
{
|
|
22
|
+
cancel: options.cancel,
|
|
23
|
+
start: (controller) => {
|
|
24
|
+
// @ts-expect-error
|
|
25
|
+
this.writable = new WritableStream<I>(
|
|
26
|
+
{
|
|
27
|
+
write: (_chunk) => {
|
|
28
|
+
const chunk = options?.transform
|
|
29
|
+
? options?.transform(_chunk)
|
|
30
|
+
: _chunk
|
|
31
|
+
controller.enqueue(chunk as O)
|
|
32
|
+
},
|
|
33
|
+
abort: (reason) => controller.error(reason),
|
|
34
|
+
close: () => {
|
|
35
|
+
options?.close?.()
|
|
36
|
+
try {
|
|
37
|
+
controller.close()
|
|
38
|
+
} catch {
|
|
39
|
+
// Controller may already be closed (e.g., via cancel)
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
options.writableStrategy,
|
|
44
|
+
)
|
|
45
|
+
options.start?.(controller)
|
|
46
|
+
},
|
|
47
|
+
pull: options?.pull,
|
|
48
|
+
},
|
|
49
|
+
options.readableStrategy,
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const TSErrorSymbol: unique symbol = Symbol('TSError')
|
|
2
|
+
|
|
3
|
+
export type TSError<T extends string = string> = `Error: ${T}` & {
|
|
4
|
+
[TSErrorSymbol]: true
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface TypeProvider {
|
|
8
|
+
readonly input: unknown
|
|
9
|
+
readonly output: unknown
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type CallTypeProvider<T extends TypeProvider, V> = (T & {
|
|
13
|
+
input: V
|
|
14
|
+
})['output']
|
|
15
|
+
|
|
16
|
+
export type ClassConstructor<T = any, A extends any[] = any[]> =
|
|
17
|
+
| (abstract new (
|
|
18
|
+
...args: A
|
|
19
|
+
) => T)
|
|
20
|
+
| (new (
|
|
21
|
+
...args: A
|
|
22
|
+
) => T)
|
|
23
|
+
|
|
24
|
+
export type ClassInstance<T> = T extends ClassConstructor<infer U> ? U : never
|
|
25
|
+
export type ClassConstructorArgs<T, A = never> = T extends ClassConstructor<
|
|
26
|
+
any,
|
|
27
|
+
infer U
|
|
28
|
+
>
|
|
29
|
+
? U
|
|
30
|
+
: A
|
|
31
|
+
|
|
32
|
+
export type Callback<T extends any[] = any[], R = any> = (...args: T) => R
|
|
33
|
+
export type OmitFirstItem<T extends any[]> = T extends [any, ...infer U]
|
|
34
|
+
? U
|
|
35
|
+
: []
|
|
36
|
+
export type ErrorClass = new (...args: any[]) => Error
|
|
37
|
+
export type Extra = Record<string, any>
|
|
38
|
+
export type Async<T> = T | Promise<T>
|
|
39
|
+
|
|
40
|
+
export type ArrayMap<T extends readonly any[], K extends keyof T[number]> = {
|
|
41
|
+
[I in keyof T]: T[I][K]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type UnionToIntersection<U> = (
|
|
45
|
+
U extends any
|
|
46
|
+
? (k: U) => void
|
|
47
|
+
: never
|
|
48
|
+
) extends (k: infer I) => void
|
|
49
|
+
? I
|
|
50
|
+
: never
|
|
51
|
+
|
|
52
|
+
export type Merge<
|
|
53
|
+
T1 extends Record<string, any>,
|
|
54
|
+
T2 extends Record<string, any>,
|
|
55
|
+
> = {
|
|
56
|
+
[K in keyof T1 | keyof T2]: K extends keyof T2
|
|
57
|
+
? T2[K]
|
|
58
|
+
: K extends keyof T1
|
|
59
|
+
? T1[K]
|
|
60
|
+
: never
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type OneOf<
|
|
64
|
+
TypesArray extends any[],
|
|
65
|
+
Res = never,
|
|
66
|
+
AllProperties = MergeTypes<TypesArray>,
|
|
67
|
+
> = TypesArray extends [infer Head, ...infer Rem]
|
|
68
|
+
? OneOf<Rem, Res | OnlyFirst<Head, AllProperties>, AllProperties>
|
|
69
|
+
: Res
|
|
70
|
+
|
|
71
|
+
type MergeTypes<TypesArray extends any[], Res = {}> = TypesArray extends [
|
|
72
|
+
infer Head,
|
|
73
|
+
...infer Rem,
|
|
74
|
+
]
|
|
75
|
+
? MergeTypes<Rem, Res & Head>
|
|
76
|
+
: Res
|
|
77
|
+
|
|
78
|
+
type OnlyFirst<F, S> = F & { [Key in keyof Omit<S, keyof F>]?: never }
|
|
79
|
+
|
|
80
|
+
export type Pattern = RegExp | string | ((value: string) => boolean)
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type { Callback, Pattern } from './types.ts'
|
|
2
|
+
|
|
3
|
+
export const noopFn = () => {}
|
|
4
|
+
|
|
5
|
+
export function merge<T extends any[]>(...objects: T) {
|
|
6
|
+
return Object.assign({}, ...objects)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function unique<T>(array: Iterable<T>): Iterable<T> {
|
|
10
|
+
return new Set(array).values()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function defer<T extends Callback>(
|
|
14
|
+
cb: T,
|
|
15
|
+
ms = 1,
|
|
16
|
+
...args: Parameters<T>
|
|
17
|
+
): Promise<Awaited<ReturnType<T>>> {
|
|
18
|
+
return new Promise((resolve, reject) =>
|
|
19
|
+
setTimeout(async () => {
|
|
20
|
+
try {
|
|
21
|
+
resolve(await cb(...args))
|
|
22
|
+
} catch (error) {
|
|
23
|
+
reject(error)
|
|
24
|
+
}
|
|
25
|
+
}, ms),
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function range(count: number, start = 0) {
|
|
30
|
+
let current = start
|
|
31
|
+
return {
|
|
32
|
+
[Symbol.iterator]() {
|
|
33
|
+
return {
|
|
34
|
+
next() {
|
|
35
|
+
if (current < count) {
|
|
36
|
+
return { done: false, value: current++ }
|
|
37
|
+
} else {
|
|
38
|
+
return { done: true, value: current }
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function debounce(cb: Callback, delay: number) {
|
|
47
|
+
let timer: any
|
|
48
|
+
const clear = () => timer && clearTimeout(timer)
|
|
49
|
+
const fn = (...args: any[]) => {
|
|
50
|
+
clear()
|
|
51
|
+
timer = setTimeout(cb, delay, ...args)
|
|
52
|
+
}
|
|
53
|
+
return Object.assign(fn, { clear })
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// TODO: Promise.withResolvers?
|
|
57
|
+
export interface Future<T = any> {
|
|
58
|
+
promise: Promise<T>
|
|
59
|
+
resolve: (value: T) => void
|
|
60
|
+
reject: (error: any) => void
|
|
61
|
+
}
|
|
62
|
+
// TODO: Promise.withResolvers?
|
|
63
|
+
export function createFuture<T>(): Future<T> {
|
|
64
|
+
let resolve: Future<T>['resolve']
|
|
65
|
+
let reject: Future<T>['reject']
|
|
66
|
+
const promise = new Promise<T>((res, rej) => {
|
|
67
|
+
resolve = res
|
|
68
|
+
reject = rej
|
|
69
|
+
})
|
|
70
|
+
// @ts-expect-error
|
|
71
|
+
return { resolve, reject, promise }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function onAbort<T extends Callback>(
|
|
75
|
+
signal: AbortSignal,
|
|
76
|
+
cb: T,
|
|
77
|
+
reason?: any,
|
|
78
|
+
) {
|
|
79
|
+
const listener = () => cb(reason ?? signal.reason)
|
|
80
|
+
signal.addEventListener('abort', listener, { once: true })
|
|
81
|
+
return () => signal.removeEventListener('abort', listener)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function withTimeout(
|
|
85
|
+
value: Promise<any>,
|
|
86
|
+
timeout: number,
|
|
87
|
+
timeoutError: Error,
|
|
88
|
+
) {
|
|
89
|
+
return Promise.race([
|
|
90
|
+
value,
|
|
91
|
+
new Promise((_, reject) => setTimeout(reject, timeout, timeoutError)),
|
|
92
|
+
])
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function tryCaptureStackTrace(depth = 0) {
|
|
96
|
+
const traceLines = new Error().stack?.split('\n')
|
|
97
|
+
if (traceLines) {
|
|
98
|
+
for (const traceLine of traceLines) {
|
|
99
|
+
const trimmed = traceLine.trim()
|
|
100
|
+
|
|
101
|
+
if (trimmed.startsWith('at eval (') && trimmed.endsWith(')')) {
|
|
102
|
+
const trace = trimmed.slice(9, -1)
|
|
103
|
+
return trace
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return undefined
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function isGeneratorFunction(value: any): value is GeneratorFunction {
|
|
111
|
+
return (
|
|
112
|
+
typeof value === 'function' &&
|
|
113
|
+
value.constructor.name === 'GeneratorFunction'
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function isAsyncGeneratorFunction(
|
|
118
|
+
value: any,
|
|
119
|
+
): value is AsyncGeneratorFunction {
|
|
120
|
+
return (
|
|
121
|
+
typeof value === 'function' &&
|
|
122
|
+
value.constructor.name === 'AsyncGeneratorFunction'
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
export function isAsyncIterable(value: any): value is AsyncIterable<unknown> {
|
|
126
|
+
return value && typeof value === 'object' && Symbol.asyncIterator in value
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function throwError(message: string, ErrorClass = Error): never {
|
|
130
|
+
throw new ErrorClass(message)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function once(target: EventTarget, event: string) {
|
|
134
|
+
return new Promise<void>((resolve) => {
|
|
135
|
+
target.addEventListener(event, () => resolve(), { once: true })
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function onceAborted(signal: AbortSignal) {
|
|
140
|
+
return once(signal, 'abort')
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function isAbortError(error) {
|
|
144
|
+
return (
|
|
145
|
+
(error instanceof Error &&
|
|
146
|
+
error.name === 'AbortError' &&
|
|
147
|
+
'code' in error &&
|
|
148
|
+
(error.code === 20 || error.code === 'ABORT_ERR')) ||
|
|
149
|
+
(error instanceof Event && error.type === 'abort')
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Very simple pattern matching function.
|
|
155
|
+
*/
|
|
156
|
+
export function match(value: string, pattern: Pattern) {
|
|
157
|
+
if (typeof pattern === 'function') {
|
|
158
|
+
return pattern(value)
|
|
159
|
+
} else if (typeof pattern === 'string') {
|
|
160
|
+
if (pattern === '*' || pattern === '**') {
|
|
161
|
+
return true
|
|
162
|
+
} else if (pattern.at(0) === '*' && pattern.at(-1) === '*') {
|
|
163
|
+
return value.includes(pattern.slice(1, -1))
|
|
164
|
+
} else if (pattern.at(-1) === '*') {
|
|
165
|
+
return value.startsWith(pattern.slice(0, -1))
|
|
166
|
+
} else if (pattern.at(0) === '*') {
|
|
167
|
+
return value.endsWith(pattern.slice(1))
|
|
168
|
+
} else {
|
|
169
|
+
return value === pattern
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
return pattern.test(value)
|
|
173
|
+
}
|
|
174
|
+
}
|