autotel 2.1.0

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 (272) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1946 -0
  3. package/dist/chunk-2LNRY4QK.js +273 -0
  4. package/dist/chunk-2LNRY4QK.js.map +1 -0
  5. package/dist/chunk-3HENGDW2.js +587 -0
  6. package/dist/chunk-3HENGDW2.js.map +1 -0
  7. package/dist/chunk-4OAT42CA.cjs +73 -0
  8. package/dist/chunk-4OAT42CA.cjs.map +1 -0
  9. package/dist/chunk-5GWX5LFW.js +70 -0
  10. package/dist/chunk-5GWX5LFW.js.map +1 -0
  11. package/dist/chunk-5R2M36QB.js +195 -0
  12. package/dist/chunk-5R2M36QB.js.map +1 -0
  13. package/dist/chunk-5ZN622AO.js +73 -0
  14. package/dist/chunk-5ZN622AO.js.map +1 -0
  15. package/dist/chunk-77MSMAUQ.cjs +498 -0
  16. package/dist/chunk-77MSMAUQ.cjs.map +1 -0
  17. package/dist/chunk-ABPEQ6RK.cjs +596 -0
  18. package/dist/chunk-ABPEQ6RK.cjs.map +1 -0
  19. package/dist/chunk-BWYGJKRB.js +95 -0
  20. package/dist/chunk-BWYGJKRB.js.map +1 -0
  21. package/dist/chunk-BZHG5IZ4.js +73 -0
  22. package/dist/chunk-BZHG5IZ4.js.map +1 -0
  23. package/dist/chunk-G7VZBCD6.cjs +35 -0
  24. package/dist/chunk-G7VZBCD6.cjs.map +1 -0
  25. package/dist/chunk-GVLK7YUU.cjs +30 -0
  26. package/dist/chunk-GVLK7YUU.cjs.map +1 -0
  27. package/dist/chunk-HCCXC7XG.js +205 -0
  28. package/dist/chunk-HCCXC7XG.js.map +1 -0
  29. package/dist/chunk-HE6T6FIX.cjs +203 -0
  30. package/dist/chunk-HE6T6FIX.cjs.map +1 -0
  31. package/dist/chunk-KIXWPOCO.cjs +100 -0
  32. package/dist/chunk-KIXWPOCO.cjs.map +1 -0
  33. package/dist/chunk-KVGNW3FC.js +87 -0
  34. package/dist/chunk-KVGNW3FC.js.map +1 -0
  35. package/dist/chunk-LITNXTTT.js +3 -0
  36. package/dist/chunk-LITNXTTT.js.map +1 -0
  37. package/dist/chunk-M4ANN7RL.js +114 -0
  38. package/dist/chunk-M4ANN7RL.js.map +1 -0
  39. package/dist/chunk-NC52UBR2.cjs +32 -0
  40. package/dist/chunk-NC52UBR2.cjs.map +1 -0
  41. package/dist/chunk-NHCNRQD3.cjs +212 -0
  42. package/dist/chunk-NHCNRQD3.cjs.map +1 -0
  43. package/dist/chunk-NZ72VDNY.cjs +4 -0
  44. package/dist/chunk-NZ72VDNY.cjs.map +1 -0
  45. package/dist/chunk-P6JUDYNO.js +57 -0
  46. package/dist/chunk-P6JUDYNO.js.map +1 -0
  47. package/dist/chunk-RJYY7BWX.js +1349 -0
  48. package/dist/chunk-RJYY7BWX.js.map +1 -0
  49. package/dist/chunk-TRI4V5BF.cjs +126 -0
  50. package/dist/chunk-TRI4V5BF.cjs.map +1 -0
  51. package/dist/chunk-UL33I6IS.js +139 -0
  52. package/dist/chunk-UL33I6IS.js.map +1 -0
  53. package/dist/chunk-URRW6M2C.cjs +61 -0
  54. package/dist/chunk-URRW6M2C.cjs.map +1 -0
  55. package/dist/chunk-UY3UYPBZ.cjs +77 -0
  56. package/dist/chunk-UY3UYPBZ.cjs.map +1 -0
  57. package/dist/chunk-W3253FGB.cjs +277 -0
  58. package/dist/chunk-W3253FGB.cjs.map +1 -0
  59. package/dist/chunk-W7LHZVQF.js +26 -0
  60. package/dist/chunk-W7LHZVQF.js.map +1 -0
  61. package/dist/chunk-WBWNM6LB.cjs +1360 -0
  62. package/dist/chunk-WBWNM6LB.cjs.map +1 -0
  63. package/dist/chunk-WFJ7L2RV.js +494 -0
  64. package/dist/chunk-WFJ7L2RV.js.map +1 -0
  65. package/dist/chunk-X4RMFFMR.js +28 -0
  66. package/dist/chunk-X4RMFFMR.js.map +1 -0
  67. package/dist/chunk-Y4Y2S7BM.cjs +92 -0
  68. package/dist/chunk-Y4Y2S7BM.cjs.map +1 -0
  69. package/dist/chunk-YLPNXZFI.cjs +143 -0
  70. package/dist/chunk-YLPNXZFI.cjs.map +1 -0
  71. package/dist/chunk-YTXEZ4SD.cjs +77 -0
  72. package/dist/chunk-YTXEZ4SD.cjs.map +1 -0
  73. package/dist/chunk-Z6ZWNWWR.js +30 -0
  74. package/dist/chunk-Z6ZWNWWR.js.map +1 -0
  75. package/dist/config.cjs +26 -0
  76. package/dist/config.cjs.map +1 -0
  77. package/dist/config.d.cts +75 -0
  78. package/dist/config.d.ts +75 -0
  79. package/dist/config.js +5 -0
  80. package/dist/config.js.map +1 -0
  81. package/dist/db.cjs +233 -0
  82. package/dist/db.cjs.map +1 -0
  83. package/dist/db.d.cts +123 -0
  84. package/dist/db.d.ts +123 -0
  85. package/dist/db.js +228 -0
  86. package/dist/db.js.map +1 -0
  87. package/dist/decorators.cjs +67 -0
  88. package/dist/decorators.cjs.map +1 -0
  89. package/dist/decorators.d.cts +91 -0
  90. package/dist/decorators.d.ts +91 -0
  91. package/dist/decorators.js +65 -0
  92. package/dist/decorators.js.map +1 -0
  93. package/dist/event-subscriber.cjs +6 -0
  94. package/dist/event-subscriber.cjs.map +1 -0
  95. package/dist/event-subscriber.d.cts +116 -0
  96. package/dist/event-subscriber.d.ts +116 -0
  97. package/dist/event-subscriber.js +3 -0
  98. package/dist/event-subscriber.js.map +1 -0
  99. package/dist/event-testing.cjs +21 -0
  100. package/dist/event-testing.cjs.map +1 -0
  101. package/dist/event-testing.d.cts +110 -0
  102. package/dist/event-testing.d.ts +110 -0
  103. package/dist/event-testing.js +4 -0
  104. package/dist/event-testing.js.map +1 -0
  105. package/dist/event.cjs +30 -0
  106. package/dist/event.cjs.map +1 -0
  107. package/dist/event.d.cts +282 -0
  108. package/dist/event.d.ts +282 -0
  109. package/dist/event.js +13 -0
  110. package/dist/event.js.map +1 -0
  111. package/dist/exporters.cjs +17 -0
  112. package/dist/exporters.cjs.map +1 -0
  113. package/dist/exporters.d.cts +1 -0
  114. package/dist/exporters.d.ts +1 -0
  115. package/dist/exporters.js +4 -0
  116. package/dist/exporters.js.map +1 -0
  117. package/dist/functional.cjs +46 -0
  118. package/dist/functional.cjs.map +1 -0
  119. package/dist/functional.d.cts +478 -0
  120. package/dist/functional.d.ts +478 -0
  121. package/dist/functional.js +13 -0
  122. package/dist/functional.js.map +1 -0
  123. package/dist/http.cjs +189 -0
  124. package/dist/http.cjs.map +1 -0
  125. package/dist/http.d.cts +169 -0
  126. package/dist/http.d.ts +169 -0
  127. package/dist/http.js +184 -0
  128. package/dist/http.js.map +1 -0
  129. package/dist/index.cjs +333 -0
  130. package/dist/index.cjs.map +1 -0
  131. package/dist/index.d.cts +758 -0
  132. package/dist/index.d.ts +758 -0
  133. package/dist/index.js +143 -0
  134. package/dist/index.js.map +1 -0
  135. package/dist/instrumentation.cjs +182 -0
  136. package/dist/instrumentation.cjs.map +1 -0
  137. package/dist/instrumentation.d.cts +49 -0
  138. package/dist/instrumentation.d.ts +49 -0
  139. package/dist/instrumentation.js +179 -0
  140. package/dist/instrumentation.js.map +1 -0
  141. package/dist/logger.cjs +19 -0
  142. package/dist/logger.cjs.map +1 -0
  143. package/dist/logger.d.cts +146 -0
  144. package/dist/logger.d.ts +146 -0
  145. package/dist/logger.js +6 -0
  146. package/dist/logger.js.map +1 -0
  147. package/dist/metric-helpers.cjs +31 -0
  148. package/dist/metric-helpers.cjs.map +1 -0
  149. package/dist/metric-helpers.d.cts +13 -0
  150. package/dist/metric-helpers.d.ts +13 -0
  151. package/dist/metric-helpers.js +6 -0
  152. package/dist/metric-helpers.js.map +1 -0
  153. package/dist/metric-testing.cjs +21 -0
  154. package/dist/metric-testing.cjs.map +1 -0
  155. package/dist/metric-testing.d.cts +110 -0
  156. package/dist/metric-testing.d.ts +110 -0
  157. package/dist/metric-testing.js +4 -0
  158. package/dist/metric-testing.js.map +1 -0
  159. package/dist/metric.cjs +26 -0
  160. package/dist/metric.cjs.map +1 -0
  161. package/dist/metric.d.cts +240 -0
  162. package/dist/metric.d.ts +240 -0
  163. package/dist/metric.js +9 -0
  164. package/dist/metric.js.map +1 -0
  165. package/dist/processors.cjs +17 -0
  166. package/dist/processors.cjs.map +1 -0
  167. package/dist/processors.d.cts +1 -0
  168. package/dist/processors.d.ts +1 -0
  169. package/dist/processors.js +4 -0
  170. package/dist/processors.js.map +1 -0
  171. package/dist/sampling.cjs +40 -0
  172. package/dist/sampling.cjs.map +1 -0
  173. package/dist/sampling.d.cts +260 -0
  174. package/dist/sampling.d.ts +260 -0
  175. package/dist/sampling.js +7 -0
  176. package/dist/sampling.js.map +1 -0
  177. package/dist/semantic-helpers.cjs +35 -0
  178. package/dist/semantic-helpers.cjs.map +1 -0
  179. package/dist/semantic-helpers.d.cts +442 -0
  180. package/dist/semantic-helpers.d.ts +442 -0
  181. package/dist/semantic-helpers.js +14 -0
  182. package/dist/semantic-helpers.js.map +1 -0
  183. package/dist/tail-sampling-processor.cjs +13 -0
  184. package/dist/tail-sampling-processor.cjs.map +1 -0
  185. package/dist/tail-sampling-processor.d.cts +27 -0
  186. package/dist/tail-sampling-processor.d.ts +27 -0
  187. package/dist/tail-sampling-processor.js +4 -0
  188. package/dist/tail-sampling-processor.js.map +1 -0
  189. package/dist/testing.cjs +286 -0
  190. package/dist/testing.cjs.map +1 -0
  191. package/dist/testing.d.cts +291 -0
  192. package/dist/testing.d.ts +291 -0
  193. package/dist/testing.js +263 -0
  194. package/dist/testing.js.map +1 -0
  195. package/dist/trace-context-DRZdUvVY.d.cts +181 -0
  196. package/dist/trace-context-DRZdUvVY.d.ts +181 -0
  197. package/dist/trace-helpers.cjs +54 -0
  198. package/dist/trace-helpers.cjs.map +1 -0
  199. package/dist/trace-helpers.d.cts +524 -0
  200. package/dist/trace-helpers.d.ts +524 -0
  201. package/dist/trace-helpers.js +5 -0
  202. package/dist/trace-helpers.js.map +1 -0
  203. package/dist/tracer-provider.cjs +21 -0
  204. package/dist/tracer-provider.cjs.map +1 -0
  205. package/dist/tracer-provider.d.cts +169 -0
  206. package/dist/tracer-provider.d.ts +169 -0
  207. package/dist/tracer-provider.js +4 -0
  208. package/dist/tracer-provider.js.map +1 -0
  209. package/package.json +280 -0
  210. package/src/baggage-span-processor.test.ts +202 -0
  211. package/src/baggage-span-processor.ts +98 -0
  212. package/src/circuit-breaker.test.ts +341 -0
  213. package/src/circuit-breaker.ts +184 -0
  214. package/src/config.test.ts +94 -0
  215. package/src/config.ts +169 -0
  216. package/src/db.test.ts +252 -0
  217. package/src/db.ts +447 -0
  218. package/src/decorators.test.ts +203 -0
  219. package/src/decorators.ts +188 -0
  220. package/src/env-config.test.ts +246 -0
  221. package/src/env-config.ts +158 -0
  222. package/src/event-queue.test.ts +222 -0
  223. package/src/event-queue.ts +203 -0
  224. package/src/event-subscriber.ts +136 -0
  225. package/src/event-testing.ts +197 -0
  226. package/src/event.test.ts +718 -0
  227. package/src/event.ts +556 -0
  228. package/src/exporters.ts +96 -0
  229. package/src/functional.test.ts +1059 -0
  230. package/src/functional.ts +2295 -0
  231. package/src/http.test.ts +487 -0
  232. package/src/http.ts +424 -0
  233. package/src/index.ts +158 -0
  234. package/src/init.customization.test.ts +210 -0
  235. package/src/init.integrations.test.ts +366 -0
  236. package/src/init.openllmetry.test.ts +282 -0
  237. package/src/init.protocol.test.ts +215 -0
  238. package/src/init.ts +1426 -0
  239. package/src/instrumentation.test.ts +108 -0
  240. package/src/instrumentation.ts +308 -0
  241. package/src/logger.test.ts +117 -0
  242. package/src/logger.ts +246 -0
  243. package/src/metric-helpers.ts +47 -0
  244. package/src/metric-testing.ts +197 -0
  245. package/src/metric.ts +434 -0
  246. package/src/metrics.test.ts +205 -0
  247. package/src/operation-context.ts +93 -0
  248. package/src/processors.ts +106 -0
  249. package/src/rate-limiter.test.ts +199 -0
  250. package/src/rate-limiter.ts +98 -0
  251. package/src/sampling.test.ts +513 -0
  252. package/src/sampling.ts +428 -0
  253. package/src/semantic-helpers.test.ts +311 -0
  254. package/src/semantic-helpers.ts +584 -0
  255. package/src/shutdown.test.ts +311 -0
  256. package/src/shutdown.ts +222 -0
  257. package/src/stub.integration.test.ts +361 -0
  258. package/src/tail-sampling-processor.test.ts +226 -0
  259. package/src/tail-sampling-processor.ts +51 -0
  260. package/src/testing.ts +670 -0
  261. package/src/trace-context.ts +470 -0
  262. package/src/trace-helpers.new.test.ts +278 -0
  263. package/src/trace-helpers.test.ts +242 -0
  264. package/src/trace-helpers.ts +690 -0
  265. package/src/tracer-provider.test.ts +183 -0
  266. package/src/tracer-provider.ts +266 -0
  267. package/src/track.test.ts +153 -0
  268. package/src/track.ts +120 -0
  269. package/src/validation.test.ts +306 -0
  270. package/src/validation.ts +239 -0
  271. package/src/variable-name-inference.test.ts +178 -0
  272. package/src/variable-name-inference.ts +242 -0
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Operation context tracking using AsyncLocalStorage
3
+ *
4
+ * This module provides a way to track operation names across async boundaries
5
+ * so they can be automatically captured in events events.
6
+ *
7
+ * We cannot read span attributes from OpenTelemetry's API (it's write-only),
8
+ * so we maintain our own async context storage.
9
+ */
10
+
11
+ import { AsyncLocalStorage } from 'node:async_hooks';
12
+
13
+ /**
14
+ * Operation context that flows through async operations
15
+ */
16
+ export interface OperationContext {
17
+ /**
18
+ * The name of the current operation
19
+ * This is set by trace() or span() and can be read by events
20
+ */
21
+ name: string;
22
+ }
23
+
24
+ /**
25
+ * AsyncLocalStorage instance for tracking operation context
26
+ */
27
+ const operationStorage = new AsyncLocalStorage<OperationContext>();
28
+
29
+ /**
30
+ * Get the current operation context (if any)
31
+ *
32
+ * @returns The current operation context, or undefined if not in an operation
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * const ctx = getOperationContext();
37
+ * if (ctx) {
38
+ * console.log('Current operation:', ctx.name);
39
+ * }
40
+ * ```
41
+ */
42
+ export function getOperationContext(): OperationContext | undefined {
43
+ return operationStorage.getStore();
44
+ }
45
+
46
+ /**
47
+ * Run a function within an operation context
48
+ *
49
+ * This sets the operation name for the duration of the function execution,
50
+ * including all async operations spawned from it.
51
+ *
52
+ * @param name - The operation name to set
53
+ * @param fn - The function to execute within the context
54
+ * @returns The result of the function
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * const result = await runInOperationContext('user.create', async () => {
59
+ * // Any events.trackEvent() calls here will automatically capture
60
+ * // 'operation.name': 'user.create'
61
+ * await createUser();
62
+ * return 'success';
63
+ * });
64
+ * ```
65
+ */
66
+ export function runInOperationContext<T>(name: string, fn: () => T): T {
67
+ return operationStorage.run({ name }, fn);
68
+ }
69
+
70
+ /**
71
+ * Update the operation name in the current context
72
+ *
73
+ * This is useful when you want to change the operation name within
74
+ * an already-established context (e.g., when entering a nested span).
75
+ *
76
+ * @param name - The new operation name
77
+ *
78
+ * @example
79
+ * ```typescript
80
+ * runInOperationContext('parent.operation', () => {
81
+ * // operation.name is 'parent.operation'
82
+ *
83
+ * updateOperationName('nested.operation');
84
+ * // operation.name is now 'nested.operation'
85
+ * });
86
+ * ```
87
+ */
88
+ export function updateOperationName(name: string): void {
89
+ const store = operationStorage.getStore();
90
+ if (store) {
91
+ store.name = name;
92
+ }
93
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * OpenTelemetry Span Processors
3
+ *
4
+ * Re-exports commonly-needed OpenTelemetry span processors for custom configurations.
5
+ *
6
+ * These processors are already included in autotel's dependencies, so re-exporting
7
+ * them provides a "one install is all you need" developer experience without any
8
+ * bundle size impact.
9
+ *
10
+ * Use these when you need custom span processing logic beyond what `init()` provides.
11
+ *
12
+ * @example Simple processor (synchronous, for testing)
13
+ * ```typescript
14
+ * import { init } from 'autotel'
15
+ * import { InMemorySpanExporter } from 'autotel/exporters'
16
+ * import { SimpleSpanProcessor } from 'autotel/processors'
17
+ *
18
+ * const exporter = new InMemorySpanExporter()
19
+ * init({
20
+ * service: 'test',
21
+ * spanProcessor: new SimpleSpanProcessor(exporter),
22
+ * })
23
+ * ```
24
+ *
25
+ * @example Batch processor (async batching, for production)
26
+ * ```typescript
27
+ * import { init } from 'autotel'
28
+ * import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
29
+ * import { BatchSpanProcessor } from 'autotel/processors'
30
+ *
31
+ * const exporter = new OTLPTraceExporter({ url: 'http://collector:4318/v1/traces' })
32
+ * init({
33
+ * service: 'my-app',
34
+ * spanProcessor: new BatchSpanProcessor(exporter, {
35
+ * maxQueueSize: 2048,
36
+ * scheduledDelayMillis: 5000,
37
+ * exportTimeoutMillis: 30000,
38
+ * maxExportBatchSize: 512,
39
+ * }),
40
+ * })
41
+ * ```
42
+ *
43
+ * Note: Most users don't need to use processors directly - `init()` configures
44
+ * BatchSpanProcessor by default. Use these when you need custom processing logic.
45
+ *
46
+ * @module autotel/processors
47
+ * @see {@link https://opentelemetry.io/docs/specs/otel/trace/sdk/#span-processor | OTel Span Processor Spec}
48
+ */
49
+
50
+ export {
51
+ /**
52
+ * Simple span processor - processes spans synchronously.
53
+ *
54
+ * Perfect for:
55
+ * - Unit testing (synchronous span export)
56
+ * - Development (immediate span visibility)
57
+ * - Debugging (no batching delays)
58
+ *
59
+ * How it works:
60
+ * - Spans are exported immediately when they end
61
+ * - No batching or queuing
62
+ * - Blocking (export happens on the same thread)
63
+ *
64
+ * Warning: Not recommended for production - use BatchSpanProcessor instead.
65
+ * SimpleSpanProcessor can impact performance in high-throughput scenarios.
66
+ *
67
+ * @example
68
+ * ```typescript
69
+ * import { SimpleSpanProcessor } from 'autotel/processors'
70
+ * import { ConsoleSpanExporter } from 'autotel/exporters'
71
+ *
72
+ * const processor = new SimpleSpanProcessor(new ConsoleSpanExporter())
73
+ * ```
74
+ */
75
+ SimpleSpanProcessor,
76
+
77
+ /**
78
+ * Batch span processor - batches spans before exporting.
79
+ *
80
+ * Perfect for:
81
+ * - Production use (efficient, non-blocking)
82
+ * - High-throughput applications
83
+ * - Custom export configurations
84
+ *
85
+ * How it works:
86
+ * - Spans are queued in memory
87
+ * - Exported in batches at regular intervals
88
+ * - Non-blocking (export happens on background thread)
89
+ * - Configurable batch size, delay, queue size
90
+ *
91
+ * This is the default processor used by `init()`.
92
+ *
93
+ * @example Custom configuration
94
+ * ```typescript
95
+ * import { BatchSpanProcessor } from 'autotel/processors'
96
+ *
97
+ * const processor = new BatchSpanProcessor(exporter, {
98
+ * maxQueueSize: 4096, // Max spans in queue
99
+ * scheduledDelayMillis: 10000, // Export every 10s
100
+ * exportTimeoutMillis: 30000, // 30s export timeout
101
+ * maxExportBatchSize: 1024, // Max 1024 spans per batch
102
+ * })
103
+ * ```
104
+ */
105
+ BatchSpanProcessor,
106
+ } from '@opentelemetry/sdk-trace-base';
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Tests for token bucket rate limiter
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
6
+ import { TokenBucketRateLimiter } from './rate-limiter';
7
+
8
+ describe('TokenBucketRateLimiter', () => {
9
+ beforeEach(() => {
10
+ vi.useFakeTimers();
11
+ });
12
+
13
+ describe('tryConsume()', () => {
14
+ it('should allow events within rate limit', () => {
15
+ const limiter = new TokenBucketRateLimiter({
16
+ maxEventsPerSecond: 10,
17
+ burstCapacity: 20,
18
+ });
19
+
20
+ // Should allow first 20 events (burst capacity)
21
+ for (let i = 0; i < 20; i++) {
22
+ expect(limiter.tryConsume()).toBe(true);
23
+ }
24
+
25
+ // 21st event should be rejected
26
+ expect(limiter.tryConsume()).toBe(false);
27
+ });
28
+
29
+ it('should refill tokens over time', () => {
30
+ const limiter = new TokenBucketRateLimiter({
31
+ maxEventsPerSecond: 10, // 10 events/sec = 1 event/100ms
32
+ burstCapacity: 10,
33
+ });
34
+
35
+ // Consume all tokens
36
+ for (let i = 0; i < 10; i++) {
37
+ expect(limiter.tryConsume()).toBe(true);
38
+ }
39
+
40
+ // Should be rate limited
41
+ expect(limiter.tryConsume()).toBe(false);
42
+
43
+ // Advance time by 100ms (1 token should be added)
44
+ vi.advanceTimersByTime(100);
45
+
46
+ // Should allow 1 more event
47
+ expect(limiter.tryConsume()).toBe(true);
48
+ expect(limiter.tryConsume()).toBe(false);
49
+
50
+ // Advance time by 500ms (5 tokens should be added)
51
+ vi.advanceTimersByTime(500);
52
+
53
+ // Should allow 5 more events
54
+ for (let i = 0; i < 5; i++) {
55
+ expect(limiter.tryConsume()).toBe(true);
56
+ }
57
+ expect(limiter.tryConsume()).toBe(false);
58
+ });
59
+
60
+ it('should not exceed max tokens', () => {
61
+ const limiter = new TokenBucketRateLimiter({
62
+ maxEventsPerSecond: 10,
63
+ burstCapacity: 20,
64
+ });
65
+
66
+ // Wait a long time
67
+ vi.advanceTimersByTime(10_000);
68
+
69
+ // Should only have 20 tokens (burstCapacity), not more
70
+ expect(limiter.getAvailableTokens()).toBe(20);
71
+ });
72
+
73
+ it('should consume multiple tokens at once', () => {
74
+ const limiter = new TokenBucketRateLimiter({
75
+ maxEventsPerSecond: 100,
76
+ burstCapacity: 200,
77
+ });
78
+
79
+ // Consume 50 tokens at once
80
+ expect(limiter.tryConsume(50)).toBe(true);
81
+ expect(limiter.getAvailableTokens()).toBe(150);
82
+
83
+ // Consume another 150 tokens
84
+ expect(limiter.tryConsume(150)).toBe(true);
85
+ expect(limiter.getAvailableTokens()).toBe(0);
86
+
87
+ // Should reject request for 1 token
88
+ expect(limiter.tryConsume(1)).toBe(false);
89
+ });
90
+ });
91
+
92
+ describe('waitForToken()', () => {
93
+ it('should wait until token is available', async () => {
94
+ const limiter = new TokenBucketRateLimiter({
95
+ maxEventsPerSecond: 10, // 1 token every 100ms
96
+ burstCapacity: 1,
97
+ });
98
+
99
+ // Consume the only token
100
+ expect(limiter.tryConsume()).toBe(true);
101
+ expect(limiter.tryConsume()).toBe(false);
102
+
103
+ // Wait for next token
104
+ const promise = limiter.waitForToken();
105
+
106
+ // Advance time by 100ms
107
+ vi.advanceTimersByTime(100);
108
+
109
+ // Should resolve after 100ms
110
+ await promise;
111
+
112
+ // Token should be consumed
113
+ expect(limiter.tryConsume()).toBe(false);
114
+ });
115
+
116
+ it('should calculate correct wait time for multiple tokens', async () => {
117
+ const limiter = new TokenBucketRateLimiter({
118
+ maxEventsPerSecond: 10, // 1 token every 100ms
119
+ burstCapacity: 10,
120
+ });
121
+
122
+ // Consume all tokens
123
+ expect(limiter.tryConsume(10)).toBe(true);
124
+
125
+ // Request 5 tokens (should wait 500ms)
126
+ const promise = limiter.waitForToken(5);
127
+
128
+ // Advance by 400ms (not enough)
129
+ vi.advanceTimersByTime(400);
130
+
131
+ // Promise should not resolve yet
132
+ let resolved = false;
133
+ promise.then(() => {
134
+ resolved = true;
135
+ });
136
+
137
+ await vi.runAllTimersAsync();
138
+
139
+ // Should be resolved now
140
+ expect(resolved).toBe(true);
141
+ });
142
+ });
143
+
144
+ describe('getAvailableTokens()', () => {
145
+ it('should return current token count', () => {
146
+ const limiter = new TokenBucketRateLimiter({
147
+ maxEventsPerSecond: 100,
148
+ burstCapacity: 200,
149
+ });
150
+
151
+ expect(limiter.getAvailableTokens()).toBe(200);
152
+
153
+ limiter.tryConsume(50);
154
+ expect(limiter.getAvailableTokens()).toBe(150);
155
+
156
+ // Advance time by 100ms (10 tokens added)
157
+ vi.advanceTimersByTime(100);
158
+ expect(limiter.getAvailableTokens()).toBe(160);
159
+ });
160
+ });
161
+
162
+ describe('reset()', () => {
163
+ it('should reset to full capacity', () => {
164
+ const limiter = new TokenBucketRateLimiter({
165
+ maxEventsPerSecond: 10,
166
+ burstCapacity: 20,
167
+ });
168
+
169
+ // Consume all tokens
170
+ limiter.tryConsume(20);
171
+ expect(limiter.getAvailableTokens()).toBe(0);
172
+
173
+ // Reset
174
+ limiter.reset();
175
+ expect(limiter.getAvailableTokens()).toBe(20);
176
+ });
177
+ });
178
+
179
+ describe('Burst capacity', () => {
180
+ it('should default to 2x rate if not specified', () => {
181
+ const limiter = new TokenBucketRateLimiter({
182
+ maxEventsPerSecond: 50,
183
+ // burstCapacity not specified
184
+ });
185
+
186
+ // Should have 100 tokens (2x rate)
187
+ expect(limiter.getAvailableTokens()).toBe(100);
188
+ });
189
+
190
+ it('should allow custom burst capacity', () => {
191
+ const limiter = new TokenBucketRateLimiter({
192
+ maxEventsPerSecond: 50,
193
+ burstCapacity: 500, // 10x rate
194
+ });
195
+
196
+ expect(limiter.getAvailableTokens()).toBe(500);
197
+ });
198
+ });
199
+ });
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Token bucket rate limiter for event subscribers
3
+ *
4
+ * Prevents overwhelming downstream events platforms with too many events.
5
+ * Uses token bucket algorithm for smooth rate limiting with burst capacity.
6
+ */
7
+
8
+ export interface RateLimiterConfig {
9
+ /** Maximum events per second (default: 100) */
10
+ maxEventsPerSecond: number;
11
+ /** Burst capacity - max events in a single burst (default: 2x rate) */
12
+ burstCapacity?: number;
13
+ }
14
+
15
+ /**
16
+ * Token bucket rate limiter
17
+ *
18
+ * Allows bursts up to burstCapacity, then smooths to maxEventsPerSecond.
19
+ * Thread-safe for async operations.
20
+ */
21
+ export class TokenBucketRateLimiter {
22
+ private tokens: number;
23
+ private readonly maxTokens: number;
24
+ private readonly refillRate: number; // tokens per millisecond
25
+ private lastRefill: number;
26
+
27
+ constructor(config: RateLimiterConfig) {
28
+ this.maxTokens = config.burstCapacity || config.maxEventsPerSecond * 2;
29
+ this.tokens = this.maxTokens; // Start with full bucket
30
+ this.refillRate = config.maxEventsPerSecond / 1000; // Convert to per-ms
31
+ this.lastRefill = Date.now();
32
+ }
33
+
34
+ /**
35
+ * Try to consume a token (allow an event)
36
+ * Returns true if allowed, false if rate limit exceeded
37
+ */
38
+ tryConsume(count = 1): boolean {
39
+ this.refill();
40
+
41
+ if (this.tokens >= count) {
42
+ this.tokens -= count;
43
+ return true;
44
+ }
45
+
46
+ return false;
47
+ }
48
+
49
+ /**
50
+ * Wait until a token is available (async rate limiting)
51
+ * Returns a promise that resolves when the event can be processed
52
+ */
53
+ async waitForToken(count = 1): Promise<void> {
54
+ this.refill();
55
+
56
+ if (this.tokens >= count) {
57
+ this.tokens -= count;
58
+ return;
59
+ }
60
+
61
+ // Calculate wait time until we have enough tokens
62
+ const tokensNeeded = count - this.tokens;
63
+ const waitMs = Math.ceil(tokensNeeded / this.refillRate);
64
+
65
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
66
+
67
+ // After waiting, try again (recursive)
68
+ return this.waitForToken(count);
69
+ }
70
+
71
+ /**
72
+ * Refill tokens based on elapsed time
73
+ */
74
+ private refill(): void {
75
+ const now = Date.now();
76
+ const elapsed = now - this.lastRefill;
77
+ const tokensToAdd = elapsed * this.refillRate;
78
+
79
+ this.tokens = Math.min(this.maxTokens, this.tokens + tokensToAdd);
80
+ this.lastRefill = now;
81
+ }
82
+
83
+ /**
84
+ * Get current available tokens (for testing/debugging)
85
+ */
86
+ getAvailableTokens(): number {
87
+ this.refill();
88
+ return Math.floor(this.tokens);
89
+ }
90
+
91
+ /**
92
+ * Reset the rate limiter (for testing)
93
+ */
94
+ reset(): void {
95
+ this.tokens = this.maxTokens;
96
+ this.lastRefill = Date.now();
97
+ }
98
+ }