autotel-subscribers 4.0.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 (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +669 -0
  3. package/dist/amplitude.cjs +2486 -0
  4. package/dist/amplitude.cjs.map +1 -0
  5. package/dist/amplitude.d.cts +49 -0
  6. package/dist/amplitude.d.ts +49 -0
  7. package/dist/amplitude.js +2463 -0
  8. package/dist/amplitude.js.map +1 -0
  9. package/dist/event-subscriber-base-CnF3V56W.d.cts +182 -0
  10. package/dist/event-subscriber-base-CnF3V56W.d.ts +182 -0
  11. package/dist/factories.cjs +16660 -0
  12. package/dist/factories.cjs.map +1 -0
  13. package/dist/factories.d.cts +304 -0
  14. package/dist/factories.d.ts +304 -0
  15. package/dist/factories.js +16624 -0
  16. package/dist/factories.js.map +1 -0
  17. package/dist/index.cjs +16575 -0
  18. package/dist/index.cjs.map +1 -0
  19. package/dist/index.d.cts +179 -0
  20. package/dist/index.d.ts +179 -0
  21. package/dist/index.js +16539 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/middleware.cjs +220 -0
  24. package/dist/middleware.cjs.map +1 -0
  25. package/dist/middleware.d.cts +227 -0
  26. package/dist/middleware.d.ts +227 -0
  27. package/dist/middleware.js +208 -0
  28. package/dist/middleware.js.map +1 -0
  29. package/dist/mixpanel.cjs +2940 -0
  30. package/dist/mixpanel.cjs.map +1 -0
  31. package/dist/mixpanel.d.cts +47 -0
  32. package/dist/mixpanel.d.ts +47 -0
  33. package/dist/mixpanel.js +2932 -0
  34. package/dist/mixpanel.js.map +1 -0
  35. package/dist/posthog.cjs +4115 -0
  36. package/dist/posthog.cjs.map +1 -0
  37. package/dist/posthog.d.cts +299 -0
  38. package/dist/posthog.d.ts +299 -0
  39. package/dist/posthog.js +4113 -0
  40. package/dist/posthog.js.map +1 -0
  41. package/dist/segment.cjs +6822 -0
  42. package/dist/segment.cjs.map +1 -0
  43. package/dist/segment.d.cts +49 -0
  44. package/dist/segment.d.ts +49 -0
  45. package/dist/segment.js +6794 -0
  46. package/dist/segment.js.map +1 -0
  47. package/dist/slack.cjs +368 -0
  48. package/dist/slack.cjs.map +1 -0
  49. package/dist/slack.d.cts +126 -0
  50. package/dist/slack.d.ts +126 -0
  51. package/dist/slack.js +366 -0
  52. package/dist/slack.js.map +1 -0
  53. package/dist/webhook.cjs +100 -0
  54. package/dist/webhook.cjs.map +1 -0
  55. package/dist/webhook.d.cts +53 -0
  56. package/dist/webhook.d.ts +53 -0
  57. package/dist/webhook.js +98 -0
  58. package/dist/webhook.js.map +1 -0
  59. package/examples/quickstart-custom-subscriber.ts +144 -0
  60. package/examples/subscriber-bigquery.ts +219 -0
  61. package/examples/subscriber-databricks.ts +280 -0
  62. package/examples/subscriber-kafka.ts +326 -0
  63. package/examples/subscriber-kinesis.ts +307 -0
  64. package/examples/subscriber-posthog.ts +421 -0
  65. package/examples/subscriber-pubsub.ts +336 -0
  66. package/examples/subscriber-snowflake.ts +232 -0
  67. package/package.json +141 -0
  68. package/src/amplitude.test.ts +231 -0
  69. package/src/amplitude.ts +148 -0
  70. package/src/event-subscriber-base.ts +325 -0
  71. package/src/factories.ts +197 -0
  72. package/src/index.ts +50 -0
  73. package/src/middleware.ts +489 -0
  74. package/src/mixpanel.test.ts +194 -0
  75. package/src/mixpanel.ts +134 -0
  76. package/src/mock-event-subscriber.ts +333 -0
  77. package/src/posthog.test.ts +629 -0
  78. package/src/posthog.ts +530 -0
  79. package/src/segment.test.ts +228 -0
  80. package/src/segment.ts +148 -0
  81. package/src/slack.ts +383 -0
  82. package/src/streaming-event-subscriber.ts +323 -0
  83. package/src/testing/index.ts +37 -0
  84. package/src/testing/mock-webhook-server.ts +242 -0
  85. package/src/testing/subscriber-test-harness.ts +365 -0
  86. package/src/webhook.test.ts +264 -0
  87. package/src/webhook.ts +158 -0
@@ -0,0 +1,98 @@
1
+ // src/webhook.ts
2
+ var WebhookSubscriber = class {
3
+ name = "WebhookSubscriber";
4
+ version = "1.0.0";
5
+ config;
6
+ enabled;
7
+ pendingRequests = /* @__PURE__ */ new Set();
8
+ constructor(config) {
9
+ this.config = config;
10
+ this.enabled = config.enabled ?? true;
11
+ }
12
+ async send(payload) {
13
+ if (!this.enabled) return;
14
+ const maxRetries = this.config.maxRetries ?? 3;
15
+ let lastError;
16
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
17
+ try {
18
+ const response = await fetch(this.config.url, {
19
+ method: "POST",
20
+ headers: {
21
+ "Content-Type": "application/json",
22
+ ...this.config.headers
23
+ },
24
+ body: JSON.stringify(payload)
25
+ });
26
+ if (!response.ok) {
27
+ throw new Error(`Webhook returned ${response.status}: ${response.statusText}`);
28
+ }
29
+ return;
30
+ } catch (error) {
31
+ lastError = error;
32
+ if (attempt < maxRetries - 1) {
33
+ await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempt) * 1e3));
34
+ }
35
+ }
36
+ }
37
+ console.error(`Webhook subscriber failed after ${maxRetries} attempts:`, lastError);
38
+ }
39
+ async trackEvent(name, attributes) {
40
+ const request = this.send({
41
+ type: "event",
42
+ name,
43
+ attributes,
44
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
45
+ });
46
+ this.trackRequest(request);
47
+ await request;
48
+ }
49
+ async trackFunnelStep(funnelName, step, attributes) {
50
+ const request = this.send({
51
+ type: "funnel",
52
+ funnel: funnelName,
53
+ step,
54
+ attributes,
55
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
56
+ });
57
+ this.trackRequest(request);
58
+ await request;
59
+ }
60
+ async trackOutcome(operationName, outcome, attributes) {
61
+ const request = this.send({
62
+ type: "outcome",
63
+ operation: operationName,
64
+ outcome,
65
+ attributes,
66
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
67
+ });
68
+ this.trackRequest(request);
69
+ await request;
70
+ }
71
+ async trackValue(name, value, attributes) {
72
+ const request = this.send({
73
+ type: "value",
74
+ name,
75
+ value,
76
+ attributes,
77
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
78
+ });
79
+ this.trackRequest(request);
80
+ await request;
81
+ }
82
+ trackRequest(request) {
83
+ this.pendingRequests.add(request);
84
+ void request.finally(() => {
85
+ this.pendingRequests.delete(request);
86
+ });
87
+ }
88
+ /** Wait for all pending webhook requests to complete */
89
+ async shutdown() {
90
+ if (this.pendingRequests.size > 0) {
91
+ await Promise.allSettled(this.pendingRequests);
92
+ }
93
+ }
94
+ };
95
+
96
+ export { WebhookSubscriber };
97
+ //# sourceMappingURL=webhook.js.map
98
+ //# sourceMappingURL=webhook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/webhook.ts"],"names":[],"mappings":"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","file":"webhook.js","sourcesContent":["/**\n * Webhook Subscriber for autotel\n *\n * Send events to any webhook endpoint (custom integrations, Zapier, Make.com, etc.).\n *\n * @example\n * ```typescript\n * import { Events } from 'autotel/events';\n * import { WebhookSubscriber } from 'autotel-subscribers/webhook';\n *\n * const events = new Events('checkout', {\n * subscribers: [\n * new WebhookSubscriber({\n * url: 'https://hooks.zapier.com/hooks/catch/...',\n * headers: { 'X-API-Key': 'secret' }\n * })\n * ]\n * });\n *\n * events.trackEvent('order.completed', { userId: '123', amount: 99.99 });\n * ```\n */\n\nimport type {\n EventSubscriber,\n EventAttributes,\n FunnelStatus,\n OutcomeStatus,\n} from 'autotel/event-subscriber';\n\nexport interface WebhookConfig {\n /** Webhook URL */\n url: string;\n /** Optional headers (e.g., API keys) */\n headers?: Record<string, string>;\n /** Enable/disable the subscriber */\n enabled?: boolean;\n /** Retry failed requests (default: 3) */\n maxRetries?: number;\n}\n\nexport class WebhookSubscriber implements EventSubscriber {\n readonly name = 'WebhookSubscriber';\n readonly version = '1.0.0';\n\n private config: WebhookConfig;\n private enabled: boolean;\n private pendingRequests: Set<Promise<void>> = new Set();\n\n constructor(config: WebhookConfig) {\n this.config = config;\n this.enabled = config.enabled ?? true;\n }\n\n private async send(payload: any): Promise<void> {\n if (!this.enabled) return;\n\n const maxRetries = this.config.maxRetries ?? 3;\n let lastError: Error | undefined;\n\n for (let attempt = 0; attempt < maxRetries; attempt++) {\n try {\n const response = await fetch(this.config.url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...this.config.headers,\n },\n body: JSON.stringify(payload),\n });\n\n if (!response.ok) {\n throw new Error(`Webhook returned ${response.status}: ${response.statusText}`);\n }\n\n return; // Success\n } catch (error) {\n lastError = error as Error;\n if (attempt < maxRetries - 1) {\n // Exponential backoff\n await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempt) * 1000));\n }\n }\n }\n\n console.error(`Webhook subscriber failed after ${maxRetries} attempts:`, lastError);\n }\n\n async trackEvent(name: string, attributes?: EventAttributes): Promise<void> {\n const request = this.send({\n type: 'event',\n name,\n attributes,\n timestamp: new Date().toISOString(),\n });\n this.trackRequest(request);\n await request;\n }\n\n async trackFunnelStep(\n funnelName: string,\n step: FunnelStatus,\n attributes?: EventAttributes,\n ): Promise<void> {\n const request = this.send({\n type: 'funnel',\n funnel: funnelName,\n step,\n attributes,\n timestamp: new Date().toISOString(),\n });\n this.trackRequest(request);\n await request;\n }\n\n async trackOutcome(\n operationName: string,\n outcome: OutcomeStatus,\n attributes?: EventAttributes,\n ): Promise<void> {\n const request = this.send({\n type: 'outcome',\n operation: operationName,\n outcome,\n attributes,\n timestamp: new Date().toISOString(),\n });\n this.trackRequest(request);\n await request;\n }\n\n async trackValue(name: string, value: number, attributes?: EventAttributes): Promise<void> {\n const request = this.send({\n type: 'value',\n name,\n value,\n attributes,\n timestamp: new Date().toISOString(),\n });\n this.trackRequest(request);\n await request;\n }\n\n private trackRequest(request: Promise<void>): void {\n this.pendingRequests.add(request);\n void request.finally(() => {\n this.pendingRequests.delete(request);\n });\n }\n\n /** Wait for all pending webhook requests to complete */\n async shutdown(): Promise<void> {\n if (this.pendingRequests.size > 0) {\n await Promise.allSettled(this.pendingRequests);\n }\n }\n}\n\n"]}
@@ -0,0 +1,144 @@
1
+ /**
2
+ * QUICKSTART: Your First Custom Subscriber in 5 Minutes
3
+ *
4
+ * This template shows you EVERYTHING you need to write a custom events subscriber.
5
+ * Just copy-paste this code and replace the console.log statements with your API calls.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { Events } from 'autotel/events';
10
+ * import { MyFirstSubscriber } from './my-first-subscriber';
11
+ *
12
+ * const event =new Event('my-app', {
13
+ * subscribers: [new MyFirstSubscriber()]
14
+ * });
15
+ *
16
+ * events.trackEvent('user.signup', { userId: '123', email: 'user@example.com' });
17
+ * ```
18
+ */
19
+
20
+ import type { EventSubscriber, EventAttributes, FunnelStatus, OutcomeStatus } from '../src/event-subscriber-base';
21
+
22
+ export class MyFirstSubscriber implements EventSubscriber {
23
+ // Required: Subscriber name (shows up in logs)
24
+ readonly name = 'MyFirstSubscriber';
25
+
26
+ // Optional: Version for debugging
27
+ readonly version = '1.0.0';
28
+
29
+ /**
30
+ * Track a business event (e.g., "user.signup", "order.completed")
31
+ */
32
+ async trackEvent(name: string, attributes?: EventAttributes): Promise<void> {
33
+ console.log('📊 EVENT:', name, attributes);
34
+
35
+ // TODO: Replace with your API call
36
+ // Example:
37
+ // await fetch('https://your-api.com/events', {
38
+ // method: 'POST',
39
+ // headers: { 'Content-Type': 'application/json' },
40
+ // body: JSON.stringify({ event: name, properties: attributes })
41
+ // });
42
+ }
43
+
44
+ /**
45
+ * Track a funnel step (e.g., checkout flow: "started" → "completed")
46
+ */
47
+ async trackFunnelStep(
48
+ funnel: string,
49
+ step: FunnelStatus,
50
+ attributes?: EventAttributes
51
+ ): Promise<void> {
52
+ console.log('🔄 FUNNEL:', funnel, step, attributes);
53
+
54
+ // TODO: Replace with your API call
55
+ // Most platforms just treat this as a regular event:
56
+ // await this.trackEvent(`${funnel}.${step}`, { ...attributes, funnel, step });
57
+ }
58
+
59
+ /**
60
+ * Track an operation outcome (e.g., payment: "success" or "failure")
61
+ */
62
+ async trackOutcome(
63
+ operation: string,
64
+ outcome: OutcomeStatus,
65
+ attributes?: EventAttributes
66
+ ): Promise<void> {
67
+ console.log('✅ OUTCOME:', operation, outcome, attributes);
68
+
69
+ // TODO: Replace with your API call
70
+ // await this.trackEvent(`${operation}.${outcome}`, { ...attributes, operation, outcome });
71
+ }
72
+
73
+ /**
74
+ * Track a numeric value (e.g., revenue, response time)
75
+ */
76
+ async trackValue(
77
+ name: string,
78
+ value: number,
79
+ attributes?: EventAttributes
80
+ ): Promise<void> {
81
+ console.log('💰 VALUE:', name, value, attributes);
82
+
83
+ // TODO: Replace with your API call
84
+ // await this.trackEvent(name, { ...attributes, value });
85
+ }
86
+
87
+ /**
88
+ * Optional: Cleanup on shutdown
89
+ * Use this to flush buffered events, close connections, etc.
90
+ */
91
+ async shutdown(): Promise<void> {
92
+ console.log('👋 SHUTDOWN');
93
+
94
+ // TODO: Add cleanup logic
95
+ // Example:
96
+ // await this.flushBuffer();
97
+ // await this.httpClient.close();
98
+ }
99
+ }
100
+
101
+ // ============================================================================
102
+ // USAGE EXAMPLE
103
+ // ============================================================================
104
+
105
+ // Uncomment to test:
106
+ // import { Events } from 'autotel/events';
107
+ //
108
+ // const events = new Events('my-app', {
109
+ // subscribers: [new MyFirstSubscriber()]
110
+ // });
111
+ //
112
+ // // Track events
113
+ // await events.trackEvent('user.signup', {
114
+ // userId: 'user-123',
115
+ // email: 'user@example.com',
116
+ // plan: 'pro'
117
+ // });
118
+ //
119
+ // await events.trackFunnelStep('checkout', 'started', {
120
+ // cartValue: 99.99
121
+ // });
122
+ //
123
+ // await events.trackOutcome('payment', 'success', {
124
+ // amount: 99.99,
125
+ // method: 'credit_card'
126
+ // });
127
+ //
128
+ // await events.trackValue('revenue', 99.99, {
129
+ // currency: 'USD',
130
+ // orderId: 'ord-456'
131
+ // });
132
+ //
133
+ // // Cleanup
134
+ // await events.shutdown();
135
+
136
+ // ============================================================================
137
+ // NEXT STEPS
138
+ // ============================================================================
139
+
140
+ // 1. Copy this file to your project
141
+ // 2. Rename "MyFirstSubscriber" to your service name (e.g., "AmplitudeSubscriber")
142
+ // 3. Replace console.log with your API calls
143
+ // 4. Test it! (see examples/testing-custom-adapter.ts)
144
+ // 5. Add error handling and retry logic (see docs/adapter-guide.md)
@@ -0,0 +1,219 @@
1
+ /**
2
+ * BigQuery Subscriber Example
3
+ *
4
+ * Sends events events to Google BigQuery data warehouse.
5
+ * This is a complete, production-ready implementation.
6
+ *
7
+ * Installation:
8
+ * ```bash
9
+ * pnpm add @google-cloud/bigquery
10
+ * ```
11
+ *
12
+ * Setup BigQuery table:
13
+ * ```sql
14
+ * CREATE TABLE `project.dataset.events_events` (
15
+ * event_id STRING NOT NULL,
16
+ * event_type STRING NOT NULL,
17
+ * event_name STRING NOT NULL,
18
+ * attributes JSON,
19
+ * funnel STRING,
20
+ * step STRING,
21
+ * operation STRING,
22
+ * outcome STRING,
23
+ * value NUMERIC,
24
+ * timestamp TIMESTAMP NOT NULL,
25
+ * created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP()
26
+ * )
27
+ * PARTITION BY DATE(timestamp)
28
+ * CLUSTER BY event_type, event_name;
29
+ * ```
30
+ *
31
+ * Setup Authentication:
32
+ * ```bash
33
+ * # Set environment variable
34
+ * export GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account-key.json"
35
+ * ```
36
+ *
37
+ * Usage:
38
+ * ```typescript
39
+ * import { Events } from 'autotel/events';
40
+ * import { BigQuerySubscriber } from './adapter-bigquery';
41
+ *
42
+ * const events = new Events('app', {
43
+ * subscribers: [
44
+ * new BigQuerySubscriber({
45
+ * projectId: 'my-gcp-project',
46
+ * dataset: 'events',
47
+ * table: 'events'
48
+ * })
49
+ * ]
50
+ * });
51
+ *
52
+ * events.trackEvent('order.completed', { orderId: 'ord_123', amount: 99.99 });
53
+ * ```
54
+ */
55
+
56
+ import {
57
+ EventSubscriber,
58
+ type EventPayload,
59
+ } from '../src/event-subscriber-base';
60
+ import { BigQuery } from '@google-cloud/bigquery';
61
+
62
+ export interface BigQuerySubscriberConfig {
63
+ /** GCP Project ID */
64
+ projectId: string;
65
+ /** BigQuery dataset name */
66
+ dataset: string;
67
+ /** BigQuery table name */
68
+ table: string;
69
+ /** Service account key file path (optional, uses GOOGLE_APPLICATION_CREDENTIALS if not set) */
70
+ keyFilename?: string;
71
+ /** Enable/disable subscriber */
72
+ enabled?: boolean;
73
+ /** Batch size (default: 500) */
74
+ batchSize?: number;
75
+ /** Flush interval in ms (default: 10000) */
76
+ flushInterval?: number;
77
+ }
78
+
79
+ export class BigQuerySubscriber extends EventSubscriber {
80
+ readonly name = 'BigQuerySubscriber';
81
+ readonly version = '1.0.0';
82
+
83
+ private client: BigQuery;
84
+ private tableRef: any;
85
+ private config: Required<BigQuerySubscriberConfig>;
86
+ private buffer: EventPayload[] = [];
87
+ private flushIntervalHandle: NodeJS.Timeout | null = null;
88
+
89
+ constructor(config: BigQuerySubscriberConfig) {
90
+ super();
91
+
92
+ // Set defaults
93
+ this.config = {
94
+ keyFilename: '',
95
+ enabled: true,
96
+ batchSize: 500,
97
+ flushInterval: 10_000,
98
+ ...config,
99
+ };
100
+
101
+ this.enabled = this.config.enabled;
102
+
103
+ if (this.enabled) {
104
+ this.initializeClient();
105
+ this.startFlushInterval();
106
+ }
107
+ }
108
+
109
+ private initializeClient(): void {
110
+ try {
111
+ const options: any = {
112
+ projectId: this.config.projectId,
113
+ };
114
+
115
+ if (this.config.keyFilename) {
116
+ options.keyFilename = this.config.keyFilename;
117
+ }
118
+
119
+ this.client = new BigQuery(options);
120
+
121
+ const dataset = this.client.dataset(this.config.dataset);
122
+ this.tableRef = dataset.table(this.config.table);
123
+
124
+ console.log('[BigQuerySubscriber] Initialized successfully');
125
+ } catch (error) {
126
+ console.error('[BigQuerySubscriber] Failed to initialize:', error);
127
+ this.enabled = false;
128
+ }
129
+ }
130
+
131
+ private startFlushInterval(): void {
132
+ this.flushIntervalHandle = setInterval(() => {
133
+ void this.flushBuffer();
134
+ }, this.config.flushInterval);
135
+ }
136
+
137
+ protected async sendToDestination(payload: EventPayload): Promise<void> {
138
+ this.buffer.push(payload);
139
+
140
+ // Auto-flush at batch size
141
+ if (this.buffer.length >= this.config.batchSize) {
142
+ await this.flushBuffer();
143
+ }
144
+ }
145
+
146
+ private async flushBuffer(): Promise<void> {
147
+ if (this.buffer.length === 0) return;
148
+
149
+ const batch = [...this.buffer];
150
+ this.buffer = [];
151
+
152
+ try {
153
+ await this.insertBatch(batch);
154
+ } catch (error) {
155
+ console.error('[BigQuerySubscriber] Failed to flush batch:', error);
156
+ // Re-add to buffer for retry
157
+ this.buffer.unshift(...batch);
158
+ }
159
+ }
160
+
161
+ private async insertBatch(events: EventPayload[]): Promise<void> {
162
+ const rows = events.map((event) => ({
163
+ event_id: crypto.randomUUID(),
164
+ event_type: event.type,
165
+ event_name: event.name,
166
+ attributes: event.attributes || {},
167
+ funnel: event.funnel || null,
168
+ step: event.step || null,
169
+ operation: event.operation || null,
170
+ outcome: event.outcome || null,
171
+ value: event.value || null,
172
+ timestamp: event.timestamp,
173
+ }));
174
+
175
+ // Insert rows
176
+ await this.tableRef.insert(rows, {
177
+ // Skip invalid rows (don't fail entire batch)
178
+ skipInvalidRows: false,
179
+ // Don't ignore unknown values
180
+ ignoreUnknownValues: false,
181
+ });
182
+ }
183
+
184
+ protected handleError(error: Error, payload: EventPayload): void {
185
+ console.error(
186
+ `[BigQuerySubscriber] Failed to send ${payload.type}:`,
187
+ error,
188
+ {
189
+ eventName: payload.name,
190
+ attributes: payload.attributes,
191
+ }
192
+ );
193
+
194
+ // BigQuery-specific error handling
195
+ if (error.message.includes('quota')) {
196
+ console.error('[BigQuerySubscriber] Quota exceeded - consider increasing batchSize or flushInterval');
197
+ }
198
+
199
+ if (error.message.includes('schema')) {
200
+ console.error('[BigQuerySubscriber] Schema mismatch - check table schema');
201
+ }
202
+ }
203
+
204
+ async shutdown(): Promise<void> {
205
+ // Clear flush interval
206
+ if (this.flushIntervalHandle) {
207
+ clearInterval(this.flushIntervalHandle);
208
+ this.flushIntervalHandle = null;
209
+ }
210
+
211
+ // Flush remaining events
212
+ await this.flushBuffer();
213
+
214
+ // Wait for pending requests
215
+ await super.shutdown();
216
+
217
+ console.log('[BigQuerySubscriber] Shutdown complete');
218
+ }
219
+ }