topsyde-utils 1.3.1 → 1.3.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.
|
@@ -237,7 +237,7 @@ export default class Channel {
|
|
|
237
237
|
delete() {
|
|
238
238
|
//first remove all members
|
|
239
239
|
this.members.forEach((member) => {
|
|
240
|
-
this.removeMember(member);
|
|
240
|
+
this.removeMember(member, { notify: true });
|
|
241
241
|
});
|
|
242
242
|
//then clear members map
|
|
243
243
|
this.members.clear();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Channel.js","sourceRoot":"","sources":["../../../../src/server/bun/websocket/Channel.ts"],"names":[],"mappings":"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","sourcesContent":["import { Guards, Lib } from \"../../../utils\";\nimport Message from \"./Message\";\nimport Websocket from \"./Websocket\";\nimport type {\n\tBroadcastOptions,\n\tI_WebsocketChannel,\n\tI_WebsocketClient,\n\tI_WebsocketEntity,\n\tWebsocketChannel,\n\tWebsocketMessage,\n\tAddMemberResult,\n\tAddMemberOptions,\n\tRemoveMemberOptions,\n} from \"./websocket.types\";\nimport { E_WebsocketMessageType } from \"./websocket.enums\";\n\n/**\n * Channel - Pub/sub topic for WebSocket clients\n *\n * ## Membership Contract\n * - `addMember()` validates capacity and adds to `members` map\n * - Client drives join via `joinChannel()` which subscribes and handles rollback\n * - If subscription fails, membership is automatically rolled back\n * - Member count never exceeds `limit`\n *\n * @example\n * const channel = new Channel(\"game-1\", \"Game Room\", ws, 10);\n * const result = channel.addMember(client);\n * if (result.success) {\n * channel.broadcast({ type: \"player.joined\", content: { player: client.whoami() } });\n * }\n */\nexport default class Channel<T extends Websocket = Websocket> implements I_WebsocketChannel<T> {\n\tpublic createdAt: Date = new Date();\n\tpublic id: string;\n\tpublic name: string;\n\tpublic limit: number;\n\tpublic members: Map<string, I_WebsocketClient>;\n\tpublic metadata: Record<string, string>;\n\tpublic ws: T;\n\n\tconstructor(id: string, name: string, ws: T, limit?: number, members?: Map<string, I_WebsocketClient>, metadata?: Record<string, string>) {\n\t\tthis.id = id;\n\t\tthis.name = name;\n\t\tthis.limit = limit ?? 5;\n\t\tthis.members = members ?? new Map();\n\t\tthis.metadata = metadata ?? {};\n\t\tthis.ws = ws;\n\t}\n\n\tpublic broadcast(message: WebsocketMessage | string, options?: BroadcastOptions) {\n\t\tif (Guards.IsString(message)) {\n\t\t\tconst msg: WebsocketMessage = {\n\t\t\t\ttype: \"message\",\n\t\t\t\tcontent: { message },\n\t\t\t};\n\t\t\tmessage = msg;\n\t\t}\n\n\t\tconst output = Message.Create(message, { ...options, channel: this.id });\n\n\t\t// Include channel metadata if requested\n\t\tif (options?.includeMetadata) {\n\t\t\toutput.metadata = options.includeMetadata === true ? this.getMetadata() : this.getFilteredMetadata(options.includeMetadata);\n\t\t}\n\n\t\tconst serializedMessage = Message.Serialize(output);\n\n\t\t// If we need to exclude clients, send individually to prevent excluded clients from receiving\n\t\tif (options?.excludeClients && options.excludeClients.length > 0) {\n\t\t\tconst excludeSet = new Set(options.excludeClients); // O(1) lookup\n\n\t\t\tfor (const [clientId, client] of this.members) {\n\t\t\t\tif (!excludeSet.has(clientId)) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tclient.ws.send(serializedMessage);\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tLib.Warn(`Failed to send to client ${clientId}:`, error);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// Otherwise use pub/sub for everyone\n\t\tthis.ws.server.publish(this.id, serializedMessage);\n\t}\n\n\t// Helper method for filtered metadata\n\tprivate getFilteredMetadata(keys: string[]) {\n\t\tconst metadata = this.getMetadata();\n\t\tconst filtered: Record<string, string> = {};\n\n\t\tfor (const key of keys) {\n\t\t\tif (metadata[key] !== undefined) {\n\t\t\t\tfiltered[key] = metadata[key];\n\t\t\t}\n\t\t}\n\n\t\treturn filtered;\n\t}\n\n\tpublic hasMember(client: I_WebsocketEntity | string) {\n\t\tif (typeof client === \"string\") return this.members.has(client);\n\t\treturn this.members.has(client.id);\n\t}\n\n\t/**\n\t * ATOMIC: Add member to channel (membership only, no side effects)\n\t * Internal method used for rollback-safe operations\n\t * @internal\n\t */\n\tprivate addToMembersMap(client: I_WebsocketClient, options?: AddMemberOptions): AddMemberResult {\n\t\t// Check if already a member\n\t\tif (this.members.has(client.id)) {\n\t\t\treturn { success: false, reason: \"already_member\" };\n\t\t}\n\n\t\t// Check capacity\n\t\tif (!this.canAddMember()) {\n\t\t\t// Optionally notify client why they can't join\n\t\t\tif (options?.notify_when_full) {\n\t\t\t\tthis.notifyChannelFull(client);\n\t\t\t}\n\t\t\treturn { success: false, reason: \"full\" };\n\t\t}\n\n\t\ttry {\n\t\t\tthis.members.set(client.id, client);\n\t\t\treturn { success: true, client };\n\t\t} catch (error) {\n\t\t\t// Rollback\n\t\t\tthis.members.delete(client.id);\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\treason: \"error\",\n\t\t\t\terror: error instanceof Error ? error : new Error(String(error)),\n\t\t\t};\n\t\t}\n\t}\n\n\t/**\n\t * Add a client to this channel with full coordination\n\t * Handles: membership + WebSocket subscription + client-side tracking + optional notification\n\t * This ensures two-way coordination between channel and client\n\t */\n\tpublic addMember(client: I_WebsocketClient, options?: AddMemberOptions): AddMemberResult {\n\t\t// 1. Atomic membership add\n\t\tconst result = this.addToMembersMap(client, options);\n\t\tif (!result.success) {\n\t\t\treturn result;\n\t\t}\n\n\t\ttry {\n\t\t\t// 2. Subscribe client's WebSocket to channel pub/sub topic\n\t\t\t// CRITICAL: Without this, client won't receive channel.broadcast() messages\n\t\t\tclient.subscribe(this.id);\n\n\t\t\t// 3. Track channel on client side (client's channels map)\n\t\t\tclient.trackChannel(this);\n\n\t\t\t// 4. Optional welcome notification\n\t\t\tif (options?.notify) {\n\t\t\t\tclient.send({\n\t\t\t\t\ttype: E_WebsocketMessageType.CLIENT_JOIN_CHANNEL,\n\t\t\t\t\tcontent: { message: \"Welcome to the channel\" },\n\t\t\t\t\tchannel: this.id,\n\t\t\t\t\tclient: client.whoami(),\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn result;\n\t\t} catch (error) {\n\t\t\t// Rollback on failure: remove membership + unsubscribe + untrack\n\t\t\tthis.removeFromMembersMap(client);\n\t\t\tclient.unsubscribe(this.id);\n\t\t\tclient.untrackChannel(this);\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tpublic addMembers(clients: I_WebsocketClient[], options?: AddMemberOptions): AddMemberResult[] {\n\t\tconst results: AddMemberResult[] = [];\n\t\tfor (const client of clients) {\n\t\t\tconst result = this.addMember(client, options);\n\t\t\tresults.push(result);\n\t\t\tif (!result.success) {\n\t\t\t\t// Stop adding further members on failure\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\treturn results;\n\t}\n\n\tprivate notifyChannelFull(client: I_WebsocketClient): void {\n\t\tclient.send({\n\t\t\ttype: E_WebsocketMessageType.ERROR,\n\t\t\tcontent: {\n\t\t\t\tmessage: `Channel \"${this.name}\" is full (${this.limit} members)`,\n\t\t\t\tcode: \"CHANNEL_FULL\",\n\t\t\t\tchannel: this.id,\n\t\t\t},\n\t\t});\n\t}\n\n\t/**\n\t * Internal method to remove a member without triggering client-side cleanup.\n\t * Used for rollback operations when joinChannel fails.\n\t * @internal\n\t */\n\tpublic removeFromMembersMap(client: I_WebsocketClient): void {\n\t\tthis.members.delete(client.id);\n\t}\n\n\t/**\n\t * Remove a client from this channel with full coordination\n\t * Handles: membership removal + WebSocket unsubscription + client-side tracking removal + optional notification\n\t * This ensures two-way coordination between channel and client\n\t */\n\tpublic removeMember(entity: I_WebsocketEntity, options?: RemoveMemberOptions) {\n\t\t// 1. Check if member exists\n\t\tif (!this.members.has(entity.id)) return false;\n\t\tconst client = this.members.get(entity.id);\n\t\tif (!client) return false;\n\n\t\t// 2. Remove from channel members (atomic operation)\n\t\tthis.removeFromMembersMap(client);\n\n\t\t// 3. Unsubscribe client's WebSocket from channel pub/sub topic\n\t\tclient.unsubscribe(this.id);\n\n\t\t// 4. Untrack channel on client side (remove from client's channels map)\n\t\tclient.untrackChannel(this);\n\n\t\t// 5. Optional goodbye notification\n\t\tif (options?.notify) {\n\t\t\tclient.send({\n\t\t\t\ttype: E_WebsocketMessageType.CLIENT_LEAVE_CHANNEL,\n\t\t\t\tcontent: { message: \"You left the channel\" },\n\t\t\t\tchannel: this.id,\n\t\t\t\tclient: client.whoami(),\n\t\t\t});\n\t\t}\n\n\t\treturn client;\n\t}\n\n\tpublic getMember(client: I_WebsocketEntity | string) {\n\t\tif (typeof client === \"string\") return this.members.get(client);\n\t\treturn this.members.get(client.id);\n\t}\n\n\tpublic getMembers(clients?: string[] | I_WebsocketEntity[]): I_WebsocketClient[] {\n\t\tif (!clients) return Array.from(this.members.values());\n\t\treturn clients.map((client) => this.getMember(client)).filter((client) => client !== undefined) as I_WebsocketClient[];\n\t}\n\n\tpublic getMetadata() {\n\t\treturn this.metadata;\n\t}\n\n\tpublic getCreatedAt() {\n\t\treturn this.createdAt;\n\t}\n\n\tpublic getId() {\n\t\treturn this.id;\n\t}\n\n\tpublic getName() {\n\t\treturn this.name;\n\t}\n\n\tpublic getLimit() {\n\t\treturn this.limit;\n\t}\n\n\tpublic getSize() {\n\t\treturn this.members.size;\n\t}\n\n\tpublic canAddMember() {\n\t\tconst size = this.getSize();\n\t\treturn size < this.limit;\n\t}\n\n\tpublic delete() {\n\t\t//first remove all members\n\t\tthis.members.forEach((member) => {\n\t\t\tthis.removeMember(member);\n\t\t});\n\t\t//then clear members map\n\t\tthis.members.clear();\n\t}\n\n\tpublic static GetChannelType(channels: WebsocketChannel<I_WebsocketChannel> | undefined) {\n\t\tif (!channels) return Channel;\n\t\tif (channels.size > 0) {\n\t\t\tconst firstChannel = channels.values().next().value;\n\t\t\tif (firstChannel) {\n\t\t\t\treturn firstChannel.constructor as typeof Channel;\n\t\t\t} else {\n\t\t\t\treturn Channel;\n\t\t\t}\n\t\t} else {\n\t\t\tLib.Warn(\"Channels are empty, using default channel class\");\n\t\t\treturn Channel;\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"Channel.js","sourceRoot":"","sources":["../../../../src/server/bun/websocket/Channel.ts"],"names":[],"mappings":"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","sourcesContent":["import { Guards, Lib } from \"../../../utils\";\nimport Message from \"./Message\";\nimport Websocket from \"./Websocket\";\nimport type {\n\tBroadcastOptions,\n\tI_WebsocketChannel,\n\tI_WebsocketClient,\n\tI_WebsocketEntity,\n\tWebsocketChannel,\n\tWebsocketMessage,\n\tAddMemberResult,\n\tAddMemberOptions,\n\tRemoveMemberOptions,\n} from \"./websocket.types\";\nimport { E_WebsocketMessageType } from \"./websocket.enums\";\n\n/**\n * Channel - Pub/sub topic for WebSocket clients\n *\n * ## Membership Contract\n * - `addMember()` validates capacity and adds to `members` map\n * - Client drives join via `joinChannel()` which subscribes and handles rollback\n * - If subscription fails, membership is automatically rolled back\n * - Member count never exceeds `limit`\n *\n * @example\n * const channel = new Channel(\"game-1\", \"Game Room\", ws, 10);\n * const result = channel.addMember(client);\n * if (result.success) {\n * channel.broadcast({ type: \"player.joined\", content: { player: client.whoami() } });\n * }\n */\nexport default class Channel<T extends Websocket = Websocket> implements I_WebsocketChannel<T> {\n\tpublic createdAt: Date = new Date();\n\tpublic id: string;\n\tpublic name: string;\n\tpublic limit: number;\n\tpublic members: Map<string, I_WebsocketClient>;\n\tpublic metadata: Record<string, string>;\n\tpublic ws: T;\n\n\tconstructor(id: string, name: string, ws: T, limit?: number, members?: Map<string, I_WebsocketClient>, metadata?: Record<string, string>) {\n\t\tthis.id = id;\n\t\tthis.name = name;\n\t\tthis.limit = limit ?? 5;\n\t\tthis.members = members ?? new Map();\n\t\tthis.metadata = metadata ?? {};\n\t\tthis.ws = ws;\n\t}\n\n\tpublic broadcast(message: WebsocketMessage | string, options?: BroadcastOptions) {\n\t\tif (Guards.IsString(message)) {\n\t\t\tconst msg: WebsocketMessage = {\n\t\t\t\ttype: \"message\",\n\t\t\t\tcontent: { message },\n\t\t\t};\n\t\t\tmessage = msg;\n\t\t}\n\n\t\tconst output = Message.Create(message, { ...options, channel: this.id });\n\n\t\t// Include channel metadata if requested\n\t\tif (options?.includeMetadata) {\n\t\t\toutput.metadata = options.includeMetadata === true ? this.getMetadata() : this.getFilteredMetadata(options.includeMetadata);\n\t\t}\n\n\t\tconst serializedMessage = Message.Serialize(output);\n\n\t\t// If we need to exclude clients, send individually to prevent excluded clients from receiving\n\t\tif (options?.excludeClients && options.excludeClients.length > 0) {\n\t\t\tconst excludeSet = new Set(options.excludeClients); // O(1) lookup\n\n\t\t\tfor (const [clientId, client] of this.members) {\n\t\t\t\tif (!excludeSet.has(clientId)) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tclient.ws.send(serializedMessage);\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tLib.Warn(`Failed to send to client ${clientId}:`, error);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// Otherwise use pub/sub for everyone\n\t\tthis.ws.server.publish(this.id, serializedMessage);\n\t}\n\n\t// Helper method for filtered metadata\n\tprivate getFilteredMetadata(keys: string[]) {\n\t\tconst metadata = this.getMetadata();\n\t\tconst filtered: Record<string, string> = {};\n\n\t\tfor (const key of keys) {\n\t\t\tif (metadata[key] !== undefined) {\n\t\t\t\tfiltered[key] = metadata[key];\n\t\t\t}\n\t\t}\n\n\t\treturn filtered;\n\t}\n\n\tpublic hasMember(client: I_WebsocketEntity | string) {\n\t\tif (typeof client === \"string\") return this.members.has(client);\n\t\treturn this.members.has(client.id);\n\t}\n\n\t/**\n\t * ATOMIC: Add member to channel (membership only, no side effects)\n\t * Internal method used for rollback-safe operations\n\t * @internal\n\t */\n\tprivate addToMembersMap(client: I_WebsocketClient, options?: AddMemberOptions): AddMemberResult {\n\t\t// Check if already a member\n\t\tif (this.members.has(client.id)) {\n\t\t\treturn { success: false, reason: \"already_member\" };\n\t\t}\n\n\t\t// Check capacity\n\t\tif (!this.canAddMember()) {\n\t\t\t// Optionally notify client why they can't join\n\t\t\tif (options?.notify_when_full) {\n\t\t\t\tthis.notifyChannelFull(client);\n\t\t\t}\n\t\t\treturn { success: false, reason: \"full\" };\n\t\t}\n\n\t\ttry {\n\t\t\tthis.members.set(client.id, client);\n\t\t\treturn { success: true, client };\n\t\t} catch (error) {\n\t\t\t// Rollback\n\t\t\tthis.members.delete(client.id);\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\treason: \"error\",\n\t\t\t\terror: error instanceof Error ? error : new Error(String(error)),\n\t\t\t};\n\t\t}\n\t}\n\n\t/**\n\t * Add a client to this channel with full coordination\n\t * Handles: membership + WebSocket subscription + client-side tracking + optional notification\n\t * This ensures two-way coordination between channel and client\n\t */\n\tpublic addMember(client: I_WebsocketClient, options?: AddMemberOptions): AddMemberResult {\n\t\t// 1. Atomic membership add\n\t\tconst result = this.addToMembersMap(client, options);\n\t\tif (!result.success) {\n\t\t\treturn result;\n\t\t}\n\n\t\ttry {\n\t\t\t// 2. Subscribe client's WebSocket to channel pub/sub topic\n\t\t\t// CRITICAL: Without this, client won't receive channel.broadcast() messages\n\t\t\tclient.subscribe(this.id);\n\n\t\t\t// 3. Track channel on client side (client's channels map)\n\t\t\tclient.trackChannel(this);\n\n\t\t\t// 4. Optional welcome notification\n\t\t\tif (options?.notify) {\n\t\t\t\tclient.send({\n\t\t\t\t\ttype: E_WebsocketMessageType.CLIENT_JOIN_CHANNEL,\n\t\t\t\t\tcontent: { message: \"Welcome to the channel\" },\n\t\t\t\t\tchannel: this.id,\n\t\t\t\t\tclient: client.whoami(),\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn result;\n\t\t} catch (error) {\n\t\t\t// Rollback on failure: remove membership + unsubscribe + untrack\n\t\t\tthis.removeFromMembersMap(client);\n\t\t\tclient.unsubscribe(this.id);\n\t\t\tclient.untrackChannel(this);\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tpublic addMembers(clients: I_WebsocketClient[], options?: AddMemberOptions): AddMemberResult[] {\n\t\tconst results: AddMemberResult[] = [];\n\t\tfor (const client of clients) {\n\t\t\tconst result = this.addMember(client, options);\n\t\t\tresults.push(result);\n\t\t\tif (!result.success) {\n\t\t\t\t// Stop adding further members on failure\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\treturn results;\n\t}\n\n\tprivate notifyChannelFull(client: I_WebsocketClient): void {\n\t\tclient.send({\n\t\t\ttype: E_WebsocketMessageType.ERROR,\n\t\t\tcontent: {\n\t\t\t\tmessage: `Channel \"${this.name}\" is full (${this.limit} members)`,\n\t\t\t\tcode: \"CHANNEL_FULL\",\n\t\t\t\tchannel: this.id,\n\t\t\t},\n\t\t});\n\t}\n\n\t/**\n\t * Internal method to remove a member without triggering client-side cleanup.\n\t * Used for rollback operations when joinChannel fails.\n\t * @internal\n\t */\n\tpublic removeFromMembersMap(client: I_WebsocketClient): void {\n\t\tthis.members.delete(client.id);\n\t}\n\n\t/**\n\t * Remove a client from this channel with full coordination\n\t * Handles: membership removal + WebSocket unsubscription + client-side tracking removal + optional notification\n\t * This ensures two-way coordination between channel and client\n\t */\n\tpublic removeMember(entity: I_WebsocketEntity, options?: RemoveMemberOptions) {\n\t\t// 1. Check if member exists\n\t\tif (!this.members.has(entity.id)) return false;\n\t\tconst client = this.members.get(entity.id);\n\t\tif (!client) return false;\n\n\t\t// 2. Remove from channel members (atomic operation)\n\t\tthis.removeFromMembersMap(client);\n\n\t\t// 3. Unsubscribe client's WebSocket from channel pub/sub topic\n\t\tclient.unsubscribe(this.id);\n\n\t\t// 4. Untrack channel on client side (remove from client's channels map)\n\t\tclient.untrackChannel(this);\n\n\t\t// 5. Optional goodbye notification\n\t\tif (options?.notify) {\n\t\t\tclient.send({\n\t\t\t\ttype: E_WebsocketMessageType.CLIENT_LEAVE_CHANNEL,\n\t\t\t\tcontent: { message: \"You left the channel\" },\n\t\t\t\tchannel: this.id,\n\t\t\t\tclient: client.whoami(),\n\t\t\t});\n\t\t}\n\n\t\treturn client;\n\t}\n\n\tpublic getMember(client: I_WebsocketEntity | string) {\n\t\tif (typeof client === \"string\") return this.members.get(client);\n\t\treturn this.members.get(client.id);\n\t}\n\n\tpublic getMembers(clients?: string[] | I_WebsocketEntity[]): I_WebsocketClient[] {\n\t\tif (!clients) return Array.from(this.members.values());\n\t\treturn clients.map((client) => this.getMember(client)).filter((client) => client !== undefined) as I_WebsocketClient[];\n\t}\n\n\tpublic getMetadata() {\n\t\treturn this.metadata;\n\t}\n\n\tpublic getCreatedAt() {\n\t\treturn this.createdAt;\n\t}\n\n\tpublic getId() {\n\t\treturn this.id;\n\t}\n\n\tpublic getName() {\n\t\treturn this.name;\n\t}\n\n\tpublic getLimit() {\n\t\treturn this.limit;\n\t}\n\n\tpublic getSize() {\n\t\treturn this.members.size;\n\t}\n\n\tpublic canAddMember() {\n\t\tconst size = this.getSize();\n\t\treturn size < this.limit;\n\t}\n\n\tpublic delete() {\n\t\t//first remove all members\n\t\tthis.members.forEach((member) => {\n\t\t\tthis.removeMember(member, { notify: true });\n\t\t});\n\t\t//then clear members map\n\t\tthis.members.clear();\n\t}\n\n\tpublic static GetChannelType(channels: WebsocketChannel<I_WebsocketChannel> | undefined) {\n\t\tif (!channels) return Channel;\n\t\tif (channels.size > 0) {\n\t\t\tconst firstChannel = channels.values().next().value;\n\t\t\tif (firstChannel) {\n\t\t\t\treturn firstChannel.constructor as typeof Channel;\n\t\t\t} else {\n\t\t\t\treturn Channel;\n\t\t\t}\n\t\t} else {\n\t\t\tLib.Warn(\"Channels are empty, using default channel class\");\n\t\t\treturn Channel;\n\t\t}\n\t}\n}\n"]}
|
package/package.json
CHANGED
|
@@ -287,7 +287,7 @@ export default class Channel<T extends Websocket = Websocket> implements I_Webso
|
|
|
287
287
|
public delete() {
|
|
288
288
|
//first remove all members
|
|
289
289
|
this.members.forEach((member) => {
|
|
290
|
-
this.removeMember(member);
|
|
290
|
+
this.removeMember(member, { notify: true });
|
|
291
291
|
});
|
|
292
292
|
//then clear members map
|
|
293
293
|
this.members.clear();
|