diff --git a/src/modules/chat/emotes.js b/src/modules/chat/emotes.js index f66e14e5..13cf3e90 100644 --- a/src/modules/chat/emotes.js +++ b/src/modules/chat/emotes.js @@ -65,6 +65,8 @@ export default class Emotes extends Module { this.global_sets = new SourcedSet; this.emote_sets = {}; + this._set_refs = {}; + this._set_timers = {}; } onEnable() { @@ -121,7 +123,44 @@ export default class Emotes extends Module { } // ======================================================================== - // FFZ Emote Sets + // Emote Set Ref Counting + // ======================================================================== + + addDefaultSet(provider, set_id, data) { + if ( ! this.default_sets.sourceIncludes(provider, set_id) ) { + this.default_sets.push(provider, set_id); + this.refSet(set_id); + } + + if ( data ) + this.loadSetData(set_id, data); + } + + removeDefaultSet(provider, set_id) { + if ( this.default_sets.sourceIncludes(provider, set_id) ) { + this.default_sets.remove(provider, set_id); + this.unrefSet(set_id); + } + } + + refSet(set_id) { + this._set_refs[set_id] = (this._set_refs[set_id] || 0) + 1; + if ( this._set_timers[set_id] ) { + clearTimeout(this._set_timers[set_id]); + this._set_timers[set_id] = null; + } + + } + + unrefSet(set_id) { + const c = this._set_refs[set_id] = (this._set_refs[set_id] || 1) - 1; + if ( c <= 0 && ! this._set_timers[set_id] ) + this._set_timers[set_id] = setTimeout(() => this.unloadSet(set_id), 5000); + } + + + // ======================================================================== + // Emote Set Loading // ======================================================================== async loadGlobalSets(tries = 0) { @@ -149,13 +188,12 @@ export default class Emotes extends Module { const sets = data.sets || {}; - this.default_sets.extend('ffz-global', ...data.default_sets); + for(const set_id of data.default_sets) + this.addDefaultSet('ffz-global', set_id); for(const set_id in sets) - if ( has(sets, set_id) ) { - this.global_sets.push('ffz-global', set_id); + if ( has(sets, set_id) ) this.loadSetData(set_id, sets[set_id]); - } if ( data.users ) this.loadSetUsers(data.users); @@ -168,12 +206,9 @@ export default class Emotes extends Module { const emote_set = this.emote_sets[set_id], users = data[set_id]; - for(const login of users) { - const user = this.parent.getUser(undefined, login), - sets = user.emote_sets; - - sets.push('ffz-global', set_id); - } + for(const login of users) + this.parent.getUser(undefined, login) + .addSet('ffz-global', set_id); this.log.info(`Added "${emote_set ? emote_set.title : set_id}" emote set to ${users.length} users.`); } @@ -191,8 +226,6 @@ export default class Emotes extends Module { this.emote_sets[set_id] = data; - data.users = old_set ? old_set.users : 0; - let count = 0; const ems = data.emotes || data.emoticons, new_ems = data.emotes = {}, @@ -237,11 +270,30 @@ export default class Emotes extends Module { else if ( css.length ) data.pending_css = css.join(''); - this.log.info(`Updated emotes for set #${set_id}: ${data.title}`); + this.log.info(`Loaded emote set #${set_id}: ${data.title} (${count} emotes)`); this.emit(':loaded', set_id, data); } + unloadSet(set_id, force = false) { + const old_set = this.emote_sets[set_id], + count = this._set_refs[set_id] || 0; + + if ( ! old_set ) + return; + + if ( count > 0 ) { + if ( ! force ) + return this.log.warn(`Attempted to unload emote set #${set_id} with ${count} users.`); + this.log.warn(`Unloading emote set ${set_id} with ${count} users.`); + } + + this.log.info(`Unloaded emote set #${set_id}: ${old_set.title}`); + this.emit(':unloaded', set_id, old_set); + this.emote_sets[set_id] = null; + } + + // ======================================================================== // Emote CSS // ======================================================================== diff --git a/src/modules/chat/index.js b/src/modules/chat/index.js index e34e13f5..334a5452 100644 --- a/src/modules/chat/index.js +++ b/src/modules/chat/index.js @@ -9,12 +9,13 @@ const WEBKIT = IS_WEBKIT ? '-webkit-' : ''; import Module from 'utilities/module'; import {createElement, ManagedStyle} from 'utilities/dom'; -import {timeout, has, SourcedSet} from 'utilities/object'; +import {timeout, has} from 'utilities/object'; import Badges from './badges'; import Emotes from './emotes'; import Room from './room'; +import User from './user'; import * as TOKENIZERS from './tokenizers'; @@ -308,6 +309,8 @@ export default class Chat extends Module { getUser(id, login, no_create, no_login) { let user; + if ( id && typeof id === 'number' ) + id = `${id}`; if ( this.user_ids[id] ) user = this.user_ids[id]; @@ -319,7 +322,7 @@ export default class Chat extends Module { return null; else - user = {id, login, badges: [], emote_sets: new SourcedSet}; + user = new User(this, null, id, login); if ( id && id !== user.id ) { // If the ID isn't what we expected, something is very wrong here. @@ -328,7 +331,7 @@ export default class Chat extends Module { throw new Error('id mismatch'); // Otherwise, we're just here to set the ID. - user.id = id; + user._id = id; this.user_ids[id] = user; } @@ -354,6 +357,8 @@ export default class Chat extends Module { getRoom(id, login, no_create, no_login) { let room; + if ( id && typeof id === 'number' ) + id = `${id}`; if ( this.room_ids[id] ) room = this.room_ids[id]; @@ -374,7 +379,7 @@ export default class Chat extends Module { throw new Error('id mismatch'); // Otherwise, we're just here to set the ID. - room.id = id; + room._id = id; this.room_ids[id] = room; } diff --git a/src/modules/chat/room.js b/src/modules/chat/room.js index f8e56394..f7301685 100644 --- a/src/modules/chat/room.js +++ b/src/modules/chat/room.js @@ -4,19 +4,18 @@ // Room // ============================================================================ +import User from './user'; + import {API_SERVER, IS_WEBKIT} from 'utilities/constants'; -import {EventEmitter} from 'utilities/events'; import {ManagedStyle} from 'utilities/dom'; import {has, SourcedSet} from 'utilities/object'; const WEBKIT = IS_WEBKIT ? '-webkit-' : ''; -export default class Room extends EventEmitter { +export default class Room { constructor(manager, id, login) { - super(); - this._destroy_timer = null; this.manager = manager; @@ -30,8 +29,8 @@ export default class Room extends EventEmitter { this.style = new ManagedStyle(`room--${login}`); this.emote_sets = new SourcedSet; - this.users = []; - this.user_ids = []; + this.users = {}; + this.user_ids = {}; this.manager.emit(':room-add', this); this.load_data(); @@ -48,6 +47,22 @@ export default class Room extends EventEmitter { this.style.destroy(); + for(const user of Object.values(this.user_ids)) { + if ( user ) + user.destroy(); + } + + for(const user of Object.values(this.users)) { + if ( user ) + user.destroy(); + } + + for(const set_id of this.emote_sets._cache) + this.manager.emotes.unrefSet(set_id); + + this.emote_sets = null; + this.style = null; + if ( this._login ) { if ( this.manager.rooms[this._login] === this ) this.manager.rooms[this._login] = null; @@ -74,11 +89,10 @@ export default class Room extends EventEmitter { if ( this._login ) { const old_room = this.manager.rooms[this._login]; - if ( old_room !== this ) - old_room.login = null; - - this.manager.rooms[this._login] = null; - this.manager.socket.unsubscribe(`room.${this.login}`); + if ( old_room === this ) { + this.manager.rooms[this._login] = null; + this.manager.socket.unsubscribe(`room.${this.login}`); + } } this._login = val; @@ -95,6 +109,54 @@ export default class Room extends EventEmitter { } + getUser(id, login, no_create, no_login) { + let user; + if ( id && typeof id === 'number' ) + id = `${id}`; + + if ( this.user_ids[id] ) + user = this.user_ids[id]; + + else if ( this.users[login] && ! no_login ) + user = this.users[login]; + + else if ( no_create ) + return null; + + else + user = new User(this.manager, this, id, login); + + if ( id && id !== user.id ) { + // If the ID isn't what we expected, something is very wrong here. + // Blame name changes. + if ( user.id ) + throw new Error('id mismatch'); + + // Otherwise, we're just here to set the ID. + user._id = id; + this.user_ids[id] = user; + } + + if ( login ) { + const other = this.users[login]; + if ( other ) { + if ( other !== this && ! no_login ) { + // If the other has an ID, something weird happened. Screw it + // and just take over. + if ( other.id ) + this.users[login] = user; + else { + // TODO: Merge Logic~~ + } + } + } else + this.users[login] = user; + } + + return user; + } + + // ======================================================================== // FFZ Data // ======================================================================== @@ -163,6 +225,27 @@ export default class Room extends EventEmitter { } + // ======================================================================== + // Emote Sets + // ======================================================================== + + addSet(provider, set_id, data) { + if ( ! this.emote_sets.sourceIncludes(provider, set_id) ) { + this.emote_sets.push(provider, set_id); + this.manager.emotes.refSet(set_id); + } + + if ( data ) + this.manager.emotes.loadSetData(set_id, data); + } + + removeSet(provider, set_id) { + if ( this.emote_sets.sourceIncludes(provider, set_id) ) { + this.emote_sets.remove(provider, set_id); + this.manager.emotes.unrefSet(set_id); + } + } + // ======================================================================== // Life Cycle diff --git a/src/modules/chat/user.js b/src/modules/chat/user.js new file mode 100644 index 00000000..6d3d45b3 --- /dev/null +++ b/src/modules/chat/user.js @@ -0,0 +1,82 @@ +'use strict'; + +// ============================================================================ +// User +// ============================================================================ + +import {SourcedSet} from 'utilities/object'; + +export default class User { + constructor(manager, room, id, login) { + this.manager = manager; + this.room = room; + + this._id = id; + this.login = login; + + if ( id ) + (room || manager).user_ids[id] = this; + + this.emote_sets = new SourcedSet; + } + + destroy() { + this.destroyed = true; + + for(const set_id of this.emote_sets._cache) + this.manager.emotes.unrefSet(set_id); + } + + get id() { + return this._id; + } + + get login() { + return this._login; + } + + set login(val) { + if ( this._login === val ) + return; + + const obj = this.room || this.manager; + + if ( this._login ) { + const old_user = obj.users[this._login]; + if ( old_user === this ) + obj.users[this._login] = null; + } + + this._login = val; + if ( ! val ) + return; + + const old_user = obj.users[val]; + if ( old_user && old_user !== this ) + old_user.login = null; + + obj.users[val] = this; + } + + + // ======================================================================== + // Emote Sets + // ======================================================================== + + addSet(provider, set_id, data) { + if ( ! this.emote_sets.sourceIncludes(provider, set_id) ) { + this.emote_sets.push(provider, set_id); + this.manager.emotes.refSet(set_id); + } + + if ( data ) + this.manager.emotes.loadSetData(set_id, data); + } + + removeSet(provider, set_id) { + if ( this.emote_sets.sourceIncludes(provider, set_id) ) { + this.emote_sets.remove(provider, set_id); + this.manager.emotes.unrefSet(set_id); + } + } +} \ No newline at end of file diff --git a/src/utilities/object.js b/src/utilities/object.js index dc6e9de7..8fba2c12 100644 --- a/src/utilities/object.js +++ b/src/utilities/object.js @@ -186,6 +186,11 @@ export class SourcedSet { get(key) { return this._sources && this._sources.get(key) } has(key) { return this._sources ? this._sources.has(key) : false } + sourceIncludes(key, val) { + const src = this._sources && this._sources.get(key); + return src && src.includes(val); + } + includes(val) { return this._cache.includes(val); }