@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.
Files changed (2) hide show
  1. package/README.md +161 -551
  2. package/package.json +3 -3
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @onivoro/server-aws-kinesis
2
2
 
3
- A NestJS module for integrating with AWS Kinesis Data Streams, providing real-time data streaming, event publishing, and stream processing capabilities for your server applications.
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
- ## Features
11
+ ## Overview
12
12
 
13
- - **Real-time Data Streaming**: Publish data to Kinesis streams in real-time
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
- ## Quick Start
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
- AWS_REGION: 'us-east-1',
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
- ### ServerAwsKinesisConfig
31
+ The module uses environment-based configuration:
78
32
 
79
33
  ```typescript
80
- import { ServerAwsKinesisConfig } from '@onivoro/server-aws-kinesis';
81
-
82
- export class AppKinesisConfig extends ServerAwsKinesisConfig {
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
- ### Environment Variables
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 operations:
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 publishBusinessEvent<T>(eventType: string, entityId: string, eventData: T) {
157
- const event = {
158
- eventId: this.generateEventId(),
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
- metadata: {
166
- source: 'business-service',
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
- console.log(`Published ${eventType} event for entity ${entityId}`);
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
- private generateCorrelationId(): string {
191
- return `corr_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
192
- }
193
-
194
- private getEntityType(eventType: string): string {
195
- const entityMappings = {
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 entityMappings[eventType] || 'unknown';
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
- ### Stream Analytics Service
81
+ ## Method Details
209
82
 
210
- ```typescript
211
- import { KinesisService } from '@onivoro/server-aws-kinesis';
83
+ ### publish(params)
212
84
 
213
- @Injectable()
214
- export class StreamAnalyticsService {
215
- constructor(private kinesisService: KinesisService) {}
85
+ The `publish` method accepts an object with the following properties:
216
86
 
217
- async publishUserBehavior(userId: string, action: string, context: any) {
218
- const behaviorEvent = {
219
- userId,
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
- // Use user ID as partition key for user-specific analytics
237
- await this.kinesisService.publish(behaviorEvent, userId);
238
- }
91
+ ## Direct Client Access
239
92
 
240
- async publishPerformanceMetrics(metrics: PerformanceMetrics) {
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 { KinesisService } from '@onivoro/server-aws-kinesis';
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 PartitionStrategyService {
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
- private generateRandomPartition(shardCount: number): string {
332
- const randomNum = Math.floor(Math.random() * shardCount);
333
- return `random_${randomNum}`;
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
- private generateRoundRobinPartition(shardCount: number): string {
337
- // This would need to track state across calls
338
- const counter = this.getRoundRobinCounter();
339
- const partition = counter % shardCount;
340
- this.incrementRoundRobinCounter();
341
- return `rr_${partition}`;
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 createStreamCommand = new CreateStreamCommand({
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
- ## Advanced Usage
440
-
441
- ### Stream Consumer Base Class
133
+ ## Complete Example
442
134
 
443
135
  ```typescript
444
- import { KinesisClient, GetRecordsCommand, GetShardIteratorCommand } from '@aws-sdk/client-kinesis';
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
- ### Monitoring and Metrics
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 KinesisMonitoringService {
512
- constructor(private kinesisService: KinesisService) {}
147
+ export class OrderEventService {
148
+ constructor(private readonly kinesisService: KinesisService) {}
513
149
 
514
- private metricsBuffer: Array<{ eventType: string; timestamp: number; size: number }> = [];
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(data, partitionKey);
521
-
522
- const endTime = Date.now();
523
- const duration = endTime - startTime;
524
- const dataSize = JSON.stringify(data).length;
525
-
526
- this.recordMetrics(eventType, duration, dataSize, 'success');
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
- const endTime = Date.now();
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
- private recordMetrics(eventType: string, duration: number, size: number, status: string) {
537
- this.metricsBuffer.push({
538
- eventType,
539
- timestamp: Date.now(),
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
- // Log metrics
544
- console.log(`Kinesis publish - Type: ${eventType}, Duration: ${duration}ms, Size: ${size} bytes, Status: ${status}`);
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
- private async publishMetricsToCloudWatch(eventType: string, duration: number, size: number, status: string) {
551
- // Implementation would send metrics to CloudWatch
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
- getMetricsSummary() {
556
- const now = Date.now();
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
- ## Best Practices
199
+ ## Batch Publishing Example
574
200
 
575
- ### 1. Partition Key Strategy
201
+ For better performance with multiple records:
576
202
 
577
203
  ```typescript
578
- // Good: Use entity ID for ordered processing
579
- await kinesisService.publish(orderEvent, orderId);
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
- // Avoid: Using timestamp (creates hot shards)
585
- // await kinesisService.publish(event, Date.now().toString());
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
- ### 2. Error Handling
215
+ const command = new PutRecordsCommand({
216
+ StreamName: streamName,
217
+ Records: records
218
+ });
589
219
 
590
- ```typescript
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
- ## Testing
225
+ ## Environment Variables
623
226
 
624
- ```typescript
625
- import { Test, TestingModule } from '@nestjs/testing';
626
- import { ServerAwsKinesisModule, KinesisService } from '@onivoro/server-aws-kinesis';
227
+ ```bash
228
+ # Required: AWS region
229
+ AWS_REGION=us-east-1
627
230
 
628
- describe('KinesisService', () => {
629
- let service: KinesisService;
231
+ # Optional: AWS profile
232
+ AWS_PROFILE=my-profile
233
+ ```
630
234
 
631
- beforeEach(async () => {
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
- service = module.get<KinesisService>(KinesisService);
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
- it('should be defined', () => {
644
- expect(service).toBeDefined();
645
- });
242
+ ## Error Handling
646
243
 
647
- it('should publish event', async () => {
648
- const testData = { test: 'data' };
649
- const partitionKey = 'test-key';
650
-
651
- await expect(service.publish(testData, partitionKey)).resolves.not.toThrow();
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
- ## API Reference
260
+ ## Limitations
657
261
 
658
- ### Exported Classes
659
- - `ServerAwsKinesisConfig`: Configuration class for Kinesis settings
660
- - `ServerAwsKinesisModule`: NestJS module for Kinesis integration
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
- ### Exported Services
663
- - `KinesisService`: Main Kinesis service with data publishing capabilities
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
- This library is licensed under the MIT License. See the LICENSE file in this package for details.
277
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onivoro/server-aws-kinesis",
3
- "version": "24.30.12",
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.12",
14
- "@onivoro/server-common": "24.30.12",
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": {