This commit is contained in:
Carl-Gerhard Lindesvärd
2023-10-11 12:34:35 +02:00
commit 903fd155c3
40 changed files with 1385 additions and 0 deletions

191
packages/sdk/index.ts Normal file
View File

@@ -0,0 +1,191 @@
import {
EventPayload,
MixanErrorResponse,
MixanIssuesResponse,
MixanResponse,
ProfilePayload,
} from '@mixan/types'
type MixanOptions = {
url: string
clientSecret: string
batchInterval?: number
maxBatchSize?: number
verbose?: boolean
}
class Fetcher {
private url: string
private clientSecret: string
private logger: (...args: any[]) => void
constructor(options: MixanOptions) {
this.url = options.url
this.clientSecret = options.clientSecret
this.logger = options.verbose ? console.log : () => {}
}
post(path: string, data: Record<string, any>) {
const url = `${this.url}${path}`
this.logger(`Mixan request: ${url}`, JSON.stringify(data, null, 2))
return fetch(url, {
headers: {
['mixan-client-secret']: this.clientSecret,
'Content-Type': 'application/json',
},
method: 'POST',
body: JSON.stringify(data),
})
.then(async (res) => {
const response = await res.json<
MixanIssuesResponse | MixanErrorResponse | MixanResponse<unknown>
>()
if ('status' in response && response.status === 'ok') {
return response
}
if ('code' in response) {
this.logger(`Mixan error: [${response.code}] ${response.message}`)
return null
}
if ('issues' in response) {
this.logger(`Mixan issues:`)
response.issues.forEach((issue) => {
this.logger(` - ${issue.message} (${issue.value})`)
})
return null
}
return null
})
.catch(() => {
return null
})
}
}
class Batcher<T extends any> {
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<EventPayload>
private profile: ProfilePayload | null = null
constructor(options: MixanOptions) {
this.fetch = new Fetcher(options)
this.eventBatcher = new Batcher(options, (queue) => {
this.fetch.post(
'/events',
queue.map((item) => ({
...item,
externalId: item.externalId || this.profile?.id,
}))
)
})
}
timestamp() {
return new Date().toISOString()
}
event(name: string, properties: Record<string, any>) {
this.eventBatcher.add({
name,
properties,
time: this.timestamp(),
externalId: this.profile?.id || null,
})
}
async setUser(profile: ProfilePayload) {
this.profile = profile
await this.fetch.post('/profiles', profile)
}
async setUserProperty(name: string, value: any) {
await this.fetch.post('/profiles', {
...this.profile,
properties: {
[name]: value,
},
})
}
async increment(name: string, value: number = 1) {
if (!this.profile) {
return
}
await this.fetch.post('/profiles/increment', {
id: this.profile.id,
name,
value,
})
}
async decrement(name: string, value: number = 1) {
if (!this.profile) {
return
}
await this.fetch.post('/profiles/decrement', {
id: this.profile.id,
name,
value,
})
}
screenView(route: string, properties?: Record<string, any>) {
this.event('screen_view', {
...(properties || {}),
route,
})
}
}

13
packages/sdk/package.json Normal file
View File

@@ -0,0 +1,13 @@
{
"name": "@mixan/sdk",
"version": "0.0.1",
"type": "module",
"module": "index.ts",
"devDependencies": {
"@mixan/types": "workspace:*",
"bun-types": "latest"
},
"dependencies": {
"typescript": "^5.0.0"
}
}

View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"lib": ["ESNext"],
"module": "esnext",
"target": "esnext",
"moduleResolution": "bundler",
"moduleDetection": "force",
"composite": true,
"strict": true,
"downlevelIteration": true,
"skipLibCheck": true,
"jsx": "react-jsx",
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"allowJs": true,
"outDir": "dist",
"allowImportingTsExtensions": false,
"noEmit": false,
"types": [
"bun-types" // add Bun global
],
}
}

View File

@@ -0,0 +1,10 @@
import { defineConfig } from "tsup";
export default defineConfig({
entry: ["index.ts"],
format: ["cjs", "esm"], // Build for commonJS and ESmodules
dts: true, // Generate declaration file (.d.ts)
splitting: false,
sourcemap: true,
clean: true,
});

15
packages/types/README.md Normal file
View File

@@ -0,0 +1,15 @@
# types
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run index.ts
```
This project was created using `bun init` in bun v1.0.4. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.

74
packages/types/index.ts Normal file
View File

@@ -0,0 +1,74 @@
export type MixanJson = Record<string, any>
export type EventPayload = {
name: string
time: string
externalId: string | null
properties: MixanJson
}
export type ProfilePayload = {
first_name?: string
last_name?: string
email?: string
avatar?: string
id: string
properties: MixanJson
}
export type ProfileIncrementPayload = {
name: string
value: number
id: string
}
export type ProfileDecrementPayload = {
name: string
value: number
id: string
}
// Batching
export type BatchEvent = {
type: 'event',
payload: EventPayload
}
export type BatchProfile = {
type: 'profile',
payload: ProfilePayload
}
export type BatchProfileIncrement = {
type: 'profile_increment',
payload: ProfileIncrementPayload
}
export type BatchProfileDecrement = {
type: 'profile_decrement',
payload: ProfileDecrementPayload
}
export type BatchItem = BatchEvent | BatchProfile | BatchProfileIncrement | BatchProfileDecrement
export type BatchPayload = Array<BatchItem>
export type MixanIssue = {
field: string
message: string
value: any
}
export type MixanIssuesResponse = {
issues: Array<MixanIssue>,
}
export type MixanErrorResponse = {
code: string
message: string
}
export type MixanResponse<T> = {
result: T
status: 'ok'
}

View File

@@ -0,0 +1,12 @@
{
"name": "@mixan/types",
"version": "0.0.1",
"type": "module",
"module": "index.ts",
"devDependencies": {
"bun-types": "latest"
},
"dependencies": {
"typescript": "^5.0.0"
}
}

View File

@@ -0,0 +1,22 @@
{
"compilerOptions": {
"lib": ["ESNext"],
"module": "esnext",
"target": "esnext",
"moduleResolution": "bundler",
"moduleDetection": "force",
"allowImportingTsExtensions": true,
"noEmit": true,
"composite": true,
"strict": true,
"downlevelIteration": true,
"skipLibCheck": true,
"jsx": "react-jsx",
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"allowJs": true,
"types": [
"bun-types" // add Bun global
]
}
}

View File

@@ -0,0 +1,10 @@
import { defineConfig } from "tsup";
export default defineConfig({
entry: ["index.ts"],
format: ["cjs", "esm"], // Build for commonJS and ESmodules
dts: true, // Generate declaration file (.d.ts)
splitting: false,
sourcemap: false,
clean: true,
});