@customerio/cdp-analytics-core 0.0.1
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 +149 -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 +41 -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 +146 -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 +37 -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 +27 -0
- package/dist/types/events/index.d.ts.map +1 -0
- package/dist/types/events/interfaces.d.ts +373 -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 +7 -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 +39 -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 +280 -0
- package/src/events/interfaces.ts +447 -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 +43 -0
- package/src/validation/helpers.ts +27 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
type CompactMetricType = 'g' | 'c'
|
|
2
|
+
|
|
3
|
+
export type CoreMetricType = 'gauge' | 'counter'
|
|
4
|
+
|
|
5
|
+
export interface CoreMetric {
|
|
6
|
+
metric: string
|
|
7
|
+
value: number
|
|
8
|
+
type: CoreMetricType
|
|
9
|
+
tags: string[]
|
|
10
|
+
timestamp: number // unit milliseconds
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface CompactMetric {
|
|
14
|
+
m: string // metric name
|
|
15
|
+
v: number // value
|
|
16
|
+
k: CompactMetricType
|
|
17
|
+
t: string[] // tags
|
|
18
|
+
e: number // timestamp in unit milliseconds
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const compactMetricType = (type: CoreMetricType): CompactMetricType => {
|
|
22
|
+
const enums: Record<CoreMetricType, CompactMetricType> = {
|
|
23
|
+
gauge: 'g',
|
|
24
|
+
counter: 'c',
|
|
25
|
+
}
|
|
26
|
+
return enums[type]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export abstract class CoreStats {
|
|
30
|
+
metrics: CoreMetric[] = []
|
|
31
|
+
increment(metric: string, by = 1, tags?: string[]): void {
|
|
32
|
+
this.metrics.push({
|
|
33
|
+
metric,
|
|
34
|
+
value: by,
|
|
35
|
+
tags: tags ?? [],
|
|
36
|
+
type: 'counter',
|
|
37
|
+
timestamp: Date.now(),
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
gauge(metric: string, value: number, tags?: string[]): void {
|
|
42
|
+
this.metrics.push({
|
|
43
|
+
metric,
|
|
44
|
+
value,
|
|
45
|
+
tags: tags ?? [],
|
|
46
|
+
type: 'gauge',
|
|
47
|
+
timestamp: Date.now(),
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
flush(): void {
|
|
52
|
+
const formatted = this.metrics.map((m) => ({
|
|
53
|
+
...m,
|
|
54
|
+
tags: m.tags.join(','),
|
|
55
|
+
}))
|
|
56
|
+
// ie doesn't like console.table
|
|
57
|
+
if (console.table) {
|
|
58
|
+
console.table(formatted)
|
|
59
|
+
} else {
|
|
60
|
+
console.log(formatted)
|
|
61
|
+
}
|
|
62
|
+
this.metrics = []
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* compact keys for smaller payload
|
|
67
|
+
*/
|
|
68
|
+
serialize(): CompactMetric[] {
|
|
69
|
+
return this.metrics.map((m) => {
|
|
70
|
+
return {
|
|
71
|
+
m: m.metric,
|
|
72
|
+
v: m.value,
|
|
73
|
+
t: m.tags,
|
|
74
|
+
k: compactMetricType(m.type),
|
|
75
|
+
e: m.timestamp,
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export class NullStats extends CoreStats {
|
|
82
|
+
override gauge(..._args: Parameters<CoreStats['gauge']>) {}
|
|
83
|
+
override increment(..._args: Parameters<CoreStats['increment']>) {}
|
|
84
|
+
override flush(..._args: Parameters<CoreStats['flush']>) {}
|
|
85
|
+
override serialize(..._args: Parameters<CoreStats['serialize']>) {
|
|
86
|
+
return []
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { isThenable } from '../utils/is-thenable'
|
|
2
|
+
|
|
3
|
+
export type TaskGroup = {
|
|
4
|
+
done: () => Promise<void>
|
|
5
|
+
run: <Operation extends (...args: any[]) => any>(
|
|
6
|
+
op: Operation
|
|
7
|
+
) => ReturnType<Operation>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const createTaskGroup = (): TaskGroup => {
|
|
11
|
+
let taskCompletionPromise: Promise<void>
|
|
12
|
+
let resolvePromise: () => void
|
|
13
|
+
let count = 0
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
done: () => taskCompletionPromise,
|
|
17
|
+
run: (op) => {
|
|
18
|
+
const returnValue = op()
|
|
19
|
+
|
|
20
|
+
if (isThenable(returnValue)) {
|
|
21
|
+
if (++count === 1) {
|
|
22
|
+
taskCompletionPromise = new Promise((res) => (resolvePromise = res))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
returnValue.finally(() => --count === 0 && resolvePromise())
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return returnValue
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function bindAll<
|
|
2
|
+
ObjType extends { [key: string]: any },
|
|
3
|
+
KeyType extends keyof ObjType
|
|
4
|
+
>(obj: ObjType): ObjType {
|
|
5
|
+
const proto = obj.constructor.prototype
|
|
6
|
+
for (const key of Object.getOwnPropertyNames(proto)) {
|
|
7
|
+
if (key !== 'constructor') {
|
|
8
|
+
const desc = Object.getOwnPropertyDescriptor(
|
|
9
|
+
obj.constructor.prototype,
|
|
10
|
+
key
|
|
11
|
+
)
|
|
12
|
+
if (!!desc && typeof desc.value === 'function') {
|
|
13
|
+
obj[key as KeyType] = obj[key].bind(obj)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return obj
|
|
19
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// This an imperfect polyfill for globalThis
|
|
2
|
+
export const getGlobal = () => {
|
|
3
|
+
if (typeof globalThis !== 'undefined') {
|
|
4
|
+
return globalThis
|
|
5
|
+
}
|
|
6
|
+
if (typeof self !== 'undefined') {
|
|
7
|
+
return self
|
|
8
|
+
}
|
|
9
|
+
if (typeof window !== 'undefined') {
|
|
10
|
+
return window
|
|
11
|
+
}
|
|
12
|
+
if (typeof global !== 'undefined') {
|
|
13
|
+
return global
|
|
14
|
+
}
|
|
15
|
+
return null
|
|
16
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
type Grouper<T> = (obj: T) => string | number
|
|
2
|
+
|
|
3
|
+
export function groupBy<T>(
|
|
4
|
+
collection: T[],
|
|
5
|
+
grouper: keyof T | Grouper<T>
|
|
6
|
+
): Record<string, T[]> {
|
|
7
|
+
const results: Record<string, T[]> = {}
|
|
8
|
+
|
|
9
|
+
collection.forEach((item) => {
|
|
10
|
+
let key: string | number | undefined = undefined
|
|
11
|
+
|
|
12
|
+
if (typeof grouper === 'string') {
|
|
13
|
+
const suggestedKey = item[grouper]
|
|
14
|
+
key =
|
|
15
|
+
typeof suggestedKey !== 'string'
|
|
16
|
+
? JSON.stringify(suggestedKey)
|
|
17
|
+
: suggestedKey
|
|
18
|
+
} else if (grouper instanceof Function) {
|
|
19
|
+
key = grouper(item)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (key === undefined) {
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
results[key] = [...(results[key] ?? []), item]
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
return results
|
|
30
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Code derived from https://github.com/jonschlinkert/is-plain-object/blob/master/is-plain-object.js
|
|
2
|
+
|
|
3
|
+
function isObject(o: unknown): o is Object {
|
|
4
|
+
return Object.prototype.toString.call(o) === '[object Object]'
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function isPlainObject(o: unknown): o is Record<PropertyKey, unknown> {
|
|
8
|
+
if (isObject(o) === false) return false
|
|
9
|
+
|
|
10
|
+
// If has modified constructor
|
|
11
|
+
const ctor = (o as any).constructor
|
|
12
|
+
if (ctor === undefined) return true
|
|
13
|
+
|
|
14
|
+
// If has modified prototype
|
|
15
|
+
const prot = ctor.prototype
|
|
16
|
+
if (isObject(prot) === false) return false
|
|
17
|
+
|
|
18
|
+
// If constructor does not have an Object-specific method
|
|
19
|
+
// eslint-disable-next-line no-prototype-builtins
|
|
20
|
+
if ((prot as Object).hasOwnProperty('isPrototypeOf') === false) {
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Most likely a plain Object
|
|
25
|
+
return true
|
|
26
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const pWhile = async <T>(
|
|
2
|
+
condition: (value: T | undefined) => boolean,
|
|
3
|
+
action: () => T | PromiseLike<T>
|
|
4
|
+
): Promise<void> => {
|
|
5
|
+
const loop = async (actionResult: T | undefined): Promise<void> => {
|
|
6
|
+
if (condition(actionResult)) {
|
|
7
|
+
return loop(await action())
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return loop(undefined)
|
|
12
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remove Index Signature
|
|
3
|
+
*/
|
|
4
|
+
export type RemoveIndexSignature<T> = {
|
|
5
|
+
[K in keyof T as {} extends Record<K, 1> ? never : K]: T[K]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Recursively make all object properties nullable
|
|
10
|
+
*/
|
|
11
|
+
export type DeepNullable<T> = {
|
|
12
|
+
[K in keyof T]: T[K] extends object ? DeepNullable<T[K]> | null : T[K] | null
|
|
13
|
+
}
|