@knocklabs/client 0.14.5 → 0.14.6
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.
- package/CHANGELOG.md +10 -0
- package/dist/cjs/clients/feed/feed.js +1 -1
- package/dist/cjs/clients/feed/feed.js.map +1 -1
- package/dist/cjs/clients/feed/index.js +1 -1
- package/dist/cjs/clients/feed/index.js.map +1 -1
- package/dist/cjs/clients/feed/socket-manager.js +2 -0
- package/dist/cjs/clients/feed/socket-manager.js.map +1 -0
- package/dist/esm/clients/feed/feed.mjs +117 -102
- package/dist/esm/clients/feed/feed.mjs.map +1 -1
- package/dist/esm/clients/feed/index.mjs +31 -15
- package/dist/esm/clients/feed/index.mjs.map +1 -1
- package/dist/esm/clients/feed/socket-manager.mjs +81 -0
- package/dist/esm/clients/feed/socket-manager.mjs.map +1 -0
- package/dist/types/clients/feed/feed.d.ts +11 -6
- package/dist/types/clients/feed/feed.d.ts.map +1 -1
- package/dist/types/clients/feed/index.d.ts +2 -0
- package/dist/types/clients/feed/index.d.ts.map +1 -1
- package/dist/types/clients/feed/socket-manager.d.ts +31 -0
- package/dist/types/clients/feed/socket-manager.d.ts.map +1 -0
- package/dist/types/clients/feed/types.d.ts +4 -3
- package/dist/types/clients/feed/types.d.ts.map +1 -1
- package/package.json +4 -3
- package/src/clients/feed/feed.ts +51 -41
- package/src/clients/feed/index.ts +27 -3
- package/src/clients/feed/socket-manager.ts +185 -0
- package/src/clients/feed/types.ts +5 -3
|
@@ -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
|
|
26
|
-
|
|
27
|
-
}
|
|
26
|
+
export type FeedMessagesReceivedPayload = Extract<
|
|
27
|
+
SocketEventPayload,
|
|
28
|
+
{ event: typeof SocketEventType.NewMessage }
|
|
29
|
+
>;
|
|
28
30
|
|
|
29
31
|
/*
|
|
30
32
|
Event types:
|