@onivoro/server-aws-kinesis 24.30.12 → 24.30.14
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/README.md +161 -551
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @onivoro/server-aws-kinesis
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
AWS Kinesis Data Streams integration for NestJS applications.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -8,660 +8,270 @@ A NestJS module for integrating with AWS Kinesis Data Streams, providing real-ti
|
|
|
8
8
|
npm install @onivoro/server-aws-kinesis
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Overview
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
- **Event Publishing**: Send structured events with partition keys
|
|
15
|
-
- **Stream Management**: Create and manage Kinesis data streams
|
|
16
|
-
- **Partition Key Strategy**: Intelligent partition key generation for data distribution
|
|
17
|
-
- **Error Handling**: Robust error handling for stream operations
|
|
18
|
-
- **Batch Publishing**: Support for batch data publishing
|
|
19
|
-
- **Consumer Support**: Tools for building Kinesis stream consumers
|
|
20
|
-
- **Environment-Based Configuration**: Configurable stream settings per environment
|
|
13
|
+
This library provides a simple AWS Kinesis Data Streams integration for NestJS applications, allowing you to publish data to Kinesis streams.
|
|
21
14
|
|
|
22
|
-
##
|
|
23
|
-
|
|
24
|
-
### 1. Module Configuration
|
|
15
|
+
## Module Setup
|
|
25
16
|
|
|
26
17
|
```typescript
|
|
18
|
+
import { Module } from '@nestjs/common';
|
|
27
19
|
import { ServerAwsKinesisModule } from '@onivoro/server-aws-kinesis';
|
|
28
20
|
|
|
29
21
|
@Module({
|
|
30
22
|
imports: [
|
|
31
|
-
ServerAwsKinesisModule.configure(
|
|
32
|
-
|
|
33
|
-
AWS_KINESIS_NAME: process.env.KINESIS_STREAM_NAME,
|
|
34
|
-
AWS_PROFILE: process.env.AWS_PROFILE || 'default',
|
|
35
|
-
}),
|
|
36
|
-
],
|
|
23
|
+
ServerAwsKinesisModule.configure()
|
|
24
|
+
]
|
|
37
25
|
})
|
|
38
26
|
export class AppModule {}
|
|
39
27
|
```
|
|
40
28
|
|
|
41
|
-
### 2. Basic Usage
|
|
42
|
-
|
|
43
|
-
```typescript
|
|
44
|
-
import { KinesisService } from '@onivoro/server-aws-kinesis';
|
|
45
|
-
|
|
46
|
-
@Injectable()
|
|
47
|
-
export class EventStreamingService {
|
|
48
|
-
constructor(private kinesisService: KinesisService) {}
|
|
49
|
-
|
|
50
|
-
async publishUserEvent(userId: string, eventData: any) {
|
|
51
|
-
const event = {
|
|
52
|
-
eventType: 'USER_ACTION',
|
|
53
|
-
userId,
|
|
54
|
-
timestamp: new Date().toISOString(),
|
|
55
|
-
data: eventData
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
await this.kinesisService.publish(event, userId);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async publishOrderEvent(orderId: string, orderData: any) {
|
|
62
|
-
const event = {
|
|
63
|
-
eventType: 'ORDER_CREATED',
|
|
64
|
-
orderId,
|
|
65
|
-
timestamp: new Date().toISOString(),
|
|
66
|
-
data: orderData
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
// Use orderId as partition key to ensure order events are processed in sequence
|
|
70
|
-
await this.kinesisService.publish(event, orderId);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
```
|
|
74
|
-
|
|
75
29
|
## Configuration
|
|
76
30
|
|
|
77
|
-
|
|
31
|
+
The module uses environment-based configuration:
|
|
78
32
|
|
|
79
33
|
```typescript
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
AWS_REGION = process.env.AWS_REGION || 'us-east-1';
|
|
84
|
-
AWS_KINESIS_NAME = process.env.KINESIS_STREAM_NAME || 'my-data-stream';
|
|
85
|
-
AWS_PROFILE = process.env.AWS_PROFILE || 'default';
|
|
86
|
-
KINESIS_SHARD_COUNT = parseInt(process.env.KINESIS_SHARD_COUNT) || 1;
|
|
87
|
-
KINESIS_RETENTION_PERIOD = parseInt(process.env.KINESIS_RETENTION_PERIOD) || 24; // hours
|
|
34
|
+
export class ServerAwsKinesisConfig {
|
|
35
|
+
AWS_REGION: string;
|
|
36
|
+
AWS_PROFILE?: string; // Optional AWS profile
|
|
88
37
|
}
|
|
89
38
|
```
|
|
90
39
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
```bash
|
|
94
|
-
# AWS Configuration
|
|
95
|
-
AWS_REGION=us-east-1
|
|
96
|
-
AWS_PROFILE=default
|
|
97
|
-
|
|
98
|
-
# Kinesis Configuration
|
|
99
|
-
KINESIS_STREAM_NAME=my-application-stream
|
|
100
|
-
KINESIS_SHARD_COUNT=4
|
|
101
|
-
KINESIS_RETENTION_PERIOD=168 # 7 days in hours
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
## Services
|
|
40
|
+
## Service
|
|
105
41
|
|
|
106
42
|
### KinesisService
|
|
107
43
|
|
|
108
|
-
The main service for Kinesis
|
|
109
|
-
|
|
110
|
-
```typescript
|
|
111
|
-
import { KinesisService } from '@onivoro/server-aws-kinesis';
|
|
112
|
-
|
|
113
|
-
@Injectable()
|
|
114
|
-
export class RealTimeDataService {
|
|
115
|
-
constructor(private kinesisService: KinesisService) {}
|
|
116
|
-
|
|
117
|
-
async publishMetrics(metrics: ApplicationMetrics) {
|
|
118
|
-
const event = {
|
|
119
|
-
type: 'METRICS',
|
|
120
|
-
timestamp: new Date().toISOString(),
|
|
121
|
-
metrics,
|
|
122
|
-
source: 'application-server'
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
// Use timestamp-based partition key for even distribution
|
|
126
|
-
const partitionKey = `metrics-${Date.now() % 1000}`;
|
|
127
|
-
await this.kinesisService.publish(event, partitionKey);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
async publishLogEvent(logLevel: string, message: string, context: any) {
|
|
131
|
-
const logEvent = {
|
|
132
|
-
level: logLevel,
|
|
133
|
-
message,
|
|
134
|
-
context,
|
|
135
|
-
timestamp: new Date().toISOString(),
|
|
136
|
-
service: 'my-service'
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
// Use log level as partition key to group similar logs
|
|
140
|
-
await this.kinesisService.publish(logEvent, logLevel);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
## Usage Examples
|
|
146
|
-
|
|
147
|
-
### Event Publisher Service
|
|
44
|
+
The main service for publishing data to Kinesis streams:
|
|
148
45
|
|
|
149
46
|
```typescript
|
|
47
|
+
import { Injectable } from '@nestjs/common';
|
|
150
48
|
import { KinesisService } from '@onivoro/server-aws-kinesis';
|
|
151
49
|
|
|
152
50
|
@Injectable()
|
|
153
51
|
export class EventPublisherService {
|
|
154
|
-
constructor(private kinesisService: KinesisService) {}
|
|
52
|
+
constructor(private readonly kinesisService: KinesisService) {}
|
|
155
53
|
|
|
156
|
-
async
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
eventType,
|
|
160
|
-
entityId,
|
|
161
|
-
entityType: this.getEntityType(eventType),
|
|
162
|
-
timestamp: new Date().toISOString(),
|
|
163
|
-
version: '1.0',
|
|
54
|
+
async publishEvent(streamName: string, eventData: any) {
|
|
55
|
+
const result = await this.kinesisService.publish({
|
|
56
|
+
streamName,
|
|
164
57
|
data: eventData,
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
correlationId: this.generateCorrelationId()
|
|
168
|
-
}
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
// Use entity ID as partition key to maintain order for the same entity
|
|
172
|
-
await this.kinesisService.publish(event, entityId);
|
|
58
|
+
partitionKey: eventData.id || 'default'
|
|
59
|
+
});
|
|
173
60
|
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
async publishBulkEvents<T>(events: Array<{ eventType: string; entityId: string; data: T }>) {
|
|
178
|
-
const publishPromises = events.map(event =>
|
|
179
|
-
this.publishBusinessEvent(event.eventType, event.entityId, event.data)
|
|
180
|
-
);
|
|
181
|
-
|
|
182
|
-
await Promise.all(publishPromises);
|
|
183
|
-
console.log(`Published ${events.length} events to Kinesis stream`);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
private generateEventId(): string {
|
|
187
|
-
return `evt_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
61
|
+
return result;
|
|
188
62
|
}
|
|
189
63
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
'USER_CREATED': 'user',
|
|
197
|
-
'USER_UPDATED': 'user',
|
|
198
|
-
'ORDER_CREATED': 'order',
|
|
199
|
-
'ORDER_UPDATED': 'order',
|
|
200
|
-
'PAYMENT_PROCESSED': 'payment'
|
|
64
|
+
async publishUserActivity(userId: string, activity: any) {
|
|
65
|
+
const streamName = 'user-activity-stream';
|
|
66
|
+
const data = {
|
|
67
|
+
userId,
|
|
68
|
+
activity,
|
|
69
|
+
timestamp: new Date().toISOString()
|
|
201
70
|
};
|
|
202
71
|
|
|
203
|
-
return
|
|
72
|
+
return await this.kinesisService.publish({
|
|
73
|
+
streamName,
|
|
74
|
+
data,
|
|
75
|
+
partitionKey: userId // Use userId as partition key for ordering
|
|
76
|
+
});
|
|
204
77
|
}
|
|
205
78
|
}
|
|
206
79
|
```
|
|
207
80
|
|
|
208
|
-
|
|
81
|
+
## Method Details
|
|
209
82
|
|
|
210
|
-
|
|
211
|
-
import { KinesisService } from '@onivoro/server-aws-kinesis';
|
|
83
|
+
### publish(params)
|
|
212
84
|
|
|
213
|
-
|
|
214
|
-
export class StreamAnalyticsService {
|
|
215
|
-
constructor(private kinesisService: KinesisService) {}
|
|
85
|
+
The `publish` method accepts an object with the following properties:
|
|
216
86
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
action,
|
|
221
|
-
context,
|
|
222
|
-
timestamp: new Date().toISOString(),
|
|
223
|
-
sessionId: context.sessionId,
|
|
224
|
-
deviceInfo: {
|
|
225
|
-
userAgent: context.userAgent,
|
|
226
|
-
ipAddress: context.ipAddress,
|
|
227
|
-
platform: context.platform
|
|
228
|
-
},
|
|
229
|
-
pageInfo: {
|
|
230
|
-
url: context.url,
|
|
231
|
-
referrer: context.referrer,
|
|
232
|
-
title: context.title
|
|
233
|
-
}
|
|
234
|
-
};
|
|
87
|
+
- **streamName** (string, required): The name of the Kinesis stream
|
|
88
|
+
- **data** (any, required): The data to publish (will be JSON stringified)
|
|
89
|
+
- **partitionKey** (string, required): Used to determine which shard to send the record to
|
|
235
90
|
|
|
236
|
-
|
|
237
|
-
await this.kinesisService.publish(behaviorEvent, userId);
|
|
238
|
-
}
|
|
91
|
+
## Direct Client Access
|
|
239
92
|
|
|
240
|
-
|
|
241
|
-
const performanceEvent = {
|
|
242
|
-
type: 'PERFORMANCE_METRICS',
|
|
243
|
-
metrics: {
|
|
244
|
-
responseTime: metrics.responseTime,
|
|
245
|
-
throughput: metrics.throughput,
|
|
246
|
-
errorRate: metrics.errorRate,
|
|
247
|
-
cpuUsage: metrics.cpuUsage,
|
|
248
|
-
memoryUsage: metrics.memoryUsage
|
|
249
|
-
},
|
|
250
|
-
timestamp: new Date().toISOString(),
|
|
251
|
-
service: metrics.serviceName,
|
|
252
|
-
environment: process.env.NODE_ENV || 'development'
|
|
253
|
-
};
|
|
254
|
-
|
|
255
|
-
// Use service name as partition key
|
|
256
|
-
await this.kinesisService.publish(performanceEvent, metrics.serviceName);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
async publishBusinessInsights(insight: BusinessInsight) {
|
|
260
|
-
const insightEvent = {
|
|
261
|
-
type: 'BUSINESS_INSIGHT',
|
|
262
|
-
category: insight.category,
|
|
263
|
-
metric: insight.metric,
|
|
264
|
-
value: insight.value,
|
|
265
|
-
dimensions: insight.dimensions,
|
|
266
|
-
timestamp: new Date().toISOString(),
|
|
267
|
-
period: insight.period,
|
|
268
|
-
metadata: insight.metadata
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
// Use category as partition key for business insights
|
|
272
|
-
await this.kinesisService.publish(insightEvent, insight.category);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
### Partition Strategy Service
|
|
93
|
+
The service exposes the underlying Kinesis client for advanced operations:
|
|
278
94
|
|
|
279
95
|
```typescript
|
|
280
|
-
import {
|
|
96
|
+
import {
|
|
97
|
+
DescribeStreamCommand,
|
|
98
|
+
ListStreamsCommand,
|
|
99
|
+
GetRecordsCommand,
|
|
100
|
+
GetShardIteratorCommand,
|
|
101
|
+
CreateStreamCommand
|
|
102
|
+
} from '@aws-sdk/client-kinesis';
|
|
281
103
|
|
|
282
104
|
@Injectable()
|
|
283
|
-
export class
|
|
284
|
-
constructor(private kinesisService: KinesisService) {}
|
|
285
|
-
|
|
286
|
-
async publishWithHashPartitioning<T>(data: T, partitionField: string) {
|
|
287
|
-
const partitionKey = this.generateHashPartition(data[partitionField]);
|
|
288
|
-
await this.kinesisService.publish(data, partitionKey);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
async publishWithTimeBasedPartitioning<T>(data: T, timeWindow: number = 60000) {
|
|
292
|
-
// Group events by time windows (default 1 minute)
|
|
293
|
-
const timeSlot = Math.floor(Date.now() / timeWindow);
|
|
294
|
-
const partitionKey = `time_${timeSlot}`;
|
|
295
|
-
await this.kinesisService.publish(data, partitionKey);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
async publishWithCustomPartitioning<T>(data: T, partitionStrategy: PartitionStrategy) {
|
|
299
|
-
let partitionKey: string;
|
|
300
|
-
|
|
301
|
-
switch (partitionStrategy.type) {
|
|
302
|
-
case 'random':
|
|
303
|
-
partitionKey = this.generateRandomPartition(partitionStrategy.shardCount);
|
|
304
|
-
break;
|
|
305
|
-
case 'round-robin':
|
|
306
|
-
partitionKey = this.generateRoundRobinPartition(partitionStrategy.shardCount);
|
|
307
|
-
break;
|
|
308
|
-
case 'field-based':
|
|
309
|
-
partitionKey = data[partitionStrategy.field];
|
|
310
|
-
break;
|
|
311
|
-
case 'composite':
|
|
312
|
-
partitionKey = this.generateCompositePartition(data, partitionStrategy.fields);
|
|
313
|
-
break;
|
|
314
|
-
default:
|
|
315
|
-
partitionKey = 'default';
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
await this.kinesisService.publish(data, partitionKey);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
private generateHashPartition(value: string): string {
|
|
322
|
-
let hash = 0;
|
|
323
|
-
for (let i = 0; i < value.length; i++) {
|
|
324
|
-
const char = value.charCodeAt(i);
|
|
325
|
-
hash = ((hash << 5) - hash) + char;
|
|
326
|
-
hash = hash & hash; // Convert to 32bit integer
|
|
327
|
-
}
|
|
328
|
-
return `hash_${Math.abs(hash)}`;
|
|
329
|
-
}
|
|
105
|
+
export class AdvancedKinesisService {
|
|
106
|
+
constructor(private readonly kinesisService: KinesisService) {}
|
|
330
107
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
108
|
+
// List all Kinesis streams
|
|
109
|
+
async listStreams() {
|
|
110
|
+
const command = new ListStreamsCommand({});
|
|
111
|
+
return await this.kinesisService.kinesisClient.send(command);
|
|
334
112
|
}
|
|
335
113
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
return
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
private generateCompositePartition(data: any, fields: string[]): string {
|
|
345
|
-
const values = fields.map(field => data[field]).join('_');
|
|
346
|
-
return `composite_${values}`;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
private getRoundRobinCounter(): number {
|
|
350
|
-
// Implementation would store counter state
|
|
351
|
-
return 0;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
private incrementRoundRobinCounter(): void {
|
|
355
|
-
// Implementation would increment counter state
|
|
114
|
+
// Describe stream details
|
|
115
|
+
async describeStream(streamName: string) {
|
|
116
|
+
const command = new DescribeStreamCommand({
|
|
117
|
+
StreamName: streamName
|
|
118
|
+
});
|
|
119
|
+
return await this.kinesisService.kinesisClient.send(command);
|
|
356
120
|
}
|
|
357
|
-
}
|
|
358
|
-
```
|
|
359
|
-
|
|
360
|
-
### Stream Management Service
|
|
361
|
-
|
|
362
|
-
```typescript
|
|
363
|
-
import { KinesisClient, CreateStreamCommand, DescribeStreamCommand, DeleteStreamCommand } from '@aws-sdk/client-kinesis';
|
|
364
|
-
|
|
365
|
-
@Injectable()
|
|
366
|
-
export class KinesisStreamManagementService {
|
|
367
|
-
constructor(private kinesisClient: KinesisClient) {}
|
|
368
121
|
|
|
122
|
+
// Create a new stream
|
|
369
123
|
async createStream(streamName: string, shardCount: number = 1) {
|
|
370
|
-
const
|
|
124
|
+
const command = new CreateStreamCommand({
|
|
371
125
|
StreamName: streamName,
|
|
372
126
|
ShardCount: shardCount
|
|
373
127
|
});
|
|
374
|
-
|
|
375
|
-
return this.kinesisClient.send(createStreamCommand);
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
async getStreamStatus(streamName: string) {
|
|
379
|
-
const describeStreamCommand = new DescribeStreamCommand({
|
|
380
|
-
StreamName: streamName
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
return this.kinesisClient.send(describeStreamCommand);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
async waitForStreamActive(streamName: string, maxAttempts: number = 30) {
|
|
387
|
-
let attempts = 0;
|
|
388
|
-
|
|
389
|
-
while (attempts < maxAttempts) {
|
|
390
|
-
const response = await this.getStreamStatus(streamName);
|
|
391
|
-
const status = response.StreamDescription?.StreamStatus;
|
|
392
|
-
|
|
393
|
-
if (status === 'ACTIVE') {
|
|
394
|
-
console.log(`Stream ${streamName} is active`);
|
|
395
|
-
return response;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
if (status === 'DELETING') {
|
|
399
|
-
throw new Error(`Stream ${streamName} is being deleted`);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
console.log(`Stream ${streamName} status: ${status}, waiting...`);
|
|
403
|
-
await this.delay(10000); // Wait 10 seconds
|
|
404
|
-
attempts++;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
throw new Error(`Stream ${streamName} did not become active within timeout`);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
async deleteStream(streamName: string) {
|
|
411
|
-
const deleteStreamCommand = new DeleteStreamCommand({
|
|
412
|
-
StreamName: streamName
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
return this.kinesisClient.send(deleteStreamCommand);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
async ensureStreamExists(streamName: string, shardCount: number = 1) {
|
|
419
|
-
try {
|
|
420
|
-
const response = await this.getStreamStatus(streamName);
|
|
421
|
-
console.log(`Stream ${streamName} already exists with status: ${response.StreamDescription?.StreamStatus}`);
|
|
422
|
-
return response;
|
|
423
|
-
} catch (error: any) {
|
|
424
|
-
if (error.name === 'ResourceNotFoundException') {
|
|
425
|
-
console.log(`Creating stream ${streamName} with ${shardCount} shards`);
|
|
426
|
-
await this.createStream(streamName, shardCount);
|
|
427
|
-
return this.waitForStreamActive(streamName);
|
|
428
|
-
}
|
|
429
|
-
throw error;
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
private delay(ms: number): Promise<void> {
|
|
434
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
128
|
+
return await this.kinesisService.kinesisClient.send(command);
|
|
435
129
|
}
|
|
436
130
|
}
|
|
437
131
|
```
|
|
438
132
|
|
|
439
|
-
##
|
|
440
|
-
|
|
441
|
-
### Stream Consumer Base Class
|
|
133
|
+
## Complete Example
|
|
442
134
|
|
|
443
135
|
```typescript
|
|
444
|
-
import {
|
|
445
|
-
|
|
446
|
-
export abstract class KinesisConsumerBase {
|
|
447
|
-
protected abstract processRecord(record: any): Promise<void>;
|
|
448
|
-
|
|
449
|
-
constructor(
|
|
450
|
-
protected kinesisClient: KinesisClient,
|
|
451
|
-
protected streamName: string
|
|
452
|
-
) {}
|
|
453
|
-
|
|
454
|
-
async startConsuming(shardId: string, iteratorType: string = 'LATEST') {
|
|
455
|
-
try {
|
|
456
|
-
// Get shard iterator
|
|
457
|
-
const shardIteratorResponse = await this.kinesisClient.send(
|
|
458
|
-
new GetShardIteratorCommand({
|
|
459
|
-
StreamName: this.streamName,
|
|
460
|
-
ShardId: shardId,
|
|
461
|
-
ShardIteratorType: iteratorType
|
|
462
|
-
})
|
|
463
|
-
);
|
|
464
|
-
|
|
465
|
-
let shardIterator = shardIteratorResponse.ShardIterator;
|
|
466
|
-
|
|
467
|
-
// Start consuming records
|
|
468
|
-
while (shardIterator) {
|
|
469
|
-
const recordsResponse = await this.kinesisClient.send(
|
|
470
|
-
new GetRecordsCommand({
|
|
471
|
-
ShardIterator: shardIterator
|
|
472
|
-
})
|
|
473
|
-
);
|
|
474
|
-
|
|
475
|
-
const records = recordsResponse.Records || [];
|
|
476
|
-
|
|
477
|
-
if (records.length > 0) {
|
|
478
|
-
console.log(`Processing ${records.length} records`);
|
|
479
|
-
|
|
480
|
-
for (const record of records) {
|
|
481
|
-
try {
|
|
482
|
-
await this.processRecord(record);
|
|
483
|
-
} catch (error) {
|
|
484
|
-
console.error('Error processing record:', error);
|
|
485
|
-
// Implement your error handling strategy here
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
shardIterator = recordsResponse.NextShardIterator;
|
|
491
|
-
|
|
492
|
-
// Add delay to avoid hitting API limits
|
|
493
|
-
await this.delay(1000);
|
|
494
|
-
}
|
|
495
|
-
} catch (error) {
|
|
496
|
-
console.error('Error in consumer loop:', error);
|
|
497
|
-
throw error;
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
private delay(ms: number): Promise<void> {
|
|
502
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
```
|
|
136
|
+
import { Module, Injectable } from '@nestjs/common';
|
|
137
|
+
import { ServerAwsKinesisModule, KinesisService } from '@onivoro/server-aws-kinesis';
|
|
506
138
|
|
|
507
|
-
|
|
139
|
+
@Module({
|
|
140
|
+
imports: [ServerAwsKinesisModule.configure()],
|
|
141
|
+
providers: [OrderEventService],
|
|
142
|
+
exports: [OrderEventService]
|
|
143
|
+
})
|
|
144
|
+
export class OrderModule {}
|
|
508
145
|
|
|
509
|
-
```typescript
|
|
510
146
|
@Injectable()
|
|
511
|
-
export class
|
|
512
|
-
constructor(private kinesisService: KinesisService) {}
|
|
147
|
+
export class OrderEventService {
|
|
148
|
+
constructor(private readonly kinesisService: KinesisService) {}
|
|
513
149
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
async publishWithMetrics<T>(data: T, partitionKey: string, eventType: string) {
|
|
517
|
-
const startTime = Date.now();
|
|
150
|
+
async publishOrderEvent(orderId: string, eventType: string, eventData: any) {
|
|
151
|
+
const streamName = 'order-events-stream';
|
|
518
152
|
|
|
153
|
+
const event = {
|
|
154
|
+
orderId,
|
|
155
|
+
eventType,
|
|
156
|
+
eventData,
|
|
157
|
+
timestamp: new Date().toISOString(),
|
|
158
|
+
version: '1.0'
|
|
159
|
+
};
|
|
160
|
+
|
|
519
161
|
try {
|
|
520
|
-
await this.kinesisService.publish(
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
162
|
+
const result = await this.kinesisService.publish({
|
|
163
|
+
streamName,
|
|
164
|
+
data: event,
|
|
165
|
+
partitionKey: orderId
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
console.log(`Published ${eventType} event for order ${orderId}:`, {
|
|
169
|
+
shardId: result.ShardId,
|
|
170
|
+
sequenceNumber: result.SequenceNumber
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
return result;
|
|
527
174
|
} catch (error) {
|
|
528
|
-
|
|
529
|
-
const duration = endTime - startTime;
|
|
530
|
-
|
|
531
|
-
this.recordMetrics(eventType, duration, 0, 'error');
|
|
175
|
+
console.error(`Failed to publish event for order ${orderId}:`, error);
|
|
532
176
|
throw error;
|
|
533
177
|
}
|
|
534
178
|
}
|
|
535
179
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
size
|
|
541
|
-
});
|
|
180
|
+
// Publish different order events
|
|
181
|
+
async orderCreated(order: any) {
|
|
182
|
+
return this.publishOrderEvent(order.id, 'ORDER_CREATED', order);
|
|
183
|
+
}
|
|
542
184
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
// Publish metrics to monitoring system (e.g., CloudWatch)
|
|
547
|
-
this.publishMetricsToCloudWatch(eventType, duration, size, status);
|
|
185
|
+
async orderUpdated(orderId: string, updates: any) {
|
|
186
|
+
return this.publishOrderEvent(orderId, 'ORDER_UPDATED', updates);
|
|
548
187
|
}
|
|
549
188
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
// This is a placeholder for the actual implementation
|
|
189
|
+
async orderShipped(orderId: string, trackingInfo: any) {
|
|
190
|
+
return this.publishOrderEvent(orderId, 'ORDER_SHIPPED', trackingInfo);
|
|
553
191
|
}
|
|
554
192
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
const oneHourAgo = now - (60 * 60 * 1000);
|
|
558
|
-
|
|
559
|
-
const recentMetrics = this.metricsBuffer.filter(m => m.timestamp > oneHourAgo);
|
|
560
|
-
|
|
561
|
-
return {
|
|
562
|
-
totalEvents: recentMetrics.length,
|
|
563
|
-
totalSize: recentMetrics.reduce((sum, m) => sum + m.size, 0),
|
|
564
|
-
eventsByType: recentMetrics.reduce((acc, m) => {
|
|
565
|
-
acc[m.eventType] = (acc[m.eventType] || 0) + 1;
|
|
566
|
-
return acc;
|
|
567
|
-
}, {} as Record<string, number>)
|
|
568
|
-
};
|
|
193
|
+
async orderDelivered(orderId: string, deliveryInfo: any) {
|
|
194
|
+
return this.publishOrderEvent(orderId, 'ORDER_DELIVERED', deliveryInfo);
|
|
569
195
|
}
|
|
570
196
|
}
|
|
571
197
|
```
|
|
572
198
|
|
|
573
|
-
##
|
|
199
|
+
## Batch Publishing Example
|
|
574
200
|
|
|
575
|
-
|
|
201
|
+
For better performance with multiple records:
|
|
576
202
|
|
|
577
203
|
```typescript
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
// Good: Use hash for even distribution
|
|
582
|
-
const partitionKey = hashFunction(userId) % shardCount;
|
|
204
|
+
@Injectable()
|
|
205
|
+
export class BatchEventService {
|
|
206
|
+
constructor(private readonly kinesisService: KinesisService) {}
|
|
583
207
|
|
|
584
|
-
|
|
585
|
-
//
|
|
586
|
-
|
|
208
|
+
async publishBatch(streamName: string, events: any[]) {
|
|
209
|
+
// Use the exposed client for batch operations
|
|
210
|
+
const records = events.map(event => ({
|
|
211
|
+
Data: Buffer.from(JSON.stringify(event.data)),
|
|
212
|
+
PartitionKey: event.partitionKey
|
|
213
|
+
}));
|
|
587
214
|
|
|
588
|
-
|
|
215
|
+
const command = new PutRecordsCommand({
|
|
216
|
+
StreamName: streamName,
|
|
217
|
+
Records: records
|
|
218
|
+
});
|
|
589
219
|
|
|
590
|
-
|
|
591
|
-
async safePublish<T>(data: T, partitionKey: string, retries: number = 3): Promise<boolean> {
|
|
592
|
-
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
593
|
-
try {
|
|
594
|
-
await this.kinesisService.publish(data, partitionKey);
|
|
595
|
-
return true;
|
|
596
|
-
} catch (error: any) {
|
|
597
|
-
console.error(`Publish attempt ${attempt} failed:`, error);
|
|
598
|
-
|
|
599
|
-
if (attempt === retries) {
|
|
600
|
-
console.error('Max retries reached, publish failed');
|
|
601
|
-
return false;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
// Exponential backoff
|
|
605
|
-
await this.delay(Math.pow(2, attempt) * 1000);
|
|
606
|
-
}
|
|
220
|
+
return await this.kinesisService.kinesisClient.send(command);
|
|
607
221
|
}
|
|
608
|
-
return false;
|
|
609
|
-
}
|
|
610
|
-
```
|
|
611
|
-
|
|
612
|
-
### 3. Data Validation
|
|
613
|
-
|
|
614
|
-
```typescript
|
|
615
|
-
validateEventData<T>(data: T): boolean {
|
|
616
|
-
return data &&
|
|
617
|
-
typeof data === 'object' &&
|
|
618
|
-
JSON.stringify(data).length <= 1000000; // 1MB limit
|
|
619
222
|
}
|
|
620
223
|
```
|
|
621
224
|
|
|
622
|
-
##
|
|
225
|
+
## Environment Variables
|
|
623
226
|
|
|
624
|
-
```
|
|
625
|
-
|
|
626
|
-
|
|
227
|
+
```bash
|
|
228
|
+
# Required: AWS region
|
|
229
|
+
AWS_REGION=us-east-1
|
|
627
230
|
|
|
628
|
-
|
|
629
|
-
|
|
231
|
+
# Optional: AWS profile
|
|
232
|
+
AWS_PROFILE=my-profile
|
|
233
|
+
```
|
|
630
234
|
|
|
631
|
-
|
|
632
|
-
const module: TestingModule = await Test.createTestingModule({
|
|
633
|
-
imports: [ServerAwsKinesisModule.configure({
|
|
634
|
-
AWS_REGION: 'us-east-1',
|
|
635
|
-
AWS_KINESIS_NAME: 'test-stream',
|
|
636
|
-
AWS_PROFILE: 'test'
|
|
637
|
-
})],
|
|
638
|
-
}).compile();
|
|
235
|
+
## AWS Credentials
|
|
639
236
|
|
|
640
|
-
|
|
641
|
-
|
|
237
|
+
The module uses the standard AWS SDK credential chain:
|
|
238
|
+
1. Environment variables
|
|
239
|
+
2. Shared credentials file
|
|
240
|
+
3. IAM roles (for EC2/ECS/Lambda)
|
|
642
241
|
|
|
643
|
-
|
|
644
|
-
expect(service).toBeDefined();
|
|
645
|
-
});
|
|
242
|
+
## Error Handling
|
|
646
243
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
244
|
+
```typescript
|
|
245
|
+
try {
|
|
246
|
+
await kinesisService.publish({
|
|
247
|
+
streamName: 'my-stream',
|
|
248
|
+
data: eventData,
|
|
249
|
+
partitionKey: 'key'
|
|
652
250
|
});
|
|
653
|
-
})
|
|
251
|
+
} catch (error) {
|
|
252
|
+
if (error.name === 'ResourceNotFoundException') {
|
|
253
|
+
console.error('Kinesis stream does not exist');
|
|
254
|
+
} else if (error.name === 'ProvisionedThroughputExceededException') {
|
|
255
|
+
console.error('Rate limit exceeded, implement retry logic');
|
|
256
|
+
}
|
|
257
|
+
}
|
|
654
258
|
```
|
|
655
259
|
|
|
656
|
-
##
|
|
260
|
+
## Limitations
|
|
657
261
|
|
|
658
|
-
|
|
659
|
-
-
|
|
660
|
-
-
|
|
262
|
+
- This library only provides a single `publish` method
|
|
263
|
+
- No built-in support for batch publishing or consumer operations
|
|
264
|
+
- For advanced Kinesis operations, use the exposed `kinesisClient` directly
|
|
265
|
+
- No automatic retry logic for throughput exceptions
|
|
266
|
+
|
|
267
|
+
## Best Practices
|
|
661
268
|
|
|
662
|
-
|
|
663
|
-
|
|
269
|
+
1. **Partition Key Selection**: Choose partition keys that evenly distribute data across shards
|
|
270
|
+
2. **Data Size**: Keep record size under 1 MB (Kinesis limit)
|
|
271
|
+
3. **Error Handling**: Implement retry logic for transient errors
|
|
272
|
+
4. **Monitoring**: Use CloudWatch metrics to monitor stream performance
|
|
273
|
+
5. **Scaling**: Monitor shard metrics and scale as needed
|
|
664
274
|
|
|
665
275
|
## License
|
|
666
276
|
|
|
667
|
-
|
|
277
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onivoro/server-aws-kinesis",
|
|
3
|
-
"version": "24.30.
|
|
3
|
+
"version": "24.30.14",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
"url": "https://github.com/onivoro/monorepo.git"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@onivoro/server-aws-credential-providers": "24.30.
|
|
14
|
-
"@onivoro/server-common": "24.30.
|
|
13
|
+
"@onivoro/server-aws-credential-providers": "24.30.14",
|
|
14
|
+
"@onivoro/server-common": "24.30.14",
|
|
15
15
|
"tslib": "^2.3.0"
|
|
16
16
|
},
|
|
17
17
|
"peerDependencies": {
|