init
This commit is contained in:
191
packages/sdk/index.ts
Normal file
191
packages/sdk/index.ts
Normal 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
13
packages/sdk/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
25
packages/sdk/tsconfig.json
Normal file
25
packages/sdk/tsconfig.json
Normal 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
|
||||
],
|
||||
}
|
||||
}
|
||||
10
packages/sdk/tsup.config.ts
Normal file
10
packages/sdk/tsup.config.ts
Normal 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
15
packages/types/README.md
Normal 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
74
packages/types/index.ts
Normal 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'
|
||||
}
|
||||
12
packages/types/package.json
Normal file
12
packages/types/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
22
packages/types/tsconfig.json
Normal file
22
packages/types/tsconfig.json
Normal 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
|
||||
]
|
||||
}
|
||||
}
|
||||
10
packages/types/tsup.config.ts
Normal file
10
packages/types/tsup.config.ts
Normal 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,
|
||||
});
|
||||
Reference in New Issue
Block a user