• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

WolferyScripting / resclient-ts / #45

29 Aug 2025 04:19PM UTC coverage: 48.342% (-0.04%) from 48.379%
#45

push

DonovanDMC
1.1.10

230 of 292 branches covered (78.77%)

Branch coverage included in aggregate %.

1665 of 3628 relevant lines covered (45.89%)

9.86 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

5.6
/lib/models/ResCollectionModel.ts
1
import type ResClient from "./ResClient.js";
1✔
2
import ResModel, { type ResModelResourceEvents, type ResModelOptions } from "./ResModel.js";
1✔
3
import { changeDiff } from "../util/util.js";
1✔
4
import Properties from "../util/Properties.js";
1✔
5
import { copy } from "../includes/utils/obj.js";
1✔
6

1✔
7
export interface ResCollectionModelEvents<V = unknown> {
1✔
8
    add: [data: CollectionModelAddRemove<V>];
1✔
9
    remove: [data: CollectionModelAddRemove<V>];
1✔
10
}
1✔
11
export interface CollectionModelAddRemove<V = unknown> { item: V; key: string; }
1✔
12
export interface ResCollectionModelOptions<V = unknown> extends Omit<ResModelOptions, "definition"> {
1✔
13
    idCallback?(item: V): string;
1✔
14
}
1✔
15
export default class ResCollectionModel<V = unknown, ResourceEvents extends { [K in keyof ResourceEvents]: Array<unknown> } = ResModelResourceEvents<Record<string, V>>, ModelEvents extends { [K in keyof ModelEvents]: Array<unknown> } = ResCollectionModelEvents<V>> extends ResModel<Record<string, V>, ResourceEvents, ModelEvents> implements Iterable<V> {
1!
16
    private _idCallback?: (item: V) => string;
×
17
    private _list: Array<V> = [];
×
18
    private _map!: Record<string, V> | null;
×
19
    private _validateItem!: (item: V) => boolean;
×
20
    private onChange = this._onChange.bind(this);
×
21
    constructor(api: ResClient, rid: string, validateItem: (item: V) => boolean, options?: ResCollectionModelOptions<V>) {
×
22
        // @ts-expect-error since ResModelOptions only has one option which we're excluding this ends up with no overlap
×
23
        super(api, rid, options);
×
24
        options = copy(options ?? {}, {
×
25
            idCallback: { type: "?function" }
×
26
        });
×
27
        Properties.of(this)
×
28
            .writable("_idCallback", options.idCallback?.bind(this))
×
29
            .writable("_list", [])
×
30
            .readOnly("_map", options.idCallback ? {} : null)
×
31
            .readOnly("_validateItem", validateItem)
×
32
            .readOnly("onChange");
×
33
    }
×
34

×
35
    private _hasID(): void {
×
36
        if (!this._idCallback) {
×
37
            throw new Error("No id callback defined");
×
38
        }
×
39
    }
×
40

×
41
    private _onChange(data: Record<string, V | undefined>): void {
×
42
        const { added, removed } = changeDiff(this, data);
×
43

×
44
        for (const add of added) {
×
45
            this._list.push(add.item);
×
46
            this.api.eventBus.emit(this, "add", add);
×
47
        }
×
48

×
49
        for (const remove of removed) {
×
50
            const index = this._list.indexOf(remove.item);
×
51
            if (index !== -1) this._list.splice(index, 1);
×
52
            this.api.eventBus.emit(this, "remove", remove);
×
53
        }
×
54
    }
×
55

×
56
    protected override async _listen(on: boolean, changes = true): Promise<void> {
×
57
        await super._listen(on);
×
58
        if (changes) {
×
59
            const m = on ? "resourceOn" : "resourceOff";
×
60
            this[m]("change", this.onChange);
×
61
        }
×
62
    }
×
63

×
64
    protected override _shouldPromoteKey(key: string, value: V): boolean {
×
65
        return !this._validateItem(value) && super._shouldPromoteKey(key, value);
×
66
    }
×
67

×
68
    /** If this collection is empty. */
×
69
    get empty(): boolean {
×
70
        return this.length === 0;
×
71
    }
×
72

×
73
    get length(): number {
×
74
        return this._list.length;
×
75
    }
×
76

×
77
    get list(): Array<V> {
×
78
        return this._list;
×
79
    }
×
80

×
81
    [Symbol.iterator](): Iterator<V, undefined> {
×
82
        return this._list[Symbol.iterator]();
×
83
    }
×
84

×
85
    _add(item: V, index: number): void {
×
86
        this._list.splice(index, 0, item);
×
87

×
88
        if (this._idCallback) {
×
89
            const id = String(this._idCallback(item));
×
90
            if (["", "undefined", "null"].includes(id) || id.replaceAll(/\W/g, "") === "") {
×
91
                console.debug(item);
×
92
                throw new Error("No id for item");
×
93
            }
×
94
            if (this._map![id]) {
×
95
                throw new Error(`Duplicate id - ${id}`);
×
96
            }
×
97
            this._map![id] = item;
×
98
        }
×
99
    }
×
100

×
101
    _remove(index: number): V | undefined {
×
102
        const item = this._list[index];
×
103
        if (item !== undefined) {
×
104
            this._list.splice(index, 1);
×
105

×
106
            if (this._idCallback) {
×
107
                delete this._map![this._idCallback(item)];
×
108
            }
×
109
        }
×
110

×
111
        return item;
×
112
    }
×
113

×
114
    at(index: number): V | undefined {
×
115
        return this._list[index];
×
116
    }
×
117

×
118
    /** See: {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/every | Array#every } */
×
119
    every<T extends V, ThisArg = ResCollectionModel<V>>(predicate: (value: V, index: number, array: Array<V>) => value is T, thisArg?: ThisArg): this is Array<T>;
×
120
    every<ThisArg = ResCollectionModel<V>>(predicate: (value: V, index: number, array: Array<V>) => unknown, thisArg?: ThisArg): boolean;
×
121
    every(predicate: (value: V, index: number, array: Array<V>) => unknown, thisArg?: unknown): boolean {
×
122
        return this.toArray().every(predicate, thisArg);
×
123

×
124
    }
×
125

×
126
    /** See: {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter | Array#filter } */
×
127
    filter<S extends V, ThisArg = ResCollectionModel<V>>(predicate: (this: ThisArg, value: V, index: number, array: Array<V>) => value is S, thisArg?: ThisArg): Array<S>;
×
128
    filter<ThisArg = ResCollectionModel<V>>(predicate: (this: ThisArg, value: V, index: number, array: Array<V>) => unknown, thisArg?: ThisArg): Array<V>;
×
129
    filter(predicate: (value: V, index: number, array: Array<V>) => unknown, thisArg?: unknown): Array<V> {
×
130
        return this.toArray().filter(predicate, thisArg) ;
×
131
    }
×
132

×
133
    /** See: {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find | Array#find } */
×
134
    find<S extends V, ThisArg = ResCollectionModel<V>>(predicate: (this: ThisArg, value: V, index: number, obj: Array<V>) => value is S, thisArg?: ThisArg): S | undefined;
×
135
    find<ThisArg = ResCollectionModel<V>>(predicate: (this: ThisArg, value: V, index: number, obj: Array<V>) => unknown, thisArg?: ThisArg): V | undefined;
×
136
    find(predicate: (value: V, index: number, obj: Array<V>) => unknown, thisArg?: unknown): V | undefined {
×
137
        return this.toArray().find(predicate, thisArg);
×
138
    }
×
139

×
140
    /** See: {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex | Array#findIndex } */
×
141
    findIndex(predicate: (value: V, index: number, obj: Array<V>) => unknown, thisArg?: unknown): number {
×
142
        return this.toArray().findIndex(predicate, thisArg);
×
143
    }
×
144

×
145
    /**
×
146
     * Get the first element, or first X elements if a number is provided.
×
147
     * @param amount The amount of elements to get.
×
148
     */
×
149
    first(amount?: undefined): V | undefined;
×
150
    first(amount: number): Array<V>;
×
151
    first(amount?: number): V | Array<V> | undefined {
×
152
        if (amount === undefined) {
×
153
            const iterable = this[Symbol.iterator]();
×
154
            return iterable.next().value;
×
155
        }
×
156

×
157
        if (amount < 0) {
×
158
            return this.last(amount * -1);
×
159
        }
×
160
        amount = Math.min(amount, this.length);
×
161

×
162
        const iterable = this[Symbol.iterator]();
×
163
        return Array.from({ length: amount }, () => iterable.next().value!);
×
164
    }
×
165

×
166
    get(id: string | number): V | undefined {
×
167
        this._hasID();
×
168
        return this._map![id];
×
169
    }
×
170

×
171
    getOrThrow(id: string | number): V {
×
172
        const item = this.get(id);
×
173
        if (item === undefined) {
×
174
            throw new TypeError(`${id} not found in ${this.rid}`);
×
175
        }
×
176

×
177
        return item;
×
178
    }
×
179

×
180
    has(item: V): boolean {
×
181
        return this._list.includes(item);
×
182
    }
×
183

×
184
    hasKey(key: string | number): boolean {
×
185
        this._hasID();
×
186
        return key in this._map!;
×
187
    }
×
188

×
189
    indexOf(item: V): number {
×
190
        return this._list.indexOf(item);
×
191
    }
×
192

×
193
    override async init(data?: Record<string, V> | undefined): Promise<this> {
×
194
        await super.init(data);
×
195
        if (data) {
×
196
            for (const key of Object.keys(data)) {
×
197
                this._list.push(data[key]!);
×
198
            }
×
199
        }
×
200

×
201
        return this;
×
202
    }
×
203

×
204
    /**
×
205
     * Get the last element, or last X elements if a number is provided.
×
206
     * @param amount The amount of elements to get.
×
207
     */
×
208
    last(amount?: undefined): V | undefined;
×
209
    last(amount: number): Array<V>;
×
210
    last(amount?: number): V | Array<V> | undefined {
×
211
        const iterator = Array.from(this._list);
×
212
        if (amount === undefined) {
×
213
            return iterator.at(-1);
×
214
        }
×
215
        if (amount < 0) {
×
216
            return this.first(amount * -1);
×
217
        }
×
218
        if (!amount) {
×
219
            return [];
×
220
        }
×
221

×
222
        return iterator.slice(-amount);
×
223
    }
×
224

×
225
    /** See: {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map | Array#map } */
×
226
    map<T>(predicate: (value: V, index: number, obj: Array<V>) => T, thisArg?: unknown): Array<T> {
×
227
        return this.toArray().map(predicate, thisArg);
×
228
    }
×
229

×
230
    /**
×
231
     * Pick a random element from the collection, or undefined if the collection is empty.
×
232
     */
×
233
    random(): V | undefined {
×
234
        if (this.empty) {
×
235
            return undefined;
×
236
        }
×
237
        const iterable = Array.from(this._list);
×
238

×
239
        return iterable[Math.floor(Math.random() * iterable.length)];
×
240
    }
×
241

×
242
    /** See: {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce | Array#reduce } */
×
243
    reduce(predicate: (previousValue: V, currentValue: V, currentIndex: number, array: Array<V>) => V): V;
×
244
    reduce(predicate: (previousValue: V, currentValue: V, currentIndex: number, array: Array<V>) => V, initialValue: V): V;
×
245
    reduce<T>(predicate: (previousValue: T, currentValue: V, currentIndex: number, array: Array<V>) => T, initialValue: T): T;
×
246
    reduce<T>(predicate: (previousValue: T, currentValue: V, currentIndex: number, array: Array<V>) => T, initialValue?: T): T {
×
247
        return this.toArray().reduce(predicate, initialValue!);
×
248
    }
×
249

×
250
    /** See: {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduceRight | Array#reduceRight } */
×
251
    reduceRight(predicate: (previousValue: V, currentValue: V, currentIndex: number, array: Array<V>) => V): V;
×
252
    reduceRight(predicate: (previousValue: V, currentValue: V, currentIndex: number, array: Array<V>) => V, initialValue: V): V;
×
253
    reduceRight<T>(predicate: (previousValue: T, currentValue: V, currentIndex: number, array: Array<V>) => T, initialValue: T): T;
×
254
    reduceRight<T>(predicate: (previousValue: T, currentValue: V, currentIndex: number, array: Array<V>) => T, initialValue?: T): T {
×
255
        return this.toArray().reduceRight(predicate, initialValue!);
×
256
    }
×
257

×
258
    /** See: {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some | Array#some } */
×
259
    some<ThisArg = ResCollectionModel<V>>(predicate: (value: V, index: number, array: Array<V>) => unknown, thisArg?: ThisArg): boolean {
×
260
        return this.toArray().some(predicate, thisArg);
×
261
    }
×
262

×
263
    /** Get the values of this collection as an array. */
×
264
    toArray(): Array<V> {
×
265
        return Array.from(this._list);
×
266
    }
×
267
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc