@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.
- package/CHANGELOG.md +15 -0
- package/README.md +3 -1
- package/dist/routing/EnhancedRouter.js +63 -0
- 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/dist/utils/config/environment-var-normalizer.js +233 -0
- package/docs/CHANGELOG.md +1877 -0
- package/docs/api-reference.md +153 -0
- 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;
|