@colyseus/core 0.17.0 → 0.17.1

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.
@@ -0,0 +1,141 @@
1
+ import type { ExtractMessageType, Room } from '../Room.ts';
2
+
3
+ export type RoomMethodName = 'onCreate'
4
+ | 'onAuth'
5
+ | 'onJoin'
6
+ | 'onLeave'
7
+ | 'onDispose'
8
+ | 'onMessage'
9
+ | 'setSimulationInterval'
10
+ | 'setInterval'
11
+ | 'setTimeout';
12
+
13
+ export type RoomException<R extends Room = Room> =
14
+ OnCreateException<R> |
15
+ OnAuthException<R> |
16
+ OnJoinException<R> |
17
+ OnLeaveException<R> |
18
+ OnDisposeException |
19
+ OnMessageException<R> |
20
+ SimulationIntervalException |
21
+ TimedEventException;
22
+
23
+ export class OnCreateException<R extends Room = Room> extends Error {
24
+ options: Parameters<R['onCreate']>[0];
25
+ constructor(
26
+ cause: Error,
27
+ message: string,
28
+ options: Parameters<R['onCreate']>[0],
29
+ ) {
30
+ super(message, { cause });
31
+ this.name = 'OnCreateException';
32
+ this.options = options;
33
+ }
34
+ }
35
+
36
+ export class OnAuthException<R extends Room = Room> extends Error {
37
+ client: Parameters<R['onAuth']>[0];
38
+ options: Parameters<R['onAuth']>[1];
39
+ constructor(
40
+ cause: Error,
41
+ message: string,
42
+ client: Parameters<R['onAuth']>[0],
43
+ options: Parameters<R['onAuth']>[1],
44
+ ) {
45
+ super(message, { cause });
46
+ this.name = 'OnAuthException';
47
+ this.client = client;
48
+ this.options = options;
49
+ }
50
+ }
51
+
52
+ export class OnJoinException<R extends Room = Room> extends Error {
53
+ client: Parameters<R['onJoin']>[0];
54
+ options: Parameters<R['onJoin']>[1];
55
+ auth: Parameters<R['onJoin']>[2];
56
+ constructor(
57
+ cause: Error,
58
+ message: string,
59
+ client: Parameters<R['onJoin']>[0],
60
+ options: Parameters<R['onJoin']>[1],
61
+ auth: Parameters<R['onJoin']>[2],
62
+ ) {
63
+ super(message, { cause });
64
+ this.name = 'OnJoinException';
65
+ this.client = client;
66
+ this.options = options;
67
+ this.auth = auth;
68
+ }
69
+ }
70
+
71
+ export class OnLeaveException<R extends Room = Room> extends Error {
72
+ client: Parameters<R['onLeave']>[0];
73
+ consented: Parameters<R['onLeave']>[1];
74
+ constructor(
75
+ cause: Error,
76
+ message: string,
77
+ client: Parameters<R['onLeave']>[0],
78
+ consented: Parameters<R['onLeave']>[1],
79
+ ) {
80
+ super(message, { cause });
81
+ this.name = 'OnLeaveException';
82
+ this.client = client;
83
+ this.consented = consented;
84
+ }
85
+ }
86
+
87
+ export class OnDisposeException extends Error {
88
+ constructor(
89
+ cause: Error,
90
+ message: string,
91
+ ) {
92
+ super(message, { cause });
93
+ this.name = 'OnDisposeException';
94
+ }
95
+ }
96
+
97
+ export class OnMessageException<R extends Room, MessageType extends keyof R['messages'] = keyof R['messages']> extends Error {
98
+ client: R['~client'];
99
+ payload: ExtractMessageType<R['messages'][MessageType]>;
100
+ type: MessageType;
101
+ constructor(
102
+ cause: Error,
103
+ message: string,
104
+ client: R['~client'],
105
+ payload: ExtractMessageType<R['messages'][MessageType]>,
106
+ type: MessageType,
107
+ ) {
108
+ super(message, { cause });
109
+ this.name = 'OnMessageException';
110
+ this.client = client;
111
+ this.payload = payload;
112
+ this.type = type;
113
+ }
114
+
115
+ public isType<T extends keyof R['messages']>(type: T): this is OnMessageException<R, T> {
116
+ return (this.type as string) === type;
117
+ }
118
+ }
119
+
120
+ export class SimulationIntervalException extends Error {
121
+ constructor(
122
+ cause: Error,
123
+ message: string,
124
+ ) {
125
+ super(message, { cause });
126
+ this.name = 'SimulationIntervalException';
127
+ }
128
+ }
129
+
130
+ export class TimedEventException extends Error {
131
+ public args: any[];
132
+ constructor(
133
+ cause: Error,
134
+ message: string,
135
+ ...args: any[]
136
+ ) {
137
+ super(message, { cause });
138
+ this.name = 'TimedEventException';
139
+ this.args = args;
140
+ }
141
+ }
@@ -0,0 +1,5 @@
1
+ export class SeatReservationError extends Error {
2
+ constructor(message: string) {
3
+ super(message);
4
+ }
5
+ }
@@ -0,0 +1,17 @@
1
+ import { ErrorCode } from '../Protocol.ts';
2
+
3
+ export class ServerError extends Error {
4
+ public code: number;
5
+
6
+ constructor(code: number = ErrorCode.MATCHMAKE_UNHANDLED, message?: string) {
7
+ super(message);
8
+
9
+ // Maintains proper stack trace for where our error was thrown (only available on V8)
10
+ if (Error.captureStackTrace) {
11
+ Error.captureStackTrace(this, ServerError);
12
+ }
13
+
14
+ this.name = 'ServerError';
15
+ this.code = code;
16
+ }
17
+ }
package/src/index.ts ADDED
@@ -0,0 +1,81 @@
1
+ import { ClockTimer as Clock, Delayed } from '@colyseus/timer';
2
+
3
+ // Core classes
4
+ export { Server, defineRoom, defineServer, type ServerOptions, type SDKTypes } from './Server.ts';
5
+ export { Room, room, RoomInternalState, validate, type RoomOptions, type MessageHandlerWithFormat, type ExtractMessageType, type Messages, type ExtractRoomState, type ExtractRoomMetadata, type ExtractRoomClient } from './Room.ts';
6
+ export { Protocol, ErrorCode, getMessageBytes, CloseCode } from './Protocol.ts';
7
+ export { RegisteredHandler } from './matchmaker/RegisteredHandler.ts';
8
+ export { ServerError } from './errors/ServerError.ts';
9
+
10
+ export {
11
+ type RoomException,
12
+ type RoomMethodName,
13
+ OnCreateException,
14
+ OnAuthException,
15
+ OnJoinException,
16
+ OnLeaveException,
17
+ OnDisposeException,
18
+ OnMessageException,
19
+ SimulationIntervalException,
20
+ TimedEventException,
21
+ } from './errors/RoomExceptions.ts';
22
+
23
+ // MatchMaker
24
+ import * as matchMaker from './MatchMaker.ts';
25
+ export { matchMaker };
26
+ export { updateLobby, subscribeLobby } from './matchmaker/Lobby.ts';
27
+
28
+ // Driver
29
+ export * from './matchmaker/LocalDriver/LocalDriver.ts';
30
+ export { initializeRoomCache } from './matchmaker/driver.ts';
31
+
32
+ // Transport
33
+ export { type Client, type ClientPrivate, type AuthContext, ClientState, ClientArray, Transport, type ISendOptions, connectClientToRoom } from './Transport.ts';
34
+
35
+ // Presence
36
+ export { type Presence } from './presence/Presence.ts';
37
+ export { LocalPresence } from './presence/LocalPresence.ts';
38
+
39
+ // Serializers
40
+ export { type Serializer } from './serializer/Serializer.ts';
41
+ export { SchemaSerializer } from './serializer/SchemaSerializer.ts';
42
+ // export { SchemaSerializerDebug } from './serializer/SchemaSerializerDebug.ts';
43
+
44
+ // Utilities
45
+ export { Clock, Delayed };
46
+ export { generateId, Deferred, HttpServerMock, spliceOne, getBearerToken } from './utils/Utils.ts';
47
+ export { isDevMode } from './utils/DevMode.ts';
48
+
49
+ // IPC
50
+ export { subscribeIPC, requestFromIPC } from './IPC.ts';
51
+
52
+ // Debug
53
+ export {
54
+ debugMatchMaking,
55
+ debugMessage,
56
+ debugPatch,
57
+ debugError,
58
+ debugConnection,
59
+ debugDriver,
60
+ debugPresence,
61
+ debugAndPrintError,
62
+ } from './Debug.ts';
63
+
64
+ // Default rooms
65
+ export { LobbyRoom } from './rooms/LobbyRoom.ts';
66
+ export { RelayRoom } from './rooms/RelayRoom.ts';
67
+ export { RankedQueueRoom, type RankedQueueOptions, type MatchGroup, type MatchTeam, type ClientQueueData } from './rooms/RankedQueueRoom.ts';
68
+
69
+ // Router / Endpoints
70
+ export {
71
+ createEndpoint,
72
+ createInternalContext,
73
+ createMiddleware,
74
+ createRouter,
75
+ toNodeHandler,
76
+ __globalEndpoints,
77
+ type Router,
78
+ } from './router/index.ts';
79
+
80
+ // Abstract logging support
81
+ export { logger } from './Logger.ts';
@@ -0,0 +1,68 @@
1
+ import * as matchMaker from '../MatchMaker.ts';
2
+
3
+ import type { Room } from '../Room.ts';
4
+ import type { IRoomCache } from './driver.ts';
5
+
6
+ const LOBBY_CHANNEL = '$lobby';
7
+
8
+ /*
9
+ * TODO: refactor this on 1.0
10
+ *
11
+ * Some users might be relying on "1" = "removed" from the lobby due to this workaround: https://github.com/colyseus/colyseus/issues/617
12
+ * Though, for consistency, we should invert as "0" = "invisible" and "1" = "visible".
13
+ *
14
+ * - rename "removed" to "isVisible" and swap the logic
15
+ * - emit "visibility-change" with inverted value (isVisible)
16
+ * - update "subscribeLobby" to check "1" as "isVisible" instead of "removed"
17
+ */
18
+
19
+ export function updateLobby<T extends Room>(room: T, removed: boolean = false) {
20
+ const listing = room['_listing'];
21
+
22
+ if (listing.unlisted || !listing.roomId) {
23
+ return;
24
+ }
25
+
26
+ if (removed) {
27
+ matchMaker.presence.publish(LOBBY_CHANNEL, `${listing.roomId},1`);
28
+ } else if (!listing.private) {
29
+ matchMaker.presence.publish(LOBBY_CHANNEL, `${listing.roomId},0`);
30
+ }
31
+ }
32
+
33
+ export async function subscribeLobby(callback: (roomId: string, roomListing: IRoomCache) => void) {
34
+ // Track removed roomIds to prevent race conditions where pending queries
35
+ // complete after a room has been removed
36
+ const removedRoomIds = new Set<string>();
37
+
38
+ const cb = async (message: string) => {
39
+ const [roomId, isRemove] = message.split(',');
40
+
41
+ if (isRemove === '1') {
42
+ // Mark as removed and process immediately
43
+ removedRoomIds.add(roomId);
44
+ callback(roomId, null);
45
+
46
+ // Clean up after a short timeout to prevent memory leaks
47
+ setTimeout(() => removedRoomIds.delete(roomId), 2000);
48
+
49
+ } else {
50
+ // Clear removed status - room might be coming back (e.g., visibility change)
51
+ removedRoomIds.delete(roomId);
52
+
53
+ const room = (await matchMaker.query({ roomId }))[0];
54
+
55
+ // Check if room was removed while we were querying
56
+ // See "updating metadata should not cause race condition" test in LobbyRoom.test.ts
57
+ if (removedRoomIds.has(roomId)) {
58
+ return;
59
+ }
60
+
61
+ callback(roomId, room);
62
+ }
63
+ };
64
+
65
+ await matchMaker.presence.subscribe(LOBBY_CHANNEL, cb);
66
+
67
+ return () => matchMaker.presence.unsubscribe(LOBBY_CHANNEL, cb);
68
+ }
@@ -0,0 +1,92 @@
1
+ import { debugMatchMaking } from '../../Debug.ts';
2
+ import type { IRoomCache, SortOptions, MatchMakerDriver } from '../driver.ts';
3
+ import { Query } from './Query.ts';
4
+
5
+ // re-export
6
+ export type { IRoomCache, SortOptions, MatchMakerDriver };
7
+
8
+ export class LocalDriver implements MatchMakerDriver {
9
+ public rooms: IRoomCache[] = [];
10
+
11
+ public has(roomId: string) {
12
+ return this.rooms.some((room) => room.roomId === roomId);
13
+ }
14
+
15
+ public query(conditions: Partial<IRoomCache>, sortOptions?: SortOptions) {
16
+ const query = new Query<IRoomCache>(this.rooms, conditions);
17
+
18
+ if (sortOptions) {
19
+ query.sort(sortOptions);
20
+ }
21
+
22
+ return query.filter(conditions);
23
+ }
24
+
25
+ public cleanup(processId: string) {
26
+ const cachedRooms = this.query({ processId });
27
+ debugMatchMaking("removing stale rooms by processId %s (%s rooms found)", processId, cachedRooms.length);
28
+
29
+ cachedRooms.forEach((room) => this.remove(room.roomId));
30
+ return Promise.resolve();
31
+ }
32
+
33
+ public findOne(conditions: Partial<IRoomCache>, sortOptions?: SortOptions) {
34
+ const query = new Query<IRoomCache>(this.rooms, conditions);
35
+
36
+ if (sortOptions) {
37
+ query.sort(sortOptions);
38
+ }
39
+
40
+ return query as unknown as Promise<IRoomCache>;
41
+ }
42
+
43
+ public update(room: IRoomCache, operations: Partial<{ $set: Partial<IRoomCache>, $inc: Partial<IRoomCache> }>) {
44
+ if (operations.$set) {
45
+ for (const field in operations.$set) {
46
+ if (operations.$set.hasOwnProperty(field)) {
47
+ room[field] = operations.$set[field];
48
+ }
49
+ }
50
+ }
51
+
52
+ if (operations.$inc) {
53
+ for (const field in operations.$inc) {
54
+ if (operations.$inc.hasOwnProperty(field)) {
55
+ room[field] += operations.$inc[field];
56
+ }
57
+ }
58
+ }
59
+
60
+ return true;
61
+ }
62
+
63
+ public persist(room: IRoomCache, create: boolean = false) {
64
+ // if (this.rooms.indexOf(room) !== -1) {
65
+ // // already in the list
66
+ // return true;
67
+ // }
68
+
69
+ if (!create) { return false; }
70
+
71
+ // add to the list
72
+ this.rooms.push(room);
73
+
74
+ return true;
75
+ }
76
+
77
+ public remove(roomId: string) {
78
+ const roomIndex = this.rooms.findIndex((room) => room.roomId === roomId);
79
+ if (roomIndex !== -1) {
80
+ this.rooms.splice(roomIndex, 1);
81
+ return true;
82
+ }
83
+ return false;
84
+ }
85
+
86
+ public clear() {
87
+ this.rooms = [];
88
+ }
89
+
90
+ public shutdown() {
91
+ }
92
+ }
@@ -0,0 +1,94 @@
1
+ import type { IRoomCache, SortOptions } from '../driver.ts';
2
+
3
+ export class Query<T extends IRoomCache> {
4
+ private $rooms: T[];
5
+ private conditions: any;
6
+ private sortOptions?: SortOptions;
7
+
8
+ constructor(rooms: any[], conditions) {
9
+ this.$rooms = rooms.slice(0);
10
+ this.conditions = conditions;
11
+ }
12
+
13
+ public sort(options: SortOptions) {
14
+ // Store sort options instead of sorting immediately
15
+ // This allows filtering first, then sorting fewer items
16
+ this.sortOptions = options;
17
+ return this;
18
+ }
19
+
20
+ private applySort(rooms: T[]): T[] {
21
+ if (!this.sortOptions) {
22
+ return rooms;
23
+ }
24
+
25
+ return rooms.sort((room1: T, room2: T) => {
26
+ for (const field in this.sortOptions) {
27
+ const direction = this.sortOptions[field];
28
+ if (room1.hasOwnProperty(field)) {
29
+ /**
30
+ * IRoomCache field
31
+ */
32
+ if (direction === 1 || direction === 'asc' || direction === 'ascending') {
33
+ if (room1[field] > room2[field]) { return 1; }
34
+ if (room1[field] < room2[field]) { return -1; }
35
+
36
+ } else {
37
+ if (room1[field] > room2[field]) { return -1; }
38
+ if (room1[field] < room2[field]) { return 1; }
39
+ }
40
+ } else if (room1.metadata?.hasOwnProperty(field)) {
41
+ /**
42
+ * metadata field
43
+ */
44
+ if (direction === 1 || direction === 'asc' || direction === 'ascending') {
45
+ if (room1.metadata[field] > room2.metadata[field]) { return 1; }
46
+ if (room1.metadata[field] < room2.metadata[field]) { return -1; }
47
+ } else {
48
+ if (room1.metadata[field] > room2.metadata[field]) { return -1; }
49
+ if (room1.metadata[field] < room2.metadata[field]) { return 1; }
50
+ }
51
+ }
52
+ }
53
+ return 0;
54
+ });
55
+ }
56
+
57
+ private applyFilter(rooms: T[], conditions: any): T[] {
58
+ return rooms.filter(((room) => {
59
+ for (const field in conditions) {
60
+ if (conditions.hasOwnProperty(field)) {
61
+ // Check if field exists in room (IRoomCache base fields)
62
+ if (room.hasOwnProperty(field)) {
63
+ if (room[field] !== conditions[field]) {
64
+ return false;
65
+ }
66
+ } else if (room.metadata?.hasOwnProperty(field)) {
67
+ // Check if field exists in metadata
68
+ if (room.metadata[field] !== conditions[field]) {
69
+ return false;
70
+ }
71
+ } else {
72
+ // Field doesn't exist in room or metadata
73
+ return false;
74
+ }
75
+ }
76
+ }
77
+ return true;
78
+ }));
79
+ }
80
+
81
+ public filter(conditions: any) {
82
+ // Filter first to reduce the number of items to sort
83
+ const filtered = this.applyFilter(this.$rooms, conditions);
84
+ return this.applySort(filtered);
85
+ }
86
+
87
+ public then(resolve, reject) {
88
+ // Filter first to reduce the number of items to sort
89
+ const filtered = this.applyFilter(this.$rooms, this.conditions);
90
+ const sorted = this.applySort(filtered);
91
+ const result: any = sorted[0];
92
+ return resolve(result);
93
+ }
94
+ }
@@ -0,0 +1,172 @@
1
+ import { EventEmitter } from 'events';
2
+ import { logger } from '../Logger.ts';
3
+ import { Room } from './../Room.ts';
4
+ import { updateLobby } from './Lobby.ts';
5
+
6
+ import type { IRoomCache, SortOptions, IRoomCacheFilterByKeys, IRoomCacheSortByKeys, ExtractMetadata } from './driver.ts';
7
+ import type { Client } from '../Transport.ts';
8
+ import type { Type } from "../utils/Utils.ts";
9
+
10
+ export const INVALID_OPTION_KEYS: Array<keyof IRoomCache> = [
11
+ 'clients',
12
+ 'locked',
13
+ 'private',
14
+ // 'maxClients', - maxClients can be useful as filter options
15
+ 'metadata',
16
+ 'name',
17
+ 'processId',
18
+ 'roomId',
19
+ ];
20
+
21
+ /**
22
+ * Type for filterBy that supports both onCreate options and metadata fields
23
+ */
24
+ type FilterByKeys<RoomType extends Room> =
25
+ | IRoomCacheFilterByKeys
26
+ | (ExtractMetadata<RoomType> extends object
27
+ ? keyof ExtractMetadata<RoomType> & string
28
+ : never)
29
+
30
+ /**
31
+ * Type for sortBy that supports room cache fields and metadata fields
32
+ */
33
+ type SortByKeys<RoomType extends Room> =
34
+ | IRoomCacheSortByKeys
35
+ | (ExtractMetadata<RoomType> extends object
36
+ ? keyof ExtractMetadata<RoomType> & string
37
+ : never);
38
+
39
+ export interface RegisteredHandlerEvents<RoomType extends Type<Room> = any> {
40
+ create: [room: InstanceType<RoomType>];
41
+ lock: [room: InstanceType<RoomType>];
42
+ unlock: [room: InstanceType<RoomType>];
43
+ join: [room: InstanceType<RoomType>, client: Client];
44
+ leave: [room: InstanceType<RoomType>, client: Client, willDispose: boolean];
45
+ dispose: [room: InstanceType<RoomType>];
46
+ 'visibility-change': [room: InstanceType<RoomType>, isVisible: boolean];
47
+ 'metadata-change': [room: InstanceType<RoomType>];
48
+ }
49
+
50
+ export class RegisteredHandler<
51
+ RoomType extends Type<Room> = any
52
+ > extends EventEmitter<RegisteredHandlerEvents<RoomType>> {
53
+ '~room': RoomType;
54
+
55
+ public klass: RoomType;
56
+ public options: any;
57
+
58
+ public name: string;
59
+ public filterOptions: Array<FilterByKeys<InstanceType<RoomType>>> = [];
60
+ public sortOptions?: SortOptions;
61
+
62
+ public realtimeListingEnabled: boolean = false;
63
+
64
+ constructor(klass: RoomType, options?: any) {
65
+ super();
66
+
67
+ this.klass = klass;
68
+ this.options = options;
69
+
70
+ if (typeof(klass) !== 'function') {
71
+ logger.debug('You are likely not importing your room class correctly.');
72
+ throw new Error(`class is expected but ${typeof(klass)} was provided.`);
73
+ }
74
+ }
75
+
76
+ public enableRealtimeListing() {
77
+ this.realtimeListingEnabled = true;
78
+ this.on('create', (room) => updateLobby(room));
79
+ this.on('lock', (room) => updateLobby(room));
80
+ this.on('unlock', (room) => updateLobby(room));
81
+ this.on('join', (room) => updateLobby(room));
82
+ this.on('leave', (room, _, willDispose) => {
83
+ if (!willDispose) {
84
+ updateLobby(room);
85
+ }
86
+ });
87
+ this.on('visibility-change', (room, isVisible) => updateLobby(room, isVisible));
88
+ this.on('metadata-change', (room) => updateLobby(room));
89
+ this.on('dispose', (room) => updateLobby(room, true));
90
+ return this;
91
+ }
92
+
93
+ /**
94
+ * Define which fields should be used for filtering rooms.
95
+ * Supports both onCreate options and metadata fields using dot notation.
96
+ *
97
+ * @example
98
+ * // Filter by IRoomCache fields
99
+ * .filterBy(['maxClients'])
100
+ *
101
+ * @example
102
+ * // Filter by metadata fields
103
+ * .filterBy(['difficulty', 'metadata.region'])
104
+ *
105
+ * @example
106
+ * // Mix both
107
+ * .filterBy(['mode', 'difficulty', 'maxClients'])
108
+ */
109
+ public filterBy<T extends FilterByKeys<InstanceType<RoomType>>>(
110
+ options: T[]
111
+ ) {
112
+ this.filterOptions = options;
113
+ return this;
114
+ }
115
+
116
+ /**
117
+ * Define how rooms should be sorted when querying.
118
+ * Supports both room cache fields and metadata fields using dot notation.
119
+ *
120
+ * @example
121
+ * // Sort by number of clients (descending)
122
+ * .sortBy({ clients: -1 })
123
+ *
124
+ * @example
125
+ * // Sort by metadata field
126
+ * .sortBy({ 'metadata.rating': -1 })
127
+ *
128
+ * @example
129
+ * // Multiple sort criteria
130
+ * .sortBy({ 'metadata.skillLevel': 1, clients: -1 })
131
+ */
132
+ public sortBy<T extends SortByKeys<InstanceType<RoomType>>>(
133
+ options: { [K in T]: SortOptions[string] }
134
+ ): this {
135
+ this.sortOptions = options as unknown as SortOptions;
136
+ return this;
137
+ }
138
+
139
+ public getMetadataFromOptions(options: any) {
140
+ const metadata = this.getFilterOptions(options);
141
+
142
+ if (this.sortOptions) {
143
+ for (const field in this.sortOptions) {
144
+ if (field in options && !(field in metadata)) {
145
+ metadata[field] = options[field];
146
+ }
147
+ }
148
+ }
149
+
150
+ return Object.keys(metadata).length > 0 ? { metadata } : {};
151
+ }
152
+
153
+ /**
154
+ * Extract filter options from client options.
155
+ */
156
+ public getFilterOptions(options: any) {
157
+ return this.filterOptions.reduce((prev, curr, i, arr) => {
158
+ const field = String(arr[i]);
159
+
160
+ // Handle regular (non-metadata) fields
161
+ if (options.hasOwnProperty(field)) {
162
+ if (INVALID_OPTION_KEYS.indexOf(field as any) !== -1) {
163
+ logger.warn(`option "${field}" has internal usage and is going to be ignored.`);
164
+ } else {
165
+ prev[field] = options[field];
166
+ }
167
+ }
168
+
169
+ return prev;
170
+ }, {});
171
+ }
172
+ }