@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,276 @@
1
+ /**
2
+ * Scheduled/Cron Job Utilities
3
+ * Helpers for Cloudflare Workers Cron Triggers
4
+ *
5
+ * @example
6
+ * import { ScheduledHandler, CronJob } from '@tamyla/clodo-framework/utilities/scheduled';
7
+ *
8
+ * const handler = new ScheduledHandler()
9
+ * .register('0 * * * *', new CleanupJob())
10
+ * .register('0 0 * * *', new DailyReportJob())
11
+ * .register('* /5 * * * *', async (event, env) => {
12
+ * // Run every 5 minutes
13
+ * });
14
+ *
15
+ * export default {
16
+ * scheduled: (event, env, ctx) => handler.handle(event, env, ctx)
17
+ * }
18
+ */
19
+
20
+ /**
21
+ * Base class for cron jobs
22
+ */
23
+ export class CronJob {
24
+ /**
25
+ * Execute the job
26
+ * @param {ScheduledEvent} event - Scheduled event
27
+ * @param {Object} env - Environment bindings
28
+ * @param {ExecutionContext} ctx - Execution context
29
+ */
30
+ async execute(event, env, ctx) {
31
+ throw new Error('execute() must be implemented');
32
+ }
33
+
34
+ /**
35
+ * Check if job should run
36
+ * @param {ScheduledEvent} event
37
+ * @returns {boolean}
38
+ */
39
+ shouldRun(event) {
40
+ return true;
41
+ }
42
+
43
+ /**
44
+ * Called before execution
45
+ */
46
+ async beforeExecute(event, env) {}
47
+
48
+ /**
49
+ * Called after successful execution
50
+ */
51
+ async afterExecute(event, env, result) {}
52
+
53
+ /**
54
+ * Called on error
55
+ */
56
+ async onError(event, env, error) {
57
+ console.error(`Job error: ${error.message}`);
58
+ throw error;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Scheduled event handler with job registration
64
+ */
65
+ export class ScheduledHandler {
66
+ constructor() {
67
+ this.jobs = new Map();
68
+ this.defaultJob = null;
69
+ this.middleware = [];
70
+ }
71
+
72
+ /**
73
+ * Register a job for a cron pattern
74
+ * @param {string} cron - Cron pattern (e.g., '0 * * * *')
75
+ * @param {CronJob|Function} job - Job instance or function
76
+ */
77
+ register(cron, job) {
78
+ if (typeof job === 'function') {
79
+ // Wrap function in CronJob
80
+ const fn = job;
81
+ job = new class extends CronJob {
82
+ async execute(event, env, ctx) {
83
+ return fn(event, env, ctx);
84
+ }
85
+ }();
86
+ }
87
+ this.jobs.set(cron, job);
88
+ return this;
89
+ }
90
+
91
+ /**
92
+ * Set default job for unmatched cron patterns
93
+ */
94
+ setDefault(job) {
95
+ if (typeof job === 'function') {
96
+ const fn = job;
97
+ job = new class extends CronJob {
98
+ async execute(event, env, ctx) {
99
+ return fn(event, env, ctx);
100
+ }
101
+ }();
102
+ }
103
+ this.defaultJob = job;
104
+ return this;
105
+ }
106
+
107
+ /**
108
+ * Add middleware
109
+ * @param {Function} fn - Middleware function (event, env, next) => Promise
110
+ */
111
+ use(fn) {
112
+ this.middleware.push(fn);
113
+ return this;
114
+ }
115
+
116
+ /**
117
+ * Handle scheduled event
118
+ * @param {ScheduledEvent} event
119
+ * @param {Object} env
120
+ * @param {ExecutionContext} ctx
121
+ */
122
+ async handle(event, env, ctx) {
123
+ const job = this.jobs.get(event.cron) || this.defaultJob;
124
+ if (!job) {
125
+ console.warn(`No job registered for cron: ${event.cron}`);
126
+ return;
127
+ }
128
+ if (!job.shouldRun(event)) {
129
+ console.log(`Job skipped for cron: ${event.cron}`);
130
+ return;
131
+ }
132
+
133
+ // Run middleware chain
134
+ const runWithMiddleware = async () => {
135
+ let index = 0;
136
+ const next = async () => {
137
+ if (index < this.middleware.length) {
138
+ const middleware = this.middleware[index++];
139
+ return middleware(event, env, next);
140
+ }
141
+ return this._executeJob(job, event, env, ctx);
142
+ };
143
+ return next();
144
+ };
145
+ return runWithMiddleware();
146
+ }
147
+ async _executeJob(job, event, env, ctx) {
148
+ try {
149
+ await job.beforeExecute(event, env);
150
+ const result = await job.execute(event, env, ctx);
151
+ await job.afterExecute(event, env, result);
152
+ return result;
153
+ } catch (error) {
154
+ return job.onError(event, env, error);
155
+ }
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Job scheduler for dynamic job management
161
+ */
162
+ export class JobScheduler {
163
+ constructor(kvNamespace) {
164
+ this.kv = kvNamespace;
165
+ this.prefix = 'jobs:';
166
+ }
167
+
168
+ /**
169
+ * Schedule a job for future execution
170
+ * @param {string} jobId - Unique job ID
171
+ * @param {Object} jobData - Job data
172
+ * @param {Date|number} runAt - When to run (Date or timestamp)
173
+ */
174
+ async schedule(jobId, jobData, runAt) {
175
+ const timestamp = runAt instanceof Date ? runAt.getTime() : runAt;
176
+ await this.kv.put(`${this.prefix}${timestamp}:${jobId}`, JSON.stringify({
177
+ id: jobId,
178
+ data: jobData,
179
+ scheduledAt: Date.now(),
180
+ runAt: timestamp
181
+ }), {
182
+ expirationTtl: Math.ceil((timestamp - Date.now()) / 1000) + 86400
183
+ });
184
+ }
185
+
186
+ /**
187
+ * Get jobs ready to run
188
+ * @returns {Promise<Array>}
189
+ */
190
+ async getReadyJobs() {
191
+ const now = Date.now();
192
+ const jobs = [];
193
+ const {
194
+ keys
195
+ } = await this.kv.list({
196
+ prefix: this.prefix
197
+ });
198
+ for (const key of keys) {
199
+ const [, timestampAndId] = key.name.split(this.prefix);
200
+ const [timestamp] = timestampAndId.split(':');
201
+ if (parseInt(timestamp) <= now) {
202
+ const jobData = await this.kv.get(key.name, {
203
+ type: 'json'
204
+ });
205
+ if (jobData) {
206
+ jobs.push(jobData);
207
+ }
208
+ }
209
+ }
210
+ return jobs;
211
+ }
212
+
213
+ /**
214
+ * Mark job as completed
215
+ */
216
+ async complete(jobId, timestamp) {
217
+ await this.kv.delete(`${this.prefix}${timestamp}:${jobId}`);
218
+ }
219
+
220
+ /**
221
+ * Reschedule a job
222
+ */
223
+ async reschedule(jobId, currentTimestamp, newRunAt) {
224
+ const key = `${this.prefix}${currentTimestamp}:${jobId}`;
225
+ const jobData = await this.kv.get(key, {
226
+ type: 'json'
227
+ });
228
+ if (jobData) {
229
+ await this.kv.delete(key);
230
+ await this.schedule(jobId, jobData.data, newRunAt);
231
+ }
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Registry for tracking scheduled jobs
237
+ */
238
+ export class ScheduledJobRegistry {
239
+ constructor() {
240
+ this.jobs = new Map();
241
+ }
242
+
243
+ /**
244
+ * Define a named job
245
+ */
246
+ define(name, JobClass) {
247
+ this.jobs.set(name, JobClass);
248
+ return this;
249
+ }
250
+
251
+ /**
252
+ * Get a job class by name
253
+ */
254
+ get(name) {
255
+ return this.jobs.get(name);
256
+ }
257
+
258
+ /**
259
+ * Create a job instance
260
+ */
261
+ create(name, ...args) {
262
+ const JobClass = this.get(name);
263
+ if (!JobClass) {
264
+ throw new Error(`Unknown job: ${name}`);
265
+ }
266
+ return new JobClass(...args);
267
+ }
268
+
269
+ /**
270
+ * List all registered jobs
271
+ */
272
+ list() {
273
+ return Array.from(this.jobs.keys());
274
+ }
275
+ }
276
+ export default ScheduledHandler;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Scheduled/Cron Utilities
3
+ * @module @tamyla/clodo-framework/utilities/scheduled
4
+ */
5
+
6
+ export { ScheduledHandler, CronJob, JobScheduler, ScheduledJobRegistry } from './handler.js';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Storage Utilities
3
+ * @module @tamyla/clodo-framework/utilities/storage
4
+ */
5
+
6
+ export { R2Storage, handleFileUpload, serveFile } from './r2.js';
@@ -0,0 +1,314 @@
1
+ /**
2
+ * R2 Storage Utilities
3
+ * Provides convenient methods for working with Cloudflare R2 buckets
4
+ *
5
+ * @example
6
+ * import { R2Storage } from '@tamyla/clodo-framework/utilities/storage';
7
+ *
8
+ * export default {
9
+ * async fetch(request, env) {
10
+ * const storage = new R2Storage(env.BUCKET);
11
+ *
12
+ * // Upload a file
13
+ * await storage.upload('images/photo.jpg', imageData, {
14
+ * contentType: 'image/jpeg',
15
+ * metadata: { uploadedBy: 'user-123' }
16
+ * });
17
+ *
18
+ * // Get a file
19
+ * const file = await storage.get('images/photo.jpg');
20
+ *
21
+ * // List files
22
+ * const files = await storage.list('images/', { limit: 100 });
23
+ * }
24
+ * }
25
+ */
26
+
27
+ /**
28
+ * R2 Storage wrapper class
29
+ */
30
+ export class R2Storage {
31
+ /**
32
+ * @param {R2Bucket} bucket - The R2 bucket binding
33
+ */
34
+ constructor(bucket) {
35
+ if (!bucket) {
36
+ throw new Error('R2 bucket binding is required');
37
+ }
38
+ this.bucket = bucket;
39
+ }
40
+
41
+ /**
42
+ * Upload a file to R2
43
+ * @param {string} key - Object key (path)
44
+ * @param {ReadableStream|ArrayBuffer|string|Blob} data - File data
45
+ * @param {Object} options - Upload options
46
+ * @param {string} options.contentType - MIME type
47
+ * @param {Object} options.metadata - Custom metadata
48
+ * @param {string} options.cacheControl - Cache-Control header
49
+ * @returns {Promise<R2Object>}
50
+ */
51
+ async upload(key, data, options = {}) {
52
+ const httpMetadata = {};
53
+ if (options.contentType) {
54
+ httpMetadata.contentType = options.contentType;
55
+ }
56
+ if (options.cacheControl) {
57
+ httpMetadata.cacheControl = options.cacheControl;
58
+ }
59
+ if (options.contentDisposition) {
60
+ httpMetadata.contentDisposition = options.contentDisposition;
61
+ }
62
+ if (options.contentEncoding) {
63
+ httpMetadata.contentEncoding = options.contentEncoding;
64
+ }
65
+ if (options.contentLanguage) {
66
+ httpMetadata.contentLanguage = options.contentLanguage;
67
+ }
68
+ return this.bucket.put(key, data, {
69
+ httpMetadata,
70
+ customMetadata: options.metadata || {}
71
+ });
72
+ }
73
+
74
+ /**
75
+ * Get a file from R2
76
+ * @param {string} key - Object key
77
+ * @returns {Promise<R2ObjectBody|null>}
78
+ */
79
+ async get(key) {
80
+ return this.bucket.get(key);
81
+ }
82
+
83
+ /**
84
+ * Get only object metadata (head)
85
+ * @param {string} key - Object key
86
+ * @returns {Promise<R2Object|null>}
87
+ */
88
+ async head(key) {
89
+ return this.bucket.head(key);
90
+ }
91
+
92
+ /**
93
+ * Delete a file from R2
94
+ * @param {string} key - Object key
95
+ * @returns {Promise<void>}
96
+ */
97
+ async delete(key) {
98
+ return this.bucket.delete(key);
99
+ }
100
+
101
+ /**
102
+ * Delete multiple files
103
+ * @param {string[]} keys - Array of object keys
104
+ * @returns {Promise<void>}
105
+ */
106
+ async deleteMany(keys) {
107
+ return this.bucket.delete(keys);
108
+ }
109
+
110
+ /**
111
+ * List objects in the bucket
112
+ * @param {string} prefix - Key prefix to filter by
113
+ * @param {Object} options - List options
114
+ * @param {number} options.limit - Maximum number of results
115
+ * @param {string} options.cursor - Pagination cursor
116
+ * @param {string} options.delimiter - Delimiter for hierarchy
117
+ * @returns {Promise<R2Objects>}
118
+ */
119
+ async list(prefix = '', options = {}) {
120
+ return this.bucket.list({
121
+ prefix,
122
+ limit: options.limit || 1000,
123
+ cursor: options.cursor,
124
+ delimiter: options.delimiter,
125
+ include: options.include || ['httpMetadata', 'customMetadata']
126
+ });
127
+ }
128
+
129
+ /**
130
+ * List all objects (handles pagination automatically)
131
+ * @param {string} prefix - Key prefix to filter by
132
+ * @returns {AsyncGenerator<R2Object>}
133
+ */
134
+ async *listAll(prefix = '') {
135
+ let cursor;
136
+ do {
137
+ const result = await this.list(prefix, {
138
+ cursor
139
+ });
140
+ for (const object of result.objects) {
141
+ yield object;
142
+ }
143
+ cursor = result.truncated ? result.cursor : null;
144
+ } while (cursor);
145
+ }
146
+
147
+ /**
148
+ * Check if an object exists
149
+ * @param {string} key - Object key
150
+ * @returns {Promise<boolean>}
151
+ */
152
+ async exists(key) {
153
+ const head = await this.head(key);
154
+ return head !== null;
155
+ }
156
+
157
+ /**
158
+ * Copy an object within the bucket
159
+ * @param {string} sourceKey - Source object key
160
+ * @param {string} destKey - Destination object key
161
+ * @returns {Promise<R2Object>}
162
+ */
163
+ async copy(sourceKey, destKey) {
164
+ const source = await this.get(sourceKey);
165
+ if (!source) {
166
+ throw new Error(`Source object not found: ${sourceKey}`);
167
+ }
168
+ return this.upload(destKey, source.body, {
169
+ contentType: source.httpMetadata?.contentType,
170
+ metadata: source.customMetadata
171
+ });
172
+ }
173
+
174
+ /**
175
+ * Move an object within the bucket
176
+ * @param {string} sourceKey - Source object key
177
+ * @param {string} destKey - Destination object key
178
+ * @returns {Promise<R2Object>}
179
+ */
180
+ async move(sourceKey, destKey) {
181
+ const result = await this.copy(sourceKey, destKey);
182
+ await this.delete(sourceKey);
183
+ return result;
184
+ }
185
+
186
+ /**
187
+ * Get a signed URL info for temporary access
188
+ * Note: R2 doesn't have built-in signed URLs, this returns info for custom implementation
189
+ * @param {string} key - Object key
190
+ * @param {Object} options - Signing options
191
+ * @returns {Object} URL info object
192
+ */
193
+ createSignedUrlInfo(key, options = {}) {
194
+ const expiresIn = options.expiresIn || 3600;
195
+ const expiresAt = Date.now() + expiresIn * 1000;
196
+ return {
197
+ key,
198
+ bucket: this.bucket,
199
+ expiresAt,
200
+ expiresIn,
201
+ // Implement custom signing logic in your application
202
+ // This is a placeholder for the signing parameters
203
+ signatureParams: {
204
+ key,
205
+ expires: expiresAt,
206
+ ...options
207
+ }
208
+ };
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Handle file upload from multipart form data
214
+ * @param {Request} request - Incoming request
215
+ * @param {R2Storage} storage - R2Storage instance
216
+ * @param {Object} options - Upload options
217
+ * @returns {Promise<Object>} Upload result
218
+ */
219
+ export async function handleFileUpload(request, storage, options = {}) {
220
+ const {
221
+ fieldName = 'file',
222
+ keyPrefix = '',
223
+ maxSize = 10 * 1024 * 1024,
224
+ // 10MB default
225
+ allowedTypes = null,
226
+ generateKey = null
227
+ } = options;
228
+ const formData = await request.formData();
229
+ const file = formData.get(fieldName);
230
+ if (!file || !(file instanceof File)) {
231
+ throw new Error('No file provided');
232
+ }
233
+
234
+ // Check file size
235
+ if (file.size > maxSize) {
236
+ throw new Error(`File too large. Maximum size is ${maxSize} bytes`);
237
+ }
238
+
239
+ // Check content type
240
+ if (allowedTypes && !allowedTypes.includes(file.type)) {
241
+ throw new Error(`File type not allowed: ${file.type}`);
242
+ }
243
+
244
+ // Generate key
245
+ const key = generateKey ? generateKey(file) : `${keyPrefix}${Date.now()}-${file.name}`;
246
+
247
+ // Upload
248
+ const result = await storage.upload(key, file, {
249
+ contentType: file.type,
250
+ metadata: {
251
+ originalName: file.name,
252
+ uploadedAt: new Date().toISOString()
253
+ }
254
+ });
255
+ return {
256
+ key,
257
+ size: file.size,
258
+ type: file.type,
259
+ name: file.name,
260
+ etag: result.etag
261
+ };
262
+ }
263
+
264
+ /**
265
+ * Serve a file from R2 with proper headers
266
+ * @param {R2Storage} storage - R2Storage instance
267
+ * @param {string} key - Object key
268
+ * @param {Request} request - Original request (for range headers)
269
+ * @returns {Promise<Response>}
270
+ */
271
+ export async function serveFile(storage, key, request) {
272
+ const object = await storage.get(key);
273
+ if (!object) {
274
+ return new Response('Not Found', {
275
+ status: 404
276
+ });
277
+ }
278
+ const headers = new Headers();
279
+
280
+ // Set content type
281
+ if (object.httpMetadata?.contentType) {
282
+ headers.set('Content-Type', object.httpMetadata.contentType);
283
+ }
284
+
285
+ // Set cache control
286
+ if (object.httpMetadata?.cacheControl) {
287
+ headers.set('Cache-Control', object.httpMetadata.cacheControl);
288
+ } else {
289
+ headers.set('Cache-Control', 'public, max-age=31536000');
290
+ }
291
+
292
+ // Set ETag
293
+ headers.set('ETag', object.etag);
294
+
295
+ // Set content length
296
+ headers.set('Content-Length', object.size.toString());
297
+
298
+ // Handle range requests
299
+ const range = request.headers.get('Range');
300
+ if (range) {
301
+ const [start, end] = range.replace('bytes=', '').split('-').map(Number);
302
+ const actualEnd = end || object.size - 1;
303
+ headers.set('Content-Range', `bytes ${start}-${actualEnd}/${object.size}`);
304
+ headers.set('Content-Length', (actualEnd - start + 1).toString());
305
+ return new Response(object.body, {
306
+ status: 206,
307
+ headers
308
+ });
309
+ }
310
+ return new Response(object.body, {
311
+ headers
312
+ });
313
+ }
314
+ export default R2Storage;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Vectorize Utilities
3
+ * @module @tamyla/clodo-framework/utilities/vectorize
4
+ */
5
+
6
+ export { VectorStore, VectorSearch, EmbeddingHelper } from './store.js';