@@ -9,6 +9,55 @@ type Headers = {
99 [key: string]: string;
1010}
1111
12+ type RealtimeResponse = {
13+ type: "error"|"event"|"connected"|"response";
14+ data: RealtimeResponseAuthenticated|RealtimeResponseConnected|RealtimeResponseError|RealtimeResponseEvent<unknown >;
15+ }
16+
17+ type RealtimeRequest = {
18+ type: "authentication";
19+ data: RealtimeRequestAuthenticate;
20+ }
21+
22+ type RealtimeResponseEvent<T extends unknown > = {
23+ event: string;
24+ channels: string[];
25+ timestamp: number;
26+ payload: T;
27+ }
28+
29+ type RealtimeResponseError = {
30+ code: number;
31+ message: string;
32+ }
33+
34+ type RealtimeResponseConnected = {
35+ channels: string[];
36+ user?: object;
37+ }
38+
39+ type RealtimeResponseAuthenticated = {
40+ to: string;
41+ success: boolean;
42+ user: object;
43+ }
44+
45+ type RealtimeRequestAuthenticate = {
46+ session: string;
47+ }
48+
49+ type Realtime = {
50+ socket?: WebSocket;
51+ timeout?: number;
52+ lastMessage?: RealtimeResponse;
53+ channels: {
54+ [key: string]: ((event: MessageEvent) => void)[]
55+ },
56+ createSocket: () => void;
57+ authenticate: (event: MessageEvent) => void;
58+ onMessage: <T extends unknown >(channel: string, callback: (response: RealtimeResponseEvent<T >) => void) => (event: MessageEvent) => void;
59+ }
60+
1261class {{spec .title | caseUcfirst }}Exception extends Error {
1362 code: number;
1463 response: string;
@@ -24,6 +73,7 @@ class {{spec.title | caseUcfirst}}Exception extends Error {
2473class {{ spec .title | caseUcfirst }} {
2574 config = {
2675 endpoint: '{{ spec .endpoint }}',
76+ endpointRealtime: '',
2777{% for header in spec .global .headers %}
2878 {{ header .key | caseLower }}: '',
2979{% endfor %}
@@ -46,6 +96,20 @@ class {{ spec.title | caseUcfirst }} {
4696 */
4797 setEndpoint(endpoint: string): this {
4898 this.config.endpoint = endpoint;
99+ this.config.endpointRealtime = this.config.endpointRealtime || this.config.endpoint.replace("https://", "wss://").replace("http://", "ws://");
100+
101+ return this;
102+ }
103+
104+ /**
105+ * Set Realtime Endpoint
106+ *
107+ * @param {string} endpointRealtime
108+ *
109+ * @returns {this}
110+ */
111+ setEndpointRealtime(endpointRealtime: string): this {
112+ this.config.endpointRealtime = endpointRealtime;
49113
50114 return this;
51115 }
@@ -69,6 +133,130 @@ class {{ spec.title | caseUcfirst }} {
69133 }
70134
71135{% endfor %}
136+
137+ private realtime: Realtime = {
138+ socket: undefined,
139+ timeout: undefined,
140+ channels: {},
141+ lastMessage: undefined,
142+ createSocket: () => {
143+ const channels = new URLSearchParams();
144+ channels.set('project', this.config.project);
145+ for (const property in this.realtime.channels) {
146+ channels.append('channels[]', property);
147+ }
148+ if (this.realtime.socket?.readyState === WebSocket.OPEN) {
149+ this.realtime.socket.close();
150+ }
151+
152+ this.realtime.socket = new WebSocket(this.config.endpointRealtime + '/realtime?' + channels.toString());
153+ this.realtime.socket?.addEventListener('message', this.realtime.authenticate);
154+
155+ for (const channel in this.realtime.channels) {
156+ this.realtime.channels[channel].forEach(callback => {
157+ this.realtime.socket?.addEventListener('message', callback);
158+ });
159+ }
160+
161+ this.realtime.socket.addEventListener('close', event => {
162+ if (this.realtime?.lastMessage?.type === 'error' && (<RealtimeResponseError >this.realtime?.lastMessage.data).code === 1008) {
163+ return;
164+ }
165+ console.error('Realtime got disconnected. Reconnect will be attempted in 1 second.', event.reason);
166+ setTimeout(() => {
167+ this.realtime.createSocket();
168+ }, 1000);
169+ })
170+ },
171+ authenticate: (event) => {
172+ const message: RealtimeResponse = JSON.parse(event.data);
173+ if (message.type === 'connected') {
174+ const cookie = JSON.parse(window.localStorage.getItem('cookieFallback') ?? "{}");
175+ const session = cookie?.[`a_session_${this.config.project}`];
176+ const data = <RealtimeResponseConnected >message.data;
177+
178+ if (session && !data.user) {
179+ this.realtime.socket?.send(JSON.stringify(<RealtimeRequest >{
180+ type: "authentication",
181+ data: {
182+ session
183+ }
184+ }));
185+ }
186+ }
187+ },
188+ onMessage: <T extends unknown >(channel: string, callback: (response: RealtimeResponseEvent<T >) => void) =>
189+ (event) => {
190+ try {
191+ const message: RealtimeResponse = JSON.parse(event.data);
192+ this.realtime.lastMessage = message;
193+ if (message.type === 'event') {
194+ let data = <RealtimeResponseEvent <T >>message.data;
195+ if (data.channels && data.channels.includes(channel)) {
196+ callback(data);
197+ }
198+ } else if (message.type === 'error') {
199+ throw message.data;
200+ }
201+ } catch (e) {
202+ console.error(e);
203+ }
204+ }
205+ }
206+
207+ /**
208+ * Subscribes to Appwrite events and passes you the payload in realtime.
209+ *
210+ * @param {string|string[]} channels
211+ * Channel to subscribe - pass a single channel as a string or multiple with an array of strings.
212+ *
213+ * Possible channels are:
214+ * - account
215+ * - collections
216+ * - collections.[ID]
217+ * - collections.[ID].documents
218+ * - documents
219+ * - documents.[ID]
220+ * - files
221+ * - files.[ID]
222+ * - executions
223+ * - executions.[ID]
224+ * - functions.[ID]
225+ * - teams
226+ * - teams.[ID]
227+ * - memberships
228+ * - memberships.[ID]
229+ * @param {(payload: RealtimeMessage) => void} callback Is called on every realtime update.
230+ * @returns {() => void} Unsubscribes from events.
231+ */
232+ subscribe<T extends unknown >(channels: string | string[], callback: (payload: RealtimeResponseEvent<T >) => void): () => void {
233+ let channelArray = typeof channels === 'string' ? [channels] : channels;
234+ let savedChannels: {
235+ name: string;
236+ index: number;
237+ }[] = [];
238+ channelArray.forEach((channel, index) => {
239+ if (!(channel in this.realtime.channels)) {
240+ this.realtime.channels[channel] = [];
241+ }
242+ savedChannels[index] = {
243+ name: channel,
244+ index: (this.realtime.channels[channel].push(this.realtime.onMessage<T >(channel, callback)) - 1)
245+ };
246+ clearTimeout(this.realtime.timeout);
247+ this.realtime.timeout = window?.setTimeout(() => {
248+ this.realtime.createSocket();
249+ }, 1);
250+ });
251+
252+ return () => {
253+ savedChannels.forEach(channel => {
254+ this.realtime.socket?.removeEventListener('message', this.realtime.channels[channel.name][channel.index]);
255+ this.realtime.channels[channel.name].splice(channel.index, 1);
256+ })
257+ }
258+ }
259+
72260 private async call(method: string, url: URL, headers: Headers = {}, params: Payload = {}): Promise<any > {
73261 method = method.toUpperCase();
74262 headers = {
@@ -137,7 +325,7 @@ class {{ spec.title | caseUcfirst }} {
137325
138326 return data;
139327 } catch (e) {
140- throw new {{spec .title | caseUcfirst }}Exception(e .message);
328+ throw new {{spec .title | caseUcfirst }}Exception((< Error >e) .message);
141329 }
142330 }
143331
0 commit comments