@spfn/core 0.3.0-beta.23 → 0.3.0-beta.25
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 +1 -0
- package/dist/contract/index.d.ts +1 -1
- package/dist/contract/index.js +2 -2
- package/dist/contract/index.js.map +1 -1
- package/dist/event/index.d.ts +3 -3
- package/dist/event/index.js +160 -2
- package/dist/event/index.js.map +1 -1
- package/dist/event/sse/client.d.ts +2 -2
- package/dist/event/sse/client.js +20 -1
- package/dist/event/sse/client.js.map +1 -1
- package/dist/event/sse/index.d.ts +16 -4
- package/dist/event/sse/index.js +366 -109
- package/dist/event/sse/index.js.map +1 -1
- package/dist/event/ws/client.d.ts +2 -2
- package/dist/event/ws/index.d.ts +3 -3
- package/dist/router-D5B0cpVN.d.ts +378 -0
- package/dist/server/index.d.ts +3 -3
- package/dist/server/index.js +366 -109
- package/dist/server/index.js.map +1 -1
- package/dist/state/index.d.ts +380 -0
- package/dist/state/index.js +571 -0
- package/dist/state/index.js.map +1 -0
- package/dist/{types-W4u_S0lF.d.ts → types-CqTsDETV.d.ts} +1 -1
- package/dist/{types-B8zAxFsE.d.ts → types-DqXYtI69.d.ts} +27 -2
- package/package.json +6 -1
- package/src/contract/README.md +5 -3
- package/src/event/README.md +150 -13
- package/src/state/README.md +350 -0
- package/dist/router-CysBmgv3.d.ts +0 -232
package/dist/server/index.js
CHANGED
|
@@ -468,6 +468,124 @@ function createBoundedWriter(stream, maxQueue, onClose) {
|
|
|
468
468
|
}
|
|
469
469
|
};
|
|
470
470
|
}
|
|
471
|
+
var registryLogger = logger.child("@spfn/core:sse");
|
|
472
|
+
var SUBSCRIPTION_GRANTED = "subscription.granted";
|
|
473
|
+
var SUBSCRIPTION_REVOKED = "subscription.revoked";
|
|
474
|
+
function createConnectionRegistry() {
|
|
475
|
+
const bySubject = /* @__PURE__ */ new Map();
|
|
476
|
+
const pendingBySubject = /* @__PURE__ */ new Map();
|
|
477
|
+
const beginAdmission = (subject, signal) => {
|
|
478
|
+
const changes = [];
|
|
479
|
+
addToSet(pendingBySubject, subject, changes);
|
|
480
|
+
const end = endOnce(signal, () => removeFromSet(pendingBySubject, subject, changes));
|
|
481
|
+
return {
|
|
482
|
+
cancel: end,
|
|
483
|
+
open: (connection) => {
|
|
484
|
+
if (!end()) {
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
applyRecorded(connection, changes);
|
|
488
|
+
addToSet(bySubject, subject, connection);
|
|
489
|
+
return true;
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
};
|
|
493
|
+
const change = (kind, subject, eventName, match) => {
|
|
494
|
+
const pending = pendingBySubject.get(subject);
|
|
495
|
+
const connections = bySubject.get(subject);
|
|
496
|
+
pending?.forEach((changes) => changes.push({ kind, eventName, match }));
|
|
497
|
+
connections?.forEach((connection) => {
|
|
498
|
+
if (matchesRequest(match, connection.request)) {
|
|
499
|
+
(kind === "grant" ? grantOn : revokeOn)(connection, eventName);
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
if (!pending && !connections) {
|
|
503
|
+
registryLogger.debug("SSE subscription change for a subject with no open connection ignored", {
|
|
504
|
+
event: eventName,
|
|
505
|
+
subject
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
return {
|
|
510
|
+
beginAdmission,
|
|
511
|
+
remove: (connection) => {
|
|
512
|
+
if (connection.subject) {
|
|
513
|
+
removeFromSet(bySubject, connection.subject, connection);
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
/** Open connections only — a pending admission is not counted. */
|
|
517
|
+
count: (subject) => bySubject.get(subject)?.size ?? 0,
|
|
518
|
+
pendingCount: (subject) => pendingBySubject.get(subject)?.size ?? 0,
|
|
519
|
+
grant: (subject, eventName, match) => change("grant", subject, eventName, match),
|
|
520
|
+
revoke: (subject, eventName, match) => change("revoke", subject, eventName, match)
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
function endOnce(signal, remove) {
|
|
524
|
+
let ended = false;
|
|
525
|
+
const end = () => {
|
|
526
|
+
if (ended) {
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
ended = true;
|
|
530
|
+
signal.removeEventListener("abort", end);
|
|
531
|
+
remove();
|
|
532
|
+
return true;
|
|
533
|
+
};
|
|
534
|
+
signal.addEventListener("abort", end, { once: true });
|
|
535
|
+
if (signal.aborted) {
|
|
536
|
+
end();
|
|
537
|
+
}
|
|
538
|
+
return end;
|
|
539
|
+
}
|
|
540
|
+
function addToSet(map, key, value) {
|
|
541
|
+
const set = map.get(key) ?? /* @__PURE__ */ new Set();
|
|
542
|
+
set.add(value);
|
|
543
|
+
map.set(key, set);
|
|
544
|
+
}
|
|
545
|
+
function removeFromSet(map, key, value) {
|
|
546
|
+
const set = map.get(key);
|
|
547
|
+
set?.delete(value);
|
|
548
|
+
if (set?.size === 0) {
|
|
549
|
+
map.delete(key);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
function matchesRequest(match, request) {
|
|
553
|
+
return match === void 0 || Object.entries(match.query).every(([field, value]) => Object.hasOwn(request.query, field) && request.query[field] === value);
|
|
554
|
+
}
|
|
555
|
+
function applyRecorded(connection, changes) {
|
|
556
|
+
changes.forEach(({ kind, eventName, match }) => {
|
|
557
|
+
if (!matchesRequest(match, connection.request)) {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
const [from, to] = kind === "grant" ? [connection.denied, connection.allowed] : [connection.allowed, connection.denied];
|
|
561
|
+
if (from.delete(eventName)) {
|
|
562
|
+
to.add(eventName);
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
function grantOn(connection, eventName) {
|
|
567
|
+
if (!connection.denied.has(eventName)) {
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
connection.unsubscribes.set(eventName, connection.subscribe(eventName));
|
|
571
|
+
connection.denied.delete(eventName);
|
|
572
|
+
connection.allowed.add(eventName);
|
|
573
|
+
connection.notify(SUBSCRIPTION_GRANTED, eventName);
|
|
574
|
+
}
|
|
575
|
+
function revokeOn(connection, eventName) {
|
|
576
|
+
if (!connection.allowed.has(eventName)) {
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
connection.unsubscribes.get(eventName)?.();
|
|
580
|
+
connection.unsubscribes.delete(eventName);
|
|
581
|
+
connection.allowed.delete(eventName);
|
|
582
|
+
connection.denied.add(eventName);
|
|
583
|
+
connection.notify(SUBSCRIPTION_REVOKED, eventName);
|
|
584
|
+
}
|
|
585
|
+
function unsubscribeAll(connection) {
|
|
586
|
+
connection.unsubscribes.forEach((unsubscribe) => unsubscribe());
|
|
587
|
+
connection.unsubscribes.clear();
|
|
588
|
+
}
|
|
471
589
|
|
|
472
590
|
// src/event/sse/handler.ts
|
|
473
591
|
var sseLogger = logger.child("@spfn/core:sse");
|
|
@@ -498,125 +616,264 @@ function createSSEHandler(router, config = {}, tokenManager) {
|
|
|
498
616
|
auth: authConfig,
|
|
499
617
|
maxQueue = DEFAULT_MAX_QUEUE
|
|
500
618
|
} = config;
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
619
|
+
const registry = createConnectionRegistry();
|
|
620
|
+
registerSubscriptionControls(router, registry);
|
|
621
|
+
const streamConfig = { router, authConfig, registry, pingInterval, maxQueue };
|
|
622
|
+
const handler = async (c) => {
|
|
623
|
+
const admission = await admitConnection(c, streamConfig, tokenManager);
|
|
624
|
+
if (admission instanceof Response) {
|
|
625
|
+
return admission;
|
|
505
626
|
}
|
|
506
|
-
if (
|
|
507
|
-
|
|
627
|
+
if (c.req.raw.signal.aborted) {
|
|
628
|
+
admission.pending?.cancel();
|
|
629
|
+
return new Response(null, { status: 499 });
|
|
508
630
|
}
|
|
509
|
-
|
|
510
|
-
|
|
631
|
+
c.header("X-Accel-Buffering", "no");
|
|
632
|
+
return streamSSE(c, (stream) => runStream(c, stream, admission, streamConfig), async (err) => {
|
|
633
|
+
sseLogger.error("SSE stream error", {
|
|
634
|
+
error: err.message
|
|
635
|
+
});
|
|
636
|
+
});
|
|
637
|
+
};
|
|
638
|
+
return Object.assign(handler, {
|
|
639
|
+
/** @internal Open registered connections of a subject (tests). */
|
|
640
|
+
_openConnections: (subject) => registry.count(subject),
|
|
641
|
+
/** @internal Connects of a subject still in admission (tests). */
|
|
642
|
+
_pendingAdmissions: (subject) => registry.pendingCount(subject)
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
function registerSubscriptionControls(router, registry) {
|
|
646
|
+
for (const eventName of router.eventNames) {
|
|
647
|
+
router.events[eventName]._registerSubscriptionControl({
|
|
648
|
+
grant: (subject, match) => registry.grant(subject, eventName, match),
|
|
649
|
+
revoke: (subject, match) => registry.revoke(subject, eventName, match)
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
async function admitConnection(c, config, tokenManager) {
|
|
654
|
+
const { router, authConfig, registry } = config;
|
|
655
|
+
const subject = await authenticateToken(c, tokenManager);
|
|
656
|
+
if (subject === false) {
|
|
657
|
+
return c.json({ error: "Missing token parameter" }, 401);
|
|
658
|
+
}
|
|
659
|
+
if (subject === null) {
|
|
660
|
+
return c.json({ error: "Invalid or expired token" }, 401);
|
|
661
|
+
}
|
|
662
|
+
if (subject) {
|
|
663
|
+
c.set("sseSubject", subject);
|
|
664
|
+
}
|
|
665
|
+
const requested = parseRequestedEvents(c);
|
|
666
|
+
if (!requested) {
|
|
667
|
+
return c.json({ error: "Missing events parameter" }, 400);
|
|
668
|
+
}
|
|
669
|
+
const validEventNames = router.eventNames;
|
|
670
|
+
const invalidEvents = requested.filter((e) => !validEventNames.includes(e));
|
|
671
|
+
if (invalidEvents.length > 0) {
|
|
672
|
+
return c.json({
|
|
673
|
+
error: "Invalid event names",
|
|
674
|
+
invalidEvents,
|
|
675
|
+
validEvents: validEventNames
|
|
676
|
+
}, 400);
|
|
677
|
+
}
|
|
678
|
+
return whilePending(
|
|
679
|
+
subject ? registry.beginAdmission(subject, c.req.raw.signal) : void 0,
|
|
680
|
+
() => authorizeConnection(c, router, authConfig, subject, requested)
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
async function whilePending(pending, check) {
|
|
684
|
+
let outcome;
|
|
685
|
+
try {
|
|
686
|
+
outcome = await check();
|
|
687
|
+
return outcome instanceof Response ? outcome : { ...outcome, pending };
|
|
688
|
+
} finally {
|
|
689
|
+
if (!outcome || outcome instanceof Response) {
|
|
690
|
+
pending?.cancel();
|
|
511
691
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
async function authorizeConnection(c, router, authConfig, subject, requested) {
|
|
695
|
+
const authorized = await authorizeEvents(subject, requested, authConfig);
|
|
696
|
+
if (authorized === null) {
|
|
697
|
+
return c.json({ error: "Not authorized for any requested events" }, 403);
|
|
698
|
+
}
|
|
699
|
+
const request = { query: c.req.query(), headers: c.req.header() };
|
|
700
|
+
const resolved = await resolveConnectionPrincipal(subject, request, authConfig);
|
|
701
|
+
if (!resolved) {
|
|
702
|
+
return c.json({ error: "Principal could not be resolved" }, 403);
|
|
703
|
+
}
|
|
704
|
+
const allowed = subject ? await checkCanSubscribe(router, authorized, { subject, principal: resolved.principal, request }) : authorized;
|
|
705
|
+
const denied = requested.filter((e) => !allowed.includes(e));
|
|
706
|
+
sseLogger.debug("SSE connection requested", {
|
|
707
|
+
events: allowed,
|
|
708
|
+
deniedEvents: denied,
|
|
709
|
+
subject: subject || void 0,
|
|
710
|
+
clientIp: c.req.header("x-forwarded-for") || c.req.header("x-real-ip")
|
|
711
|
+
});
|
|
712
|
+
return { subject, requested, allowed, denied, request };
|
|
713
|
+
}
|
|
714
|
+
async function resolveConnectionPrincipal(subject, request, authConfig) {
|
|
715
|
+
if (!subject || !authConfig?.resolvePrincipal) {
|
|
716
|
+
return { principal: void 0 };
|
|
717
|
+
}
|
|
718
|
+
try {
|
|
719
|
+
return { principal: await authConfig.resolvePrincipal(subject, request) };
|
|
720
|
+
} catch {
|
|
721
|
+
sseLogger.warn("SSE principal could not be resolved \u2014 connection refused", { subject });
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
async function checkCanSubscribe(router, events, ctx) {
|
|
726
|
+
const verdicts = await Promise.all(events.map((eventName) => askCanSubscribe(eventName, router.events[eventName], ctx)));
|
|
727
|
+
return events.filter((_, i) => verdicts[i]);
|
|
728
|
+
}
|
|
729
|
+
async function askCanSubscribe(eventName, eventDef, ctx) {
|
|
730
|
+
if (!eventDef) {
|
|
731
|
+
return false;
|
|
732
|
+
}
|
|
733
|
+
if (!eventDef._canSubscribe) {
|
|
734
|
+
return true;
|
|
735
|
+
}
|
|
736
|
+
try {
|
|
737
|
+
return await eventDef._canSubscribe(ctx) === true;
|
|
738
|
+
} catch {
|
|
739
|
+
sseLogger.warn("SSE canSubscribe threw \u2014 event denied on this connection", {
|
|
740
|
+
event: eventName,
|
|
741
|
+
subject: ctx.subject
|
|
742
|
+
});
|
|
743
|
+
return false;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
async function runStream(c, stream, admission, config) {
|
|
747
|
+
const { allowed, denied, subject, request } = admission;
|
|
748
|
+
let connectionDead = false;
|
|
749
|
+
let pingTimer;
|
|
750
|
+
let onClosed;
|
|
751
|
+
const cleanup = (reason) => {
|
|
752
|
+
if (connectionDead) return;
|
|
753
|
+
connectionDead = true;
|
|
754
|
+
clearInterval(pingTimer);
|
|
755
|
+
unsubscribeAll(connection);
|
|
756
|
+
config.registry.remove(connection);
|
|
757
|
+
writer.close();
|
|
758
|
+
onClosed?.();
|
|
759
|
+
sseLogger.info("SSE dead connection cleaned up", {
|
|
760
|
+
events: [...connection.allowed],
|
|
761
|
+
reason
|
|
762
|
+
});
|
|
763
|
+
};
|
|
764
|
+
const writer = createBoundedWriter(stream, config.maxQueue, (reason) => {
|
|
765
|
+
sseLogger.warn("SSE connection closed", { events: [...connection.allowed], reason });
|
|
766
|
+
cleanup(reason);
|
|
767
|
+
});
|
|
768
|
+
const connection = {
|
|
769
|
+
subject,
|
|
770
|
+
requested: admission.requested,
|
|
771
|
+
allowed: new Set(allowed),
|
|
772
|
+
denied: new Set(denied),
|
|
773
|
+
unsubscribes: /* @__PURE__ */ new Map(),
|
|
774
|
+
request,
|
|
775
|
+
subscribe: createEventSubscriber({
|
|
776
|
+
router: config.router,
|
|
777
|
+
authConfig: config.authConfig,
|
|
778
|
+
subject,
|
|
779
|
+
request,
|
|
780
|
+
writer,
|
|
781
|
+
isDead: () => connectionDead
|
|
782
|
+
}),
|
|
783
|
+
notify: (frame, eventName) => {
|
|
784
|
+
if (connectionDead) return;
|
|
785
|
+
writer.enqueue({ event: frame, data: JSON.stringify({ event: eventName }) });
|
|
515
786
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
return c.json({ error: "Not authorized for any requested events" }, 403);
|
|
528
|
-
}
|
|
529
|
-
sseLogger.debug("SSE connection requested", {
|
|
530
|
-
events: allowedEvents,
|
|
531
|
-
subject: subject || void 0,
|
|
532
|
-
clientIp: c.req.header("x-forwarded-for") || c.req.header("x-real-ip")
|
|
787
|
+
};
|
|
788
|
+
if (admission.pending?.open(connection) === false) {
|
|
789
|
+
cleanup("aborted during admission");
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
openSubscriptions(connection, writer);
|
|
793
|
+
pingTimer = setInterval(() => {
|
|
794
|
+
if (connectionDead) return;
|
|
795
|
+
writer.enqueue({
|
|
796
|
+
event: "ping",
|
|
797
|
+
data: JSON.stringify({ timestamp: Date.now() })
|
|
533
798
|
});
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
event: eventName,
|
|
580
|
-
messageId
|
|
581
|
-
});
|
|
582
|
-
writer.enqueue({
|
|
583
|
-
id: String(messageId),
|
|
584
|
-
event: eventName,
|
|
585
|
-
data: serializeFrame(eventName, payload)
|
|
586
|
-
});
|
|
587
|
-
});
|
|
588
|
-
unsubscribes.push(unsubscribe);
|
|
799
|
+
}, config.pingInterval);
|
|
800
|
+
await waitForDisconnect(c.req.raw.signal, () => connectionDead, (resolve2) => {
|
|
801
|
+
onClosed = resolve2;
|
|
802
|
+
});
|
|
803
|
+
cleanup();
|
|
804
|
+
}
|
|
805
|
+
function openSubscriptions(connection, writer) {
|
|
806
|
+
writer.enqueue({
|
|
807
|
+
event: "connected",
|
|
808
|
+
data: JSON.stringify({
|
|
809
|
+
subscribedEvents: [...connection.allowed],
|
|
810
|
+
deniedEvents: [...connection.denied],
|
|
811
|
+
timestamp: Date.now()
|
|
812
|
+
})
|
|
813
|
+
});
|
|
814
|
+
for (const eventName of connection.allowed) {
|
|
815
|
+
connection.unsubscribes.set(eventName, connection.subscribe(eventName));
|
|
816
|
+
}
|
|
817
|
+
sseLogger.info("SSE connection established", {
|
|
818
|
+
events: [...connection.allowed],
|
|
819
|
+
deniedEvents: [...connection.denied],
|
|
820
|
+
subscriptionCount: connection.unsubscribes.size
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
async function waitForDisconnect(abortSignal, isDead, onServerClose) {
|
|
824
|
+
if (abortSignal.aborted || isDead()) {
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
await new Promise((resolve2) => {
|
|
828
|
+
const onAbort = () => resolve2();
|
|
829
|
+
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
830
|
+
onServerClose(() => {
|
|
831
|
+
abortSignal.removeEventListener("abort", onAbort);
|
|
832
|
+
resolve2();
|
|
833
|
+
});
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
function createEventSubscriber(ctx) {
|
|
837
|
+
let messageId = 0;
|
|
838
|
+
return (eventName) => {
|
|
839
|
+
const eventDef = ctx.router.events[eventName];
|
|
840
|
+
let active = true;
|
|
841
|
+
const unsubscribe = eventDef.subscribe((payload) => {
|
|
842
|
+
if (!active || ctx.isDead() || !passesFrameFilters(eventName, eventDef, payload, ctx)) {
|
|
843
|
+
return;
|
|
589
844
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
845
|
+
messageId++;
|
|
846
|
+
sseLogger.debug("SSE sending event", {
|
|
847
|
+
event: eventName,
|
|
848
|
+
messageId
|
|
593
849
|
});
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
data: JSON.stringify({ timestamp: Date.now() })
|
|
599
|
-
});
|
|
600
|
-
}, pingInterval);
|
|
601
|
-
const abortSignal = c.req.raw.signal;
|
|
602
|
-
if (!abortSignal.aborted && !connectionDead) {
|
|
603
|
-
await new Promise((resolve2) => {
|
|
604
|
-
const onAbort = () => resolve2();
|
|
605
|
-
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
606
|
-
onClosed = () => {
|
|
607
|
-
abortSignal.removeEventListener("abort", onAbort);
|
|
608
|
-
resolve2();
|
|
609
|
-
};
|
|
610
|
-
});
|
|
611
|
-
}
|
|
612
|
-
cleanup();
|
|
613
|
-
}, async (err) => {
|
|
614
|
-
sseLogger.error("SSE stream error", {
|
|
615
|
-
error: err.message
|
|
850
|
+
ctx.writer.enqueue({
|
|
851
|
+
id: String(messageId),
|
|
852
|
+
event: eventName,
|
|
853
|
+
data: serializeFrame(eventName, payload)
|
|
616
854
|
});
|
|
617
855
|
});
|
|
856
|
+
return () => {
|
|
857
|
+
active = false;
|
|
858
|
+
unsubscribe();
|
|
859
|
+
};
|
|
618
860
|
};
|
|
619
861
|
}
|
|
862
|
+
function passesFrameFilters(eventName, eventDef, payload, ctx) {
|
|
863
|
+
const globalFilter = ctx.authConfig?.filter?.[eventName];
|
|
864
|
+
if (ctx.subject && globalFilter && !globalFilter(ctx.subject, payload)) {
|
|
865
|
+
return false;
|
|
866
|
+
}
|
|
867
|
+
if (!eventDef._filter) {
|
|
868
|
+
return true;
|
|
869
|
+
}
|
|
870
|
+
try {
|
|
871
|
+
return eventDef._filter(payload, { subject: ctx.subject, request: ctx.request }) === true;
|
|
872
|
+
} catch {
|
|
873
|
+
sseLogger.warn("SSE event filter threw \u2014 frame dropped", { event: eventName, subject: ctx.subject });
|
|
874
|
+
return false;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
620
877
|
async function authenticateToken(c, tokenManager) {
|
|
621
878
|
if (!tokenManager) {
|
|
622
879
|
return void 0;
|
|
@@ -632,7 +889,7 @@ function parseRequestedEvents(c) {
|
|
|
632
889
|
if (!eventsParam) {
|
|
633
890
|
return null;
|
|
634
891
|
}
|
|
635
|
-
return eventsParam.split(",").map((e) => e.trim());
|
|
892
|
+
return [...new Set(eventsParam.split(",").map((e) => e.trim()))];
|
|
636
893
|
}
|
|
637
894
|
async function authorizeEvents(subject, requestedEvents, authConfig) {
|
|
638
895
|
if (!subject || !authConfig?.authorize) {
|