@customerio/cdp-analytics-core 0.0.0-test-oidc
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.MD +22 -0
- package/README.md +3 -0
- package/dist/cjs/analytics/dispatch.js +54 -0
- package/dist/cjs/analytics/dispatch.js.map +1 -0
- package/dist/cjs/analytics/index.js +3 -0
- package/dist/cjs/analytics/index.js.map +1 -0
- package/dist/cjs/callback/index.js +46 -0
- package/dist/cjs/callback/index.js.map +1 -0
- package/dist/cjs/connection/index.js +16 -0
- package/dist/cjs/connection/index.js.map +1 -0
- package/dist/cjs/context/index.js +87 -0
- package/dist/cjs/context/index.js.map +1 -0
- package/dist/cjs/emitter/index.js +66 -0
- package/dist/cjs/emitter/index.js.map +1 -0
- package/dist/cjs/emitter/interface.js +3 -0
- package/dist/cjs/emitter/interface.js.map +1 -0
- package/dist/cjs/events/index.js +154 -0
- package/dist/cjs/events/index.js.map +1 -0
- package/dist/cjs/events/interfaces.js +3 -0
- package/dist/cjs/events/interfaces.js.map +1 -0
- package/dist/cjs/index.js +25 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/logger/index.js +62 -0
- package/dist/cjs/logger/index.js.map +1 -0
- package/dist/cjs/plugins/index.js +3 -0
- package/dist/cjs/plugins/index.js.map +1 -0
- package/dist/cjs/priority-queue/backoff.js +10 -0
- package/dist/cjs/priority-queue/backoff.js.map +1 -0
- package/dist/cjs/priority-queue/index.js +92 -0
- package/dist/cjs/priority-queue/index.js.map +1 -0
- package/dist/cjs/queue/delivery.js +69 -0
- package/dist/cjs/queue/delivery.js.map +1 -0
- package/dist/cjs/queue/event-queue.js +340 -0
- package/dist/cjs/queue/event-queue.js.map +1 -0
- package/dist/cjs/stats/index.js +96 -0
- package/dist/cjs/stats/index.js.map +1 -0
- package/dist/cjs/task/task-group.js +24 -0
- package/dist/cjs/task/task-group.js.map +1 -0
- package/dist/cjs/user/index.js +3 -0
- package/dist/cjs/user/index.js.map +1 -0
- package/dist/cjs/utils/bind-all.js +18 -0
- package/dist/cjs/utils/bind-all.js.map +1 -0
- package/dist/cjs/utils/environment.js +12 -0
- package/dist/cjs/utils/environment.js.map +1 -0
- package/dist/cjs/utils/get-global.js +21 -0
- package/dist/cjs/utils/get-global.js.map +1 -0
- package/dist/cjs/utils/group-by.js +28 -0
- package/dist/cjs/utils/group-by.js.map +1 -0
- package/dist/cjs/utils/has-properties.js +13 -0
- package/dist/cjs/utils/has-properties.js.map +1 -0
- package/dist/cjs/utils/is-plain-object.js +28 -0
- package/dist/cjs/utils/is-plain-object.js.map +1 -0
- package/dist/cjs/utils/is-thenable.js +15 -0
- package/dist/cjs/utils/is-thenable.js.map +1 -0
- package/dist/cjs/utils/p-while.js +25 -0
- package/dist/cjs/utils/p-while.js.map +1 -0
- package/dist/cjs/utils/pick.js +10 -0
- package/dist/cjs/utils/pick.js.map +1 -0
- package/dist/cjs/utils/ts-helpers.js +3 -0
- package/dist/cjs/utils/ts-helpers.js.map +1 -0
- package/dist/cjs/validation/assertions.js +51 -0
- package/dist/cjs/validation/assertions.js.map +1 -0
- package/dist/cjs/validation/helpers.js +26 -0
- package/dist/cjs/validation/helpers.js.map +1 -0
- package/dist/esm/analytics/dispatch.js +49 -0
- package/dist/esm/analytics/dispatch.js.map +1 -0
- package/dist/esm/analytics/index.js +2 -0
- package/dist/esm/analytics/index.js.map +1 -0
- package/dist/esm/callback/index.js +40 -0
- package/dist/esm/callback/index.js.map +1 -0
- package/dist/esm/connection/index.js +11 -0
- package/dist/esm/connection/index.js.map +1 -0
- package/dist/esm/context/index.js +84 -0
- package/dist/esm/context/index.js.map +1 -0
- package/dist/esm/emitter/index.js +63 -0
- package/dist/esm/emitter/index.js.map +1 -0
- package/dist/esm/emitter/interface.js +2 -0
- package/dist/esm/emitter/interface.js.map +1 -0
- package/dist/esm/events/index.js +151 -0
- package/dist/esm/events/index.js.map +1 -0
- package/dist/esm/events/interfaces.js +2 -0
- package/dist/esm/events/interfaces.js.map +1 -0
- package/dist/esm/index.js +19 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/logger/index.js +59 -0
- package/dist/esm/logger/index.js.map +1 -0
- package/dist/esm/plugins/index.js +2 -0
- package/dist/esm/plugins/index.js.map +1 -0
- package/dist/esm/priority-queue/backoff.js +6 -0
- package/dist/esm/priority-queue/backoff.js.map +1 -0
- package/dist/esm/priority-queue/index.js +89 -0
- package/dist/esm/priority-queue/index.js.map +1 -0
- package/dist/esm/queue/delivery.js +64 -0
- package/dist/esm/queue/delivery.js.map +1 -0
- package/dist/esm/queue/event-queue.js +337 -0
- package/dist/esm/queue/event-queue.js.map +1 -0
- package/dist/esm/stats/index.js +93 -0
- package/dist/esm/stats/index.js.map +1 -0
- package/dist/esm/task/task-group.js +20 -0
- package/dist/esm/task/task-group.js.map +1 -0
- package/dist/esm/user/index.js +2 -0
- package/dist/esm/user/index.js.map +1 -0
- package/dist/esm/utils/bind-all.js +14 -0
- package/dist/esm/utils/bind-all.js.map +1 -0
- package/dist/esm/utils/environment.js +7 -0
- package/dist/esm/utils/environment.js.map +1 -0
- package/dist/esm/utils/get-global.js +17 -0
- package/dist/esm/utils/get-global.js.map +1 -0
- package/dist/esm/utils/group-by.js +24 -0
- package/dist/esm/utils/group-by.js.map +1 -0
- package/dist/esm/utils/has-properties.js +9 -0
- package/dist/esm/utils/has-properties.js.map +1 -0
- package/dist/esm/utils/is-plain-object.js +24 -0
- package/dist/esm/utils/is-plain-object.js.map +1 -0
- package/dist/esm/utils/is-thenable.js +11 -0
- package/dist/esm/utils/is-thenable.js.map +1 -0
- package/dist/esm/utils/p-while.js +21 -0
- package/dist/esm/utils/p-while.js.map +1 -0
- package/dist/esm/utils/pick.js +6 -0
- package/dist/esm/utils/pick.js.map +1 -0
- package/dist/esm/utils/ts-helpers.js +2 -0
- package/dist/esm/utils/ts-helpers.js.map +1 -0
- package/dist/esm/validation/assertions.js +46 -0
- package/dist/esm/validation/assertions.js.map +1 -0
- package/dist/esm/validation/helpers.js +18 -0
- package/dist/esm/validation/helpers.js.map +1 -0
- package/dist/types/analytics/dispatch.d.ts +20 -0
- package/dist/types/analytics/dispatch.d.ts.map +1 -0
- package/dist/types/analytics/index.d.ts +12 -0
- package/dist/types/analytics/index.d.ts.map +1 -0
- package/dist/types/callback/index.d.ts +11 -0
- package/dist/types/callback/index.d.ts.map +1 -0
- package/dist/types/connection/index.d.ts +3 -0
- package/dist/types/connection/index.d.ts.map +1 -0
- package/dist/types/context/index.d.ts +44 -0
- package/dist/types/context/index.d.ts.map +1 -0
- package/dist/types/emitter/index.d.ts +25 -0
- package/dist/types/emitter/index.d.ts.map +1 -0
- package/dist/types/emitter/interface.d.ts +27 -0
- package/dist/types/emitter/interface.d.ts.map +1 -0
- package/dist/types/events/index.d.ts +31 -0
- package/dist/types/events/index.d.ts.map +1 -0
- package/dist/types/events/interfaces.d.ts +379 -0
- package/dist/types/events/interfaces.d.ts.map +1 -0
- package/dist/types/index.d.ts +19 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/logger/index.d.ts +19 -0
- package/dist/types/logger/index.d.ts.map +1 -0
- package/dist/types/plugins/index.d.ts +25 -0
- package/dist/types/plugins/index.d.ts.map +1 -0
- package/dist/types/priority-queue/backoff.d.ts +13 -0
- package/dist/types/priority-queue/backoff.d.ts.map +1 -0
- package/dist/types/priority-queue/index.d.ts +25 -0
- package/dist/types/priority-queue/index.d.ts.map +1 -0
- package/dist/types/queue/delivery.d.ts +5 -0
- package/dist/types/queue/delivery.d.ts.map +1 -0
- package/dist/types/queue/event-queue.d.ts +43 -0
- package/dist/types/queue/event-queue.d.ts.map +1 -0
- package/dist/types/stats/index.d.ts +34 -0
- package/dist/types/stats/index.d.ts.map +1 -0
- package/dist/types/task/task-group.d.ts +6 -0
- package/dist/types/task/task-group.d.ts.map +1 -0
- package/dist/types/user/index.d.ts +6 -0
- package/dist/types/user/index.d.ts.map +1 -0
- package/dist/types/utils/bind-all.d.ts +4 -0
- package/dist/types/utils/bind-all.d.ts.map +1 -0
- package/dist/types/utils/environment.d.ts +3 -0
- package/dist/types/utils/environment.d.ts.map +1 -0
- package/dist/types/utils/get-global.d.ts +2 -0
- package/dist/types/utils/get-global.d.ts.map +1 -0
- package/dist/types/utils/group-by.d.ts +4 -0
- package/dist/types/utils/group-by.d.ts.map +1 -0
- package/dist/types/utils/has-properties.d.ts +4 -0
- package/dist/types/utils/has-properties.d.ts.map +1 -0
- package/dist/types/utils/is-plain-object.d.ts +2 -0
- package/dist/types/utils/is-plain-object.d.ts.map +1 -0
- package/dist/types/utils/is-thenable.d.ts +6 -0
- package/dist/types/utils/is-thenable.d.ts.map +1 -0
- package/dist/types/utils/p-while.d.ts +2 -0
- package/dist/types/utils/p-while.d.ts.map +1 -0
- package/dist/types/utils/pick.d.ts +2 -0
- package/dist/types/utils/pick.d.ts.map +1 -0
- package/dist/types/utils/ts-helpers.d.ts +13 -0
- package/dist/types/utils/ts-helpers.d.ts.map +1 -0
- package/dist/types/validation/assertions.d.ts +8 -0
- package/dist/types/validation/assertions.d.ts.map +1 -0
- package/dist/types/validation/helpers.d.ts +7 -0
- package/dist/types/validation/helpers.d.ts.map +1 -0
- package/package.json +40 -0
- package/src/analytics/dispatch.ts +58 -0
- package/src/analytics/index.ts +11 -0
- package/src/callback/index.ts +51 -0
- package/src/connection/index.ts +13 -0
- package/src/context/index.ts +123 -0
- package/src/emitter/index.ts +65 -0
- package/src/emitter/interface.ts +31 -0
- package/src/events/index.ts +285 -0
- package/src/events/interfaces.ts +453 -0
- package/src/index.ts +18 -0
- package/src/logger/index.ts +74 -0
- package/src/plugins/index.ts +43 -0
- package/src/priority-queue/backoff.ts +24 -0
- package/src/priority-queue/index.ts +103 -0
- package/src/queue/delivery.ts +73 -0
- package/src/queue/event-queue.ts +320 -0
- package/src/stats/index.ts +88 -0
- package/src/task/task-group.ts +31 -0
- package/src/user/index.ts +7 -0
- package/src/utils/bind-all.ts +19 -0
- package/src/utils/environment.ts +7 -0
- package/src/utils/get-global.ts +16 -0
- package/src/utils/group-by.ts +30 -0
- package/src/utils/has-properties.ts +7 -0
- package/src/utils/is-plain-object.ts +26 -0
- package/src/utils/is-thenable.ts +9 -0
- package/src/utils/p-while.ts +12 -0
- package/src/utils/pick.ts +8 -0
- package/src/utils/ts-helpers.ts +13 -0
- package/src/validation/assertions.ts +54 -0
- package/src/validation/helpers.ts +27 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export * from './emitter'
|
|
2
|
+
export * from './emitter/interface'
|
|
3
|
+
export * from './plugins'
|
|
4
|
+
export * from './events/interfaces'
|
|
5
|
+
export * from './events'
|
|
6
|
+
export * from './callback'
|
|
7
|
+
export * from './priority-queue'
|
|
8
|
+
export { backoff } from './priority-queue/backoff'
|
|
9
|
+
export * from './context'
|
|
10
|
+
export * from './queue/event-queue'
|
|
11
|
+
export * from './analytics'
|
|
12
|
+
export * from './analytics/dispatch'
|
|
13
|
+
export * from './validation/helpers'
|
|
14
|
+
export * from './validation/assertions'
|
|
15
|
+
export * from './utils/bind-all'
|
|
16
|
+
export * from './stats'
|
|
17
|
+
export { CoreLogger } from './logger'
|
|
18
|
+
export * from './queue/delivery'
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error'
|
|
2
|
+
export type LogMessage = {
|
|
3
|
+
level: LogLevel
|
|
4
|
+
message: string
|
|
5
|
+
time?: Date
|
|
6
|
+
extras?: Record<string, any>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface GenericLogger {
|
|
10
|
+
log(level: LogLevel, message: string, extras?: object): void
|
|
11
|
+
flush(): void
|
|
12
|
+
logs: LogMessage[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class CoreLogger implements GenericLogger {
|
|
16
|
+
private _logs: LogMessage[] = []
|
|
17
|
+
|
|
18
|
+
log(level: LogLevel, message: string, extras?: object) {
|
|
19
|
+
const time = new Date()
|
|
20
|
+
this._logs.push({
|
|
21
|
+
level,
|
|
22
|
+
message,
|
|
23
|
+
time,
|
|
24
|
+
extras,
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public get logs(): LogMessage[] {
|
|
29
|
+
return this._logs
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public flush(): void {
|
|
33
|
+
if (this.logs.length > 1) {
|
|
34
|
+
const formatted = this._logs.reduce((logs, log) => {
|
|
35
|
+
const line = {
|
|
36
|
+
...log,
|
|
37
|
+
json: JSON.stringify(log.extras, null, ' '),
|
|
38
|
+
extras: log.extras,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
delete line['time']
|
|
42
|
+
|
|
43
|
+
let key = log.time?.toISOString() ?? ''
|
|
44
|
+
if (logs[key]) {
|
|
45
|
+
key = `${key}-${Math.random()}`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
...logs,
|
|
50
|
+
[key]: line,
|
|
51
|
+
}
|
|
52
|
+
}, {} as Record<string, LogMessage>)
|
|
53
|
+
|
|
54
|
+
// ie doesn't like console.table
|
|
55
|
+
if (console.table) {
|
|
56
|
+
console.table(formatted)
|
|
57
|
+
} else {
|
|
58
|
+
console.log(formatted)
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
this.logs.forEach((logEntry) => {
|
|
62
|
+
const { level, message, extras } = logEntry
|
|
63
|
+
|
|
64
|
+
if (level === 'info' || level === 'debug') {
|
|
65
|
+
console.log(message, extras ?? '')
|
|
66
|
+
} else {
|
|
67
|
+
console[level](message, extras ?? '')
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
this._logs = []
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { CoreAnalytics } from '../analytics'
|
|
2
|
+
import type { CoreContext } from '../context'
|
|
3
|
+
|
|
4
|
+
interface CorePluginConfig {
|
|
5
|
+
options: any
|
|
6
|
+
priority: 'critical' | 'non-critical' // whether AJS should expect this plugin to be loaded before starting event delivery
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type PluginType =
|
|
10
|
+
| 'before'
|
|
11
|
+
| 'after'
|
|
12
|
+
| 'destination'
|
|
13
|
+
| 'enrichment'
|
|
14
|
+
| 'utility'
|
|
15
|
+
|
|
16
|
+
// enrichment - modifies the event. Enrichment can happen in parallel, by reducing all changes in the final event. Failures in this stage could halt event delivery.
|
|
17
|
+
// destination - runs in parallel at the end of the lifecycle. Cannot modify the event, can fail and not halt execution.
|
|
18
|
+
// utility - do not affect lifecycle. Should be run and executed once. Their `track/identify` calls don't really do anything. example
|
|
19
|
+
|
|
20
|
+
export interface CorePlugin<
|
|
21
|
+
Ctx extends CoreContext = CoreContext,
|
|
22
|
+
Analytics extends CoreAnalytics = any
|
|
23
|
+
> {
|
|
24
|
+
name: string
|
|
25
|
+
alternativeNames?: string[]
|
|
26
|
+
version: string
|
|
27
|
+
type: PluginType
|
|
28
|
+
isLoaded: () => boolean
|
|
29
|
+
load: (
|
|
30
|
+
ctx: Ctx,
|
|
31
|
+
instance: Analytics,
|
|
32
|
+
config?: CorePluginConfig
|
|
33
|
+
) => Promise<unknown>
|
|
34
|
+
|
|
35
|
+
unload?: (ctx: Ctx, instance: Analytics) => Promise<unknown> | unknown
|
|
36
|
+
ready?: () => Promise<unknown>
|
|
37
|
+
track?: (ctx: Ctx) => Promise<Ctx> | Ctx
|
|
38
|
+
identify?: (ctx: Ctx) => Promise<Ctx> | Ctx
|
|
39
|
+
page?: (ctx: Ctx) => Promise<Ctx> | Ctx
|
|
40
|
+
group?: (ctx: Ctx) => Promise<Ctx> | Ctx
|
|
41
|
+
alias?: (ctx: Ctx) => Promise<Ctx> | Ctx
|
|
42
|
+
screen?: (ctx: Ctx) => Promise<Ctx> | Ctx
|
|
43
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
type BackoffParams = {
|
|
2
|
+
/** The number of milliseconds before starting the first retry. Default is 500 */
|
|
3
|
+
minTimeout?: number
|
|
4
|
+
|
|
5
|
+
/** The maximum number of milliseconds between two retries. Default is Infinity */
|
|
6
|
+
maxTimeout?: number
|
|
7
|
+
|
|
8
|
+
/** The exponential factor to use. Default is 2. */
|
|
9
|
+
factor?: number
|
|
10
|
+
|
|
11
|
+
/** The current attempt */
|
|
12
|
+
attempt: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function backoff(params: BackoffParams): number {
|
|
16
|
+
const random = Math.random() + 1
|
|
17
|
+
const {
|
|
18
|
+
minTimeout = 500,
|
|
19
|
+
factor = 2,
|
|
20
|
+
attempt,
|
|
21
|
+
maxTimeout = Infinity,
|
|
22
|
+
} = params
|
|
23
|
+
return Math.min(random * minTimeout * Math.pow(factor, attempt), maxTimeout)
|
|
24
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Emitter } from '../emitter'
|
|
2
|
+
import { backoff } from './backoff'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
export const ON_REMOVE_FROM_FUTURE = 'onRemoveFromFuture'
|
|
8
|
+
|
|
9
|
+
interface QueueItem {
|
|
10
|
+
id: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class PriorityQueue<Item extends QueueItem = QueueItem> extends Emitter {
|
|
14
|
+
protected future: Item[] = []
|
|
15
|
+
protected queue: Item[]
|
|
16
|
+
protected seen: Record<string, number>
|
|
17
|
+
|
|
18
|
+
public maxAttempts: number
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
maxAttempts: number,
|
|
22
|
+
queue: Item[],
|
|
23
|
+
seen?: Record<string, number>
|
|
24
|
+
) {
|
|
25
|
+
super()
|
|
26
|
+
this.maxAttempts = maxAttempts
|
|
27
|
+
this.queue = queue
|
|
28
|
+
this.seen = seen ?? {}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
push(...items: Item[]): boolean[] {
|
|
32
|
+
const accepted = items.map((operation) => {
|
|
33
|
+
const attempts = this.updateAttempts(operation)
|
|
34
|
+
|
|
35
|
+
if (attempts > this.maxAttempts || this.includes(operation)) {
|
|
36
|
+
return false
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.queue.push(operation)
|
|
40
|
+
return true
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
this.queue = this.queue.sort(
|
|
44
|
+
(a, b) => this.getAttempts(a) - this.getAttempts(b)
|
|
45
|
+
)
|
|
46
|
+
return accepted
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
pushWithBackoff(item: Item): boolean {
|
|
50
|
+
if (this.getAttempts(item) === 0) {
|
|
51
|
+
return this.push(item)[0]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const attempt = this.updateAttempts(item)
|
|
55
|
+
|
|
56
|
+
if (attempt > this.maxAttempts || this.includes(item)) {
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const timeout = backoff({ attempt: attempt - 1 })
|
|
61
|
+
|
|
62
|
+
setTimeout(() => {
|
|
63
|
+
this.queue.push(item)
|
|
64
|
+
// remove from future list
|
|
65
|
+
this.future = this.future.filter((f) => f.id !== item.id)
|
|
66
|
+
// Lets listeners know that a 'future' message is now available in the queue
|
|
67
|
+
this.emit(ON_REMOVE_FROM_FUTURE)
|
|
68
|
+
}, timeout)
|
|
69
|
+
|
|
70
|
+
this.future.push(item)
|
|
71
|
+
return true
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public getAttempts(item: Item): number {
|
|
75
|
+
return this.seen[item.id] ?? 0
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
public updateAttempts(item: Item): number {
|
|
79
|
+
this.seen[item.id] = this.getAttempts(item) + 1
|
|
80
|
+
return this.getAttempts(item)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
includes(item: Item): boolean {
|
|
84
|
+
return (
|
|
85
|
+
this.queue.includes(item) ||
|
|
86
|
+
this.future.includes(item) ||
|
|
87
|
+
Boolean(this.queue.find((i) => i.id === item.id)) ||
|
|
88
|
+
Boolean(this.future.find((i) => i.id === item.id))
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
pop(): Item | undefined {
|
|
93
|
+
return this.queue.shift()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public get length(): number {
|
|
97
|
+
return this.queue.length
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
public get todo(): number {
|
|
101
|
+
return this.queue.length + this.future.length
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { CoreContext, ContextCancelation } from '../context'
|
|
2
|
+
import { CorePlugin } from '../plugins'
|
|
3
|
+
|
|
4
|
+
async function tryAsync<T>(fn: () => T | Promise<T>): Promise<T> {
|
|
5
|
+
try {
|
|
6
|
+
return await fn()
|
|
7
|
+
} catch (err) {
|
|
8
|
+
return Promise.reject(err)
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function attempt<Ctx extends CoreContext = CoreContext>(
|
|
13
|
+
ctx: Ctx,
|
|
14
|
+
plugin: CorePlugin<Ctx>
|
|
15
|
+
): Promise<Ctx | ContextCancelation | Error> {
|
|
16
|
+
ctx.log('debug', 'plugin', { plugin: plugin.name })
|
|
17
|
+
const start = new Date().getTime()
|
|
18
|
+
|
|
19
|
+
const hook = plugin[ctx.event.type]
|
|
20
|
+
if (hook === undefined) {
|
|
21
|
+
return Promise.resolve(ctx)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const newCtx = tryAsync(() => hook.apply(plugin, [ctx]))
|
|
25
|
+
.then((ctx) => {
|
|
26
|
+
const done = new Date().getTime() - start
|
|
27
|
+
ctx.stats.gauge('plugin_time', done, [`plugin:${plugin.name}`])
|
|
28
|
+
|
|
29
|
+
return ctx
|
|
30
|
+
})
|
|
31
|
+
.catch((err: Error | ContextCancelation) => {
|
|
32
|
+
if (
|
|
33
|
+
err instanceof ContextCancelation &&
|
|
34
|
+
err.type === 'middleware_cancellation'
|
|
35
|
+
) {
|
|
36
|
+
throw err
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (err instanceof ContextCancelation) {
|
|
40
|
+
ctx.log('warn', err.type, {
|
|
41
|
+
plugin: plugin.name,
|
|
42
|
+
error: err,
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
return err
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
ctx.log('error', 'plugin Error', {
|
|
49
|
+
plugin: plugin.name,
|
|
50
|
+
error: err,
|
|
51
|
+
})
|
|
52
|
+
ctx.stats.increment('plugin_error', 1, [`plugin:${plugin.name}`])
|
|
53
|
+
|
|
54
|
+
return err
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
return newCtx
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function ensure<Ctx extends CoreContext = CoreContext>(
|
|
61
|
+
ctx: Ctx,
|
|
62
|
+
plugin: CorePlugin<Ctx>
|
|
63
|
+
): Promise<Ctx | undefined> {
|
|
64
|
+
return attempt(ctx, plugin).then((newContext) => {
|
|
65
|
+
if (newContext instanceof CoreContext) {
|
|
66
|
+
return newContext
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
ctx.log('debug', 'Context canceled')
|
|
70
|
+
ctx.stats.increment('context_canceled')
|
|
71
|
+
ctx.cancel(newContext)
|
|
72
|
+
})
|
|
73
|
+
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { CoreAnalytics } from '../analytics'
|
|
2
|
+
import { groupBy } from '../utils/group-by'
|
|
3
|
+
import { ON_REMOVE_FROM_FUTURE, PriorityQueue } from '../priority-queue'
|
|
4
|
+
|
|
5
|
+
import { CoreContext, ContextCancelation } from '../context'
|
|
6
|
+
import { Emitter } from '../emitter'
|
|
7
|
+
import { Integrations, JSONObject } from '../events/interfaces'
|
|
8
|
+
import { CorePlugin } from '../plugins'
|
|
9
|
+
import { createTaskGroup, TaskGroup } from '../task/task-group'
|
|
10
|
+
import { attempt, ensure } from './delivery'
|
|
11
|
+
import { isOffline } from '../connection'
|
|
12
|
+
|
|
13
|
+
export type EventQueueEmitterContract<Ctx extends CoreContext> = {
|
|
14
|
+
message_delivered: [ctx: Ctx]
|
|
15
|
+
message_enriched: [ctx: Ctx, plugin: CorePlugin<Ctx>]
|
|
16
|
+
delivery_success: [ctx: Ctx]
|
|
17
|
+
delivery_retry: [ctx: Ctx]
|
|
18
|
+
delivery_failure: [ctx: Ctx, err: Ctx | Error | ContextCancelation]
|
|
19
|
+
flush: [ctx: Ctx, delivered: boolean]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export abstract class CoreEventQueue<
|
|
23
|
+
Ctx extends CoreContext = CoreContext,
|
|
24
|
+
Plugin extends CorePlugin<Ctx> = CorePlugin<Ctx>
|
|
25
|
+
> extends Emitter<EventQueueEmitterContract<Ctx>> {
|
|
26
|
+
/**
|
|
27
|
+
* All event deliveries get suspended until all the tasks in this task group are complete.
|
|
28
|
+
* For example: a middleware that augments the event object should be loaded safely as a
|
|
29
|
+
* critical task, this way, event queue will wait for it to be ready before sending events.
|
|
30
|
+
*
|
|
31
|
+
* This applies to all the events already in the queue, and the upcoming ones
|
|
32
|
+
*/
|
|
33
|
+
criticalTasks: TaskGroup = createTaskGroup()
|
|
34
|
+
queue: PriorityQueue<Ctx>
|
|
35
|
+
plugins: Plugin[] = []
|
|
36
|
+
failedInitializations: string[] = []
|
|
37
|
+
private flushing = false
|
|
38
|
+
|
|
39
|
+
constructor(priorityQueue: PriorityQueue<Ctx>) {
|
|
40
|
+
super()
|
|
41
|
+
|
|
42
|
+
this.queue = priorityQueue
|
|
43
|
+
this.queue.on(ON_REMOVE_FROM_FUTURE, () => {
|
|
44
|
+
this.scheduleFlush(0)
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async register(
|
|
49
|
+
ctx: Ctx,
|
|
50
|
+
plugin: Plugin,
|
|
51
|
+
instance: CoreAnalytics
|
|
52
|
+
): Promise<void> {
|
|
53
|
+
await Promise.resolve(plugin.load(ctx, instance))
|
|
54
|
+
.then(() => {
|
|
55
|
+
this.plugins.push(plugin)
|
|
56
|
+
})
|
|
57
|
+
.catch((err) => {
|
|
58
|
+
if (plugin.type === 'destination') {
|
|
59
|
+
this.failedInitializations.push(plugin.name)
|
|
60
|
+
console.warn(plugin.name, err)
|
|
61
|
+
|
|
62
|
+
ctx.log('warn', 'Failed to load destination', {
|
|
63
|
+
plugin: plugin.name,
|
|
64
|
+
error: err,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
throw err
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async deregister(
|
|
75
|
+
ctx: Ctx,
|
|
76
|
+
plugin: CorePlugin<Ctx>,
|
|
77
|
+
instance: CoreAnalytics
|
|
78
|
+
): Promise<void> {
|
|
79
|
+
try {
|
|
80
|
+
if (plugin.unload) {
|
|
81
|
+
await Promise.resolve(plugin.unload(ctx, instance))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this.plugins = this.plugins.filter((p) => p.name !== plugin.name)
|
|
85
|
+
} catch (e) {
|
|
86
|
+
ctx.log('warn', 'Failed to unload destination', {
|
|
87
|
+
plugin: plugin.name,
|
|
88
|
+
error: e,
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async dispatch(ctx: Ctx): Promise<Ctx> {
|
|
94
|
+
ctx.log('debug', 'Dispatching')
|
|
95
|
+
ctx.stats.increment('message_dispatched')
|
|
96
|
+
|
|
97
|
+
this.queue.push(ctx)
|
|
98
|
+
const willDeliver = this.subscribeToDelivery(ctx)
|
|
99
|
+
this.scheduleFlush(0)
|
|
100
|
+
return willDeliver
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private async subscribeToDelivery(ctx: Ctx): Promise<Ctx> {
|
|
104
|
+
return new Promise((resolve) => {
|
|
105
|
+
const onDeliver = (flushed: Ctx, delivered: boolean): void => {
|
|
106
|
+
if (flushed.isSame(ctx)) {
|
|
107
|
+
this.off('flush', onDeliver)
|
|
108
|
+
if (delivered) {
|
|
109
|
+
resolve(flushed)
|
|
110
|
+
} else {
|
|
111
|
+
resolve(flushed)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this.on('flush', onDeliver)
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async dispatchSingle(ctx: Ctx): Promise<Ctx> {
|
|
121
|
+
ctx.log('debug', 'Dispatching')
|
|
122
|
+
ctx.stats.increment('message_dispatched')
|
|
123
|
+
|
|
124
|
+
this.queue.updateAttempts(ctx)
|
|
125
|
+
ctx.attempts = 1
|
|
126
|
+
|
|
127
|
+
return this.deliver(ctx).catch((err) => {
|
|
128
|
+
const accepted = this.enqueuRetry(err, ctx)
|
|
129
|
+
if (!accepted) {
|
|
130
|
+
ctx.setFailedDelivery({ reason: err })
|
|
131
|
+
return ctx
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return this.subscribeToDelivery(ctx)
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
isEmpty(): boolean {
|
|
139
|
+
return this.queue.length === 0
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private scheduleFlush(timeout = 500): void {
|
|
143
|
+
if (this.flushing) {
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this.flushing = true
|
|
148
|
+
|
|
149
|
+
setTimeout(() => {
|
|
150
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
151
|
+
this.flush().then(() => {
|
|
152
|
+
setTimeout(() => {
|
|
153
|
+
this.flushing = false
|
|
154
|
+
|
|
155
|
+
if (this.queue.length) {
|
|
156
|
+
this.scheduleFlush(0)
|
|
157
|
+
}
|
|
158
|
+
}, 0)
|
|
159
|
+
})
|
|
160
|
+
}, timeout)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private async deliver(ctx: Ctx): Promise<Ctx> {
|
|
164
|
+
await this.criticalTasks.done()
|
|
165
|
+
|
|
166
|
+
const start = Date.now()
|
|
167
|
+
try {
|
|
168
|
+
ctx = await this.flushOne(ctx)
|
|
169
|
+
const done = Date.now() - start
|
|
170
|
+
this.emit('delivery_success', ctx)
|
|
171
|
+
ctx.stats.gauge('delivered', done)
|
|
172
|
+
ctx.log('debug', 'Delivered', ctx.event)
|
|
173
|
+
return ctx
|
|
174
|
+
} catch (err: any) {
|
|
175
|
+
const error = err as Ctx | Error | ContextCancelation
|
|
176
|
+
ctx.log('error', 'Failed to deliver', error)
|
|
177
|
+
this.emit('delivery_failure', ctx, error)
|
|
178
|
+
ctx.stats.increment('delivery_failed')
|
|
179
|
+
throw err
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private enqueuRetry(err: Error, ctx: Ctx): boolean {
|
|
184
|
+
const retriable = !(err instanceof ContextCancelation) || err.retry
|
|
185
|
+
if (!retriable) {
|
|
186
|
+
return false
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return this.queue.pushWithBackoff(ctx)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async flush(): Promise<Ctx[]> {
|
|
193
|
+
if (this.queue.length === 0 || isOffline()) {
|
|
194
|
+
return []
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let ctx = this.queue.pop()
|
|
198
|
+
if (!ctx) {
|
|
199
|
+
return []
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
ctx.attempts = this.queue.getAttempts(ctx)
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
ctx = await this.deliver(ctx)
|
|
206
|
+
this.emit('flush', ctx, true)
|
|
207
|
+
} catch (err: any) {
|
|
208
|
+
const accepted = this.enqueuRetry(err, ctx)
|
|
209
|
+
|
|
210
|
+
if (!accepted) {
|
|
211
|
+
ctx.setFailedDelivery({ reason: err })
|
|
212
|
+
this.emit('flush', ctx, false)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return []
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return [ctx]
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private isReady(): boolean {
|
|
222
|
+
// return this.plugins.every((p) => p.isLoaded())
|
|
223
|
+
// should we wait for every plugin to load?
|
|
224
|
+
return true
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private availableExtensions(denyList: Integrations) {
|
|
228
|
+
const available = this.plugins.filter((p) => {
|
|
229
|
+
// Only filter out destination plugins or the Customer.io Data Pipeline plugin
|
|
230
|
+
if (p.type !== 'destination' && p.name !== 'Customer.io Data Pipelines') {
|
|
231
|
+
return true
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
let alternativeNameMatch: boolean | JSONObject | undefined = undefined
|
|
235
|
+
p.alternativeNames?.forEach((name) => {
|
|
236
|
+
if (denyList[name] !== undefined) {
|
|
237
|
+
alternativeNameMatch = denyList[name]
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
// Explicit integration option takes precedence, `All: false` does not apply to Customer.io Data Pipelines
|
|
242
|
+
return (
|
|
243
|
+
denyList[p.name] ??
|
|
244
|
+
alternativeNameMatch ??
|
|
245
|
+
(p.name === 'Customer.io Data Pipelines' ? true : denyList.All) !==
|
|
246
|
+
false
|
|
247
|
+
)
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
const {
|
|
251
|
+
before = [],
|
|
252
|
+
enrichment = [],
|
|
253
|
+
destination = [],
|
|
254
|
+
after = [],
|
|
255
|
+
} = groupBy(available, 'type')
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
before,
|
|
259
|
+
enrichment,
|
|
260
|
+
destinations: destination,
|
|
261
|
+
after,
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private async flushOne(ctx: Ctx): Promise<Ctx> {
|
|
266
|
+
if (!this.isReady()) {
|
|
267
|
+
throw new Error('Not ready')
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (ctx.attempts > 1) {
|
|
271
|
+
this.emit('delivery_retry', ctx)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const { before, enrichment } = this.availableExtensions(
|
|
275
|
+
ctx.event.integrations ?? {}
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
for (const beforeWare of before) {
|
|
279
|
+
const temp = await ensure(ctx, beforeWare)
|
|
280
|
+
if (temp instanceof CoreContext) {
|
|
281
|
+
ctx = temp
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
this.emit('message_enriched', ctx, beforeWare)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
for (const enrichmentWare of enrichment) {
|
|
288
|
+
const temp = await attempt(ctx, enrichmentWare)
|
|
289
|
+
if (temp instanceof CoreContext) {
|
|
290
|
+
ctx = temp
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
this.emit('message_enriched', ctx, enrichmentWare)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Enrichment and before plugins can re-arrange the deny list dynamically
|
|
297
|
+
// so we need to pluck them at the end
|
|
298
|
+
const { destinations, after } = this.availableExtensions(
|
|
299
|
+
ctx.event.integrations ?? {}
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
await new Promise((resolve, reject) => {
|
|
303
|
+
setTimeout(() => {
|
|
304
|
+
const attempts = destinations.map((destination) =>
|
|
305
|
+
attempt(ctx, destination)
|
|
306
|
+
)
|
|
307
|
+
Promise.all(attempts).then(resolve).catch(reject)
|
|
308
|
+
}, 0)
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
ctx.stats.increment('message_delivered')
|
|
312
|
+
|
|
313
|
+
this.emit('message_delivered', ctx)
|
|
314
|
+
|
|
315
|
+
const afterCalls = after.map((after) => attempt(ctx, after))
|
|
316
|
+
await Promise.all(afterCalls)
|
|
317
|
+
|
|
318
|
+
return ctx
|
|
319
|
+
}
|
|
320
|
+
}
|