@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.
Files changed (34) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +34 -8
  3. package/dist/utilities/ai/client.js +276 -0
  4. package/dist/utilities/ai/index.js +6 -0
  5. package/dist/utilities/analytics/index.js +6 -0
  6. package/dist/utilities/analytics/writer.js +226 -0
  7. package/dist/utilities/bindings/client.js +283 -0
  8. package/dist/utilities/bindings/index.js +6 -0
  9. package/dist/utilities/cache/index.js +9 -0
  10. package/dist/utilities/cache/leaderboard.js +52 -0
  11. package/dist/utilities/cache/rate-limiter.js +57 -0
  12. package/dist/utilities/cache/session.js +69 -0
  13. package/dist/utilities/cache/upstash.js +200 -0
  14. package/dist/utilities/durable-objects/base.js +200 -0
  15. package/dist/utilities/durable-objects/counter.js +117 -0
  16. package/dist/utilities/durable-objects/index.js +10 -0
  17. package/dist/utilities/durable-objects/rate-limiter.js +80 -0
  18. package/dist/utilities/durable-objects/session-store.js +126 -0
  19. package/dist/utilities/durable-objects/websocket-room.js +223 -0
  20. package/dist/utilities/email/handler.js +359 -0
  21. package/dist/utilities/email/index.js +6 -0
  22. package/dist/utilities/index.js +65 -0
  23. package/dist/utilities/kv/index.js +6 -0
  24. package/dist/utilities/kv/storage.js +268 -0
  25. package/dist/utilities/queues/consumer.js +188 -0
  26. package/dist/utilities/queues/index.js +7 -0
  27. package/dist/utilities/queues/producer.js +74 -0
  28. package/dist/utilities/scheduled/handler.js +276 -0
  29. package/dist/utilities/scheduled/index.js +6 -0
  30. package/dist/utilities/storage/index.js +6 -0
  31. package/dist/utilities/storage/r2.js +314 -0
  32. package/dist/utilities/vectorize/index.js +6 -0
  33. package/dist/utilities/vectorize/store.js +273 -0
  34. 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,7 @@
1
+ /**
2
+ * Queue Utilities
3
+ * @module @tamyla/clodo-framework/utilities/queues
4
+ */
5
+
6
+ export { QueueProducer } from './producer.js';
7
+ export { QueueConsumer, MessageBuilder, createMessage, MessageTypes } from './consumer.js';
@@ -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;