mutts 1.0.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/README.md +150 -0
- package/dist/chunks/decorator-BXsign4Z.js +176 -0
- package/dist/chunks/decorator-BXsign4Z.js.map +1 -0
- package/dist/chunks/decorator-CPbZNnsX.esm.js +168 -0
- package/dist/chunks/decorator-CPbZNnsX.esm.js.map +1 -0
- package/dist/decorator.d.ts +50 -0
- package/dist/decorator.esm.js +2 -0
- package/dist/decorator.esm.js.map +1 -0
- package/dist/decorator.js +11 -0
- package/dist/decorator.js.map +1 -0
- package/dist/destroyable.d.ts +48 -0
- package/dist/destroyable.esm.js +91 -0
- package/dist/destroyable.esm.js.map +1 -0
- package/dist/destroyable.js +98 -0
- package/dist/destroyable.js.map +1 -0
- package/dist/eventful.d.ts +11 -0
- package/dist/eventful.esm.js +88 -0
- package/dist/eventful.esm.js.map +1 -0
- package/dist/eventful.js +90 -0
- package/dist/eventful.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.esm.js +7 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +52 -0
- package/dist/index.js.map +1 -0
- package/dist/indexable.d.ts +31 -0
- package/dist/indexable.esm.js +85 -0
- package/dist/indexable.esm.js.map +1 -0
- package/dist/indexable.js +89 -0
- package/dist/indexable.js.map +1 -0
- package/dist/mutts.umd.js +2 -0
- package/dist/mutts.umd.js.map +1 -0
- package/dist/mutts.umd.min.js +2 -0
- package/dist/mutts.umd.min.js.map +1 -0
- package/dist/promiseChain.d.ts +11 -0
- package/dist/promiseChain.esm.js +72 -0
- package/dist/promiseChain.esm.js.map +1 -0
- package/dist/promiseChain.js +74 -0
- package/dist/promiseChain.js.map +1 -0
- package/dist/reactive.d.ts +114 -0
- package/dist/reactive.esm.js +1455 -0
- package/dist/reactive.esm.js.map +1 -0
- package/dist/reactive.js +1472 -0
- package/dist/reactive.js.map +1 -0
- package/dist/std-decorators.d.ts +17 -0
- package/dist/std-decorators.esm.js +161 -0
- package/dist/std-decorators.esm.js.map +1 -0
- package/dist/std-decorators.js +169 -0
- package/dist/std-decorators.js.map +1 -0
- package/docs/decorator.md +300 -0
- package/docs/destroyable.md +294 -0
- package/docs/events.md +225 -0
- package/docs/indexable.md +561 -0
- package/docs/promiseChain.md +218 -0
- package/docs/reactive.md +2072 -0
- package/docs/std-decorators.md +558 -0
- package/package.json +132 -0
- package/src/decorator.test.ts +495 -0
- package/src/decorator.ts +205 -0
- package/src/destroyable.test.ts +155 -0
- package/src/destroyable.ts +158 -0
- package/src/eventful.test.ts +380 -0
- package/src/eventful.ts +69 -0
- package/src/index.ts +7 -0
- package/src/indexable.test.ts +388 -0
- package/src/indexable.ts +124 -0
- package/src/promiseChain.test.ts +201 -0
- package/src/promiseChain.ts +99 -0
- package/src/reactive/array.test.ts +923 -0
- package/src/reactive/array.ts +352 -0
- package/src/reactive/core.test.ts +1663 -0
- package/src/reactive/core.ts +866 -0
- package/src/reactive/index.ts +28 -0
- package/src/reactive/interface.test.ts +1477 -0
- package/src/reactive/interface.ts +231 -0
- package/src/reactive/map.test.ts +866 -0
- package/src/reactive/map.ts +162 -0
- package/src/reactive/set.test.ts +289 -0
- package/src/reactive/set.ts +142 -0
- package/src/std-decorators.test.ts +679 -0
- package/src/std-decorators.ts +182 -0
- package/src/utils.ts +52 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { chainPromise } from './promiseChain'
|
|
2
|
+
|
|
3
|
+
describe('chain', () => {
|
|
4
|
+
describe('basic functionality', () => {
|
|
5
|
+
it('should return non-promise values as-is', () => {
|
|
6
|
+
const value = { name: 'test', value: 42 }
|
|
7
|
+
const result = chainPromise(value)
|
|
8
|
+
expect(result).toEqual(value)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('should chain method calls on resolved promise values', async () => {
|
|
12
|
+
const obj = {
|
|
13
|
+
getName: () => 'John',
|
|
14
|
+
getAge: () => 30,
|
|
15
|
+
getInfo: () => ({ name: 'John', age: 30 }),
|
|
16
|
+
}
|
|
17
|
+
const promise = Promise.resolve(obj)
|
|
18
|
+
|
|
19
|
+
const result = await chainPromise(promise).getName()
|
|
20
|
+
expect(result).toBe('John')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('should support multiple method chaining', async () => {
|
|
24
|
+
const obj = {
|
|
25
|
+
getName: () => 'John',
|
|
26
|
+
getAge: () => 30,
|
|
27
|
+
getInfo: () => ({ name: 'John', age: 30 }),
|
|
28
|
+
}
|
|
29
|
+
const promise = Promise.resolve(obj)
|
|
30
|
+
|
|
31
|
+
const result = await chainPromise(promise).getInfo().name
|
|
32
|
+
expect(result).toBe('John')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('should handle async methods', async () => {
|
|
36
|
+
const obj = {
|
|
37
|
+
getName: async () => 'John',
|
|
38
|
+
getAge: async () => 30,
|
|
39
|
+
}
|
|
40
|
+
const promise = Promise.resolve(obj)
|
|
41
|
+
|
|
42
|
+
//const result = await chainPromise(promise).getName()
|
|
43
|
+
const chained = chainPromise(promise)
|
|
44
|
+
const getName = chained.getName
|
|
45
|
+
const result = await getName()
|
|
46
|
+
expect(result).toBe('John')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('should handle methods with parameters', async () => {
|
|
50
|
+
const obj = {
|
|
51
|
+
add: (a: number, b: number) => a + b,
|
|
52
|
+
multiply: (a: number, b: number) => a * b,
|
|
53
|
+
}
|
|
54
|
+
const promise = Promise.resolve(obj)
|
|
55
|
+
|
|
56
|
+
const result = await chainPromise(promise).add(5, 3)
|
|
57
|
+
expect(result).toBe(8)
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe('function calls', () => {
|
|
62
|
+
it('should allow calling the resolved value as a function', async () => {
|
|
63
|
+
const func = (name: string, age: number) => `${name} is ${age} years old`
|
|
64
|
+
const promise = Promise.resolve(func)
|
|
65
|
+
|
|
66
|
+
const result = await (chainPromise(promise) as any)('John', 30)
|
|
67
|
+
expect(result).toBe('John is 30 years old')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('should handle async functions', async () => {
|
|
71
|
+
const asyncFunc = async (name: string) => `Hello ${name}`
|
|
72
|
+
const promise = Promise.resolve(asyncFunc)
|
|
73
|
+
|
|
74
|
+
const result = await (chainPromise(promise) as any)('World')
|
|
75
|
+
expect(result).toBe('Hello World')
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
/*
|
|
79
|
+
describe('caching behavior', () => {
|
|
80
|
+
it('should return the same proxy for the same promise', () => {
|
|
81
|
+
const promise = Promise.resolve({ test: () => 'value' })
|
|
82
|
+
const chain1 = chainPromise(promise)
|
|
83
|
+
const chain2 = chainPromise(promise)
|
|
84
|
+
|
|
85
|
+
expect(chain1).toBe(chain2)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('should not cache different promises', async () => {
|
|
89
|
+
const promise1 = Promise.resolve({ test: () => 'value1' })
|
|
90
|
+
const promise2 = Promise.resolve({ test: () => 'value2' })
|
|
91
|
+
const chain1 = chainPromise(promise1)
|
|
92
|
+
const chain2 = chainPromise(promise2)
|
|
93
|
+
|
|
94
|
+
expect(chain1).not.toBe(chain2)
|
|
95
|
+
expect(await chain1).not.toBe(await chain2)
|
|
96
|
+
})
|
|
97
|
+
})*/
|
|
98
|
+
|
|
99
|
+
describe('complex scenarios', () => {
|
|
100
|
+
it('should handle nested object methods', async () => {
|
|
101
|
+
const obj = {
|
|
102
|
+
user: {
|
|
103
|
+
getName: () => 'John',
|
|
104
|
+
getProfile: () => ({
|
|
105
|
+
age: 30,
|
|
106
|
+
email: 'john@example.com',
|
|
107
|
+
}),
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
const promise = Promise.resolve(obj)
|
|
111
|
+
|
|
112
|
+
const result = await chainPromise(promise).user.getProfile().email
|
|
113
|
+
expect(result).toBe('john@example.com')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('should handle array methods', async () => {
|
|
117
|
+
const arr = [1, 2, 3, 4, 5]
|
|
118
|
+
const promise = Promise.resolve(arr)
|
|
119
|
+
|
|
120
|
+
//const result = await chainPromise(promise).filter(x => x > 2).map(x => x * 2)
|
|
121
|
+
|
|
122
|
+
const chained = chainPromise(promise)
|
|
123
|
+
const filtered = chained.filter((x) => {
|
|
124
|
+
return x > 2
|
|
125
|
+
})
|
|
126
|
+
const result = filtered.map((x) => {
|
|
127
|
+
return x * 2
|
|
128
|
+
})
|
|
129
|
+
const awaitResult = await result
|
|
130
|
+
expect(awaitResult).toEqual([6, 8, 10])
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('should handle methods that return promises', async () => {
|
|
134
|
+
const obj = {
|
|
135
|
+
getData: () => Promise.resolve({ id: 1, name: 'test' }),
|
|
136
|
+
processData: (data: any) => Promise.resolve({ ...data, processed: true }),
|
|
137
|
+
}
|
|
138
|
+
const promise = Promise.resolve(obj)
|
|
139
|
+
|
|
140
|
+
const result = await chainPromise(promise).getData().name
|
|
141
|
+
expect(result).toBe('test')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('should handle methods that return promises recursively', async () => {
|
|
145
|
+
const obj = {
|
|
146
|
+
getData: () => Promise.resolve({ id: 1, name: 'test' }),
|
|
147
|
+
processData: (data: any) => Promise.resolve({ ...data, processed: true }),
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
//const result = await chainPromise(obj).getData().name
|
|
151
|
+
const chain = chainPromise(obj)
|
|
152
|
+
const getData = chain.getData
|
|
153
|
+
const data = getData()
|
|
154
|
+
const result = await data.name
|
|
155
|
+
expect(result).toBe('test')
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
describe('error handling', () => {
|
|
160
|
+
it('should handle methods that throw errors', async () => {
|
|
161
|
+
const obj = {
|
|
162
|
+
errorMethod: () => {
|
|
163
|
+
throw new Error('Method error')
|
|
164
|
+
},
|
|
165
|
+
}
|
|
166
|
+
const promise = Promise.resolve(obj)
|
|
167
|
+
|
|
168
|
+
await expect(chainPromise(promise).errorMethod()).rejects.toThrow('Method error')
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
describe('edge cases', () => {
|
|
173
|
+
it('should handle null and undefined values', () => {
|
|
174
|
+
expect(chainPromise(null)).toBeNull()
|
|
175
|
+
expect(chainPromise(undefined)).toBeUndefined()
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('should handle primitive values', () => {
|
|
179
|
+
expect(chainPromise(42)).toBe(42)
|
|
180
|
+
expect(chainPromise('string')).toBe('string')
|
|
181
|
+
expect(chainPromise(true)).toBe(true)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('should handle objects without methods', async () => {
|
|
185
|
+
const obj = { name: 'test', value: 42 }
|
|
186
|
+
const promise = Promise.resolve(obj)
|
|
187
|
+
|
|
188
|
+
const result = await chainPromise(promise).name
|
|
189
|
+
expect(result).toBe('test')
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('should handle symbols as property names', async () => {
|
|
193
|
+
const sym = Symbol('test')
|
|
194
|
+
const obj = { [sym]: () => 'symbol value' }
|
|
195
|
+
const promise = Promise.resolve(obj)
|
|
196
|
+
|
|
197
|
+
const result = await chainPromise(promise)[sym]()
|
|
198
|
+
expect(result).toBe('symbol value')
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
})
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
type Resolved<T> = T extends Promise<infer U>
|
|
2
|
+
? Resolved<U>
|
|
3
|
+
: T extends (...args: infer Args) => infer R
|
|
4
|
+
? (...args: Args) => Resolved<R>
|
|
5
|
+
: T extends object
|
|
6
|
+
? {
|
|
7
|
+
[k in keyof T]: k extends 'then' | 'catch' | 'finally' ? T[k] : Resolved<T[k]>
|
|
8
|
+
}
|
|
9
|
+
: T
|
|
10
|
+
type PromiseAnd<T> = Resolved<T> & Promise<Resolved<T>>
|
|
11
|
+
export type PromiseChain<T> = T extends (...args: infer Args) => infer R
|
|
12
|
+
? PromiseAnd<(...args: Args) => PromiseChain<Resolved<R>>>
|
|
13
|
+
: T extends object
|
|
14
|
+
? PromiseAnd<{
|
|
15
|
+
[k in keyof T]: k extends 'then' | 'catch' | 'finally' ? T[k] : PromiseChain<Resolved<T[k]>>
|
|
16
|
+
}>
|
|
17
|
+
: Promise<Resolved<T>>
|
|
18
|
+
|
|
19
|
+
const forward =
|
|
20
|
+
(name: string, target: any) =>
|
|
21
|
+
(...args: any[]) => {
|
|
22
|
+
return target[name](...args)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const alreadyChained = new WeakMap<any, PromiseChain<any>>()
|
|
26
|
+
const originals = new WeakMap<Promise<any>, any>()
|
|
27
|
+
|
|
28
|
+
function cache(target: any, rv: PromiseChain<any>) {
|
|
29
|
+
originals.set(rv, target)
|
|
30
|
+
alreadyChained.set(target, rv)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type ChainedFunction<T> = ((...args: any[]) => PromiseChain<T>) & {
|
|
34
|
+
then: Promise<T>['then']
|
|
35
|
+
catch: Promise<T>['catch']
|
|
36
|
+
finally: Promise<T>['finally']
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const promiseProxyHandler: ProxyHandler<ChainedFunction<any>> = {
|
|
40
|
+
//@ts-expect-error
|
|
41
|
+
[Symbol.toStringTag]: 'MutTs PromiseChain function',
|
|
42
|
+
get(target, prop) {
|
|
43
|
+
if (prop === Symbol.toStringTag) return 'PromiseProxy'
|
|
44
|
+
if (typeof prop === 'string' && ['then', 'catch', 'finally'].includes(prop))
|
|
45
|
+
return target[prop as keyof typeof target]
|
|
46
|
+
return chainPromise(target.then((r) => r[prop as keyof typeof r]))
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
const promiseForward = (target: any) => ({
|
|
50
|
+
// biome-ignore lint/suspicious/noThenProperty: This one is the whole point
|
|
51
|
+
then: forward('then', target),
|
|
52
|
+
catch: forward('catch', target),
|
|
53
|
+
finally: forward('finally', target),
|
|
54
|
+
})
|
|
55
|
+
const objectProxyHandler: ProxyHandler<any> = {
|
|
56
|
+
//@ts-expect-error
|
|
57
|
+
[Symbol.toStringTag]: 'MutTs PromiseChain object',
|
|
58
|
+
get(target, prop, receiver) {
|
|
59
|
+
const getter = Object.getOwnPropertyDescriptor(target, prop)?.get
|
|
60
|
+
const rv = getter ? getter.call(receiver) : target[prop]
|
|
61
|
+
// Allows fct.call or fct.apply to bypass the chain system
|
|
62
|
+
if (typeof target === 'function') return rv
|
|
63
|
+
return chainPromise(rv)
|
|
64
|
+
},
|
|
65
|
+
apply(target, thisArg, args) {
|
|
66
|
+
return chainPromise(target.apply(thisArg, args))
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
function chainObject<T extends object | Function>(given: T): PromiseChain<T> {
|
|
70
|
+
const rv = new Proxy(given, objectProxyHandler) as PromiseChain<T>
|
|
71
|
+
cache(given, rv)
|
|
72
|
+
return rv
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function chainable(x: any): x is object | Function {
|
|
76
|
+
return x && ['function', 'object'].includes(typeof x)
|
|
77
|
+
}
|
|
78
|
+
export function chainPromise<T>(given: Promise<T> | T): PromiseChain<T> {
|
|
79
|
+
if (!chainable(given)) return given as PromiseChain<T>
|
|
80
|
+
if (alreadyChained.has(given)) return alreadyChained.get(given) as PromiseChain<T>
|
|
81
|
+
if (!(given instanceof Promise)) return chainObject(given)
|
|
82
|
+
// @ts-expect-error It's ok as we check if it's an object above
|
|
83
|
+
given = given.then((r) => (chainable(r) ? chainObject(r) : r))
|
|
84
|
+
const target = Object.assign(function (this: any, ...args: any[]) {
|
|
85
|
+
return chainPromise(
|
|
86
|
+
given.then((r) => {
|
|
87
|
+
return this?.then
|
|
88
|
+
? this.then((t: any) => (r as any).apply(t, args))
|
|
89
|
+
: (r as any).apply(this, args)
|
|
90
|
+
})
|
|
91
|
+
)
|
|
92
|
+
}, promiseForward(given)) as ChainedFunction<T>
|
|
93
|
+
const chained = new Proxy(
|
|
94
|
+
target,
|
|
95
|
+
promiseProxyHandler as ProxyHandler<ChainedFunction<T>>
|
|
96
|
+
) as PromiseChain<T>
|
|
97
|
+
cache(given, chained as PromiseChain<any>)
|
|
98
|
+
return chained
|
|
99
|
+
}
|