@spfn/core 0.2.0-beta.42 → 0.2.0-beta.44

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.
@@ -438,15 +438,28 @@ function createSSEHandler(router, config = {}, tokenManager) {
438
438
  subject: subject || void 0,
439
439
  clientIp: c.req.header("x-forwarded-for") || c.req.header("x-real-ip")
440
440
  });
441
+ c.header("X-Accel-Buffering", "no");
441
442
  return streamSSE(c, async (stream) => {
442
443
  const unsubscribes = [];
443
444
  let messageId = 0;
445
+ let connectionDead = false;
446
+ let pingTimer;
447
+ const cleanup = () => {
448
+ if (connectionDead) return;
449
+ connectionDead = true;
450
+ clearInterval(pingTimer);
451
+ unsubscribes.forEach((fn) => fn());
452
+ sseLogger.info("SSE dead connection cleaned up", {
453
+ events: allowedEvents
454
+ });
455
+ };
444
456
  for (const eventName of allowedEvents) {
445
457
  const eventDef = router.events[eventName];
446
458
  if (!eventDef) {
447
459
  continue;
448
460
  }
449
461
  const unsubscribe = eventDef.subscribe((payload) => {
462
+ if (connectionDead) return;
450
463
  if (subject && authConfig?.filter?.[eventName]) {
451
464
  if (!authConfig.filter[eventName](subject, payload)) {
452
465
  return;
@@ -461,10 +474,17 @@ function createSSEHandler(router, config = {}, tokenManager) {
461
474
  event: eventName,
462
475
  messageId
463
476
  });
464
- void stream.writeSSE({
477
+ stream.writeSSE({
465
478
  id: String(messageId),
466
479
  event: eventName,
467
480
  data: JSON.stringify(message)
481
+ }).catch((err) => {
482
+ sseLogger.warn("SSE write failed", {
483
+ event: eventName,
484
+ messageId,
485
+ error: err.message
486
+ });
487
+ cleanup();
468
488
  });
469
489
  });
470
490
  unsubscribes.push(unsubscribe);
@@ -480,21 +500,23 @@ function createSSEHandler(router, config = {}, tokenManager) {
480
500
  timestamp: Date.now()
481
501
  })
482
502
  });
483
- const pingTimer = setInterval(() => {
484
- void stream.writeSSE({
503
+ pingTimer = setInterval(() => {
504
+ if (connectionDead) return;
505
+ stream.writeSSE({
485
506
  event: "ping",
486
507
  data: JSON.stringify({ timestamp: Date.now() })
508
+ }).catch((err) => {
509
+ sseLogger.warn("SSE ping failed", {
510
+ error: err.message
511
+ });
512
+ cleanup();
487
513
  });
488
514
  }, pingInterval);
489
515
  const abortSignal = c.req.raw.signal;
490
- while (!abortSignal.aborted) {
516
+ while (!abortSignal.aborted && !connectionDead) {
491
517
  await stream.sleep(pingInterval);
492
518
  }
493
- clearInterval(pingTimer);
494
- unsubscribes.forEach((fn) => fn());
495
- sseLogger.info("SSE connection closed", {
496
- events: allowedEvents
497
- });
519
+ cleanup();
498
520
  }, async (err) => {
499
521
  sseLogger.error("SSE stream error", {
500
522
  error: err.message
@@ -1285,36 +1307,53 @@ async function registerJobs(router) {
1285
1307
  async function ensureQueue(boss, queueName) {
1286
1308
  await boss.createQueue(queueName);
1287
1309
  }
1310
+ async function executeJobHandler(job2, pgBossJob) {
1311
+ jobLogger2.debug(`[Job:${job2.name}] Executing...`, { jobId: pgBossJob.id });
1312
+ const startTime = Date.now();
1313
+ try {
1314
+ if (job2.inputSchema) {
1315
+ await job2.handler(pgBossJob.data);
1316
+ } else {
1317
+ await job2.handler();
1318
+ }
1319
+ const duration = Date.now() - startTime;
1320
+ jobLogger2.info(`[Job:${job2.name}] Completed in ${duration}ms`, {
1321
+ jobId: pgBossJob.id,
1322
+ duration
1323
+ });
1324
+ } catch (error) {
1325
+ const duration = Date.now() - startTime;
1326
+ jobLogger2.error(`[Job:${job2.name}] Failed after ${duration}ms`, {
1327
+ jobId: pgBossJob.id,
1328
+ duration,
1329
+ error: error instanceof Error ? error.message : String(error)
1330
+ });
1331
+ throw error;
1332
+ }
1333
+ }
1288
1334
  async function registerWorker(boss, job2, queueName) {
1289
1335
  await ensureQueue(boss, queueName);
1336
+ const batchSize = job2.options?.batchSize ?? 1;
1290
1337
  await boss.work(
1291
1338
  queueName,
1292
- { batchSize: 1 },
1293
- async (jobs) => {
1294
- for (const pgBossJob of jobs) {
1295
- jobLogger2.debug(`[Job:${job2.name}] Executing...`, { jobId: pgBossJob.id });
1296
- const startTime = Date.now();
1297
- try {
1298
- if (job2.inputSchema) {
1299
- await job2.handler(pgBossJob.data);
1300
- } else {
1301
- await job2.handler();
1302
- }
1303
- const duration = Date.now() - startTime;
1304
- jobLogger2.info(`[Job:${job2.name}] Completed in ${duration}ms`, {
1305
- jobId: pgBossJob.id,
1306
- duration
1307
- });
1308
- } catch (error) {
1309
- const duration = Date.now() - startTime;
1310
- jobLogger2.error(`[Job:${job2.name}] Failed after ${duration}ms`, {
1311
- jobId: pgBossJob.id,
1312
- duration,
1313
- error: error instanceof Error ? error.message : String(error)
1314
- });
1315
- throw error;
1339
+ { batchSize },
1340
+ async (pgBossJobs) => {
1341
+ if (batchSize <= 1) {
1342
+ await executeJobHandler(job2, pgBossJobs[0]);
1343
+ return;
1344
+ }
1345
+ const results = await Promise.allSettled(
1346
+ pgBossJobs.map((pgBossJob) => executeJobHandler(job2, pgBossJob))
1347
+ );
1348
+ const failedIds = [];
1349
+ for (let i = 0; i < results.length; i++) {
1350
+ if (results[i].status === "rejected") {
1351
+ failedIds.push(pgBossJobs[i].id);
1316
1352
  }
1317
1353
  }
1354
+ if (failedIds.length > 0) {
1355
+ await boss.fail(queueName, failedIds);
1356
+ }
1318
1357
  }
1319
1358
  );
1320
1359
  }