@spfn/core 0.2.0-beta.49 → 0.2.0-beta.50
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/LICENSE +1 -1
- package/README.md +181 -366
- package/dist/cache/index.js.map +1 -1
- package/dist/codegen/index.js.map +1 -1
- package/dist/config/index.js.map +1 -1
- package/dist/db/index.d.ts +7 -1
- package/dist/db/index.js +1 -1
- package/dist/db/index.js.map +1 -1
- package/dist/env/index.d.ts +1 -1
- package/dist/env/index.js.map +1 -1
- package/dist/env/loader.d.ts +2 -3
- package/dist/env/loader.js +0 -1
- package/dist/env/loader.js.map +1 -1
- package/dist/errors/index.js.map +1 -1
- package/dist/event/index.js.map +1 -1
- package/dist/event/sse/client.js.map +1 -1
- package/dist/event/sse/index.js.map +1 -1
- package/dist/event/ws/client.js.map +1 -1
- package/dist/event/ws/index.js.map +1 -1
- package/dist/job/index.js.map +1 -1
- package/dist/logger/index.js.map +1 -1
- package/dist/middleware/index.js.map +1 -1
- package/dist/nextjs/index.js.map +1 -1
- package/dist/nextjs/server.js.map +1 -1
- package/dist/route/index.js.map +1 -1
- package/dist/server/index.js +0 -1
- package/dist/server/index.js.map +1 -1
- package/package.json +3 -3
- package/docs/cache.md +0 -133
- package/docs/codegen.md +0 -74
- package/docs/database.md +0 -436
- package/docs/entity.md +0 -539
- package/docs/env.md +0 -499
- package/docs/errors.md +0 -319
- package/docs/event.md +0 -443
- package/docs/job.md +0 -131
- package/docs/logger.md +0 -108
- package/docs/middleware.md +0 -337
- package/docs/nextjs.md +0 -247
- package/docs/repository.md +0 -496
- package/docs/route.md +0 -497
- package/docs/server.md +0 -429
package/docs/event.md
DELETED
|
@@ -1,443 +0,0 @@
|
|
|
1
|
-
# Event
|
|
2
|
-
|
|
3
|
-
Decoupled pub/sub event system with SSE support for real-time frontend updates.
|
|
4
|
-
|
|
5
|
-
## Define Events
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// src/server/events/index.ts
|
|
9
|
-
import { defineEvent } from '@spfn/core/event';
|
|
10
|
-
import { Type } from '@sinclair/typebox';
|
|
11
|
-
|
|
12
|
-
// Event with typed payload
|
|
13
|
-
export const userCreated = defineEvent('user.created', Type.Object({
|
|
14
|
-
userId: Type.String(),
|
|
15
|
-
email: Type.String(),
|
|
16
|
-
}));
|
|
17
|
-
|
|
18
|
-
export const orderPlaced = defineEvent('order.placed', Type.Object({
|
|
19
|
-
orderId: Type.String(),
|
|
20
|
-
amount: Type.Number(),
|
|
21
|
-
}));
|
|
22
|
-
|
|
23
|
-
// Event without payload
|
|
24
|
-
export const serverStarted = defineEvent('server.started');
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
## Subscribe & Emit
|
|
28
|
-
|
|
29
|
-
```typescript
|
|
30
|
-
import { userCreated, serverStarted } from './events';
|
|
31
|
-
|
|
32
|
-
// Subscribe to event
|
|
33
|
-
const unsubscribe = userCreated.subscribe((payload) => {
|
|
34
|
-
console.log('User created:', payload.userId);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
// Emit event
|
|
38
|
-
await userCreated.emit({ userId: '123', email: 'user@example.com' });
|
|
39
|
-
|
|
40
|
-
// Emit event without payload
|
|
41
|
-
await serverStarted.emit();
|
|
42
|
-
|
|
43
|
-
// Unsubscribe when done
|
|
44
|
-
unsubscribe();
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
## Multiple Subscribers
|
|
48
|
-
|
|
49
|
-
```typescript
|
|
50
|
-
// Multiple independent handlers
|
|
51
|
-
userCreated.subscribe(async (payload) => {
|
|
52
|
-
await sendWelcomeEmail(payload.email);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
userCreated.subscribe(async (payload) => {
|
|
56
|
-
await createDefaultSettings(payload.userId);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
userCreated.subscribe(async (payload) => {
|
|
60
|
-
await notifyAdmins(payload.userId);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
// All handlers execute when event is emitted
|
|
64
|
-
await userCreated.emit({ userId: '123', email: 'user@example.com' });
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
## Event Router for SSE
|
|
68
|
-
|
|
69
|
-
```typescript
|
|
70
|
-
// src/server/events/router.ts
|
|
71
|
-
import { defineEventRouter } from '@spfn/core/event';
|
|
72
|
-
import { userCreated, orderPlaced } from './index';
|
|
73
|
-
|
|
74
|
-
export const eventRouter = defineEventRouter({
|
|
75
|
-
userCreated,
|
|
76
|
-
orderPlaced,
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
export type EventRouter = typeof eventRouter;
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
## Server Setup
|
|
83
|
-
|
|
84
|
-
```typescript
|
|
85
|
-
// server.config.ts
|
|
86
|
-
import { defineServerConfig } from '@spfn/core/server';
|
|
87
|
-
import { eventRouter } from './events/router';
|
|
88
|
-
|
|
89
|
-
export default defineServerConfig()
|
|
90
|
-
.routes(appRouter)
|
|
91
|
-
.jobs(jobRouter)
|
|
92
|
-
.events(eventRouter) // → GET /events/stream
|
|
93
|
-
.build();
|
|
94
|
-
|
|
95
|
-
// Custom path and options
|
|
96
|
-
.events(eventRouter, {
|
|
97
|
-
path: '/sse', // Custom endpoint path
|
|
98
|
-
pingInterval: 30000, // Keep-alive interval (default: 30s)
|
|
99
|
-
})
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
## SSE Authentication
|
|
103
|
-
|
|
104
|
-
Browser `EventSource` API does not support custom headers. SPFN uses a **Token Exchange** pattern:
|
|
105
|
-
|
|
106
|
-
1. Client sends `POST /events/token` with Bearer JWT
|
|
107
|
-
2. Server returns a one-time token (30s TTL)
|
|
108
|
-
3. Client connects to `GET /events/stream?token=...&events=...`
|
|
109
|
-
|
|
110
|
-
```typescript
|
|
111
|
-
// server.config.ts
|
|
112
|
-
import { defineServerConfig } from '@spfn/core/server';
|
|
113
|
-
import { authenticate } from '@spfn/auth/server';
|
|
114
|
-
|
|
115
|
-
export default defineServerConfig()
|
|
116
|
-
.middlewares([authenticate])
|
|
117
|
-
.routes(appRouter)
|
|
118
|
-
.events(eventRouter, {
|
|
119
|
-
auth: { enabled: true }, // This is all you need
|
|
120
|
-
})
|
|
121
|
-
.build();
|
|
122
|
-
// → POST /events/token (protected by authenticate middleware)
|
|
123
|
-
// → GET /events/stream?token=...&events=... (token verified)
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
### Authorization Hooks
|
|
127
|
-
|
|
128
|
-
#### `authorize` — Subscription Authorization (once on connect)
|
|
129
|
-
|
|
130
|
-
```typescript
|
|
131
|
-
.events(eventRouter, {
|
|
132
|
-
auth: {
|
|
133
|
-
enabled: true,
|
|
134
|
-
authorize: async (subject, events) =>
|
|
135
|
-
{
|
|
136
|
-
// events: ('userCreated' | 'orderPlaced')[] — type inferred
|
|
137
|
-
const user = await usersRepository.findById(subject);
|
|
138
|
-
if (user.role === 'admin') return events;
|
|
139
|
-
return events.filter(e => !e.startsWith('admin.'));
|
|
140
|
-
},
|
|
141
|
-
},
|
|
142
|
-
})
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
#### `filter` — Payload Filtering (on every event emission)
|
|
146
|
-
|
|
147
|
-
```typescript
|
|
148
|
-
.events(eventRouter, {
|
|
149
|
-
auth: {
|
|
150
|
-
enabled: true,
|
|
151
|
-
filter: {
|
|
152
|
-
// payload type inferred per-event — no casting needed
|
|
153
|
-
orderPlaced: (subject, payload) => payload.userId === subject,
|
|
154
|
-
// userCreated: no filter → sent to all authenticated users
|
|
155
|
-
},
|
|
156
|
-
},
|
|
157
|
-
})
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
## Browser Client
|
|
161
|
-
|
|
162
|
-
```typescript
|
|
163
|
-
import { createSSEClient } from '@spfn/core/event/sse/client';
|
|
164
|
-
import type { EventRouter } from '@/server/events/router';
|
|
165
|
-
|
|
166
|
-
// Create client (uses defaults: NEXT_PUBLIC_SPFN_API_URL + /events/stream)
|
|
167
|
-
const client = createSSEClient<EventRouter>();
|
|
168
|
-
|
|
169
|
-
// Or with custom configuration
|
|
170
|
-
const client = createSSEClient<EventRouter>({
|
|
171
|
-
host: 'https://api.example.com',
|
|
172
|
-
pathname: '/sse',
|
|
173
|
-
reconnect: true,
|
|
174
|
-
reconnectDelay: 3000,
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
// Subscribe to events
|
|
178
|
-
const unsubscribe = client.subscribe({
|
|
179
|
-
events: ['userCreated', 'orderPlaced'],
|
|
180
|
-
handlers: {
|
|
181
|
-
userCreated: (payload) => {
|
|
182
|
-
console.log('New user:', payload.userId);
|
|
183
|
-
},
|
|
184
|
-
orderPlaced: (payload) => {
|
|
185
|
-
console.log('New order:', payload.orderId);
|
|
186
|
-
},
|
|
187
|
-
},
|
|
188
|
-
onOpen: () => console.log('SSE connected'),
|
|
189
|
-
onError: (err) => console.error('SSE error:', err),
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
// Cleanup
|
|
193
|
-
unsubscribe();
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
### With Authentication
|
|
197
|
-
|
|
198
|
-
Use `createAuthSSEClient` which handles token acquisition automatically via the RPC proxy:
|
|
199
|
-
|
|
200
|
-
```typescript
|
|
201
|
-
import { createAuthSSEClient } from '@spfn/core/event/sse/client';
|
|
202
|
-
import type { EventRouter } from '@/server/events/router';
|
|
203
|
-
|
|
204
|
-
const client = createAuthSSEClient<EventRouter>();
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
This requires `eventRouteMap` to be merged into your RPC proxy (one-time setup):
|
|
208
|
-
|
|
209
|
-
```typescript
|
|
210
|
-
// app/api/rpc/[routeName]/route.ts
|
|
211
|
-
import '@spfn/auth/nextjs/api';
|
|
212
|
-
import { createRpcProxy } from '@spfn/core/nextjs/server';
|
|
213
|
-
import { eventRouteMap } from '@spfn/core/event';
|
|
214
|
-
import { routeMap } from '@/generated/route-map';
|
|
215
|
-
|
|
216
|
-
export const { GET, POST } = createRpcProxy({
|
|
217
|
-
routeMap: { ...routeMap, ...eventRouteMap },
|
|
218
|
-
});
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
Tokens are acquired on every (re)connect — one-time tokens are handled automatically.
|
|
222
|
-
|
|
223
|
-
## Simple Subscribe Helper
|
|
224
|
-
|
|
225
|
-
```typescript
|
|
226
|
-
import { subscribeToEvents } from '@spfn/core/event/sse/client';
|
|
227
|
-
import type { EventRouter } from '@/server/events/router';
|
|
228
|
-
|
|
229
|
-
// One-liner subscription
|
|
230
|
-
const unsubscribe = subscribeToEvents<EventRouter>(
|
|
231
|
-
['userCreated'],
|
|
232
|
-
{
|
|
233
|
-
userCreated: (payload) => console.log('User:', payload),
|
|
234
|
-
}
|
|
235
|
-
);
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
## Job Integration
|
|
239
|
-
|
|
240
|
-
```typescript
|
|
241
|
-
import { defineEvent } from '@spfn/core/event';
|
|
242
|
-
import { job, defineJobRouter } from '@spfn/core/job';
|
|
243
|
-
|
|
244
|
-
// Define event
|
|
245
|
-
export const orderPlaced = defineEvent('order.placed', Type.Object({
|
|
246
|
-
orderId: Type.String(),
|
|
247
|
-
userId: Type.String(),
|
|
248
|
-
}));
|
|
249
|
-
|
|
250
|
-
// Jobs subscribe to event
|
|
251
|
-
export const sendOrderConfirmation = job('send-order-confirmation')
|
|
252
|
-
.on(orderPlaced)
|
|
253
|
-
.handler(async (payload) => {
|
|
254
|
-
await emailService.sendOrderConfirmation(payload.orderId);
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
export const updateInventory = job('update-inventory')
|
|
258
|
-
.on(orderPlaced)
|
|
259
|
-
.handler(async (payload) => {
|
|
260
|
-
await inventoryService.reserve(payload.orderId);
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
// Register jobs
|
|
264
|
-
export const jobRouter = defineJobRouter({
|
|
265
|
-
sendOrderConfirmation,
|
|
266
|
-
updateInventory,
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
// Emit event - all subscribed jobs execute
|
|
270
|
-
await orderPlaced.emit({ orderId: 'ord-123', userId: 'user-456' });
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
## Multi-Instance Support
|
|
274
|
-
|
|
275
|
-
For applications running multiple instances, use cache-based pub/sub.
|
|
276
|
-
|
|
277
|
-
```typescript
|
|
278
|
-
import { defineEvent } from '@spfn/core/event';
|
|
279
|
-
import { getCache } from '@spfn/core/cache';
|
|
280
|
-
|
|
281
|
-
const userCreated = defineEvent('user.created', Type.Object({
|
|
282
|
-
userId: Type.String(),
|
|
283
|
-
}));
|
|
284
|
-
|
|
285
|
-
// Enable cache-based pub/sub
|
|
286
|
-
const cache = getCache();
|
|
287
|
-
if (cache)
|
|
288
|
-
{
|
|
289
|
-
await userCreated.useCache({
|
|
290
|
-
publish: async (channel, message) => {
|
|
291
|
-
await cache.publish(channel, JSON.stringify(message));
|
|
292
|
-
},
|
|
293
|
-
subscribe: async (channel, handler) => {
|
|
294
|
-
const subscriber = cache.duplicate();
|
|
295
|
-
await subscriber.subscribe(channel);
|
|
296
|
-
subscriber.on('message', (ch, msg) => {
|
|
297
|
-
if (ch === channel)
|
|
298
|
-
{
|
|
299
|
-
handler(JSON.parse(msg));
|
|
300
|
-
}
|
|
301
|
-
});
|
|
302
|
-
},
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// Events now broadcast to all instances
|
|
307
|
-
await userCreated.emit({ userId: '123' });
|
|
308
|
-
```
|
|
309
|
-
|
|
310
|
-
### SSE Token Store
|
|
311
|
-
|
|
312
|
-
SSE 토큰 저장소도 멀티 인스턴스에서 공유되어야 합니다.
|
|
313
|
-
캐시가 연결되면 `CacheTokenStore`가 **자동 사용**됩니다.
|
|
314
|
-
|
|
315
|
-
| 환경 | 토큰 저장소 | 설정 |
|
|
316
|
-
|------|------------|------|
|
|
317
|
-
| `CACHE_URL` 없음 | `InMemoryTokenStore` | 자동 |
|
|
318
|
-
| `CACHE_URL` 설정됨 | `CacheTokenStore` | 자동 감지 |
|
|
319
|
-
| 커스텀 | `SSETokenStore` 구현 | `auth.store` 옵션 |
|
|
320
|
-
|
|
321
|
-
## API Reference
|
|
322
|
-
|
|
323
|
-
### defineEvent(name)
|
|
324
|
-
|
|
325
|
-
Define an event without payload.
|
|
326
|
-
|
|
327
|
-
```typescript
|
|
328
|
-
export const serverStarted = defineEvent('server.started');
|
|
329
|
-
|
|
330
|
-
serverStarted.subscribe(() => { ... });
|
|
331
|
-
await serverStarted.emit();
|
|
332
|
-
```
|
|
333
|
-
|
|
334
|
-
### defineEvent(name, schema)
|
|
335
|
-
|
|
336
|
-
Define an event with typed payload.
|
|
337
|
-
|
|
338
|
-
```typescript
|
|
339
|
-
export const userCreated = defineEvent('user.created', Type.Object({
|
|
340
|
-
userId: Type.String(),
|
|
341
|
-
}));
|
|
342
|
-
|
|
343
|
-
userCreated.subscribe((payload) => { ... });
|
|
344
|
-
await userCreated.emit({ userId: '123' });
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
### EventDef Methods
|
|
348
|
-
|
|
349
|
-
| Method | Description |
|
|
350
|
-
|--------|-------------|
|
|
351
|
-
| `subscribe(handler)` | Subscribe to event. Returns unsubscribe function |
|
|
352
|
-
| `unsubscribeAll()` | Remove all subscribers |
|
|
353
|
-
| `emit(payload?)` | Emit event to all subscribers |
|
|
354
|
-
| `useCache(cache)` | Enable cache-based pub/sub for multi-instance |
|
|
355
|
-
|
|
356
|
-
### SSE Client Options
|
|
357
|
-
|
|
358
|
-
| Option | Type | Default | Description |
|
|
359
|
-
|--------|------|---------|-------------|
|
|
360
|
-
| `host` | string | `NEXT_PUBLIC_SPFN_API_URL` | Backend API host URL |
|
|
361
|
-
| `pathname` | string | `/events/stream` | SSE endpoint pathname |
|
|
362
|
-
| `reconnect` | boolean | `true` | Auto reconnect on disconnect |
|
|
363
|
-
| `reconnectDelay` | number | `3000` | Reconnect delay (ms) |
|
|
364
|
-
| `maxReconnectAttempts` | number | `0` | Max attempts (0 = infinite) |
|
|
365
|
-
| `withCredentials` | boolean | `false` | Include cookies |
|
|
366
|
-
| `acquireToken` | () => Promise\<string\> | - | Acquire one-time SSE token before connecting |
|
|
367
|
-
|
|
368
|
-
### SSE Auth Options
|
|
369
|
-
|
|
370
|
-
| Option | Type | Default | Description |
|
|
371
|
-
|--------|------|---------|-------------|
|
|
372
|
-
| `enabled` | boolean | `false` | Enable token authentication |
|
|
373
|
-
| `tokenTtl` | number | `30000` | Token TTL in milliseconds |
|
|
374
|
-
| `store` | SSETokenStore | InMemory | Custom token store (e.g., Redis) |
|
|
375
|
-
| `getSubject` | (c) => string \| null | `c.get('auth')?.userId` | Extract subject from context |
|
|
376
|
-
| `authorize` | (subject, events) => events[] | - | Subscription authorization hook |
|
|
377
|
-
| `filter` | { [event]: (subject, payload) => boolean } | - | Per-event payload filter |
|
|
378
|
-
|
|
379
|
-
## Event Flow Architecture
|
|
380
|
-
|
|
381
|
-
```
|
|
382
|
-
userCreated.emit({ ... })
|
|
383
|
-
│
|
|
384
|
-
┌───────────────────┼───────────────────┐
|
|
385
|
-
▼ ▼ ▼
|
|
386
|
-
┌──────────┐ ┌──────────┐ ┌──────────┐
|
|
387
|
-
│ Backend │ │ Job │ │ SSE │
|
|
388
|
-
│ Handler │ │ Queue │ │ Stream │
|
|
389
|
-
└──────────┘ └──────────┘ └──────────┘
|
|
390
|
-
.subscribe() .on(event) ↓
|
|
391
|
-
│ │ ┌──────────┐
|
|
392
|
-
▼ ▼ │ Browser │
|
|
393
|
-
[Logging, [Background │ Client │
|
|
394
|
-
Analytics] Processing] └──────────┘
|
|
395
|
-
```
|
|
396
|
-
|
|
397
|
-
## Best Practices
|
|
398
|
-
|
|
399
|
-
```typescript
|
|
400
|
-
// 1. Use descriptive event names
|
|
401
|
-
defineEvent('user.created')
|
|
402
|
-
defineEvent('order.completed')
|
|
403
|
-
defineEvent('payment.failed')
|
|
404
|
-
|
|
405
|
-
// 2. Keep payloads minimal - just IDs, not full objects
|
|
406
|
-
defineEvent('user.deleted', Type.Object({
|
|
407
|
-
userId: Type.String(),
|
|
408
|
-
}));
|
|
409
|
-
|
|
410
|
-
// 3. Handler errors are isolated - one failing handler doesn't affect others
|
|
411
|
-
userCreated.subscribe(async (payload) => {
|
|
412
|
-
throw new Error('This fails');
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
userCreated.subscribe(async (payload) => {
|
|
416
|
-
// This still executes
|
|
417
|
-
console.log('Handler 2 runs');
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
// 4. Use events for side effects, not core logic
|
|
421
|
-
// Core: await userRepo.create(data);
|
|
422
|
-
// Side effect: await userCreated.emit({ ... });
|
|
423
|
-
|
|
424
|
-
// 5. Await useCache() before emitting for multi-instance
|
|
425
|
-
await userCreated.useCache(cache);
|
|
426
|
-
await userCreated.emit({ userId: '123' });
|
|
427
|
-
```
|
|
428
|
-
|
|
429
|
-
## Event vs Direct Job
|
|
430
|
-
|
|
431
|
-
| Aspect | Event + Job | Direct Job |
|
|
432
|
-
|--------|-------------|------------|
|
|
433
|
-
| Coupling | Loose (producer doesn't know consumers) | Tight (producer calls specific job) |
|
|
434
|
-
| Multiple consumers | Easy (multiple jobs subscribe) | Manual (call each job) |
|
|
435
|
-
| Extensibility | Add consumers without modifying producer | Modify producer for each consumer |
|
|
436
|
-
|
|
437
|
-
**Use Event when:**
|
|
438
|
-
- Multiple systems need to react to the same occurrence
|
|
439
|
-
- You want to decouple producers from consumers
|
|
440
|
-
|
|
441
|
-
**Use Direct Job when:**
|
|
442
|
-
- Single, known consumer
|
|
443
|
-
- Simpler mental model preferred
|
package/docs/job.md
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
# Job
|
|
2
|
-
|
|
3
|
-
Background job processing.
|
|
4
|
-
|
|
5
|
-
## Define Jobs
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// src/server/jobs/send-email.job.ts
|
|
9
|
-
import { defineJob } from '@spfn/core/job';
|
|
10
|
-
|
|
11
|
-
export const sendEmailJob = defineJob<{
|
|
12
|
-
to: string;
|
|
13
|
-
subject: string;
|
|
14
|
-
body: string;
|
|
15
|
-
}>({
|
|
16
|
-
name: 'send-email',
|
|
17
|
-
handler: async (payload) => {
|
|
18
|
-
await emailService.send({
|
|
19
|
-
to: payload.to,
|
|
20
|
-
subject: payload.subject,
|
|
21
|
-
body: payload.body
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
## Enqueue Jobs
|
|
28
|
-
|
|
29
|
-
```typescript
|
|
30
|
-
import { enqueue } from '@spfn/core/job';
|
|
31
|
-
import { sendEmailJob } from './jobs/send-email.job';
|
|
32
|
-
|
|
33
|
-
// Enqueue for immediate processing
|
|
34
|
-
await enqueue(sendEmailJob, {
|
|
35
|
-
to: 'user@example.com',
|
|
36
|
-
subject: 'Welcome',
|
|
37
|
-
body: 'Welcome to our app!'
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
// Enqueue with delay
|
|
41
|
-
await enqueue(sendEmailJob, payload, {
|
|
42
|
-
delay: 60000 // 1 minute
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
// Enqueue with options
|
|
46
|
-
await enqueue(sendEmailJob, payload, {
|
|
47
|
-
priority: 'high',
|
|
48
|
-
attempts: 3,
|
|
49
|
-
backoff: 'exponential'
|
|
50
|
-
});
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
## Job Options
|
|
54
|
-
|
|
55
|
-
```typescript
|
|
56
|
-
defineJob({
|
|
57
|
-
name: 'process-image',
|
|
58
|
-
concurrency: 5, // Max concurrent jobs
|
|
59
|
-
attempts: 3, // Retry attempts
|
|
60
|
-
backoff: 'exponential', // Backoff strategy
|
|
61
|
-
timeout: 30000, // Job timeout (ms)
|
|
62
|
-
handler: async (payload) => {
|
|
63
|
-
// ...
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
## Scheduled Jobs
|
|
69
|
-
|
|
70
|
-
```typescript
|
|
71
|
-
import { schedule } from '@spfn/core/job';
|
|
72
|
-
|
|
73
|
-
// Run every hour
|
|
74
|
-
schedule('cleanup', '0 * * * *', async () => {
|
|
75
|
-
await cleanupExpiredSessions();
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
// Run daily at midnight
|
|
79
|
-
schedule('daily-report', '0 0 * * *', async () => {
|
|
80
|
-
await generateDailyReport();
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
// Run every 5 minutes
|
|
84
|
-
schedule('health-check', '*/5 * * * *', async () => {
|
|
85
|
-
await checkExternalServices();
|
|
86
|
-
});
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
## Job Registration
|
|
90
|
-
|
|
91
|
-
```typescript
|
|
92
|
-
// src/server/jobs/index.ts
|
|
93
|
-
import { registerJobs } from '@spfn/core/job';
|
|
94
|
-
import { sendEmailJob } from './send-email.job';
|
|
95
|
-
import { processImageJob } from './process-image.job';
|
|
96
|
-
|
|
97
|
-
export function initializeJobs()
|
|
98
|
-
{
|
|
99
|
-
registerJobs([
|
|
100
|
-
sendEmailJob,
|
|
101
|
-
processImageJob
|
|
102
|
-
]);
|
|
103
|
-
}
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
## Best Practices
|
|
107
|
-
|
|
108
|
-
```typescript
|
|
109
|
-
// 1. Keep jobs idempotent
|
|
110
|
-
handler: async (payload) => {
|
|
111
|
-
// Check if already processed
|
|
112
|
-
const existing = await db.findProcessed(payload.id);
|
|
113
|
-
if (existing) return;
|
|
114
|
-
|
|
115
|
-
await processItem(payload);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// 2. Use appropriate timeout
|
|
119
|
-
timeout: 30000 // Don't set too high
|
|
120
|
-
|
|
121
|
-
// 3. Handle failures gracefully
|
|
122
|
-
attempts: 3,
|
|
123
|
-
backoff: 'exponential'
|
|
124
|
-
|
|
125
|
-
// 4. Log job progress
|
|
126
|
-
handler: async (payload) => {
|
|
127
|
-
logger.info('Processing job', { jobId: payload.id });
|
|
128
|
-
// ...
|
|
129
|
-
logger.info('Job completed', { jobId: payload.id });
|
|
130
|
-
}
|
|
131
|
-
```
|
package/docs/logger.md
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
# Logger
|
|
2
|
-
|
|
3
|
-
Structured logging with context support.
|
|
4
|
-
|
|
5
|
-
## Basic Usage
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
import { logger } from '@spfn/core/logger';
|
|
9
|
-
|
|
10
|
-
logger.info('User created', { userId: '123' });
|
|
11
|
-
logger.warn('Rate limit approaching', { remaining: 10 });
|
|
12
|
-
logger.error('Failed to process', { error: err.message });
|
|
13
|
-
logger.debug('Processing request', { path: '/api/users' });
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
## Log Levels
|
|
17
|
-
|
|
18
|
-
| Level | Usage |
|
|
19
|
-
|-------|-------|
|
|
20
|
-
| `debug` | Development debugging |
|
|
21
|
-
| `info` | General information |
|
|
22
|
-
| `warn` | Warning conditions |
|
|
23
|
-
| `error` | Error conditions |
|
|
24
|
-
|
|
25
|
-
## Structured Logging
|
|
26
|
-
|
|
27
|
-
```typescript
|
|
28
|
-
// Good - structured data
|
|
29
|
-
logger.info('User login', {
|
|
30
|
-
userId: user.id,
|
|
31
|
-
email: user.email,
|
|
32
|
-
ip: request.ip
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
// Bad - string concatenation
|
|
36
|
-
logger.info(`User ${user.id} logged in from ${request.ip}`);
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
## Create Scoped Logger
|
|
40
|
-
|
|
41
|
-
```typescript
|
|
42
|
-
import { createLogger } from '@spfn/core/logger';
|
|
43
|
-
|
|
44
|
-
const userLogger = createLogger('user-service');
|
|
45
|
-
|
|
46
|
-
userLogger.info('Created user');
|
|
47
|
-
// Output: [user-service] Created user
|
|
48
|
-
|
|
49
|
-
const paymentLogger = createLogger('payment');
|
|
50
|
-
paymentLogger.error('Payment failed', { orderId: '123' });
|
|
51
|
-
// Output: [payment] Payment failed { orderId: '123' }
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
## Context Logging
|
|
55
|
-
|
|
56
|
-
```typescript
|
|
57
|
-
import { withLogContext } from '@spfn/core/logger';
|
|
58
|
-
|
|
59
|
-
// Add context to all logs in scope
|
|
60
|
-
await withLogContext({ requestId: '123', userId: 'abc' }, async () => {
|
|
61
|
-
logger.info('Processing request');
|
|
62
|
-
// Output includes: { requestId: '123', userId: 'abc' }
|
|
63
|
-
|
|
64
|
-
await doSomething();
|
|
65
|
-
logger.info('Request complete');
|
|
66
|
-
// Also includes context
|
|
67
|
-
});
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
## Log Format
|
|
71
|
-
|
|
72
|
-
Development (pretty):
|
|
73
|
-
```
|
|
74
|
-
2024-01-15 10:30:45 INFO User created { userId: '123', email: 'user@example.com' }
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
Production (JSON):
|
|
78
|
-
```json
|
|
79
|
-
{"timestamp":"2024-01-15T10:30:45.123Z","level":"info","message":"User created","userId":"123","email":"user@example.com"}
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
## Best Practices
|
|
83
|
-
|
|
84
|
-
```typescript
|
|
85
|
-
// 1. Use structured data
|
|
86
|
-
logger.info('Operation complete', { duration: 150, result: 'success' });
|
|
87
|
-
|
|
88
|
-
// 2. Include error details
|
|
89
|
-
logger.error('Request failed', {
|
|
90
|
-
error: err.message,
|
|
91
|
-
stack: err.stack,
|
|
92
|
-
path: req.path
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
// 3. Use appropriate levels
|
|
96
|
-
logger.debug(...) // Development only
|
|
97
|
-
logger.info(...) // Normal operations
|
|
98
|
-
logger.warn(...) // Potential issues
|
|
99
|
-
logger.error(...) // Errors requiring attention
|
|
100
|
-
|
|
101
|
-
// 4. Create scoped loggers for modules
|
|
102
|
-
const dbLogger = createLogger('database');
|
|
103
|
-
const authLogger = createLogger('auth');
|
|
104
|
-
|
|
105
|
-
// 5. Don't log sensitive data
|
|
106
|
-
logger.info('User login', { userId: '123' }); // Good
|
|
107
|
-
logger.info('User login', { password: '...' }); // Bad!
|
|
108
|
-
```
|