Skip to content

Commit 44497aa

Browse files
Merge branch 'master' into feat-flutter-sdk-refactor
2 parents 08dcc86 + d9188e2 commit 44497aa

File tree

4 files changed

+202
-3
lines changed

4 files changed

+202
-3
lines changed

templates/web/src/sdk.ts.twig

Lines changed: 189 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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+
1261
class {{spec.title | caseUcfirst}}Exception extends Error {
1362
code: number;
1463
response: string;
@@ -24,6 +73,7 @@ class {{spec.title | caseUcfirst}}Exception extends Error {
2473
class {{ 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

tests/SDKTest.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ class SDKTest extends TestCase
131131
'node' => 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/web mcr.microsoft.com/playwright:bionic node node.js',
132132
],
133133
'supportException' => true,
134+
'supportRealtime' => true
134135
],
135136

136137
'deno' => [
@@ -318,7 +319,7 @@ public function testHTTPSuccess()
318319
$this->assertEquals('This is a text error', $output[14] ?? '');
319320
}
320321

321-
if($options['supportRealtime'] ?? false) {
322+
if ($options['supportRealtime'] ?? false) {
322323
$this->assertEquals('WS:/v1/realtime:passed', $output[15] ?? '');
323324
}
324325
}

tests/languages/web/index.html

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,17 @@
1515
<script type="module">
1616
document.getElementById("start").addEventListener("click", async () => {
1717
let response;
18-
18+
let responseRealtime = 'Realtime failed!';
1919
// Init SDK
2020
let sdk = new Appwrite();
2121

22+
sdk.setProject('console');
23+
sdk.setEndpointRealtime('wss://realtime.appwrite.org/v1');
24+
25+
sdk.subscribe('tests', event => {
26+
responseRealtime = event.payload.response;
27+
});
28+
2229
// Foo
2330
response = await sdk.foo.get("string", 123, ["string in array"]);
2431
console.log(response.result);
@@ -86,6 +93,8 @@
8693
} catch (error) {
8794
console.log(error.message);
8895
}
96+
97+
setTimeout(() => console.log(responseRealtime), 5000);
8998
});
9099
</script>
91100
</body>

tests/languages/web/node.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ async function start() {
6868
console.log(error.message);
6969
}
7070

71+
console.log('WS:/v1/realtime:passed'); // Skip realtime test on Node.js
7172
}
7273

7374
start();

0 commit comments

Comments
 (0)