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,679 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cache,
|
|
3
|
+
cached,
|
|
4
|
+
debounce,
|
|
5
|
+
deprecated,
|
|
6
|
+
describe as describeDecorator,
|
|
7
|
+
isCached,
|
|
8
|
+
throttle,
|
|
9
|
+
} from './std-decorators'
|
|
10
|
+
|
|
11
|
+
describe('cached decorator', () => {
|
|
12
|
+
it('should cache the result of a getter', () => {
|
|
13
|
+
let callCount = 0
|
|
14
|
+
class Test {
|
|
15
|
+
@cached
|
|
16
|
+
get value(): number {
|
|
17
|
+
callCount++
|
|
18
|
+
return 42
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const t = new Test()
|
|
22
|
+
expect(callCount).toBe(0)
|
|
23
|
+
expect(t.value).toBe(42)
|
|
24
|
+
expect(callCount).toBe(1)
|
|
25
|
+
expect(t.value).toBe(42)
|
|
26
|
+
expect(callCount).toBe(1)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('should cache per instance', () => {
|
|
30
|
+
let callCount = 0
|
|
31
|
+
class Test {
|
|
32
|
+
@cached
|
|
33
|
+
get value(): number {
|
|
34
|
+
callCount++
|
|
35
|
+
return callCount
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const t1 = new Test()
|
|
39
|
+
const t2 = new Test()
|
|
40
|
+
const v1 = t1.value
|
|
41
|
+
const v2 = t2.value
|
|
42
|
+
expect(v1).not.toBe(v2)
|
|
43
|
+
expect(callCount).toBe(2)
|
|
44
|
+
expect(t1.value).toBe(v1)
|
|
45
|
+
expect(t2.value).toBe(v2)
|
|
46
|
+
expect(callCount).toBe(2)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('should throw on circular dependency', () => {
|
|
50
|
+
class Test {
|
|
51
|
+
@cached
|
|
52
|
+
get a(): number {
|
|
53
|
+
return this.b
|
|
54
|
+
}
|
|
55
|
+
@cached
|
|
56
|
+
get b(): number {
|
|
57
|
+
return this.a
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const t = new Test()
|
|
61
|
+
expect(() => t.a).toThrow(/Circular dependency detected/)
|
|
62
|
+
expect(() => t.b).toThrow(/Circular dependency detected/)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should throw if used on non-getter', () => {
|
|
66
|
+
expect(() => {
|
|
67
|
+
class Test {
|
|
68
|
+
// @ts-expect-error
|
|
69
|
+
@cached
|
|
70
|
+
value = 1
|
|
71
|
+
}
|
|
72
|
+
return new Test()
|
|
73
|
+
}).toThrow('Decorator cannot be applied to a field')
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe('isCached', () => {
|
|
78
|
+
it('should return false before caching', () => {
|
|
79
|
+
class Test {
|
|
80
|
+
@cached
|
|
81
|
+
get value(): number {
|
|
82
|
+
return 1
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const t = new Test()
|
|
86
|
+
expect(isCached(t, 'value')).toBe(false)
|
|
87
|
+
void t.value
|
|
88
|
+
expect(isCached(t, 'value')).toBe(true)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('should return true after manual cache', () => {
|
|
92
|
+
let callCount = 0
|
|
93
|
+
class Test {
|
|
94
|
+
get foo(): number {
|
|
95
|
+
callCount++
|
|
96
|
+
return 1
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const obj = new Test()
|
|
100
|
+
expect(isCached(obj, 'foo')).toBe(false)
|
|
101
|
+
cache(obj, 'foo', 123)
|
|
102
|
+
expect(isCached(obj, 'foo')).toBe(true)
|
|
103
|
+
expect(obj.foo).toBe(123)
|
|
104
|
+
expect(callCount).toBe(0)
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
describe('describe decorator', () => {
|
|
109
|
+
it('should make properties readonly', () => {
|
|
110
|
+
const readonly = describeDecorator({ writable: false })
|
|
111
|
+
|
|
112
|
+
@readonly('id', 'createdAt')
|
|
113
|
+
class User {
|
|
114
|
+
id: string = 'user-123'
|
|
115
|
+
name: string = 'John'
|
|
116
|
+
createdAt: Date = new Date()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const user = new User()
|
|
120
|
+
|
|
121
|
+
// Readonly properties should not be writable
|
|
122
|
+
expect(() => {
|
|
123
|
+
user.id = 'new-id'
|
|
124
|
+
}).toThrow()
|
|
125
|
+
|
|
126
|
+
expect(() => {
|
|
127
|
+
user.createdAt = new Date()
|
|
128
|
+
}).toThrow()
|
|
129
|
+
|
|
130
|
+
// Non-readonly properties should still be writable
|
|
131
|
+
user.name = 'Jane'
|
|
132
|
+
expect(user.name).toBe('Jane')
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('should make properties non-enumerable', () => {
|
|
136
|
+
const hidden = describeDecorator({ enumerable: false })
|
|
137
|
+
|
|
138
|
+
@hidden('_private', '_cache')
|
|
139
|
+
class DataStore {
|
|
140
|
+
public data: any[] = []
|
|
141
|
+
_private: string = 'secret'
|
|
142
|
+
_cache: Map<string, any> = new Map()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const store = new DataStore()
|
|
146
|
+
|
|
147
|
+
// Only public properties should be enumerable
|
|
148
|
+
expect(Object.keys(store)).toEqual(['data'])
|
|
149
|
+
|
|
150
|
+
// All properties should exist
|
|
151
|
+
expect(Object.getOwnPropertyNames(store)).toContain('_private')
|
|
152
|
+
expect(Object.getOwnPropertyNames(store)).toContain('_cache')
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('should combine multiple descriptor properties', () => {
|
|
156
|
+
const readonlyHidden = describeDecorator({
|
|
157
|
+
writable: false,
|
|
158
|
+
enumerable: false,
|
|
159
|
+
configurable: false,
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
@readonlyHidden('secret')
|
|
163
|
+
class SecureData {
|
|
164
|
+
public info: string = 'public'
|
|
165
|
+
secret: string = 'top secret'
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const data = new SecureData()
|
|
169
|
+
|
|
170
|
+
// Secret should be read-only
|
|
171
|
+
expect(() => {
|
|
172
|
+
data.secret = 'leaked'
|
|
173
|
+
}).toThrow()
|
|
174
|
+
|
|
175
|
+
// Secret should be hidden from enumeration
|
|
176
|
+
expect(Object.keys(data)).toEqual(['info'])
|
|
177
|
+
|
|
178
|
+
// Secret should not be configurable
|
|
179
|
+
expect(() => {
|
|
180
|
+
Object.defineProperty(data, 'secret', { value: 'new' })
|
|
181
|
+
}).toThrow()
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('should work with multiple properties', () => {
|
|
185
|
+
const readonly = describeDecorator({ writable: false })
|
|
186
|
+
|
|
187
|
+
@readonly('id', 'version', 'createdAt')
|
|
188
|
+
class Document {
|
|
189
|
+
id: string = 'doc-1'
|
|
190
|
+
title: string = 'My Document'
|
|
191
|
+
version: number = 1
|
|
192
|
+
createdAt: Date = new Date()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const doc = new Document()
|
|
196
|
+
|
|
197
|
+
// All specified properties should be readonly
|
|
198
|
+
expect(() => {
|
|
199
|
+
doc.id = 'new-id'
|
|
200
|
+
}).toThrow()
|
|
201
|
+
expect(() => {
|
|
202
|
+
doc.version = 2
|
|
203
|
+
}).toThrow()
|
|
204
|
+
expect(() => {
|
|
205
|
+
doc.createdAt = new Date()
|
|
206
|
+
}).toThrow()
|
|
207
|
+
|
|
208
|
+
// Non-specified properties should remain writable
|
|
209
|
+
doc.title = 'Updated Title'
|
|
210
|
+
expect(doc.title).toBe('Updated Title')
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('should preserve existing property descriptors', () => {
|
|
214
|
+
const readonly = describeDecorator({ writable: false })
|
|
215
|
+
|
|
216
|
+
@readonly('value')
|
|
217
|
+
class Test {
|
|
218
|
+
value: string = 'test'
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const obj = new Test()
|
|
222
|
+
const descriptor = Object.getOwnPropertyDescriptor(obj, 'value')
|
|
223
|
+
|
|
224
|
+
// Should preserve enumerable and configurable, only change writable
|
|
225
|
+
expect(descriptor?.writable).toBe(false)
|
|
226
|
+
expect(descriptor?.enumerable).toBe(true) // Default for class fields
|
|
227
|
+
expect(descriptor?.configurable).toBe(true) // Default for class fields
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it('should work with inheritance', () => {
|
|
231
|
+
const readonly = describeDecorator({ writable: false })
|
|
232
|
+
|
|
233
|
+
class Base {
|
|
234
|
+
baseValue: string = 'base'
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
@readonly('baseValue', 'derivedValue')
|
|
238
|
+
class Derived extends Base {
|
|
239
|
+
derivedValue: string = 'derived'
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const obj = new Derived()
|
|
243
|
+
|
|
244
|
+
// Both base and derived properties should be readonly
|
|
245
|
+
expect(() => {
|
|
246
|
+
obj.baseValue = 'new base'
|
|
247
|
+
}).toThrow()
|
|
248
|
+
expect(() => {
|
|
249
|
+
obj.derivedValue = 'new derived'
|
|
250
|
+
}).toThrow()
|
|
251
|
+
})
|
|
252
|
+
/* Once a proper
|
|
253
|
+
it('should create reusable descriptor configurations', () => {
|
|
254
|
+
// Create reusable configurations
|
|
255
|
+
const readonly = describeDecorator({ writable: false })
|
|
256
|
+
const hidden = describeDecorator({ enumerable: false })
|
|
257
|
+
const locked = describeDecorator({ configurable: false })
|
|
258
|
+
|
|
259
|
+
@readonly('id')
|
|
260
|
+
@hidden('_private')
|
|
261
|
+
@locked('critical')
|
|
262
|
+
class MultiConfig {
|
|
263
|
+
id: string = 'id-1'
|
|
264
|
+
_private: string = 'secret'
|
|
265
|
+
critical: string = 'locked'
|
|
266
|
+
normal: string = 'normal'
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const obj = new MultiConfig()
|
|
270
|
+
|
|
271
|
+
// Test readonly
|
|
272
|
+
expect(() => {
|
|
273
|
+
obj.id = 'new-id'
|
|
274
|
+
}).toThrow()
|
|
275
|
+
|
|
276
|
+
// Test hidden
|
|
277
|
+
expect(Object.keys(obj)).toEqual(['id', 'critical', 'normal'])
|
|
278
|
+
|
|
279
|
+
// Test locked
|
|
280
|
+
expect(() => {
|
|
281
|
+
Object.defineProperty(obj, 'critical', { value: 'new' })
|
|
282
|
+
}).toThrow()
|
|
283
|
+
|
|
284
|
+
// Normal property should work
|
|
285
|
+
obj.normal = 'updated'
|
|
286
|
+
expect(obj.normal).toBe('updated')
|
|
287
|
+
})*/
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
describe('debounce decorator', () => {
|
|
291
|
+
it('should debounce method calls', async () => {
|
|
292
|
+
let callCount = 0
|
|
293
|
+
|
|
294
|
+
class SearchInput {
|
|
295
|
+
@debounce(100)
|
|
296
|
+
search(query: string) {
|
|
297
|
+
callCount++
|
|
298
|
+
return `Searching for: ${query}`
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const input = new SearchInput()
|
|
303
|
+
|
|
304
|
+
// Call multiple times rapidly
|
|
305
|
+
input.search('a')
|
|
306
|
+
input.search('ab')
|
|
307
|
+
input.search('abc')
|
|
308
|
+
|
|
309
|
+
// Should not have been called yet
|
|
310
|
+
expect(callCount).toBe(0)
|
|
311
|
+
|
|
312
|
+
// Wait for debounce delay
|
|
313
|
+
await new Promise((resolve) => setTimeout(resolve, 150))
|
|
314
|
+
|
|
315
|
+
// Should have been called only once with the last value
|
|
316
|
+
expect(callCount).toBe(1)
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
it('should debounce with different delays', async () => {
|
|
320
|
+
let fastCalls = 0
|
|
321
|
+
let slowCalls = 0
|
|
322
|
+
|
|
323
|
+
class TestClass {
|
|
324
|
+
@debounce(50)
|
|
325
|
+
fast() {
|
|
326
|
+
fastCalls++
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
@debounce(150)
|
|
330
|
+
slow() {
|
|
331
|
+
slowCalls++
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const obj = new TestClass()
|
|
336
|
+
|
|
337
|
+
// Call both methods
|
|
338
|
+
obj.fast()
|
|
339
|
+
obj.slow()
|
|
340
|
+
|
|
341
|
+
// Wait for fast debounce
|
|
342
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
343
|
+
expect(fastCalls).toBe(1)
|
|
344
|
+
expect(slowCalls).toBe(0)
|
|
345
|
+
|
|
346
|
+
// Wait for slow debounce
|
|
347
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
348
|
+
expect(slowCalls).toBe(1)
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
it('should handle multiple rapid calls correctly', async () => {
|
|
352
|
+
const calls: string[] = []
|
|
353
|
+
|
|
354
|
+
class TestClass {
|
|
355
|
+
@debounce(100)
|
|
356
|
+
log(message: string) {
|
|
357
|
+
calls.push(message)
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const obj = new TestClass()
|
|
362
|
+
|
|
363
|
+
// Rapid calls
|
|
364
|
+
obj.log('first')
|
|
365
|
+
obj.log('second')
|
|
366
|
+
obj.log('third')
|
|
367
|
+
|
|
368
|
+
// Wait for debounce
|
|
369
|
+
await new Promise((resolve) => setTimeout(resolve, 150))
|
|
370
|
+
|
|
371
|
+
// Should only have the last call
|
|
372
|
+
expect(calls).toEqual(['third'])
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
it('should preserve method context and arguments', async () => {
|
|
376
|
+
let lastArgs: any[] = []
|
|
377
|
+
let lastContext: any = null
|
|
378
|
+
|
|
379
|
+
class TestClass {
|
|
380
|
+
value = 'test'
|
|
381
|
+
|
|
382
|
+
@debounce(50)
|
|
383
|
+
method(...args: any[]) {
|
|
384
|
+
lastArgs = args
|
|
385
|
+
lastContext = this
|
|
386
|
+
return this.value
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const obj = new TestClass()
|
|
391
|
+
obj.method('arg1', 'arg2', 123)
|
|
392
|
+
|
|
393
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
394
|
+
|
|
395
|
+
expect(lastArgs).toEqual(['arg1', 'arg2', 123])
|
|
396
|
+
expect(lastContext).toBe(obj)
|
|
397
|
+
expect(lastContext.value).toBe('test')
|
|
398
|
+
})
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
describe('throttle decorator', () => {
|
|
402
|
+
it('should throttle method calls', async () => {
|
|
403
|
+
let callCount = 0
|
|
404
|
+
|
|
405
|
+
class ScrollHandler {
|
|
406
|
+
@throttle(100)
|
|
407
|
+
onScroll() {
|
|
408
|
+
callCount++
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const handler = new ScrollHandler()
|
|
413
|
+
|
|
414
|
+
// First call should execute immediately
|
|
415
|
+
handler.onScroll()
|
|
416
|
+
expect(callCount).toBe(1)
|
|
417
|
+
|
|
418
|
+
// Immediate second call should be throttled
|
|
419
|
+
handler.onScroll()
|
|
420
|
+
expect(callCount).toBe(1)
|
|
421
|
+
|
|
422
|
+
// Call after throttle period should execute
|
|
423
|
+
await new Promise((resolve) => setTimeout(resolve, 150))
|
|
424
|
+
handler.onScroll()
|
|
425
|
+
expect(callCount).toBe(2)
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
it('should execute first call immediately', async () => {
|
|
429
|
+
let callCount = 0
|
|
430
|
+
|
|
431
|
+
class TestClass {
|
|
432
|
+
@throttle(200)
|
|
433
|
+
method() {
|
|
434
|
+
callCount++
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const obj = new TestClass()
|
|
439
|
+
|
|
440
|
+
// First call should execute immediately
|
|
441
|
+
obj.method()
|
|
442
|
+
expect(callCount).toBe(1)
|
|
443
|
+
|
|
444
|
+
// Second call should be throttled (and scheduled)
|
|
445
|
+
obj.method()
|
|
446
|
+
expect(callCount).toBe(1)
|
|
447
|
+
|
|
448
|
+
// Wait long enough for the scheduled trailing call to run so no timers remain
|
|
449
|
+
await new Promise((resolve) => setTimeout(resolve, 250))
|
|
450
|
+
expect(callCount).toBe(2)
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
it('should schedule delayed execution for throttled calls', async () => {
|
|
454
|
+
const timestamps: number[] = []
|
|
455
|
+
|
|
456
|
+
class TestClass {
|
|
457
|
+
@throttle(100)
|
|
458
|
+
method() {
|
|
459
|
+
timestamps.push(Date.now())
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const obj = new TestClass()
|
|
464
|
+
|
|
465
|
+
// First call - immediate
|
|
466
|
+
// const startTime = Date.now()
|
|
467
|
+
obj.method()
|
|
468
|
+
|
|
469
|
+
// Second call - should be throttled and scheduled
|
|
470
|
+
obj.method()
|
|
471
|
+
|
|
472
|
+
// Wait for scheduled execution
|
|
473
|
+
await new Promise((resolve) => setTimeout(resolve, 150))
|
|
474
|
+
|
|
475
|
+
expect(timestamps).toHaveLength(2)
|
|
476
|
+
expect(timestamps[1] - timestamps[0]).toBeGreaterThanOrEqual(95) // Allow some tolerance
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
it('should handle multiple rapid calls with correct timing', async () => {
|
|
480
|
+
const calls: number[] = []
|
|
481
|
+
|
|
482
|
+
class TestClass {
|
|
483
|
+
@throttle(100)
|
|
484
|
+
method() {
|
|
485
|
+
calls.push(Date.now())
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const obj = new TestClass()
|
|
490
|
+
|
|
491
|
+
// Multiple rapid calls
|
|
492
|
+
// const startTime = Date.now()
|
|
493
|
+
obj.method() // Immediate
|
|
494
|
+
obj.method() // Throttled
|
|
495
|
+
obj.method() // Throttled
|
|
496
|
+
|
|
497
|
+
// Wait for scheduled execution
|
|
498
|
+
await new Promise((resolve) => setTimeout(resolve, 150))
|
|
499
|
+
|
|
500
|
+
expect(calls).toHaveLength(2)
|
|
501
|
+
expect(calls[1] - calls[0]).toBeGreaterThanOrEqual(95)
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
it('should preserve method context and arguments', async () => {
|
|
505
|
+
let lastArgs: any[] = []
|
|
506
|
+
let lastContext: any = null
|
|
507
|
+
|
|
508
|
+
class TestClass {
|
|
509
|
+
value = 'throttled'
|
|
510
|
+
|
|
511
|
+
@throttle(50)
|
|
512
|
+
method(...args: any[]) {
|
|
513
|
+
lastArgs = args
|
|
514
|
+
lastContext = this
|
|
515
|
+
return this.value
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const obj = new TestClass()
|
|
520
|
+
obj.method('arg1', 'arg2')
|
|
521
|
+
|
|
522
|
+
expect(lastArgs).toEqual(['arg1', 'arg2'])
|
|
523
|
+
expect(lastContext).toBe(obj)
|
|
524
|
+
expect(lastContext.value).toBe('throttled')
|
|
525
|
+
|
|
526
|
+
// Test throttled call
|
|
527
|
+
obj.method('throttled1', 'throttled2')
|
|
528
|
+
|
|
529
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
530
|
+
|
|
531
|
+
expect(lastArgs).toEqual(['throttled1', 'throttled2'])
|
|
532
|
+
})
|
|
533
|
+
it('should handle different throttle delays', async () => {
|
|
534
|
+
let fastCalls = 0
|
|
535
|
+
let slowCalls = 0
|
|
536
|
+
|
|
537
|
+
class TestClass {
|
|
538
|
+
@throttle(50)
|
|
539
|
+
fast() {
|
|
540
|
+
fastCalls++
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
@throttle(150)
|
|
544
|
+
slow() {
|
|
545
|
+
slowCalls++
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const obj = new TestClass()
|
|
550
|
+
|
|
551
|
+
// Call both methods
|
|
552
|
+
obj.fast()
|
|
553
|
+
obj.slow()
|
|
554
|
+
|
|
555
|
+
expect(fastCalls).toBe(1)
|
|
556
|
+
expect(slowCalls).toBe(1)
|
|
557
|
+
|
|
558
|
+
// Call again immediately (throttled)
|
|
559
|
+
obj.fast()
|
|
560
|
+
obj.slow()
|
|
561
|
+
|
|
562
|
+
expect(fastCalls).toBe(1)
|
|
563
|
+
expect(slowCalls).toBe(1)
|
|
564
|
+
|
|
565
|
+
// Wait for fast throttle window; scheduled fast should have fired
|
|
566
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
567
|
+
expect(fastCalls).toBe(2)
|
|
568
|
+
obj.fast()
|
|
569
|
+
expect(fastCalls).toBe(3)
|
|
570
|
+
expect(slowCalls).toBe(1)
|
|
571
|
+
|
|
572
|
+
// Wait to exceed slow window; scheduled slow should have fired
|
|
573
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
574
|
+
expect(slowCalls).toBe(2)
|
|
575
|
+
obj.slow()
|
|
576
|
+
// Immediate call is within new window start; should schedule, not increment now
|
|
577
|
+
expect(slowCalls).toBe(2)
|
|
578
|
+
})
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
describe('deprecated decorator with string parameter', () => {
|
|
582
|
+
it('should use custom warning message for methods', () => {
|
|
583
|
+
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation()
|
|
584
|
+
|
|
585
|
+
class TestClass {
|
|
586
|
+
@deprecated('Use newMethod() instead')
|
|
587
|
+
oldMethod() {
|
|
588
|
+
return 'old'
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const obj = new TestClass()
|
|
593
|
+
obj.oldMethod()
|
|
594
|
+
|
|
595
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
596
|
+
'TestClass.oldMethod is deprecated: Use newMethod() instead'
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
consoleSpy.mockRestore()
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
it('should use custom warning message for getters', () => {
|
|
603
|
+
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation()
|
|
604
|
+
|
|
605
|
+
class TestClass {
|
|
606
|
+
@deprecated('Use newValue instead')
|
|
607
|
+
get oldValue() {
|
|
608
|
+
return 'old'
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const obj = new TestClass()
|
|
613
|
+
obj.oldValue
|
|
614
|
+
|
|
615
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
616
|
+
'TestClass.oldValue is deprecated: Use newValue instead'
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
consoleSpy.mockRestore()
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
it('should use custom warning message for setters', () => {
|
|
623
|
+
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation()
|
|
624
|
+
|
|
625
|
+
class TestClass {
|
|
626
|
+
@deprecated('Use setNewValue() instead')
|
|
627
|
+
set oldValue(_value: string) {
|
|
628
|
+
// deprecated setter
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const obj = new TestClass()
|
|
633
|
+
obj.oldValue = 'test'
|
|
634
|
+
|
|
635
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
636
|
+
'TestClass.oldValue is deprecated: Use setNewValue() instead'
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
consoleSpy.mockRestore()
|
|
640
|
+
})
|
|
641
|
+
|
|
642
|
+
it('should use custom warning message for classes', () => {
|
|
643
|
+
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation()
|
|
644
|
+
|
|
645
|
+
@deprecated('Use NewClass instead')
|
|
646
|
+
class OldClass {
|
|
647
|
+
constructor() {}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
new OldClass()
|
|
651
|
+
|
|
652
|
+
expect(consoleSpy).toHaveBeenCalledWith('.constructor is deprecated: Use NewClass instead')
|
|
653
|
+
|
|
654
|
+
consoleSpy.mockRestore()
|
|
655
|
+
})
|
|
656
|
+
|
|
657
|
+
it('should work with different custom messages', () => {
|
|
658
|
+
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation()
|
|
659
|
+
|
|
660
|
+
class TestClass {
|
|
661
|
+
@deprecated('This will be removed in v2.0')
|
|
662
|
+
method1() {}
|
|
663
|
+
|
|
664
|
+
@deprecated('Use the new API')
|
|
665
|
+
method2() {}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const obj = new TestClass()
|
|
669
|
+
obj.method1()
|
|
670
|
+
obj.method2()
|
|
671
|
+
|
|
672
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
673
|
+
'TestClass.method1 is deprecated: This will be removed in v2.0'
|
|
674
|
+
)
|
|
675
|
+
expect(consoleSpy).toHaveBeenCalledWith('TestClass.method2 is deprecated: Use the new API')
|
|
676
|
+
|
|
677
|
+
consoleSpy.mockRestore()
|
|
678
|
+
})
|
|
679
|
+
})
|