@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.
Files changed (220) hide show
  1. package/LICENSE.MD +22 -0
  2. package/README.md +3 -0
  3. package/dist/cjs/analytics/dispatch.js +54 -0
  4. package/dist/cjs/analytics/dispatch.js.map +1 -0
  5. package/dist/cjs/analytics/index.js +3 -0
  6. package/dist/cjs/analytics/index.js.map +1 -0
  7. package/dist/cjs/callback/index.js +46 -0
  8. package/dist/cjs/callback/index.js.map +1 -0
  9. package/dist/cjs/connection/index.js +16 -0
  10. package/dist/cjs/connection/index.js.map +1 -0
  11. package/dist/cjs/context/index.js +87 -0
  12. package/dist/cjs/context/index.js.map +1 -0
  13. package/dist/cjs/emitter/index.js +66 -0
  14. package/dist/cjs/emitter/index.js.map +1 -0
  15. package/dist/cjs/emitter/interface.js +3 -0
  16. package/dist/cjs/emitter/interface.js.map +1 -0
  17. package/dist/cjs/events/index.js +154 -0
  18. package/dist/cjs/events/index.js.map +1 -0
  19. package/dist/cjs/events/interfaces.js +3 -0
  20. package/dist/cjs/events/interfaces.js.map +1 -0
  21. package/dist/cjs/index.js +25 -0
  22. package/dist/cjs/index.js.map +1 -0
  23. package/dist/cjs/logger/index.js +62 -0
  24. package/dist/cjs/logger/index.js.map +1 -0
  25. package/dist/cjs/plugins/index.js +3 -0
  26. package/dist/cjs/plugins/index.js.map +1 -0
  27. package/dist/cjs/priority-queue/backoff.js +10 -0
  28. package/dist/cjs/priority-queue/backoff.js.map +1 -0
  29. package/dist/cjs/priority-queue/index.js +92 -0
  30. package/dist/cjs/priority-queue/index.js.map +1 -0
  31. package/dist/cjs/queue/delivery.js +69 -0
  32. package/dist/cjs/queue/delivery.js.map +1 -0
  33. package/dist/cjs/queue/event-queue.js +340 -0
  34. package/dist/cjs/queue/event-queue.js.map +1 -0
  35. package/dist/cjs/stats/index.js +96 -0
  36. package/dist/cjs/stats/index.js.map +1 -0
  37. package/dist/cjs/task/task-group.js +24 -0
  38. package/dist/cjs/task/task-group.js.map +1 -0
  39. package/dist/cjs/user/index.js +3 -0
  40. package/dist/cjs/user/index.js.map +1 -0
  41. package/dist/cjs/utils/bind-all.js +18 -0
  42. package/dist/cjs/utils/bind-all.js.map +1 -0
  43. package/dist/cjs/utils/environment.js +12 -0
  44. package/dist/cjs/utils/environment.js.map +1 -0
  45. package/dist/cjs/utils/get-global.js +21 -0
  46. package/dist/cjs/utils/get-global.js.map +1 -0
  47. package/dist/cjs/utils/group-by.js +28 -0
  48. package/dist/cjs/utils/group-by.js.map +1 -0
  49. package/dist/cjs/utils/has-properties.js +13 -0
  50. package/dist/cjs/utils/has-properties.js.map +1 -0
  51. package/dist/cjs/utils/is-plain-object.js +28 -0
  52. package/dist/cjs/utils/is-plain-object.js.map +1 -0
  53. package/dist/cjs/utils/is-thenable.js +15 -0
  54. package/dist/cjs/utils/is-thenable.js.map +1 -0
  55. package/dist/cjs/utils/p-while.js +25 -0
  56. package/dist/cjs/utils/p-while.js.map +1 -0
  57. package/dist/cjs/utils/pick.js +10 -0
  58. package/dist/cjs/utils/pick.js.map +1 -0
  59. package/dist/cjs/utils/ts-helpers.js +3 -0
  60. package/dist/cjs/utils/ts-helpers.js.map +1 -0
  61. package/dist/cjs/validation/assertions.js +51 -0
  62. package/dist/cjs/validation/assertions.js.map +1 -0
  63. package/dist/cjs/validation/helpers.js +26 -0
  64. package/dist/cjs/validation/helpers.js.map +1 -0
  65. package/dist/esm/analytics/dispatch.js +49 -0
  66. package/dist/esm/analytics/dispatch.js.map +1 -0
  67. package/dist/esm/analytics/index.js +2 -0
  68. package/dist/esm/analytics/index.js.map +1 -0
  69. package/dist/esm/callback/index.js +40 -0
  70. package/dist/esm/callback/index.js.map +1 -0
  71. package/dist/esm/connection/index.js +11 -0
  72. package/dist/esm/connection/index.js.map +1 -0
  73. package/dist/esm/context/index.js +84 -0
  74. package/dist/esm/context/index.js.map +1 -0
  75. package/dist/esm/emitter/index.js +63 -0
  76. package/dist/esm/emitter/index.js.map +1 -0
  77. package/dist/esm/emitter/interface.js +2 -0
  78. package/dist/esm/emitter/interface.js.map +1 -0
  79. package/dist/esm/events/index.js +151 -0
  80. package/dist/esm/events/index.js.map +1 -0
  81. package/dist/esm/events/interfaces.js +2 -0
  82. package/dist/esm/events/interfaces.js.map +1 -0
  83. package/dist/esm/index.js +19 -0
  84. package/dist/esm/index.js.map +1 -0
  85. package/dist/esm/logger/index.js +59 -0
  86. package/dist/esm/logger/index.js.map +1 -0
  87. package/dist/esm/plugins/index.js +2 -0
  88. package/dist/esm/plugins/index.js.map +1 -0
  89. package/dist/esm/priority-queue/backoff.js +6 -0
  90. package/dist/esm/priority-queue/backoff.js.map +1 -0
  91. package/dist/esm/priority-queue/index.js +89 -0
  92. package/dist/esm/priority-queue/index.js.map +1 -0
  93. package/dist/esm/queue/delivery.js +64 -0
  94. package/dist/esm/queue/delivery.js.map +1 -0
  95. package/dist/esm/queue/event-queue.js +337 -0
  96. package/dist/esm/queue/event-queue.js.map +1 -0
  97. package/dist/esm/stats/index.js +93 -0
  98. package/dist/esm/stats/index.js.map +1 -0
  99. package/dist/esm/task/task-group.js +20 -0
  100. package/dist/esm/task/task-group.js.map +1 -0
  101. package/dist/esm/user/index.js +2 -0
  102. package/dist/esm/user/index.js.map +1 -0
  103. package/dist/esm/utils/bind-all.js +14 -0
  104. package/dist/esm/utils/bind-all.js.map +1 -0
  105. package/dist/esm/utils/environment.js +7 -0
  106. package/dist/esm/utils/environment.js.map +1 -0
  107. package/dist/esm/utils/get-global.js +17 -0
  108. package/dist/esm/utils/get-global.js.map +1 -0
  109. package/dist/esm/utils/group-by.js +24 -0
  110. package/dist/esm/utils/group-by.js.map +1 -0
  111. package/dist/esm/utils/has-properties.js +9 -0
  112. package/dist/esm/utils/has-properties.js.map +1 -0
  113. package/dist/esm/utils/is-plain-object.js +24 -0
  114. package/dist/esm/utils/is-plain-object.js.map +1 -0
  115. package/dist/esm/utils/is-thenable.js +11 -0
  116. package/dist/esm/utils/is-thenable.js.map +1 -0
  117. package/dist/esm/utils/p-while.js +21 -0
  118. package/dist/esm/utils/p-while.js.map +1 -0
  119. package/dist/esm/utils/pick.js +6 -0
  120. package/dist/esm/utils/pick.js.map +1 -0
  121. package/dist/esm/utils/ts-helpers.js +2 -0
  122. package/dist/esm/utils/ts-helpers.js.map +1 -0
  123. package/dist/esm/validation/assertions.js +46 -0
  124. package/dist/esm/validation/assertions.js.map +1 -0
  125. package/dist/esm/validation/helpers.js +18 -0
  126. package/dist/esm/validation/helpers.js.map +1 -0
  127. package/dist/types/analytics/dispatch.d.ts +20 -0
  128. package/dist/types/analytics/dispatch.d.ts.map +1 -0
  129. package/dist/types/analytics/index.d.ts +12 -0
  130. package/dist/types/analytics/index.d.ts.map +1 -0
  131. package/dist/types/callback/index.d.ts +11 -0
  132. package/dist/types/callback/index.d.ts.map +1 -0
  133. package/dist/types/connection/index.d.ts +3 -0
  134. package/dist/types/connection/index.d.ts.map +1 -0
  135. package/dist/types/context/index.d.ts +44 -0
  136. package/dist/types/context/index.d.ts.map +1 -0
  137. package/dist/types/emitter/index.d.ts +25 -0
  138. package/dist/types/emitter/index.d.ts.map +1 -0
  139. package/dist/types/emitter/interface.d.ts +27 -0
  140. package/dist/types/emitter/interface.d.ts.map +1 -0
  141. package/dist/types/events/index.d.ts +31 -0
  142. package/dist/types/events/index.d.ts.map +1 -0
  143. package/dist/types/events/interfaces.d.ts +379 -0
  144. package/dist/types/events/interfaces.d.ts.map +1 -0
  145. package/dist/types/index.d.ts +19 -0
  146. package/dist/types/index.d.ts.map +1 -0
  147. package/dist/types/logger/index.d.ts +19 -0
  148. package/dist/types/logger/index.d.ts.map +1 -0
  149. package/dist/types/plugins/index.d.ts +25 -0
  150. package/dist/types/plugins/index.d.ts.map +1 -0
  151. package/dist/types/priority-queue/backoff.d.ts +13 -0
  152. package/dist/types/priority-queue/backoff.d.ts.map +1 -0
  153. package/dist/types/priority-queue/index.d.ts +25 -0
  154. package/dist/types/priority-queue/index.d.ts.map +1 -0
  155. package/dist/types/queue/delivery.d.ts +5 -0
  156. package/dist/types/queue/delivery.d.ts.map +1 -0
  157. package/dist/types/queue/event-queue.d.ts +43 -0
  158. package/dist/types/queue/event-queue.d.ts.map +1 -0
  159. package/dist/types/stats/index.d.ts +34 -0
  160. package/dist/types/stats/index.d.ts.map +1 -0
  161. package/dist/types/task/task-group.d.ts +6 -0
  162. package/dist/types/task/task-group.d.ts.map +1 -0
  163. package/dist/types/user/index.d.ts +6 -0
  164. package/dist/types/user/index.d.ts.map +1 -0
  165. package/dist/types/utils/bind-all.d.ts +4 -0
  166. package/dist/types/utils/bind-all.d.ts.map +1 -0
  167. package/dist/types/utils/environment.d.ts +3 -0
  168. package/dist/types/utils/environment.d.ts.map +1 -0
  169. package/dist/types/utils/get-global.d.ts +2 -0
  170. package/dist/types/utils/get-global.d.ts.map +1 -0
  171. package/dist/types/utils/group-by.d.ts +4 -0
  172. package/dist/types/utils/group-by.d.ts.map +1 -0
  173. package/dist/types/utils/has-properties.d.ts +4 -0
  174. package/dist/types/utils/has-properties.d.ts.map +1 -0
  175. package/dist/types/utils/is-plain-object.d.ts +2 -0
  176. package/dist/types/utils/is-plain-object.d.ts.map +1 -0
  177. package/dist/types/utils/is-thenable.d.ts +6 -0
  178. package/dist/types/utils/is-thenable.d.ts.map +1 -0
  179. package/dist/types/utils/p-while.d.ts +2 -0
  180. package/dist/types/utils/p-while.d.ts.map +1 -0
  181. package/dist/types/utils/pick.d.ts +2 -0
  182. package/dist/types/utils/pick.d.ts.map +1 -0
  183. package/dist/types/utils/ts-helpers.d.ts +13 -0
  184. package/dist/types/utils/ts-helpers.d.ts.map +1 -0
  185. package/dist/types/validation/assertions.d.ts +8 -0
  186. package/dist/types/validation/assertions.d.ts.map +1 -0
  187. package/dist/types/validation/helpers.d.ts +7 -0
  188. package/dist/types/validation/helpers.d.ts.map +1 -0
  189. package/package.json +40 -0
  190. package/src/analytics/dispatch.ts +58 -0
  191. package/src/analytics/index.ts +11 -0
  192. package/src/callback/index.ts +51 -0
  193. package/src/connection/index.ts +13 -0
  194. package/src/context/index.ts +123 -0
  195. package/src/emitter/index.ts +65 -0
  196. package/src/emitter/interface.ts +31 -0
  197. package/src/events/index.ts +285 -0
  198. package/src/events/interfaces.ts +453 -0
  199. package/src/index.ts +18 -0
  200. package/src/logger/index.ts +74 -0
  201. package/src/plugins/index.ts +43 -0
  202. package/src/priority-queue/backoff.ts +24 -0
  203. package/src/priority-queue/index.ts +103 -0
  204. package/src/queue/delivery.ts +73 -0
  205. package/src/queue/event-queue.ts +320 -0
  206. package/src/stats/index.ts +88 -0
  207. package/src/task/task-group.ts +31 -0
  208. package/src/user/index.ts +7 -0
  209. package/src/utils/bind-all.ts +19 -0
  210. package/src/utils/environment.ts +7 -0
  211. package/src/utils/get-global.ts +16 -0
  212. package/src/utils/group-by.ts +30 -0
  213. package/src/utils/has-properties.ts +7 -0
  214. package/src/utils/is-plain-object.ts +26 -0
  215. package/src/utils/is-thenable.ts +9 -0
  216. package/src/utils/p-while.ts +12 -0
  217. package/src/utils/pick.ts +8 -0
  218. package/src/utils/ts-helpers.ts +13 -0
  219. package/src/validation/assertions.ts +54 -0
  220. 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
+ }