@spfn/core 0.2.0-beta.2 → 0.2.0-beta.21

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.
Files changed (64) hide show
  1. package/README.md +262 -1092
  2. package/dist/{boss-D-fGtVgM.d.ts → boss-DI1r4kTS.d.ts} +68 -11
  3. package/dist/codegen/index.d.ts +55 -8
  4. package/dist/codegen/index.js +179 -5
  5. package/dist/codegen/index.js.map +1 -1
  6. package/dist/config/index.d.ts +204 -6
  7. package/dist/config/index.js +44 -11
  8. package/dist/config/index.js.map +1 -1
  9. package/dist/db/index.d.ts +13 -0
  10. package/dist/db/index.js +92 -33
  11. package/dist/db/index.js.map +1 -1
  12. package/dist/env/index.d.ts +83 -3
  13. package/dist/env/index.js +83 -15
  14. package/dist/env/index.js.map +1 -1
  15. package/dist/env/loader.d.ts +95 -0
  16. package/dist/env/loader.js +78 -0
  17. package/dist/env/loader.js.map +1 -0
  18. package/dist/event/index.d.ts +29 -70
  19. package/dist/event/index.js +15 -1
  20. package/dist/event/index.js.map +1 -1
  21. package/dist/event/sse/client.d.ts +157 -0
  22. package/dist/event/sse/client.js +169 -0
  23. package/dist/event/sse/client.js.map +1 -0
  24. package/dist/event/sse/index.d.ts +46 -0
  25. package/dist/event/sse/index.js +205 -0
  26. package/dist/event/sse/index.js.map +1 -0
  27. package/dist/job/index.d.ts +54 -8
  28. package/dist/job/index.js +61 -12
  29. package/dist/job/index.js.map +1 -1
  30. package/dist/middleware/index.d.ts +124 -11
  31. package/dist/middleware/index.js +41 -7
  32. package/dist/middleware/index.js.map +1 -1
  33. package/dist/nextjs/index.d.ts +2 -2
  34. package/dist/nextjs/index.js +37 -5
  35. package/dist/nextjs/index.js.map +1 -1
  36. package/dist/nextjs/server.d.ts +45 -24
  37. package/dist/nextjs/server.js +87 -66
  38. package/dist/nextjs/server.js.map +1 -1
  39. package/dist/route/index.d.ts +207 -14
  40. package/dist/route/index.js +304 -31
  41. package/dist/route/index.js.map +1 -1
  42. package/dist/route/types.d.ts +2 -31
  43. package/dist/router-Di7ENoah.d.ts +151 -0
  44. package/dist/server/index.d.ts +321 -10
  45. package/dist/server/index.js +798 -189
  46. package/dist/server/index.js.map +1 -1
  47. package/dist/{types-DRG2XMTR.d.ts → types-7Mhoxnnt.d.ts} +97 -4
  48. package/dist/types-DHQMQlcb.d.ts +305 -0
  49. package/docs/cache.md +133 -0
  50. package/docs/codegen.md +74 -0
  51. package/docs/database.md +346 -0
  52. package/docs/entity.md +539 -0
  53. package/docs/env.md +499 -0
  54. package/docs/errors.md +319 -0
  55. package/docs/event.md +432 -0
  56. package/docs/file-upload.md +717 -0
  57. package/docs/job.md +131 -0
  58. package/docs/logger.md +108 -0
  59. package/docs/middleware.md +337 -0
  60. package/docs/nextjs.md +247 -0
  61. package/docs/repository.md +496 -0
  62. package/docs/route.md +497 -0
  63. package/docs/server.md +429 -0
  64. package/package.json +19 -3
package/docs/event.md ADDED
@@ -0,0 +1,432 @@
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
+ ## API Reference
311
+
312
+ ### defineEvent(name)
313
+
314
+ Define an event without payload.
315
+
316
+ ```typescript
317
+ export const serverStarted = defineEvent('server.started');
318
+
319
+ serverStarted.subscribe(() => { ... });
320
+ await serverStarted.emit();
321
+ ```
322
+
323
+ ### defineEvent(name, schema)
324
+
325
+ Define an event with typed payload.
326
+
327
+ ```typescript
328
+ export const userCreated = defineEvent('user.created', Type.Object({
329
+ userId: Type.String(),
330
+ }));
331
+
332
+ userCreated.subscribe((payload) => { ... });
333
+ await userCreated.emit({ userId: '123' });
334
+ ```
335
+
336
+ ### EventDef Methods
337
+
338
+ | Method | Description |
339
+ |--------|-------------|
340
+ | `subscribe(handler)` | Subscribe to event. Returns unsubscribe function |
341
+ | `unsubscribeAll()` | Remove all subscribers |
342
+ | `emit(payload?)` | Emit event to all subscribers |
343
+ | `useCache(cache)` | Enable cache-based pub/sub for multi-instance |
344
+
345
+ ### SSE Client Options
346
+
347
+ | Option | Type | Default | Description |
348
+ |--------|------|---------|-------------|
349
+ | `host` | string | `NEXT_PUBLIC_SPFN_API_URL` | Backend API host URL |
350
+ | `pathname` | string | `/events/stream` | SSE endpoint pathname |
351
+ | `reconnect` | boolean | `true` | Auto reconnect on disconnect |
352
+ | `reconnectDelay` | number | `3000` | Reconnect delay (ms) |
353
+ | `maxReconnectAttempts` | number | `0` | Max attempts (0 = infinite) |
354
+ | `withCredentials` | boolean | `false` | Include cookies |
355
+ | `acquireToken` | () => Promise\<string\> | - | Acquire one-time SSE token before connecting |
356
+
357
+ ### SSE Auth Options
358
+
359
+ | Option | Type | Default | Description |
360
+ |--------|------|---------|-------------|
361
+ | `enabled` | boolean | `false` | Enable token authentication |
362
+ | `tokenTtl` | number | `30000` | Token TTL in milliseconds |
363
+ | `store` | SSETokenStore | InMemory | Custom token store (e.g., Redis) |
364
+ | `getSubject` | (c) => string \| null | `c.get('auth')?.userId` | Extract subject from context |
365
+ | `authorize` | (subject, events) => events[] | - | Subscription authorization hook |
366
+ | `filter` | { [event]: (subject, payload) => boolean } | - | Per-event payload filter |
367
+
368
+ ## Event Flow Architecture
369
+
370
+ ```
371
+ userCreated.emit({ ... })
372
+
373
+ ┌───────────────────┼───────────────────┐
374
+ ▼ ▼ ▼
375
+ ┌──────────┐ ┌──────────┐ ┌──────────┐
376
+ │ Backend │ │ Job │ │ SSE │
377
+ │ Handler │ │ Queue │ │ Stream │
378
+ └──────────┘ └──────────┘ └──────────┘
379
+ .subscribe() .on(event) ↓
380
+ │ │ ┌──────────┐
381
+ ▼ ▼ │ Browser │
382
+ [Logging, [Background │ Client │
383
+ Analytics] Processing] └──────────┘
384
+ ```
385
+
386
+ ## Best Practices
387
+
388
+ ```typescript
389
+ // 1. Use descriptive event names
390
+ defineEvent('user.created')
391
+ defineEvent('order.completed')
392
+ defineEvent('payment.failed')
393
+
394
+ // 2. Keep payloads minimal - just IDs, not full objects
395
+ defineEvent('user.deleted', Type.Object({
396
+ userId: Type.String(),
397
+ }));
398
+
399
+ // 3. Handler errors are isolated - one failing handler doesn't affect others
400
+ userCreated.subscribe(async (payload) => {
401
+ throw new Error('This fails');
402
+ });
403
+
404
+ userCreated.subscribe(async (payload) => {
405
+ // This still executes
406
+ console.log('Handler 2 runs');
407
+ });
408
+
409
+ // 4. Use events for side effects, not core logic
410
+ // Core: await userRepo.create(data);
411
+ // Side effect: await userCreated.emit({ ... });
412
+
413
+ // 5. Await useCache() before emitting for multi-instance
414
+ await userCreated.useCache(cache);
415
+ await userCreated.emit({ userId: '123' });
416
+ ```
417
+
418
+ ## Event vs Direct Job
419
+
420
+ | Aspect | Event + Job | Direct Job |
421
+ |--------|-------------|------------|
422
+ | Coupling | Loose (producer doesn't know consumers) | Tight (producer calls specific job) |
423
+ | Multiple consumers | Easy (multiple jobs subscribe) | Manual (call each job) |
424
+ | Extensibility | Add consumers without modifying producer | Modify producer for each consumer |
425
+
426
+ **Use Event when:**
427
+ - Multiple systems need to react to the same occurrence
428
+ - You want to decouple producers from consumers
429
+
430
+ **Use Direct Job when:**
431
+ - Single, known consumer
432
+ - Simpler mental model preferred