@@ -6,14 +6,15 @@ import {
66 ChannelCreateEvent , ChannelDeleteEvent , ChannelUpdateEvent ,
77 GuildCreateEvent ,
88 GuildRemoveEvent , GuildUpdateEvent ,
9+ GuildsAvailableEvent ,
910 MemberJoinEvent ,
1011 MemberRemoveEvent , MemberUpdateEvent ,
1112 MessageCreateEvent ,
1213 MessageDeleteEvent , MessageUpdateEvent ,
1314 PresenceUpdateEvent ,
1415 ReadyEvent ,
1516 RelationshipCreateEvent ,
16- RelationshipRemoveEvent , RoleCreateEvent , RoleDeleteEvent , RolePositionsUpdateEvent , RoleUpdateEvent ,
17+ RelationshipRemoveEvent , RequestGuildsPayload , RoleCreateEvent , RoleDeleteEvent , RolePositionsUpdateEvent , RoleUpdateEvent ,
1718 TypingStartEvent ,
1819 TypingStopEvent ,
1920 UpdatePresencePayload ,
@@ -50,6 +51,10 @@ export const WsEventHandlers: Record<string, WsEventHandler> = {
5051
5152 ws . api . cache ?. updateUser ( data . after )
5253 } ,
54+ guilds_available ( ws : WsClient , data : GuildsAvailableEvent ) {
55+ for ( const guild of data . guilds )
56+ ws . api . cache ?. updateGuild ( guild )
57+ } ,
5358 guild_create ( ws : WsClient , data : GuildCreateEvent ) {
5459 ws . api . cache ?. updateGuild ( data . guild )
5560 } ,
@@ -224,9 +229,7 @@ export default class WsClient {
224229
225230 async forceReady ( ) {
226231 let [ guilds , dmChannels , clientUser , relationships ] = await Promise . all ( [
227- this . api . request ( 'GET' , '/guilds' , {
228- params : { channels : true , members : true , roles : true }
229- } ) ,
232+ this . api . request ( 'GET' , '/guilds' ) ,
230233 this . api . request ( 'GET' , '/users/me/channels' ) ,
231234 this . api . request ( 'GET' , '/users/me' ) ,
232235 this . api . request ( 'GET' , '/relationships' )
@@ -328,6 +331,121 @@ export default class WsClient {
328331 localStorage . setItem ( 'presence' , JSON . stringify ( presence ) )
329332 }
330333
334+ /**
335+ * Requests full guild data for up to 20 guild IDs. For each requested guild that the client is
336+ * a member of, harmony responds with a `guilds_available` event which is handled automatically
337+ * by the cache. Returns a promise that resolves once all responses have arrived.
338+ */
339+ requestGuilds ( guildIds : bigint [ ] , nonce ?: string ) : Promise < void > {
340+ const resolvedNonce = nonce ?? Math . random ( ) . toString ( 36 ) . slice ( 2 )
341+ const payload : RequestGuildsPayload = { guild_ids : guildIds , nonce : resolvedNonce }
342+ this . connection ?. send ( msgpack . encode ( { op : 'request_guilds' , ...payload } ) )
343+
344+ // Mark guilds as loading
345+ const cache = this . api . cache
346+ if ( cache ) {
347+ for ( const id of guildIds ) {
348+ if ( ! cache . isGuildLoaded ( id ) )
349+ cache . guildLoadingStates . set ( id , true )
350+ }
351+ }
352+
353+ return new Promise ( ( resolve ) => {
354+ const remove = this . on ( 'guilds_available' , ( data : GuildsAvailableEvent , removeListener ) => {
355+ if ( data . nonce !== resolvedNonce ) return
356+ removeListener ( )
357+ // Clear loading states for guilds that were returned
358+ if ( cache ) {
359+ for ( const guild of data . guilds )
360+ cache . guildLoadingStates . delete ( guild . id )
361+ // Also clear any that weren't returned (e.g. guild no longer exists)
362+ for ( const id of guildIds )
363+ cache . guildLoadingStates . delete ( id )
364+ }
365+ resolve ( )
366+ } )
367+ setTimeout ( ( ) => {
368+ remove ( )
369+ if ( cache ) {
370+ for ( const id of guildIds )
371+ cache . guildLoadingStates . delete ( id )
372+ }
373+ resolve ( )
374+ } , 10_000 )
375+ } )
376+ }
377+
378+ /**
379+ * Ensures that a guild's full data (channels, roles, members, emojis) is loaded. If already
380+ * loaded, resolves immediately. If currently loading, waits for it to finish. Otherwise,
381+ * sends a request and waits for the response.
382+ */
383+ ensureGuildLoaded ( guildId : bigint ) : Promise < void > {
384+ const cache = this . api . cache
385+ if ( ! cache ) return Promise . resolve ( )
386+ if ( cache . isGuildLoaded ( guildId ) ) return Promise . resolve ( )
387+
388+ if ( cache . isGuildLoading ( guildId ) ) {
389+ // Wait for the loading to complete by watching guilds_available
390+ return new Promise ( ( resolve ) => {
391+ const remove = this . on ( 'guilds_available' , ( data : GuildsAvailableEvent ) => {
392+ if ( data . guilds . some ( g => g . id === guildId ) || ! cache . isGuildLoading ( guildId ) ) {
393+ remove ( )
394+ resolve ( )
395+ }
396+ } )
397+ setTimeout ( ( ) => { remove ( ) ; resolve ( ) } , 10_000 )
398+ } )
399+ }
400+
401+ return this . requestGuilds ( [ guildId ] )
402+ }
403+
404+ /**
405+ * Loads all guilds in the background after the ready event, in priority order:
406+ * 1. The focused guild (from the current URL, if any)
407+ * 2. All remaining guilds sorted by member count (descending)
408+ *
409+ * Batches up to 20 guilds per request.
410+ */
411+ async startBackgroundGuildLoading ( focusedGuildId ?: bigint ) {
412+ const cache = this . api . cache
413+ if ( ! cache ) return
414+
415+ const allGuildIds = cache . guildList
416+
417+ // Build priority-ordered list
418+ const ordered : bigint [ ] = [ ]
419+ const seen = new Set < bigint > ( )
420+
421+ const add = ( id : bigint ) => {
422+ if ( ! seen . has ( id ) && ! cache . isGuildLoaded ( id ) ) {
423+ seen . add ( id )
424+ ordered . push ( id )
425+ }
426+ }
427+
428+ // Priority 1: focused guild
429+ if ( focusedGuildId != null ) add ( focusedGuildId )
430+
431+ // Priority 2: remaining guilds sorted by member count desc
432+ const remaining = allGuildIds
433+ . filter ( id => ! seen . has ( id ) )
434+ . sort ( ( a , b ) => {
435+ const aCount = cache . guilds . get ( a ) ?. member_count ?. total ?? 0
436+ const bCount = cache . guilds . get ( b ) ?. member_count ?. total ?? 0
437+ return bCount - aCount
438+ } )
439+ for ( const id of remaining ) add ( id )
440+
441+ // Load in batches of up to 20
442+ for ( let i = 0 ; i < ordered . length ; i += 20 ) {
443+ const batch = ordered . slice ( i , i + 20 )
444+ console . debug ( '[WS] Background loading guild batch:' , batch . map ( String ) )
445+ await this . requestGuilds ( batch )
446+ }
447+ }
448+
331449 reconnectWithBackoff ( ) {
332450 const delay = this . backoff . delay ( )
333451 console . debug ( `[WS] Connection closed, attempting reconnect in ${ delay / 1000 } seconds` )
0 commit comments