1
0
Fork 0
mirror of synced 2024-06-01 18:39:57 +12:00
appwrite/public/sdk-project/client.ts
2022-09-20 14:24:38 +05:30

443 lines
14 KiB
TypeScript

import 'isomorphic-form-data';
import { fetch } from 'cross-fetch';
import { Models } from './models';
import { Service } from './service';
type Payload = {
[key: string]: any;
}
type Headers = {
[key: string]: string;
}
type RealtimeResponse = {
type: 'error' | 'event' | 'connected' | 'response';
data: RealtimeResponseAuthenticated | RealtimeResponseConnected | RealtimeResponseError | RealtimeResponseEvent<unknown>;
}
type RealtimeRequest = {
type: 'authentication';
data: RealtimeRequestAuthenticate;
}
export type RealtimeResponseEvent<T extends unknown> = {
events: string[];
channels: string[];
timestamp: number;
payload: T;
}
type RealtimeResponseError = {
code: number;
message: string;
}
type RealtimeResponseConnected = {
channels: string[];
user?: object;
}
type RealtimeResponseAuthenticated = {
to: string;
success: boolean;
user: object;
}
type RealtimeRequestAuthenticate = {
session: string;
}
type Realtime = {
socket?: WebSocket;
timeout?: number;
url?: string;
lastMessage?: RealtimeResponse;
channels: Set<string>;
subscriptions: Map<number, {
channels: string[];
callback: (payload: RealtimeResponseEvent<any>) => void
}>;
subscriptionsCounter: number;
reconnect: boolean;
reconnectAttempts: number;
getTimeout: () => number;
connect: () => void;
createSocket: () => void;
cleanUp: (channels: string[]) => void;
onMessage: (event: MessageEvent) => void;
}
export type UploadProgress = {
$id: string;
progress: number;
sizeUploaded: number;
chunksTotal: number;
chunksUploaded: number;
}
class AppwriteException extends Error {
code: number;
response: string;
type: string;
constructor(message: string, code: number = 0, type: string = '', response: string = '') {
super(message);
this.name = 'AppwriteException';
this.message = message;
this.code = code;
this.type = type;
this.response = response;
}
}
class Client {
config = {
endpoint: 'https://HOSTNAME/v1',
endpointRealtime: '',
project: '',
jwt: '',
locale: '',
};
headers: Headers = {
'x-sdk-name': 'Web',
'x-sdk-platform': 'client',
'x-sdk-language': 'web',
'x-sdk-version': '10.0.1',
'X-Appwrite-Response-Format': '1.0.0',
};
/**
* Set Endpoint
*
* Your project endpoint
*
* @param {string} endpoint
*
* @returns {this}
*/
setEndpoint(endpoint: string): this {
this.config.endpoint = endpoint;
this.config.endpointRealtime = this.config.endpointRealtime || this.config.endpoint.replace('https://', 'wss://').replace('http://', 'ws://');
return this;
}
/**
* Set Realtime Endpoint
*
* @param {string} endpointRealtime
*
* @returns {this}
*/
setEndpointRealtime(endpointRealtime: string): this {
this.config.endpointRealtime = endpointRealtime;
return this;
}
/**
* Set Project
*
* Your project ID
*
* @param value string
*
* @return {this}
*/
setProject(value: string): this {
this.headers['X-Appwrite-Project'] = value;
this.config.project = value;
return this;
}
/**
* Set JWT
*
* Your secret JSON Web Token
*
* @param value string
*
* @return {this}
*/
setJWT(value: string): this {
this.headers['X-Appwrite-JWT'] = value;
this.config.jwt = value;
return this;
}
/**
* Set Locale
*
* @param value string
*
* @return {this}
*/
setLocale(value: string): this {
this.headers['X-Appwrite-Locale'] = value;
this.config.locale = value;
return this;
}
private realtime: Realtime = {
socket: undefined,
timeout: undefined,
url: '',
channels: new Set(),
subscriptions: new Map(),
subscriptionsCounter: 0,
reconnect: true,
reconnectAttempts: 0,
lastMessage: undefined,
connect: () => {
clearTimeout(this.realtime.timeout);
this.realtime.timeout = window?.setTimeout(() => {
this.realtime.createSocket();
}, 50);
},
getTimeout: () => {
switch (true) {
case this.realtime.reconnectAttempts < 5:
return 1000;
case this.realtime.reconnectAttempts < 15:
return 5000;
case this.realtime.reconnectAttempts < 100:
return 10_000;
default:
return 60_000;
}
},
createSocket: () => {
if (this.realtime.channels.size < 1) return;
const channels = new URLSearchParams();
channels.set('project', this.config.project);
this.realtime.channels.forEach(channel => {
channels.append('channels[]', channel);
});
const url = this.config.endpointRealtime + '/realtime?' + channels.toString();
if (
url !== this.realtime.url || // Check if URL is present
!this.realtime.socket || // Check if WebSocket has not been created
this.realtime.socket?.readyState > WebSocket.OPEN // Check if WebSocket is CLOSING (3) or CLOSED (4)
) {
if (
this.realtime.socket &&
this.realtime.socket?.readyState < WebSocket.CLOSING // Close WebSocket if it is CONNECTING (0) or OPEN (1)
) {
this.realtime.reconnect = false;
this.realtime.socket.close();
}
this.realtime.url = url;
this.realtime.socket = new WebSocket(url);
this.realtime.socket.addEventListener('message', this.realtime.onMessage);
this.realtime.socket.addEventListener('open', _event => {
this.realtime.reconnectAttempts = 0;
});
this.realtime.socket.addEventListener('close', event => {
if (
!this.realtime.reconnect ||
(
this.realtime?.lastMessage?.type === 'error' && // Check if last message was of type error
(<RealtimeResponseError>this.realtime?.lastMessage.data).code === 1008 // Check for policy violation 1008
)
) {
this.realtime.reconnect = true;
return;
}
const timeout = this.realtime.getTimeout();
console.error(`Realtime got disconnected. Reconnect will be attempted in ${timeout / 1000} seconds.`, event.reason);
setTimeout(() => {
this.realtime.reconnectAttempts++;
this.realtime.createSocket();
}, timeout);
})
}
},
onMessage: (event) => {
try {
const message: RealtimeResponse = JSON.parse(event.data);
this.realtime.lastMessage = message;
switch (message.type) {
case 'connected':
const cookie = JSON.parse(window.localStorage.getItem('cookieFallback') ?? '{}');
const session = cookie?.[`a_session_${this.config.project}`];
const messageData = <RealtimeResponseConnected>message.data;
if (session && !messageData.user) {
this.realtime.socket?.send(JSON.stringify(<RealtimeRequest>{
type: 'authentication',
data: {
session
}
}));
}
break;
case 'event':
let data = <RealtimeResponseEvent<unknown>>message.data;
if (data?.channels) {
const isSubscribed = data.channels.some(channel => this.realtime.channels.has(channel));
if (!isSubscribed) return;
this.realtime.subscriptions.forEach(subscription => {
if (data.channels.some(channel => subscription.channels.includes(channel))) {
setTimeout(() => subscription.callback(data));
}
})
}
break;
case 'error':
throw message.data;
default:
break;
}
} catch (e) {
console.error(e);
}
},
cleanUp: channels => {
this.realtime.channels.forEach(channel => {
if (channels.includes(channel)) {
let found = Array.from(this.realtime.subscriptions).some(([_key, subscription] )=> {
return subscription.channels.includes(channel);
})
if (!found) {
this.realtime.channels.delete(channel);
}
}
})
}
}
/**
* Subscribes to Appwrite events and passes you the payload in realtime.
*
* @param {string|string[]} channels
* Channel to subscribe - pass a single channel as a string or multiple with an array of strings.
*
* Possible channels are:
* - account
* - collections
* - collections.[ID]
* - collections.[ID].documents
* - documents
* - documents.[ID]
* - files
* - files.[ID]
* - executions
* - executions.[ID]
* - functions.[ID]
* - teams
* - teams.[ID]
* - memberships
* - memberships.[ID]
* @param {(payload: RealtimeMessage) => void} callback Is called on every realtime update.
* @returns {() => void} Unsubscribes from events.
*/
subscribe<T extends unknown>(channels: string | string[], callback: (payload: RealtimeResponseEvent<T>) => void): () => void {
let channelArray = typeof channels === 'string' ? [channels] : channels;
channelArray.forEach(channel => this.realtime.channels.add(channel));
const counter = this.realtime.subscriptionsCounter++;
this.realtime.subscriptions.set(counter, {
channels: channelArray,
callback
});
this.realtime.connect();
return () => {
this.realtime.subscriptions.delete(counter);
this.realtime.cleanUp(channelArray);
this.realtime.connect();
}
}
async call(method: string, url: URL, headers: Headers = {}, params: Payload = {}): Promise<any> {
method = method.toUpperCase();
headers = Object.assign({}, this.headers, headers);
let options: RequestInit = {
method,
headers,
credentials: 'include'
};
if (typeof window !== 'undefined' && window.localStorage) {
headers['X-Fallback-Cookies'] = window.localStorage.getItem('cookieFallback') ?? '';
}
if (method === 'GET') {
for (const [key, value] of Object.entries(Service.flatten(params))) {
url.searchParams.append(key, value);
}
} else {
switch (headers['content-type']) {
case 'application/json':
options.body = JSON.stringify(params);
break;
case 'multipart/form-data':
let formData = new FormData();
for (const key in params) {
if (Array.isArray(params[key])) {
params[key].forEach((value: any) => {
formData.append(key + '[]', value);
})
} else {
formData.append(key, params[key]);
}
}
options.body = formData;
delete headers['content-type'];
break;
}
}
try {
let data = null;
const response = await fetch(url.toString(), options);
if (response.headers.get('content-type')?.includes('application/json')) {
data = await response.json();
} else {
data = {
message: await response.text()
};
}
if (400 <= response.status) {
throw new AppwriteException(data?.message, response.status, data?.type, data);
}
const cookieFallback = response.headers.get('X-Fallback-Cookies');
if (typeof window !== 'undefined' && window.localStorage && cookieFallback) {
window.console.warn('Appwrite is using localStorage for session management. Increase your security by adding a custom domain as your API endpoint.');
window.localStorage.setItem('cookieFallback', cookieFallback);
}
return data;
} catch (e) {
if (e instanceof AppwriteException) {
throw e;
}
throw new AppwriteException((<Error>e).message);
}
}
}
export { Client, AppwriteException };
export { Query } from './query';
export type { Models, Payload };
export type { QueryTypes, QueryTypesList } from './query';