@spfn/core 0.3.0-beta.2 → 0.3.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.
- package/README.md +343 -16
- package/dist/app-config/index.d.ts +134 -0
- package/dist/app-config/index.js +58 -0
- package/dist/app-config/index.js.map +1 -0
- package/dist/authz/index.js +1 -381
- package/dist/authz/index.js.map +1 -1
- package/dist/{boss-D16fO2oG.d.ts → boss-BiDCbbAQ.d.ts} +60 -7
- package/dist/codegen/index.d.ts +93 -15
- package/dist/codegen/index.js +626 -165
- package/dist/codegen/index.js.map +1 -1
- package/dist/config/index.d.ts +0 -63
- package/dist/config/index.js +15 -15
- package/dist/config/index.js.map +1 -1
- package/dist/contract/index.d.ts +40 -17
- package/dist/contract/index.js +147 -23
- package/dist/contract/index.js.map +1 -1
- package/dist/db/index.d.ts +201 -34
- package/dist/db/index.js +294 -158
- package/dist/db/index.js.map +1 -1
- package/dist/{define-middleware-DfDP39Nq.d.ts → define-middleware-CVKgqo8S.d.ts} +1 -1
- package/dist/env/index.d.ts +6 -442
- package/dist/env/index.js +13 -16
- package/dist/env/index.js.map +1 -1
- package/dist/env/loader.js +24 -1
- package/dist/env/loader.js.map +1 -1
- package/dist/error-TsUIjp_x.d.ts +157 -0
- package/dist/errors/index.js +1 -381
- package/dist/errors/index.js.map +1 -1
- package/dist/event/index.d.ts +5 -5
- package/dist/event/index.js +32 -1
- package/dist/event/index.js.map +1 -1
- package/dist/event/sse/client.d.ts +18 -4
- package/dist/event/sse/client.js +38 -2
- package/dist/event/sse/client.js.map +1 -1
- package/dist/event/sse/index.d.ts +5 -4
- package/dist/event/ws/client.d.ts +3 -2
- package/dist/event/ws/index.d.ts +4 -3
- package/dist/job/index.d.ts +48 -3
- package/dist/job/index.js +131 -52
- package/dist/job/index.js.map +1 -1
- package/dist/logger/index.js +0 -12
- package/dist/logger/index.js.map +1 -1
- package/dist/middleware/index.d.ts +6 -2
- package/dist/middleware/index.js +7 -388
- package/dist/middleware/index.js.map +1 -1
- package/dist/nextjs/index.d.ts +88 -8
- package/dist/nextjs/index.js +138 -62
- package/dist/nextjs/index.js.map +1 -1
- package/dist/nextjs/server.d.ts +45 -7
- package/dist/nextjs/server.js +14 -0
- package/dist/nextjs/server.js.map +1 -1
- package/dist/ops/index.d.ts +62 -43
- package/dist/ops/index.js +638 -97
- package/dist/ops/index.js.map +1 -1
- package/dist/registry-C2KQCh0m.d.ts +473 -0
- package/dist/route/index.d.ts +6 -3
- package/dist/route/index.js +97 -28
- package/dist/route/index.js.map +1 -1
- package/dist/route/types.d.ts +7 -1
- package/dist/{router-Qbssr11H.d.ts → route-builder-DRW-MSCJ.d.ts} +52 -127
- package/dist/router-4YoIy5fN.d.ts +172 -0
- package/dist/{token-manager-BT5EnUAR.d.ts → router-CysBmgv3.d.ts} +81 -127
- package/dist/server/index.d.ts +185 -14
- package/dist/server/index.js +883 -78
- package/dist/server/index.js.map +1 -1
- package/dist/surface/index.d.ts +115 -0
- package/dist/surface/index.js +1003 -0
- package/dist/surface/index.js.map +1 -0
- package/dist/token-manager-Ckjt2-qq.d.ts +122 -0
- package/dist/{types-D1c57Ko-.d.ts → types-5Rl11_Nb.d.ts} +43 -4
- package/dist/{types-ZQODsBft.d.ts → types-B8zAxFsE.d.ts} +2 -1
- package/dist/types-CxhPH4s_.d.ts +375 -0
- package/dist/{types-2AbaW4Ie.d.ts → types-W4u_S0lF.d.ts} +4 -3
- package/docs/file-upload.md +195 -333
- package/package.json +20 -9
- package/src/cache/README.md +330 -0
- package/src/codegen/README.md +634 -0
- package/src/config/README.md +333 -0
- package/src/contract/README.md +440 -0
- package/src/db/README.md +589 -0
- package/src/db/manager/README.md +500 -0
- package/src/db/schema/README.md +354 -0
- package/src/db/transaction/README.md +822 -0
- package/src/env/README.md +787 -0
- package/src/errors/README.md +429 -0
- package/src/event/README.md +789 -0
- package/src/job/README.md +613 -0
- package/src/logger/README.md +321 -0
- package/src/middleware/README.md +634 -0
- package/src/nextjs/README.md +688 -0
- package/src/route/README.md +763 -0
- package/src/security/README.md +100 -0
- package/src/server/README.md +704 -0
- package/dist/types-9oszaJqp.d.ts +0 -250
package/dist/event/sse/client.js
CHANGED
|
@@ -1,3 +1,35 @@
|
|
|
1
|
+
// src/nextjs/client/csrf.ts
|
|
2
|
+
var CSRF_HEADER = "x-spfn-csrf";
|
|
3
|
+
var CSRF_COOKIE_PATTERN = /^spfn_csrf(_\d+)?$/;
|
|
4
|
+
var HEADER_SAFE_VALUE = /^[!-~]+$/;
|
|
5
|
+
var MAX_CANDIDATES = 32;
|
|
6
|
+
function selectCandidates(values) {
|
|
7
|
+
const unique = Array.from(new Set(values));
|
|
8
|
+
if (unique.length <= MAX_CANDIDATES) {
|
|
9
|
+
return unique;
|
|
10
|
+
}
|
|
11
|
+
const half = MAX_CANDIDATES / 2;
|
|
12
|
+
return [...unique.slice(0, half), ...unique.slice(-half)];
|
|
13
|
+
}
|
|
14
|
+
function csrfHeaderValue(cookies) {
|
|
15
|
+
const values = Array.from(cookies).filter(([name, value]) => CSRF_COOKIE_PATTERN.test(name) && HEADER_SAFE_VALUE.test(value) && !value.includes(",")).map(([, value]) => value);
|
|
16
|
+
const candidates = selectCandidates(values);
|
|
17
|
+
return candidates.length > 0 ? candidates.join(",") : void 0;
|
|
18
|
+
}
|
|
19
|
+
function documentCookieEntries() {
|
|
20
|
+
if (typeof document === "undefined" || !document.cookie) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
const entries = [];
|
|
24
|
+
for (const pair of document.cookie.split(";")) {
|
|
25
|
+
const separator = pair.indexOf("=");
|
|
26
|
+
if (separator > 0) {
|
|
27
|
+
entries.push([pair.slice(0, separator).trim(), pair.slice(separator + 1).trim()]);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return entries;
|
|
31
|
+
}
|
|
32
|
+
|
|
1
33
|
// src/event/sse/client.ts
|
|
2
34
|
var SSE_DEFAULTS = {
|
|
3
35
|
host: typeof process !== "undefined" ? process.env.NEXT_PUBLIC_SPFN_API_URL || "http://localhost:8790" : "http://localhost:8790",
|
|
@@ -173,6 +205,10 @@ function subscribeToEvents(events, handlers, options) {
|
|
|
173
205
|
handlers
|
|
174
206
|
});
|
|
175
207
|
}
|
|
208
|
+
function tokenRequestHeaders(cookies = documentCookieEntries()) {
|
|
209
|
+
const csrf = csrfHeaderValue(cookies);
|
|
210
|
+
return csrf === void 0 ? { "Content-Type": "application/json" } : { "Content-Type": "application/json", [CSRF_HEADER]: csrf };
|
|
211
|
+
}
|
|
176
212
|
function createAuthSSEClient(config = {}) {
|
|
177
213
|
const { rpcBaseUrl = "/api/rpc", ...sseConfig } = config;
|
|
178
214
|
return createSSEClient({
|
|
@@ -181,7 +217,7 @@ function createAuthSSEClient(config = {}) {
|
|
|
181
217
|
const res = await fetch(`${rpcBaseUrl}/eventsToken`, {
|
|
182
218
|
method: "POST",
|
|
183
219
|
credentials: "include",
|
|
184
|
-
headers:
|
|
220
|
+
headers: tokenRequestHeaders(),
|
|
185
221
|
body: JSON.stringify({})
|
|
186
222
|
});
|
|
187
223
|
if (!res.ok) {
|
|
@@ -193,6 +229,6 @@ function createAuthSSEClient(config = {}) {
|
|
|
193
229
|
});
|
|
194
230
|
}
|
|
195
231
|
|
|
196
|
-
export { createAuthSSEClient, createSSEClient, subscribeToEvents };
|
|
232
|
+
export { createAuthSSEClient, createSSEClient, subscribeToEvents, tokenRequestHeaders };
|
|
197
233
|
//# sourceMappingURL=client.js.map
|
|
198
234
|
//# sourceMappingURL=client.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/event/sse/client.ts"],"names":[],"mappings":"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","file":"client.js","sourcesContent":["/**\n * SSE Client\n *\n * Type-safe EventSource wrapper for event subscription\n *\n * @example\n * ```typescript\n * import { createSSEClient } from '@spfn/core/event/sse/client';\n * import type { EventRouter } from '@/server/events';\n *\n * // Uses defaults: NEXT_PUBLIC_SPFN_API_URL + /events/stream\n * const client = createSSEClient<EventRouter>();\n *\n * // Or with custom host/pathname\n * const client = createSSEClient<EventRouter>({\n * host: 'https://api.example.com',\n * pathname: '/sse',\n * });\n *\n * // With token authentication (recommended: use createAuthSSEClient)\n * import { createAuthSSEClient } from '@spfn/core/event/sse/client';\n * const client = createAuthSSEClient<EventRouter>();\n *\n * const unsubscribe = client.subscribe({\n * events: ['userCreated', 'orderPlaced'],\n * handlers: {\n * userCreated: (payload) => console.log('User:', payload.userId),\n * orderPlaced: (payload) => console.log('Order:', payload.orderId),\n * },\n * });\n *\n * // Later: cleanup\n * unsubscribe();\n * ```\n */\n\nimport type { EventRouterDef, InferEventNames } from '../router';\nimport type {\n SSEClientConfig,\n SSESubscribeOptions,\n SSEUnsubscribe,\n SSEConnectionState,\n SSEMessage,\n} from './types';\n\n/**\n * SSE Client instance\n */\nexport interface SSEClient<TRouter extends EventRouterDef<any>>\n{\n /**\n * Subscribe to events\n */\n subscribe(options: SSESubscribeOptions<TRouter>): SSEUnsubscribe;\n\n /**\n * Get current connection state\n */\n getState(): SSEConnectionState;\n\n /**\n * Close all connections\n */\n close(): void;\n}\n\n/**\n * Default SSE configuration\n */\nconst SSE_DEFAULTS = {\n host: typeof process !== 'undefined'\n ? (process.env.NEXT_PUBLIC_SPFN_API_URL || 'http://localhost:8790')\n : 'http://localhost:8790',\n pathname: '/events/stream',\n} as const;\n\n/**\n * Create type-safe SSE client\n *\n * @example\n * ```typescript\n * // Uses defaults (NEXT_PUBLIC_SPFN_API_URL + /events/stream)\n * const client = createSSEClient<EventRouter>();\n *\n * // Or with custom configuration\n * const client = createSSEClient<EventRouter>({\n * host: 'https://api.example.com',\n * pathname: '/sse',\n * reconnect: true,\n * reconnectDelay: 3000,\n * });\n *\n * // Subscribe to events\n * const unsubscribe = client.subscribe({\n * events: ['userCreated', 'orderPlaced'],\n * handlers: {\n * userCreated: (payload) => {\n * console.log('New user:', payload.userId);\n * },\n * orderPlaced: (payload) => {\n * console.log('New order:', payload.orderId);\n * },\n * },\n * onOpen: () => console.log('Connected'),\n * onError: (err) => console.error('Error:', err),\n * onReconnect: (attempt) => console.log('Reconnecting...', attempt),\n * });\n *\n * // Cleanup\n * unsubscribe();\n * ```\n */\n/**\n * Per-subscription connection resources.\n *\n * Grouped so an in-flight async connect can be cancelled atomically and\n * `close()` can tear down whichever subscription is currently active —\n * the shared module-level EventSource used to leak across the token await.\n */\ninterface SSEConnection\n{\n eventSource: EventSource | null;\n reconnectTimer: ReturnType<typeof setTimeout> | null;\n reconnectAttempts: number;\n closed: boolean;\n onClose?: () => void;\n}\n\nexport function createSSEClient<TRouter extends EventRouterDef<any>>(\n config: SSEClientConfig = {},\n): SSEClient<TRouter>\n{\n const {\n url,\n host = SSE_DEFAULTS.host,\n pathname = SSE_DEFAULTS.pathname,\n reconnect = true,\n reconnectDelay = 3000,\n maxReconnectAttempts = 0,\n withCredentials = false,\n acquireToken,\n } = config;\n\n // Build base URL: url takes precedence, otherwise host + pathname\n const baseUrl = url || `${host}${pathname}`;\n\n let state: SSEConnectionState = 'closed';\n let active: SSEConnection | null = null;\n\n // Idempotent teardown: closes the connection, fires onClose exactly once,\n // and clears the active slot if this was the live connection.\n function closeConn(conn: SSEConnection)\n {\n if (conn.closed)\n {\n return;\n }\n\n conn.closed = true;\n\n if (conn.reconnectTimer)\n {\n clearTimeout(conn.reconnectTimer);\n conn.reconnectTimer = null;\n }\n\n if (conn.eventSource)\n {\n conn.eventSource.close();\n conn.eventSource = null;\n }\n\n if (active === conn)\n {\n active = null;\n state = 'closed';\n }\n\n conn.onClose?.();\n }\n\n function subscribe(options: SSESubscribeOptions<TRouter>): SSEUnsubscribe\n {\n const { events, handlers, onOpen, onError, onReconnect, onClose } = options;\n\n // A new subscription supersedes any previous one on this client.\n if (active)\n {\n closeConn(active);\n }\n\n const conn: SSEConnection = {\n eventSource: null,\n reconnectTimer: null,\n reconnectAttempts: 0,\n closed: false,\n onClose,\n };\n active = conn;\n\n const eventNames = events as string[];\n\n function connect()\n {\n state = 'connecting';\n\n const init = async () =>\n {\n let tokenParam = '';\n\n if (acquireToken)\n {\n const token = await acquireToken();\n\n // Cancelled during the token await (e.g. StrictMode cleanup) —\n // don't open a connection that has no teardown waiting for it.\n if (conn.closed)\n {\n return;\n }\n\n tokenParam = `&token=${encodeURIComponent(token)}`;\n }\n\n if (conn.closed)\n {\n return;\n }\n\n const streamUrl = `${baseUrl}?events=${eventNames.join(',')}${tokenParam}`;\n\n conn.eventSource = new EventSource(streamUrl, {\n withCredentials,\n });\n\n setupEventHandlers(conn.eventSource, eventNames, handlers, onOpen, onError);\n setupReconnect(onReconnect);\n };\n\n init().catch(() =>\n {\n // Don't resurrect a torn-down subscription via reconnect.\n if (conn.closed)\n {\n return;\n }\n\n state = 'error';\n attemptReconnect(onReconnect);\n });\n }\n\n function setupEventHandlers(\n es: EventSource,\n names: string[],\n handlerMap: SSESubscribeOptions<TRouter>['handlers'],\n onOpenCb?: () => void,\n onErrorCb?: (error: Event) => void,\n )\n {\n es.onopen = () =>\n {\n state = 'open';\n conn.reconnectAttempts = 0;\n onOpenCb?.();\n };\n\n es.onerror = (error) =>\n {\n state = 'error';\n onErrorCb?.(error);\n };\n\n // Handle connected event (server sends this on connection)\n es.addEventListener('connected', (e: MessageEvent) =>\n {\n try\n {\n const data = JSON.parse(e.data);\n console.debug('[SSE] Connected:', data);\n }\n catch\n {\n // Ignore parse errors\n }\n });\n\n // Handle ping (keep-alive)\n es.addEventListener('ping', () =>\n {\n // Ping received, connection is alive\n });\n\n // Register handlers for each event\n for (const eventName of names)\n {\n const handler = (handlerMap as Record<string, ((payload: unknown) => void) | undefined>)[eventName];\n\n if (!handler)\n {\n continue;\n }\n\n es.addEventListener(eventName, (e: MessageEvent) =>\n {\n try\n {\n const message: SSEMessage = JSON.parse(e.data);\n handler(message.data);\n }\n catch (err)\n {\n console.error(`[SSE] Failed to parse event \"${eventName}\":`, err);\n }\n });\n }\n }\n\n function setupReconnect(onReconnectCb?: (attempt: number) => void)\n {\n if (!conn.eventSource)\n {\n return;\n }\n\n const currentEs = conn.eventSource;\n const originalOnError = currentEs.onerror;\n\n currentEs.onerror = (error) =>\n {\n if (originalOnError)\n {\n (originalOnError as (ev: Event) => void)(error);\n }\n\n // Token-auth 사용 시 브라우저 auto-retry는 소비된 토큰으로 재시도하므로\n // 즉시 close하고 우리 reconnect로 새 토큰 발급받아 재연결\n if (reconnect && acquireToken)\n {\n currentEs.close();\n attemptReconnect(onReconnectCb);\n }\n else if (reconnect && currentEs.readyState === EventSource.CLOSED)\n {\n attemptReconnect(onReconnectCb);\n }\n };\n }\n\n function attemptReconnect(onReconnectCb?: (attempt: number) => void)\n {\n if (conn.closed || !reconnect)\n {\n return;\n }\n\n if (maxReconnectAttempts > 0 && conn.reconnectAttempts >= maxReconnectAttempts)\n {\n closeConn(conn);\n\n return;\n }\n\n conn.reconnectAttempts++;\n onReconnectCb?.(conn.reconnectAttempts);\n\n conn.reconnectTimer = setTimeout(() =>\n {\n connect();\n }, reconnectDelay);\n }\n\n // Start connection\n connect();\n\n // Return unsubscribe function\n return () =>\n {\n closeConn(conn);\n };\n }\n\n function getState(): SSEConnectionState\n {\n return state;\n }\n\n function close()\n {\n if (active)\n {\n closeConn(active);\n }\n else\n {\n state = 'closed';\n }\n }\n\n return {\n subscribe,\n getState,\n close,\n };\n}\n\n/**\n * Simple subscribe function for one-off subscriptions\n *\n * @example\n * ```typescript\n * import { subscribeToEvents } from '@spfn/core/event/sse/client';\n * import type { EventRouter } from '@/server/events';\n *\n * // Using defaults\n * const unsubscribe = subscribeToEvents<EventRouter>(\n * ['userCreated', 'orderPlaced'],\n * {\n * userCreated: (payload) => console.log('User:', payload),\n * orderPlaced: (payload) => console.log('Order:', payload),\n * }\n * );\n *\n * // With custom host\n * const unsubscribe = subscribeToEvents<EventRouter>(\n * ['userCreated'],\n * { userCreated: (payload) => console.log('User:', payload) },\n * { host: 'https://api.example.com' }\n * );\n * ```\n */\nexport function subscribeToEvents<TRouter extends EventRouterDef<any>>(\n events: InferEventNames<TRouter>[],\n handlers: SSESubscribeOptions<TRouter>['handlers'],\n options?: SSEClientConfig,\n): SSEUnsubscribe\n{\n const client = createSSEClient<TRouter>(options);\n\n return client.subscribe({\n events,\n handlers,\n });\n}\n\n// ============================================================================\n// Auth SSE Client\n// ============================================================================\n\n/**\n * SSE client configuration for authenticated connections\n *\n * Same as SSEClientConfig but without acquireToken (auto-configured).\n */\nexport interface AuthSSEClientConfig extends Omit<SSEClientConfig, 'acquireToken'>\n{\n /**\n * RPC proxy base URL for token acquisition\n * @default '/api/rpc'\n */\n rpcBaseUrl?: string;\n}\n\n/**\n * Create SSE client with built-in token authentication\n *\n * Acquires one-time SSE tokens via RPC proxy automatically.\n * Requires eventRouteMap to be merged into RPC proxy config.\n *\n * @example\n * ```typescript\n * import { createAuthSSEClient } from '@spfn/core/event/sse/client';\n * import type { EventRouter } from '@/server/events';\n *\n * const client = createAuthSSEClient<EventRouter>();\n *\n * client.subscribe({\n * events: ['userCreated'],\n * handlers: {\n * userCreated: (payload) => console.log(payload),\n * },\n * });\n * ```\n */\nexport function createAuthSSEClient<TRouter extends EventRouterDef<any>>(\n config: AuthSSEClientConfig = {},\n): SSEClient<TRouter>\n{\n const { rpcBaseUrl = '/api/rpc', ...sseConfig } = config;\n\n return createSSEClient<TRouter>({\n ...sseConfig,\n acquireToken: async () =>\n {\n const res = await fetch(`${rpcBaseUrl}/eventsToken`, {\n method: 'POST',\n credentials: 'include',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({}),\n });\n\n if (!res.ok)\n {\n throw new Error(`Failed to acquire SSE token: ${res.status}`);\n }\n\n const data = await res.json();\n\n return data.token;\n },\n });\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/nextjs/client/csrf.ts","../../../src/event/sse/client.ts"],"names":[],"mappings":"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","file":"client.js","sourcesContent":["/**\n * CSRF token transport for the RPC client\n *\n * The Next.js proxy sets a readable (non-HttpOnly) cookie carrying an HMAC of\n * the session's key id; this module mirrors it into a request header. The proxy\n * never compares cookie against header — it recomputes the value from the\n * session — so the cookie is transport only and reading it here is not a trust\n * decision. See the CSRF section of the @spfn/auth README.\n */\n\n/**\n * Header the readable CSRF cookie is mirrored into.\n *\n * Must match CSRF_HEADER in @spfn/auth. It is duplicated rather than imported\n * because @spfn/core cannot depend on @spfn/auth.\n */\nexport const CSRF_HEADER = 'x-spfn-csrf';\n\n/**\n * Cookie names carrying a CSRF token.\n *\n * @spfn/auth suffixes its cookie names with the server port so several local dev\n * instances on the same host do not overwrite each other's session, and the\n * browser cannot know that port — so match the family, not one name. Every\n * candidate is sent and the proxy accepts the request if any of them recomputes\n * to the expected value; a candidate an attacker tossed in never will.\n */\nconst CSRF_COOKIE_PATTERN = /^spfn_csrf(_\\d+)?$/;\n\n/**\n * Values that can be put in a header at all: printable ASCII, no space.\n *\n * A cookie jar is attacker-writable in the very case this feature exists for, and\n * `new Headers()` rejects a value outside Latin-1 — so a sibling subdomain that\n * tosses `spfn_csrf=한` would make every RPC call throw before it was sent, not\n * merely fail the check. A genuine token is 64 hex characters; anything that\n * cannot be a header value cannot be one.\n */\nconst HEADER_SAFE_VALUE = /^[!-~]+$/;\n\n/**\n * Upper bound on candidates sent, so a cookie-flooded jar stays a bounded header.\n *\n * Deliberately generous. The server recomputes the expected value once and then\n * only compares fixed-length strings, so extra candidates cost it almost nothing,\n * while every candidate dropped here is a chance to lock the user out of every\n * mutation — the far worse failure. Must not exceed the matching bound in\n * @spfn/auth's `matchesCsrfToken`, or values sent would never be looked at.\n */\nconst MAX_CANDIDATES = 32;\n\n/**\n * Choose which candidates to send when the jar holds more than the cap.\n *\n * `document.cookie` exposes neither Domain nor Path, so a tossed cookie is\n * indistinguishable from the genuine one here — the proxy is what tells them\n * apart, by recomputing. What this can do is refuse to drop the genuine value on\n * the floor. Browsers order the jar by path length descending, then by age\n * ascending, so a flood pushes the genuine cookie towards the end (attacker\n * cookies on a longer path) or towards the start (attacker cookies written later\n * on the same path). Keeping both ends survives either, where a plain prefix\n * survives only one.\n *\n * A jar flooded far past the cap is a denial of service that no client-side\n * choice fully prevents; the structural fix is a `__Host-` prefixed cookie name,\n * which a sibling subdomain cannot write at all.\n */\nfunction selectCandidates(values: string[]): string[]\n{\n // Identical duplicates carry no extra information, so collapsing them buys\n // back slots that a flood of one repeated value would otherwise burn.\n const unique = Array.from(new Set(values));\n\n if (unique.length <= MAX_CANDIDATES)\n {\n return unique;\n }\n\n const half = MAX_CANDIDATES / 2;\n\n return [...unique.slice(0, half), ...unique.slice(-half)];\n}\n\n/**\n * Collect the CSRF token candidates from cookie entries.\n *\n * Takes entries rather than a map because a name can legitimately appear twice:\n * a sibling subdomain that tosses `spfn_csrf` for the parent domain leaves the\n * browser holding two cookies of that name, and folding them into a map would\n * throw away whichever the tossed one displaced — locking the user out of every\n * mutation. Sending both keeps the real one in play, and the tossed one is inert\n * because the proxy recomputes what it expects.\n *\n * @returns Header value (comma-separated), or undefined when no cookie exists\n */\nexport function csrfHeaderValue(cookies: Iterable<[string, string]>): string | undefined\n{\n // A comma separates candidates on the wire, so a value containing one would\n // arrive as several. That cannot be a genuine token — they are hex — but it\n // would let a single tossed cookie spend the receiver's whole candidate\n // budget and bury the real value behind it. Drop those, and anything that\n // could not be a header value at all.\n const values = Array.from(cookies)\n .filter(([name, value]) => CSRF_COOKIE_PATTERN.test(name)\n && HEADER_SAFE_VALUE.test(value)\n && !value.includes(','))\n .map(([, value]) => value);\n\n const candidates = selectCandidates(values);\n\n return candidates.length > 0 ? candidates.join(',') : undefined;\n}\n\n/**\n * Read the browser cookie jar as entries, duplicates preserved.\n *\n * Empty outside a browser (SSR reads the jar it forwards instead).\n */\nexport function documentCookieEntries(): Array<[string, string]>\n{\n if (typeof document === 'undefined' || !document.cookie)\n {\n return [];\n }\n\n const entries: Array<[string, string]> = [];\n\n for (const pair of document.cookie.split(';'))\n {\n const separator = pair.indexOf('=');\n\n if (separator > 0)\n {\n entries.push([pair.slice(0, separator).trim(), pair.slice(separator + 1).trim()]);\n }\n }\n\n return entries;\n}\n","/**\n * SSE Client\n *\n * Type-safe EventSource wrapper for event subscription\n *\n * @example\n * ```typescript\n * import { createSSEClient } from '@spfn/core/event/sse/client';\n * import type { EventRouter } from '@/server/events';\n *\n * // Uses defaults: NEXT_PUBLIC_SPFN_API_URL + /events/stream\n * const client = createSSEClient<EventRouter>();\n *\n * // Or with custom host/pathname\n * const client = createSSEClient<EventRouter>({\n * host: 'https://api.example.com',\n * pathname: '/sse',\n * });\n *\n * // With token authentication (recommended: use createAuthSSEClient)\n * import { createAuthSSEClient } from '@spfn/core/event/sse/client';\n * const client = createAuthSSEClient<EventRouter>();\n *\n * const unsubscribe = client.subscribe({\n * events: ['userCreated', 'orderPlaced'],\n * handlers: {\n * userCreated: (payload) => console.log('User:', payload.userId),\n * orderPlaced: (payload) => console.log('Order:', payload.orderId),\n * },\n * });\n *\n * // Later: cleanup\n * unsubscribe();\n * ```\n */\n\nimport type { EventRouterDef, InferEventNames } from '../router';\nimport { CSRF_HEADER, csrfHeaderValue, documentCookieEntries } from '../../nextjs/client/csrf';\nimport type {\n SSEClientConfig,\n SSESubscribeOptions,\n SSEUnsubscribe,\n SSEConnectionState,\n SSEMessage,\n} from './types';\n\n/**\n * SSE Client instance\n */\nexport interface SSEClient<TRouter extends EventRouterDef<any>>\n{\n /**\n * Subscribe to events\n */\n subscribe(options: SSESubscribeOptions<TRouter>): SSEUnsubscribe;\n\n /**\n * Get current connection state\n */\n getState(): SSEConnectionState;\n\n /**\n * Close all connections\n */\n close(): void;\n}\n\n/**\n * Default SSE configuration\n */\nconst SSE_DEFAULTS = {\n host: typeof process !== 'undefined'\n ? (process.env.NEXT_PUBLIC_SPFN_API_URL || 'http://localhost:8790')\n : 'http://localhost:8790',\n pathname: '/events/stream',\n} as const;\n\n/**\n * Create type-safe SSE client\n *\n * @example\n * ```typescript\n * // Uses defaults (NEXT_PUBLIC_SPFN_API_URL + /events/stream)\n * const client = createSSEClient<EventRouter>();\n *\n * // Or with custom configuration\n * const client = createSSEClient<EventRouter>({\n * host: 'https://api.example.com',\n * pathname: '/sse',\n * reconnect: true,\n * reconnectDelay: 3000,\n * });\n *\n * // Subscribe to events\n * const unsubscribe = client.subscribe({\n * events: ['userCreated', 'orderPlaced'],\n * handlers: {\n * userCreated: (payload) => {\n * console.log('New user:', payload.userId);\n * },\n * orderPlaced: (payload) => {\n * console.log('New order:', payload.orderId);\n * },\n * },\n * onOpen: () => console.log('Connected'),\n * onError: (err) => console.error('Error:', err),\n * onReconnect: (attempt) => console.log('Reconnecting...', attempt),\n * });\n *\n * // Cleanup\n * unsubscribe();\n * ```\n */\n/**\n * Per-subscription connection resources.\n *\n * Grouped so an in-flight async connect can be cancelled atomically and\n * `close()` can tear down whichever subscription is currently active —\n * the shared module-level EventSource used to leak across the token await.\n */\ninterface SSEConnection\n{\n eventSource: EventSource | null;\n reconnectTimer: ReturnType<typeof setTimeout> | null;\n reconnectAttempts: number;\n closed: boolean;\n onClose?: () => void;\n}\n\nexport function createSSEClient<TRouter extends EventRouterDef<any>>(\n config: SSEClientConfig = {},\n): SSEClient<TRouter>\n{\n const {\n url,\n host = SSE_DEFAULTS.host,\n pathname = SSE_DEFAULTS.pathname,\n reconnect = true,\n reconnectDelay = 3000,\n maxReconnectAttempts = 0,\n withCredentials = false,\n acquireToken,\n } = config;\n\n // Build base URL: url takes precedence, otherwise host + pathname\n const baseUrl = url || `${host}${pathname}`;\n\n let state: SSEConnectionState = 'closed';\n let active: SSEConnection | null = null;\n\n // Idempotent teardown: closes the connection, fires onClose exactly once,\n // and clears the active slot if this was the live connection.\n function closeConn(conn: SSEConnection)\n {\n if (conn.closed)\n {\n return;\n }\n\n conn.closed = true;\n\n if (conn.reconnectTimer)\n {\n clearTimeout(conn.reconnectTimer);\n conn.reconnectTimer = null;\n }\n\n if (conn.eventSource)\n {\n conn.eventSource.close();\n conn.eventSource = null;\n }\n\n if (active === conn)\n {\n active = null;\n state = 'closed';\n }\n\n conn.onClose?.();\n }\n\n function subscribe(options: SSESubscribeOptions<TRouter>): SSEUnsubscribe\n {\n const { events, handlers, onOpen, onError, onReconnect, onClose } = options;\n\n // A new subscription supersedes any previous one on this client.\n if (active)\n {\n closeConn(active);\n }\n\n const conn: SSEConnection = {\n eventSource: null,\n reconnectTimer: null,\n reconnectAttempts: 0,\n closed: false,\n onClose,\n };\n active = conn;\n\n const eventNames = events as string[];\n\n function connect()\n {\n state = 'connecting';\n\n const init = async () =>\n {\n let tokenParam = '';\n\n if (acquireToken)\n {\n const token = await acquireToken();\n\n // Cancelled during the token await (e.g. StrictMode cleanup) —\n // don't open a connection that has no teardown waiting for it.\n if (conn.closed)\n {\n return;\n }\n\n tokenParam = `&token=${encodeURIComponent(token)}`;\n }\n\n if (conn.closed)\n {\n return;\n }\n\n const streamUrl = `${baseUrl}?events=${eventNames.join(',')}${tokenParam}`;\n\n conn.eventSource = new EventSource(streamUrl, {\n withCredentials,\n });\n\n setupEventHandlers(conn.eventSource, eventNames, handlers, onOpen, onError);\n setupReconnect(onReconnect);\n };\n\n init().catch(() =>\n {\n // Don't resurrect a torn-down subscription via reconnect.\n if (conn.closed)\n {\n return;\n }\n\n state = 'error';\n attemptReconnect(onReconnect);\n });\n }\n\n function setupEventHandlers(\n es: EventSource,\n names: string[],\n handlerMap: SSESubscribeOptions<TRouter>['handlers'],\n onOpenCb?: () => void,\n onErrorCb?: (error: Event) => void,\n )\n {\n es.onopen = () =>\n {\n state = 'open';\n conn.reconnectAttempts = 0;\n onOpenCb?.();\n };\n\n es.onerror = (error) =>\n {\n state = 'error';\n onErrorCb?.(error);\n };\n\n // Handle connected event (server sends this on connection)\n es.addEventListener('connected', (e: MessageEvent) =>\n {\n try\n {\n const data = JSON.parse(e.data);\n console.debug('[SSE] Connected:', data);\n }\n catch\n {\n // Ignore parse errors\n }\n });\n\n // Handle ping (keep-alive)\n es.addEventListener('ping', () =>\n {\n // Ping received, connection is alive\n });\n\n // Register handlers for each event\n for (const eventName of names)\n {\n const handler = (handlerMap as Record<string, ((payload: unknown) => void) | undefined>)[eventName];\n\n if (!handler)\n {\n continue;\n }\n\n es.addEventListener(eventName, (e: MessageEvent) =>\n {\n try\n {\n const message: SSEMessage = JSON.parse(e.data);\n handler(message.data);\n }\n catch (err)\n {\n console.error(`[SSE] Failed to parse event \"${eventName}\":`, err);\n }\n });\n }\n }\n\n function setupReconnect(onReconnectCb?: (attempt: number) => void)\n {\n if (!conn.eventSource)\n {\n return;\n }\n\n const currentEs = conn.eventSource;\n const originalOnError = currentEs.onerror;\n\n currentEs.onerror = (error) =>\n {\n if (originalOnError)\n {\n (originalOnError as (ev: Event) => void)(error);\n }\n\n // Token-auth 사용 시 브라우저 auto-retry는 소비된 토큰으로 재시도하므로\n // 즉시 close하고 우리 reconnect로 새 토큰 발급받아 재연결\n if (reconnect && acquireToken)\n {\n currentEs.close();\n attemptReconnect(onReconnectCb);\n }\n else if (reconnect && currentEs.readyState === EventSource.CLOSED)\n {\n attemptReconnect(onReconnectCb);\n }\n };\n }\n\n function attemptReconnect(onReconnectCb?: (attempt: number) => void)\n {\n if (conn.closed || !reconnect)\n {\n return;\n }\n\n if (maxReconnectAttempts > 0 && conn.reconnectAttempts >= maxReconnectAttempts)\n {\n closeConn(conn);\n\n return;\n }\n\n conn.reconnectAttempts++;\n onReconnectCb?.(conn.reconnectAttempts);\n\n conn.reconnectTimer = setTimeout(() =>\n {\n connect();\n }, reconnectDelay);\n }\n\n // Start connection\n connect();\n\n // Return unsubscribe function\n return () =>\n {\n closeConn(conn);\n };\n }\n\n function getState(): SSEConnectionState\n {\n return state;\n }\n\n function close()\n {\n if (active)\n {\n closeConn(active);\n }\n else\n {\n state = 'closed';\n }\n }\n\n return {\n subscribe,\n getState,\n close,\n };\n}\n\n/**\n * Simple subscribe function for one-off subscriptions\n *\n * @example\n * ```typescript\n * import { subscribeToEvents } from '@spfn/core/event/sse/client';\n * import type { EventRouter } from '@/server/events';\n *\n * // Using defaults\n * const unsubscribe = subscribeToEvents<EventRouter>(\n * ['userCreated', 'orderPlaced'],\n * {\n * userCreated: (payload) => console.log('User:', payload),\n * orderPlaced: (payload) => console.log('Order:', payload),\n * }\n * );\n *\n * // With custom host\n * const unsubscribe = subscribeToEvents<EventRouter>(\n * ['userCreated'],\n * { userCreated: (payload) => console.log('User:', payload) },\n * { host: 'https://api.example.com' }\n * );\n * ```\n */\nexport function subscribeToEvents<TRouter extends EventRouterDef<any>>(\n events: InferEventNames<TRouter>[],\n handlers: SSESubscribeOptions<TRouter>['handlers'],\n options?: SSEClientConfig,\n): SSEUnsubscribe\n{\n const client = createSSEClient<TRouter>(options);\n\n return client.subscribe({\n events,\n handlers,\n });\n}\n\n// ============================================================================\n// Auth SSE Client\n// ============================================================================\n\n/**\n * SSE client configuration for authenticated connections\n *\n * Same as SSEClientConfig but without acquireToken (auto-configured).\n */\nexport interface AuthSSEClientConfig extends Omit<SSEClientConfig, 'acquireToken'>\n{\n /**\n * RPC proxy base URL for token acquisition\n * @default '/api/rpc'\n */\n rpcBaseUrl?: string;\n}\n\n/**\n * Headers of the one-time token request.\n *\n * The token route is a cookie-authenticated POST through the RPC proxy, so the\n * @spfn/auth CSRF rule applies to it exactly as to any other mutation: the\n * readable `spfn_csrf` cookie family is mirrored into `x-spfn-csrf`, the way\n * the api client does. Without the header, `SPFN_AUTH_CSRF=enforce` refuses\n * the request and the stream never opens.\n *\n * @internal Exported for tests\n */\nexport function tokenRequestHeaders(cookies: Iterable<[string, string]> = documentCookieEntries()): Record<string, string>\n{\n const csrf = csrfHeaderValue(cookies);\n\n return csrf === undefined\n ? { 'Content-Type': 'application/json' }\n : { 'Content-Type': 'application/json', [CSRF_HEADER]: csrf };\n}\n\n/**\n * Create SSE client with built-in token authentication\n *\n * Acquires one-time SSE tokens via RPC proxy automatically, sending the\n * `x-spfn-csrf` header whenever a `spfn_csrf` cookie is present.\n * Requires eventRouteMap to be merged into RPC proxy config.\n *\n * @example\n * ```typescript\n * import { createAuthSSEClient } from '@spfn/core/event/sse/client';\n * import type { EventRouter } from '@/server/events';\n *\n * const client = createAuthSSEClient<EventRouter>();\n *\n * client.subscribe({\n * events: ['userCreated'],\n * handlers: {\n * userCreated: (payload) => console.log(payload),\n * },\n * });\n * ```\n */\nexport function createAuthSSEClient<TRouter extends EventRouterDef<any>>(\n config: AuthSSEClientConfig = {},\n): SSEClient<TRouter>\n{\n const { rpcBaseUrl = '/api/rpc', ...sseConfig } = config;\n\n return createSSEClient<TRouter>({\n ...sseConfig,\n acquireToken: async () =>\n {\n const res = await fetch(`${rpcBaseUrl}/eventsToken`, {\n method: 'POST',\n credentials: 'include',\n headers: tokenRequestHeaders(),\n body: JSON.stringify({}),\n });\n\n if (!res.ok)\n {\n throw new Error(`Failed to acquire SSE token: ${res.status}`);\n }\n\n const data = await res.json();\n\n return data.token;\n },\n });\n}\n"]}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Context } from 'hono';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import { E as EventRouterDef } from '../../router-CysBmgv3.js';
|
|
3
|
+
import { d as SSEHandlerConfig } from '../../types-B8zAxFsE.js';
|
|
4
|
+
export { e as SSEAuthConfig, S as SSEClientConfig, c as SSEConnectionState, f as SSEEventHandler, g as SSEEventHandlers, h as SSEHandlerAuthConfig, i as SSEMessage, a as SSESubscribeOptions, b as SSEUnsubscribe } from '../../types-B8zAxFsE.js';
|
|
5
|
+
import { S as SSETokenManager } from '../../token-manager-Ckjt2-qq.js';
|
|
6
|
+
export { C as CacheTokenStore, a as SSEToken, b as SSETokenManagerConfig, c as SSETokenStore } from '../../token-manager-Ckjt2-qq.js';
|
|
6
7
|
import '@sinclair/typebox';
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { W as WSRouterDef, a as WSSubscribeOptions, b as WSUnsubscribe, c as WSConnectionState, d as WSClientConfig } from '../../types-
|
|
1
|
+
import { W as WSRouterDef, a as WSSubscribeOptions, b as WSUnsubscribe, c as WSConnectionState, d as WSClientConfig } from '../../types-W4u_S0lF.js';
|
|
2
2
|
import 'hono';
|
|
3
|
-
import '../../
|
|
3
|
+
import '../../router-CysBmgv3.js';
|
|
4
4
|
import '@sinclair/typebox';
|
|
5
|
+
import '../../token-manager-Ckjt2-qq.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* WebSocket Client
|
package/dist/event/ws/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { e as WSMessageHandlers, W as WSRouterDef, f as WSHandlerConfig } from '../../types-
|
|
3
|
-
export { g as WSAuthConfig, d as WSClientConfig, c as WSConnectionState, h as WSEventHandlers, i as WSHandlerAuthConfig, j as WSMessageContext, k as WSMessageHandlerFn, l as WSRawConnection, a as WSSubscribeOptions, b as WSUnsubscribe } from '../../types-
|
|
1
|
+
import { a as EventDef } from '../../router-CysBmgv3.js';
|
|
2
|
+
import { e as WSMessageHandlers, W as WSRouterDef, f as WSHandlerConfig } from '../../types-W4u_S0lF.js';
|
|
3
|
+
export { g as WSAuthConfig, d as WSClientConfig, c as WSConnectionState, h as WSEventHandlers, i as WSHandlerAuthConfig, j as WSMessageContext, k as WSMessageHandlerFn, l as WSRawConnection, a as WSSubscribeOptions, b as WSUnsubscribe } from '../../types-W4u_S0lF.js';
|
|
4
4
|
import { Server } from 'node:http';
|
|
5
|
+
import { S as SSETokenManager } from '../../token-manager-Ckjt2-qq.js';
|
|
5
6
|
import '@sinclair/typebox';
|
|
6
7
|
import 'hono';
|
|
7
8
|
|
package/dist/job/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { J as JobOptions, C as CompensateHandler, a as JobHandler, b as JobDef, c as JobRouter, d as JobRouterEntry } from '../boss-
|
|
2
|
-
export { B as BossConfig,
|
|
1
|
+
import { J as JobOptions, C as CompensateHandler, a as JobHandler, b as JobDef, c as JobRouter, d as JobRouterEntry, e as JobQueuePolicy } from '../boss-BiDCbbAQ.js';
|
|
2
|
+
export { B as BossConfig, f as BossOptions, I as InferJobInput, g as InferJobOutput, h as JobSendOptions, i as getBoss, j as initBoss, k as isBossRunning, s as shouldClearOnStart, l as stopBoss } from '../boss-BiDCbbAQ.js';
|
|
3
3
|
import * as _sinclair_typebox from '@sinclair/typebox';
|
|
4
4
|
import { Static } from '@sinclair/typebox';
|
|
5
5
|
import { EventDef, InferEventPayload } from '@spfn/core/event';
|
|
@@ -168,11 +168,56 @@ declare function isJobRouter(value: unknown): value is JobRouter<any>;
|
|
|
168
168
|
* ```
|
|
169
169
|
*/
|
|
170
170
|
declare function defineJobRouter<TJobs extends Record<string, JobRouterEntry>>(jobs: TJobs): JobRouter<TJobs>;
|
|
171
|
+
/**
|
|
172
|
+
* Merge two job routers into a new one
|
|
173
|
+
*
|
|
174
|
+
* `defineServerConfig().jobs()` may be called once per domain router, the way
|
|
175
|
+
* `.routes()` is; each call merges into what is already registered instead of
|
|
176
|
+
* replacing it. Keys are the router's own namespace, so a key present in both
|
|
177
|
+
* routers is a mistake the app cannot recover from silently — one of the two
|
|
178
|
+
* would disappear.
|
|
179
|
+
*
|
|
180
|
+
* Neither input is mutated: a fresh router is returned.
|
|
181
|
+
*
|
|
182
|
+
* @throws if a top-level key exists in both routers
|
|
183
|
+
*/
|
|
184
|
+
declare function mergeJobRouters(base: JobRouter<any>, extra: JobRouter<any>): JobRouter<any>;
|
|
171
185
|
/**
|
|
172
186
|
* Collect all JobDefs from a JobRouter (including nested)
|
|
173
187
|
*/
|
|
174
188
|
declare function collectJobs(router: JobRouter<any>, prefix?: string): JobDef<any>[];
|
|
175
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Queue Policy Resolution
|
|
192
|
+
*
|
|
193
|
+
* Lives apart from register-jobs.ts and job-builder.ts so both can read the
|
|
194
|
+
* policy of a job definition without importing each other.
|
|
195
|
+
*/
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* The part of a job definition that decides its queue policy
|
|
199
|
+
*
|
|
200
|
+
* `JobDef` satisfies this; the builder can also pass its own fields before the
|
|
201
|
+
* definition object exists.
|
|
202
|
+
*/
|
|
203
|
+
interface QueuePolicySource {
|
|
204
|
+
readonly runOnce?: boolean;
|
|
205
|
+
readonly options?: JobOptions;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Resolve the pg-boss queue policy for a job definition
|
|
209
|
+
*
|
|
210
|
+
* An explicit `.options({ policy })` always wins. Otherwise a job that asks
|
|
211
|
+
* for deduplication — `.runOnce()` or an `options.singletonKey` — gets
|
|
212
|
+
* `exclusive`, the only policy under which a second enqueue is dropped while
|
|
213
|
+
* the first is still pending or running.
|
|
214
|
+
*
|
|
215
|
+
* Everything else stays `standard`: `exclusive` without a `singletonKey`
|
|
216
|
+
* deduplicates on `COALESCE(singleton_key, '')` and would collapse a plain
|
|
217
|
+
* queue to a single pending job.
|
|
218
|
+
*/
|
|
219
|
+
declare function resolveQueuePolicy(job: QueuePolicySource): JobQueuePolicy;
|
|
220
|
+
|
|
176
221
|
/**
|
|
177
222
|
* Job Registration
|
|
178
223
|
*
|
|
@@ -215,4 +260,4 @@ declare function collectJobs(router: JobRouter<any>, prefix?: string): JobDef<an
|
|
|
215
260
|
*/
|
|
216
261
|
declare function registerJobs(router: JobRouter<any>): Promise<void>;
|
|
217
262
|
|
|
218
|
-
export { CompensateHandler, JobDef, JobHandler, JobOptions, JobRouter, JobRouterEntry, collectJobs, defineJobRouter, isJobDef, isJobRouter, job, registerJobs };
|
|
263
|
+
export { CompensateHandler, JobDef, JobHandler, JobOptions, JobQueuePolicy, JobRouter, JobRouterEntry, collectJobs, defineJobRouter, isJobDef, isJobRouter, job, mergeJobRouters, registerJobs, resolveQueuePolicy };
|
package/dist/job/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import PgBoss from 'pg-boss';
|
|
2
1
|
import { logger } from '@spfn/core/logger';
|
|
2
|
+
import PgBoss from 'pg-boss';
|
|
3
3
|
import { env } from '@spfn/core/config';
|
|
4
4
|
|
|
5
|
-
// src/job/
|
|
5
|
+
// src/job/job-builder.ts
|
|
6
6
|
var jobLogger = logger.child("@spfn/core:job");
|
|
7
7
|
function requiresSSLWithoutVerification(connectionString) {
|
|
8
8
|
try {
|
|
@@ -94,9 +94,42 @@ function shouldSweepOrphanSchedules() {
|
|
|
94
94
|
return getBossConfig()?.sweepOrphanSchedules ?? false;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
// src/job/queue-policy.ts
|
|
98
|
+
function resolveQueuePolicy(job2) {
|
|
99
|
+
if (job2.options?.policy) {
|
|
100
|
+
return job2.options.policy;
|
|
101
|
+
}
|
|
102
|
+
if (job2.runOnce || job2.options?.singletonKey) {
|
|
103
|
+
return "exclusive";
|
|
104
|
+
}
|
|
105
|
+
return "standard";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// src/job/retry-options.ts
|
|
109
|
+
function toPgBossRetryOptions(options) {
|
|
110
|
+
return {
|
|
111
|
+
retryLimit: options?.retryLimit ?? 3,
|
|
112
|
+
retryDelay: Math.ceil((options?.retryDelay ?? 1e3) / 1e3),
|
|
113
|
+
// Exponential backoff by default — a failed cohort (e.g. a provider 429)
|
|
114
|
+
// would otherwise all retry at the same fixed offset (thundering herd).
|
|
115
|
+
retryBackoff: options?.retryBackoff ?? true
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
97
119
|
// src/job/job-builder.ts
|
|
120
|
+
var jobLogger2 = logger.child("@spfn/core:job");
|
|
121
|
+
var warnedIgnoredSingletonKeys = /* @__PURE__ */ new Set();
|
|
122
|
+
function warnIfSingletonKeyIgnored(name, source) {
|
|
123
|
+
if (resolveQueuePolicy(source) !== "standard" || warnedIgnoredSingletonKeys.has(name)) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
warnedIgnoredSingletonKeys.add(name);
|
|
127
|
+
jobLogger2.warn(
|
|
128
|
+
`[Job:${name}] singletonKey ignored: queue policy is "standard"; set .options({ policy: 'exclusive' }) (or another non-standard policy) to deduplicate`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
98
131
|
function buildPgBossOptions(defaults, sendOptions) {
|
|
99
|
-
const options = {};
|
|
132
|
+
const options = { ...toPgBossRetryOptions(defaults) };
|
|
100
133
|
if (sendOptions?.startAfter) {
|
|
101
134
|
options.startAfter = sendOptions.startAfter;
|
|
102
135
|
}
|
|
@@ -106,12 +139,6 @@ function buildPgBossOptions(defaults, sendOptions) {
|
|
|
106
139
|
if (sendOptions?.priority !== void 0) {
|
|
107
140
|
options.priority = sendOptions.priority;
|
|
108
141
|
}
|
|
109
|
-
if (defaults?.retryLimit !== void 0) {
|
|
110
|
-
options.retryLimit = defaults.retryLimit;
|
|
111
|
-
}
|
|
112
|
-
if (defaults?.retryDelay !== void 0) {
|
|
113
|
-
options.retryDelay = defaults.retryDelay;
|
|
114
|
-
}
|
|
115
142
|
if (defaults?.expireInSeconds !== void 0) {
|
|
116
143
|
options.expireInSeconds = defaults.expireInSeconds;
|
|
117
144
|
}
|
|
@@ -255,6 +282,9 @@ var JobBuilder = class _JobBuilder {
|
|
|
255
282
|
);
|
|
256
283
|
}
|
|
257
284
|
const [input, sendOptions] = inputSchema ? [inputOrOptions, maybeOptions] : [void 0, inputOrOptions];
|
|
285
|
+
if (sendOptions?.singletonKey) {
|
|
286
|
+
warnIfSingletonKeyIgnored(name, { runOnce, options });
|
|
287
|
+
}
|
|
258
288
|
return await boss.send(
|
|
259
289
|
name,
|
|
260
290
|
input ?? {},
|
|
@@ -276,6 +306,9 @@ var JobBuilder = class _JobBuilder {
|
|
|
276
306
|
);
|
|
277
307
|
}
|
|
278
308
|
const [inputs, sendOptions] = inputSchema ? [inputsOrOptions, maybeOptions] : [void 0, inputsOrOptions];
|
|
309
|
+
if (sendOptions?.singletonKey) {
|
|
310
|
+
warnIfSingletonKeyIgnored(name, { runOnce, options });
|
|
311
|
+
}
|
|
279
312
|
const pgBossOptions = buildPgBossOptions(options, sendOptions);
|
|
280
313
|
const jobs = (inputs ?? [{}]).map((data) => ({
|
|
281
314
|
name,
|
|
@@ -320,29 +353,38 @@ function defineJobRouter(jobs) {
|
|
|
320
353
|
_jobs: jobs
|
|
321
354
|
};
|
|
322
355
|
}
|
|
323
|
-
function
|
|
324
|
-
const jobs
|
|
356
|
+
function mergeJobRouters(base, extra) {
|
|
357
|
+
for (const key of Object.keys(extra.jobs)) {
|
|
358
|
+
if (key in base.jobs) {
|
|
359
|
+
throw new Error(
|
|
360
|
+
`jobs(): key "${key}" is already registered by an earlier .jobs() call; rename it or nest it under a domain router`
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return defineJobRouter({ ...base.jobs, ...extra.jobs });
|
|
365
|
+
}
|
|
366
|
+
function collectJobEntries(router, prefix = "") {
|
|
367
|
+
const entries = [];
|
|
325
368
|
for (const [key, value] of Object.entries(router.jobs)) {
|
|
326
|
-
const
|
|
369
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
327
370
|
if (isJobRouter(value)) {
|
|
328
|
-
|
|
371
|
+
entries.push(...collectJobEntries(value, path));
|
|
329
372
|
} else if (isJobDef(value)) {
|
|
330
|
-
|
|
373
|
+
entries.push({ key: path, job: value });
|
|
331
374
|
}
|
|
332
375
|
}
|
|
333
|
-
return
|
|
376
|
+
return entries;
|
|
334
377
|
}
|
|
335
|
-
|
|
378
|
+
function collectJobs(router, prefix = "") {
|
|
379
|
+
return collectJobEntries(router, prefix).map((entry) => entry.job);
|
|
380
|
+
}
|
|
381
|
+
var jobLogger3 = logger.child("@spfn/core:job");
|
|
336
382
|
function getEventQueueName(eventName) {
|
|
337
383
|
return `event:${eventName}`;
|
|
338
384
|
}
|
|
339
385
|
function getDefaultJobOptions(options) {
|
|
340
386
|
return {
|
|
341
|
-
|
|
342
|
-
retryDelay: options?.retryDelay ?? 1e3,
|
|
343
|
-
// Exponential backoff by default — a failed cohort (e.g. a provider 429)
|
|
344
|
-
// would otherwise all retry at the same fixed offset (thundering herd).
|
|
345
|
-
retryBackoff: options?.retryBackoff ?? true,
|
|
387
|
+
...toPgBossRetryOptions(options),
|
|
346
388
|
expireInSeconds: options?.expireInSeconds ?? 300
|
|
347
389
|
};
|
|
348
390
|
}
|
|
@@ -353,11 +395,12 @@ async function registerJobs(router) {
|
|
|
353
395
|
"pg-boss not initialized. Call initBoss() before registerJobs()"
|
|
354
396
|
);
|
|
355
397
|
}
|
|
398
|
+
assertUniqueJobNames(router);
|
|
356
399
|
const jobs = collectJobs(router);
|
|
357
400
|
const clearOnStart = shouldClearOnStart();
|
|
358
|
-
|
|
401
|
+
jobLogger3.info(`Registering ${jobs.length} job(s)...`);
|
|
359
402
|
if (clearOnStart) {
|
|
360
|
-
|
|
403
|
+
jobLogger3.info("Clearing existing jobs before registration...");
|
|
361
404
|
for (const job2 of jobs) {
|
|
362
405
|
await boss.deleteAllJobs(job2.name);
|
|
363
406
|
if (job2.subscribedEvent) {
|
|
@@ -365,7 +408,7 @@ async function registerJobs(router) {
|
|
|
365
408
|
await boss.deleteAllJobs(eventQueue);
|
|
366
409
|
}
|
|
367
410
|
}
|
|
368
|
-
|
|
411
|
+
jobLogger3.info("Existing jobs cleared");
|
|
369
412
|
}
|
|
370
413
|
await Promise.all(jobs.map((job2) => registerJob(job2)));
|
|
371
414
|
for (const job2 of jobs) {
|
|
@@ -376,42 +419,77 @@ async function registerJobs(router) {
|
|
|
376
419
|
if (shouldSweepOrphanSchedules()) {
|
|
377
420
|
await sweepOrphanSchedules(boss);
|
|
378
421
|
}
|
|
379
|
-
|
|
422
|
+
jobLogger3.info("All jobs registered successfully");
|
|
423
|
+
}
|
|
424
|
+
function assertUniqueJobNames(router) {
|
|
425
|
+
const keysByName = /* @__PURE__ */ new Map();
|
|
426
|
+
for (const { key, job: job2 } of collectJobEntries(router)) {
|
|
427
|
+
const existingKey = keysByName.get(job2.name);
|
|
428
|
+
if (existingKey) {
|
|
429
|
+
throw new Error(
|
|
430
|
+
`registerJobs(): job name "${job2.name}" is defined twice (keys "${existingKey}" and "${key}"); job names are pg-boss queue names and must be unique`
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
keysByName.set(job2.name, key);
|
|
434
|
+
}
|
|
380
435
|
}
|
|
381
436
|
var registeredCronNames = /* @__PURE__ */ new Set();
|
|
382
437
|
async function sweepOrphanSchedules(boss) {
|
|
383
438
|
if (registeredCronNames.size === 0) {
|
|
384
|
-
|
|
439
|
+
jobLogger3.debug("No cron jobs declared; skipping orphan schedule sweep");
|
|
385
440
|
return;
|
|
386
441
|
}
|
|
387
442
|
try {
|
|
388
443
|
const schedules = await boss.getSchedules();
|
|
389
444
|
const orphans = schedules.filter((schedule) => !registeredCronNames.has(schedule.name));
|
|
390
445
|
if (orphans.length === 0) {
|
|
391
|
-
|
|
446
|
+
jobLogger3.debug("No orphan schedules found");
|
|
392
447
|
return;
|
|
393
448
|
}
|
|
394
449
|
await Promise.all(orphans.map(async (orphan) => {
|
|
395
450
|
try {
|
|
396
451
|
await boss.unschedule(orphan.name, orphan.key);
|
|
397
|
-
|
|
452
|
+
jobLogger3.info(`Removed orphan schedule: ${orphan.name}`);
|
|
398
453
|
} catch (error) {
|
|
399
|
-
|
|
454
|
+
jobLogger3.error(`Failed to remove orphan schedule: ${orphan.name}`, {
|
|
400
455
|
error: error instanceof Error ? error.message : String(error)
|
|
401
456
|
});
|
|
402
457
|
}
|
|
403
458
|
}));
|
|
404
459
|
} catch (error) {
|
|
405
|
-
|
|
460
|
+
jobLogger3.error("Orphan schedule sweep failed", {
|
|
406
461
|
error: error instanceof Error ? error.message : String(error)
|
|
407
462
|
});
|
|
408
463
|
}
|
|
409
464
|
}
|
|
410
|
-
async function ensureQueue(boss, queueName) {
|
|
411
|
-
await boss.createQueue(queueName);
|
|
465
|
+
async function ensureQueue(boss, queueName, policy) {
|
|
466
|
+
await boss.createQueue(queueName, { policy });
|
|
467
|
+
const existing = await boss.getQueue(queueName);
|
|
468
|
+
if (!existing || existing.policy === policy) {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (shouldClearOnStart()) {
|
|
472
|
+
await boss.deleteQueue(queueName);
|
|
473
|
+
await boss.createQueue(queueName, { policy });
|
|
474
|
+
jobLogger3.info(`[Queue:${queueName}] policy ${existing.policy} \u2192 ${policy} (recreated: clearOnStart)`);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
jobLogger3.error(
|
|
478
|
+
`[Queue:${queueName}] policy mismatch: the queue exists as "${existing.policy}" but its jobs require "${policy}". Delete the queue in a quiet window (getBoss().deleteQueue('${queueName}') drops its job rows) or set clearOnStart in development; until then singletonKey/runOnce on this queue do not deduplicate`
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
function createQueueEnsurer(boss, policy) {
|
|
482
|
+
const ensured = /* @__PURE__ */ new Set();
|
|
483
|
+
return async (queueName) => {
|
|
484
|
+
if (ensured.has(queueName)) {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
ensured.add(queueName);
|
|
488
|
+
await ensureQueue(boss, queueName, policy);
|
|
489
|
+
};
|
|
412
490
|
}
|
|
413
491
|
async function executeJobHandler(job2, pgBossJob) {
|
|
414
|
-
|
|
492
|
+
jobLogger3.debug(`[Job:${job2.name}] Executing...`, { jobId: pgBossJob.id });
|
|
415
493
|
const startTime = Date.now();
|
|
416
494
|
try {
|
|
417
495
|
if (job2.inputSchema) {
|
|
@@ -420,13 +498,13 @@ async function executeJobHandler(job2, pgBossJob) {
|
|
|
420
498
|
await job2.handler();
|
|
421
499
|
}
|
|
422
500
|
const duration = Date.now() - startTime;
|
|
423
|
-
|
|
501
|
+
jobLogger3.info(`[Job:${job2.name}] Completed in ${duration}ms`, {
|
|
424
502
|
jobId: pgBossJob.id,
|
|
425
503
|
duration
|
|
426
504
|
});
|
|
427
505
|
} catch (error) {
|
|
428
506
|
const duration = Date.now() - startTime;
|
|
429
|
-
|
|
507
|
+
jobLogger3.error(`[Job:${job2.name}] Failed after ${duration}ms`, {
|
|
430
508
|
jobId: pgBossJob.id,
|
|
431
509
|
duration,
|
|
432
510
|
error: error instanceof Error ? error.message : String(error)
|
|
@@ -434,8 +512,8 @@ async function executeJobHandler(job2, pgBossJob) {
|
|
|
434
512
|
throw error;
|
|
435
513
|
}
|
|
436
514
|
}
|
|
437
|
-
async function registerWorker(boss, job2, queueName) {
|
|
438
|
-
await
|
|
515
|
+
async function registerWorker(boss, job2, queueName, ensureQueueOnce) {
|
|
516
|
+
await ensureQueueOnce(queueName);
|
|
439
517
|
const batchSize = job2.options?.batchSize ?? 1;
|
|
440
518
|
const pollingIntervalSeconds = job2.options?.pollingIntervalSeconds ?? env.JOB_POLLING_INTERVAL_SECONDS;
|
|
441
519
|
await boss.work(
|
|
@@ -469,28 +547,28 @@ function connectEventToQueue(boss, job2, queueName) {
|
|
|
469
547
|
eventDef._registerJobQueue(queueName, async (queue, payload) => {
|
|
470
548
|
await boss.send(queue, payload, getDefaultJobOptions(job2.options));
|
|
471
549
|
});
|
|
472
|
-
|
|
550
|
+
jobLogger3.debug(`[Job:${job2.name}] Connected to event: ${job2.subscribedEvent}`);
|
|
473
551
|
}
|
|
474
|
-
async function registerCronSchedule(boss, job2) {
|
|
552
|
+
async function registerCronSchedule(boss, job2, ensureQueueOnce) {
|
|
475
553
|
if (!job2.cronExpression) {
|
|
476
554
|
return;
|
|
477
555
|
}
|
|
478
|
-
|
|
479
|
-
await
|
|
556
|
+
jobLogger3.debug(`[Job:${job2.name}] Scheduling cron: ${job2.cronExpression}`);
|
|
557
|
+
await ensureQueueOnce(job2.name);
|
|
480
558
|
await boss.schedule(
|
|
481
559
|
job2.name,
|
|
482
560
|
job2.cronExpression,
|
|
483
561
|
{},
|
|
484
562
|
getDefaultJobOptions(job2.options)
|
|
485
563
|
);
|
|
486
|
-
|
|
564
|
+
jobLogger3.info(`[Job:${job2.name}] Cron scheduled: ${job2.cronExpression}`);
|
|
487
565
|
}
|
|
488
|
-
async function queueRunOnceJob(boss, job2) {
|
|
566
|
+
async function queueRunOnceJob(boss, job2, ensureQueueOnce) {
|
|
489
567
|
if (!job2.runOnce) {
|
|
490
568
|
return;
|
|
491
569
|
}
|
|
492
|
-
|
|
493
|
-
await
|
|
570
|
+
jobLogger3.debug(`[Job:${job2.name}] Queuing runOnce job`);
|
|
571
|
+
await ensureQueueOnce(job2.name);
|
|
494
572
|
await boss.send(
|
|
495
573
|
job2.name,
|
|
496
574
|
{},
|
|
@@ -499,7 +577,7 @@ async function queueRunOnceJob(boss, job2) {
|
|
|
499
577
|
singletonKey: `runOnce:${job2.name}`
|
|
500
578
|
}
|
|
501
579
|
);
|
|
502
|
-
|
|
580
|
+
jobLogger3.info(`[Job:${job2.name}] runOnce job queued`);
|
|
503
581
|
}
|
|
504
582
|
async function registerJob(job2) {
|
|
505
583
|
const boss = getBoss();
|
|
@@ -507,17 +585,18 @@ async function registerJob(job2) {
|
|
|
507
585
|
throw new Error("pg-boss not initialized");
|
|
508
586
|
}
|
|
509
587
|
const queueName = job2.subscribedEvent ? getEventQueueName(job2.subscribedEvent) : job2.name;
|
|
510
|
-
|
|
588
|
+
jobLogger3.debug(`Registering job: ${job2.name}`, {
|
|
511
589
|
queueName,
|
|
512
590
|
subscribedEvent: job2.subscribedEvent
|
|
513
591
|
});
|
|
514
|
-
|
|
592
|
+
const ensureQueueOnce = createQueueEnsurer(boss, resolveQueuePolicy(job2));
|
|
593
|
+
await registerWorker(boss, job2, queueName, ensureQueueOnce);
|
|
515
594
|
connectEventToQueue(boss, job2, queueName);
|
|
516
|
-
await registerCronSchedule(boss, job2);
|
|
517
|
-
await queueRunOnceJob(boss, job2);
|
|
518
|
-
|
|
595
|
+
await registerCronSchedule(boss, job2, ensureQueueOnce);
|
|
596
|
+
await queueRunOnceJob(boss, job2, ensureQueueOnce);
|
|
597
|
+
jobLogger3.debug(`Job registered: ${job2.name}`);
|
|
519
598
|
}
|
|
520
599
|
|
|
521
|
-
export { collectJobs, defineJobRouter, getBoss, initBoss, isBossRunning, isJobDef, isJobRouter, job, registerJobs, shouldClearOnStart, stopBoss };
|
|
600
|
+
export { collectJobs, defineJobRouter, getBoss, initBoss, isBossRunning, isJobDef, isJobRouter, job, mergeJobRouters, registerJobs, resolveQueuePolicy, shouldClearOnStart, stopBoss };
|
|
522
601
|
//# sourceMappingURL=index.js.map
|
|
523
602
|
//# sourceMappingURL=index.js.map
|