@crossdelta/cloudevents 0.5.7 → 0.6.1
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 +22 -2
- package/dist/index.cjs +1602 -0
- package/dist/index.d.mts +812 -0
- package/dist/index.d.ts +812 -9
- package/dist/index.js +1574 -6
- package/package.json +20 -18
- package/dist/adapters/cloudevents/cloudevents.d.ts +0 -14
- package/dist/adapters/cloudevents/cloudevents.js +0 -58
- package/dist/adapters/cloudevents/index.d.ts +0 -8
- package/dist/adapters/cloudevents/index.js +0 -7
- package/dist/adapters/cloudevents/parsers/binary-mode.d.ts +0 -5
- package/dist/adapters/cloudevents/parsers/binary-mode.js +0 -32
- package/dist/adapters/cloudevents/parsers/pubsub.d.ts +0 -5
- package/dist/adapters/cloudevents/parsers/pubsub.js +0 -54
- package/dist/adapters/cloudevents/parsers/raw-event.d.ts +0 -5
- package/dist/adapters/cloudevents/parsers/raw-event.js +0 -17
- package/dist/adapters/cloudevents/parsers/structured-mode.d.ts +0 -5
- package/dist/adapters/cloudevents/parsers/structured-mode.js +0 -18
- package/dist/adapters/cloudevents/types.d.ts +0 -29
- package/dist/adapters/cloudevents/types.js +0 -1
- package/dist/domain/contract-helper.d.ts +0 -63
- package/dist/domain/contract-helper.js +0 -61
- package/dist/domain/discovery.d.ts +0 -24
- package/dist/domain/discovery.js +0 -201
- package/dist/domain/handler-factory.d.ts +0 -49
- package/dist/domain/handler-factory.js +0 -169
- package/dist/domain/index.d.ts +0 -6
- package/dist/domain/index.js +0 -4
- package/dist/domain/types.d.ts +0 -108
- package/dist/domain/types.js +0 -6
- package/dist/domain/validation.d.ts +0 -37
- package/dist/domain/validation.js +0 -53
- package/dist/infrastructure/errors.d.ts +0 -53
- package/dist/infrastructure/errors.js +0 -54
- package/dist/infrastructure/index.d.ts +0 -4
- package/dist/infrastructure/index.js +0 -2
- package/dist/infrastructure/logging.d.ts +0 -18
- package/dist/infrastructure/logging.js +0 -27
- package/dist/middlewares/cloudevents-middleware.d.ts +0 -171
- package/dist/middlewares/cloudevents-middleware.js +0 -276
- package/dist/middlewares/index.d.ts +0 -1
- package/dist/middlewares/index.js +0 -1
- package/dist/processing/dlq-safe.d.ts +0 -82
- package/dist/processing/dlq-safe.js +0 -108
- package/dist/processing/handler-cache.d.ts +0 -36
- package/dist/processing/handler-cache.js +0 -94
- package/dist/processing/idempotency.d.ts +0 -51
- package/dist/processing/idempotency.js +0 -112
- package/dist/processing/index.d.ts +0 -4
- package/dist/processing/index.js +0 -4
- package/dist/processing/validation.d.ts +0 -41
- package/dist/processing/validation.js +0 -48
- package/dist/publishing/index.d.ts +0 -2
- package/dist/publishing/index.js +0 -2
- package/dist/publishing/nats.publisher.d.ts +0 -19
- package/dist/publishing/nats.publisher.js +0 -115
- package/dist/publishing/pubsub.publisher.d.ts +0 -39
- package/dist/publishing/pubsub.publisher.js +0 -84
- package/dist/transports/nats/base-message-processor.d.ts +0 -44
- package/dist/transports/nats/base-message-processor.js +0 -118
- package/dist/transports/nats/index.d.ts +0 -5
- package/dist/transports/nats/index.js +0 -5
- package/dist/transports/nats/jetstream-consumer.d.ts +0 -217
- package/dist/transports/nats/jetstream-consumer.js +0 -367
- package/dist/transports/nats/jetstream-message-processor.d.ts +0 -9
- package/dist/transports/nats/jetstream-message-processor.js +0 -32
- package/dist/transports/nats/nats-consumer.d.ts +0 -36
- package/dist/transports/nats/nats-consumer.js +0 -84
- package/dist/transports/nats/nats-message-processor.d.ts +0 -11
- package/dist/transports/nats/nats-message-processor.js +0 -32
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DLQ-Safe mode utilities
|
|
3
|
-
* Handles quarantine and error publishing for CloudEvents processing
|
|
4
|
-
*/
|
|
5
|
-
import { logger } from '../infrastructure';
|
|
6
|
-
import { publishRawEvent } from '../publishing';
|
|
7
|
-
/**
|
|
8
|
-
* Checks if DLQ-Safe mode is enabled
|
|
9
|
-
*/
|
|
10
|
-
export const isDlqSafeMode = (options) => {
|
|
11
|
-
return !!(options.quarantineTopic || options.errorTopic);
|
|
12
|
-
};
|
|
13
|
-
/**
|
|
14
|
-
* Publishes a message to the quarantine topic for "poison messages" that can't be processed
|
|
15
|
-
*/
|
|
16
|
-
export const quarantineMessage = async (processingContext, reason, options, error) => {
|
|
17
|
-
if (!options.quarantineTopic) {
|
|
18
|
-
logger.warn('No quarantine topic configured, skipping quarantine');
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
try {
|
|
22
|
-
// Serialize error properly - handle ValidationError specially
|
|
23
|
-
let serializedError;
|
|
24
|
-
if (error) {
|
|
25
|
-
if (typeof error === 'object' && error !== null && 'type' in error && error.type === 'ValidationError') {
|
|
26
|
-
serializedError = JSON.stringify(error, null, 2);
|
|
27
|
-
}
|
|
28
|
-
else if (error instanceof Error) {
|
|
29
|
-
serializedError = JSON.stringify({
|
|
30
|
-
name: error.name,
|
|
31
|
-
message: error.message,
|
|
32
|
-
stack: error.stack,
|
|
33
|
-
}, null, 2);
|
|
34
|
-
}
|
|
35
|
-
else {
|
|
36
|
-
serializedError = JSON.stringify(error, null, 2);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
const quarantineData = {
|
|
40
|
-
originalMessageId: processingContext.messageId,
|
|
41
|
-
originalEventType: processingContext.eventType,
|
|
42
|
-
originalEventData: processingContext.eventData,
|
|
43
|
-
originalEventContext: processingContext.eventContext,
|
|
44
|
-
originalCloudEvent: processingContext.originalCloudEvent,
|
|
45
|
-
quarantinedAt: new Date().toISOString(),
|
|
46
|
-
quarantineReason: reason,
|
|
47
|
-
error: serializedError,
|
|
48
|
-
};
|
|
49
|
-
await publishRawEvent(options.quarantineTopic, 'hono.cloudevents.quarantined', quarantineData, {
|
|
50
|
-
projectId: options.projectId,
|
|
51
|
-
subject: `quarantine.${reason}`,
|
|
52
|
-
source: options.source || 'hono-cloudevents',
|
|
53
|
-
attributes: {
|
|
54
|
-
'quarantine-reason': reason,
|
|
55
|
-
},
|
|
56
|
-
});
|
|
57
|
-
logger.info(`Message ${processingContext.messageId} quarantined: ${reason}`);
|
|
58
|
-
}
|
|
59
|
-
catch (publishError) {
|
|
60
|
-
const errorMessage = publishError instanceof Error ? publishError.message : 'Unknown error';
|
|
61
|
-
logger.error(`Failed to quarantine message ${processingContext.messageId}: ${errorMessage}`);
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
/**
|
|
65
|
-
* Publishes recoverable processing errors to the error topic
|
|
66
|
-
*/
|
|
67
|
-
export const publishRecoverableError = async (processingContext, error, options) => {
|
|
68
|
-
if (!options.errorTopic) {
|
|
69
|
-
logger.warn('No error topic configured, skipping error publishing');
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
try {
|
|
73
|
-
const errorData = {
|
|
74
|
-
originalMessageId: processingContext.messageId,
|
|
75
|
-
originalEventType: processingContext.eventType,
|
|
76
|
-
originalEventData: processingContext.eventData,
|
|
77
|
-
originalEventContext: processingContext.eventContext,
|
|
78
|
-
originalCloudEvent: processingContext.originalCloudEvent,
|
|
79
|
-
errorTimestamp: new Date().toISOString(),
|
|
80
|
-
error: {
|
|
81
|
-
message: error instanceof Error ? error.message : String(error),
|
|
82
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
83
|
-
type: error instanceof Error ? error.constructor.name : typeof error,
|
|
84
|
-
},
|
|
85
|
-
};
|
|
86
|
-
await publishRawEvent(options.errorTopic, 'hono.cloudevents.processing-error', errorData, {
|
|
87
|
-
projectId: options.projectId,
|
|
88
|
-
subject: 'processing.error',
|
|
89
|
-
source: options.source || 'hono-cloudevents',
|
|
90
|
-
});
|
|
91
|
-
logger.info(`Processing error published for message ${processingContext.messageId}`);
|
|
92
|
-
}
|
|
93
|
-
catch (publishError) {
|
|
94
|
-
const errorMessage = publishError instanceof Error ? publishError.message : 'Unknown error';
|
|
95
|
-
logger.error(`Failed to publish error for message ${processingContext.messageId}: ${errorMessage}`);
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
/**
|
|
99
|
-
* Creates processing context from event data
|
|
100
|
-
*/
|
|
101
|
-
export const createProcessingContext = (eventType, eventData, context, originalCloudEvent) => ({
|
|
102
|
-
messageId: context?.messageId || 'unknown',
|
|
103
|
-
eventType,
|
|
104
|
-
eventData,
|
|
105
|
-
eventContext: context,
|
|
106
|
-
timestamp: new Date().toISOString(),
|
|
107
|
-
originalCloudEvent,
|
|
108
|
-
});
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Handler Cache Management
|
|
3
|
-
* Immutable cache operations for CloudEvents handlers
|
|
4
|
-
*/
|
|
5
|
-
import type { ZodTypeAny } from 'zod';
|
|
6
|
-
import { type EnrichedEvent, type HandlerConstructor } from '../domain';
|
|
7
|
-
export interface ProcessedHandler {
|
|
8
|
-
type: string;
|
|
9
|
-
name: string;
|
|
10
|
-
schema: ZodTypeAny;
|
|
11
|
-
handle: (payload: unknown, context?: unknown) => Promise<void>;
|
|
12
|
-
match?: (event: EnrichedEvent<unknown>) => boolean;
|
|
13
|
-
safeParse?: boolean;
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Clears the handler cache. Useful for testing.
|
|
17
|
-
* @internal
|
|
18
|
-
*/
|
|
19
|
-
export declare const clearHandlerCache: () => void;
|
|
20
|
-
export declare const getCachedHandlers: (key: string) => ProcessedHandler[];
|
|
21
|
-
export declare const setCachedHandlers: (key: string, handlers: ProcessedHandler[]) => void;
|
|
22
|
-
export declare const hasCachedHandlers: (key: string) => boolean;
|
|
23
|
-
export declare const isMessageProcessed: (messageId: string) => boolean;
|
|
24
|
-
export declare const addProcessedMessage: (messageId: string) => void;
|
|
25
|
-
/**
|
|
26
|
-
* Converts HandlerConstructor to ProcessedHandler
|
|
27
|
-
*/
|
|
28
|
-
export declare const processHandler: (HandlerClass: HandlerConstructor) => ProcessedHandler | null;
|
|
29
|
-
/**
|
|
30
|
-
* Creates a cache key from configuration with environment awareness
|
|
31
|
-
*/
|
|
32
|
-
export declare const createCacheKey: (discover?: string, handlers?: HandlerConstructor[]) => string;
|
|
33
|
-
/**
|
|
34
|
-
* Setup handlers with caching
|
|
35
|
-
*/
|
|
36
|
-
export declare const setupHandlers: (cacheKey: string, discover?: string, handlers?: HandlerConstructor[], log?: boolean | "pretty" | "structured") => Promise<void>;
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Handler Cache Management
|
|
3
|
-
* Immutable cache operations for CloudEvents handlers
|
|
4
|
-
*/
|
|
5
|
-
import { discoverHandlers, extractTypeFromSchema } from '../domain';
|
|
6
|
-
import { logger } from '../infrastructure/logging';
|
|
7
|
-
const createEmptyCache = () => new Map();
|
|
8
|
-
const createEmptyMessageIds = () => new Set();
|
|
9
|
-
// Global state (necessary for middleware persistence)
|
|
10
|
-
let handlerCache = createEmptyCache();
|
|
11
|
-
let processedMessageIds = createEmptyMessageIds();
|
|
12
|
-
/**
|
|
13
|
-
* Clears the handler cache. Useful for testing.
|
|
14
|
-
* @internal
|
|
15
|
-
*/
|
|
16
|
-
export const clearHandlerCache = () => {
|
|
17
|
-
handlerCache = createEmptyCache();
|
|
18
|
-
processedMessageIds = createEmptyMessageIds();
|
|
19
|
-
};
|
|
20
|
-
// Pure cache operations
|
|
21
|
-
export const getCachedHandlers = (key) => handlerCache.get(key) || [];
|
|
22
|
-
export const setCachedHandlers = (key, handlers) => {
|
|
23
|
-
handlerCache = new Map(handlerCache).set(key, handlers);
|
|
24
|
-
};
|
|
25
|
-
export const hasCachedHandlers = (key) => handlerCache.has(key);
|
|
26
|
-
export const isMessageProcessed = (messageId) => processedMessageIds.has(messageId);
|
|
27
|
-
export const addProcessedMessage = (messageId) => {
|
|
28
|
-
processedMessageIds = new Set(processedMessageIds).add(messageId);
|
|
29
|
-
};
|
|
30
|
-
/**
|
|
31
|
-
* Converts HandlerConstructor to ProcessedHandler
|
|
32
|
-
*/
|
|
33
|
-
export const processHandler = (HandlerClass) => {
|
|
34
|
-
const metadata = HandlerClass.__eventarcMetadata;
|
|
35
|
-
if (!metadata)
|
|
36
|
-
return null;
|
|
37
|
-
const instance = new HandlerClass();
|
|
38
|
-
const eventType = metadata.declaredType || extractTypeFromSchema(metadata.schema);
|
|
39
|
-
if (!eventType)
|
|
40
|
-
return null;
|
|
41
|
-
return {
|
|
42
|
-
type: eventType,
|
|
43
|
-
name: HandlerClass.name,
|
|
44
|
-
schema: metadata.schema,
|
|
45
|
-
handle: async (payload, context) => {
|
|
46
|
-
await Promise.resolve(instance.handle(payload, context));
|
|
47
|
-
},
|
|
48
|
-
match: metadata.match,
|
|
49
|
-
safeParse: metadata.safeParse || false,
|
|
50
|
-
};
|
|
51
|
-
};
|
|
52
|
-
/**
|
|
53
|
-
* Creates a cache key from configuration with environment awareness
|
|
54
|
-
*/
|
|
55
|
-
export const createCacheKey = (discover, handlers = []) => {
|
|
56
|
-
const discoverKey = discover || 'no-discover';
|
|
57
|
-
const handlersKey = handlers
|
|
58
|
-
.map((h) => h.name)
|
|
59
|
-
.sort()
|
|
60
|
-
.join(',') || 'no-handlers';
|
|
61
|
-
const envKey = process.env.NODE_ENV || 'development';
|
|
62
|
-
return `${envKey}:${discoverKey}:${handlersKey}`;
|
|
63
|
-
};
|
|
64
|
-
/**
|
|
65
|
-
* Setup handlers with caching
|
|
66
|
-
*/
|
|
67
|
-
export const setupHandlers = async (cacheKey, discover, handlers = [], log = false) => {
|
|
68
|
-
if (hasCachedHandlers(cacheKey))
|
|
69
|
-
return;
|
|
70
|
-
try {
|
|
71
|
-
const allHandlers = discover ? await discoverHandlers(discover, { log: !!log }) : handlers;
|
|
72
|
-
const processedHandlers = allHandlers.map(processHandler).filter(Boolean);
|
|
73
|
-
setCachedHandlers(cacheKey, processedHandlers);
|
|
74
|
-
if (log && processedHandlers.length > 0) {
|
|
75
|
-
if (log === 'pretty') {
|
|
76
|
-
logger.info(`Discovered ${processedHandlers.length} handler${processedHandlers.length === 1 ? '' : 's'}:`);
|
|
77
|
-
for (const handler of processedHandlers) {
|
|
78
|
-
logger.info(` • ${handler.name}`);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
// structured or true
|
|
83
|
-
logger.info('Handler discovery completed', {
|
|
84
|
-
handlerCount: processedHandlers.length,
|
|
85
|
-
handlers: processedHandlers.map((h) => h.name),
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
catch (error) {
|
|
91
|
-
logger.error('Failed to setup handlers:', error);
|
|
92
|
-
setCachedHandlers(cacheKey, []);
|
|
93
|
-
}
|
|
94
|
-
};
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Idempotency utilities for CloudEvents processing
|
|
3
|
-
*
|
|
4
|
-
* Provides deduplication support to ensure handlers process each message exactly once.
|
|
5
|
-
* Uses CloudEvent ID as the deduplication key.
|
|
6
|
-
*/
|
|
7
|
-
import type { IdempotencyStore } from '../domain/types';
|
|
8
|
-
/**
|
|
9
|
-
* Options for the in-memory idempotency store
|
|
10
|
-
*/
|
|
11
|
-
export interface InMemoryIdempotencyStoreOptions {
|
|
12
|
-
/** Maximum number of message IDs to store (LRU eviction). @default 10000 */
|
|
13
|
-
maxSize?: number;
|
|
14
|
-
/** Default TTL for entries in milliseconds. @default 86400000 (24 hours) */
|
|
15
|
-
defaultTtlMs?: number;
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Creates an in-memory idempotency store with LRU eviction and TTL support.
|
|
19
|
-
*
|
|
20
|
-
* Suitable for single-instance deployments or development. For production
|
|
21
|
-
* multi-instance deployments, use a Redis-based store.
|
|
22
|
-
*
|
|
23
|
-
* @example
|
|
24
|
-
* ```typescript
|
|
25
|
-
* const store = createInMemoryIdempotencyStore({ maxSize: 5000, defaultTtlMs: 3600000 })
|
|
26
|
-
*
|
|
27
|
-
* await consumeJetStreamEvents({
|
|
28
|
-
* stream: 'ORDERS',
|
|
29
|
-
* subjects: ['orders.>'],
|
|
30
|
-
* consumer: 'notifications',
|
|
31
|
-
* discover: `./src/events/**\/*.handler.ts`,
|
|
32
|
-
* idempotencyStore: store,
|
|
33
|
-
* })
|
|
34
|
-
* ```
|
|
35
|
-
*/
|
|
36
|
-
export declare function createInMemoryIdempotencyStore(options?: InMemoryIdempotencyStoreOptions): IdempotencyStore;
|
|
37
|
-
/**
|
|
38
|
-
* Gets or creates the default idempotency store
|
|
39
|
-
*/
|
|
40
|
-
export declare function getDefaultIdempotencyStore(): IdempotencyStore;
|
|
41
|
-
/**
|
|
42
|
-
* Resets the default idempotency store. Useful for testing.
|
|
43
|
-
*/
|
|
44
|
-
export declare function resetDefaultIdempotencyStore(): void;
|
|
45
|
-
/**
|
|
46
|
-
* Checks if a message should be processed (not a duplicate)
|
|
47
|
-
* and marks it as processed if so.
|
|
48
|
-
*
|
|
49
|
-
* @returns true if the message should be processed, false if it's a duplicate
|
|
50
|
-
*/
|
|
51
|
-
export declare function checkAndMarkProcessed(store: IdempotencyStore, messageId: string, ttlMs?: number): Promise<boolean>;
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Idempotency utilities for CloudEvents processing
|
|
3
|
-
*
|
|
4
|
-
* Provides deduplication support to ensure handlers process each message exactly once.
|
|
5
|
-
* Uses CloudEvent ID as the deduplication key.
|
|
6
|
-
*/
|
|
7
|
-
/**
|
|
8
|
-
* Creates an in-memory idempotency store with LRU eviction and TTL support.
|
|
9
|
-
*
|
|
10
|
-
* Suitable for single-instance deployments or development. For production
|
|
11
|
-
* multi-instance deployments, use a Redis-based store.
|
|
12
|
-
*
|
|
13
|
-
* @example
|
|
14
|
-
* ```typescript
|
|
15
|
-
* const store = createInMemoryIdempotencyStore({ maxSize: 5000, defaultTtlMs: 3600000 })
|
|
16
|
-
*
|
|
17
|
-
* await consumeJetStreamEvents({
|
|
18
|
-
* stream: 'ORDERS',
|
|
19
|
-
* subjects: ['orders.>'],
|
|
20
|
-
* consumer: 'notifications',
|
|
21
|
-
* discover: `./src/events/**\/*.handler.ts`,
|
|
22
|
-
* idempotencyStore: store,
|
|
23
|
-
* })
|
|
24
|
-
* ```
|
|
25
|
-
*/
|
|
26
|
-
export function createInMemoryIdempotencyStore(options = {}) {
|
|
27
|
-
const { maxSize = 10_000, defaultTtlMs = 24 * 60 * 60 * 1000 } = options;
|
|
28
|
-
// Use Map for insertion-order iteration (LRU approximation)
|
|
29
|
-
const cache = new Map();
|
|
30
|
-
const evictExpired = () => {
|
|
31
|
-
const now = Date.now();
|
|
32
|
-
for (const [key, entry] of cache) {
|
|
33
|
-
if (entry.expiresAt <= now) {
|
|
34
|
-
cache.delete(key);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
const evictOldest = (count) => {
|
|
39
|
-
const keysToDelete = [...cache.keys()].slice(0, count);
|
|
40
|
-
for (const key of keysToDelete) {
|
|
41
|
-
cache.delete(key);
|
|
42
|
-
}
|
|
43
|
-
};
|
|
44
|
-
return {
|
|
45
|
-
has(messageId) {
|
|
46
|
-
const entry = cache.get(messageId);
|
|
47
|
-
if (!entry)
|
|
48
|
-
return false;
|
|
49
|
-
// Check if expired
|
|
50
|
-
if (entry.expiresAt <= Date.now()) {
|
|
51
|
-
cache.delete(messageId);
|
|
52
|
-
return false;
|
|
53
|
-
}
|
|
54
|
-
// Move to end for LRU (re-insert)
|
|
55
|
-
cache.delete(messageId);
|
|
56
|
-
cache.set(messageId, entry);
|
|
57
|
-
return true;
|
|
58
|
-
},
|
|
59
|
-
add(messageId, ttlMs) {
|
|
60
|
-
// Evict expired entries periodically
|
|
61
|
-
if (cache.size >= maxSize) {
|
|
62
|
-
evictExpired();
|
|
63
|
-
}
|
|
64
|
-
// If still at capacity, evict oldest entries
|
|
65
|
-
if (cache.size >= maxSize) {
|
|
66
|
-
const evictCount = Math.max(1, Math.floor(maxSize * 0.1)); // Evict 10%
|
|
67
|
-
evictOldest(evictCount);
|
|
68
|
-
}
|
|
69
|
-
cache.set(messageId, {
|
|
70
|
-
expiresAt: Date.now() + (ttlMs ?? defaultTtlMs),
|
|
71
|
-
});
|
|
72
|
-
},
|
|
73
|
-
clear() {
|
|
74
|
-
cache.clear();
|
|
75
|
-
},
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Default idempotency store instance.
|
|
80
|
-
* Used when no custom store is provided to consumers.
|
|
81
|
-
*/
|
|
82
|
-
let defaultStore = null;
|
|
83
|
-
/**
|
|
84
|
-
* Gets or creates the default idempotency store
|
|
85
|
-
*/
|
|
86
|
-
export function getDefaultIdempotencyStore() {
|
|
87
|
-
if (!defaultStore) {
|
|
88
|
-
defaultStore = createInMemoryIdempotencyStore();
|
|
89
|
-
}
|
|
90
|
-
return defaultStore;
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* Resets the default idempotency store. Useful for testing.
|
|
94
|
-
*/
|
|
95
|
-
export function resetDefaultIdempotencyStore() {
|
|
96
|
-
defaultStore?.clear?.();
|
|
97
|
-
defaultStore = null;
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* Checks if a message should be processed (not a duplicate)
|
|
101
|
-
* and marks it as processed if so.
|
|
102
|
-
*
|
|
103
|
-
* @returns true if the message should be processed, false if it's a duplicate
|
|
104
|
-
*/
|
|
105
|
-
export async function checkAndMarkProcessed(store, messageId, ttlMs) {
|
|
106
|
-
const isDuplicate = await store.has(messageId);
|
|
107
|
-
if (isDuplicate) {
|
|
108
|
-
return false;
|
|
109
|
-
}
|
|
110
|
-
await store.add(messageId, ttlMs);
|
|
111
|
-
return true;
|
|
112
|
-
}
|
package/dist/processing/index.js
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Validation utilities for CloudEvents processing
|
|
3
|
-
*/
|
|
4
|
-
import type { HandlerValidationError } from '../domain';
|
|
5
|
-
import type { ProcessedHandler } from './handler-cache';
|
|
6
|
-
/**
|
|
7
|
-
* Creates validation details from Zod error
|
|
8
|
-
*/
|
|
9
|
-
export declare const createValidationDetails: (error: {
|
|
10
|
-
issues: Array<{
|
|
11
|
-
code: string;
|
|
12
|
-
message: string;
|
|
13
|
-
path: PropertyKey[];
|
|
14
|
-
expected?: unknown;
|
|
15
|
-
received?: unknown;
|
|
16
|
-
}>;
|
|
17
|
-
}) => {
|
|
18
|
-
code: string;
|
|
19
|
-
message: string;
|
|
20
|
-
path: (string | number)[];
|
|
21
|
-
expected: string;
|
|
22
|
-
received: string;
|
|
23
|
-
}[];
|
|
24
|
-
/**
|
|
25
|
-
* Validates event data against handler schema
|
|
26
|
-
* Returns structured result for both normal and safeParse modes
|
|
27
|
-
*/
|
|
28
|
-
export declare const validateEventData: (handler: ProcessedHandler, eventData: unknown) => {
|
|
29
|
-
success: true;
|
|
30
|
-
} | {
|
|
31
|
-
error: HandlerValidationError;
|
|
32
|
-
shouldSkip: boolean;
|
|
33
|
-
};
|
|
34
|
-
/**
|
|
35
|
-
* Checks if handler matches event
|
|
36
|
-
*/
|
|
37
|
-
export declare const isHandlerMatching: (eventType: string, eventData: unknown, context: unknown) => (handler: ProcessedHandler) => boolean;
|
|
38
|
-
/**
|
|
39
|
-
* Throws validation error in standard mode
|
|
40
|
-
*/
|
|
41
|
-
export declare const throwValidationError: (handlerType: string, error: HandlerValidationError) => never;
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { createValidationError } from '../infrastructure';
|
|
2
|
-
/**
|
|
3
|
-
* Creates validation details from Zod error
|
|
4
|
-
*/
|
|
5
|
-
export const createValidationDetails = (error) => error.issues.map((issue) => ({
|
|
6
|
-
code: issue.code,
|
|
7
|
-
message: issue.message,
|
|
8
|
-
path: issue.path.filter((p) => typeof p !== 'symbol'),
|
|
9
|
-
expected: String(issue.expected),
|
|
10
|
-
received: String(issue.received),
|
|
11
|
-
}));
|
|
12
|
-
/**
|
|
13
|
-
* Validates event data against handler schema
|
|
14
|
-
* Returns structured result for both normal and safeParse modes
|
|
15
|
-
*/
|
|
16
|
-
export const validateEventData = (handler, eventData) => {
|
|
17
|
-
const result = handler.schema.safeParse(eventData);
|
|
18
|
-
if (!result.success) {
|
|
19
|
-
const validationDetails = createValidationDetails(result.error);
|
|
20
|
-
const handlerValidationError = {
|
|
21
|
-
handlerName: handler.name,
|
|
22
|
-
validationErrors: validationDetails,
|
|
23
|
-
};
|
|
24
|
-
return {
|
|
25
|
-
error: handlerValidationError,
|
|
26
|
-
shouldSkip: handler.safeParse || false,
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
return { success: true };
|
|
30
|
-
};
|
|
31
|
-
/**
|
|
32
|
-
* Checks if handler matches event
|
|
33
|
-
*/
|
|
34
|
-
export const isHandlerMatching = (eventType, eventData, context) => (handler) => {
|
|
35
|
-
if (handler.type !== eventType)
|
|
36
|
-
return false;
|
|
37
|
-
if (handler.match) {
|
|
38
|
-
const enrichedEvent = { data: eventData, ...context };
|
|
39
|
-
return handler.match(enrichedEvent);
|
|
40
|
-
}
|
|
41
|
-
return true;
|
|
42
|
-
};
|
|
43
|
-
/**
|
|
44
|
-
* Throws validation error in standard mode
|
|
45
|
-
*/
|
|
46
|
-
export const throwValidationError = (handlerType, error) => {
|
|
47
|
-
throw createValidationError(handlerType, [error]);
|
|
48
|
-
};
|
package/dist/publishing/index.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { ZodTypeAny } from 'zod';
|
|
2
|
-
import { type RoutingConfig } from '../domain';
|
|
3
|
-
export interface PublishNatsEventOptions {
|
|
4
|
-
servers?: string;
|
|
5
|
-
source?: string;
|
|
6
|
-
subject?: string;
|
|
7
|
-
tenantId?: string;
|
|
8
|
-
}
|
|
9
|
-
export declare const deriveSubjectFromType: (eventType: string, config?: RoutingConfig) => string;
|
|
10
|
-
export declare const deriveStreamFromType: (eventType: string, config?: RoutingConfig) => string | undefined;
|
|
11
|
-
export declare const publishNatsRawEvent: (subjectName: string, eventType: string, eventData: unknown, options?: PublishNatsEventOptions) => Promise<string>;
|
|
12
|
-
export declare const publishNatsEvent: <T extends ZodTypeAny>(subjectName: string, schema: T, eventData: unknown, options?: PublishNatsEventOptions) => Promise<string>;
|
|
13
|
-
export declare const publish: (eventTypeOrContract: string | {
|
|
14
|
-
type: string;
|
|
15
|
-
channel?: {
|
|
16
|
-
subject?: string;
|
|
17
|
-
};
|
|
18
|
-
}, eventData: unknown, options?: PublishNatsEventOptions) => Promise<string>;
|
|
19
|
-
export declare const __resetNatsPublisher: () => void;
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { connect, StringCodec } from 'nats';
|
|
2
|
-
import { extractTypeFromSchema } from '../domain';
|
|
3
|
-
import { createValidationError, logger } from '../infrastructure';
|
|
4
|
-
const sc = StringCodec();
|
|
5
|
-
let natsConnectionPromise = null;
|
|
6
|
-
const pluralize = (word) => {
|
|
7
|
-
if (word.endsWith('y') && !['a', 'e', 'i', 'o', 'u'].includes(word[word.length - 2])) {
|
|
8
|
-
return `${word.slice(0, -1)}ies`;
|
|
9
|
-
}
|
|
10
|
-
if (word.endsWith('s') || word.endsWith('sh') || word.endsWith('ch') || word.endsWith('x')) {
|
|
11
|
-
return `${word}es`;
|
|
12
|
-
}
|
|
13
|
-
return `${word}s`;
|
|
14
|
-
};
|
|
15
|
-
const deriveSubjectFromEventType = (eventType) => {
|
|
16
|
-
const parts = eventType.split('.');
|
|
17
|
-
if (parts.length < 2)
|
|
18
|
-
return eventType;
|
|
19
|
-
const domain = parts[0];
|
|
20
|
-
const action = parts.slice(1).join('.');
|
|
21
|
-
const pluralDomain = pluralize(domain);
|
|
22
|
-
return `${pluralDomain}.${action}`;
|
|
23
|
-
};
|
|
24
|
-
const getNatsConnection = async (servers) => {
|
|
25
|
-
if (!natsConnectionPromise) {
|
|
26
|
-
const url = servers ?? process.env.NATS_URL ?? 'nats://localhost:4222';
|
|
27
|
-
natsConnectionPromise = connect({ servers: url })
|
|
28
|
-
.then((connection) => {
|
|
29
|
-
logger.debug(`[NATS] connected to ${url}`);
|
|
30
|
-
return connection;
|
|
31
|
-
})
|
|
32
|
-
.catch((error) => {
|
|
33
|
-
logger.error('[NATS] connection error', error);
|
|
34
|
-
natsConnectionPromise = null;
|
|
35
|
-
throw error;
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
return natsConnectionPromise;
|
|
39
|
-
};
|
|
40
|
-
export const deriveSubjectFromType = (eventType, config) => {
|
|
41
|
-
if (!config?.typeToSubjectMap) {
|
|
42
|
-
return config?.defaultSubjectPrefix ? `${config.defaultSubjectPrefix}.${eventType}` : eventType;
|
|
43
|
-
}
|
|
44
|
-
const sortedPrefixes = Object.keys(config.typeToSubjectMap).sort((a, b) => b.length - a.length);
|
|
45
|
-
for (const prefix of sortedPrefixes) {
|
|
46
|
-
if (eventType.startsWith(prefix)) {
|
|
47
|
-
const suffix = eventType.slice(prefix.length);
|
|
48
|
-
const mappedPrefix = config.typeToSubjectMap[prefix];
|
|
49
|
-
const cleanSuffix = suffix.startsWith('.') ? suffix.slice(1) : suffix;
|
|
50
|
-
return cleanSuffix ? `${mappedPrefix}.${cleanSuffix}` : mappedPrefix;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return config.defaultSubjectPrefix ? `${config.defaultSubjectPrefix}.${eventType}` : eventType;
|
|
54
|
-
};
|
|
55
|
-
export const deriveStreamFromType = (eventType, config) => {
|
|
56
|
-
if (!config?.typeToStreamMap)
|
|
57
|
-
return undefined;
|
|
58
|
-
const sortedPrefixes = Object.keys(config.typeToStreamMap).sort((a, b) => b.length - a.length);
|
|
59
|
-
for (const prefix of sortedPrefixes) {
|
|
60
|
-
if (eventType.startsWith(prefix)) {
|
|
61
|
-
return config.typeToStreamMap[prefix];
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return undefined;
|
|
65
|
-
};
|
|
66
|
-
export const publishNatsRawEvent = async (subjectName, eventType, eventData, options) => {
|
|
67
|
-
const cloudEvent = {
|
|
68
|
-
specversion: '1.0',
|
|
69
|
-
type: eventType,
|
|
70
|
-
source: options?.source || 'hono-service',
|
|
71
|
-
id: crypto.randomUUID(),
|
|
72
|
-
time: new Date().toISOString(),
|
|
73
|
-
datacontenttype: 'application/json',
|
|
74
|
-
data: eventData,
|
|
75
|
-
...(options?.subject && { subject: options.subject }),
|
|
76
|
-
...(options?.tenantId && { tenantid: options.tenantId }),
|
|
77
|
-
};
|
|
78
|
-
const data = JSON.stringify(cloudEvent);
|
|
79
|
-
const nc = await getNatsConnection(options?.servers);
|
|
80
|
-
nc.publish(subjectName, sc.encode(data));
|
|
81
|
-
logger.debug(`Published CloudEvent ${eventType} to NATS subject ${subjectName} (id=${cloudEvent.id})`);
|
|
82
|
-
return cloudEvent.id;
|
|
83
|
-
};
|
|
84
|
-
export const publishNatsEvent = async (subjectName, schema, eventData, options) => {
|
|
85
|
-
const eventType = extractTypeFromSchema(schema);
|
|
86
|
-
if (!eventType) {
|
|
87
|
-
throw new Error('Could not extract event type from schema. Make sure your schema has proper metadata.');
|
|
88
|
-
}
|
|
89
|
-
const validationResult = schema.safeParse(eventData);
|
|
90
|
-
if (!validationResult.success) {
|
|
91
|
-
const validationDetails = validationResult.error.issues.map((issue) => ({
|
|
92
|
-
code: issue.code,
|
|
93
|
-
message: issue.message,
|
|
94
|
-
path: issue.path.filter((p) => typeof p !== 'symbol'),
|
|
95
|
-
expected: 'expected' in issue ? String(issue.expected) : undefined,
|
|
96
|
-
received: 'received' in issue ? String(issue.received) : undefined,
|
|
97
|
-
}));
|
|
98
|
-
const handlerValidationError = {
|
|
99
|
-
handlerName: `NatsPublisher:${eventType}`,
|
|
100
|
-
validationErrors: validationDetails,
|
|
101
|
-
};
|
|
102
|
-
throw createValidationError(eventType, [handlerValidationError]);
|
|
103
|
-
}
|
|
104
|
-
return publishNatsRawEvent(subjectName, eventType, validationResult.data, options);
|
|
105
|
-
};
|
|
106
|
-
export const publish = async (eventTypeOrContract, eventData, options) => {
|
|
107
|
-
const eventType = typeof eventTypeOrContract === 'string' ? eventTypeOrContract : eventTypeOrContract.type;
|
|
108
|
-
const natsSubject = typeof eventTypeOrContract === 'string'
|
|
109
|
-
? deriveSubjectFromEventType(eventTypeOrContract)
|
|
110
|
-
: (eventTypeOrContract.channel?.subject ?? eventTypeOrContract.type);
|
|
111
|
-
return publishNatsRawEvent(natsSubject, eventType, eventData, options);
|
|
112
|
-
};
|
|
113
|
-
export const __resetNatsPublisher = () => {
|
|
114
|
-
natsConnectionPromise = null;
|
|
115
|
-
};
|