mutts 1.0.0 → 1.0.2

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.
Files changed (85) hide show
  1. package/README.md +24 -2
  2. package/dist/chunks/_tslib-C-cuVLvZ.js +73 -0
  3. package/dist/chunks/_tslib-C-cuVLvZ.js.map +1 -0
  4. package/dist/chunks/_tslib-CMEnd0VE.esm.js +68 -0
  5. package/dist/chunks/_tslib-CMEnd0VE.esm.js.map +1 -0
  6. package/dist/chunks/{decorator-BXsign4Z.js → decorator-D4DU97Zg.js} +70 -4
  7. package/dist/chunks/decorator-D4DU97Zg.js.map +1 -0
  8. package/dist/chunks/{decorator-CPbZNnsX.esm.js → decorator-GnHw1Az7.esm.js} +67 -5
  9. package/dist/chunks/decorator-GnHw1Az7.esm.js.map +1 -0
  10. package/dist/chunks/index-DBScoeCX.esm.js +1960 -0
  11. package/dist/chunks/index-DBScoeCX.esm.js.map +1 -0
  12. package/dist/chunks/index-DOTmXL89.js +1983 -0
  13. package/dist/chunks/index-DOTmXL89.js.map +1 -0
  14. package/dist/decorator.d.ts +58 -1
  15. package/dist/decorator.esm.js +1 -1
  16. package/dist/decorator.js +1 -1
  17. package/dist/destroyable.d.ts +42 -0
  18. package/dist/destroyable.esm.js +19 -1
  19. package/dist/destroyable.esm.js.map +1 -1
  20. package/dist/destroyable.js +19 -1
  21. package/dist/destroyable.js.map +1 -1
  22. package/dist/eventful.d.ts +10 -1
  23. package/dist/eventful.esm.js +5 -27
  24. package/dist/eventful.esm.js.map +1 -1
  25. package/dist/eventful.js +15 -37
  26. package/dist/eventful.js.map +1 -1
  27. package/dist/index.d.ts +52 -3
  28. package/dist/index.esm.js +3 -2
  29. package/dist/index.esm.js.map +1 -1
  30. package/dist/index.js +18 -3
  31. package/dist/index.js.map +1 -1
  32. package/dist/indexable.d.ts +26 -0
  33. package/dist/indexable.esm.js +6 -0
  34. package/dist/indexable.esm.js.map +1 -1
  35. package/dist/indexable.js +6 -0
  36. package/dist/indexable.js.map +1 -1
  37. package/dist/mutts.umd.js +1 -1
  38. package/dist/mutts.umd.js.map +1 -1
  39. package/dist/mutts.umd.min.js +1 -1
  40. package/dist/mutts.umd.min.js.map +1 -1
  41. package/dist/promiseChain.d.ts +10 -0
  42. package/dist/promiseChain.esm.js +6 -0
  43. package/dist/promiseChain.esm.js.map +1 -1
  44. package/dist/promiseChain.js +6 -0
  45. package/dist/promiseChain.js.map +1 -1
  46. package/dist/reactive.d.ts +258 -20
  47. package/dist/reactive.esm.js +4 -1454
  48. package/dist/reactive.esm.js.map +1 -1
  49. package/dist/reactive.js +29 -1466
  50. package/dist/reactive.js.map +1 -1
  51. package/dist/std-decorators.d.ts +35 -0
  52. package/dist/std-decorators.esm.js +36 -1
  53. package/dist/std-decorators.esm.js.map +1 -1
  54. package/dist/std-decorators.js +36 -1
  55. package/dist/std-decorators.js.map +1 -1
  56. package/docs/mixin.md +229 -0
  57. package/docs/reactive.md +7931 -458
  58. package/package.json +1 -2
  59. package/dist/chunks/decorator-BXsign4Z.js.map +0 -1
  60. package/dist/chunks/decorator-CPbZNnsX.esm.js.map +0 -1
  61. package/src/decorator.test.ts +0 -495
  62. package/src/decorator.ts +0 -205
  63. package/src/destroyable.test.ts +0 -155
  64. package/src/destroyable.ts +0 -158
  65. package/src/eventful.test.ts +0 -380
  66. package/src/eventful.ts +0 -69
  67. package/src/index.ts +0 -7
  68. package/src/indexable.test.ts +0 -388
  69. package/src/indexable.ts +0 -124
  70. package/src/promiseChain.test.ts +0 -201
  71. package/src/promiseChain.ts +0 -99
  72. package/src/reactive/array.test.ts +0 -923
  73. package/src/reactive/array.ts +0 -352
  74. package/src/reactive/core.test.ts +0 -1663
  75. package/src/reactive/core.ts +0 -866
  76. package/src/reactive/index.ts +0 -28
  77. package/src/reactive/interface.test.ts +0 -1477
  78. package/src/reactive/interface.ts +0 -231
  79. package/src/reactive/map.test.ts +0 -866
  80. package/src/reactive/map.ts +0 -162
  81. package/src/reactive/set.test.ts +0 -289
  82. package/src/reactive/set.ts +0 -142
  83. package/src/std-decorators.test.ts +0 -679
  84. package/src/std-decorators.ts +0 -182
  85. package/src/utils.ts +0 -52
@@ -1,679 +0,0 @@
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
- })