{"version":3,"file":"socket-manager.mjs","sources":["../../../../src/clients/feed/socket-manager.ts"],"sourcesContent":["import { Store } from \"@tanstack/store\";\nimport { Channel, Socket } from \"phoenix\";\n\nimport Feed from \"./feed\";\nimport type { FeedClientOptions, FeedMetadata } from \"./interfaces\";\n\nexport const SocketEventType = {\n  NewMessage: \"new-message\",\n} as const;\n\nconst SOCKET_EVENT_TYPES = [SocketEventType.NewMessage];\n\ntype ClientQueryParams = FeedClientOptions;\n\n// e.g. feeds:<channel_id>:<user_id>\ntype ChannelTopic = string;\n\n// Unique reference id of a feed client\ntype ClientReferenceId = string;\n\ntype NewMessageEventPayload = {\n  event: typeof SocketEventType.NewMessage;\n  /**\n   * @deprecated Top-level feed metadata. Exists for legacy reasons.\n   */\n  metadata: FeedMetadata;\n  /** Feed metadata, keyed by client reference id. */\n  data: Record<ClientReferenceId, { metadata: FeedMetadata }>;\n};\n\nexport type SocketEventPayload = NewMessageEventPayload;\n\n// \"attn\" field contains a list of client reference ids that should be notified\n// of a socket event.\ntype WithAttn<P> = P & { attn: ClientReferenceId[] };\n\ntype FeedSocketInbox = Record<ClientReferenceId, SocketEventPayload>;\n\n/*\n * Manages socket subscriptions for feeds, allowing multiple feed clients\n * to listen for real time updates from the socket API via a single socket\n * connection.\n */\nexport class FeedSocketManager {\n  // Mapping of live channels by topic. Note, there can be one or more feed\n  // client(s) that can subscribe.\n  private channels: Record<ChannelTopic, Channel>;\n\n  // Mapping of query params for each feeds client, partitioned by reference id,\n  // and grouped by channel topic. It's a double nested object that looks like:\n  //  {\n  //    \"feeds:<channel_1>:<user_1>\": {\n  //      \"ref-1\": {\n  //        \"tenant\": \"foo\",\n  //      },\n  //      \"ref-2\": {\n  //        \"tenant\": \"bar\",\n  //      },\n  //    },\n  //    \"feeds:<channel_2>:<user_1>\": {\n  //      \"ref-3\": {\n  //        \"tenant\": \"baz\",\n  //      },\n  //    }\n  //  }\n  //\n  // Each time a new feed client joins a channel, we send all cumulated\n  // params such that the socket API can apply filtering rules and figure out\n  // which feed clients should be notified based on reference ids in\n  // \"attn\" field of the event payload when sending out an event.\n  private params: Record<\n    ChannelTopic,\n    Record<ClientReferenceId, ClientQueryParams>\n  >;\n\n  // A reactive store that captures a new socket event, that notifies any feed\n  // clients that have subscribed.\n  private inbox: Store<\n    FeedSocketInbox,\n    (cb: FeedSocketInbox) => FeedSocketInbox\n  >;\n\n  constructor(readonly socket: Socket) {\n    this.channels = {};\n    this.params = {};\n    this.inbox = new Store<FeedSocketInbox>({});\n  }\n\n  join(feed: Feed) {\n    const topic = feed.socketChannelTopic;\n    const referenceId = feed.referenceId;\n    const params = feed.defaultOptions;\n\n    // Ensure a live socket connection if not yet connected.\n    if (!this.socket.isConnected()) {\n      this.socket.connect();\n    }\n\n    // If a new feed client joins, or has updated query params, then\n    // track the updated params and (re)join with the latest query params.\n    // Note, each time we send combined params of all feed clients that\n    // have subscribed for a given feed channel and user, grouped by\n    // client's reference id.\n    if (!this.params[topic]) {\n      this.params[topic] = {};\n    }\n\n    const maybeParams = this.params[topic][referenceId];\n    const hasNewOrUpdatedParams =\n      !maybeParams || JSON.stringify(maybeParams) !== JSON.stringify(params);\n\n    if (hasNewOrUpdatedParams) {\n      // Tracks all subscribed clients' params by reference id and by topic.\n      this.params[topic] = { ...this.params[topic], [referenceId]: params };\n    }\n\n    if (!this.channels[topic] || hasNewOrUpdatedParams) {\n      const newChannel = this.socket.channel(topic, this.params[topic]);\n      for (const eventType of SOCKET_EVENT_TYPES) {\n        newChannel.on(eventType, (payload) => this.setInbox(payload));\n      }\n      // Tracks live channels by channel topic.\n      this.channels[topic] = newChannel;\n    }\n\n    const channel = this.channels[topic];\n\n    // Join the channel if not already joined or joining or leaving.\n    if ([\"closed\", \"errored\"].includes(channel.state)) {\n      channel.join();\n    }\n\n    // Let the feed client subscribe to the \"inbox\", so it can be notified\n    // when there's a new socket event that is relevant to it\n    const unsub = this.inbox.subscribe(() => {\n      const payload = this.inbox.state[referenceId];\n      if (!payload) return;\n\n      feed.handleSocketEvent(payload);\n    });\n\n    return unsub;\n  }\n\n  leave(feed: Feed) {\n    feed.unsubscribeFromSocketEvents?.();\n\n    const topic = feed.socketChannelTopic;\n    const referenceId = feed.referenceId;\n\n    const partitionedParams = { ...this.params };\n    const paramsForTopic = partitionedParams[topic] || {};\n    const paramsForReferenceClient = paramsForTopic[referenceId];\n\n    if (paramsForReferenceClient) {\n      delete paramsForTopic[referenceId];\n    }\n\n    const channels = { ...this.channels };\n    const channelForTopic = channels[topic];\n    if (channelForTopic && Object.keys(paramsForTopic).length === 0) {\n      for (const eventType of SOCKET_EVENT_TYPES) {\n        channelForTopic.off(eventType);\n      }\n      channelForTopic.leave();\n      delete channels[topic];\n    }\n\n    this.params = partitionedParams;\n    this.channels = channels;\n  }\n\n  private setInbox(payload: WithAttn<SocketEventPayload>) {\n    const { attn, ...rest } = payload;\n\n    // Set the incoming socket event into the inbox, keyed by relevant client\n    // reference ids provided by the server (via attn field), so we can notify\n    // only the clients that need to be notified.\n    this.inbox.setState(() =>\n      attn.reduce((acc, referenceId) => {\n        return { ...acc, [referenceId]: rest };\n      }, {}),\n    );\n  }\n}\n"],"names":["SocketEventType","SOCKET_EVENT_TYPES","FeedSocketManager","socket","__publicField","Store","feed","topic","referenceId","params","maybeParams","hasNewOrUpdatedParams","newChannel","eventType","payload","channel","_a","partitionedParams","paramsForTopic","channels","channelForTopic","attn","rest","acc"],"mappings":";;;;AAMO,MAAMA,IAAkB;AAAA,EAC7B,YAAY;AACd,GAEMC,IAAqB,CAACD,EAAgB,UAAU;AAiC/C,MAAME,EAAkB;AAAA,EAuC7B,YAAqBC,GAAgB;AApC7B;AAAA;AAAA,IAAAC,EAAA;AAwBA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAAAA,EAAA;AAOA;AAAA;AAAA,IAAAA,EAAA;AAKa,SAAA,SAAAD,GACnB,KAAK,WAAW,CAAC,GACjB,KAAK,SAAS,CAAC,GACf,KAAK,QAAQ,IAAIE,EAAuB,EAAE;AAAA,EAAA;AAAA,EAG5C,KAAKC,GAAY;AACf,UAAMC,IAAQD,EAAK,oBACbE,IAAcF,EAAK,aACnBG,IAASH,EAAK;AAGpB,IAAK,KAAK,OAAO,iBACf,KAAK,OAAO,QAAQ,GAQjB,KAAK,OAAOC,CAAK,MACf,KAAA,OAAOA,CAAK,IAAI,CAAC;AAGxB,UAAMG,IAAc,KAAK,OAAOH,CAAK,EAAEC,CAAW,GAC5CG,IACJ,CAACD,KAAe,KAAK,UAAUA,CAAW,MAAM,KAAK,UAAUD,CAAM;AAOvE,QALIE,MAEF,KAAK,OAAOJ,CAAK,IAAI,EAAE,GAAG,KAAK,OAAOA,CAAK,GAAG,CAACC,CAAW,GAAGC,EAAO,IAGlE,CAAC,KAAK,SAASF,CAAK,KAAKI,GAAuB;AAC5C,YAAAC,IAAa,KAAK,OAAO,QAAQL,GAAO,KAAK,OAAOA,CAAK,CAAC;AAChE,iBAAWM,KAAaZ;AACtB,QAAAW,EAAW,GAAGC,GAAW,CAACC,MAAY,KAAK,SAASA,CAAO,CAAC;AAGzD,WAAA,SAASP,CAAK,IAAIK;AAAA,IAAA;AAGnB,UAAAG,IAAU,KAAK,SAASR,CAAK;AAGnC,WAAI,CAAC,UAAU,SAAS,EAAE,SAASQ,EAAQ,KAAK,KAC9CA,EAAQ,KAAK,GAKD,KAAK,MAAM,UAAU,MAAM;AACvC,YAAMD,IAAU,KAAK,MAAM,MAAMN,CAAW;AAC5C,MAAKM,KAELR,EAAK,kBAAkBQ,CAAO;AAAA,IAAA,CAC/B;AAAA,EAEM;AAAA,EAGT,MAAMR,GAAY;;AAChB,KAAAU,IAAAV,EAAK,gCAAL,QAAAU,EAAA,KAAAV;AAEA,UAAMC,IAAQD,EAAK,oBACbE,IAAcF,EAAK,aAEnBW,IAAoB,EAAE,GAAG,KAAK,OAAO,GACrCC,IAAiBD,EAAkBV,CAAK,KAAK,CAAC;AAGpD,IAFiCW,EAAeV,CAAW,KAGzD,OAAOU,EAAeV,CAAW;AAGnC,UAAMW,IAAW,EAAE,GAAG,KAAK,SAAS,GAC9BC,IAAkBD,EAASZ,CAAK;AACtC,QAAIa,KAAmB,OAAO,KAAKF,CAAc,EAAE,WAAW,GAAG;AAC/D,iBAAWL,KAAaZ;AACtB,QAAAmB,EAAgB,IAAIP,CAAS;AAE/B,MAAAO,EAAgB,MAAM,GACtB,OAAOD,EAASZ,CAAK;AAAA,IAAA;AAGvB,SAAK,SAASU,GACd,KAAK,WAAWE;AAAA,EAAA;AAAA,EAGV,SAASL,GAAuC;AACtD,UAAM,EAAE,MAAAO,GAAM,GAAGC,EAAA,IAASR;AAK1B,SAAK,MAAM;AAAA,MAAS,MAClBO,EAAK,OAAO,CAACE,GAAKf,OACT,EAAE,GAAGe,GAAK,CAACf,CAAW,GAAGc,EAAK,IACpC,CAAE,CAAA;AAAA,IACP;AAAA,EAAA;AAEJ;"}