@spfn/core 0.3.0-beta.24 → 0.3.0-beta.26

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.
@@ -21,6 +21,18 @@ interface PubSubCache {
21
21
  * Event handler function type
22
22
  */
23
23
  type EventHandler<TPayload> = (payload: TPayload) => void | Promise<void>;
24
+ /**
25
+ * Options for `EventDef.subscribe`.
26
+ */
27
+ interface EventSubscribeOptions {
28
+ /**
29
+ * Run only for an `emit` made in this process, never for a delivery the
30
+ * cache transport received from another pod. For a handler whose work must
31
+ * happen once per emit, not once per pod (it would otherwise run on every
32
+ * pod the event is cached to). Without a cache it runs on every `emit`.
33
+ */
34
+ localOnly?: boolean;
35
+ }
24
36
  /**
25
37
  * Job queue sender function type (used by job module)
26
38
  */
@@ -87,15 +99,24 @@ interface FrameContext {
87
99
  * the connection asked for and never reads storage.
88
100
  */
89
101
  type FramePredicate<TPayload> = (payload: TPayload, ctx: FrameContext) => boolean;
102
+ /**
103
+ * Which of a subject's connections a `grant` / `revoke` reaches: those whose
104
+ * connect query carries every named field with exactly that value. A
105
+ * connection that declared no such field never matches. Plain JSON, so it
106
+ * travels to other pods unchanged.
107
+ */
108
+ interface SubscriptionMatch {
109
+ query: Record<string, string>;
110
+ }
90
111
  /**
91
112
  * How an SSE handler lets `grant` / `revoke` reach its open connections.
92
113
  * Each handler registers one per router key of the event.
93
114
  */
94
115
  interface SubscriptionControl {
95
- /** Activate the event on every open connection of `subject` where it is denied. */
96
- grant: (subject: string) => void;
97
- /** Deactivate the event on every open connection of `subject` where it is allowed. */
98
- revoke: (subject: string) => void;
116
+ /** Activate the event on every open connection of `subject` (matching `match`, when given) where it is denied. */
117
+ grant: (subject: string, match?: SubscriptionMatch) => void;
118
+ /** Deactivate the event on every open connection of `subject` (matching `match`, when given) where it is allowed. */
119
+ revoke: (subject: string, match?: SubscriptionMatch) => void;
99
120
  }
100
121
  /**
101
122
  * Event definition interface
@@ -163,11 +184,13 @@ interface EventDef<TPayload = void> {
163
184
  */
164
185
  filter: (predicate: FramePredicate<TPayload>) => EventDef<TPayload>;
165
186
  /**
166
- * Subscribe to this event (in-memory handler)
187
+ * Subscribe to this event (in-memory handler). With the cache transport a
188
+ * handler runs on every pod for every emit, unless `{ localOnly: true }`
189
+ * keeps it to emits made in this process.
167
190
  */
168
- subscribe: (handler: EventHandler<TPayload>) => () => void;
191
+ subscribe: (handler: EventHandler<TPayload>, options?: EventSubscribeOptions) => () => void;
169
192
  /**
170
- * Unsubscribe all handlers
193
+ * Unsubscribe all handlers, `localOnly` ones included
171
194
  */
172
195
  unsubscribeAll: () => void;
173
196
  /**
@@ -181,16 +204,22 @@ interface EventDef<TPayload = void> {
181
204
  * Reaches every pod when the cache transport is attached. Never re-runs
182
205
  * `canSubscribe`: it is an operator's statement about an open connection.
183
206
  * An event the connection never requested cannot be granted.
207
+ *
208
+ * With `match`, only the connections whose connect query carries every
209
+ * field of `match.query` with that value are activated.
184
210
  */
185
- grant: (subject: string) => Promise<void>;
211
+ grant: (subject: string, match?: SubscriptionMatch) => Promise<void>;
186
212
  /**
187
213
  * Deactivate this event on every open SSE connection of `subject` where it
188
214
  * is allowed; the client receives a `subscription.revoked` frame.
189
215
  *
190
216
  * Reaches every pod when the cache transport is attached. Affects open
191
217
  * connections only — the next connect asks `canSubscribe` afresh.
218
+ *
219
+ * With `match`, only the connections whose connect query carries every
220
+ * field of `match.query` with that value are deactivated.
192
221
  */
193
- revoke: (subject: string) => Promise<void>;
222
+ revoke: (subject: string, match?: SubscriptionMatch) => Promise<void>;
194
223
  /**
195
224
  * Enable cache-based pub/sub for multi-instance support
196
225
  * Must await before emitting events to ensure subscription is ready
@@ -346,4 +375,4 @@ type InferEventPayloads<T extends EventRouterBase<any>> = T['_types'];
346
375
  */
347
376
  declare function defineEventRouter<TEvents extends Record<string, EventDef<any>>>(events: TEvents): EventRouterDef<TEvents>;
348
377
 
349
- export { type EventRouterDef as E, type FrameContext as F, type InferEventNames as I, type JobQueueSender as J, type PubSubCache as P, type SubscribeContext as S, type EventDef as a, type EventContract as b, type EventHandler as c, type EventRouterContract as d, type FramePredicate as e, type InferEventPayload$1 as f, type InferEventPayloads as g, type InferEventPayload as h, type SubscribePredicate as i, type SubscribeRequest as j, defineEventRouter as k, type EventRouterBase as l };
378
+ export { type EventRouterDef as E, type FrameContext as F, type InferEventNames as I, type JobQueueSender as J, type PubSubCache as P, type SubscribeContext as S, type EventDef as a, type EventContract as b, type EventHandler as c, type EventRouterContract as d, type EventSubscribeOptions as e, type FramePredicate as f, type InferEventPayload$1 as g, type InferEventPayloads as h, type InferEventPayload as i, type SubscribePredicate as j, type SubscribeRequest as k, type SubscriptionMatch as l, defineEventRouter as m, type EventRouterBase as n };
@@ -6,9 +6,9 @@ import { NamedMiddleware, Router } from '@spfn/core/route';
6
6
  import { OnErrorContext, ProxyGuardConfig, RateLimitOptions } from '@spfn/core/middleware';
7
7
  import { SafeFetchPolicy } from '@spfn/core/security';
8
8
  import { c as JobRouter, f as BossOptions } from '../boss-BiDCbbAQ.js';
9
- import { E as EventRouterDef, a as EventDef } from '../router-ZDV1ISTo.js';
10
- import { d as SSEHandlerConfig, e as SSEAuthConfig } from '../types-C_g6gD-w.js';
11
- import { W as WSRouterDef, f as WSHandlerConfig, e as WSMessageHandlers, g as WSAuthConfig } from '../types-cwMX0cyJ.js';
9
+ import { E as EventRouterDef, a as EventDef } from '../router-D5B0cpVN.js';
10
+ import { d as SSEHandlerConfig, e as SSEAuthConfig } from '../types-04gu3uMj.js';
11
+ import { W as WSRouterDef, f as WSHandlerConfig, e as WSMessageHandlers, g as WSAuthConfig } from '../types-CqTsDETV.js';
12
12
  import { DatabaseProvider, MigrationStatus, MigrationStatusDb } from '@spfn/core/db';
13
13
  import * as _sinclair_typebox from '@sinclair/typebox';
14
14
  import { Static } from '@sinclair/typebox';
@@ -475,7 +475,7 @@ function createConnectionRegistry() {
475
475
  const bySubject = /* @__PURE__ */ new Map();
476
476
  const pendingBySubject = /* @__PURE__ */ new Map();
477
477
  const beginAdmission = (subject, signal) => {
478
- const changes = /* @__PURE__ */ new Map();
478
+ const changes = [];
479
479
  addToSet(pendingBySubject, subject, changes);
480
480
  const end = endOnce(signal, () => removeFromSet(pendingBySubject, subject, changes));
481
481
  return {
@@ -490,11 +490,15 @@ function createConnectionRegistry() {
490
490
  }
491
491
  };
492
492
  };
493
- const change = (kind, subject, eventName) => {
493
+ const change = (kind, subject, eventName, match) => {
494
494
  const pending = pendingBySubject.get(subject);
495
495
  const connections = bySubject.get(subject);
496
- pending?.forEach((changes) => changes.set(eventName, kind));
497
- connections?.forEach((connection) => (kind === "grant" ? grantOn : revokeOn)(connection, eventName));
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
+ });
498
502
  if (!pending && !connections) {
499
503
  registryLogger.debug("SSE subscription change for a subject with no open connection ignored", {
500
504
  event: eventName,
@@ -512,8 +516,8 @@ function createConnectionRegistry() {
512
516
  /** Open connections only — a pending admission is not counted. */
513
517
  count: (subject) => bySubject.get(subject)?.size ?? 0,
514
518
  pendingCount: (subject) => pendingBySubject.get(subject)?.size ?? 0,
515
- grant: (subject, eventName) => change("grant", subject, eventName),
516
- revoke: (subject, eventName) => change("revoke", subject, eventName)
519
+ grant: (subject, eventName, match) => change("grant", subject, eventName, match),
520
+ revoke: (subject, eventName, match) => change("revoke", subject, eventName, match)
517
521
  };
518
522
  }
519
523
  function endOnce(signal, remove) {
@@ -545,8 +549,14 @@ function removeFromSet(map, key, value) {
545
549
  map.delete(key);
546
550
  }
547
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
+ }
548
555
  function applyRecorded(connection, changes) {
549
- changes.forEach((kind, eventName) => {
556
+ changes.forEach(({ kind, eventName, match }) => {
557
+ if (!matchesRequest(match, connection.request)) {
558
+ return;
559
+ }
550
560
  const [from, to] = kind === "grant" ? [connection.denied, connection.allowed] : [connection.allowed, connection.denied];
551
561
  if (from.delete(eventName)) {
552
562
  to.add(eventName);
@@ -635,8 +645,8 @@ function createSSEHandler(router, config = {}, tokenManager) {
635
645
  function registerSubscriptionControls(router, registry) {
636
646
  for (const eventName of router.eventNames) {
637
647
  router.events[eventName]._registerSubscriptionControl({
638
- grant: (subject) => registry.grant(subject, eventName),
639
- revoke: (subject) => registry.revoke(subject, eventName)
648
+ grant: (subject, match) => registry.grant(subject, eventName, match),
649
+ revoke: (subject, match) => registry.revoke(subject, eventName, match)
640
650
  });
641
651
  }
642
652
  }