@tamyla/clodo-framework 4.3.5 → 4.4.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +3 -1
  3. package/dist/routing/EnhancedRouter.js +63 -0
  4. package/dist/utilities/ai/client.js +276 -0
  5. package/dist/utilities/ai/index.js +6 -0
  6. package/dist/utilities/analytics/index.js +6 -0
  7. package/dist/utilities/analytics/writer.js +226 -0
  8. package/dist/utilities/bindings/client.js +283 -0
  9. package/dist/utilities/bindings/index.js +6 -0
  10. package/dist/utilities/cache/index.js +9 -0
  11. package/dist/utilities/cache/leaderboard.js +52 -0
  12. package/dist/utilities/cache/rate-limiter.js +57 -0
  13. package/dist/utilities/cache/session.js +69 -0
  14. package/dist/utilities/cache/upstash.js +200 -0
  15. package/dist/utilities/durable-objects/base.js +200 -0
  16. package/dist/utilities/durable-objects/counter.js +117 -0
  17. package/dist/utilities/durable-objects/index.js +10 -0
  18. package/dist/utilities/durable-objects/rate-limiter.js +80 -0
  19. package/dist/utilities/durable-objects/session-store.js +126 -0
  20. package/dist/utilities/durable-objects/websocket-room.js +223 -0
  21. package/dist/utilities/email/handler.js +359 -0
  22. package/dist/utilities/email/index.js +6 -0
  23. package/dist/utilities/index.js +65 -0
  24. package/dist/utilities/kv/index.js +6 -0
  25. package/dist/utilities/kv/storage.js +268 -0
  26. package/dist/utilities/queues/consumer.js +188 -0
  27. package/dist/utilities/queues/index.js +7 -0
  28. package/dist/utilities/queues/producer.js +74 -0
  29. package/dist/utilities/scheduled/handler.js +276 -0
  30. package/dist/utilities/scheduled/index.js +6 -0
  31. package/dist/utilities/storage/index.js +6 -0
  32. package/dist/utilities/storage/r2.js +314 -0
  33. package/dist/utilities/vectorize/index.js +6 -0
  34. package/dist/utilities/vectorize/store.js +273 -0
  35. package/dist/utils/config/environment-var-normalizer.js +233 -0
  36. package/docs/CHANGELOG.md +1877 -0
  37. package/docs/api-reference.md +153 -0
  38. package/package.json +14 -2
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Durable Object Base Class
3
+ * Provides a foundation for building stateful Durable Objects
4
+ *
5
+ * @example
6
+ * import { DurableObjectBase } from '@tamyla/clodo-framework/utilities/durable-objects';
7
+ *
8
+ * export class MyCounter extends DurableObjectBase {
9
+ * async increment() {
10
+ * const current = await this.getState('count', 0);
11
+ * const newCount = current + 1;
12
+ * await this.setState('count', newCount);
13
+ * return newCount;
14
+ * }
15
+ * }
16
+ */
17
+
18
+ /**
19
+ * Base class for Durable Objects with common patterns
20
+ */
21
+ export class DurableObjectBase {
22
+ /**
23
+ * @param {DurableObjectState} state - DO state
24
+ * @param {Object} env - Environment bindings
25
+ */
26
+ constructor(state, env) {
27
+ this.state = state;
28
+ this.env = env;
29
+ this.storage = state.storage;
30
+ this.id = state.id;
31
+ this.initialized = false;
32
+ this.initPromise = null;
33
+ }
34
+
35
+ /**
36
+ * Override this method for async initialization
37
+ * Called once when the DO is first accessed
38
+ */
39
+ async initialize() {
40
+ // Override in subclass
41
+ }
42
+
43
+ /**
44
+ * Ensure initialization is complete
45
+ */
46
+ async ensureInitialized() {
47
+ if (this.initialized) return;
48
+ if (!this.initPromise) {
49
+ this.initPromise = this.state.blockConcurrencyWhile(async () => {
50
+ if (!this.initialized) {
51
+ await this.initialize();
52
+ this.initialized = true;
53
+ }
54
+ });
55
+ }
56
+ await this.initPromise;
57
+ }
58
+
59
+ /**
60
+ * Get a value from storage with optional default
61
+ * @param {string} key - Storage key
62
+ * @param {*} defaultValue - Default if key doesn't exist
63
+ * @returns {Promise<*>}
64
+ */
65
+ async getState(key, defaultValue = undefined) {
66
+ const value = await this.storage.get(key);
67
+ return value !== undefined ? value : defaultValue;
68
+ }
69
+
70
+ /**
71
+ * Set a value in storage
72
+ * @param {string} key - Storage key
73
+ * @param {*} value - Value to store
74
+ * @returns {Promise<void>}
75
+ */
76
+ async setState(key, value) {
77
+ await this.storage.put(key, value);
78
+ }
79
+
80
+ /**
81
+ * Delete a key from storage
82
+ * @param {string} key - Storage key
83
+ * @returns {Promise<boolean>}
84
+ */
85
+ async deleteState(key) {
86
+ return this.storage.delete(key);
87
+ }
88
+
89
+ /**
90
+ * Get multiple values from storage
91
+ * @param {string[]} keys - Storage keys
92
+ * @returns {Promise<Map<string, *>>}
93
+ */
94
+ async getMany(keys) {
95
+ return this.storage.get(keys);
96
+ }
97
+
98
+ /**
99
+ * Set multiple values in storage
100
+ * @param {Object} entries - Key-value pairs
101
+ * @returns {Promise<void>}
102
+ */
103
+ async setMany(entries) {
104
+ return this.storage.put(entries);
105
+ }
106
+
107
+ /**
108
+ * List all keys with a prefix
109
+ * @param {Object} options - List options
110
+ * @returns {Promise<Map<string, *>>}
111
+ */
112
+ async listState(options = {}) {
113
+ return this.storage.list(options);
114
+ }
115
+
116
+ /**
117
+ * Execute a transaction
118
+ * @param {Function} callback - Transaction callback
119
+ * @returns {Promise<*>}
120
+ */
121
+ async transaction(callback) {
122
+ return this.state.blockConcurrencyWhile(callback);
123
+ }
124
+
125
+ /**
126
+ * Set an alarm for future execution
127
+ * @param {Date|number} time - When to trigger (Date or ms from now)
128
+ * @returns {Promise<void>}
129
+ */
130
+ async setAlarm(time) {
131
+ const alarmTime = typeof time === 'number' ? Date.now() + time : time.getTime();
132
+ await this.storage.setAlarm(alarmTime);
133
+ }
134
+
135
+ /**
136
+ * Get the current alarm time
137
+ * @returns {Promise<Date|null>}
138
+ */
139
+ async getAlarm() {
140
+ const alarm = await this.storage.getAlarm();
141
+ return alarm ? new Date(alarm) : null;
142
+ }
143
+
144
+ /**
145
+ * Delete the current alarm
146
+ * @returns {Promise<void>}
147
+ */
148
+ async deleteAlarm() {
149
+ await this.storage.deleteAlarm();
150
+ }
151
+
152
+ /**
153
+ * Default alarm handler - override in subclass
154
+ */
155
+ async alarm() {
156
+ // Override in subclass
157
+ }
158
+
159
+ /**
160
+ * Handle HTTP requests - override in subclass
161
+ * @param {Request} request
162
+ * @returns {Promise<Response>}
163
+ */
164
+ async fetch(request) {
165
+ await this.ensureInitialized();
166
+
167
+ // Default implementation - override for custom behavior
168
+ return new Response('Not implemented', {
169
+ status: 501
170
+ });
171
+ }
172
+
173
+ /**
174
+ * JSON response helper
175
+ * @param {*} data - Data to serialize
176
+ * @param {number} status - HTTP status
177
+ * @returns {Response}
178
+ */
179
+ json(data, status = 200) {
180
+ return new Response(JSON.stringify(data), {
181
+ status,
182
+ headers: {
183
+ 'Content-Type': 'application/json'
184
+ }
185
+ });
186
+ }
187
+
188
+ /**
189
+ * Error response helper
190
+ * @param {string} message - Error message
191
+ * @param {number} status - HTTP status
192
+ * @returns {Response}
193
+ */
194
+ error(message, status = 500) {
195
+ return this.json({
196
+ error: message
197
+ }, status);
198
+ }
199
+ }
200
+ export default DurableObjectBase;
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Counter Durable Object
3
+ * Simple atomic counter with history
4
+ *
5
+ * @example
6
+ * const id = env.COUNTER.idFromName('page-views');
7
+ * const counter = env.COUNTER.get(id);
8
+ * const response = await counter.fetch(new Request('https://counter/increment'));
9
+ */
10
+
11
+ import { DurableObjectBase } from './base.js';
12
+ export class Counter extends DurableObjectBase {
13
+ async fetch(request) {
14
+ await this.ensureInitialized();
15
+ const url = new URL(request.url);
16
+ const action = url.pathname.split('/').pop();
17
+ switch (action) {
18
+ case 'increment':
19
+ return this.increment(request);
20
+ case 'decrement':
21
+ return this.decrement(request);
22
+ case 'set':
23
+ return this.set(request);
24
+ case 'reset':
25
+ return this.reset();
26
+ case 'history':
27
+ return this.getHistory();
28
+ default:
29
+ return this.getValue();
30
+ }
31
+ }
32
+ async getValue() {
33
+ const value = await this.getState('value', 0);
34
+ return this.json({
35
+ value
36
+ });
37
+ }
38
+ async increment(request) {
39
+ const url = new URL(request.url);
40
+ const amount = parseInt(url.searchParams.get('amount')) || 1;
41
+ return this.transaction(async () => {
42
+ let value = await this.getState('value', 0);
43
+ value += amount;
44
+ await this.setState('value', value);
45
+ await this.recordHistory('increment', amount, value);
46
+ return this.json({
47
+ value,
48
+ change: amount
49
+ });
50
+ });
51
+ }
52
+ async decrement(request) {
53
+ const url = new URL(request.url);
54
+ const amount = parseInt(url.searchParams.get('amount')) || 1;
55
+ return this.transaction(async () => {
56
+ let value = await this.getState('value', 0);
57
+ value -= amount;
58
+ await this.setState('value', value);
59
+ await this.recordHistory('decrement', -amount, value);
60
+ return this.json({
61
+ value,
62
+ change: -amount
63
+ });
64
+ });
65
+ }
66
+ async set(request) {
67
+ const url = new URL(request.url);
68
+ const newValue = parseInt(url.searchParams.get('value'));
69
+ if (isNaN(newValue)) {
70
+ return this.error('Invalid value', 400);
71
+ }
72
+ return this.transaction(async () => {
73
+ const oldValue = await this.getState('value', 0);
74
+ await this.setState('value', newValue);
75
+ await this.recordHistory('set', newValue - oldValue, newValue);
76
+ return this.json({
77
+ value: newValue,
78
+ previousValue: oldValue
79
+ });
80
+ });
81
+ }
82
+ async reset() {
83
+ return this.transaction(async () => {
84
+ const oldValue = await this.getState('value', 0);
85
+ await this.setState('value', 0);
86
+ await this.recordHistory('reset', -oldValue, 0);
87
+ return this.json({
88
+ value: 0,
89
+ previousValue: oldValue
90
+ });
91
+ });
92
+ }
93
+ async recordHistory(action, change, newValue) {
94
+ const history = await this.getState('history', []);
95
+ history.push({
96
+ action,
97
+ change,
98
+ value: newValue,
99
+ timestamp: Date.now()
100
+ });
101
+
102
+ // Keep only last 100 entries
103
+ if (history.length > 100) {
104
+ history.shift();
105
+ }
106
+ await this.setState('history', history);
107
+ }
108
+ async getHistory() {
109
+ const history = await this.getState('history', []);
110
+ const value = await this.getState('value', 0);
111
+ return this.json({
112
+ currentValue: value,
113
+ history
114
+ });
115
+ }
116
+ }
117
+ export default Counter;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Durable Object Utilities
3
+ * @module @tamyla/clodo-framework/utilities/durable-objects
4
+ */
5
+
6
+ export { DurableObjectBase } from './base.js';
7
+ export { RateLimiter } from './rate-limiter.js';
8
+ export { SessionStore } from './session-store.js';
9
+ export { Counter } from './counter.js';
10
+ export { WebSocketRoom } from './websocket-room.js';
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Rate Limiter Durable Object
3
+ * Implements sliding window rate limiting
4
+ *
5
+ * @example
6
+ * // In wrangler.toml
7
+ * [[durable_objects.bindings]]
8
+ * name = "RATE_LIMITER"
9
+ * class_name = "RateLimiter"
10
+ *
11
+ * // Usage
12
+ * const id = env.RATE_LIMITER.idFromName(clientIP);
13
+ * const limiter = env.RATE_LIMITER.get(id);
14
+ * const response = await limiter.fetch(request);
15
+ */
16
+
17
+ import { DurableObjectBase } from './base.js';
18
+ export class RateLimiter extends DurableObjectBase {
19
+ constructor(state, env) {
20
+ super(state, env);
21
+ this.defaultLimit = 100;
22
+ this.defaultWindow = 60000; // 1 minute in ms
23
+ }
24
+ async fetch(request) {
25
+ await this.ensureInitialized();
26
+ const url = new URL(request.url);
27
+ const action = url.pathname.split('/').pop();
28
+ switch (action) {
29
+ case 'check':
30
+ return this.checkLimit(request);
31
+ case 'reset':
32
+ return this.resetLimit();
33
+ case 'status':
34
+ return this.getStatus();
35
+ default:
36
+ return this.checkLimit(request);
37
+ }
38
+ }
39
+ async checkLimit(request) {
40
+ const url = new URL(request.url);
41
+ const limit = parseInt(url.searchParams.get('limit')) || this.defaultLimit;
42
+ const window = parseInt(url.searchParams.get('window')) || this.defaultWindow;
43
+ const now = Date.now();
44
+ const windowStart = now - window;
45
+
46
+ // Get current requests
47
+ let requests = await this.getState('requests', []);
48
+
49
+ // Filter to only requests within window
50
+ requests = requests.filter(timestamp => timestamp > windowStart);
51
+ const allowed = requests.length < limit;
52
+ if (allowed) {
53
+ requests.push(now);
54
+ await this.setState('requests', requests);
55
+ }
56
+ return this.json({
57
+ allowed,
58
+ remaining: Math.max(0, limit - requests.length),
59
+ reset: windowStart + window,
60
+ limit,
61
+ current: requests.length
62
+ });
63
+ }
64
+ async resetLimit() {
65
+ await this.setState('requests', []);
66
+ return this.json({
67
+ success: true,
68
+ message: 'Rate limit reset'
69
+ });
70
+ }
71
+ async getStatus() {
72
+ const requests = await this.getState('requests', []);
73
+ return this.json({
74
+ currentRequests: requests.length,
75
+ oldestRequest: requests[0] || null,
76
+ newestRequest: requests[requests.length - 1] || null
77
+ });
78
+ }
79
+ }
80
+ export default RateLimiter;
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Session Store Durable Object
3
+ * Manages user sessions with automatic expiry
4
+ *
5
+ * @example
6
+ * const id = env.SESSION_STORE.idFromName(sessionId);
7
+ * const store = env.SESSION_STORE.get(id);
8
+ * await store.fetch(new Request('https://session/set', {
9
+ * method: 'POST',
10
+ * body: JSON.stringify({ userId: '123', data: { role: 'admin' } })
11
+ * }));
12
+ */
13
+
14
+ import { DurableObjectBase } from './base.js';
15
+ export class SessionStore extends DurableObjectBase {
16
+ constructor(state, env) {
17
+ super(state, env);
18
+ this.defaultTTL = 24 * 60 * 60 * 1000; // 24 hours
19
+ }
20
+ async fetch(request) {
21
+ await this.ensureInitialized();
22
+ const url = new URL(request.url);
23
+ const action = url.pathname.split('/').pop();
24
+ switch (request.method) {
25
+ case 'GET':
26
+ return this.getSession();
27
+ case 'POST':
28
+ return this.setSession(request);
29
+ case 'PATCH':
30
+ return this.updateSession(request);
31
+ case 'DELETE':
32
+ return this.deleteSession();
33
+ default:
34
+ return this.error('Method not allowed', 405);
35
+ }
36
+ }
37
+ async getSession() {
38
+ const session = await this.getState('session');
39
+ if (!session) {
40
+ return this.json({
41
+ exists: false,
42
+ session: null
43
+ });
44
+ }
45
+
46
+ // Check expiry
47
+ if (session.expiresAt && Date.now() > session.expiresAt) {
48
+ await this.deleteState('session');
49
+ return this.json({
50
+ exists: false,
51
+ expired: true,
52
+ session: null
53
+ });
54
+ }
55
+
56
+ // Update last accessed
57
+ session.lastAccessed = Date.now();
58
+ await this.setState('session', session);
59
+ return this.json({
60
+ exists: true,
61
+ session
62
+ });
63
+ }
64
+ async setSession(request) {
65
+ const body = await request.json();
66
+ const ttl = body.ttl || this.defaultTTL;
67
+ const session = {
68
+ data: body.data || {},
69
+ userId: body.userId,
70
+ createdAt: Date.now(),
71
+ lastAccessed: Date.now(),
72
+ expiresAt: Date.now() + ttl,
73
+ metadata: body.metadata || {}
74
+ };
75
+ await this.setState('session', session);
76
+
77
+ // Set alarm for cleanup
78
+ await this.setAlarm(ttl);
79
+ return this.json({
80
+ success: true,
81
+ session
82
+ });
83
+ }
84
+ async updateSession(request) {
85
+ const session = await this.getState('session');
86
+ if (!session) {
87
+ return this.error('Session not found', 404);
88
+ }
89
+ const body = await request.json();
90
+
91
+ // Merge data
92
+ session.data = {
93
+ ...session.data,
94
+ ...body.data
95
+ };
96
+ session.lastAccessed = Date.now();
97
+
98
+ // Optionally extend expiry
99
+ if (body.extend) {
100
+ const ttl = body.ttl || this.defaultTTL;
101
+ session.expiresAt = Date.now() + ttl;
102
+ await this.setAlarm(ttl);
103
+ }
104
+ await this.setState('session', session);
105
+ return this.json({
106
+ success: true,
107
+ session
108
+ });
109
+ }
110
+ async deleteSession() {
111
+ await this.deleteState('session');
112
+ await this.deleteAlarm();
113
+ return this.json({
114
+ success: true,
115
+ message: 'Session deleted'
116
+ });
117
+ }
118
+ async alarm() {
119
+ // Clean up expired session
120
+ const session = await this.getState('session');
121
+ if (session && session.expiresAt && Date.now() > session.expiresAt) {
122
+ await this.deleteState('session');
123
+ }
124
+ }
125
+ }
126
+ export default SessionStore;