@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,248 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: hash
|
|
3
|
+
description: "Pluggable hashing and verification with driver-based backends for @maestro-js/hash. Use when working with @maestro-js/hash, hashing values, verifying hashes, detecting when a rehash is needed, or rotating signing keys. Built-in drivers: SHA256 (HMAC-SHA256 with key rotation and timing-safe comparison)."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @maestro-js/hash
|
|
7
|
+
|
|
8
|
+
HMAC-SHA256 signing and verification with transparent key rotation. Built on the
|
|
9
|
+
Provider pattern from `@maestro-js/service-registry`.
|
|
10
|
+
|
|
11
|
+
## Quick Setup
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { Hash } from '@maestro-js/hash'
|
|
15
|
+
|
|
16
|
+
Hash.Provider.register('default', Hash.Provider.create({
|
|
17
|
+
key: process.env.HMAC_SIGNING_KEY!,
|
|
18
|
+
previousKeys: [process.env.HMAC_SIGNING_KEY_PREV!]
|
|
19
|
+
}))
|
|
20
|
+
|
|
21
|
+
// Use the facade directly
|
|
22
|
+
const signed = Hash.signString('order.completed:ord_8k2Xp')
|
|
23
|
+
Hash.isSignatureValid({ plaintext: 'order.completed:ord_8k2Xp', signedText: signed }) // true
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## API Reference
|
|
27
|
+
|
|
28
|
+
### `Hash.Provider.create(config)` — Factory
|
|
29
|
+
|
|
30
|
+
Create a hash service instance. Accepts a keyset config and returns a `HashService`.
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
interface Keyset {
|
|
34
|
+
key: string // Current signing key, base64-encoded
|
|
35
|
+
previousKeys: string[] // Rotated-out keys, base64-encoded (verification only)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const svc = Hash.Provider.create({
|
|
39
|
+
key: 'base64EncodedCurrentKey',
|
|
40
|
+
previousKeys: ['base64EncodedOldKey1', 'base64EncodedOldKey2']
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
All keys must be valid base64 strings. The factory asserts that at least the
|
|
45
|
+
current key or the first previous key exists at creation time.
|
|
46
|
+
|
|
47
|
+
### `Hash.Provider.register(name, service)` — Registration
|
|
48
|
+
|
|
49
|
+
Register a created service under a name for later resolution.
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
Hash.Provider.register('default', svc)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### `Hash.provider(key)` — Resolution
|
|
56
|
+
|
|
57
|
+
Resolve a named instance. The default facade (`Hash.signString`, etc.) resolves
|
|
58
|
+
`'default'` automatically.
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const custom = Hash.provider('myService')
|
|
62
|
+
custom.signString('data')
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### `signString(plaintext)` — Sign
|
|
66
|
+
|
|
67
|
+
Sign a plaintext string using the current key. Returns a `keyId.digest` string
|
|
68
|
+
where `keyId` is a 4-character identifier derived from the key.
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
const signed = Hash.signString('Testing')
|
|
72
|
+
// Returns something like 'Ab1c.xYzBase64Digest...'
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### `isSignatureValid({ plaintext, signedText })` — Verify
|
|
76
|
+
|
|
77
|
+
Verify a signed string against the plaintext. Checks all keys in the keyset
|
|
78
|
+
whose ID matches the embedded key ID. Uses `crypto.timingSafeEqual` to prevent
|
|
79
|
+
timing attacks.
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
Hash.isSignatureValid({ plaintext: 'Testing', signedText: signed }) // true
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Returns `false` for tampered signatures, unknown keys, or length mismatches.
|
|
86
|
+
|
|
87
|
+
### `getSigningKeyId(signedText)` — Extract Key ID
|
|
88
|
+
|
|
89
|
+
Extract the key ID prefix from a signed string. Returns `undefined` if the
|
|
90
|
+
string has no `.` separator (malformed).
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
Hash.getSigningKeyId(signed) // 'Ab1c'
|
|
94
|
+
Hash.getSigningKeyId('no-dot') // undefined
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### `getCurrentKeyId()` — Current Key ID
|
|
98
|
+
|
|
99
|
+
Return the 4-character ID of the current active signing key.
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
Hash.getCurrentKeyId() // 'Ab1c'
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Key Rotation
|
|
106
|
+
|
|
107
|
+
Key rotation is the primary design goal. The keyset has one current key (used
|
|
108
|
+
for signing) and any number of previous keys (used only for verification).
|
|
109
|
+
|
|
110
|
+
1. Generate a new base64-encoded 256-bit key.
|
|
111
|
+
2. Move the old `key` into `previousKeys`.
|
|
112
|
+
3. Set the new key as `key`.
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
// Before rotation
|
|
116
|
+
Hash.Provider.register('default', Hash.Provider.create({
|
|
117
|
+
key: 'currentKeyBase64',
|
|
118
|
+
previousKeys: []
|
|
119
|
+
}))
|
|
120
|
+
|
|
121
|
+
// After rotation — old signatures still verify
|
|
122
|
+
Hash.Provider.register('default', Hash.Provider.create({
|
|
123
|
+
key: 'newKeyBase64',
|
|
124
|
+
previousKeys: ['currentKeyBase64']
|
|
125
|
+
}))
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
New calls to `signString` use the new key. Existing signed strings created with
|
|
129
|
+
the old key still pass `isSignatureValid` because the old key remains in the
|
|
130
|
+
keyset for verification.
|
|
131
|
+
|
|
132
|
+
### How Key IDs Work
|
|
133
|
+
|
|
134
|
+
Each key gets a 4-character ID computed as:
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
crypto.createHash('sha256').update(key).digest('base64').substring(0, 4)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Signed strings embed this ID before the dot: `keyId.hmacDigest`. During
|
|
141
|
+
verification, only keys matching the embedded ID are tested.
|
|
142
|
+
|
|
143
|
+
## Common Patterns
|
|
144
|
+
|
|
145
|
+
### Sign and Verify a Webhook Payload
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
const payload = JSON.stringify({ event: 'order.completed', orderId: 'ord_8k2Xp' })
|
|
149
|
+
const signature = Hash.signString(payload)
|
|
150
|
+
|
|
151
|
+
// Later, verify the signature
|
|
152
|
+
const valid = Hash.isSignatureValid({ plaintext: payload, signedText: signature })
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Multiple Named Instances
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
Hash.Provider.register('webhooks', Hash.Provider.create({
|
|
159
|
+
key: process.env.WEBHOOK_SIGNING_KEY!,
|
|
160
|
+
previousKeys: []
|
|
161
|
+
}))
|
|
162
|
+
|
|
163
|
+
Hash.Provider.register('tokens', Hash.Provider.create({
|
|
164
|
+
key: process.env.TOKEN_SIGNING_KEY!,
|
|
165
|
+
previousKeys: []
|
|
166
|
+
}))
|
|
167
|
+
|
|
168
|
+
const webhookSigned = Hash.provider('webhooks').signString(payload)
|
|
169
|
+
const tokenSigned = Hash.provider('tokens').signString(tokenData)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Extend `Hash.Provider.Keys` to get type-safe provider names:
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
declare module '@maestro-js/hash' {
|
|
176
|
+
namespace Hash.Provider {
|
|
177
|
+
interface Keys {
|
|
178
|
+
webhooks: unknown
|
|
179
|
+
tokens: unknown
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Generate a Key for Development
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
import crypto from 'node:crypto'
|
|
189
|
+
const key = crypto.randomBytes(32).toString('base64')
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Testing
|
|
193
|
+
|
|
194
|
+
Run tests:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
pnpm --filter @maestro-js/hash test
|
|
198
|
+
# or
|
|
199
|
+
cd packages/hash && npx beartest ./tests/hash.test.ts
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Tests use `beartest-js` with `expect`. Create service instances directly with
|
|
203
|
+
`Hash.Provider.create()` without registering — no global state needed.
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
import crypto from 'node:crypto'
|
|
207
|
+
import { Hash } from '@maestro-js/hash'
|
|
208
|
+
import { test } from 'beartest-js'
|
|
209
|
+
import { expect } from 'expect'
|
|
210
|
+
|
|
211
|
+
test('should sign and verify', () => {
|
|
212
|
+
const key = crypto.randomBytes(32).toString('base64')
|
|
213
|
+
const svc = Hash.Provider.create({ key, previousKeys: [] })
|
|
214
|
+
|
|
215
|
+
const signed = svc.signString('hello')
|
|
216
|
+
expect(svc.isSignatureValid({ plaintext: 'hello', signedText: signed })).toBe(true)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
test('key rotation preserves old signatures', () => {
|
|
220
|
+
const key1 = crypto.randomBytes(32).toString('base64')
|
|
221
|
+
const key2 = crypto.randomBytes(32).toString('base64')
|
|
222
|
+
|
|
223
|
+
const svc1 = Hash.Provider.create({ key: key1, previousKeys: [] })
|
|
224
|
+
const signed1 = svc1.signString('data')
|
|
225
|
+
|
|
226
|
+
const svc2 = Hash.Provider.create({ key: key2, previousKeys: [key1] })
|
|
227
|
+
expect(svc2.isSignatureValid({ plaintext: 'data', signedText: signed1 })).toBe(true)
|
|
228
|
+
})
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Cross-Package Integration
|
|
232
|
+
|
|
233
|
+
- **Depends on**: `@maestro-js/service-registry` for the Provider pattern
|
|
234
|
+
(registry, deferred proxy resolution, facade).
|
|
235
|
+
- **No downstream dependents**: standalone utility package.
|
|
236
|
+
- **Shares pattern with**: `@maestro-js/crypt` (same Provider + keyset
|
|
237
|
+
rotation architecture, but for encryption rather than signing).
|
|
238
|
+
|
|
239
|
+
## Package Structure
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
packages/hash/
|
|
243
|
+
src/index.ts # Single-file implementation and type declarations
|
|
244
|
+
tests/hash.test.ts # beartest-js tests
|
|
245
|
+
package.json # @maestro-js/hash
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Source file: `packages/hash/src/index.ts`
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: helpers
|
|
3
|
+
description: "Standalone utility functions for async control flow and file handling in the @maestro-js/helpers package. Use when working with @maestro-js/helpers. Key capabilities: retry with configurable attempts/delay/backoff, sleep, withMaxConcurrency for limiting parallel executions, withRateLimit with fixed or sliding window, raceWithFallback for promise racing with default values, AsyncIterableCollection for lazy async iteration (map, filter, reduce, forEach, toArray), sanitizeFilename for safe cross-platform filenames, and withRetry for wrapping functions with retry behavior."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @maestro-js/helpers
|
|
7
|
+
|
|
8
|
+
Standalone utility package providing async control flow primitives (retry, concurrency, rate limiting, promise racing) and helper functions (filename sanitization, async iterable collections). No dependencies on other maestro packages.
|
|
9
|
+
|
|
10
|
+
## Available Utilities
|
|
11
|
+
|
|
12
|
+
- **retry** - Retry an async operation with configurable attempts, delay, and conditional retry logic
|
|
13
|
+
- **withRetry** - Wrap a function to add retry behavior, returning a new function
|
|
14
|
+
- **sleep** - Pause execution for a given number of milliseconds
|
|
15
|
+
- **withMaxConcurrency** - Wrap a function to limit the number of concurrent executions
|
|
16
|
+
- **withRateLimit** - Wrap a function to enforce rate limits using fixed or sliding time windows
|
|
17
|
+
- **raceWithFallback** - Race a promise against another, returning a fallback value if the racer wins
|
|
18
|
+
- **AsyncIterableCollection** - Wrap an `AsyncIterable` with chainable collection methods (map, filter, reduce, forEach, toArray)
|
|
19
|
+
- **sanitizeFilename** - Strip illegal, control, and reserved characters from a filename string
|
|
20
|
+
|
|
21
|
+
## API Reference
|
|
22
|
+
|
|
23
|
+
### retry
|
|
24
|
+
|
|
25
|
+
Retry an async function with configurable options. Defaults: 3 attempts, 1000ms delay.
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
function retry<R = void>(
|
|
29
|
+
fn: () => Promise<R>,
|
|
30
|
+
options?: Partial<{
|
|
31
|
+
maxAttempts: number
|
|
32
|
+
delay: number | ((attempt: number) => number)
|
|
33
|
+
shouldRetry: (error: unknown) => boolean
|
|
34
|
+
}>
|
|
35
|
+
): Promise<R>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- `maxAttempts` - Maximum number of attempts (default: 3)
|
|
39
|
+
- `delay` - Fixed milliseconds or a function receiving the attempt number (default: 1000)
|
|
40
|
+
- `shouldRetry` - Predicate that receives the error; return `false` to stop retrying immediately
|
|
41
|
+
|
|
42
|
+
### withRetry
|
|
43
|
+
|
|
44
|
+
Return a new function that wraps the original with retry behavior. Accepts the same options as `retry`.
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
function withRetry<Args extends any[], R>(
|
|
48
|
+
callback: (...args: Args) => Promise<R>,
|
|
49
|
+
options?: Partial<Helpers.RetryOptions>
|
|
50
|
+
): (...args: Args) => Promise<R>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### sleep
|
|
54
|
+
|
|
55
|
+
Pause execution for the specified duration.
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
function sleep(ms: number): Promise<void>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### withMaxConcurrency
|
|
62
|
+
|
|
63
|
+
Return a new function that limits how many invocations run in parallel. Excess calls queue and wait.
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
function withMaxConcurrency<Args extends any[], R>(
|
|
67
|
+
callback: (...args: Args) => Promise<R>,
|
|
68
|
+
options?: { concurrency?: number }
|
|
69
|
+
): (...args: Args) => Promise<R>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
- `concurrency` - Maximum parallel executions (default: 10)
|
|
73
|
+
|
|
74
|
+
### withRateLimit
|
|
75
|
+
|
|
76
|
+
Return a new function that enforces a rate limit using either fixed or sliding time windows.
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
function withRateLimit<Args extends any[], R>(
|
|
80
|
+
callback: (...args: Args) => Promise<R>,
|
|
81
|
+
options?: {
|
|
82
|
+
windowType?: 'fixed' | 'sliding'
|
|
83
|
+
window: number
|
|
84
|
+
limit?: number
|
|
85
|
+
}
|
|
86
|
+
): (...args: Args) => Promise<R>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
- `windowType` - `'fixed'` (clock-aligned windows) or `'sliding'` (rolling window). Default: `'fixed'`
|
|
90
|
+
- `window` - Window size in milliseconds (default: 1000)
|
|
91
|
+
- `limit` - Maximum executions per window (default: 1)
|
|
92
|
+
|
|
93
|
+
### raceWithFallback
|
|
94
|
+
|
|
95
|
+
Race a primary promise against a competing promise. If the competing promise resolves first, return the fallback value instead.
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
function raceWithFallback<Result, Fallback = null>(
|
|
99
|
+
promise: Promise<Result>,
|
|
100
|
+
options: {
|
|
101
|
+
fallback?: Fallback
|
|
102
|
+
promiseToRace: Promise<unknown>
|
|
103
|
+
}
|
|
104
|
+
): Promise<{ value: Awaited<Result> | Fallback }>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Returns `{ value }` where value is the primary result or the fallback.
|
|
108
|
+
|
|
109
|
+
### AsyncIterableCollection
|
|
110
|
+
|
|
111
|
+
Wrap any `AsyncIterable<T>` with chainable collection methods. Create with `Helpers.AsyncIterableCollection.from(iterable)`.
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
interface AsyncIterableCollection<T> extends AsyncIterable<T> {
|
|
115
|
+
forEach(callbackfn: (item: T, index: number) => unknown): Promise<void>
|
|
116
|
+
map<S>(callbackfn: (item: T, index: number) => S): AsyncIterableCollection<S>
|
|
117
|
+
filter(predicate: (value: T, index: number) => unknown): AsyncIterableCollection<T>
|
|
118
|
+
reduce<U>(callbackfn: (prev: U, curr: T, index: number) => U, initialValue: U): Promise<U>
|
|
119
|
+
toArray(): Promise<T[]>
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
- `map` and `filter` return a new `AsyncIterableCollection` (lazy, chainable)
|
|
124
|
+
- `forEach`, `reduce`, and `toArray` are terminal operations that consume the iterable
|
|
125
|
+
|
|
126
|
+
### sanitizeFilename
|
|
127
|
+
|
|
128
|
+
Remove illegal, control, unicode whitespace, reserved, and Windows-reserved characters from a filename. Truncate to 255 bytes.
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
function sanitizeFilename(input: string): string
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Common Patterns
|
|
135
|
+
|
|
136
|
+
### Retry with exponential backoff
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import { Helpers } from '@maestro-js/helpers'
|
|
140
|
+
|
|
141
|
+
const result = await Helpers.retry(() => fetchFromApi(), {
|
|
142
|
+
maxAttempts: 5,
|
|
143
|
+
delay: (attempt) => 1000 * 2 ** (attempt - 1)
|
|
144
|
+
})
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Retry only on specific errors
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
await Helpers.retry(() => callExternalService(), {
|
|
151
|
+
maxAttempts: 3,
|
|
152
|
+
delay: 500,
|
|
153
|
+
shouldRetry: (error) => (error as any)?.code === 'TIMEOUT'
|
|
154
|
+
})
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Wrap a function with retry behavior
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
const resilientFetch = Helpers.withRetry(fetchFromApi, {
|
|
161
|
+
maxAttempts: 3,
|
|
162
|
+
delay: 1000
|
|
163
|
+
})
|
|
164
|
+
const result = await resilientFetch('/endpoint')
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Limit concurrent API calls
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
const limitedFetch = Helpers.withMaxConcurrency(fetch, { concurrency: 5 })
|
|
171
|
+
|
|
172
|
+
const results = await Promise.all(
|
|
173
|
+
urls.map((url) => limitedFetch(url))
|
|
174
|
+
)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Rate-limit outbound requests
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
const rateLimited = Helpers.withRateLimit(sendRequest, {
|
|
181
|
+
windowType: 'sliding',
|
|
182
|
+
window: 1000,
|
|
183
|
+
limit: 10
|
|
184
|
+
})
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Race a slow operation with a timeout
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
const { value } = await Helpers.raceWithFallback(
|
|
191
|
+
slowDatabaseQuery(),
|
|
192
|
+
{
|
|
193
|
+
fallback: null,
|
|
194
|
+
promiseToRace: Helpers.sleep(5000)
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
// value is the query result, or null if it took longer than 5 seconds
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Process async iterables with collection methods
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
const collection = Helpers.AsyncIterableCollection.from(asyncGenerator())
|
|
204
|
+
|
|
205
|
+
const results = await collection
|
|
206
|
+
.filter((item) => item.active)
|
|
207
|
+
.map((item) => item.name)
|
|
208
|
+
.toArray()
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Reduce an async iterable
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
const total = await Helpers.AsyncIterableCollection.from(rowStream())
|
|
215
|
+
.reduce((sum, row) => sum + row.amount, 0)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Sanitize a user-provided filename
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
const safe = Helpers.sanitizeFilename(userInput)
|
|
222
|
+
// Strips illegal chars, control chars, Windows reserved names, trailing dots/spaces
|
|
223
|
+
// Truncates to 255 bytes
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Testing
|
|
227
|
+
|
|
228
|
+
Run tests with beartest-js:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
pnpm --filter @maestro-js/helpers test
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Or run the test file directly:
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
cd packages/helpers && npx beartest ./tests/helpers.test.ts
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Tests use `beartest-js` with `expect` for assertions. Test file location: `packages/helpers/tests/helpers.test.ts`.
|
|
241
|
+
|
|
242
|
+
## Package Info
|
|
243
|
+
|
|
244
|
+
- **Package name**: `@maestro-js/helpers`
|
|
245
|
+
- **Source**: `packages/helpers/src/index.ts`
|
|
246
|
+
- **No dependencies** on other maestro packages
|
|
247
|
+
- **No downstream dependents**
|
|
248
|
+
- Build: `tsup` (ESM + .d.ts)
|
|
249
|
+
- Code style: Prettier (2-space indent, no semicolons, single quotes, 125 char width)
|