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
package/src/eventful.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export class Eventful<Events extends Record<string, (...args: any[]) => void>> {
|
|
2
|
+
readonly #events = new Map<keyof Events, ((...args: any[]) => void)[]>()
|
|
3
|
+
readonly #hooks = [] as ((...args: any[]) => void)[]
|
|
4
|
+
|
|
5
|
+
public hook(
|
|
6
|
+
cb: <EventType extends keyof Events>(
|
|
7
|
+
event: EventType,
|
|
8
|
+
...args: Parameters<Events[EventType]>
|
|
9
|
+
) => void
|
|
10
|
+
): () => void {
|
|
11
|
+
if (!this.#hooks.includes(cb)) this.#hooks.push(cb)
|
|
12
|
+
return () => {
|
|
13
|
+
this.#hooks.splice(this.#hooks.indexOf(cb), 1)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public on(events: Partial<Events>): void
|
|
18
|
+
public on<EventType extends keyof Events>(event: EventType, cb: Events[EventType]): () => void
|
|
19
|
+
public on<EventType extends keyof Events>(
|
|
20
|
+
eventOrEvents: EventType | Partial<Events>,
|
|
21
|
+
cb?: Events[EventType]
|
|
22
|
+
): () => void {
|
|
23
|
+
if (typeof eventOrEvents === 'object') {
|
|
24
|
+
for (const e of Object.keys(eventOrEvents) as (keyof Events)[]) {
|
|
25
|
+
this.on(e, eventOrEvents[e]!)
|
|
26
|
+
}
|
|
27
|
+
} else if (cb !== undefined) {
|
|
28
|
+
let callbacks = this.#events.get(eventOrEvents)
|
|
29
|
+
if (!callbacks) {
|
|
30
|
+
callbacks = []
|
|
31
|
+
this.#events.set(eventOrEvents, callbacks)
|
|
32
|
+
}
|
|
33
|
+
callbacks.push(cb)
|
|
34
|
+
}
|
|
35
|
+
// @ts-expect-error Generic case leads to generic case
|
|
36
|
+
return () => this.off(eventOrEvents, cb)
|
|
37
|
+
}
|
|
38
|
+
public off(events: Partial<Events>): void
|
|
39
|
+
public off<EventType extends keyof Events>(event: EventType, cb?: Events[EventType]): void
|
|
40
|
+
public off<EventType extends keyof Events>(
|
|
41
|
+
eventOrEvents: EventType | Partial<Events>,
|
|
42
|
+
cb?: Events[EventType]
|
|
43
|
+
): void {
|
|
44
|
+
if (typeof eventOrEvents === 'object') {
|
|
45
|
+
for (const e of Object.keys(eventOrEvents) as (keyof Events)[]) {
|
|
46
|
+
this.off(e, eventOrEvents[e])
|
|
47
|
+
}
|
|
48
|
+
} else if (cb !== null && cb !== undefined) {
|
|
49
|
+
const callbacks = this.#events.get(eventOrEvents)
|
|
50
|
+
if (callbacks) {
|
|
51
|
+
this.#events.set(
|
|
52
|
+
eventOrEvents,
|
|
53
|
+
callbacks.filter((c) => c !== cb)
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
// Remove all listeners for this event
|
|
58
|
+
this.#events.delete(eventOrEvents)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
public emit<EventType extends keyof Events>(
|
|
62
|
+
event: EventType,
|
|
63
|
+
...args: Parameters<Events[EventType]>
|
|
64
|
+
) {
|
|
65
|
+
const callbacks = this.#events.get(event)
|
|
66
|
+
if (callbacks) for (const cb of callbacks) cb.apply(this, args)
|
|
67
|
+
for (const cb of this.#hooks) cb.call(this, event, ...args)
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { getAt, Indexable, setAt } from './indexable'
|
|
2
|
+
|
|
3
|
+
// TODO: get/set became this: based
|
|
4
|
+
describe('Indexable', () => {
|
|
5
|
+
describe('Indexable(base, accessor)', () => {
|
|
6
|
+
it('should create indexable class with custom accessor', () => {
|
|
7
|
+
class Base {
|
|
8
|
+
constructor(public items: string[]) {}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const IndexableBase = Indexable(Base, {
|
|
12
|
+
get: function (this: Base, index) {
|
|
13
|
+
return this.items[index]
|
|
14
|
+
},
|
|
15
|
+
set: function (this: Base, index, value) {
|
|
16
|
+
this.items[index] = value
|
|
17
|
+
},
|
|
18
|
+
})
|
|
19
|
+
const instance = new IndexableBase(['a', 'b', 'c'])
|
|
20
|
+
|
|
21
|
+
expect(instance[0]).toBe('a')
|
|
22
|
+
expect(instance[1]).toBe('b')
|
|
23
|
+
expect(instance[2]).toBe('c')
|
|
24
|
+
expect(instance.items).toEqual(['a', 'b', 'c'])
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('should handle different item types', () => {
|
|
28
|
+
class Base {
|
|
29
|
+
constructor(public numbers: number[]) {}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const IndexableBase = Indexable(Base, {
|
|
33
|
+
get: function (this: Base, index) {
|
|
34
|
+
return this.numbers[index] * 2
|
|
35
|
+
},
|
|
36
|
+
set: function (this: Base, index, value) {
|
|
37
|
+
this.numbers[index] = value / 2
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
const instance = new IndexableBase([1, 2, 3])
|
|
41
|
+
|
|
42
|
+
expect(instance[0]).toBe(2)
|
|
43
|
+
expect(instance[1]).toBe(4)
|
|
44
|
+
expect(instance[2]).toBe(6)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('should preserve base class methods', () => {
|
|
48
|
+
class Base {
|
|
49
|
+
constructor(public items: string[]) {}
|
|
50
|
+
getLength() {
|
|
51
|
+
return this.items.length
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const IndexableBase = Indexable(Base, {
|
|
56
|
+
get: function (this: Base, index) {
|
|
57
|
+
return this.items[index]
|
|
58
|
+
},
|
|
59
|
+
set: function (this: Base, index, value) {
|
|
60
|
+
this.items[index] = value
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
const instance = new IndexableBase(['a', 'b'])
|
|
64
|
+
|
|
65
|
+
expect(instance.getLength()).toBe(2)
|
|
66
|
+
expect(instance[0]).toBe('a')
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
describe('Indexable(base with getAt and setAt methods)', () => {
|
|
71
|
+
it('should use the base class getAt and setAt methods', () => {
|
|
72
|
+
class Base {
|
|
73
|
+
constructor(public items: string[]) {}
|
|
74
|
+
[getAt](index: number): string {
|
|
75
|
+
return this.items[index]
|
|
76
|
+
}
|
|
77
|
+
[setAt](index: number, value: string): void {
|
|
78
|
+
this.items[index] = value
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const IndexableBase = Indexable(Base)
|
|
83
|
+
const instance = new IndexableBase(['x', 'y', 'z'])
|
|
84
|
+
|
|
85
|
+
expect(instance[0]).toBe('x')
|
|
86
|
+
instance[0] = 'a'
|
|
87
|
+
expect(instance[0]).toBe('a')
|
|
88
|
+
expect(instance.items[0]).toBe('a')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('should work with custom getAt and setAt implementation', () => {
|
|
92
|
+
class Base {
|
|
93
|
+
constructor(public numbers: number[]) {}
|
|
94
|
+
[getAt](index: number): number {
|
|
95
|
+
return this.numbers[index] * 3
|
|
96
|
+
}
|
|
97
|
+
[setAt](index: number, value: number): void {
|
|
98
|
+
this.numbers[index] = value / 3
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const IndexableBase = Indexable(Base)
|
|
103
|
+
const instance = new IndexableBase([1, 2, 3])
|
|
104
|
+
|
|
105
|
+
expect(instance[0]).toBe(3)
|
|
106
|
+
instance[0] = 9
|
|
107
|
+
expect(instance[0]).toBe(9)
|
|
108
|
+
expect(instance.numbers[0]).toBe(3)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('should throw when setAt method is missing', () => {
|
|
112
|
+
class Base {
|
|
113
|
+
constructor(public items: string[]) {}
|
|
114
|
+
[getAt](index: number): string {
|
|
115
|
+
return this.items[index]
|
|
116
|
+
}
|
|
117
|
+
// Missing setAt method
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const IndexableBase = Indexable(Base)
|
|
121
|
+
const instance = new IndexableBase(['x', 'y'])
|
|
122
|
+
|
|
123
|
+
expect(instance[0]).toBe('x')
|
|
124
|
+
expect(() => {
|
|
125
|
+
instance[0] = 'z'
|
|
126
|
+
}).toThrow('Indexable class has read-only numeric index access')
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
describe('Indexable()', () => {
|
|
131
|
+
it('should create abstract class with abstract getAt method', () => {
|
|
132
|
+
const AbstractIndexable = Indexable<string>()
|
|
133
|
+
|
|
134
|
+
//@ts-expect-error Should be abstract
|
|
135
|
+
void new AbstractIndexable()
|
|
136
|
+
|
|
137
|
+
// Should have abstract getAt method
|
|
138
|
+
class Concrete extends AbstractIndexable {
|
|
139
|
+
constructor(private items: string[]) {
|
|
140
|
+
super()
|
|
141
|
+
}
|
|
142
|
+
[getAt](index: number): string {
|
|
143
|
+
return this.items[index]
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const instance = new Concrete(['p', 'q', 'r'])
|
|
148
|
+
expect(instance[0]).toBe('p')
|
|
149
|
+
expect(instance[1]).toBe('q')
|
|
150
|
+
expect(instance[2]).toBe('r')
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('should enforce getAt method implementation', () => {
|
|
154
|
+
const AbstractIndexable = Indexable<number>()
|
|
155
|
+
//@ts-expect-error Should be abstract
|
|
156
|
+
class Invalid extends AbstractIndexable {}
|
|
157
|
+
|
|
158
|
+
// JavaScript doesn't enforce abstract methods at runtime
|
|
159
|
+
// So instantiation won't throw, but calling the missing method will
|
|
160
|
+
const instance = new Invalid()
|
|
161
|
+
expect(() => instance[0]).toThrow()
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
describe('edge cases', () => {
|
|
166
|
+
it('should handle out of bounds access', () => {
|
|
167
|
+
class Base {
|
|
168
|
+
constructor(public items: string[]) {}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const IndexableBase = Indexable(Base, {
|
|
172
|
+
get: function (this: Base, index) {
|
|
173
|
+
return this.items[index]
|
|
174
|
+
},
|
|
175
|
+
set: function (this: Base, index, value) {
|
|
176
|
+
this.items[index] = value
|
|
177
|
+
},
|
|
178
|
+
})
|
|
179
|
+
const instance = new IndexableBase(['a', 'b'])
|
|
180
|
+
|
|
181
|
+
expect(instance[5]).toBeUndefined()
|
|
182
|
+
expect(instance[-1]).toBeUndefined()
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('should handle empty arrays', () => {
|
|
186
|
+
class Base {
|
|
187
|
+
constructor(public items: string[]) {}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const IndexableBase = Indexable(Base, {
|
|
191
|
+
get: function (this: Base, index) {
|
|
192
|
+
return this.items[index]
|
|
193
|
+
},
|
|
194
|
+
set: function (this: Base, index, value) {
|
|
195
|
+
this.items[index] = value
|
|
196
|
+
},
|
|
197
|
+
})
|
|
198
|
+
const instance = new IndexableBase([])
|
|
199
|
+
|
|
200
|
+
expect(instance[0]).toBeUndefined()
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('should not interfere with non-numeric properties', () => {
|
|
204
|
+
class Base {
|
|
205
|
+
constructor(public items: string[]) {}
|
|
206
|
+
length = 42
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const IndexableBase = Indexable(Base, {
|
|
210
|
+
get: function (this: Base, index) {
|
|
211
|
+
return this.items[index]
|
|
212
|
+
},
|
|
213
|
+
set: function (this: Base, index, value) {
|
|
214
|
+
this.items[index] = value
|
|
215
|
+
},
|
|
216
|
+
})
|
|
217
|
+
const instance = new IndexableBase(['a', 'b'])
|
|
218
|
+
|
|
219
|
+
expect(instance.length).toBe(42)
|
|
220
|
+
expect(instance.items).toEqual(['a', 'b'])
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('should not interfere with setting non-numeric properties', () => {
|
|
224
|
+
class Base {
|
|
225
|
+
constructor(public items: string[]) {}
|
|
226
|
+
length = 42
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const IndexableBase = Indexable(Base, {
|
|
230
|
+
get: function (this: Base, index) {
|
|
231
|
+
return this.items[index]
|
|
232
|
+
},
|
|
233
|
+
set: function (this: Base, index, value) {
|
|
234
|
+
this.items[index] = value
|
|
235
|
+
},
|
|
236
|
+
})
|
|
237
|
+
const instance = new IndexableBase(['a', 'b'])
|
|
238
|
+
|
|
239
|
+
instance.length = 100
|
|
240
|
+
expect(instance.length).toBe(100)
|
|
241
|
+
})
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
describe('integration tests', () => {
|
|
245
|
+
it('should work with complex objects', () => {
|
|
246
|
+
class Person {
|
|
247
|
+
constructor(
|
|
248
|
+
public name: string,
|
|
249
|
+
public age: number
|
|
250
|
+
) {}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
class PersonList {
|
|
254
|
+
constructor(public people: Person[]) {}
|
|
255
|
+
[getAt](index: number): Person {
|
|
256
|
+
return this.people[index]
|
|
257
|
+
}
|
|
258
|
+
[setAt](index: number, person: Person): void {
|
|
259
|
+
this.people[index] = person
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const IndexablePersonList = Indexable(PersonList)
|
|
264
|
+
const people = [new Person('Alice', 30), new Person('Bob', 25), new Person('Charlie', 35)]
|
|
265
|
+
const instance = new IndexablePersonList(people)
|
|
266
|
+
|
|
267
|
+
expect((instance[0] as Person).name).toBe('Alice')
|
|
268
|
+
instance[0] = new Person('Alice Updated', 31)
|
|
269
|
+
expect((instance[0] as Person).name).toBe('Alice Updated')
|
|
270
|
+
expect((instance[0] as Person).age).toBe(31)
|
|
271
|
+
expect(instance.people[0].name).toBe('Alice Updated')
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
it('should support multiple inheritance levels', () => {
|
|
275
|
+
class Base {
|
|
276
|
+
constructor(public items: string[]) {}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
class Extended extends Indexable(Base, {
|
|
280
|
+
get: function (this: Base, index) {
|
|
281
|
+
return this.items[index]
|
|
282
|
+
},
|
|
283
|
+
set: function (this: Base, index, value) {
|
|
284
|
+
this.items[index] = value
|
|
285
|
+
},
|
|
286
|
+
}) {
|
|
287
|
+
constructor(
|
|
288
|
+
items: string[],
|
|
289
|
+
public extra: string
|
|
290
|
+
) {
|
|
291
|
+
super(items)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const instance = new Extended(['a', 'b'], 'extra')
|
|
296
|
+
expect(instance[0]).toBe('a')
|
|
297
|
+
expect(instance.extra).toBe('extra')
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it('should support setting with custom logic', () => {
|
|
301
|
+
class Base {
|
|
302
|
+
constructor(public items: string[]) {}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const IndexableBase = Indexable(Base, {
|
|
306
|
+
get: function (this: Base, index) {
|
|
307
|
+
return this.items[index]
|
|
308
|
+
},
|
|
309
|
+
set: function (this: Base, index, value) {
|
|
310
|
+
// Custom logic: convert to uppercase
|
|
311
|
+
this.items[index] = value.toUpperCase()
|
|
312
|
+
},
|
|
313
|
+
})
|
|
314
|
+
const instance = new IndexableBase(['a', 'b', 'c'])
|
|
315
|
+
|
|
316
|
+
instance[1] = 'x'
|
|
317
|
+
expect(instance[1]).toBe('X')
|
|
318
|
+
expect(instance.items[1]).toBe('X')
|
|
319
|
+
})
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
describe('Indexable(accessor)', () => {
|
|
323
|
+
it('should create indexable object with custom accessor', () => {
|
|
324
|
+
const IndexableObj = Indexable({
|
|
325
|
+
get: function (this: any, index: number) {
|
|
326
|
+
return this._arr?.[index]
|
|
327
|
+
},
|
|
328
|
+
set: function (this: any, index: number, value: any) {
|
|
329
|
+
if (!this._arr) this._arr = []
|
|
330
|
+
this._arr[index] = value
|
|
331
|
+
},
|
|
332
|
+
})
|
|
333
|
+
const instance = new IndexableObj() as any
|
|
334
|
+
instance._arr = ['a', 'b', 'c']
|
|
335
|
+
expect(instance[0]).toBe('a')
|
|
336
|
+
expect(instance[1]).toBe('b')
|
|
337
|
+
expect(instance[2]).toBe('c')
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
it('should create indexable object with custom getter and setter', () => {
|
|
341
|
+
const IndexableObj = Indexable({
|
|
342
|
+
get: function (this: any, index: number) {
|
|
343
|
+
return this._arr?.[index]
|
|
344
|
+
},
|
|
345
|
+
set: function (this: any, index: number, value: string) {
|
|
346
|
+
if (!this._arr) this._arr = []
|
|
347
|
+
this._arr[index] = value
|
|
348
|
+
},
|
|
349
|
+
})
|
|
350
|
+
const instance = new IndexableObj() as any
|
|
351
|
+
instance[0] = 'x'
|
|
352
|
+
instance[1] = 'y'
|
|
353
|
+
expect(instance[0]).toBe('x')
|
|
354
|
+
expect(instance[1]).toBe('y')
|
|
355
|
+
expect(instance._arr[0]).toBe('x')
|
|
356
|
+
expect(instance._arr[1]).toBe('y')
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
it('should support transformation in setter', () => {
|
|
360
|
+
const IndexableObj = Indexable({
|
|
361
|
+
get: function (this: any, index: number) {
|
|
362
|
+
return this._arr?.[index] * 2
|
|
363
|
+
},
|
|
364
|
+
set: function (this: any, index: number, value: number) {
|
|
365
|
+
if (!this._arr) this._arr = []
|
|
366
|
+
this._arr[index] = value / 2
|
|
367
|
+
},
|
|
368
|
+
})
|
|
369
|
+
const instance = new IndexableObj() as any
|
|
370
|
+
instance._arr = [1, 2, 3]
|
|
371
|
+
instance[0] = 10
|
|
372
|
+
expect(instance[0]).toBe(10)
|
|
373
|
+
expect(instance._arr[0]).toBe(5)
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
it('should throw if setter is missing and assignment is attempted', () => {
|
|
377
|
+
const IndexableObj = Indexable({
|
|
378
|
+
get: function (this: any, index: number) {
|
|
379
|
+
return this._arr?.[index]
|
|
380
|
+
},
|
|
381
|
+
})
|
|
382
|
+
const instance = new IndexableObj() as any
|
|
383
|
+
expect(() => {
|
|
384
|
+
instance[0] = 'fail'
|
|
385
|
+
}).toThrow('Indexable class has read-only numeric index access')
|
|
386
|
+
})
|
|
387
|
+
})
|
|
388
|
+
})
|
package/src/indexable.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
export const getAt = Symbol('getAt')
|
|
2
|
+
export const setAt = Symbol('setAt')
|
|
3
|
+
|
|
4
|
+
interface IndexingAt<Items = any> {
|
|
5
|
+
[getAt](index: number): Items
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface Accessor<T, Items> {
|
|
9
|
+
get(this: T, index: number): Items
|
|
10
|
+
set?(this: T, index: number, value: Items): void
|
|
11
|
+
getLength?(this: T): number
|
|
12
|
+
setLength?(this: T, value: number): void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
abstract class AbstractGetAt<Items = any> {
|
|
16
|
+
abstract [getAt](index: number): Items
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function Indexable<Items, Base extends abstract new (...args: any[]) => any>(
|
|
20
|
+
base: Base,
|
|
21
|
+
accessor: Accessor<InstanceType<Base>, Items>
|
|
22
|
+
): new (
|
|
23
|
+
...args: ConstructorParameters<Base>
|
|
24
|
+
) => InstanceType<Base> & { [x: number]: Items }
|
|
25
|
+
|
|
26
|
+
export function Indexable<Items>(accessor: Accessor<any, Items>): new () => { [x: number]: Items }
|
|
27
|
+
|
|
28
|
+
export function Indexable<Base extends new (...args: any[]) => IndexingAt>(
|
|
29
|
+
base: Base
|
|
30
|
+
): new (
|
|
31
|
+
...args: ConstructorParameters<Base>
|
|
32
|
+
) => InstanceType<Base> & { [x: number]: AtReturnType<InstanceType<Base>> }
|
|
33
|
+
|
|
34
|
+
export function Indexable<Items>(): abstract new (
|
|
35
|
+
...args: any[]
|
|
36
|
+
) => AbstractGetAt & { [x: number]: Items }
|
|
37
|
+
|
|
38
|
+
export function Indexable<Items, Base extends abstract new (...args: any[]) => any>(
|
|
39
|
+
base?: Base | Accessor<Base, Items>,
|
|
40
|
+
accessor?: Accessor<Base, Items>
|
|
41
|
+
) {
|
|
42
|
+
if (base && typeof base !== 'function') {
|
|
43
|
+
accessor = base as Accessor<Base, Items>
|
|
44
|
+
base = undefined
|
|
45
|
+
}
|
|
46
|
+
if (!base) {
|
|
47
|
+
//@ts-expect-error
|
|
48
|
+
base = class {} as Base
|
|
49
|
+
}
|
|
50
|
+
if (!accessor) {
|
|
51
|
+
accessor = {
|
|
52
|
+
get(this: any, index: number) {
|
|
53
|
+
if (typeof this[getAt] !== 'function') {
|
|
54
|
+
throw new Error('Indexable class must have an [getAt] method')
|
|
55
|
+
}
|
|
56
|
+
return this[getAt](index)
|
|
57
|
+
},
|
|
58
|
+
set(this: any, index: number, value: Items) {
|
|
59
|
+
if (typeof this[setAt] !== 'function') {
|
|
60
|
+
throw new Error('Indexable class has read-only numeric index access')
|
|
61
|
+
}
|
|
62
|
+
this[setAt](index, value)
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
abstract class Indexable extends (base as Base) {
|
|
68
|
+
[x: number]: Items
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
Object.setPrototypeOf(
|
|
72
|
+
Indexable.prototype,
|
|
73
|
+
new Proxy((base as Base).prototype, {
|
|
74
|
+
//@ts-expect-error
|
|
75
|
+
[Symbol.toStringTag]: 'MutTs Indexable',
|
|
76
|
+
get(target, prop, receiver) {
|
|
77
|
+
if (prop in target) {
|
|
78
|
+
const getter = Object.getOwnPropertyDescriptor(target, prop)?.get
|
|
79
|
+
return getter ? getter.call(receiver) : target[prop]
|
|
80
|
+
}
|
|
81
|
+
if (typeof prop === 'string') {
|
|
82
|
+
if (prop === 'length' && accessor.getLength) return accessor.getLength.call(receiver)
|
|
83
|
+
const numProp = Number(prop)
|
|
84
|
+
if (!Number.isNaN(numProp)) {
|
|
85
|
+
return accessor.get!.call(receiver, numProp) as Items
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return undefined
|
|
89
|
+
},
|
|
90
|
+
set(target, prop, value, receiver) {
|
|
91
|
+
if (prop in target) {
|
|
92
|
+
const setter = Object.getOwnPropertyDescriptor(target, prop)?.set
|
|
93
|
+
if (setter) setter.call(receiver, value)
|
|
94
|
+
else target[prop] = value
|
|
95
|
+
return true
|
|
96
|
+
}
|
|
97
|
+
if (typeof prop === 'string') {
|
|
98
|
+
if (prop === 'length' && accessor.setLength) {
|
|
99
|
+
accessor.setLength.call(receiver, value)
|
|
100
|
+
return true
|
|
101
|
+
}
|
|
102
|
+
const numProp = Number(prop)
|
|
103
|
+
if (!Number.isNaN(numProp)) {
|
|
104
|
+
if (!accessor.set) {
|
|
105
|
+
throw new Error('Indexable class has read-only numeric index access')
|
|
106
|
+
}
|
|
107
|
+
accessor.set!.call(receiver, numProp, value)
|
|
108
|
+
return true
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
Object.defineProperty(receiver, prop, {
|
|
112
|
+
value,
|
|
113
|
+
writable: true,
|
|
114
|
+
enumerable: true,
|
|
115
|
+
configurable: true,
|
|
116
|
+
})
|
|
117
|
+
return true
|
|
118
|
+
},
|
|
119
|
+
})
|
|
120
|
+
)
|
|
121
|
+
return Indexable
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
type AtReturnType<T> = T extends { [getAt](index: number): infer R } ? R : never
|