main page
This commit is contained in:
commit
7fb446c53a
8943 files changed
+1209030
No files matched your search
+442
@@ -0,0 +1,442 @@
|
||||
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': '13.0.2',
|
||||
'X-Appwrite-Response-Format': '1.4.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';
|
||||
Reference in new issue
Block a user