@spfn/core 0.3.0-beta.22 → 0.3.0-beta.24
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/dist/authz/index.js +46 -10
- package/dist/authz/index.js.map +1 -1
- 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/errors/index.d.ts +18 -1
- package/dist/errors/index.js +46 -10
- package/dist/errors/index.js.map +1 -1
- package/dist/event/index.d.ts +3 -3
- package/dist/event/index.js +138 -0
- 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 +356 -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/middleware/index.js +46 -10
- package/dist/middleware/index.js.map +1 -1
- package/dist/{router-CysBmgv3.d.ts → router-ZDV1ISTo.d.ts} +118 -1
- package/dist/server/index.d.ts +3 -3
- package/dist/server/index.js +356 -109
- package/dist/server/index.js.map +1 -1
- package/dist/{types-B8zAxFsE.d.ts → types-C_g6gD-w.d.ts} +27 -2
- package/dist/{types-W4u_S0lF.d.ts → types-cwMX0cyJ.d.ts} +1 -1
- package/package.json +1 -1
- package/src/contract/README.md +5 -3
- package/src/errors/README.md +16 -0
- package/src/event/README.md +128 -8
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Context } from 'hono';
|
|
2
|
-
import { E as EventRouterDef, I as InferEventNames,
|
|
2
|
+
import { E as EventRouterDef, I as InferEventNames, h as InferEventPayload, j as SubscribeRequest } from './router-ZDV1ISTo.js';
|
|
3
3
|
import { c as SSETokenStore, S as SSETokenManager } from './token-manager-Ckjt2-qq.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -79,6 +79,17 @@ interface SSEHandlerAuthConfig {
|
|
|
79
79
|
* Return false to skip sending the event to this user.
|
|
80
80
|
*/
|
|
81
81
|
filter?: Record<string, (subject: string, payload: unknown) => boolean>;
|
|
82
|
+
/**
|
|
83
|
+
* Resolve the principal every `.canSubscribe()` of a connection receives
|
|
84
|
+
* (called once per connection, after the subject is known).
|
|
85
|
+
*
|
|
86
|
+
* The stream request carries no `AuthContext`: the subject comes from the
|
|
87
|
+
* one-time token, and the stream is mounted without the app's middleware.
|
|
88
|
+
* So an app that decides on roles or memberships loads them here, once,
|
|
89
|
+
* instead of in every predicate. Absent → predicates see
|
|
90
|
+
* `principal: undefined`. A throw refuses the connection with 403.
|
|
91
|
+
*/
|
|
92
|
+
resolvePrincipal?: (subject: string, request: SubscribeRequest) => unknown | Promise<unknown>;
|
|
82
93
|
}
|
|
83
94
|
/**
|
|
84
95
|
* SSE auth configuration (user-facing, generic)
|
|
@@ -114,6 +125,7 @@ interface SSEAuthConfig<TRouter extends EventRouterDef<any>> {
|
|
|
114
125
|
filter?: {
|
|
115
126
|
[K in InferEventNames<TRouter>]?: (subject: string, payload: InferEventPayload<TRouter, K>) => boolean;
|
|
116
127
|
};
|
|
128
|
+
resolvePrincipal?: (subject: string, request: SubscribeRequest) => unknown | Promise<unknown>;
|
|
117
129
|
}
|
|
118
130
|
/**
|
|
119
131
|
* SSE Handler configuration
|
|
@@ -270,6 +282,19 @@ interface SSESubscribeOptions<TRouter extends EventRouterDef<any>> {
|
|
|
270
282
|
* Called when reconnecting
|
|
271
283
|
*/
|
|
272
284
|
onReconnect?: (attempt: number) => void;
|
|
285
|
+
/**
|
|
286
|
+
* Called when the server grants or revokes one event on the open connection
|
|
287
|
+
* (`subscription.granted` / `subscription.revoked` frames). Only events in
|
|
288
|
+
* `events` can change: an event never requested cannot be granted.
|
|
289
|
+
*/
|
|
290
|
+
onSubscriptionChange?: (change: SSESubscriptionChange<TRouter>) => void;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* A `grant` / `revoke` the server applied to the open connection.
|
|
294
|
+
*/
|
|
295
|
+
interface SSESubscriptionChange<TRouter extends EventRouterDef<any>> {
|
|
296
|
+
event: InferEventNames<TRouter>;
|
|
297
|
+
status: 'granted' | 'revoked';
|
|
273
298
|
}
|
|
274
299
|
/**
|
|
275
300
|
* SSE connection state
|
|
@@ -280,4 +305,4 @@ type SSEConnectionState = 'connecting' | 'open' | 'closed' | 'error';
|
|
|
280
305
|
*/
|
|
281
306
|
type SSEUnsubscribe = () => void;
|
|
282
307
|
|
|
283
|
-
export type { SSEClientConfig as S, SSESubscribeOptions as a, SSEUnsubscribe as b, SSEConnectionState as c, SSEHandlerConfig as d, SSEAuthConfig as e, SSEEventHandler as f, SSEEventHandlers as g, SSEHandlerAuthConfig as h, SSEMessage as i };
|
|
308
|
+
export type { SSEClientConfig as S, SSESubscribeOptions as a, SSEUnsubscribe as b, SSEConnectionState as c, SSEHandlerConfig as d, SSEAuthConfig as e, SSEEventHandler as f, SSEEventHandlers as g, SSEHandlerAuthConfig as h, SSEMessage as i, SSESubscriptionChange as j };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Context } from 'hono';
|
|
2
|
-
import { a as EventDef,
|
|
2
|
+
import { a as EventDef, l as EventRouterBase, I as InferEventNames, h as InferEventPayload } from './router-ZDV1ISTo.js';
|
|
3
3
|
import { c as SSETokenStore, S as SSETokenManager } from './token-manager-Ckjt2-qq.js';
|
|
4
4
|
|
|
5
5
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spfn/core",
|
|
3
|
-
"version": "0.3.0-beta.
|
|
3
|
+
"version": "0.3.0-beta.24",
|
|
4
4
|
"description": "Full-stack TypeScript backend for Next.js: file-based typed routes, Drizzle entities and repositories, PostgreSQL transactions and a generated end-to-end client, in one fixed vertical slice per feature",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
package/src/contract/README.md
CHANGED
|
@@ -235,7 +235,8 @@ throws instead of one declaration silently winning. A router `streamPath` that d
|
|
|
235
235
|
a client must exchange a token first;
|
|
236
236
|
- a contracted event without a payload schema (`defineEvent(name)`) — its frame carries no `data`;
|
|
237
237
|
declare `Type.Object({})` and emit `{}` instead;
|
|
238
|
-
- a contracted event under the key `connected
|
|
238
|
+
- a contracted event under the key `connected`, `ping`, `subscription.granted` or
|
|
239
|
+
`subscription.revoked` — those are the stream's own frames;
|
|
239
240
|
- a contracted event with no `since`.
|
|
240
241
|
|
|
241
242
|
**What a reader of the document needs.**
|
|
@@ -254,8 +255,9 @@ throws instead of one declaration silently winning. A router `streamPath` that d
|
|
|
254
255
|
`EventDef` registered under two keys is two items.
|
|
255
256
|
- A frame's `data` is the JSON `{ "event": name, "data": payload }`, with `payload` matching the
|
|
256
257
|
item's schema.
|
|
257
|
-
- `connected` (once, when the stream opens)
|
|
258
|
-
are
|
|
258
|
+
- `connected` (once, when the stream opens), `ping` (keep-alive), `subscription.granted` and
|
|
259
|
+
`subscription.revoked` (an event activated or deactivated on the open connection) frames are the
|
|
260
|
+
stream's own and are not items.
|
|
259
261
|
- With `auth: "tokenExchange"` the client first posts to `tokenPath`, then opens
|
|
260
262
|
`streamPath?events=…&token=…`. `tokenPath` is derived from `streamPath` exactly as the server
|
|
261
263
|
derives it.
|
package/src/errors/README.md
CHANGED
|
@@ -276,6 +276,22 @@ const maybe = registry.tryDeserialize(body); // null if __type missing/unknown (
|
|
|
276
276
|
`getRegisteredTypes()`. Constructor accepts an array mixing single classes, arrays of
|
|
277
277
|
classes, and other `ErrorRegistry` instances (`ErrorRegistryInput[]`).
|
|
278
278
|
|
|
279
|
+
### Minified browser bundles
|
|
280
|
+
|
|
281
|
+
A production browser bundle renames classes — `PaymentFailedError` becomes `Q` — but keeps the
|
|
282
|
+
string a constructor assigns to `this.name`. The server is not minified, so `__type` still
|
|
283
|
+
carries the original class name. `append()` therefore keys each class by its constructor name
|
|
284
|
+
and also by the `name` its instances set for themselves, read once by building the class with
|
|
285
|
+
`{}`. A lookup tries the constructor name first, so an unminified registry resolves exactly as
|
|
286
|
+
it always has.
|
|
287
|
+
|
|
288
|
+
For a class to be restored in a minified browser:
|
|
289
|
+
|
|
290
|
+
- **Set `this.name` to the class name in the constructor.** A class that leaves it unset only
|
|
291
|
+
inherits its parent's name, and is not keyed under it — that key belongs to the parent.
|
|
292
|
+
- **Let the constructor build with `{}`.** One that throws without its fields is keyed by its
|
|
293
|
+
constructor name only.
|
|
294
|
+
|
|
279
295
|
---
|
|
280
296
|
|
|
281
297
|
## Custom errors
|
package/src/event/README.md
CHANGED
|
@@ -45,8 +45,12 @@ From `@spfn/core/event`:
|
|
|
45
45
|
- `.contract({ since, deprecatedIn?, removedIn? })` on an event and `.contract({ auth, streamPath? })`
|
|
46
46
|
on an event router — publish the event in `contracts/current.json` (see
|
|
47
47
|
[Contracted events](#contracted-events-for-separately-compiled-clients))
|
|
48
|
+
- `.canSubscribe(predicate)`, `.filter(predicate)`, `.grant(subject)`, `.revoke(subject)` on an
|
|
49
|
+
event — who may subscribe over SSE, which frames a connection receives, and changing an open
|
|
50
|
+
connection (see [Per-event permission](#per-event-permission-cansubscribe-filter-grant--revoke))
|
|
48
51
|
- Types: `EventDef`, `EventHandler`, `InferEventPayload`, `PubSubCache`, `JobQueueSender`,
|
|
49
|
-
`EventContract`, `EventRouterContract`,
|
|
52
|
+
`EventContract`, `EventRouterContract`, `SubscribePredicate`, `SubscribeContext`,
|
|
53
|
+
`SubscribeRequest`, `FramePredicate`, `FrameContext`,
|
|
50
54
|
`EventRouterDef`, `InferEventNames`, `InferEventPayloads`,
|
|
51
55
|
`InferRouterEventPayload` (the two-arg router variant — see note)
|
|
52
56
|
|
|
@@ -56,8 +60,8 @@ From `@spfn/core/event/sse`:
|
|
|
56
60
|
- `SSETokenManager` (class), `CacheTokenStore` (class)
|
|
57
61
|
- Types: `SSEToken`, `SSETokenStore`, `SSETokenManagerConfig`, `SSEMessage`,
|
|
58
62
|
`SSEHandlerConfig`, `SSEHandlerAuthConfig`, `SSEAuthConfig`, `SSEClientConfig`,
|
|
59
|
-
`SSEEventHandler`, `SSEEventHandlers`, `SSESubscribeOptions`, `
|
|
60
|
-
`SSEUnsubscribe`
|
|
63
|
+
`SSEEventHandler`, `SSEEventHandlers`, `SSESubscribeOptions`, `SSESubscriptionChange`,
|
|
64
|
+
`SSEConnectionState`, `SSEUnsubscribe`
|
|
61
65
|
|
|
62
66
|
From `@spfn/core/event/sse/client`:
|
|
63
67
|
|
|
@@ -133,10 +137,16 @@ Promise<void>` otherwise. Handlers run via `Promise.allSettled` — see
|
|
|
133
137
|
| `emit(payload?)` | `Promise<void>` | fan-out to handlers (or cache) + job queues |
|
|
134
138
|
| `useCache(cache)` | `(PubSubCache) => Promise<EventDef>` | enable cross-instance broadcast (must `await`) |
|
|
135
139
|
| `contract(contract)` | `(EventContract) => EventDef` | publish in the contract; returns the same event; a second call throws |
|
|
140
|
+
| `canSubscribe(predicate)` | `<P>(SubscribePredicate<P>) => EventDef` | who may subscribe over SSE (once per connection); a second call throws |
|
|
141
|
+
| `filter(predicate)` | `(FramePredicate<TPayload>) => EventDef` | which frames a connection receives (sync, per frame); a second call throws |
|
|
142
|
+
| `grant(subject)` | `Promise<void>` | activate the event on the subject's open SSE connections where it is denied |
|
|
143
|
+
| `revoke(subject)` | `Promise<void>` | deactivate the event on the subject's open SSE connections where it is allowed |
|
|
136
144
|
|
|
137
|
-
> `_registerJobQueue` and `_payload` exist on the interface but
|
|
138
|
-
> `_registerJobQueue` is called by `@spfn/core/job` when a job does
|
|
139
|
-
>
|
|
145
|
+
> `_registerJobQueue`, `_registerSubscriptionControl` and `_payload` exist on the interface but
|
|
146
|
+
> are **internal** — `_registerJobQueue` is called by `@spfn/core/job` when a job does
|
|
147
|
+
> `.on(event)`; `_registerSubscriptionControl` by `createSSEHandler` so `grant` / `revoke` reach
|
|
148
|
+
> its connections; `_payload` is a type-inference carrier (always `undefined` at runtime). Don't
|
|
149
|
+
> call them.
|
|
140
150
|
|
|
141
151
|
---
|
|
142
152
|
|
|
@@ -262,8 +272,10 @@ Client Server
|
|
|
262
272
|
│ GET /events/stream?token=…&events=…
|
|
263
273
|
│ ───────────────────────────────► │ verify+consume token (one-time)
|
|
264
274
|
│ │ → authorize(subject, events)
|
|
265
|
-
│
|
|
266
|
-
│
|
|
275
|
+
│ │ → resolvePrincipal(subject, request) (once)
|
|
276
|
+
│ │ → event.canSubscribe(ctx) (once per event)
|
|
277
|
+
│ ◄═══════════════════════════════ │ SSE stream opens (connected: subscribed + denied)
|
|
278
|
+
│ event: <name> / data: {…} │ ← filter(subject, payload) + event.filter(payload, ctx) per emission
|
|
267
279
|
```
|
|
268
280
|
|
|
269
281
|
Enable it with `auth: { enabled: true }`:
|
|
@@ -381,6 +393,101 @@ Return `false` to skip. Payload is typed per event.
|
|
|
381
393
|
})
|
|
382
394
|
```
|
|
383
395
|
|
|
396
|
+
### Per-event permission (`canSubscribe`, `filter`, `grant` / `revoke`)
|
|
397
|
+
|
|
398
|
+
The global hooks above are one function for the whole router. When the rule belongs to one event,
|
|
399
|
+
declare it where the event is defined:
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
export const auditLogged = defineEvent('audit.logged', payload)
|
|
403
|
+
.canSubscribe<AuthPrincipal>(({ subject, principal, request }) => principal?.role === 'admin');
|
|
404
|
+
|
|
405
|
+
export const runtimeChanged = defineEvent('runtime.changed', payload)
|
|
406
|
+
.filter((payload, { subject, request }) => payload.workspaceId === request.query.workspaceId);
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
- **`canSubscribe(ctx)`** — once per connection per requested event, only when the connection has a
|
|
410
|
+
subject. `true` allows; `false` or a throw denies (a throw is logged at warn with the event name).
|
|
411
|
+
A denial never refuses the connection: an event denied here is listed in the `connected` frame's
|
|
412
|
+
`deniedEvents`, and a connection whose every event is denied stays open with zero subscriptions,
|
|
413
|
+
so a later `grant` can activate one. May be async.
|
|
414
|
+
- **`filter(payload, ctx)`** — per frame, with or without a subject; it matches the payload against
|
|
415
|
+
the connect request. It runs beside the global `filter` map — both must return `true`. Sync. A
|
|
416
|
+
throw drops that frame (warn) and leaves the connection alive.
|
|
417
|
+
- **`request`** in both is a snapshot taken once at connect: `{ query, headers }` with lowercased
|
|
418
|
+
header names — never the live Hono context. It is the connect request as sent: it holds the
|
|
419
|
+
`cookie` / `authorization` headers and the consumed one-time `token` in `query`, so the app must
|
|
420
|
+
never log the snapshot.
|
|
421
|
+
- Both are set once per event: an `EventDef` is a shared singleton, so a second call throws.
|
|
422
|
+
|
|
423
|
+
**`resolvePrincipal` — why the stream has no `AuthContext`.** The subject comes from the one-time
|
|
424
|
+
token, and the stream is mounted without the app's middleware, so there is no request auth object
|
|
425
|
+
to hand a predicate. `auth.resolvePrincipal(subject, request)` loads it instead, **once per
|
|
426
|
+
connection**, after the subject is known and before any `canSubscribe`; every predicate of that
|
|
427
|
+
connection receives the same value as `principal` (type it with `.canSubscribe<P>()`). Absent →
|
|
428
|
+
`principal: undefined`. A throw refuses the connection with `403 { error: 'Principal could not be
|
|
429
|
+
resolved' }`.
|
|
430
|
+
|
|
431
|
+
```typescript
|
|
432
|
+
.events(eventRouter, {
|
|
433
|
+
auth: {
|
|
434
|
+
enabled: true,
|
|
435
|
+
resolvePrincipal: async (subject) => principalsRepository.findBySubject(subject),
|
|
436
|
+
},
|
|
437
|
+
})
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
**Order.** `authorize` (an empty result is still `403`) → `canSubscribe` of each event `authorize`
|
|
441
|
+
kept → per frame, the global `filter` and the event's `filter`. An event `authorize` dropped is
|
|
442
|
+
never asked and counts as denied too.
|
|
443
|
+
|
|
444
|
+
**Cost.** At connect: one predicate call per requested event (plus one `resolvePrincipal`). Per
|
|
445
|
+
frame: sync filters only — never storage. The handler sets no timeout on them: a hanging
|
|
446
|
+
`canSubscribe` or `resolvePrincipal` holds the connect open, so the app owns the timeout. A
|
|
447
|
+
client that leaves meanwhile is forgotten at once; the late verdict opens nothing.
|
|
448
|
+
|
|
449
|
+
**`grant` / `revoke` — changing an open connection.**
|
|
450
|
+
|
|
451
|
+
```typescript
|
|
452
|
+
await workspaceRuntime.revoke(userId); // membership ended
|
|
453
|
+
await workspaceRuntime.grant(userId); // membership restored
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
- `revoke(subject)` — on every open connection of the subject where the event is allowed: drop the
|
|
457
|
+
subscription and send a `subscription.revoked` frame `{ event }` (the router key). `grant(subject)`
|
|
458
|
+
— where it is denied: subscribe (the same code path as at connect, with the stored request
|
|
459
|
+
snapshot) and send `subscription.granted`. Anything else is a no-op, and no connection resolves
|
|
460
|
+
without error.
|
|
461
|
+
- With the cache transport attached (`CACHE_URL`), the call applies on this pod and is published
|
|
462
|
+
once on the control channel `<channelPrefix>$control`; every other pod applies it to its own
|
|
463
|
+
connections. Without a cache it is local only.
|
|
464
|
+
- Three rules:
|
|
465
|
+
1. **Open connections only.** Neither re-runs `canSubscribe` or `authorize` — they are an
|
|
466
|
+
operator's statement about connections that exist now.
|
|
467
|
+
2. **Permission lives in app data.** The next connect asks `canSubscribe` afresh, so a missed
|
|
468
|
+
call delays until the client reconnects; it never produces a wrong answer after. A call that
|
|
469
|
+
lands while a connect is still in `authorize` / `resolvePrincipal` / `canSubscribe` is not
|
|
470
|
+
missed: it is recorded and applied over the predicates' verdict when that connection opens
|
|
471
|
+
(the last call wins; no frame — the `connected` frame already lists the result).
|
|
472
|
+
3. **An event never requested cannot be granted.** The connection only knows the events in its
|
|
473
|
+
`?events=` list — request every event the page may need, and let `canSubscribe` deny.
|
|
474
|
+
- A frame already enqueued before a `revoke` may still be written; none after it is.
|
|
475
|
+
- WebSocket routers are not covered.
|
|
476
|
+
|
|
477
|
+
`subscription.granted` and `subscription.revoked` are stream-owned frames like `connected` and
|
|
478
|
+
`ping`: not in the contract, and reserved as router keys of contracted events. On the client:
|
|
479
|
+
|
|
480
|
+
```typescript
|
|
481
|
+
client.subscribe({
|
|
482
|
+
events: ['runtimeChanged'],
|
|
483
|
+
handlers: { runtimeChanged: (p) => update(p) },
|
|
484
|
+
onSubscriptionChange: ({ event, status }) =>
|
|
485
|
+
{
|
|
486
|
+
// event: 'runtimeChanged' (typed); status: 'granted' | 'revoked'
|
|
487
|
+
},
|
|
488
|
+
});
|
|
489
|
+
```
|
|
490
|
+
|
|
384
491
|
### SSE auth config (`SSEAuthConfig<TRouter>`)
|
|
385
492
|
|
|
386
493
|
| Option | Type | Default | Description |
|
|
@@ -392,6 +499,7 @@ Return `false` to skip. Payload is typed per event.
|
|
|
392
499
|
| `getSubject` | `(c: Context) => string \| null` | `c.get('auth')?.userId ?? null` | extract subject on the token endpoint |
|
|
393
500
|
| `authorize` | `(subject, events[]) => events[] \| Promise<…>` | — | subscription authorization (once on connect) |
|
|
394
501
|
| `filter` | `{ [event]?: (subject, payload) => boolean }` | — | per-event payload filter |
|
|
502
|
+
| `resolvePrincipal` | `(subject, request) => unknown \| Promise<unknown>` | — | principal for every `canSubscribe` (once per connection); a throw ⇒ `403` |
|
|
395
503
|
|
|
396
504
|
---
|
|
397
505
|
|
|
@@ -742,8 +850,20 @@ interface EventDef<TPayload = void> {
|
|
|
742
850
|
useCache: (cache: PubSubCache) => Promise<EventDef<TPayload>>;
|
|
743
851
|
readonly _contract?: EventContract;
|
|
744
852
|
contract: (contract: EventContract) => EventDef<TPayload>;
|
|
853
|
+
readonly _canSubscribe?: SubscribePredicate<any>;
|
|
854
|
+
canSubscribe: <P = unknown>(predicate: SubscribePredicate<P>) => EventDef<TPayload>;
|
|
855
|
+
readonly _filter?: FramePredicate<TPayload>;
|
|
856
|
+
filter: (predicate: FramePredicate<TPayload>) => EventDef<TPayload>;
|
|
857
|
+
grant: (subject: string) => Promise<void>;
|
|
858
|
+
revoke: (subject: string) => Promise<void>;
|
|
745
859
|
}
|
|
746
860
|
|
|
861
|
+
interface SubscribeRequest { query: Record<string, string>; headers: Record<string, string>; }
|
|
862
|
+
interface SubscribeContext<P = unknown> { subject: string; principal: P | undefined; request: SubscribeRequest; }
|
|
863
|
+
type SubscribePredicate<P = unknown> = (ctx: SubscribeContext<P>) => boolean | Promise<boolean>;
|
|
864
|
+
interface FrameContext { subject: string | undefined; request: SubscribeRequest; }
|
|
865
|
+
type FramePredicate<TPayload> = (payload: TPayload, ctx: FrameContext) => boolean;
|
|
866
|
+
|
|
747
867
|
interface EventContract { since: string; deprecatedIn?: string; removedIn?: string; }
|
|
748
868
|
|
|
749
869
|
type EventHandler<TPayload> = (payload: TPayload) => void | Promise<void>;
|