@spfn/core 0.2.0-beta.2 → 0.2.0-beta.20
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 +262 -1092
- package/dist/{boss-D-fGtVgM.d.ts → boss-DI1r4kTS.d.ts} +68 -11
- package/dist/codegen/index.d.ts +55 -8
- package/dist/codegen/index.js +179 -5
- package/dist/codegen/index.js.map +1 -1
- package/dist/config/index.d.ts +204 -6
- package/dist/config/index.js +44 -11
- package/dist/config/index.js.map +1 -1
- package/dist/db/index.d.ts +13 -0
- package/dist/db/index.js +92 -33
- package/dist/db/index.js.map +1 -1
- package/dist/env/index.d.ts +83 -3
- package/dist/env/index.js +83 -15
- package/dist/env/index.js.map +1 -1
- package/dist/env/loader.d.ts +95 -0
- package/dist/env/loader.js +78 -0
- package/dist/env/loader.js.map +1 -0
- package/dist/event/index.d.ts +3 -70
- package/dist/event/index.js +10 -1
- package/dist/event/index.js.map +1 -1
- package/dist/event/sse/client.d.ts +131 -0
- package/dist/event/sse/client.js +150 -0
- package/dist/event/sse/client.js.map +1 -0
- package/dist/event/sse/index.d.ts +46 -0
- package/dist/event/sse/index.js +205 -0
- package/dist/event/sse/index.js.map +1 -0
- package/dist/job/index.d.ts +54 -8
- package/dist/job/index.js +61 -12
- package/dist/job/index.js.map +1 -1
- package/dist/middleware/index.d.ts +124 -11
- package/dist/middleware/index.js +41 -7
- package/dist/middleware/index.js.map +1 -1
- package/dist/nextjs/index.d.ts +2 -2
- package/dist/nextjs/index.js +37 -5
- package/dist/nextjs/index.js.map +1 -1
- package/dist/nextjs/server.d.ts +70 -17
- package/dist/nextjs/server.js +107 -36
- package/dist/nextjs/server.js.map +1 -1
- package/dist/route/index.d.ts +207 -14
- package/dist/route/index.js +304 -31
- package/dist/route/index.js.map +1 -1
- package/dist/route/types.d.ts +2 -31
- package/dist/router-Di7ENoah.d.ts +151 -0
- package/dist/server/index.d.ts +321 -10
- package/dist/server/index.js +798 -189
- package/dist/server/index.js.map +1 -1
- package/dist/{types-DRG2XMTR.d.ts → types-7Mhoxnnt.d.ts} +97 -4
- package/dist/types-B-lVqv6b.d.ts +298 -0
- package/docs/cache.md +133 -0
- package/docs/codegen.md +74 -0
- package/docs/database.md +346 -0
- package/docs/entity.md +539 -0
- package/docs/env.md +499 -0
- package/docs/errors.md +319 -0
- package/docs/event.md +423 -0
- package/docs/file-upload.md +717 -0
- package/docs/job.md +131 -0
- package/docs/logger.md +108 -0
- package/docs/middleware.md +337 -0
- package/docs/nextjs.md +241 -0
- package/docs/repository.md +496 -0
- package/docs/route.md +497 -0
- package/docs/server.md +377 -0
- package/package.json +19 -3
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Context } from 'hono';
|
|
2
|
+
import { E as EventRouterDef } from '../../router-Di7ENoah.js';
|
|
3
|
+
import { S as SSEHandlerConfig, b as SSETokenManager } from '../../types-B-lVqv6b.js';
|
|
4
|
+
export { a as SSEAuthConfig, h as SSEClientConfig, l as SSEConnectionState, i as SSEEventHandler, j as SSEEventHandlers, g as SSEHandlerAuthConfig, f as SSEMessage, k as SSESubscribeOptions, c as SSEToken, e as SSETokenManagerConfig, d as SSETokenStore, m as SSEUnsubscribe } from '../../types-B-lVqv6b.js';
|
|
5
|
+
import '@sinclair/typebox';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* SSE Handler for Hono
|
|
9
|
+
*
|
|
10
|
+
* Creates SSE stream endpoint for event subscription
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* import { Hono } from 'hono';
|
|
15
|
+
* import { createSSEHandler } from '@spfn/core/event/sse';
|
|
16
|
+
* import { eventRouter } from './events';
|
|
17
|
+
*
|
|
18
|
+
* const app = new Hono();
|
|
19
|
+
*
|
|
20
|
+
* // GET /events/stream?events=userCreated,orderPlaced
|
|
21
|
+
* app.get('/events/stream', createSSEHandler(eventRouter));
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
declare module 'hono' {
|
|
26
|
+
interface ContextVariableMap {
|
|
27
|
+
sseSubject?: string;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Create SSE handler for Hono
|
|
32
|
+
*
|
|
33
|
+
* Query parameters:
|
|
34
|
+
* - events: Comma-separated list of event names to subscribe
|
|
35
|
+
* - token: One-time auth token (when auth is enabled)
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```typescript
|
|
39
|
+
* app.get('/events/stream', createSSEHandler(eventRouter, {
|
|
40
|
+
* pingInterval: 30000,
|
|
41
|
+
* }));
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
declare function createSSEHandler<TRouter extends EventRouterDef<any>>(router: TRouter, config?: SSEHandlerConfig, tokenManager?: SSETokenManager): (c: Context) => Promise<Response>;
|
|
45
|
+
|
|
46
|
+
export { SSEHandlerConfig, SSETokenManager, createSSEHandler };
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { streamSSE } from 'hono/streaming';
|
|
2
|
+
import { logger } from '@spfn/core/logger';
|
|
3
|
+
import { randomBytes } from 'crypto';
|
|
4
|
+
|
|
5
|
+
// src/event/sse/handler.ts
|
|
6
|
+
var sseLogger = logger.child("@spfn/core:sse");
|
|
7
|
+
function createSSEHandler(router, config = {}, tokenManager) {
|
|
8
|
+
const {
|
|
9
|
+
pingInterval = 3e4,
|
|
10
|
+
auth: authConfig
|
|
11
|
+
} = config;
|
|
12
|
+
return async (c) => {
|
|
13
|
+
const subject = await authenticateToken(c, tokenManager);
|
|
14
|
+
if (subject === false) {
|
|
15
|
+
return c.json({ error: "Missing token parameter" }, 401);
|
|
16
|
+
}
|
|
17
|
+
if (subject === null) {
|
|
18
|
+
return c.json({ error: "Invalid or expired token" }, 401);
|
|
19
|
+
}
|
|
20
|
+
if (subject) {
|
|
21
|
+
c.set("sseSubject", subject);
|
|
22
|
+
}
|
|
23
|
+
const requestedEvents = parseRequestedEvents(c);
|
|
24
|
+
if (!requestedEvents) {
|
|
25
|
+
return c.json({ error: "Missing events parameter" }, 400);
|
|
26
|
+
}
|
|
27
|
+
const validEventNames = router.eventNames;
|
|
28
|
+
const invalidEvents = requestedEvents.filter((e) => !validEventNames.includes(e));
|
|
29
|
+
if (invalidEvents.length > 0) {
|
|
30
|
+
return c.json({
|
|
31
|
+
error: "Invalid event names",
|
|
32
|
+
invalidEvents,
|
|
33
|
+
validEvents: validEventNames
|
|
34
|
+
}, 400);
|
|
35
|
+
}
|
|
36
|
+
const allowedEvents = await authorizeEvents(subject, requestedEvents, authConfig);
|
|
37
|
+
if (allowedEvents === null) {
|
|
38
|
+
return c.json({ error: "Not authorized for any requested events" }, 403);
|
|
39
|
+
}
|
|
40
|
+
sseLogger.debug("SSE connection requested", {
|
|
41
|
+
events: allowedEvents,
|
|
42
|
+
subject: subject || void 0,
|
|
43
|
+
clientIp: c.req.header("x-forwarded-for") || c.req.header("x-real-ip")
|
|
44
|
+
});
|
|
45
|
+
return streamSSE(c, async (stream) => {
|
|
46
|
+
const unsubscribes = [];
|
|
47
|
+
let messageId = 0;
|
|
48
|
+
for (const eventName of allowedEvents) {
|
|
49
|
+
const eventDef = router.events[eventName];
|
|
50
|
+
if (!eventDef) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const unsubscribe = eventDef.subscribe((payload) => {
|
|
54
|
+
if (subject && authConfig?.filter?.[eventName]) {
|
|
55
|
+
if (!authConfig.filter[eventName](subject, payload)) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
messageId++;
|
|
60
|
+
const message = {
|
|
61
|
+
event: eventName,
|
|
62
|
+
data: payload
|
|
63
|
+
};
|
|
64
|
+
sseLogger.debug("SSE sending event", {
|
|
65
|
+
event: eventName,
|
|
66
|
+
messageId
|
|
67
|
+
});
|
|
68
|
+
void stream.writeSSE({
|
|
69
|
+
id: String(messageId),
|
|
70
|
+
event: eventName,
|
|
71
|
+
data: JSON.stringify(message)
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
unsubscribes.push(unsubscribe);
|
|
75
|
+
}
|
|
76
|
+
sseLogger.info("SSE connection established", {
|
|
77
|
+
events: allowedEvents,
|
|
78
|
+
subscriptionCount: unsubscribes.length
|
|
79
|
+
});
|
|
80
|
+
await stream.writeSSE({
|
|
81
|
+
event: "connected",
|
|
82
|
+
data: JSON.stringify({
|
|
83
|
+
subscribedEvents: allowedEvents,
|
|
84
|
+
timestamp: Date.now()
|
|
85
|
+
})
|
|
86
|
+
});
|
|
87
|
+
const pingTimer = setInterval(() => {
|
|
88
|
+
void stream.writeSSE({
|
|
89
|
+
event: "ping",
|
|
90
|
+
data: JSON.stringify({ timestamp: Date.now() })
|
|
91
|
+
});
|
|
92
|
+
}, pingInterval);
|
|
93
|
+
const abortSignal = c.req.raw.signal;
|
|
94
|
+
while (!abortSignal.aborted) {
|
|
95
|
+
await stream.sleep(pingInterval);
|
|
96
|
+
}
|
|
97
|
+
clearInterval(pingTimer);
|
|
98
|
+
unsubscribes.forEach((fn) => fn());
|
|
99
|
+
sseLogger.info("SSE connection closed", {
|
|
100
|
+
events: allowedEvents
|
|
101
|
+
});
|
|
102
|
+
}, async (err) => {
|
|
103
|
+
sseLogger.error("SSE stream error", {
|
|
104
|
+
error: err.message
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
async function authenticateToken(c, tokenManager) {
|
|
110
|
+
if (!tokenManager) {
|
|
111
|
+
return void 0;
|
|
112
|
+
}
|
|
113
|
+
const token = c.req.query("token");
|
|
114
|
+
if (!token) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
return await tokenManager.verify(token);
|
|
118
|
+
}
|
|
119
|
+
function parseRequestedEvents(c) {
|
|
120
|
+
const eventsParam = c.req.query("events");
|
|
121
|
+
if (!eventsParam) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
return eventsParam.split(",").map((e) => e.trim());
|
|
125
|
+
}
|
|
126
|
+
async function authorizeEvents(subject, requestedEvents, authConfig) {
|
|
127
|
+
if (!subject || !authConfig?.authorize) {
|
|
128
|
+
return requestedEvents;
|
|
129
|
+
}
|
|
130
|
+
const allowed = await authConfig.authorize(subject, requestedEvents);
|
|
131
|
+
if (allowed.length === 0) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
return allowed;
|
|
135
|
+
}
|
|
136
|
+
var InMemoryTokenStore = class {
|
|
137
|
+
tokens = /* @__PURE__ */ new Map();
|
|
138
|
+
async set(token, data) {
|
|
139
|
+
this.tokens.set(token, data);
|
|
140
|
+
}
|
|
141
|
+
async consume(token) {
|
|
142
|
+
const data = this.tokens.get(token);
|
|
143
|
+
if (!data) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
this.tokens.delete(token);
|
|
147
|
+
return data;
|
|
148
|
+
}
|
|
149
|
+
async cleanup() {
|
|
150
|
+
const now = Date.now();
|
|
151
|
+
for (const [token, data] of this.tokens) {
|
|
152
|
+
if (data.expiresAt <= now) {
|
|
153
|
+
this.tokens.delete(token);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
var SSETokenManager = class {
|
|
159
|
+
store;
|
|
160
|
+
ttl;
|
|
161
|
+
cleanupTimer = null;
|
|
162
|
+
constructor(config) {
|
|
163
|
+
this.ttl = config?.ttl ?? 3e4;
|
|
164
|
+
this.store = config?.store ?? new InMemoryTokenStore();
|
|
165
|
+
const cleanupInterval = config?.cleanupInterval ?? 6e4;
|
|
166
|
+
this.cleanupTimer = setInterval(() => void this.store.cleanup(), cleanupInterval);
|
|
167
|
+
this.cleanupTimer.unref();
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Issue a new one-time-use token for the given subject
|
|
171
|
+
*/
|
|
172
|
+
async issue(subject) {
|
|
173
|
+
const token = randomBytes(32).toString("hex");
|
|
174
|
+
await this.store.set(token, {
|
|
175
|
+
token,
|
|
176
|
+
subject,
|
|
177
|
+
expiresAt: Date.now() + this.ttl
|
|
178
|
+
});
|
|
179
|
+
return token;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Verify and consume a token
|
|
183
|
+
* @returns subject string if valid, null if invalid/expired/already consumed
|
|
184
|
+
*/
|
|
185
|
+
async verify(token) {
|
|
186
|
+
const data = await this.store.consume(token);
|
|
187
|
+
if (!data || data.expiresAt <= Date.now()) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
return data.subject;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Cleanup timer and resources
|
|
194
|
+
*/
|
|
195
|
+
destroy() {
|
|
196
|
+
if (this.cleanupTimer) {
|
|
197
|
+
clearInterval(this.cleanupTimer);
|
|
198
|
+
this.cleanupTimer = null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
export { SSETokenManager, createSSEHandler };
|
|
204
|
+
//# sourceMappingURL=index.js.map
|
|
205
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/event/sse/handler.ts","../../../src/event/sse/token-manager.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * SSE Handler for Hono\n *\n * Creates SSE stream endpoint for event subscription\n *\n * @example\n * ```typescript\n * import { Hono } from 'hono';\n * import { createSSEHandler } from '@spfn/core/event/sse';\n * import { eventRouter } from './events';\n *\n * const app = new Hono();\n *\n * // GET /events/stream?events=userCreated,orderPlaced\n * app.get('/events/stream', createSSEHandler(eventRouter));\n * ```\n */\n\nimport type { Context } from 'hono';\nimport { streamSSE } from 'hono/streaming';\nimport { logger } from '@spfn/core/logger';\nimport type { EventRouterDef, InferEventNames } from '../router';\nimport type { SSEHandlerConfig, SSEHandlerAuthConfig } from './types';\nimport type { SSETokenManager } from './token-manager';\n\nconst sseLogger = logger.child('@spfn/core:sse');\n\n// Extend Hono context with SSE subject\ndeclare module 'hono'\n{\n interface ContextVariableMap\n {\n sseSubject?: string;\n }\n}\n\n/**\n * Create SSE handler for Hono\n *\n * Query parameters:\n * - events: Comma-separated list of event names to subscribe\n * - token: One-time auth token (when auth is enabled)\n *\n * @example\n * ```typescript\n * app.get('/events/stream', createSSEHandler(eventRouter, {\n * pingInterval: 30000,\n * }));\n * ```\n */\nexport function createSSEHandler<TRouter extends EventRouterDef<any>>(\n router: TRouter,\n config: SSEHandlerConfig = {},\n tokenManager?: SSETokenManager\n)\n{\n const {\n pingInterval = 30000,\n auth: authConfig,\n } = config;\n\n return async (c: Context) =>\n {\n // ── 1. Token Authentication ──\n const subject = await authenticateToken(c, tokenManager);\n if (subject === false)\n {\n return c.json({ error: 'Missing token parameter' }, 401);\n }\n if (subject === null)\n {\n return c.json({ error: 'Invalid or expired token' }, 401);\n }\n if (subject)\n {\n c.set('sseSubject', subject);\n }\n\n // ── 2. Parse events from query parameter ──\n const requestedEvents = parseRequestedEvents(c);\n if (!requestedEvents)\n {\n return c.json({ error: 'Missing events parameter' }, 400);\n }\n\n // ── 3. Validate event names ──\n const validEventNames = router.eventNames as string[];\n const invalidEvents = requestedEvents.filter(e => !validEventNames.includes(e));\n\n if (invalidEvents.length > 0)\n {\n return c.json({\n error: 'Invalid event names',\n invalidEvents,\n validEvents: validEventNames,\n }, 400);\n }\n\n // ── 4. Subscription Authorization ──\n const allowedEvents = await authorizeEvents(subject, requestedEvents, authConfig);\n if (allowedEvents === null)\n {\n return c.json({ error: 'Not authorized for any requested events' }, 403);\n }\n\n sseLogger.debug('SSE connection requested', {\n events: allowedEvents,\n subject: subject || undefined,\n clientIp: c.req.header('x-forwarded-for') || c.req.header('x-real-ip'),\n });\n\n // ── 5. SSE Stream ──\n return streamSSE(c, async (stream) =>\n {\n const unsubscribes: (() => void)[] = [];\n let messageId = 0;\n\n for (const eventName of allowedEvents as InferEventNames<TRouter>[])\n {\n const eventDef = router.events[eventName];\n\n if (!eventDef)\n {\n continue;\n }\n\n const unsubscribe = eventDef.subscribe((payload: unknown) =>\n {\n // ── Payload Filtering ──\n if (subject && authConfig?.filter?.[eventName as string])\n {\n if (!authConfig.filter[eventName as string](subject, payload))\n {\n return;\n }\n }\n\n messageId++;\n\n const message = {\n event: eventName,\n data: payload,\n };\n\n sseLogger.debug('SSE sending event', {\n event: eventName,\n messageId,\n });\n\n // Fire-and-forget in sync callback\n void stream.writeSSE({\n id: String(messageId),\n event: eventName as string,\n data: JSON.stringify(message),\n });\n });\n\n unsubscribes.push(unsubscribe);\n }\n\n sseLogger.info('SSE connection established', {\n events: allowedEvents,\n subscriptionCount: unsubscribes.length,\n });\n\n // Send initial connection message\n await stream.writeSSE({\n event: 'connected',\n data: JSON.stringify({\n subscribedEvents: allowedEvents,\n timestamp: Date.now(),\n }),\n });\n\n // Keep-alive ping\n const pingTimer = setInterval(() =>\n {\n // Fire-and-forget in sync callback\n void stream.writeSSE({\n event: 'ping',\n data: JSON.stringify({ timestamp: Date.now() }),\n });\n }, pingInterval);\n\n // Wait for client disconnect using abort signal\n const abortSignal = c.req.raw.signal;\n\n while (!abortSignal.aborted)\n {\n await stream.sleep(pingInterval);\n }\n\n // Cleanup\n clearInterval(pingTimer);\n unsubscribes.forEach(fn => fn());\n\n sseLogger.info('SSE connection closed', {\n events: allowedEvents,\n });\n }, async (err: Error) =>\n {\n sseLogger.error('SSE stream error', {\n error: err.message,\n });\n });\n };\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Authenticate via one-time token\n * @returns subject string if authenticated, undefined if no auth required,\n * false if token missing, null if token invalid/expired\n */\nasync function authenticateToken(\n c: Context,\n tokenManager?: SSETokenManager\n): Promise<string | undefined | false | null>\n{\n if (!tokenManager)\n {\n return undefined;\n }\n\n const token = c.req.query('token');\n if (!token)\n {\n return false;\n }\n\n return await tokenManager.verify(token);\n}\n\n/**\n * Parse requested events from query parameter\n */\nfunction parseRequestedEvents(c: Context): string[] | null\n{\n const eventsParam = c.req.query('events');\n if (!eventsParam)\n {\n return null;\n }\n\n return eventsParam.split(',').map(e => e.trim());\n}\n\n/**\n * Authorize event subscription via auth hook\n * @returns allowed events array, or null if rejected\n */\nasync function authorizeEvents(\n subject: string | undefined,\n requestedEvents: string[],\n authConfig?: SSEHandlerAuthConfig\n): Promise<string[] | null>\n{\n if (!subject || !authConfig?.authorize)\n {\n return requestedEvents;\n }\n\n const allowed = await authConfig.authorize(subject, requestedEvents);\n\n if (allowed.length === 0)\n {\n return null;\n }\n\n return allowed;\n}\n","/**\n * SSE Token Manager\n *\n * Auth-agnostic token issuance and verification for SSE connections.\n * Issues one-time-use tokens with TTL for Token Exchange pattern.\n *\n * @example\n * ```typescript\n * const manager = new SSETokenManager({ ttl: 30000 });\n *\n * // Issue token for authenticated user\n * const token = await manager.issue('user-123');\n *\n * // Verify and consume token (one-time use)\n * const subject = await manager.verify(token); // 'user-123'\n * const again = await manager.verify(token); // null (already consumed)\n *\n * // Cleanup on shutdown\n * manager.destroy();\n * ```\n */\n\nimport { randomBytes } from 'crypto';\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Stored SSE token data\n */\nexport interface SSEToken\n{\n token: string;\n subject: string;\n expiresAt: number;\n}\n\n/**\n * Token storage interface\n *\n * Implement this for custom storage backends (e.g., Redis for multi-instance).\n */\nexport interface SSETokenStore\n{\n /** Store a token */\n set(token: string, data: SSEToken): Promise<void>;\n\n /** Get and delete a token (one-time use) */\n consume(token: string): Promise<SSEToken | null>;\n\n /** Remove expired tokens */\n cleanup(): Promise<void>;\n}\n\n/**\n * SSETokenManager configuration\n */\nexport interface SSETokenManagerConfig\n{\n /**\n * Token time-to-live in milliseconds\n * @default 30000\n */\n ttl?: number;\n\n /**\n * Custom token store (default: in-memory Map)\n */\n store?: SSETokenStore;\n\n /**\n * Cleanup interval in milliseconds\n * @default 60000\n */\n cleanupInterval?: number;\n}\n\n// ============================================================================\n// InMemoryTokenStore\n// ============================================================================\n\nclass InMemoryTokenStore implements SSETokenStore\n{\n private tokens = new Map<string, SSEToken>();\n\n async set(token: string, data: SSEToken): Promise<void>\n {\n this.tokens.set(token, data);\n }\n\n async consume(token: string): Promise<SSEToken | null>\n {\n const data = this.tokens.get(token);\n if (!data)\n {\n return null;\n }\n\n this.tokens.delete(token);\n return data;\n }\n\n async cleanup(): Promise<void>\n {\n const now = Date.now();\n\n for (const [token, data] of this.tokens)\n {\n if (data.expiresAt <= now)\n {\n this.tokens.delete(token);\n }\n }\n }\n}\n\n// ============================================================================\n// SSETokenManager\n// ============================================================================\n\nexport class SSETokenManager\n{\n private store: SSETokenStore;\n private ttl: number;\n private cleanupTimer: ReturnType<typeof setInterval> | null = null;\n\n constructor(config?: SSETokenManagerConfig)\n {\n this.ttl = config?.ttl ?? 30000;\n this.store = config?.store ?? new InMemoryTokenStore();\n\n const cleanupInterval = config?.cleanupInterval ?? 60000;\n this.cleanupTimer = setInterval(() => void this.store.cleanup(), cleanupInterval);\n this.cleanupTimer.unref();\n }\n\n /**\n * Issue a new one-time-use token for the given subject\n */\n async issue(subject: string): Promise<string>\n {\n const token = randomBytes(32).toString('hex');\n\n await this.store.set(token, {\n token,\n subject,\n expiresAt: Date.now() + this.ttl,\n });\n\n return token;\n }\n\n /**\n * Verify and consume a token\n * @returns subject string if valid, null if invalid/expired/already consumed\n */\n async verify(token: string): Promise<string | null>\n {\n const data = await this.store.consume(token);\n\n if (!data || data.expiresAt <= Date.now())\n {\n return null;\n }\n\n return data.subject;\n }\n\n /**\n * Cleanup timer and resources\n */\n destroy(): void\n {\n if (this.cleanupTimer)\n {\n clearInterval(this.cleanupTimer);\n this.cleanupTimer = null;\n }\n }\n}\n"]}
|
package/dist/job/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as JobOptions, b as JobHandler, c as JobDef, d as JobRouterEntry, J as JobRouter } from '../boss-
|
|
2
|
-
export {
|
|
1
|
+
import { a as JobOptions, C as CompensateHandler, b as JobHandler, c as JobDef, d as JobRouterEntry, J as JobRouter } from '../boss-DI1r4kTS.js';
|
|
2
|
+
export { k as BossConfig, B as BossOptions, I as InferJobInput, f as InferJobOutput, e as JobSendOptions, g as getBoss, i as initBoss, h as isBossRunning, j as shouldClearOnStart, s as stopBoss } from '../boss-DI1r4kTS.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';
|
|
@@ -8,20 +8,26 @@ import 'pg-boss';
|
|
|
8
8
|
/**
|
|
9
9
|
* Job builder class with fluent API
|
|
10
10
|
*/
|
|
11
|
-
declare class JobBuilder<TInput = void> {
|
|
12
|
-
private _name;
|
|
11
|
+
declare class JobBuilder<TInput = void, TOutput = void> {
|
|
12
|
+
private readonly _name;
|
|
13
13
|
private _inputSchema?;
|
|
14
|
+
private _outputSchema?;
|
|
14
15
|
private _cronExpression?;
|
|
15
16
|
private _runOnce?;
|
|
16
17
|
private _subscribedEvent?;
|
|
17
18
|
private _subscribedEventDef?;
|
|
18
19
|
private _options?;
|
|
19
20
|
private _handler?;
|
|
21
|
+
private _compensate?;
|
|
20
22
|
constructor(name: string);
|
|
21
23
|
/**
|
|
22
24
|
* Define input schema with TypeBox
|
|
23
25
|
*/
|
|
24
|
-
input<TSchema extends _sinclair_typebox.TSchema>(schema: TSchema): JobBuilder<Static<TSchema
|
|
26
|
+
input<TSchema extends _sinclair_typebox.TSchema>(schema: TSchema): JobBuilder<Static<TSchema>, TOutput>;
|
|
27
|
+
/**
|
|
28
|
+
* Define output schema with TypeBox (for workflow integration)
|
|
29
|
+
*/
|
|
30
|
+
output<TSchema extends _sinclair_typebox.TSchema>(schema: TSchema): JobBuilder<TInput, Static<TSchema>>;
|
|
25
31
|
/**
|
|
26
32
|
* Subscribe to an event (decoupled triggering)
|
|
27
33
|
*
|
|
@@ -38,7 +44,7 @@ declare class JobBuilder<TInput = void> {
|
|
|
38
44
|
* });
|
|
39
45
|
* ```
|
|
40
46
|
*/
|
|
41
|
-
on<TEvent extends EventDef<any>>(event: TEvent): JobBuilder<InferEventPayload<TEvent
|
|
47
|
+
on<TEvent extends EventDef<any>>(event: TEvent): JobBuilder<InferEventPayload<TEvent>, TOutput>;
|
|
42
48
|
/**
|
|
43
49
|
* Set cron expression for scheduled execution
|
|
44
50
|
*/
|
|
@@ -51,10 +57,19 @@ declare class JobBuilder<TInput = void> {
|
|
|
51
57
|
* Set job options (retry, expiration, etc.)
|
|
52
58
|
*/
|
|
53
59
|
options(options: JobOptions): this;
|
|
60
|
+
/**
|
|
61
|
+
* Set job timeout in milliseconds
|
|
62
|
+
* (Converts to expireInSeconds for pg-boss)
|
|
63
|
+
*/
|
|
64
|
+
timeout(ms: number): this;
|
|
65
|
+
/**
|
|
66
|
+
* Define compensate handler for rollback (workflow integration)
|
|
67
|
+
*/
|
|
68
|
+
compensate(fn: CompensateHandler<TInput, TOutput>): this;
|
|
54
69
|
/**
|
|
55
70
|
* Define the job handler and finalize the job definition
|
|
56
71
|
*/
|
|
57
|
-
handler(fn: JobHandler<TInput>): JobDef<TInput>;
|
|
72
|
+
handler(fn: JobHandler<TInput, TOutput>): JobDef<TInput, TOutput>;
|
|
58
73
|
}
|
|
59
74
|
/**
|
|
60
75
|
* Create a new job definition
|
|
@@ -166,7 +181,38 @@ declare function collectJobs(router: JobRouter<any>, prefix?: string): JobDef<an
|
|
|
166
181
|
|
|
167
182
|
/**
|
|
168
183
|
* Register all jobs from a JobRouter with pg-boss
|
|
184
|
+
*
|
|
185
|
+
* This function:
|
|
186
|
+
* 1. Collects all jobs from the router (including nested routers)
|
|
187
|
+
* 2. Optionally clears existing jobs (if clearOnStart is enabled)
|
|
188
|
+
* 3. Registers each job's worker handler with pg-boss
|
|
189
|
+
* 4. Sets up cron schedules for scheduled jobs
|
|
190
|
+
* 5. Queues runOnce jobs
|
|
191
|
+
* 6. Connects event subscriptions to job queues
|
|
192
|
+
*
|
|
193
|
+
* @param router - JobRouter containing job definitions
|
|
194
|
+
*
|
|
195
|
+
* @example
|
|
196
|
+
* ```typescript
|
|
197
|
+
* // Define jobs
|
|
198
|
+
* const sendEmail = job('send-email')
|
|
199
|
+
* .input(Type.Object({ to: Type.String() }))
|
|
200
|
+
* .handler(async (input) => { ... });
|
|
201
|
+
*
|
|
202
|
+
* const dailyReport = job('daily-report')
|
|
203
|
+
* .cron('0 9 * * *')
|
|
204
|
+
* .handler(async () => { ... });
|
|
205
|
+
*
|
|
206
|
+
* // Create router
|
|
207
|
+
* const jobRouter = defineJobRouter({ sendEmail, dailyReport });
|
|
208
|
+
*
|
|
209
|
+
* // Initialize pg-boss first
|
|
210
|
+
* await initBoss({ connectionString: process.env.DATABASE_URL! });
|
|
211
|
+
*
|
|
212
|
+
* // Register jobs
|
|
213
|
+
* await registerJobs(jobRouter);
|
|
214
|
+
* ```
|
|
169
215
|
*/
|
|
170
216
|
declare function registerJobs(router: JobRouter<any>): Promise<void>;
|
|
171
217
|
|
|
172
|
-
export {
|
|
218
|
+
export { CompensateHandler, JobDef, JobHandler, JobOptions, JobRouter, JobRouterEntry, collectJobs, defineJobRouter, isJobDef, isJobRouter, job, registerJobs };
|
package/dist/job/index.js
CHANGED
|
@@ -5,19 +5,22 @@ import { logger } from '@spfn/core/logger';
|
|
|
5
5
|
var jobLogger = logger.child("@spfn/core:job");
|
|
6
6
|
var bossInstance = null;
|
|
7
7
|
var bossConfig = null;
|
|
8
|
-
async function initBoss(
|
|
8
|
+
async function initBoss(options) {
|
|
9
9
|
if (bossInstance) {
|
|
10
10
|
jobLogger.warn("pg-boss already initialized, returning existing instance");
|
|
11
11
|
return bossInstance;
|
|
12
12
|
}
|
|
13
13
|
jobLogger.info("Initializing pg-boss...");
|
|
14
|
-
bossConfig =
|
|
15
|
-
|
|
16
|
-
connectionString:
|
|
17
|
-
schema:
|
|
18
|
-
maintenanceIntervalSeconds:
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
bossConfig = options;
|
|
15
|
+
const pgBossOptions = {
|
|
16
|
+
connectionString: options.connectionString,
|
|
17
|
+
schema: options.schema ?? "spfn_queue",
|
|
18
|
+
maintenanceIntervalSeconds: options.maintenanceIntervalSeconds ?? 120
|
|
19
|
+
};
|
|
20
|
+
if (options.monitorIntervalSeconds !== void 0 && options.monitorIntervalSeconds >= 1) {
|
|
21
|
+
pgBossOptions.monitorIntervalSeconds = options.monitorIntervalSeconds;
|
|
22
|
+
}
|
|
23
|
+
bossInstance = new PgBoss(pgBossOptions);
|
|
21
24
|
bossInstance.on("error", (error) => {
|
|
22
25
|
jobLogger.error("pg-boss error:", error);
|
|
23
26
|
});
|
|
@@ -86,12 +89,14 @@ function buildPgBossOptions(defaults, sendOptions) {
|
|
|
86
89
|
var JobBuilder = class _JobBuilder {
|
|
87
90
|
_name;
|
|
88
91
|
_inputSchema;
|
|
92
|
+
_outputSchema;
|
|
89
93
|
_cronExpression;
|
|
90
94
|
_runOnce;
|
|
91
95
|
_subscribedEvent;
|
|
92
96
|
_subscribedEventDef;
|
|
93
97
|
_options;
|
|
94
98
|
_handler;
|
|
99
|
+
_compensate;
|
|
95
100
|
constructor(name) {
|
|
96
101
|
this._name = name;
|
|
97
102
|
}
|
|
@@ -101,6 +106,20 @@ var JobBuilder = class _JobBuilder {
|
|
|
101
106
|
input(schema) {
|
|
102
107
|
const builder = new _JobBuilder(this._name);
|
|
103
108
|
builder._inputSchema = schema;
|
|
109
|
+
builder._outputSchema = this._outputSchema;
|
|
110
|
+
builder._cronExpression = this._cronExpression;
|
|
111
|
+
builder._runOnce = this._runOnce;
|
|
112
|
+
builder._subscribedEvent = this._subscribedEvent;
|
|
113
|
+
builder._options = this._options;
|
|
114
|
+
return builder;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Define output schema with TypeBox (for workflow integration)
|
|
118
|
+
*/
|
|
119
|
+
output(schema) {
|
|
120
|
+
const builder = new _JobBuilder(this._name);
|
|
121
|
+
builder._inputSchema = this._inputSchema;
|
|
122
|
+
builder._outputSchema = schema;
|
|
104
123
|
builder._cronExpression = this._cronExpression;
|
|
105
124
|
builder._runOnce = this._runOnce;
|
|
106
125
|
builder._subscribedEvent = this._subscribedEvent;
|
|
@@ -126,6 +145,7 @@ var JobBuilder = class _JobBuilder {
|
|
|
126
145
|
on(event) {
|
|
127
146
|
const builder = new _JobBuilder(this._name);
|
|
128
147
|
builder._inputSchema = event.schema;
|
|
148
|
+
builder._outputSchema = this._outputSchema;
|
|
129
149
|
builder._subscribedEvent = event.name;
|
|
130
150
|
builder._subscribedEventDef = event;
|
|
131
151
|
builder._cronExpression = this._cronExpression;
|
|
@@ -154,6 +174,24 @@ var JobBuilder = class _JobBuilder {
|
|
|
154
174
|
this._options = options;
|
|
155
175
|
return this;
|
|
156
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Set job timeout in milliseconds
|
|
179
|
+
* (Converts to expireInSeconds for pg-boss)
|
|
180
|
+
*/
|
|
181
|
+
timeout(ms) {
|
|
182
|
+
this._options = {
|
|
183
|
+
...this._options,
|
|
184
|
+
expireInSeconds: Math.ceil(ms / 1e3)
|
|
185
|
+
};
|
|
186
|
+
return this;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Define compensate handler for rollback (workflow integration)
|
|
190
|
+
*/
|
|
191
|
+
compensate(fn) {
|
|
192
|
+
this._compensate = fn;
|
|
193
|
+
return this;
|
|
194
|
+
}
|
|
157
195
|
/**
|
|
158
196
|
* Define the job handler and finalize the job definition
|
|
159
197
|
*/
|
|
@@ -161,12 +199,14 @@ var JobBuilder = class _JobBuilder {
|
|
|
161
199
|
this._handler = fn;
|
|
162
200
|
const name = this._name;
|
|
163
201
|
const inputSchema = this._inputSchema;
|
|
202
|
+
const outputSchema = this._outputSchema;
|
|
164
203
|
const cronExpression = this._cronExpression;
|
|
165
204
|
const runOnce = this._runOnce;
|
|
166
205
|
const subscribedEvent = this._subscribedEvent;
|
|
167
206
|
const subscribedEventDef = this._subscribedEventDef;
|
|
168
207
|
const options = this._options;
|
|
169
208
|
const handler = this._handler;
|
|
209
|
+
const compensate = this._compensate;
|
|
170
210
|
const send = async (inputOrOptions, maybeOptions) => {
|
|
171
211
|
const boss = getBoss();
|
|
172
212
|
if (!boss) {
|
|
@@ -183,23 +223,26 @@ var JobBuilder = class _JobBuilder {
|
|
|
183
223
|
};
|
|
184
224
|
const run = async (input) => {
|
|
185
225
|
if (inputSchema) {
|
|
186
|
-
await handler(input);
|
|
226
|
+
return await handler(input);
|
|
187
227
|
} else {
|
|
188
|
-
await handler();
|
|
228
|
+
return await handler();
|
|
189
229
|
}
|
|
190
230
|
};
|
|
191
231
|
return {
|
|
192
232
|
name,
|
|
193
233
|
inputSchema,
|
|
234
|
+
outputSchema,
|
|
194
235
|
cronExpression,
|
|
195
236
|
runOnce,
|
|
196
237
|
subscribedEvent,
|
|
197
238
|
_subscribedEventDef: subscribedEventDef,
|
|
198
239
|
options,
|
|
199
240
|
handler,
|
|
241
|
+
compensate,
|
|
200
242
|
send,
|
|
201
243
|
run,
|
|
202
|
-
_input: void 0
|
|
244
|
+
_input: void 0,
|
|
245
|
+
_output: void 0
|
|
203
246
|
};
|
|
204
247
|
}
|
|
205
248
|
};
|
|
@@ -269,7 +312,11 @@ async function registerJobs(router) {
|
|
|
269
312
|
}
|
|
270
313
|
jobLogger2.info("All jobs registered successfully");
|
|
271
314
|
}
|
|
315
|
+
async function ensureQueue(boss, queueName) {
|
|
316
|
+
await boss.createQueue(queueName);
|
|
317
|
+
}
|
|
272
318
|
async function registerWorker(boss, job2, queueName) {
|
|
319
|
+
await ensureQueue(boss, queueName);
|
|
273
320
|
await boss.work(
|
|
274
321
|
queueName,
|
|
275
322
|
{ batchSize: 1 },
|
|
@@ -316,6 +363,7 @@ async function registerCronSchedule(boss, job2) {
|
|
|
316
363
|
return;
|
|
317
364
|
}
|
|
318
365
|
jobLogger2.debug(`[Job:${job2.name}] Scheduling cron: ${job2.cronExpression}`);
|
|
366
|
+
await ensureQueue(boss, job2.name);
|
|
319
367
|
await boss.schedule(
|
|
320
368
|
job2.name,
|
|
321
369
|
job2.cronExpression,
|
|
@@ -329,6 +377,7 @@ async function queueRunOnceJob(boss, job2) {
|
|
|
329
377
|
return;
|
|
330
378
|
}
|
|
331
379
|
jobLogger2.debug(`[Job:${job2.name}] Queuing runOnce job`);
|
|
380
|
+
await ensureQueue(boss, job2.name);
|
|
332
381
|
await boss.send(
|
|
333
382
|
job2.name,
|
|
334
383
|
{},
|
|
@@ -356,6 +405,6 @@ async function registerJob(job2) {
|
|
|
356
405
|
jobLogger2.debug(`Job registered: ${job2.name}`);
|
|
357
406
|
}
|
|
358
407
|
|
|
359
|
-
export {
|
|
408
|
+
export { collectJobs, defineJobRouter, getBoss, initBoss, isBossRunning, isJobDef, isJobRouter, job, registerJobs, shouldClearOnStart, stopBoss };
|
|
360
409
|
//# sourceMappingURL=index.js.map
|
|
361
410
|
//# sourceMappingURL=index.js.map
|