@spfn/core 0.2.0-beta.20 → 0.2.0-beta.22

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.
@@ -22,6 +22,15 @@ import { E as EventRouterDef, I as InferEventNames, b as InferEventPayload } fro
22
22
  * manager.destroy();
23
23
  * ```
24
24
  */
25
+ /**
26
+ * Minimal cache client interface (compatible with ioredis Redis | Cluster)
27
+ */
28
+ type CacheClient = {
29
+ set(key: string, value: string, ...args: any[]): Promise<any>;
30
+ getdel?(key: string): Promise<string | null>;
31
+ get(key: string): Promise<string | null>;
32
+ del(key: string | string[]): Promise<number>;
33
+ };
25
34
  /**
26
35
  * Stored SSE token data
27
36
  */
@@ -62,6 +71,31 @@ interface SSETokenManagerConfig {
62
71
  */
63
72
  cleanupInterval?: number;
64
73
  }
74
+ /**
75
+ * Redis/Valkey-backed token store for multi-instance deployments.
76
+ *
77
+ * Uses SET EX for automatic TTL expiry and GETDEL for atomic one-time consumption.
78
+ * No cleanup needed — Redis handles expiration automatically.
79
+ *
80
+ * @example
81
+ * ```typescript
82
+ * import { getCache } from '@spfn/core/cache';
83
+ *
84
+ * const cache = getCache();
85
+ * if (cache) {
86
+ * const store = new CacheTokenStore(cache);
87
+ * const manager = new SSETokenManager({ store });
88
+ * }
89
+ * ```
90
+ */
91
+ declare class CacheTokenStore implements SSETokenStore {
92
+ private cache;
93
+ private prefix;
94
+ constructor(cache: CacheClient);
95
+ set(token: string, data: SSEToken): Promise<void>;
96
+ consume(token: string): Promise<SSEToken | null>;
97
+ cleanup(): Promise<void>;
98
+ }
65
99
  declare class SSETokenManager {
66
100
  private store;
67
101
  private ttl;
@@ -237,10 +271,17 @@ interface SSEClientConfig {
237
271
  * Called on every (re)connect. The returned token is appended
238
272
  * to the SSE URL as `?token=...`.
239
273
  *
274
+ * For automatic token acquisition via RPC proxy, use `createAuthSSEClient` instead.
275
+ *
240
276
  * @example
241
277
  * ```typescript
278
+ * // Recommended: use createAuthSSEClient for automatic token handling
279
+ * import { createAuthSSEClient } from '@spfn/core/event/sse/client';
280
+ * const client = createAuthSSEClient<EventRouter>();
281
+ *
282
+ * // Manual: provide acquireToken directly
242
283
  * acquireToken: async () => {
243
- * const res = await fetch('/api/events/token', {
284
+ * const res = await fetch('/api/rpc/eventsToken', {
244
285
  * method: 'POST',
245
286
  * credentials: 'include',
246
287
  * });
@@ -295,4 +336,4 @@ type SSEConnectionState = 'connecting' | 'open' | 'closed' | 'error';
295
336
  */
296
337
  type SSEUnsubscribe = () => void;
297
338
 
298
- export { type SSEHandlerConfig as S, type SSEAuthConfig as a, SSETokenManager as b, type SSEToken as c, type SSETokenStore as d, type SSETokenManagerConfig as e, type SSEMessage as f, type SSEHandlerAuthConfig as g, type SSEClientConfig as h, type SSEEventHandler as i, type SSEEventHandlers as j, type SSESubscribeOptions as k, type SSEConnectionState as l, type SSEUnsubscribe as m };
339
+ export { CacheTokenStore as C, type SSEHandlerConfig as S, type SSEAuthConfig as a, SSETokenManager as b, type SSEToken as c, type SSETokenStore as d, type SSETokenManagerConfig as e, type SSEMessage as f, type SSEHandlerAuthConfig as g, type SSEClientConfig as h, type SSEEventHandler as i, type SSEEventHandlers as j, type SSESubscribeOptions as k, type SSEConnectionState as l, type SSEUnsubscribe as m };
package/docs/event.md CHANGED
@@ -195,21 +195,30 @@ unsubscribe();
195
195
 
196
196
  ### With Authentication
197
197
 
198
+ Use `createAuthSSEClient` which handles token acquisition automatically via the RPC proxy:
199
+
198
200
  ```typescript
199
- const client = createSSEClient<EventRouter>({
200
- acquireToken: async () =>
201
- {
202
- const res = await fetch('/api/events/token', {
203
- method: 'POST',
204
- credentials: 'include',
205
- });
206
- const data = await res.json();
207
- return data.token;
208
- },
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 },
209
218
  });
210
219
  ```
211
220
 
212
- `acquireToken` is called on every (re)connect — one-time tokens are handled automatically.
221
+ Tokens are acquired on every (re)connect — one-time tokens are handled automatically.
213
222
 
214
223
  ## Simple Subscribe Helper
215
224
 
@@ -298,6 +307,17 @@ if (cache)
298
307
  await userCreated.emit({ userId: '123' });
299
308
  ```
300
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
+
301
321
  ## API Reference
302
322
 
303
323
  ### defineEvent(name)
package/docs/nextjs.md CHANGED
@@ -8,12 +8,14 @@ RPC proxy and type-safe API client for Next.js.
8
8
 
9
9
  ```typescript
10
10
  // app/api/rpc/[routeName]/route.ts
11
- import { appRouter } from '@/server/server.config';
11
+ import '@spfn/auth/nextjs/api';
12
12
  import { createRpcProxy } from '@spfn/core/nextjs/server';
13
+ import { authRouteMap } from '@spfn/auth';
14
+ import { eventRouteMap } from '@spfn/core/event';
15
+ import { routeMap } from '@/generated/route-map';
13
16
 
14
- export const { GET, POST, PUT, PATCH, DELETE } = createRpcProxy({
15
- router: appRouter,
16
- apiUrl: process.env.SPFN_API_URL || 'http://localhost:8790'
17
+ export const { GET, POST } = createRpcProxy({
18
+ routeMap: { ...routeMap, ...authRouteMap, ...eventRouteMap },
17
19
  });
18
20
  ```
19
21
 
@@ -135,7 +137,7 @@ const updated = await api.updateUser.call({
135
137
 
136
138
  ```typescript
137
139
  export const { GET, POST } = createRpcProxy({
138
- router: appRouter,
140
+ routeMap: { ...routeMap, ...authRouteMap },
139
141
  apiUrl: process.env.SPFN_API_URL,
140
142
  interceptors: {
141
143
  request: async (request, context) => {
@@ -208,6 +210,10 @@ SPFN_API_URL=http://localhost:8790
208
210
 
209
211
  # For production
210
212
  SPFN_API_URL=https://api.example.com
213
+
214
+ # RPC proxy timeout (AbortController, default: 120s)
215
+ # Should be shorter than FETCH_HEADERS_TIMEOUT for meaningful 504 responses
216
+ RPC_PROXY_TIMEOUT=120000
211
217
  ```
212
218
 
213
219
  ## Best Practices
package/docs/server.md CHANGED
@@ -246,12 +246,30 @@ if (shutdown.isShuttingDown())
246
246
 
247
247
  ```typescript
248
248
  defineServerConfig()
249
+ .timeout({
250
+ request: 120000, // SERVER_TIMEOUT (default: 120s)
251
+ keepAlive: 65000, // SERVER_KEEPALIVE_TIMEOUT (default: 65s)
252
+ headers: 60000, // SERVER_HEADERS_TIMEOUT (default: 60s)
253
+ })
254
+ .fetchTimeout({
255
+ connect: 10000, // FETCH_CONNECT_TIMEOUT (default: 10s)
256
+ headers: 300000, // FETCH_HEADERS_TIMEOUT (default: 300s)
257
+ body: 300000, // FETCH_BODY_TIMEOUT (default: 300s)
258
+ })
249
259
  .shutdown({
250
- timeout: 280000, // 280s (default: k8s 300s - 5s preStop - 15s margin)
260
+ timeout: 280000, // SHUTDOWN_TIMEOUT (default: 280s)
251
261
  })
252
262
  .build();
253
263
  ```
254
264
 
265
+ For long-running AI workloads, increase fetch timeouts:
266
+
267
+ ```bash
268
+ FETCH_HEADERS_TIMEOUT=600000 # 10 minutes
269
+ FETCH_BODY_TIMEOUT=600000 # 10 minutes
270
+ RPC_PROXY_TIMEOUT=580000 # Must be < FETCH_HEADERS_TIMEOUT
271
+ ```
272
+
255
273
  AI 파이프라인 등 장기 작업이 있는 앱은 Helm chart과 함께 조정:
256
274
 
257
275
  ```yaml
@@ -297,8 +315,42 @@ DATABASE_READ_URL=postgresql://replica:5432/mydb
297
315
  # Redis
298
316
  REDIS_URL=redis://localhost:6379
299
317
 
318
+ # Server Timeout (inbound HTTP server)
319
+ SERVER_TIMEOUT=120000 # Request timeout (default: 120s)
320
+ SERVER_KEEPALIVE_TIMEOUT=65000 # Keep-alive timeout (default: 65s)
321
+ SERVER_HEADERS_TIMEOUT=60000 # Headers timeout, Slowloris defense (default: 60s)
322
+
323
+ # Fetch Timeout (outbound HTTP via undici global dispatcher)
324
+ FETCH_CONNECT_TIMEOUT=10000 # TCP connection timeout (default: 10s)
325
+ FETCH_HEADERS_TIMEOUT=300000 # Response headers timeout (default: 300s)
326
+ FETCH_BODY_TIMEOUT=300000 # Body chunk interval timeout (default: 300s)
327
+
328
+ # RPC Proxy Timeout (Next.js → Backend)
329
+ RPC_PROXY_TIMEOUT=120000 # AbortController timeout (default: 120s)
330
+
300
331
  # Shutdown
301
- SHUTDOWN_TIMEOUT=280000 # Milliseconds (default: 280s)
332
+ SHUTDOWN_TIMEOUT=280000 # Graceful shutdown timeout (default: 280s)
333
+ ```
334
+
335
+ ### Timeout Architecture
336
+
337
+ ```
338
+ Request flow:
339
+ Client → Next.js API Route → fetch() → SPFN Backend
340
+
341
+ Timeout layers:
342
+ ┌──────────────────────────────────────────────────────┐
343
+ │ RPC_PROXY_TIMEOUT (AbortController) default 120s │ ← shortest, returns 504
344
+ │ FETCH_HEADERS_TIMEOUT (undici) default 300s │ ← fetch socket level
345
+ │ FETCH_BODY_TIMEOUT (undici) default 300s │ ← body chunk interval
346
+ │ FETCH_CONNECT_TIMEOUT (undici) default 10s │ ← TCP connection
347
+ ├──────────────────────────────────────────────────────┤
348
+ │ SERVER_TIMEOUT (backend server.timeout) default 120s │ ← backend HTTP server
349
+ │ SERVER_HEADERS_TIMEOUT default 60s │ ← Slowloris defense
350
+ │ SERVER_KEEPALIVE_TIMEOUT default 65s │ ← idle connection
351
+ └──────────────────────────────────────────────────────┘
352
+
353
+ Rule: RPC_PROXY_TIMEOUT < FETCH_HEADERS_TIMEOUT
302
354
  ```
303
355
 
304
356
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spfn/core",
3
- "version": "0.2.0-beta.20",
3
+ "version": "0.2.0-beta.22",
4
4
  "description": "SPFN Framework Core - File-based routing, transactions, repository pattern",
5
5
  "type": "module",
6
6
  "exports": {