@objectstack/service-cache 4.0.3 → 4.0.5
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/README.md +294 -0
- package/package.json +30 -6
- package/.turbo/turbo-build.log +0 -22
- package/CHANGELOG.md +0 -169
- package/src/cache-service-plugin.ts +0 -63
- package/src/index.ts +0 -8
- package/src/memory-cache-adapter.test.ts +0 -107
- package/src/memory-cache-adapter.ts +0 -90
- package/src/redis-cache-adapter.ts +0 -63
- package/tsconfig.json +0 -17
package/README.md
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# @objectstack/service-cache
|
|
2
|
+
|
|
3
|
+
Cache Service for ObjectStack — implements `ICacheService` with in-memory and Redis adapters.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Multiple Adapters**: In-memory (development) and Redis (production) support
|
|
8
|
+
- **Type-Safe**: Full TypeScript support with generic value types
|
|
9
|
+
- **TTL Support**: Automatic expiration with time-to-live
|
|
10
|
+
- **Namespace Support**: Organize cache keys by namespace
|
|
11
|
+
- **Pattern Matching**: Delete keys by pattern (e.g., `user:*`)
|
|
12
|
+
- **Statistics**: Track hit/miss rates and memory usage
|
|
13
|
+
- **JSON Serialization**: Automatic serialization of complex objects
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm add @objectstack/service-cache
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
For Redis adapter:
|
|
22
|
+
```bash
|
|
23
|
+
pnpm add ioredis
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Basic Usage
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { defineStack } from '@objectstack/spec';
|
|
30
|
+
import { ServiceCache } from '@objectstack/service-cache';
|
|
31
|
+
|
|
32
|
+
const stack = defineStack({
|
|
33
|
+
services: [
|
|
34
|
+
ServiceCache.configure({
|
|
35
|
+
adapter: 'memory', // or 'redis'
|
|
36
|
+
defaultTTL: 300, // 5 minutes
|
|
37
|
+
}),
|
|
38
|
+
],
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Configuration
|
|
43
|
+
|
|
44
|
+
### In-Memory Adapter (Development)
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
ServiceCache.configure({
|
|
48
|
+
adapter: 'memory',
|
|
49
|
+
defaultTTL: 300,
|
|
50
|
+
maxSize: 1000, // Maximum number of entries
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Redis Adapter (Production)
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
ServiceCache.configure({
|
|
58
|
+
adapter: 'redis',
|
|
59
|
+
redis: {
|
|
60
|
+
host: 'localhost',
|
|
61
|
+
port: 6379,
|
|
62
|
+
password: process.env.REDIS_PASSWORD,
|
|
63
|
+
db: 0,
|
|
64
|
+
},
|
|
65
|
+
defaultTTL: 600,
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Service API
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
// Get cache service
|
|
73
|
+
const cache = kernel.getService<ICacheService>('cache');
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Set/Get Operations
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
// Set a value
|
|
80
|
+
await cache.set('user:123', { name: 'John', email: 'john@example.com' });
|
|
81
|
+
|
|
82
|
+
// Set with custom TTL (in seconds)
|
|
83
|
+
await cache.set('session:abc', sessionData, { ttl: 3600 }); // 1 hour
|
|
84
|
+
|
|
85
|
+
// Get a value
|
|
86
|
+
const user = await cache.get('user:123');
|
|
87
|
+
|
|
88
|
+
// Get with type safety
|
|
89
|
+
const user = await cache.get<User>('user:123');
|
|
90
|
+
|
|
91
|
+
// Get multiple keys
|
|
92
|
+
const users = await cache.mget(['user:123', 'user:456']);
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Existence & Deletion
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
// Check if key exists
|
|
99
|
+
const exists = await cache.has('user:123');
|
|
100
|
+
|
|
101
|
+
// Delete a key
|
|
102
|
+
await cache.del('user:123');
|
|
103
|
+
|
|
104
|
+
// Delete multiple keys
|
|
105
|
+
await cache.del(['session:abc', 'session:def']);
|
|
106
|
+
|
|
107
|
+
// Delete by pattern
|
|
108
|
+
await cache.delPattern('user:*');
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Namespaced Operations
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
// Create a namespaced cache instance
|
|
115
|
+
const userCache = cache.namespace('user');
|
|
116
|
+
|
|
117
|
+
// Set in namespace (key becomes 'user:123')
|
|
118
|
+
await userCache.set('123', userData);
|
|
119
|
+
|
|
120
|
+
// Get from namespace
|
|
121
|
+
const user = await userCache.get('123');
|
|
122
|
+
|
|
123
|
+
// Clear entire namespace
|
|
124
|
+
await userCache.clear();
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### TTL Management
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
// Get remaining TTL (in seconds)
|
|
131
|
+
const ttl = await cache.ttl('session:abc');
|
|
132
|
+
|
|
133
|
+
// Update TTL
|
|
134
|
+
await cache.expire('session:abc', 7200); // 2 hours
|
|
135
|
+
|
|
136
|
+
// Make key permanent (remove expiration)
|
|
137
|
+
await cache.persist('user:123');
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Atomic Operations
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
// Increment (useful for counters)
|
|
144
|
+
await cache.incr('page:views:123'); // Returns new value
|
|
145
|
+
|
|
146
|
+
// Increment by amount
|
|
147
|
+
await cache.incrby('score:user:123', 10);
|
|
148
|
+
|
|
149
|
+
// Decrement
|
|
150
|
+
await cache.decr('inventory:product:456');
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Batch Operations
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
// Set multiple keys at once
|
|
157
|
+
await cache.mset({
|
|
158
|
+
'user:123': user1Data,
|
|
159
|
+
'user:456': user2Data,
|
|
160
|
+
'user:789': user3Data,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Get multiple keys
|
|
164
|
+
const users = await cache.mget(['user:123', 'user:456', 'user:789']);
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Advanced Features
|
|
168
|
+
|
|
169
|
+
### Cache Aside Pattern
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
async function getUser(id: string): Promise<User> {
|
|
173
|
+
// Try cache first
|
|
174
|
+
const cached = await cache.get<User>(`user:${id}`);
|
|
175
|
+
if (cached) return cached;
|
|
176
|
+
|
|
177
|
+
// Load from database
|
|
178
|
+
const user = await db.findUser(id);
|
|
179
|
+
|
|
180
|
+
// Store in cache
|
|
181
|
+
await cache.set(`user:${id}`, user, { ttl: 600 });
|
|
182
|
+
|
|
183
|
+
return user;
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Cache-Through Pattern
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
async function getUserCacheThrough(id: string): Promise<User> {
|
|
191
|
+
return cache.getOrSet(`user:${id}`, async () => {
|
|
192
|
+
return await db.findUser(id);
|
|
193
|
+
}, { ttl: 600 });
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Invalidation on Write
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
async function updateUser(id: string, data: Partial<User>) {
|
|
201
|
+
// Update database
|
|
202
|
+
await db.updateUser(id, data);
|
|
203
|
+
|
|
204
|
+
// Invalidate cache
|
|
205
|
+
await cache.del(`user:${id}`);
|
|
206
|
+
|
|
207
|
+
// Or update cache immediately
|
|
208
|
+
const updated = await db.findUser(id);
|
|
209
|
+
await cache.set(`user:${id}`, updated);
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Tagging & Invalidation
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
// Tag cache entries
|
|
217
|
+
await cache.set('product:123', productData, {
|
|
218
|
+
ttl: 600,
|
|
219
|
+
tags: ['products', 'category:electronics'],
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Invalidate by tag
|
|
223
|
+
await cache.invalidateTag('category:electronics');
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Statistics & Monitoring
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
// Get cache statistics
|
|
230
|
+
const stats = await cache.stats();
|
|
231
|
+
// {
|
|
232
|
+
// hits: 1250,
|
|
233
|
+
// misses: 325,
|
|
234
|
+
// hitRate: 0.794,
|
|
235
|
+
// keys: 450,
|
|
236
|
+
// memoryUsage: 1024000 // bytes
|
|
237
|
+
// }
|
|
238
|
+
|
|
239
|
+
// Reset statistics
|
|
240
|
+
await cache.resetStats();
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## REST API Endpoints
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
GET /api/v1/cache/stats # Get cache statistics
|
|
247
|
+
POST /api/v1/cache/clear # Clear cache
|
|
248
|
+
DELETE /api/v1/cache/:key # Delete specific key
|
|
249
|
+
DELETE /api/v1/cache/pattern/:pattern # Delete by pattern
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Best Practices
|
|
253
|
+
|
|
254
|
+
1. **Use Namespaces**: Organize cache keys with namespaces
|
|
255
|
+
2. **Set Appropriate TTLs**: Don't cache data longer than necessary
|
|
256
|
+
3. **Handle Misses**: Always have fallback logic when cache misses
|
|
257
|
+
4. **Invalidate on Write**: Clear stale cache after updates
|
|
258
|
+
5. **Monitor Hit Rates**: Track cache effectiveness with statistics
|
|
259
|
+
6. **Serialize Carefully**: Be mindful of what you serialize (avoid circular references)
|
|
260
|
+
7. **Use Redis in Production**: In-memory adapter is for development only
|
|
261
|
+
|
|
262
|
+
## Performance Considerations
|
|
263
|
+
|
|
264
|
+
- **In-Memory Adapter**: Fast but limited by server memory, not shared across instances
|
|
265
|
+
- **Redis Adapter**: Shared across instances, persistent, but network latency
|
|
266
|
+
- **TTL Strategy**: Balance between freshness and cache hit rate
|
|
267
|
+
- **Key Patterns**: Use consistent naming conventions for easier invalidation
|
|
268
|
+
|
|
269
|
+
## Contract Implementation
|
|
270
|
+
|
|
271
|
+
Implements `ICacheService` from `@objectstack/spec/contracts`:
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
interface ICacheService {
|
|
275
|
+
get<T>(key: string): Promise<T | null>;
|
|
276
|
+
set<T>(key: string, value: T, options?: CacheOptions): Promise<void>;
|
|
277
|
+
del(key: string | string[]): Promise<void>;
|
|
278
|
+
has(key: string): Promise<boolean>;
|
|
279
|
+
ttl(key: string): Promise<number>;
|
|
280
|
+
expire(key: string, ttl: number): Promise<void>;
|
|
281
|
+
clear(): Promise<void>;
|
|
282
|
+
namespace(name: string): ICacheService;
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## License
|
|
287
|
+
|
|
288
|
+
Apache-2.0
|
|
289
|
+
|
|
290
|
+
## See Also
|
|
291
|
+
|
|
292
|
+
- [Redis Documentation](https://redis.io/documentation)
|
|
293
|
+
- [@objectstack/spec/contracts](../../spec/src/contracts/)
|
|
294
|
+
- [Caching Best Practices](/content/docs/guides/caching/)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/service-cache",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.5",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Cache Service for ObjectStack — implements ICacheService with in-memory and Redis adapters",
|
|
6
6
|
"type": "module",
|
|
@@ -14,13 +14,37 @@
|
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@objectstack/core": "4.0.
|
|
18
|
-
"@objectstack/spec": "4.0.
|
|
17
|
+
"@objectstack/core": "4.0.5",
|
|
18
|
+
"@objectstack/spec": "4.0.5"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
|
-
"@types/node": "^25.6.
|
|
22
|
-
"typescript": "^6.0.
|
|
23
|
-
"vitest": "^4.1.
|
|
21
|
+
"@types/node": "^25.6.2",
|
|
22
|
+
"typescript": "^6.0.3",
|
|
23
|
+
"vitest": "^4.1.5"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"objectstack",
|
|
27
|
+
"service",
|
|
28
|
+
"cache",
|
|
29
|
+
"redis"
|
|
30
|
+
],
|
|
31
|
+
"author": "ObjectStack",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/objectstack-ai/framework.git",
|
|
35
|
+
"directory": "packages/services/service-cache"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://objectstack.ai/docs",
|
|
38
|
+
"bugs": "https://github.com/objectstack-ai/framework/issues",
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"dist",
|
|
44
|
+
"README.md"
|
|
45
|
+
],
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=18.0.0"
|
|
24
48
|
},
|
|
25
49
|
"scripts": {
|
|
26
50
|
"build": "tsup --config ../../../tsup.config.ts",
|
package/.turbo/turbo-build.log
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
> @objectstack/service-cache@4.0.3 build /home/runner/work/framework/framework/packages/services/service-cache
|
|
3
|
-
> tsup --config ../../../tsup.config.ts
|
|
4
|
-
|
|
5
|
-
[34mCLI[39m Building entry: src/index.ts
|
|
6
|
-
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
7
|
-
[34mCLI[39m tsup v8.5.1
|
|
8
|
-
[34mCLI[39m Using tsup config: /home/runner/work/framework/framework/tsup.config.ts
|
|
9
|
-
[34mCLI[39m Target: es2020
|
|
10
|
-
[34mCLI[39m Cleaning output folder
|
|
11
|
-
[34mESM[39m Build start
|
|
12
|
-
[34mCJS[39m Build start
|
|
13
|
-
[32mCJS[39m [1mdist/index.cjs [22m[32m4.09 KB[39m
|
|
14
|
-
[32mCJS[39m [1mdist/index.cjs.map [22m[32m9.50 KB[39m
|
|
15
|
-
[32mCJS[39m ⚡️ Build success in 94ms
|
|
16
|
-
[32mESM[39m [1mdist/index.js [22m[32m2.98 KB[39m
|
|
17
|
-
[32mESM[39m [1mdist/index.js.map [22m[32m8.94 KB[39m
|
|
18
|
-
[32mESM[39m ⚡️ Build success in 99ms
|
|
19
|
-
[34mDTS[39m Build start
|
|
20
|
-
[32mDTS[39m ⚡️ Build success in 12340ms
|
|
21
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[32m3.56 KB[39m
|
|
22
|
-
[32mDTS[39m [1mdist/index.d.cts [22m[32m3.56 KB[39m
|
package/CHANGELOG.md
DELETED
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
# @objectstack/service-cache
|
|
2
|
-
|
|
3
|
-
## 4.0.3
|
|
4
|
-
|
|
5
|
-
### Patch Changes
|
|
6
|
-
|
|
7
|
-
- @objectstack/spec@4.0.3
|
|
8
|
-
- @objectstack/core@4.0.3
|
|
9
|
-
|
|
10
|
-
## 4.0.2
|
|
11
|
-
|
|
12
|
-
### Patch Changes
|
|
13
|
-
|
|
14
|
-
- Updated dependencies [5f659e9]
|
|
15
|
-
- @objectstack/spec@4.0.2
|
|
16
|
-
- @objectstack/core@4.0.2
|
|
17
|
-
|
|
18
|
-
## 4.0.0
|
|
19
|
-
|
|
20
|
-
### Patch Changes
|
|
21
|
-
|
|
22
|
-
- Updated dependencies [f08ffc3]
|
|
23
|
-
- Updated dependencies [e0b0a78]
|
|
24
|
-
- @objectstack/spec@4.0.0
|
|
25
|
-
- @objectstack/core@4.0.0
|
|
26
|
-
|
|
27
|
-
## 3.3.1
|
|
28
|
-
|
|
29
|
-
### Patch Changes
|
|
30
|
-
|
|
31
|
-
- @objectstack/spec@3.3.1
|
|
32
|
-
- @objectstack/core@3.3.1
|
|
33
|
-
|
|
34
|
-
## 3.3.0
|
|
35
|
-
|
|
36
|
-
### Patch Changes
|
|
37
|
-
|
|
38
|
-
- @objectstack/spec@3.3.0
|
|
39
|
-
- @objectstack/core@3.3.0
|
|
40
|
-
|
|
41
|
-
## 3.2.9
|
|
42
|
-
|
|
43
|
-
### Patch Changes
|
|
44
|
-
|
|
45
|
-
- @objectstack/spec@3.2.9
|
|
46
|
-
- @objectstack/core@3.2.9
|
|
47
|
-
|
|
48
|
-
## 3.2.8
|
|
49
|
-
|
|
50
|
-
### Patch Changes
|
|
51
|
-
|
|
52
|
-
- @objectstack/spec@3.2.8
|
|
53
|
-
- @objectstack/core@3.2.8
|
|
54
|
-
|
|
55
|
-
## 3.2.7
|
|
56
|
-
|
|
57
|
-
### Patch Changes
|
|
58
|
-
|
|
59
|
-
- @objectstack/spec@3.2.7
|
|
60
|
-
- @objectstack/core@3.2.7
|
|
61
|
-
|
|
62
|
-
## 3.2.6
|
|
63
|
-
|
|
64
|
-
### Patch Changes
|
|
65
|
-
|
|
66
|
-
- @objectstack/spec@3.2.6
|
|
67
|
-
- @objectstack/core@3.2.6
|
|
68
|
-
|
|
69
|
-
## 3.2.5
|
|
70
|
-
|
|
71
|
-
### Patch Changes
|
|
72
|
-
|
|
73
|
-
- @objectstack/spec@3.2.5
|
|
74
|
-
- @objectstack/core@3.2.5
|
|
75
|
-
|
|
76
|
-
## 3.2.4
|
|
77
|
-
|
|
78
|
-
### Patch Changes
|
|
79
|
-
|
|
80
|
-
- @objectstack/spec@3.2.4
|
|
81
|
-
- @objectstack/core@3.2.4
|
|
82
|
-
|
|
83
|
-
## 3.2.3
|
|
84
|
-
|
|
85
|
-
### Patch Changes
|
|
86
|
-
|
|
87
|
-
- @objectstack/spec@3.2.3
|
|
88
|
-
- @objectstack/core@3.2.3
|
|
89
|
-
|
|
90
|
-
## 3.2.2
|
|
91
|
-
|
|
92
|
-
### Patch Changes
|
|
93
|
-
|
|
94
|
-
- Updated dependencies [46defbb]
|
|
95
|
-
- @objectstack/spec@3.2.2
|
|
96
|
-
- @objectstack/core@3.2.2
|
|
97
|
-
|
|
98
|
-
## 3.2.1
|
|
99
|
-
|
|
100
|
-
### Patch Changes
|
|
101
|
-
|
|
102
|
-
- Updated dependencies [850b546]
|
|
103
|
-
- @objectstack/spec@3.2.1
|
|
104
|
-
- @objectstack/core@3.2.1
|
|
105
|
-
|
|
106
|
-
## 3.2.0
|
|
107
|
-
|
|
108
|
-
### Patch Changes
|
|
109
|
-
|
|
110
|
-
- Updated dependencies [5901c29]
|
|
111
|
-
- @objectstack/spec@3.2.0
|
|
112
|
-
- @objectstack/core@3.2.0
|
|
113
|
-
|
|
114
|
-
## 3.1.1
|
|
115
|
-
|
|
116
|
-
### Patch Changes
|
|
117
|
-
|
|
118
|
-
- Updated dependencies [953d667]
|
|
119
|
-
- @objectstack/spec@3.1.1
|
|
120
|
-
- @objectstack/core@3.1.1
|
|
121
|
-
|
|
122
|
-
## 3.1.0
|
|
123
|
-
|
|
124
|
-
### Patch Changes
|
|
125
|
-
|
|
126
|
-
- Updated dependencies [0088830]
|
|
127
|
-
- @objectstack/spec@3.1.0
|
|
128
|
-
- @objectstack/core@3.1.0
|
|
129
|
-
|
|
130
|
-
## 3.0.11
|
|
131
|
-
|
|
132
|
-
### Patch Changes
|
|
133
|
-
|
|
134
|
-
- Updated dependencies [92d9d99]
|
|
135
|
-
- @objectstack/spec@3.0.11
|
|
136
|
-
- @objectstack/core@3.0.11
|
|
137
|
-
|
|
138
|
-
## 3.0.10
|
|
139
|
-
|
|
140
|
-
### Patch Changes
|
|
141
|
-
|
|
142
|
-
- Updated dependencies [d1e5d31]
|
|
143
|
-
- @objectstack/spec@3.0.10
|
|
144
|
-
- @objectstack/core@3.0.10
|
|
145
|
-
|
|
146
|
-
## 3.0.9
|
|
147
|
-
|
|
148
|
-
### Patch Changes
|
|
149
|
-
|
|
150
|
-
- Updated dependencies [15e0df6]
|
|
151
|
-
- @objectstack/spec@3.0.9
|
|
152
|
-
- @objectstack/core@3.0.9
|
|
153
|
-
|
|
154
|
-
## 3.0.8
|
|
155
|
-
|
|
156
|
-
### Patch Changes
|
|
157
|
-
|
|
158
|
-
- Updated dependencies [5a968a2]
|
|
159
|
-
- @objectstack/spec@3.0.8
|
|
160
|
-
- @objectstack/core@3.0.8
|
|
161
|
-
|
|
162
|
-
## 3.0.7
|
|
163
|
-
|
|
164
|
-
### Patch Changes
|
|
165
|
-
|
|
166
|
-
- Updated dependencies [0119bd7]
|
|
167
|
-
- Updated dependencies [5426bdf]
|
|
168
|
-
- @objectstack/spec@3.0.7
|
|
169
|
-
- @objectstack/core@3.0.7
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
import type { Plugin, PluginContext } from '@objectstack/core';
|
|
4
|
-
import { MemoryCacheAdapter } from './memory-cache-adapter.js';
|
|
5
|
-
import type { MemoryCacheAdapterOptions } from './memory-cache-adapter.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Configuration options for the CacheServicePlugin.
|
|
9
|
-
*/
|
|
10
|
-
export interface CacheServicePluginOptions {
|
|
11
|
-
/** Cache adapter type (default: 'memory') */
|
|
12
|
-
adapter?: 'memory' | 'redis';
|
|
13
|
-
/** Options for the memory cache adapter */
|
|
14
|
-
memory?: MemoryCacheAdapterOptions;
|
|
15
|
-
/** Redis connection URL (used when adapter is 'redis') */
|
|
16
|
-
redisUrl?: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* CacheServicePlugin — Production ICacheService implementation.
|
|
21
|
-
*
|
|
22
|
-
* Registers a cache service with the kernel during the init phase.
|
|
23
|
-
* Supports in-memory and Redis adapters.
|
|
24
|
-
*
|
|
25
|
-
* @example
|
|
26
|
-
* ```ts
|
|
27
|
-
* import { ObjectKernel } from '@objectstack/core';
|
|
28
|
-
* import { CacheServicePlugin } from '@objectstack/service-cache';
|
|
29
|
-
*
|
|
30
|
-
* const kernel = new ObjectKernel();
|
|
31
|
-
* kernel.use(new CacheServicePlugin({ adapter: 'memory', memory: { maxSize: 1000 } }));
|
|
32
|
-
* await kernel.bootstrap();
|
|
33
|
-
*
|
|
34
|
-
* const cache = kernel.getService('cache');
|
|
35
|
-
* await cache.set('key', 'value', 60);
|
|
36
|
-
* ```
|
|
37
|
-
*/
|
|
38
|
-
export class CacheServicePlugin implements Plugin {
|
|
39
|
-
name = 'com.objectstack.service.cache';
|
|
40
|
-
version = '1.0.0';
|
|
41
|
-
type = 'standard';
|
|
42
|
-
|
|
43
|
-
private readonly options: CacheServicePluginOptions;
|
|
44
|
-
|
|
45
|
-
constructor(options: CacheServicePluginOptions = {}) {
|
|
46
|
-
this.options = { adapter: 'memory', ...options };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async init(ctx: PluginContext): Promise<void> {
|
|
50
|
-
const adapter = this.options.adapter;
|
|
51
|
-
if (adapter === 'redis') {
|
|
52
|
-
// Redis adapter is a skeleton — throw an informative error for now
|
|
53
|
-
throw new Error(
|
|
54
|
-
'Redis cache adapter is not yet implemented. ' +
|
|
55
|
-
'Use adapter: "memory" or provide a custom ICacheService via ctx.registerService("cache", impl).'
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const cache = new MemoryCacheAdapter(this.options.memory);
|
|
60
|
-
ctx.registerService('cache', cache);
|
|
61
|
-
ctx.logger.info('CacheServicePlugin: registered memory cache adapter');
|
|
62
|
-
}
|
|
63
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
export { CacheServicePlugin } from './cache-service-plugin.js';
|
|
4
|
-
export type { CacheServicePluginOptions } from './cache-service-plugin.js';
|
|
5
|
-
export { MemoryCacheAdapter } from './memory-cache-adapter.js';
|
|
6
|
-
export type { MemoryCacheAdapterOptions } from './memory-cache-adapter.js';
|
|
7
|
-
export { RedisCacheAdapter } from './redis-cache-adapter.js';
|
|
8
|
-
export type { RedisCacheAdapterOptions } from './redis-cache-adapter.js';
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
import { describe, it, expect } from 'vitest';
|
|
4
|
-
import { MemoryCacheAdapter } from './memory-cache-adapter';
|
|
5
|
-
import type { ICacheService } from '@objectstack/spec/contracts';
|
|
6
|
-
|
|
7
|
-
describe('MemoryCacheAdapter', () => {
|
|
8
|
-
it('should implement ICacheService contract', () => {
|
|
9
|
-
const cache: ICacheService = new MemoryCacheAdapter();
|
|
10
|
-
expect(typeof cache.get).toBe('function');
|
|
11
|
-
expect(typeof cache.set).toBe('function');
|
|
12
|
-
expect(typeof cache.delete).toBe('function');
|
|
13
|
-
expect(typeof cache.has).toBe('function');
|
|
14
|
-
expect(typeof cache.clear).toBe('function');
|
|
15
|
-
expect(typeof cache.stats).toBe('function');
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('should set and get a value', async () => {
|
|
19
|
-
const cache = new MemoryCacheAdapter();
|
|
20
|
-
await cache.set('key1', 'value1');
|
|
21
|
-
expect(await cache.get('key1')).toBe('value1');
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('should return undefined for missing key', async () => {
|
|
25
|
-
const cache = new MemoryCacheAdapter();
|
|
26
|
-
expect(await cache.get('nonexistent')).toBeUndefined();
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('should delete a key', async () => {
|
|
30
|
-
const cache = new MemoryCacheAdapter();
|
|
31
|
-
await cache.set('key1', 'value1');
|
|
32
|
-
expect(await cache.delete('key1')).toBe(true);
|
|
33
|
-
expect(await cache.get('key1')).toBeUndefined();
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('should return false when deleting missing key', async () => {
|
|
37
|
-
const cache = new MemoryCacheAdapter();
|
|
38
|
-
expect(await cache.delete('missing')).toBe(false);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('should check if a key exists with has()', async () => {
|
|
42
|
-
const cache = new MemoryCacheAdapter();
|
|
43
|
-
expect(await cache.has('key1')).toBe(false);
|
|
44
|
-
await cache.set('key1', 'value1');
|
|
45
|
-
expect(await cache.has('key1')).toBe(true);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('should clear all entries', async () => {
|
|
49
|
-
const cache = new MemoryCacheAdapter();
|
|
50
|
-
await cache.set('a', 1);
|
|
51
|
-
await cache.set('b', 2);
|
|
52
|
-
await cache.clear();
|
|
53
|
-
expect(await cache.has('a')).toBe(false);
|
|
54
|
-
expect(await cache.has('b')).toBe(false);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('should expire entries based on TTL', async () => {
|
|
58
|
-
const cache = new MemoryCacheAdapter();
|
|
59
|
-
await cache.set('temp', 'data', 0.001); // 1ms TTL
|
|
60
|
-
await new Promise(r => setTimeout(r, 20));
|
|
61
|
-
expect(await cache.get('temp')).toBeUndefined();
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('should track hit/miss stats', async () => {
|
|
65
|
-
const cache = new MemoryCacheAdapter();
|
|
66
|
-
await cache.set('key1', 'value1');
|
|
67
|
-
await cache.get('key1'); // hit
|
|
68
|
-
await cache.get('missing'); // miss
|
|
69
|
-
const stats = await cache.stats();
|
|
70
|
-
expect(stats.hits).toBe(1);
|
|
71
|
-
expect(stats.misses).toBe(1);
|
|
72
|
-
expect(stats.keyCount).toBe(1);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('should apply defaultTtl when no TTL is provided', async () => {
|
|
76
|
-
const cache = new MemoryCacheAdapter({ defaultTtl: 0.001 });
|
|
77
|
-
await cache.set('key', 'value');
|
|
78
|
-
await new Promise(r => setTimeout(r, 20));
|
|
79
|
-
expect(await cache.get('key')).toBeUndefined();
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('should evict oldest entry when maxSize is reached', async () => {
|
|
83
|
-
const cache = new MemoryCacheAdapter({ maxSize: 2 });
|
|
84
|
-
await cache.set('a', 1);
|
|
85
|
-
await cache.set('b', 2);
|
|
86
|
-
await cache.set('c', 3); // should evict 'a'
|
|
87
|
-
expect(await cache.has('a')).toBe(false);
|
|
88
|
-
expect(await cache.get('b')).toBe(2);
|
|
89
|
-
expect(await cache.get('c')).toBe(3);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('should not evict when updating existing key at maxSize', async () => {
|
|
93
|
-
const cache = new MemoryCacheAdapter({ maxSize: 2 });
|
|
94
|
-
await cache.set('a', 1);
|
|
95
|
-
await cache.set('b', 2);
|
|
96
|
-
await cache.set('a', 10); // update, not new entry
|
|
97
|
-
expect(await cache.get('a')).toBe(10);
|
|
98
|
-
expect(await cache.get('b')).toBe(2);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('should handle has() with expired TTL', async () => {
|
|
102
|
-
const cache = new MemoryCacheAdapter();
|
|
103
|
-
await cache.set('expiring', 'val', 0.001);
|
|
104
|
-
await new Promise(r => setTimeout(r, 20));
|
|
105
|
-
expect(await cache.has('expiring')).toBe(false);
|
|
106
|
-
});
|
|
107
|
-
});
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
import type { ICacheService, CacheStats } from '@objectstack/spec/contracts';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* In-memory cache entry with optional TTL expiry.
|
|
7
|
-
*/
|
|
8
|
-
interface CacheEntry<T = unknown> {
|
|
9
|
-
value: T;
|
|
10
|
-
expires?: number;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Configuration options for MemoryCacheAdapter.
|
|
15
|
-
*/
|
|
16
|
-
export interface MemoryCacheAdapterOptions {
|
|
17
|
-
/** Maximum number of entries before eviction (0 = unlimited) */
|
|
18
|
-
maxSize?: number;
|
|
19
|
-
/** Default TTL in seconds (0 = no expiry) */
|
|
20
|
-
defaultTtl?: number;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* In-memory cache adapter implementing ICacheService.
|
|
25
|
-
*
|
|
26
|
-
* Uses a Map-backed store with TTL-based expiry and LRU-style eviction.
|
|
27
|
-
* Suitable for single-process environments, development, and testing.
|
|
28
|
-
*/
|
|
29
|
-
export class MemoryCacheAdapter implements ICacheService {
|
|
30
|
-
private readonly store = new Map<string, CacheEntry>();
|
|
31
|
-
private hits = 0;
|
|
32
|
-
private misses = 0;
|
|
33
|
-
private readonly maxSize: number;
|
|
34
|
-
private readonly defaultTtl: number;
|
|
35
|
-
|
|
36
|
-
constructor(options: MemoryCacheAdapterOptions = {}) {
|
|
37
|
-
this.maxSize = options.maxSize ?? 0;
|
|
38
|
-
this.defaultTtl = options.defaultTtl ?? 0;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async get<T = unknown>(key: string): Promise<T | undefined> {
|
|
42
|
-
const entry = this.store.get(key);
|
|
43
|
-
if (!entry || (entry.expires && Date.now() > entry.expires)) {
|
|
44
|
-
if (entry) this.store.delete(key);
|
|
45
|
-
this.misses++;
|
|
46
|
-
return undefined;
|
|
47
|
-
}
|
|
48
|
-
this.hits++;
|
|
49
|
-
return entry.value as T;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async set<T = unknown>(key: string, value: T, ttl?: number): Promise<void> {
|
|
53
|
-
const effectiveTtl = ttl ?? this.defaultTtl;
|
|
54
|
-
if (this.maxSize > 0 && !this.store.has(key) && this.store.size >= this.maxSize) {
|
|
55
|
-
// Evict oldest entry (first key in Map insertion order)
|
|
56
|
-
const firstKey = this.store.keys().next().value;
|
|
57
|
-
if (firstKey !== undefined) this.store.delete(firstKey);
|
|
58
|
-
}
|
|
59
|
-
this.store.set(key, {
|
|
60
|
-
value,
|
|
61
|
-
expires: effectiveTtl > 0 ? Date.now() + effectiveTtl * 1000 : undefined,
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
async delete(key: string): Promise<boolean> {
|
|
66
|
-
return this.store.delete(key);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async has(key: string): Promise<boolean> {
|
|
70
|
-
const entry = this.store.get(key);
|
|
71
|
-
if (!entry) return false;
|
|
72
|
-
if (entry.expires && Date.now() > entry.expires) {
|
|
73
|
-
this.store.delete(key);
|
|
74
|
-
return false;
|
|
75
|
-
}
|
|
76
|
-
return true;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async clear(): Promise<void> {
|
|
80
|
-
this.store.clear();
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async stats(): Promise<CacheStats> {
|
|
84
|
-
return {
|
|
85
|
-
hits: this.hits,
|
|
86
|
-
misses: this.misses,
|
|
87
|
-
keyCount: this.store.size,
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
import type { ICacheService, CacheStats } from '@objectstack/spec/contracts';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Configuration for the Redis cache adapter.
|
|
7
|
-
*/
|
|
8
|
-
export interface RedisCacheAdapterOptions {
|
|
9
|
-
/** Redis connection URL (e.g. 'redis://localhost:6379') */
|
|
10
|
-
url: string;
|
|
11
|
-
/** Key prefix for namespacing (default: 'os:') */
|
|
12
|
-
keyPrefix?: string;
|
|
13
|
-
/** Default TTL in seconds (0 = no expiry) */
|
|
14
|
-
defaultTtl?: number;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Redis cache adapter skeleton implementing ICacheService.
|
|
19
|
-
*
|
|
20
|
-
* This is a placeholder for future Redis integration.
|
|
21
|
-
* Concrete implementation will use `ioredis` or `redis` client.
|
|
22
|
-
*
|
|
23
|
-
* @example
|
|
24
|
-
* ```ts
|
|
25
|
-
* const cache = new RedisCacheAdapter({ url: 'redis://localhost:6379' });
|
|
26
|
-
* await cache.set('user:1', { name: 'Alice' }, 3600);
|
|
27
|
-
* ```
|
|
28
|
-
*/
|
|
29
|
-
export class RedisCacheAdapter implements ICacheService {
|
|
30
|
-
private readonly url: string;
|
|
31
|
-
private readonly keyPrefix: string;
|
|
32
|
-
private readonly defaultTtl: number;
|
|
33
|
-
|
|
34
|
-
constructor(options: RedisCacheAdapterOptions) {
|
|
35
|
-
this.url = options.url;
|
|
36
|
-
this.keyPrefix = options.keyPrefix ?? 'os:';
|
|
37
|
-
this.defaultTtl = options.defaultTtl ?? 0;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async get<T = unknown>(_key: string): Promise<T | undefined> {
|
|
41
|
-
throw new Error(`RedisCacheAdapter not yet implemented (url: ${this.url}, prefix: ${this.keyPrefix}, ttl: ${this.defaultTtl})`);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async set<T = unknown>(_key: string, _value: T, _ttl?: number): Promise<void> {
|
|
45
|
-
throw new Error('RedisCacheAdapter not yet implemented');
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async delete(_key: string): Promise<boolean> {
|
|
49
|
-
throw new Error('RedisCacheAdapter not yet implemented');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async has(_key: string): Promise<boolean> {
|
|
53
|
-
throw new Error('RedisCacheAdapter not yet implemented');
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async clear(): Promise<void> {
|
|
57
|
-
throw new Error('RedisCacheAdapter not yet implemented');
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async stats(): Promise<CacheStats> {
|
|
61
|
-
throw new Error('RedisCacheAdapter not yet implemented');
|
|
62
|
-
}
|
|
63
|
-
}
|
package/tsconfig.json
DELETED