@tamyla/clodo-framework 4.3.4 → 4.4.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.
- package/CHANGELOG.md +15 -0
- package/README.md +34 -8
- package/dist/utilities/ai/client.js +276 -0
- package/dist/utilities/ai/index.js +6 -0
- package/dist/utilities/analytics/index.js +6 -0
- package/dist/utilities/analytics/writer.js +226 -0
- package/dist/utilities/bindings/client.js +283 -0
- package/dist/utilities/bindings/index.js +6 -0
- package/dist/utilities/cache/index.js +9 -0
- package/dist/utilities/cache/leaderboard.js +52 -0
- package/dist/utilities/cache/rate-limiter.js +57 -0
- package/dist/utilities/cache/session.js +69 -0
- package/dist/utilities/cache/upstash.js +200 -0
- package/dist/utilities/durable-objects/base.js +200 -0
- package/dist/utilities/durable-objects/counter.js +117 -0
- package/dist/utilities/durable-objects/index.js +10 -0
- package/dist/utilities/durable-objects/rate-limiter.js +80 -0
- package/dist/utilities/durable-objects/session-store.js +126 -0
- package/dist/utilities/durable-objects/websocket-room.js +223 -0
- package/dist/utilities/email/handler.js +359 -0
- package/dist/utilities/email/index.js +6 -0
- package/dist/utilities/index.js +65 -0
- package/dist/utilities/kv/index.js +6 -0
- package/dist/utilities/kv/storage.js +268 -0
- package/dist/utilities/queues/consumer.js +188 -0
- package/dist/utilities/queues/index.js +7 -0
- package/dist/utilities/queues/producer.js +74 -0
- package/dist/utilities/scheduled/handler.js +276 -0
- package/dist/utilities/scheduled/index.js +6 -0
- package/dist/utilities/storage/index.js +6 -0
- package/dist/utilities/storage/r2.js +314 -0
- package/dist/utilities/vectorize/index.js +6 -0
- package/dist/utilities/vectorize/store.js +273 -0
- package/package.json +21 -3
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KV Storage Utilities
|
|
3
|
+
* Convenient wrapper for Cloudflare Workers KV
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* import { KVStorage, KVCache } from '@tamyla/clodo-framework/utilities/kv';
|
|
7
|
+
*
|
|
8
|
+
* const kv = new KVStorage(env.MY_KV);
|
|
9
|
+
* await kv.set('key', { data: 'value' }, { expirationTtl: 3600 });
|
|
10
|
+
* const data = await kv.get('key');
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* KV Storage wrapper with convenience methods
|
|
15
|
+
*/
|
|
16
|
+
export class KVStorage {
|
|
17
|
+
/**
|
|
18
|
+
* @param {KVNamespace} namespace - KV namespace binding
|
|
19
|
+
*/
|
|
20
|
+
constructor(namespace) {
|
|
21
|
+
if (!namespace) {
|
|
22
|
+
throw new Error('KV namespace binding is required');
|
|
23
|
+
}
|
|
24
|
+
this.kv = namespace;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get a value from KV
|
|
29
|
+
* @param {string} key - Key to retrieve
|
|
30
|
+
* @param {Object} options - Get options
|
|
31
|
+
* @param {string} options.type - Return type: 'text', 'json', 'arrayBuffer', 'stream'
|
|
32
|
+
* @returns {Promise<*>}
|
|
33
|
+
*/
|
|
34
|
+
async get(key, options = {}) {
|
|
35
|
+
const type = options.type || 'json';
|
|
36
|
+
return this.kv.get(key, {
|
|
37
|
+
type
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get value with metadata
|
|
43
|
+
* @param {string} key - Key to retrieve
|
|
44
|
+
* @param {Object} options - Get options
|
|
45
|
+
* @returns {Promise<{value: *, metadata: Object}>}
|
|
46
|
+
*/
|
|
47
|
+
async getWithMetadata(key, options = {}) {
|
|
48
|
+
const type = options.type || 'json';
|
|
49
|
+
return this.kv.getWithMetadata(key, {
|
|
50
|
+
type
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Set a value in KV
|
|
56
|
+
* @param {string} key - Key to set
|
|
57
|
+
* @param {*} value - Value to store (objects will be JSON serialized)
|
|
58
|
+
* @param {Object} options - Put options
|
|
59
|
+
* @param {number} options.expirationTtl - TTL in seconds
|
|
60
|
+
* @param {number} options.expiration - Unix timestamp for expiration
|
|
61
|
+
* @param {Object} options.metadata - Custom metadata
|
|
62
|
+
* @returns {Promise<void>}
|
|
63
|
+
*/
|
|
64
|
+
async set(key, value, options = {}) {
|
|
65
|
+
const serialized = typeof value === 'object' ? JSON.stringify(value) : value;
|
|
66
|
+
const putOptions = {};
|
|
67
|
+
if (options.expirationTtl) putOptions.expirationTtl = options.expirationTtl;
|
|
68
|
+
if (options.expiration) putOptions.expiration = options.expiration;
|
|
69
|
+
if (options.metadata) putOptions.metadata = options.metadata;
|
|
70
|
+
await this.kv.put(key, serialized, putOptions);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Delete a key from KV
|
|
75
|
+
* @param {string} key - Key to delete
|
|
76
|
+
* @returns {Promise<void>}
|
|
77
|
+
*/
|
|
78
|
+
async delete(key) {
|
|
79
|
+
await this.kv.delete(key);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* List keys in KV
|
|
84
|
+
* @param {Object} options - List options
|
|
85
|
+
* @param {string} options.prefix - Key prefix to filter by
|
|
86
|
+
* @param {number} options.limit - Maximum keys to return
|
|
87
|
+
* @param {string} options.cursor - Pagination cursor
|
|
88
|
+
* @returns {Promise<{keys: Array, list_complete: boolean, cursor: string}>}
|
|
89
|
+
*/
|
|
90
|
+
async list(options = {}) {
|
|
91
|
+
return this.kv.list({
|
|
92
|
+
prefix: options.prefix,
|
|
93
|
+
limit: options.limit || 1000,
|
|
94
|
+
cursor: options.cursor
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* List all keys (handles pagination)
|
|
100
|
+
* @param {string} prefix - Key prefix to filter by
|
|
101
|
+
* @returns {AsyncGenerator<{name: string, expiration?: number, metadata?: Object}>}
|
|
102
|
+
*/
|
|
103
|
+
async *listAll(prefix = '') {
|
|
104
|
+
let cursor;
|
|
105
|
+
do {
|
|
106
|
+
const result = await this.list({
|
|
107
|
+
prefix,
|
|
108
|
+
cursor
|
|
109
|
+
});
|
|
110
|
+
for (const key of result.keys) {
|
|
111
|
+
yield key;
|
|
112
|
+
}
|
|
113
|
+
cursor = result.list_complete ? null : result.cursor;
|
|
114
|
+
} while (cursor);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Check if a key exists
|
|
119
|
+
* @param {string} key - Key to check
|
|
120
|
+
* @returns {Promise<boolean>}
|
|
121
|
+
*/
|
|
122
|
+
async exists(key) {
|
|
123
|
+
const value = await this.kv.get(key);
|
|
124
|
+
return value !== null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get multiple keys at once
|
|
129
|
+
* @param {string[]} keys - Keys to retrieve
|
|
130
|
+
* @returns {Promise<Map<string, *>>}
|
|
131
|
+
*/
|
|
132
|
+
async getMany(keys) {
|
|
133
|
+
const results = new Map();
|
|
134
|
+
await Promise.all(keys.map(async key => {
|
|
135
|
+
const value = await this.get(key);
|
|
136
|
+
results.set(key, value);
|
|
137
|
+
}));
|
|
138
|
+
return results;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Set multiple key-value pairs
|
|
143
|
+
* @param {Object} entries - Key-value pairs to set
|
|
144
|
+
* @param {Object} options - Put options (applied to all)
|
|
145
|
+
* @returns {Promise<void>}
|
|
146
|
+
*/
|
|
147
|
+
async setMany(entries, options = {}) {
|
|
148
|
+
await Promise.all(Object.entries(entries).map(([key, value]) => this.set(key, value, options)));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* KV-backed cache with TTL and stale-while-revalidate support
|
|
154
|
+
*/
|
|
155
|
+
export class KVCache {
|
|
156
|
+
constructor(namespace, options = {}) {
|
|
157
|
+
this.kv = new KVStorage(namespace);
|
|
158
|
+
this.prefix = options.prefix || 'cache:';
|
|
159
|
+
this.defaultTTL = options.defaultTTL || 3600;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Get a cached value
|
|
164
|
+
*/
|
|
165
|
+
async get(key) {
|
|
166
|
+
return this.kv.get(this.prefix + key);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Set a cached value
|
|
171
|
+
*/
|
|
172
|
+
async set(key, value, ttl = this.defaultTTL) {
|
|
173
|
+
await this.kv.set(this.prefix + key, value, {
|
|
174
|
+
expirationTtl: ttl
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Delete a cached value
|
|
180
|
+
*/
|
|
181
|
+
async delete(key) {
|
|
182
|
+
await this.kv.delete(this.prefix + key);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Get or set with loader function
|
|
187
|
+
*/
|
|
188
|
+
async getOrSet(key, loader, ttl = this.defaultTTL) {
|
|
189
|
+
const cached = await this.get(key);
|
|
190
|
+
if (cached !== null) {
|
|
191
|
+
return cached;
|
|
192
|
+
}
|
|
193
|
+
const value = await loader();
|
|
194
|
+
await this.set(key, value, ttl);
|
|
195
|
+
return value;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Wrap a function with caching
|
|
200
|
+
*/
|
|
201
|
+
wrap(fn, keyFn, ttl = this.defaultTTL) {
|
|
202
|
+
return async (...args) => {
|
|
203
|
+
const key = keyFn(...args);
|
|
204
|
+
return this.getOrSet(key, () => fn(...args), ttl);
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Invalidate cache entries by prefix
|
|
210
|
+
*/
|
|
211
|
+
async invalidatePrefix(prefix) {
|
|
212
|
+
const fullPrefix = this.prefix + prefix;
|
|
213
|
+
for await (const key of this.kv.listAll(fullPrefix)) {
|
|
214
|
+
await this.kv.delete(key.name);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* KV with typed metadata for advanced use cases
|
|
221
|
+
*/
|
|
222
|
+
export class KVWithMetadata {
|
|
223
|
+
constructor(namespace) {
|
|
224
|
+
this.kv = new KVStorage(namespace);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Store value with metadata
|
|
229
|
+
*/
|
|
230
|
+
async set(key, value, metadata, options = {}) {
|
|
231
|
+
await this.kv.set(key, value, {
|
|
232
|
+
...options,
|
|
233
|
+
metadata
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Get value and metadata
|
|
239
|
+
*/
|
|
240
|
+
async get(key) {
|
|
241
|
+
const {
|
|
242
|
+
value,
|
|
243
|
+
metadata
|
|
244
|
+
} = await this.kv.getWithMetadata(key);
|
|
245
|
+
return {
|
|
246
|
+
value,
|
|
247
|
+
metadata: metadata || {}
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Update only metadata (re-stores value)
|
|
253
|
+
*/
|
|
254
|
+
async updateMetadata(key, metadataUpdater, options = {}) {
|
|
255
|
+
const {
|
|
256
|
+
value,
|
|
257
|
+
metadata
|
|
258
|
+
} = await this.get(key);
|
|
259
|
+
if (value === null) return false;
|
|
260
|
+
const newMetadata = typeof metadataUpdater === 'function' ? metadataUpdater(metadata) : {
|
|
261
|
+
...metadata,
|
|
262
|
+
...metadataUpdater
|
|
263
|
+
};
|
|
264
|
+
await this.set(key, value, newMetadata, options);
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
export default KVStorage;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Queue Consumer
|
|
3
|
+
* Process messages from Cloudflare Queues
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* import { QueueConsumer } from '@tamyla/clodo-framework/utilities/queues';
|
|
7
|
+
*
|
|
8
|
+
* export default {
|
|
9
|
+
* async queue(batch, env) {
|
|
10
|
+
* const consumer = new QueueConsumer(batch, env);
|
|
11
|
+
* await consumer.process(async (message) => {
|
|
12
|
+
* console.log('Processing:', message.body);
|
|
13
|
+
* });
|
|
14
|
+
* }
|
|
15
|
+
* }
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export class QueueConsumer {
|
|
19
|
+
/**
|
|
20
|
+
* @param {MessageBatch} batch - The message batch from queue handler
|
|
21
|
+
* @param {Object} env - Environment bindings
|
|
22
|
+
*/
|
|
23
|
+
constructor(batch, env) {
|
|
24
|
+
this.batch = batch;
|
|
25
|
+
this.env = env;
|
|
26
|
+
this.results = new Map();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Process all messages in the batch
|
|
31
|
+
* @param {Function} handler - Async function to handle each message
|
|
32
|
+
* @param {Object} options - Processing options
|
|
33
|
+
*/
|
|
34
|
+
async process(handler, options = {}) {
|
|
35
|
+
const {
|
|
36
|
+
continueOnError = true,
|
|
37
|
+
maxRetries = 3,
|
|
38
|
+
deadLetterQueue = null
|
|
39
|
+
} = options;
|
|
40
|
+
for (const message of this.batch.messages) {
|
|
41
|
+
try {
|
|
42
|
+
await handler(message.body, {
|
|
43
|
+
id: message.id,
|
|
44
|
+
timestamp: message.timestamp,
|
|
45
|
+
attempts: message.attempts
|
|
46
|
+
});
|
|
47
|
+
message.ack();
|
|
48
|
+
this.results.set(message.id, {
|
|
49
|
+
status: 'success'
|
|
50
|
+
});
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error(`Error processing message ${message.id}:`, error);
|
|
53
|
+
if (message.attempts >= maxRetries) {
|
|
54
|
+
// Max retries exceeded
|
|
55
|
+
if (deadLetterQueue) {
|
|
56
|
+
// Send to dead letter queue
|
|
57
|
+
await deadLetterQueue.send({
|
|
58
|
+
originalMessage: message.body,
|
|
59
|
+
error: error.message,
|
|
60
|
+
attempts: message.attempts,
|
|
61
|
+
failedAt: Date.now()
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
message.ack(); // Acknowledge to prevent infinite retry
|
|
65
|
+
this.results.set(message.id, {
|
|
66
|
+
status: 'dead_lettered',
|
|
67
|
+
error: error.message
|
|
68
|
+
});
|
|
69
|
+
} else {
|
|
70
|
+
// Retry
|
|
71
|
+
message.retry();
|
|
72
|
+
this.results.set(message.id, {
|
|
73
|
+
status: 'retrying',
|
|
74
|
+
error: error.message
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (!continueOnError) {
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return this.results;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Process messages with typed handlers
|
|
87
|
+
* @param {Object} handlers - Map of message types to handlers
|
|
88
|
+
* @param {Object} options - Processing options
|
|
89
|
+
*/
|
|
90
|
+
async processTyped(handlers, options = {}) {
|
|
91
|
+
const defaultHandler = handlers.default || (async () => {
|
|
92
|
+
console.warn('No handler for message type');
|
|
93
|
+
});
|
|
94
|
+
return this.process(async (body, meta) => {
|
|
95
|
+
const type = body.type || body._type || 'default';
|
|
96
|
+
const handler = handlers[type] || defaultHandler;
|
|
97
|
+
return handler(body, meta, this.env);
|
|
98
|
+
}, options);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Acknowledge all messages (use with caution)
|
|
103
|
+
*/
|
|
104
|
+
ackAll() {
|
|
105
|
+
this.batch.ackAll();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Retry all messages (use with caution)
|
|
110
|
+
*/
|
|
111
|
+
retryAll() {
|
|
112
|
+
this.batch.retryAll();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get batch statistics
|
|
117
|
+
*/
|
|
118
|
+
getStats() {
|
|
119
|
+
return {
|
|
120
|
+
total: this.batch.messages.length,
|
|
121
|
+
queue: this.batch.queue,
|
|
122
|
+
results: Object.fromEntries(this.results)
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Typed message builder for type-safe queue messages
|
|
129
|
+
*/
|
|
130
|
+
export class MessageBuilder {
|
|
131
|
+
constructor(type) {
|
|
132
|
+
this.message = {
|
|
133
|
+
type
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
data(data) {
|
|
137
|
+
this.message = {
|
|
138
|
+
...this.message,
|
|
139
|
+
...data
|
|
140
|
+
};
|
|
141
|
+
return this;
|
|
142
|
+
}
|
|
143
|
+
meta(meta) {
|
|
144
|
+
this.message._meta = {
|
|
145
|
+
...this.message._meta,
|
|
146
|
+
...meta
|
|
147
|
+
};
|
|
148
|
+
return this;
|
|
149
|
+
}
|
|
150
|
+
priority(priority) {
|
|
151
|
+
this.message._priority = priority;
|
|
152
|
+
return this;
|
|
153
|
+
}
|
|
154
|
+
build() {
|
|
155
|
+
return {
|
|
156
|
+
...this.message,
|
|
157
|
+
_meta: {
|
|
158
|
+
...this.message._meta,
|
|
159
|
+
createdAt: Date.now()
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Create a typed message
|
|
167
|
+
*/
|
|
168
|
+
export function createMessage(type) {
|
|
169
|
+
return new MessageBuilder(type);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Common message type definitions
|
|
174
|
+
*/
|
|
175
|
+
export const MessageTypes = {
|
|
176
|
+
EMAIL_SEND: 'email:send',
|
|
177
|
+
EMAIL_BULK: 'email:bulk',
|
|
178
|
+
NOTIFICATION_PUSH: 'notification:push',
|
|
179
|
+
NOTIFICATION_SMS: 'notification:sms',
|
|
180
|
+
TASK_PROCESS: 'task:process',
|
|
181
|
+
TASK_CLEANUP: 'task:cleanup',
|
|
182
|
+
DATA_IMPORT: 'data:import',
|
|
183
|
+
DATA_EXPORT: 'data:export',
|
|
184
|
+
DATA_SYNC: 'data:sync',
|
|
185
|
+
WEBHOOK_DELIVER: 'webhook:deliver',
|
|
186
|
+
WEBHOOK_RETRY: 'webhook:retry'
|
|
187
|
+
};
|
|
188
|
+
export default QueueConsumer;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Queue Producer
|
|
3
|
+
* Send messages to Cloudflare Queues
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* import { QueueProducer } from '@tamyla/clodo-framework/utilities/queues';
|
|
7
|
+
*
|
|
8
|
+
* const producer = new QueueProducer(env.MY_QUEUE);
|
|
9
|
+
* await producer.send({ type: 'email', to: 'user@example.com' });
|
|
10
|
+
* await producer.sendBatch([msg1, msg2, msg3]);
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export class QueueProducer {
|
|
14
|
+
/**
|
|
15
|
+
* @param {Queue} queue - Queue binding
|
|
16
|
+
*/
|
|
17
|
+
constructor(queue) {
|
|
18
|
+
if (!queue) {
|
|
19
|
+
throw new Error('Queue binding is required');
|
|
20
|
+
}
|
|
21
|
+
this.queue = queue;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Send a single message to the queue
|
|
26
|
+
* @param {*} body - Message body (will be JSON serialized)
|
|
27
|
+
* @param {Object} options - Send options
|
|
28
|
+
* @param {number} options.delaySeconds - Delay before processing (max 12 hours)
|
|
29
|
+
* @returns {Promise<void>}
|
|
30
|
+
*/
|
|
31
|
+
async send(body, options = {}) {
|
|
32
|
+
const message = {
|
|
33
|
+
body,
|
|
34
|
+
...(options.delaySeconds && {
|
|
35
|
+
delaySeconds: options.delaySeconds
|
|
36
|
+
})
|
|
37
|
+
};
|
|
38
|
+
await this.queue.send(message);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Send multiple messages in a batch
|
|
43
|
+
* @param {Array<*>} bodies - Array of message bodies
|
|
44
|
+
* @param {Object} options - Batch options
|
|
45
|
+
* @returns {Promise<void>}
|
|
46
|
+
*/
|
|
47
|
+
async sendBatch(bodies, options = {}) {
|
|
48
|
+
const messages = bodies.map(body => ({
|
|
49
|
+
body,
|
|
50
|
+
...(options.delaySeconds && {
|
|
51
|
+
delaySeconds: options.delaySeconds
|
|
52
|
+
})
|
|
53
|
+
}));
|
|
54
|
+
await this.queue.sendBatch(messages);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Send a message with automatic retry info
|
|
59
|
+
* @param {*} body - Message body
|
|
60
|
+
* @param {Object} options - Options including retry tracking
|
|
61
|
+
*/
|
|
62
|
+
async sendWithRetry(body, options = {}) {
|
|
63
|
+
const enhancedBody = {
|
|
64
|
+
...body,
|
|
65
|
+
_meta: {
|
|
66
|
+
attempt: (body._meta?.attempt || 0) + 1,
|
|
67
|
+
firstAttemptAt: body._meta?.firstAttemptAt || Date.now(),
|
|
68
|
+
lastAttemptAt: Date.now()
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
await this.send(enhancedBody, options);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export default QueueProducer;
|