@maestro-js/agent-skills 1.0.0-alpha.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.
- package/dist/choosing-an-npm-package/SKILL.md +72 -0
- package/dist/maestro-cli/SKILL.md +257 -0
- package/dist/maestro-packages/SKILL.md +42 -0
- package/dist/maestro-packages/broadcast.md +306 -0
- package/dist/maestro-packages/cache-control.md +337 -0
- package/dist/maestro-packages/cache.md +397 -0
- package/dist/maestro-packages/config.md +203 -0
- package/dist/maestro-packages/context.md +365 -0
- package/dist/maestro-packages/crypt.md +253 -0
- package/dist/maestro-packages/db.md +391 -0
- package/dist/maestro-packages/events.md +295 -0
- package/dist/maestro-packages/exceptions.md +231 -0
- package/dist/maestro-packages/form.md +391 -0
- package/dist/maestro-packages/hash.md +248 -0
- package/dist/maestro-packages/helpers.md +249 -0
- package/dist/maestro-packages/log.md +321 -0
- package/dist/maestro-packages/mail.md +365 -0
- package/dist/maestro-packages/metrics.md +236 -0
- package/dist/maestro-packages/queue.md +284 -0
- package/dist/maestro-packages/rate-limiting.md +206 -0
- package/dist/maestro-packages/react-router-file-routes.md +337 -0
- package/dist/maestro-packages/recurring-jobs.md +353 -0
- package/dist/maestro-packages/sql.md +316 -0
- package/dist/maestro-packages/tracing.md +240 -0
- package/package.json +33 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: broadcast
|
|
3
|
+
description: "\"Pattern-based event routing across channels and processes using @maestro-js/broadcast. Use when working with @maestro-js/broadcast, channel patterns, broadcast listeners, event name generation, or wildcard/dynamic-segment routing. Key capabilities: define typed channels with URL-like patterns (:id, optional segments, wildcards), emit and listen for events through a pluggable driver (e.g. Events/Redis), extract params from matched patterns, generate concrete event names from patterns, and manage listener lifecycles.\""
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @maestro-js/broadcast
|
|
7
|
+
|
|
8
|
+
Pattern-based event routing that layers typed channels, dynamic-segment matching, and wildcard support on top of a pluggable driver (typically `@maestro-js/events`). Each channel is a typed event emitter scoped to a URL-like pattern; the driver distributes emitted events across processes.
|
|
9
|
+
|
|
10
|
+
## Quick Setup
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { Broadcast } from '@maestro-js/broadcast'
|
|
14
|
+
import { Events } from '@maestro-js/events'
|
|
15
|
+
|
|
16
|
+
// 1. Define typed channels
|
|
17
|
+
const userChannel = Broadcast.channel('/user/:id').create<{ name: string }>()
|
|
18
|
+
const chatChannel = Broadcast.channel('/chat(/:room)').create<{ message: string }>()
|
|
19
|
+
|
|
20
|
+
// 2. Create and register
|
|
21
|
+
const broadcast = Broadcast.Provider.create({
|
|
22
|
+
channels: [userChannel, chatChannel],
|
|
23
|
+
driver: Events // any BroadcastingDriver
|
|
24
|
+
})
|
|
25
|
+
Broadcast.Provider.register('default', broadcast)
|
|
26
|
+
|
|
27
|
+
// 3. Use via facade
|
|
28
|
+
Broadcast.addListener('/user/42', {
|
|
29
|
+
handler({ data, params }) {
|
|
30
|
+
console.log(data.name, params.id)
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const event = Broadcast.generateEventName('/user/:id', { id: '42' })
|
|
35
|
+
Broadcast.emit(event, { name: 'Alice' })
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Channel Patterns and Parameters
|
|
39
|
+
|
|
40
|
+
Patterns use `@remix-run/route-pattern` syntax. Three segment types:
|
|
41
|
+
|
|
42
|
+
| Syntax | Example | Matches |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| `:param` | `/user/:id` | `/user/42` -> `{ id: '42' }` |
|
|
45
|
+
| `(/:param)` | `/chat(/:room)` | `/chat` or `/chat/general` |
|
|
46
|
+
| `*splat` | `/system/*splat` | `/system/logs/today` -> `{ splat: 'logs/today' }` |
|
|
47
|
+
|
|
48
|
+
Matching is case-insensitive. Static patterns (e.g. `/health`) also work.
|
|
49
|
+
|
|
50
|
+
### Pattern Matching
|
|
51
|
+
|
|
52
|
+
Use `Broadcast.matchPattern` to test if an event name matches a pattern:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
const match = Broadcast.matchPattern('/user/:id', '/user/42')
|
|
56
|
+
// match.params.id === '42'
|
|
57
|
+
// match.eventName === '/user/42'
|
|
58
|
+
// match.eventNameBase === '/user/42'
|
|
59
|
+
// match.pattern === '/user/:id'
|
|
60
|
+
|
|
61
|
+
const noMatch = Broadcast.matchPattern('/user/:id', '/order/42')
|
|
62
|
+
// noMatch === null
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
For wildcard patterns, `eventNameBase` strips the wildcard suffix:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
const match = Broadcast.matchPattern('/system/*splat', '/system/logs/today')
|
|
69
|
+
// match.params.splat === 'logs/today'
|
|
70
|
+
// match.eventNameBase === '/system'
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Event Name Generation
|
|
74
|
+
|
|
75
|
+
Interpolate params into a pattern to produce a concrete event name:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
Broadcast.generateEventName('/user/:id', { id: '42' })
|
|
79
|
+
// '/user/42'
|
|
80
|
+
|
|
81
|
+
Broadcast.generateEventName('/org/:orgId/user/:userId', { orgId: 'acme', userId: '42' })
|
|
82
|
+
// '/org/acme/user/42'
|
|
83
|
+
|
|
84
|
+
Broadcast.generateEventName('/chat(/:room)', { room: 'general' })
|
|
85
|
+
// '/chat/general'
|
|
86
|
+
|
|
87
|
+
Broadcast.generateEventName('/chat(/:room)')
|
|
88
|
+
// '/chat'
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## API Reference
|
|
92
|
+
|
|
93
|
+
### `Broadcast.channel(pattern)`
|
|
94
|
+
|
|
95
|
+
Create a channel builder. Call `.create<EventData>()` to produce a typed `Channel`.
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
function channel<Pattern extends string>(pattern: Pattern): {
|
|
99
|
+
create<EventData extends Record<string, any>>(): Channel<Pattern, EventData>
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### `Broadcast.Provider.create(config)`
|
|
104
|
+
|
|
105
|
+
Create a broadcast service instance from channels and a driver.
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
function create<Channels extends Channel<string, Record<string, any>>[]>(config: {
|
|
109
|
+
channels: Channels
|
|
110
|
+
driver: BroadcastingDriver
|
|
111
|
+
}): BroadcastService<Channels>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Throws if duplicate channel patterns are provided.
|
|
115
|
+
|
|
116
|
+
### `Broadcast.Provider.register(name, service)`
|
|
117
|
+
|
|
118
|
+
Register the service instance in the named registry.
|
|
119
|
+
|
|
120
|
+
### `Broadcast.provider(key)`
|
|
121
|
+
|
|
122
|
+
Resolve a named broadcast instance. Returns the full `BroadcastService` interface.
|
|
123
|
+
|
|
124
|
+
### `Broadcast.emit(eventName, data)`
|
|
125
|
+
|
|
126
|
+
Broadcast data through the driver. Throws if the event name does not match any registered channel pattern.
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
Broadcast.emit('/user/42', { name: 'Alice' })
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### `Broadcast.addListener(eventName, listener)`
|
|
133
|
+
|
|
134
|
+
Subscribe to events matching a channel pattern. Returns a handle with `remove()`.
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
const handle = Broadcast.addListener('/user/42', {
|
|
138
|
+
handler({ data, params, eventName }) {
|
|
139
|
+
// data: { name: string }
|
|
140
|
+
// params: { id: string }
|
|
141
|
+
// eventName: '/user/42'
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
handle.remove() // unsubscribe
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### `Broadcast.getChannel(pattern)`
|
|
149
|
+
|
|
150
|
+
Retrieve a channel by its exact pattern string. Returns the channel without `emit` (read-only view).
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
const channel = Broadcast.getChannel('/user/:id')
|
|
154
|
+
// channel.pattern === '/user/:id'
|
|
155
|
+
// channel.addListener(...)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### `Broadcast.generateEventName(pattern, params)`
|
|
159
|
+
|
|
160
|
+
Interpolate parameters into a pattern to produce a concrete event name string.
|
|
161
|
+
|
|
162
|
+
### `Broadcast.matchPattern(pattern, eventName)`
|
|
163
|
+
|
|
164
|
+
Test if an event name matches a pattern. Returns `EventNameMatch` or `null`.
|
|
165
|
+
|
|
166
|
+
### `Broadcast.removeAllListeners()`
|
|
167
|
+
|
|
168
|
+
Remove all active listeners from all channels and the driver.
|
|
169
|
+
|
|
170
|
+
## BroadcastingDriver Interface
|
|
171
|
+
|
|
172
|
+
Any driver must implement:
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
interface BroadcastingDriver {
|
|
176
|
+
emit(eventName: string, data: Record<string, any>): unknown
|
|
177
|
+
addListener(eventName: string, listener: (data: Record<string, any>) => void): unknown
|
|
178
|
+
removeListener(eventName: string, listener: (data: Record<string, any>) => void): unknown
|
|
179
|
+
removeAllListeners?(): unknown
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`@maestro-js/events` satisfies this interface. A minimal mock driver for testing:
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
const listeners: Map<string, Set<(data: Record<string, any>) => void>> = new Map()
|
|
187
|
+
|
|
188
|
+
const mockDriver = {
|
|
189
|
+
emit(eventName: string, data: Record<string, any>) {
|
|
190
|
+
const set = listeners.get(eventName)
|
|
191
|
+
if (set) {
|
|
192
|
+
for (const listener of set) listener(data)
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
addListener(eventName: string, listener: (data: Record<string, any>) => void) {
|
|
196
|
+
if (!listeners.has(eventName)) listeners.set(eventName, new Set())
|
|
197
|
+
listeners.get(eventName)!.add(listener)
|
|
198
|
+
},
|
|
199
|
+
removeListener(eventName: string, listener: (data: Record<string, any>) => void) {
|
|
200
|
+
listeners.get(eventName)?.delete(listener)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Key Types
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
// Extracts union of dynamic param names from a pattern
|
|
209
|
+
type PatternParam<Pattern extends string>
|
|
210
|
+
|
|
211
|
+
// Match result from matchPattern
|
|
212
|
+
interface EventNameMatch<Pattern extends string> {
|
|
213
|
+
params: PatternParams<Pattern>
|
|
214
|
+
eventName: string
|
|
215
|
+
eventNameBase: string
|
|
216
|
+
pattern: Pattern
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Listener object
|
|
220
|
+
interface ChannelListener<Pattern extends string, EventData extends Record<string, any>> {
|
|
221
|
+
handler: (options: { data: EventData; params: PatternParams<Pattern>; eventName: string }) => void
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Handle returned by addListener
|
|
225
|
+
type AddListenerResult = { remove(): void }
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Common Patterns
|
|
229
|
+
|
|
230
|
+
### Multiple Named Providers
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
declare module '@maestro-js/broadcast' {
|
|
234
|
+
namespace Broadcast.Provider {
|
|
235
|
+
interface Keys {
|
|
236
|
+
default: [typeof userChannel, typeof chatChannel]
|
|
237
|
+
admin: [typeof adminChannel]
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
Broadcast.Provider.register('default', defaultBroadcast)
|
|
243
|
+
Broadcast.Provider.register('admin', adminBroadcast)
|
|
244
|
+
|
|
245
|
+
const admin = Broadcast.provider('admin')
|
|
246
|
+
admin.emit('/admin/alerts', { level: 'critical' })
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Listen Then Emit
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
const orderChannel = Broadcast.channel('/order/:orderId').create<{ status: string }>()
|
|
253
|
+
const broadcast = Broadcast.Provider.create({ channels: [orderChannel], driver: Events })
|
|
254
|
+
Broadcast.Provider.register('default', broadcast)
|
|
255
|
+
|
|
256
|
+
// Subscribe
|
|
257
|
+
Broadcast.addListener('/order/123', {
|
|
258
|
+
handler({ data, params }) {
|
|
259
|
+
console.log(`Order ${params.orderId} is ${data.status}`)
|
|
260
|
+
}
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
// Emit with generated event name
|
|
264
|
+
const event = Broadcast.generateEventName('/order/:orderId', { orderId: '123' })
|
|
265
|
+
Broadcast.emit(event, { status: 'shipped' })
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Cleanup
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
// Remove a single listener
|
|
272
|
+
const handle = Broadcast.addListener('/user/42', { handler() {} })
|
|
273
|
+
handle.remove()
|
|
274
|
+
|
|
275
|
+
// Remove all listeners at once
|
|
276
|
+
Broadcast.removeAllListeners()
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Testing
|
|
280
|
+
|
|
281
|
+
Run tests:
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
pnpm --filter @maestro-js/broadcast test
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Or a single file:
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
cd packages/broadcast && npx beartest ./tests/broadcast.test.ts
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Tests use `beartest-js` with `expect` for assertions. Use the mock driver pattern shown above for unit tests -- no real Redis or Events instance required.
|
|
294
|
+
|
|
295
|
+
## Cross-Package Integration
|
|
296
|
+
|
|
297
|
+
- **@maestro-js/service-registry** -- powers the Provider pattern (create, register, resolve via proxy)
|
|
298
|
+
- **@maestro-js/events** -- primary driver for cross-process event distribution (Redis, WebSocket, or Local)
|
|
299
|
+
- **@remix-run/route-pattern** -- handles URL-like pattern matching and parameter extraction
|
|
300
|
+
|
|
301
|
+
### Source Files
|
|
302
|
+
|
|
303
|
+
- `packages/broadcast/src/index.ts` -- main export, Provider, facade, channel factory
|
|
304
|
+
- `packages/broadcast/src/broadcasting-helpers.ts` -- `matchPattern` and `generateEventName` implementations
|
|
305
|
+
- `packages/broadcast/src/broadcasting-types.ts` -- all type definitions (Channel, Broadcast, BroadcastingDriver, etc.)
|
|
306
|
+
- `packages/broadcast/tests/broadcast.test.ts` -- full test suite
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cache-control
|
|
3
|
+
description: "HTTP Cache-Control-inspired caching for async functions with caller/callee directives. Use when working with @maestro-js/cache-control. Key capabilities: wrap async functions with cacheFn to add cache-control semantics, set response directives (maxAge, staleWhileRevalidate, noStore) from the callee, set request directives (maxAge, noStore) from the caller, automatic background revalidation of stale data, in-flight request deduplication, built-in LRU cache or custom cache backends, human-readable duration strings (e.g. '5 minutes'), structured logging of all cache lifecycle events, and optional tracing integration for observability."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
`@maestro-js/cache-control` provides an HTTP Cache-Control-inspired caching layer for async functions. Both the caller and callee provide directives that together determine whether a cached response is returned, a fresh value is fetched, or a stale value is served while revalidation happens in the background. It follows the maestro Provider pattern built on `@maestro-js/service-registry`.
|
|
9
|
+
|
|
10
|
+
## Quick Setup
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { CacheControl } from '@maestro-js/cache-control'
|
|
14
|
+
import { Log } from '@maestro-js/log'
|
|
15
|
+
|
|
16
|
+
// Create a logger
|
|
17
|
+
const logger = Log.create<[CacheControl.LogMessage]>({
|
|
18
|
+
transports: [{ write(msg) { console.log(msg) } }]
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
// Create and register the service
|
|
22
|
+
const cc = CacheControl.Provider.create({
|
|
23
|
+
cache: CacheControl.caches.leastRecentlyUsed({ max: 100 }),
|
|
24
|
+
logger
|
|
25
|
+
})
|
|
26
|
+
CacheControl.Provider.register('default', cc)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
After registering the default provider, use the facade directly:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
const getUser = CacheControl.cacheFn('users/getById', async (id: string) => {
|
|
33
|
+
const user = await fetchUser(id)
|
|
34
|
+
return CacheControl.response(user, {
|
|
35
|
+
maxAge: '5 minutes',
|
|
36
|
+
staleWhileRevalidate: '1 minute'
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const user = await getUser('123', { maxAge: '5 minutes' })
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Cache Directives
|
|
44
|
+
|
|
45
|
+
### Response Directives (Callee)
|
|
46
|
+
|
|
47
|
+
Set by the function being cached via `CacheControl.response(value, options)`:
|
|
48
|
+
|
|
49
|
+
| Directive | Type | Description |
|
|
50
|
+
|-----------|------|-------------|
|
|
51
|
+
| `maxAge` | `number \| DurationString` | Seconds the response remains fresh. Required unless `noStore` is set. |
|
|
52
|
+
| `staleWhileRevalidate` | `number \| DurationString \| undefined` | Seconds a stale response can be served while revalidating in the background. |
|
|
53
|
+
| `swr` | same as above | Alias for `staleWhileRevalidate`. |
|
|
54
|
+
| `noStore` | `true` | Return the value but never store it in the cache. |
|
|
55
|
+
|
|
56
|
+
### Request Directives (Caller)
|
|
57
|
+
|
|
58
|
+
Passed as the last argument when calling a cached function:
|
|
59
|
+
|
|
60
|
+
| Directive | Type | Description |
|
|
61
|
+
|-----------|------|-------------|
|
|
62
|
+
| `maxAge` | `number \| DurationString \| null` | Maximum acceptable age (seconds) for a cached value. `null` means accept any age within response maxAge. |
|
|
63
|
+
| `noStore` | `boolean \| undefined` | Bypass the cache entirely -- always call the underlying function and do not store the result. |
|
|
64
|
+
|
|
65
|
+
### Duration Strings
|
|
66
|
+
|
|
67
|
+
Both `maxAge` and `staleWhileRevalidate` accept human-readable duration strings:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
type CacheControlDurationString = `${number} ${'day' | 'days' | 'hour' | 'hours' | 'minute' | 'minutes' | 'second' | 'seconds'}`
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Examples: `'1 second'`, `'30 seconds'`, `'5 minutes'`, `'1 hour'`, `'7 days'`
|
|
74
|
+
|
|
75
|
+
## API Reference
|
|
76
|
+
|
|
77
|
+
### `CacheControl.Provider.create(config)`
|
|
78
|
+
|
|
79
|
+
Create a new CacheControl service instance.
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
function create(config: {
|
|
83
|
+
cache: CacheControlCache
|
|
84
|
+
logger: Log.LogFunctions<[CacheControl.LogMessage]>
|
|
85
|
+
tracing?: CacheControl.Tracing
|
|
86
|
+
}): CacheControl.CacheControlService
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### `CacheControl.Provider.register(name, service)`
|
|
90
|
+
|
|
91
|
+
Register a service instance in the named registry.
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
CacheControl.Provider.register('default', cc)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### `CacheControl.provider(key)`
|
|
98
|
+
|
|
99
|
+
Resolve a named service instance. Returns `{ cacheFn }`.
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
const { cacheFn } = CacheControl.provider('analytics')
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### `CacheControl.cacheFn(options, func)`
|
|
106
|
+
|
|
107
|
+
Wrap an async function with cache-control semantics. Available on the facade (uses the `'default'` provider) or on a service instance.
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
// String key prefix (arguments are hashed with object-hash)
|
|
111
|
+
const fn = CacheControl.cacheFn('my/prefix', async (id: string) => {
|
|
112
|
+
return CacheControl.response(await fetchData(id), { maxAge: 60 })
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
// Object options with custom argument hashing
|
|
116
|
+
const fn = CacheControl.cacheFn(
|
|
117
|
+
{
|
|
118
|
+
keyPrefix: 'my/prefix',
|
|
119
|
+
hashArguments: (args) => args[0]
|
|
120
|
+
},
|
|
121
|
+
async (id: string) => {
|
|
122
|
+
return CacheControl.response(await fetchData(id), { maxAge: 60 })
|
|
123
|
+
}
|
|
124
|
+
)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Signature:**
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
cacheFn<Args, Returns>(
|
|
131
|
+
options: string | { keyPrefix: string; hashArguments?(args: Args): string },
|
|
132
|
+
func: (...args: Args) => Returns
|
|
133
|
+
): (...args: [...Args, CacheControlRequestDirectives]) => Promise<UnwrapCacheResponse<Awaited<Returns>>>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The wrapped function appends a `CacheControlRequestDirectives` argument. The callback **must** return `CacheControl.response(value, options)`.
|
|
137
|
+
|
|
138
|
+
### `CacheControl.response(value, options)`
|
|
139
|
+
|
|
140
|
+
Create a cache-control response wrapper pairing a value with freshness directives.
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
// With maxAge and staleWhileRevalidate
|
|
144
|
+
CacheControl.response(data, { maxAge: '5 minutes', staleWhileRevalidate: '15 minutes' })
|
|
145
|
+
|
|
146
|
+
// With swr alias
|
|
147
|
+
CacheControl.response(data, { maxAge: '5 minutes', swr: '15 minutes' })
|
|
148
|
+
|
|
149
|
+
// No-store (return value but do not cache)
|
|
150
|
+
CacheControl.response(data, { noStore: true })
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### `CacheControl.caches.leastRecentlyUsed(config)`
|
|
154
|
+
|
|
155
|
+
Create an in-memory LRU cache backend.
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
const cache = CacheControl.caches.leastRecentlyUsed({ max: 500 })
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### `CacheControlCache` Interface
|
|
162
|
+
|
|
163
|
+
Implement this interface to provide a custom cache backend:
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
interface CacheControlCache {
|
|
167
|
+
set(key: string, value: object, ttlSeconds: number): void | Promise<void>
|
|
168
|
+
get<T>(key: string): T | Promise<T> | undefined | null
|
|
169
|
+
del(key: string): void | Promise<void>
|
|
170
|
+
dump?(): any
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Common Patterns
|
|
175
|
+
|
|
176
|
+
### Basic Cached Function
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
const getUser = CacheControl.cacheFn('users/getById', async (id: string) => {
|
|
180
|
+
const user = await db.query('SELECT * FROM users WHERE id = ?', [id])
|
|
181
|
+
return CacheControl.response(user, { maxAge: '5 minutes', swr: '15 minutes' })
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
// Caller accepts any cached value within the response's maxAge
|
|
185
|
+
const user = await getUser('123', { maxAge: null })
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Force Fresh Data
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
// Bypass cache -- always execute the function and do not store
|
|
192
|
+
const user = await getUser('123', { noStore: true })
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Caller-Limited Staleness
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
// Only accept cached values generated within the last 60 seconds
|
|
199
|
+
const user = await getUser('123', { maxAge: 60 })
|
|
200
|
+
|
|
201
|
+
// Accept cached values up to 5 minutes old
|
|
202
|
+
const user = await getUser('123', { maxAge: '5 minutes' })
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Custom Argument Hashing
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
const getReport = CacheControl.cacheFn(
|
|
209
|
+
{
|
|
210
|
+
keyPrefix: 'reports/monthly',
|
|
211
|
+
hashArguments: (args) => args[0]
|
|
212
|
+
},
|
|
213
|
+
async (month: string) => {
|
|
214
|
+
const report = await generateReport(month)
|
|
215
|
+
return CacheControl.response(report, { maxAge: '1 hour' })
|
|
216
|
+
}
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
await getReport('2025-01', { maxAge: null })
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Multiple Named Providers
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
const fastCache = CacheControl.Provider.create({
|
|
226
|
+
cache: CacheControl.caches.leastRecentlyUsed({ max: 1000 }),
|
|
227
|
+
logger
|
|
228
|
+
})
|
|
229
|
+
CacheControl.Provider.register('fast', fastCache)
|
|
230
|
+
|
|
231
|
+
const { cacheFn } = CacheControl.provider('fast')
|
|
232
|
+
const fn = cacheFn('my/key', myFunction)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Sensitive Data (No-Store Response)
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
const getToken = CacheControl.cacheFn('auth/token', async (userId: string) => {
|
|
239
|
+
const token = await generateToken(userId)
|
|
240
|
+
return CacheControl.response(token, { noStore: true })
|
|
241
|
+
})
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Tracing Integration
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
const cc = CacheControl.Provider.create({
|
|
248
|
+
cache: CacheControl.caches.leastRecentlyUsed({ max: 100 }),
|
|
249
|
+
logger,
|
|
250
|
+
tracing: {
|
|
251
|
+
startActiveSpan: (name, fn) => tracer.startActiveSpan(name, fn),
|
|
252
|
+
getActiveSpan: () => tracer.getActiveSpan()
|
|
253
|
+
}
|
|
254
|
+
})
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Testing
|
|
258
|
+
|
|
259
|
+
Run tests:
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
pnpm --filter @maestro-js/cache-control test
|
|
263
|
+
# or
|
|
264
|
+
cd packages/cache-control && npx beartest ./tests/cache-control.test.ts
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Tests use `beartest-js` with `expect`. Create mock caches for testing:
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
function mockWorkingCache(): CacheControl.CacheControlService['cache'] {
|
|
271
|
+
const map = new Map<string, any>()
|
|
272
|
+
return {
|
|
273
|
+
get(key) { return map.get(key) },
|
|
274
|
+
set(key, value) { map.set(key, value) },
|
|
275
|
+
del(key) { map.delete(key) }
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function mockBrokenCache(): CacheControl.CacheControlService['cache'] {
|
|
280
|
+
return {
|
|
281
|
+
get() { throw new Error('This cache is broken') },
|
|
282
|
+
set(key, value) {},
|
|
283
|
+
del(key) {}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Create a testing logger that tracks cache hits and background refreshes:
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
const logger = Log.create<[CacheControl.LogMessage]>({
|
|
292
|
+
transports: [{
|
|
293
|
+
write(message) {
|
|
294
|
+
const data = message.data[0]
|
|
295
|
+
// Track events by callId to detect cache hits, errors, and background refreshes
|
|
296
|
+
}
|
|
297
|
+
}]
|
|
298
|
+
})
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Use `{ maxAge: null }` as the standard no-preference request directive in tests.
|
|
302
|
+
|
|
303
|
+
## Cross-Package Integration
|
|
304
|
+
|
|
305
|
+
### Dependencies
|
|
306
|
+
|
|
307
|
+
- **`@maestro-js/service-registry`** -- Provider pattern (create, register, resolve via Proxy)
|
|
308
|
+
- **`@maestro-js/log`** -- Structured logging of cache lifecycle events
|
|
309
|
+
|
|
310
|
+
### Cache Backend
|
|
311
|
+
|
|
312
|
+
The `CacheControlCache` interface is deliberately simple. Any backend that implements `get`, `set`, and `del` works. The built-in `CacheControl.caches.leastRecentlyUsed()` uses the `lru-cache` npm package internally.
|
|
313
|
+
|
|
314
|
+
A custom backend backed by `@maestro-js/cache` (Redis, etc.) can be created by implementing the `CacheControlCache` interface and delegating to the cache package.
|
|
315
|
+
|
|
316
|
+
### Log Events
|
|
317
|
+
|
|
318
|
+
All operations emit structured log messages of type `CacheControl.LogMessage`. Key events:
|
|
319
|
+
|
|
320
|
+
- `cacheFunctionCalled` -- function invoked with arguments and request directives
|
|
321
|
+
- `getCachedValueStart` / `getCachedValueSuccess` / `getCachedValueEmpty` / `getCachedValueError` -- cache lookup lifecycle
|
|
322
|
+
- `getCachedValueNotReused` -- caller's maxAge directive rejected a cached value
|
|
323
|
+
- `getCachedValueOutdated` -- entry expired past maxAge + staleWhileRevalidate window
|
|
324
|
+
- `getFreshValueStart` / `getFreshValueSuccess` / `getFreshValueError` -- fresh value fetch lifecycle
|
|
325
|
+
- `writeFreshValueSuccess` / `writeFreshValueError` -- writing to cache backend
|
|
326
|
+
- `refreshValueStart` / `refreshValueSuccess` / `refreshValueError` -- background stale-while-revalidate lifecycle
|
|
327
|
+
- `cacheFunctionReturned` / `cacheFunctionComplete` -- overall function lifecycle
|
|
328
|
+
|
|
329
|
+
### Key File Locations
|
|
330
|
+
|
|
331
|
+
- Source: `packages/cache-control/src/index.ts`
|
|
332
|
+
- Types: `packages/cache-control/src/cache-control-types.ts`
|
|
333
|
+
- Cache lookup logic: `packages/cache-control/src/get-cache-control-value.ts`
|
|
334
|
+
- Fresh value + write logic: `packages/cache-control/src/get-fresh-cache-control-value.ts`
|
|
335
|
+
- Response parsing + duration strings: `packages/cache-control/src/parse-cache-response.ts`
|
|
336
|
+
- LRU cache backend: `packages/cache-control/src/lru-cache.ts`
|
|
337
|
+
- Tests: `packages/cache-control/tests/cache-control.test.ts`
|