@glade-chat/glade.js 0.1.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.
Files changed (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +52 -0
  3. package/package.json +57 -0
  4. package/src/client/Client.js +355 -0
  5. package/src/errors/index.js +59 -0
  6. package/src/gateway/Gateway.js +172 -0
  7. package/src/gateway/handleDispatch.js +259 -0
  8. package/src/index.js +66 -0
  9. package/src/managers/CachedManager.js +95 -0
  10. package/src/managers/ChannelManager.js +40 -0
  11. package/src/managers/DMManager.js +43 -0
  12. package/src/managers/FriendManager.js +89 -0
  13. package/src/managers/HouseManager.js +44 -0
  14. package/src/managers/InviteManager.js +46 -0
  15. package/src/managers/MemberManager.js +41 -0
  16. package/src/managers/MessageManager.js +69 -0
  17. package/src/managers/RoleManager.js +65 -0
  18. package/src/managers/RoomManager.js +80 -0
  19. package/src/managers/UserManager.js +41 -0
  20. package/src/rest/REST.js +250 -0
  21. package/src/rest/Routes.js +85 -0
  22. package/src/structures/Base.js +70 -0
  23. package/src/structures/ClientUser.js +142 -0
  24. package/src/structures/DMChannel.js +85 -0
  25. package/src/structures/House.js +171 -0
  26. package/src/structures/Invite.js +83 -0
  27. package/src/structures/Member.js +148 -0
  28. package/src/structures/Message.js +176 -0
  29. package/src/structures/ReactionGroup.js +42 -0
  30. package/src/structures/Role.js +107 -0
  31. package/src/structures/Room.js +264 -0
  32. package/src/structures/User.js +113 -0
  33. package/src/structures/VoiceState.js +42 -0
  34. package/src/util/Collection.js +167 -0
  35. package/src/util/Constants.js +183 -0
  36. package/src/util/Permissions.js +148 -0
  37. package/src/util/Util.js +62 -0
  38. package/types/index.d.ts +784 -0
@@ -0,0 +1,107 @@
1
+ 'use strict';
2
+
3
+ import { Base } from './Base.js';
4
+ import { Routes } from '../rest/Routes.js';
5
+ import { PermissionsBitField } from '../util/Permissions.js';
6
+
7
+ /**
8
+ * Represents a role in a House.
9
+ * @extends {Base}
10
+ */
11
+ export class Role extends Base {
12
+ constructor(client, data) {
13
+ super(client);
14
+ /** @type {string} The role id. */
15
+ this.id = data.id;
16
+ this._patch(data);
17
+ }
18
+
19
+ _patch(data) {
20
+ if ('houseId' in data) {
21
+ /** @type {string} Id of the House this role belongs to. */
22
+ this.houseId = data.houseId;
23
+ }
24
+ if ('name' in data) {
25
+ /** @type {string} The role name. */
26
+ this.name = data.name;
27
+ }
28
+ if ('color' in data) {
29
+ /** @type {string | null} The role color. */
30
+ this.color = data.color ?? null;
31
+ }
32
+ if ('permissions' in data) {
33
+ /** @type {PermissionsBitField} The role's permission bitfield. */
34
+ this.permissions = new PermissionsBitField(data.permissions ?? 0);
35
+ }
36
+ if ('position' in data) {
37
+ /** @type {number} The role's sort position. */
38
+ this.position = data.position ?? 0;
39
+ }
40
+ if ('isDefault' in data) {
41
+ /** @type {boolean} Whether this is the `@everyone` role. */
42
+ this.isDefault = Boolean(data.isDefault);
43
+ }
44
+ if ('hoist' in data) {
45
+ /** @type {boolean} Whether members with this role show in their own list section. */
46
+ this.hoist = Boolean(data.hoist);
47
+ }
48
+ return data;
49
+ }
50
+
51
+ /** The House this role belongs to, if cached. */
52
+ get house() {
53
+ return this.client.houses.cache.get(this.houseId) ?? null;
54
+ }
55
+
56
+ /**
57
+ * Edits this role.
58
+ * @param {{ name?: string, color?: string | null, permissions?: import('../util/Permissions.js').PermissionResolvable, hoist?: boolean }} data
59
+ * @returns {Promise<Role>}
60
+ */
61
+ async edit(data) {
62
+ const body = { ...data };
63
+ if (data.permissions !== undefined) body.permissions = PermissionsBitField.resolve(data.permissions);
64
+ const { role } = await this.client.rest.patch(Routes.role(this.id), body);
65
+ this._patch(role);
66
+ return this;
67
+ }
68
+
69
+ /** Sets the role name. */
70
+ setName(name) {
71
+ return this.edit({ name });
72
+ }
73
+
74
+ /** Sets the role color. */
75
+ setColor(color) {
76
+ return this.edit({ color });
77
+ }
78
+
79
+ /**
80
+ * Sets the role's permissions.
81
+ * @param {import('../util/Permissions.js').PermissionResolvable} permissions
82
+ * @returns {Promise<Role>}
83
+ */
84
+ setPermissions(permissions) {
85
+ return this.edit({ permissions });
86
+ }
87
+
88
+ /** Sets whether the role is hoisted. */
89
+ setHoist(hoist) {
90
+ return this.edit({ hoist });
91
+ }
92
+
93
+ /**
94
+ * Deletes this role.
95
+ * @returns {Promise<void>}
96
+ */
97
+ async delete() {
98
+ await this.client.rest.delete(Routes.role(this.id));
99
+ this.house?.roles.cache.delete(this.id);
100
+ }
101
+
102
+ toString() {
103
+ return this.name;
104
+ }
105
+ }
106
+
107
+ export default Role;
@@ -0,0 +1,264 @@
1
+ 'use strict';
2
+
3
+ import { Base } from './Base.js';
4
+ import { Routes } from '../rest/Routes.js';
5
+ import { MessageManager } from '../managers/MessageManager.js';
6
+ import { PermissionsBitField } from '../util/Permissions.js';
7
+ import { RoomTypes } from '../util/Constants.js';
8
+ import { resolveId } from '../util/Util.js';
9
+
10
+ /**
11
+ * Base class for a Glade room (channel) inside a House.
12
+ * @extends {Base}
13
+ */
14
+ export class Room extends Base {
15
+ constructor(client, data) {
16
+ super(client);
17
+ /** @type {string} The room id. */
18
+ this.id = data.id;
19
+ /** @type {MessageManager} Cache + helpers for this room's messages. */
20
+ this.messages = new MessageManager(client, this);
21
+ this._patch(data);
22
+ }
23
+
24
+ _patch(data) {
25
+ if ('houseId' in data) {
26
+ /** @type {string} Id of the House this room belongs to. */
27
+ this.houseId = data.houseId;
28
+ }
29
+ if ('name' in data) {
30
+ /** @type {string} The room name. */
31
+ this.name = data.name;
32
+ }
33
+ if ('type' in data) {
34
+ /** @type {string} The room type: text | voice | portal. */
35
+ this.type = data.type;
36
+ }
37
+ if ('topic' in data) {
38
+ /** @type {string | null} The room topic. */
39
+ this.topic = data.topic ?? null;
40
+ }
41
+ if ('position' in data) {
42
+ /** @type {number} The room's sort position. */
43
+ this.position = data.position ?? 0;
44
+ }
45
+ if ('createdAt' in data) {
46
+ /** @type {string | null} ISO creation timestamp. */
47
+ this.createdAt = data.createdAt ?? null;
48
+ }
49
+ return data;
50
+ }
51
+
52
+ /** Whether this is a text room. */
53
+ isText() {
54
+ return this.type === RoomTypes.Text;
55
+ }
56
+ /** Whether this is a voice room. */
57
+ isVoice() {
58
+ return this.type === RoomTypes.Voice;
59
+ }
60
+ /** Whether this is a portal room. */
61
+ isPortal() {
62
+ return this.type === RoomTypes.Portal;
63
+ }
64
+
65
+ /** The House this room belongs to, if cached. */
66
+ get house() {
67
+ return this.client.houses.cache.get(this.houseId) ?? null;
68
+ }
69
+
70
+ /**
71
+ * Sends a message to this room.
72
+ * @param {string | import('../managers/MessageManager.js').MessagePayload} content
73
+ * @returns {Promise<import('./Message.js').Message>}
74
+ */
75
+ send(content) {
76
+ return this.client._sendMessage({ roomId: this.id }, content);
77
+ }
78
+
79
+ /**
80
+ * Subscribes the gateway connection to this room so it receives the room's
81
+ * realtime events (`messageCreate`, typing, reactions, pins, …). The client
82
+ * auto-subscribes cached rooms on ready unless `autoSubscribeRooms` is disabled.
83
+ * @returns {this}
84
+ */
85
+ subscribe() {
86
+ this.client.gateway.send('room:join', this.id);
87
+ return this;
88
+ }
89
+
90
+ /**
91
+ * Unsubscribes the gateway connection from this room's realtime events.
92
+ * @returns {this}
93
+ */
94
+ unsubscribe() {
95
+ this.client.gateway.send('room:leave', this.id);
96
+ return this;
97
+ }
98
+
99
+ /** Broadcasts a "typing…" indicator to the room. */
100
+ sendTyping() {
101
+ this.client.gateway.send('typing:start', { roomId: this.id });
102
+ return this;
103
+ }
104
+
105
+ /** Stops the "typing…" indicator. */
106
+ stopTyping() {
107
+ this.client.gateway.send('typing:stop', { roomId: this.id });
108
+ return this;
109
+ }
110
+
111
+ /**
112
+ * Fetches a page of messages.
113
+ * @param {{ cursor?: string, limit?: number }} [options]
114
+ * @returns {Promise<{ messages: import('./Message.js').Message[], nextCursor: string | null }>}
115
+ */
116
+ fetchMessages(options) {
117
+ return this.messages.fetch(options);
118
+ }
119
+
120
+ /** Fetches this room's pinned messages. */
121
+ fetchPins() {
122
+ return this.messages.fetchPins();
123
+ }
124
+
125
+ /**
126
+ * Edits this room.
127
+ * @param {{ name?: string, topic?: string | null }} data
128
+ * @returns {Promise<Room>}
129
+ */
130
+ async edit(data) {
131
+ const { room } = await this.client.rest.patch(Routes.room(this.id), data);
132
+ this._patch(room);
133
+ return this;
134
+ }
135
+
136
+ /** Sets the room name. */
137
+ setName(name) {
138
+ return this.edit({ name });
139
+ }
140
+
141
+ /** Sets the room topic (or null to clear). */
142
+ setTopic(topic) {
143
+ return this.edit({ topic });
144
+ }
145
+
146
+ /**
147
+ * Clones this room's settings into a new room (messages are not copied).
148
+ * @returns {Promise<Room>}
149
+ */
150
+ async clone() {
151
+ const { room } = await this.client.rest.post(Routes.roomClone(this.id));
152
+ return this.house ? this.house.rooms._add(room) : new Room(this.client, room);
153
+ }
154
+
155
+ /**
156
+ * Deletes this room.
157
+ * @returns {Promise<void>}
158
+ */
159
+ async delete() {
160
+ await this.client.rest.delete(Routes.room(this.id));
161
+ this.house?.rooms.cache.delete(this.id);
162
+ this.client.channels.cache.delete(this.id);
163
+ }
164
+
165
+ /**
166
+ * Lists this room's per-role permission overrides.
167
+ * @returns {Promise<Array<{ id: string, roomId: string, roleId: string, allow: number, deny: number }>>}
168
+ */
169
+ async fetchPermissionOverrides() {
170
+ const { permissions } = await this.client.rest.get(Routes.roomPermissions(this.id));
171
+ return permissions;
172
+ }
173
+
174
+ /**
175
+ * Sets (or clears) a role's permission override on this room. Pass `allow: 0, deny: 0` to clear.
176
+ * @param {string | import('./Role.js').Role} role
177
+ * @param {{ allow?: import('../util/Permissions.js').PermissionResolvable, deny?: import('../util/Permissions.js').PermissionResolvable }} options
178
+ * @returns {Promise<void>}
179
+ */
180
+ async setPermissionOverride(role, { allow = 0, deny = 0 } = {}) {
181
+ await this.client.rest.put(Routes.roomPermission(this.id, resolveId(role)), {
182
+ allow: PermissionsBitField.resolve(allow),
183
+ deny: PermissionsBitField.resolve(deny),
184
+ });
185
+ }
186
+
187
+ toString() {
188
+ return this.name;
189
+ }
190
+ }
191
+
192
+ /**
193
+ * A text room.
194
+ * @extends {Room}
195
+ */
196
+ export class TextRoom extends Room {}
197
+
198
+ /**
199
+ * A portal room.
200
+ * @extends {Room}
201
+ */
202
+ export class PortalRoom extends Room {}
203
+
204
+ /**
205
+ * A voice room. Adds WebRTC-mesh signaling helpers; actual audio transport is up
206
+ * to the consumer (the gateway only relays SDP/ICE between peers).
207
+ * @extends {Room}
208
+ */
209
+ export class VoiceRoom extends Room {
210
+ /**
211
+ * Joins this voice room. Resolves with the current peers, so the caller can
212
+ * begin WebRTC negotiation. Listen for `voicePeerJoin` / `voiceSignal` etc.
213
+ * @returns {Promise<{ selfSocketId: string, participants: Array<{ socketId: string, userId: string, muted: boolean }> }>}
214
+ */
215
+ async join() {
216
+ const ack = await this.client.gateway.request('voice:join', this.id);
217
+ return { selfSocketId: ack.selfSocketId, participants: ack.participants ?? [] };
218
+ }
219
+
220
+ /** Leaves this voice room. */
221
+ leave() {
222
+ this.client.gateway.send('voice:leave', this.id);
223
+ return this;
224
+ }
225
+
226
+ /**
227
+ * Updates the client's mute/deafen state within this voice room.
228
+ * @param {{ muted: boolean, deafened?: boolean }} state
229
+ */
230
+ setVoiceState(state) {
231
+ this.client.gateway.send('voice:state', { roomId: this.id, ...state });
232
+ return this;
233
+ }
234
+
235
+ /**
236
+ * Relays a WebRTC signaling payload (SDP offer/answer or ICE candidate) to a peer.
237
+ * @param {string} toSocketId
238
+ * @param {any} data
239
+ */
240
+ signal(toSocketId, data) {
241
+ this.client.gateway.send('voice:signal', { toSocketId, data });
242
+ return this;
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Instantiates the correct {@link Room} subclass for the given raw data.
248
+ * @param {import('../client/Client.js').Client} client
249
+ * @param {any} data
250
+ * @returns {Room}
251
+ */
252
+ export function createRoom(client, data) {
253
+ switch (data.type) {
254
+ case RoomTypes.Voice:
255
+ return new VoiceRoom(client, data);
256
+ case RoomTypes.Portal:
257
+ return new PortalRoom(client, data);
258
+ case RoomTypes.Text:
259
+ default:
260
+ return new TextRoom(client, data);
261
+ }
262
+ }
263
+
264
+ export default Room;
@@ -0,0 +1,113 @@
1
+ 'use strict';
2
+
3
+ import { Base } from './Base.js';
4
+
5
+ /**
6
+ * Represents a Glade user. Built from the backend's "public user" shape; message
7
+ * authors carry a reduced set of fields (id, handle, displayName, avatarUrl, bot).
8
+ * @extends {Base}
9
+ */
10
+ export class User extends Base {
11
+ constructor(client, data) {
12
+ super(client);
13
+ /** @type {string} The user's unique id. */
14
+ this.id = data.id;
15
+ this._patch(data);
16
+ }
17
+
18
+ _patch(data) {
19
+ if ('handle' in data) {
20
+ /** @type {string} The user's unique @handle. */
21
+ this.handle = data.handle;
22
+ }
23
+ if ('displayName' in data) {
24
+ /** @type {string} The user's chosen display name. */
25
+ this.displayName = data.displayName;
26
+ }
27
+ if ('avatarUrl' in data) {
28
+ /** @type {string | null} URL of the user's avatar, if set. */
29
+ this.avatarUrl = data.avatarUrl ?? null;
30
+ }
31
+ if ('bannerUrl' in data) {
32
+ /** @type {string | null} URL of the user's profile banner, if set. */
33
+ this.bannerUrl = data.bannerUrl ?? null;
34
+ }
35
+ if ('bio' in data) {
36
+ /** @type {string | null} The user's bio. */
37
+ this.bio = data.bio ?? null;
38
+ }
39
+ if ('status' in data) {
40
+ /** @type {string} The user's presence status (online | idle | dnd | offline). */
41
+ this.status = data.status ?? 'offline';
42
+ }
43
+ // The profile endpoint (GET /users/:id) carries the authoritative *live*
44
+ // presence under `presence`, alongside the stored `status` — prefer it.
45
+ if ('presence' in data) {
46
+ this.status = data.presence ?? this.status ?? 'offline';
47
+ }
48
+ if ('isBot' in data) {
49
+ /** @type {boolean} Whether this account is a bot. */
50
+ this.bot = Boolean(data.isBot);
51
+ }
52
+ if ('badges' in data) {
53
+ /** @type {string[]} Cosmetic badge ids granted to the user. */
54
+ this.badges = data.badges ?? [];
55
+ }
56
+ if ('publicKey' in data) {
57
+ /** @type {string | null} The user's E2E identity public key (base64 SPKI). */
58
+ this.publicKey = data.publicKey ?? null;
59
+ }
60
+ if ('twoFactorEnabled' in data) {
61
+ /** @type {boolean} Whether the user has two-factor enabled. */
62
+ this.twoFactorEnabled = Boolean(data.twoFactorEnabled);
63
+ }
64
+ if ('createdAt' in data) {
65
+ /**
66
+ * ISO timestamp of when the account was created.
67
+ * @type {string | null}
68
+ * @remarks Only the profile endpoint (`users.fetch(id)`) returns this; the
69
+ * self/auth payloads omit it, so `client.user.createdAt` may be `null`.
70
+ */
71
+ this.createdAt = data.createdAt ?? null;
72
+ }
73
+ return data;
74
+ }
75
+
76
+ /** The `@handle` form, suitable for mentions in message content. */
77
+ get tag() {
78
+ return `@${this.handle}`;
79
+ }
80
+
81
+ /**
82
+ * Opens (or fetches the existing) DM channel with this user.
83
+ * @returns {Promise<import('./DMChannel.js').DMChannel>}
84
+ */
85
+ createDM() {
86
+ return this.client.dms.create(this.id);
87
+ }
88
+
89
+ /**
90
+ * Sends a direct message to this user, opening a DM channel if necessary.
91
+ * @param {string | import('../managers/MessageManager.js').MessagePayload} content
92
+ * @returns {Promise<import('./Message.js').Message>}
93
+ */
94
+ async send(content) {
95
+ const dm = await this.createDM();
96
+ return dm.send(content);
97
+ }
98
+
99
+ /**
100
+ * Re-fetches this user's full profile from the API.
101
+ * @returns {Promise<User>}
102
+ */
103
+ fetch() {
104
+ return this.client.users.fetch(this.id, { force: true });
105
+ }
106
+
107
+ /** Mentions the user (`@handle`). */
108
+ toString() {
109
+ return this.tag;
110
+ }
111
+ }
112
+
113
+ export default User;
@@ -0,0 +1,42 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * A lightweight snapshot of a user's state inside a voice room, as reported by the
5
+ * gateway's voice occupancy / peer events.
6
+ */
7
+ export class VoiceState {
8
+ /**
9
+ * @param {import('../client/Client.js').Client} client
10
+ * @param {{ userId: string, muted?: boolean, deafened?: boolean, socketId?: string, roomId?: string }} data
11
+ */
12
+ constructor(client, data) {
13
+ Object.defineProperty(this, 'client', { value: client });
14
+ /** @type {string} Id of the user this state describes. */
15
+ this.userId = data.userId;
16
+ /** @type {string | null} Id of the voice room, if known. */
17
+ this.roomId = data.roomId ?? null;
18
+ /** @type {string | null} The peer's socket id, for per-peer events. */
19
+ this.socketId = data.socketId ?? null;
20
+ /** @type {boolean} Whether the user is muted. */
21
+ this.muted = Boolean(data.muted);
22
+ /** @type {boolean} Whether the user is deafened. */
23
+ this.deafened = Boolean(data.deafened);
24
+ }
25
+
26
+ /** The {@link User} this state belongs to, if cached. */
27
+ get user() {
28
+ return this.client.users.cache.get(this.userId) ?? null;
29
+ }
30
+
31
+ toJSON() {
32
+ return {
33
+ userId: this.userId,
34
+ roomId: this.roomId,
35
+ socketId: this.socketId,
36
+ muted: this.muted,
37
+ deafened: this.deafened,
38
+ };
39
+ }
40
+ }
41
+
42
+ export default VoiceState;
@@ -0,0 +1,167 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * A `Map` with additional utility methods, used throughout glade.js to hold
5
+ * cached structures keyed by their id.
6
+ *
7
+ * @template K, V
8
+ * @extends {Map<K, V>}
9
+ */
10
+ export class Collection extends Map {
11
+ /**
12
+ * Obtains the first value(s) in this collection.
13
+ * @param {number} [amount] How many values to return; negative counts from the end.
14
+ * @returns {V | V[] | undefined}
15
+ */
16
+ first(amount) {
17
+ if (amount === undefined) return this.values().next().value;
18
+ if (amount < 0) return this.last(amount * -1);
19
+ amount = Math.min(this.size, amount);
20
+ const iter = this.values();
21
+ return Array.from({ length: amount }, () => iter.next().value);
22
+ }
23
+
24
+ /**
25
+ * Obtains the last value(s) in this collection.
26
+ * @param {number} [amount]
27
+ * @returns {V | V[] | undefined}
28
+ */
29
+ last(amount) {
30
+ const arr = [...this.values()];
31
+ if (amount === undefined) return arr[arr.length - 1];
32
+ if (amount < 0) return this.first(amount * -1);
33
+ if (!amount) return [];
34
+ return arr.slice(-amount);
35
+ }
36
+
37
+ /**
38
+ * Returns a random value.
39
+ * @returns {V | undefined}
40
+ */
41
+ random() {
42
+ const arr = [...this.values()];
43
+ return arr[Math.floor(Math.random() * arr.length)];
44
+ }
45
+
46
+ /**
47
+ * Searches for a single item where the given function returns a truthy value.
48
+ * @param {(value: V, key: K, collection: this) => boolean} fn
49
+ * @returns {V | undefined}
50
+ */
51
+ find(fn) {
52
+ for (const [key, val] of this) if (fn(val, key, this)) return val;
53
+ return undefined;
54
+ }
55
+
56
+ /**
57
+ * Searches for the key of a single item where the given function returns a truthy value.
58
+ * @param {(value: V, key: K, collection: this) => boolean} fn
59
+ * @returns {K | undefined}
60
+ */
61
+ findKey(fn) {
62
+ for (const [key, val] of this) if (fn(val, key, this)) return key;
63
+ return undefined;
64
+ }
65
+
66
+ /**
67
+ * Identical to `Array.filter()` but returns a new Collection.
68
+ * @param {(value: V, key: K, collection: this) => boolean} fn
69
+ * @returns {Collection<K, V>}
70
+ */
71
+ filter(fn) {
72
+ const results = new this.constructor[Symbol.species]();
73
+ for (const [key, val] of this) if (fn(val, key, this)) results.set(key, val);
74
+ return results;
75
+ }
76
+
77
+ /**
78
+ * Maps each item to something else into an array, like `Array.map()`.
79
+ * @template T
80
+ * @param {(value: V, key: K, collection: this) => T} fn
81
+ * @returns {T[]}
82
+ */
83
+ map(fn) {
84
+ const out = [];
85
+ let i = 0;
86
+ for (const [key, val] of this) out[i++] = fn(val, key, this);
87
+ return out;
88
+ }
89
+
90
+ /**
91
+ * Checks if there exists an item that passes a test.
92
+ * @param {(value: V, key: K, collection: this) => boolean} fn
93
+ * @returns {boolean}
94
+ */
95
+ some(fn) {
96
+ for (const [key, val] of this) if (fn(val, key, this)) return true;
97
+ return false;
98
+ }
99
+
100
+ /**
101
+ * Checks if all items pass a test.
102
+ * @param {(value: V, key: K, collection: this) => boolean} fn
103
+ * @returns {boolean}
104
+ */
105
+ every(fn) {
106
+ for (const [key, val] of this) if (!fn(val, key, this)) return false;
107
+ return true;
108
+ }
109
+
110
+ /**
111
+ * Applies a function to produce a single value, like `Array.reduce()`.
112
+ * @template T
113
+ * @param {(accumulator: T, value: V, key: K, collection: this) => T} fn
114
+ * @param {T} [initial]
115
+ * @returns {T}
116
+ */
117
+ reduce(fn, initial) {
118
+ let accumulator = initial;
119
+ let first = accumulator === undefined;
120
+ for (const [key, val] of this) {
121
+ if (first) {
122
+ accumulator = val;
123
+ first = false;
124
+ continue;
125
+ }
126
+ accumulator = fn(accumulator, val, key, this);
127
+ }
128
+ if (first) throw new TypeError('Reduce of empty collection with no initial value');
129
+ return accumulator;
130
+ }
131
+
132
+ /**
133
+ * Identical to `Map.forEach()` but returns the collection for chaining.
134
+ * @param {(value: V, key: K, collection: this) => void} fn
135
+ * @returns {this}
136
+ */
137
+ each(fn) {
138
+ for (const [key, val] of this) fn(val, key, this);
139
+ return this;
140
+ }
141
+
142
+ /**
143
+ * Returns the items that pass the test as an array.
144
+ * @returns {V[]}
145
+ */
146
+ toArray() {
147
+ return [...this.values()];
148
+ }
149
+
150
+ /**
151
+ * Returns the keys as an array.
152
+ * @returns {K[]}
153
+ */
154
+ keyArray() {
155
+ return [...this.keys()];
156
+ }
157
+
158
+ /**
159
+ * Creates an identical shallow copy of this collection.
160
+ * @returns {Collection<K, V>}
161
+ */
162
+ clone() {
163
+ return new this.constructor[Symbol.species](this);
164
+ }
165
+ }
166
+
167
+ export default Collection;