import { v4 as uuid } from 'uuid' import { EventPayload, MixanErrorResponse, MixanResponse, ProfilePayload, } from '@mixan/types' type MixanOptions = { url: string clientId: string clientSecret: string batchInterval?: number maxBatchSize?: number verbose?: boolean saveProfileId: (profileId: string) => void, getProfileId: () => string | null, removeProfileId: () => void, } class Fetcher { private url: string private clientId: string private clientSecret: string private logger: (...args: any[]) => void constructor(options: MixanOptions) { this.url = options.url this.clientId = options.clientId this.clientSecret = options.clientSecret this.logger = options.verbose ? console.log : () => {} } post( path: string, data: Record, options: FetchRequestInit = {} ) { const url = `${this.url}${path}` this.logger(`Mixan request: ${url}`, JSON.stringify(data, null, 2)) return fetch(url, { headers: { ['mixan-client-id']: this.clientId, ['mixan-client-secret']: this.clientSecret, 'Content-Type': 'application/json', }, method: 'POST', body: JSON.stringify(data), ...options, }) .then(async (res) => { const response = await res.json< MixanErrorResponse | MixanResponse >() if('status' in response && response.status === 'error') { this.logger(`Mixan request failed: [${options.method || 'POST'}] ${url}`, JSON.stringify(response, null, 2)) return null } return response }) .catch(() => { return null }) } } class Batcher { queue: T[] = [] timer?: Timer callback: (queue: T[]) => void maxBatchSize = 10 batchInterval = 10000 constructor(options: MixanOptions, callback: (queue: T[]) => void) { this.callback = callback if (options.maxBatchSize) { this.maxBatchSize = options.maxBatchSize } if (options.batchInterval) { this.batchInterval = options.batchInterval } } add(payload: T) { this.queue.push(payload) this.flush() } flush() { if (this.timer) { clearTimeout(this.timer) } if (this.queue.length === 0) { return } if (this.queue.length >= this.maxBatchSize) { this.send() return } this.timer = setTimeout(this.send.bind(this), this.batchInterval) } send() { this.callback(this.queue) this.queue = [] } } export class Mixan { private fetch: Fetcher private eventBatcher: Batcher private profileId?: string private options: MixanOptions private logger: (...args: any[]) => void constructor(options: MixanOptions) { this.logger = options.verbose ? console.log : () => {} this.options = options this.fetch = new Fetcher(options) this.setAnonymousUser() this.eventBatcher = new Batcher(options, (queue) => { this.fetch.post( '/events', queue.map((item) => ({ ...item, profileId: item.profileId || this.profileId || null, })) ) }) } timestamp() { return new Date().toISOString() } event(name: string, properties: Record) { this.logger('Mixan: Queue event', name) this.eventBatcher.add({ name, properties, time: this.timestamp(), profileId: this.profileId || null, }) } private setAnonymousUser() { const profileId = this.options.getProfileId() if(profileId) { this.profileId = profileId this.logger('Mixan: Use existing ID', this.profileId); } else { this.profileId = uuid() this.logger('Mixan: Create new ID', this.profileId); this.options.saveProfileId(this.profileId) this.fetch.post('/profiles', { id: this.profileId, properties: {}, }) } } async setUser(profile: ProfilePayload) { if(!this.profileId) { return this.logger('Mixan: Set user failed, no profileId'); } this.logger('Mixan: Set user', profile); await this.fetch.post(`/profiles/${this.profileId}`, profile, { method: 'PUT' }) } async setUserProperty(name: string, value: any) { if(!this.profileId) { return this.logger('Mixan: Set user property, no profileId'); } this.logger('Mixan: Set user property', name, value); await this.fetch.post(`/profiles/${this.profileId}`, { properties: { [name]: value, }, }) } async increment(name: string, value: number = 1) { if (!this.profileId) { this.logger('Mixan: Increment failed, no profileId'); return } this.logger('Mixan: Increment user property', name, value); await this.fetch.post(`/profiles/${this.profileId}/increment`, { name, value, }, { method: 'PUT' }) } async decrement(name: string, value: number = 1) { if (!this.profileId) { this.logger('Mixan: Decrement failed, no profileId'); return } this.logger('Mixan: Decrement user property', name, value); await this.fetch.post(`/profiles/${this.profileId}/decrement`, { name, value, }, { method: 'PUT' }) } async screenView(route: string, properties?: Record) { await this.event('screen_view', { ...(properties || {}), route, }) } clear() { this.logger('Mixan: Clear, send remaining events and remove profileId'); this.eventBatcher.send() this.options.removeProfileId() this.profileId = undefined this.setAnonymousUser() } }