@knocklabs/client 0.14.5 → 0.14.7

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,185 @@
1
+ import { Store } from "@tanstack/store";
2
+ import { Channel, Socket } from "phoenix";
3
+
4
+ import Feed from "./feed";
5
+ import type { FeedClientOptions, FeedMetadata } from "./interfaces";
6
+
7
+ export const SocketEventType = {
8
+ NewMessage: "new-message",
9
+ } as const;
10
+
11
+ const SOCKET_EVENT_TYPES = [SocketEventType.NewMessage];
12
+
13
+ type ClientQueryParams = FeedClientOptions;
14
+
15
+ // e.g. feeds:<channel_id>:<user_id>
16
+ type ChannelTopic = string;
17
+
18
+ // Unique reference id of a feed client
19
+ type ClientReferenceId = string;
20
+
21
+ type NewMessageEventPayload = {
22
+ event: typeof SocketEventType.NewMessage;
23
+ /**
24
+ * @deprecated Top-level feed metadata. Exists for legacy reasons.
25
+ */
26
+ metadata: FeedMetadata;
27
+ /** Feed metadata, keyed by client reference id. */
28
+ data: Record<ClientReferenceId, { metadata: FeedMetadata }>;
29
+ };
30
+
31
+ export type SocketEventPayload = NewMessageEventPayload;
32
+
33
+ // "attn" field contains a list of client reference ids that should be notified
34
+ // of a socket event.
35
+ type WithAttn<P> = P & { attn: ClientReferenceId[] };
36
+
37
+ type FeedSocketInbox = Record<ClientReferenceId, SocketEventPayload>;
38
+
39
+ /*
40
+ * Manages socket subscriptions for feeds, allowing multiple feed clients
41
+ * to listen for real time updates from the socket API via a single socket
42
+ * connection.
43
+ */
44
+ export class FeedSocketManager {
45
+ // Mapping of live channels by topic. Note, there can be one or more feed
46
+ // client(s) that can subscribe.
47
+ private channels: Record<ChannelTopic, Channel>;
48
+
49
+ // Mapping of query params for each feeds client, partitioned by reference id,
50
+ // and grouped by channel topic. It's a double nested object that looks like:
51
+ // {
52
+ // "feeds:<channel_1>:<user_1>": {
53
+ // "ref-1": {
54
+ // "tenant": "foo",
55
+ // },
56
+ // "ref-2": {
57
+ // "tenant": "bar",
58
+ // },
59
+ // },
60
+ // "feeds:<channel_2>:<user_1>": {
61
+ // "ref-3": {
62
+ // "tenant": "baz",
63
+ // },
64
+ // }
65
+ // }
66
+ //
67
+ // Each time a new feed client joins a channel, we send all cumulated
68
+ // params such that the socket API can apply filtering rules and figure out
69
+ // which feed clients should be notified based on reference ids in
70
+ // "attn" field of the event payload when sending out an event.
71
+ private params: Record<
72
+ ChannelTopic,
73
+ Record<ClientReferenceId, ClientQueryParams>
74
+ >;
75
+
76
+ // A reactive store that captures a new socket event, that notifies any feed
77
+ // clients that have subscribed.
78
+ private inbox: Store<
79
+ FeedSocketInbox,
80
+ (cb: FeedSocketInbox) => FeedSocketInbox
81
+ >;
82
+
83
+ constructor(readonly socket: Socket) {
84
+ this.channels = {};
85
+ this.params = {};
86
+ this.inbox = new Store<FeedSocketInbox>({});
87
+ }
88
+
89
+ join(feed: Feed) {
90
+ const topic = feed.socketChannelTopic;
91
+ const referenceId = feed.referenceId;
92
+ const params = feed.defaultOptions;
93
+
94
+ // Ensure a live socket connection if not yet connected.
95
+ if (!this.socket.isConnected()) {
96
+ this.socket.connect();
97
+ }
98
+
99
+ // If a new feed client joins, or has updated query params, then
100
+ // track the updated params and (re)join with the latest query params.
101
+ // Note, each time we send combined params of all feed clients that
102
+ // have subscribed for a given feed channel and user, grouped by
103
+ // client's reference id.
104
+ if (!this.params[topic]) {
105
+ this.params[topic] = {};
106
+ }
107
+
108
+ const maybeParams = this.params[topic][referenceId];
109
+ const hasNewOrUpdatedParams =
110
+ !maybeParams || JSON.stringify(maybeParams) !== JSON.stringify(params);
111
+
112
+ if (hasNewOrUpdatedParams) {
113
+ // Tracks all subscribed clients' params by reference id and by topic.
114
+ this.params[topic] = { ...this.params[topic], [referenceId]: params };
115
+ }
116
+
117
+ if (!this.channels[topic] || hasNewOrUpdatedParams) {
118
+ const newChannel = this.socket.channel(topic, this.params[topic]);
119
+ for (const eventType of SOCKET_EVENT_TYPES) {
120
+ newChannel.on(eventType, (payload) => this.setInbox(payload));
121
+ }
122
+ // Tracks live channels by channel topic.
123
+ this.channels[topic] = newChannel;
124
+ }
125
+
126
+ const channel = this.channels[topic];
127
+
128
+ // Join the channel if not already joined or joining or leaving.
129
+ if (["closed", "errored"].includes(channel.state)) {
130
+ channel.join();
131
+ }
132
+
133
+ // Let the feed client subscribe to the "inbox", so it can be notified
134
+ // when there's a new socket event that is relevant to it
135
+ const unsub = this.inbox.subscribe(() => {
136
+ const payload = this.inbox.state[referenceId];
137
+ if (!payload) return;
138
+
139
+ feed.handleSocketEvent(payload);
140
+ });
141
+
142
+ return unsub;
143
+ }
144
+
145
+ leave(feed: Feed) {
146
+ feed.unsubscribeFromSocketEvents?.();
147
+
148
+ const topic = feed.socketChannelTopic;
149
+ const referenceId = feed.referenceId;
150
+
151
+ const partitionedParams = { ...this.params };
152
+ const paramsForTopic = partitionedParams[topic] || {};
153
+ const paramsForReferenceClient = paramsForTopic[referenceId];
154
+
155
+ if (paramsForReferenceClient) {
156
+ delete paramsForTopic[referenceId];
157
+ }
158
+
159
+ const channels = { ...this.channels };
160
+ const channelForTopic = channels[topic];
161
+ if (channelForTopic && Object.keys(paramsForTopic).length === 0) {
162
+ for (const eventType of SOCKET_EVENT_TYPES) {
163
+ channelForTopic.off(eventType);
164
+ }
165
+ channelForTopic.leave();
166
+ delete channels[topic];
167
+ }
168
+
169
+ this.params = partitionedParams;
170
+ this.channels = channels;
171
+ }
172
+
173
+ private setInbox(payload: WithAttn<SocketEventPayload>) {
174
+ const { attn, ...rest } = payload;
175
+
176
+ // Set the incoming socket event into the inbox, keyed by relevant client
177
+ // reference ids provided by the server (via attn field), so we can notify
178
+ // only the clients that need to be notified.
179
+ this.inbox.setState(() =>
180
+ attn.reduce((acc, referenceId) => {
181
+ return { ...acc, [referenceId]: rest };
182
+ }, {}),
183
+ );
184
+ }
185
+ }
@@ -3,6 +3,7 @@ import { GenericData, PageInfo } from "@knocklabs/types";
3
3
  import { NetworkStatus } from "../../networkStatus";
4
4
 
5
5
  import { FeedItem, FeedMetadata, FeedResponse } from "./interfaces";
6
+ import { SocketEventPayload, SocketEventType } from "./socket-manager";
6
7
 
7
8
  export type StoreFeedResultOptions = {
8
9
  shouldSetPage?: boolean;
@@ -22,9 +23,10 @@ export interface FeedStoreState {
22
23
  resetStore: (metadata?: FeedMetadata) => void;
23
24
  }
24
25
 
25
- export interface FeedMessagesReceivedPayload {
26
- metadata: FeedMetadata;
27
- }
26
+ export type FeedMessagesReceivedPayload = Extract<
27
+ SocketEventPayload,
28
+ { event: typeof SocketEventType.NewMessage }
29
+ >;
28
30
 
29
31
  /*
30
32
  Event types: