@beignet/core 0.0.53 → 0.0.55

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/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # @beignet/core
2
2
 
3
+ ## 0.0.55
4
+
5
+ ### Patch Changes
6
+
7
+ - a3085a6: Allow broadcast connections to configure lifetimes up to one hour while keeping
8
+ the 60-second default. Browser clients honor the server's advertised lifetime
9
+ with bounded watchdog grace, preserve heartbeat timeouts, and reconcile on
10
+ renewal. Align CLI diagnostics and hosting guidance with configurable lifetimes.
11
+ - a3085a6: Coordinate broadcast refreshes with matching pending mutations or custom write
12
+ gates. Add connection admission with automatic resource cleanup, and expose
13
+ connection/reconciliation reasons. Keep the 60-second default, require valid
14
+ lifetime metadata, and use explicit connection-wide renewal control frames.
15
+
16
+ ## 0.0.54
17
+
18
+ ### Patch Changes
19
+
20
+ - b71b34f: Add typed, authorized browser broadcasting with bounded SSE connections, memory
21
+ and Redis providers, reconnect reconciliation, React Query invalidation,
22
+ initiating-client exclusion, notification delivery, and transactional inbox
23
+ scaffolding. Raw routes now preserve context added by route hooks.
24
+
3
25
  ## 0.0.53
4
26
 
5
27
  ### Patch Changes
package/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
  This package provides Beignet's framework primitives: contracts, server runtime,
12
12
  typed client, use cases, agent capabilities, ports, domain helpers, app errors, config, events,
13
13
  idempotency, locks, outbox, mail, notifications, payments, search, webhooks,
14
- feature flags, error reporting, encryption, schedules, uploads, entitlements, pagination
14
+ feature flags, error reporting, encryption, broadcasting, schedules, uploads, entitlements, pagination
15
15
  helpers, testing helpers, and OpenAPI generation.
16
16
 
17
17
  ## Installation
@@ -50,6 +50,9 @@ name the framework area they depend on.
50
50
  | --- | --- |
51
51
  | `@beignet/core/agent-capabilities` | Typed agent capability definitions, registries, validation, and execution |
52
52
  | `@beignet/core/application` | Use case builder and test helpers |
53
+ | `@beignet/core/broadcasting` | Browser-safe typed channel definitions and transport validation |
54
+ | `@beignet/core/broadcasting/server` | BroadcastPort, authorization bindings, registry, and authenticated origins |
55
+ | `@beignet/core/broadcasting/client` | Multiplexed streaming browser client with reconnect reconciliation |
53
56
  | `@beignet/core/client` | Typed HTTP client |
54
57
  | `@beignet/core/client-only` | Static lint marker for modules intended for client-side imports |
55
58
  | `@beignet/core/config` | Environment config validation |
@@ -1176,7 +1179,7 @@ contracts should stay on explicit paths; catch-all contract patterns such as
1176
1179
  For routes that cannot be contracts at all — third-party callback endpoints
1177
1180
  with externally defined request shapes, signature-verified webhooks,
1178
1181
  streaming endpoints that own body consumption —
1179
- `server.rawRoute({ name, method, path, metadata }).handle(fn)` builds a
1182
+ `server.rawRoute({ name, method, path, metadata, hooks }).handle(fn)` builds a
1180
1183
  handler that still runs the whole pipeline (hooks, context creation,
1181
1184
  instrumentation, framework error mapping) without contract parsing or
1182
1185
  validation. The request body stays unconsumed for the handler, `metadata`
@@ -2743,6 +2746,118 @@ field requiredness, descriptions, and constraints.
2743
2746
  - [`@beignet/nuqs`](https://beignetjs.com/nuqs) - URL query state integration with nuqs
2744
2747
  - [`@beignet/devtools`](https://beignetjs.com/devtools) - Local request, provider, and audit timeline
2745
2748
 
2749
+ ## Broadcasting
2750
+
2751
+ Define browser-safe channels with `defineChannel(name, { params, events })` from
2752
+ `@beignet/core/broadcasting`. Parameters parse to flat string records; event
2753
+ schemas produce stable canonical JSON. `BroadcastValidationError` rejects
2754
+ invalid data and non-idempotent transforms.
2755
+
2756
+ On the server, import `BroadcastPort` and `createBroadcasting<AppContext>()`
2757
+ from `@beignet/core/broadcasting/server`. Its `defineChannelBinding` requires an
2758
+ explicit authorization callback for every channel, and `defineChannelRegistry`
2759
+ rejects duplicate names. Use memory or Redis providers and expose the registry
2760
+ with `createBroadcastRoute` from `@beignet/web` or `@beignet/next`.
2761
+
2762
+ ```ts
2763
+ import { defineChannel } from "@beignet/core/broadcasting";
2764
+ import { z } from "zod";
2765
+
2766
+ export const changes = defineChannel("issues.changes", {
2767
+ params: z.object({ workspaceId: z.string() }),
2768
+ events: { changed: z.object({ issueId: z.string() }) },
2769
+ });
2770
+ ```
2771
+
2772
+ `broadcast.publish(changes, { params, event: "changed", data })` resolves on
2773
+ provider acceptance. `broadcast.subscribe(changes, { params, onEvent,
2774
+ onDisconnect })` returns `{ ready, unsubscribe }`; await readiness and cleanup.
2775
+ Publish after commit, or record a publication job in the transaction's outbox
2776
+ when the attempt must be retryable. Browser delivery is ephemeral.
2777
+
2778
+ `createBroadcastClient({ url, headers?, credentials?, fetch? })` from
2779
+ `@beignet/core/broadcasting/client` shares one streaming Fetch connection.
2780
+ Subscribe with `{ params, onEvent, onSync, onError?, onStatusChange? }` and use
2781
+ `onSync` to refetch after initial readiness and recovery. Each subscription
2782
+ exposes `unsubscribe()` and `getStatus()`. The client also exposes `getStatus()`,
2783
+ `resume()`, `close()`, and `getRequestHeaders()` for opt-in origin exclusion.
2784
+ Create a fresh client on user/workspace changes. Callbacks that already started
2785
+ remain application-owned work after unsubscribe.
2786
+
2787
+ `resolveBroadcastOrigin({ headers, principalId, tenantId, namespace })` binds
2788
+ the optional client header to authenticated server identity. Forward a captured
2789
+ origin through a job as `broadcastOrigin`, validate with `broadcastOriginSchema`
2790
+ when using Standard Schema directly, and pass it as `excludeOrigin` to publish.
2791
+ Use the same resolver on the stream endpoint. Missing, malformed, or anonymous
2792
+ origins disable exclusion. Default redaction hides origin headers and fields.
2793
+
2794
+ `defineBroadcastNotificationChannel({ channel, render })` from
2795
+ `@beignet/core/notifications` publishes rendered notifications through the port;
2796
+ return `undefined` to skip. Existing preferences and per-channel retries apply.
2797
+ A sent result means provider acceptance. Persistent inbox writes and their
2798
+ publication jobs must share an app-owned transaction; independent notification
2799
+ channels have no execution ordering guarantee.
2800
+
2801
+ The browser protocol has no replay IDs. Limits are 20 subscriptions, an 8 KiB
2802
+ encoded request, 64 KiB event data, 1 MiB buffers, 25-second heartbeats,
2803
+ 10-second readiness waits, and a default stream lifetime of 60 seconds.
2804
+ `createBroadcastRoute` in `@beignet/next` or `@beignet/web` accepts
2805
+ `maxLifetimeMs` as a positive safe integer up to `3_600_000` (one hour), including
2806
+ `240_000` for four-minute streams. Leave setup/cleanup headroom below your
2807
+ hosting request deadline. Longer streams reduce renewal/refetch frequency and
2808
+ increase the interval between authorization checks.
2809
+
2810
+ The client honors the lifetime advertised in the first readiness message,
2811
+ with five seconds of watchdog grace measured from streaming response receipt.
2812
+ Additional readiness messages and heartbeats never extend the connection
2813
+ deadline. Every readiness message must include a valid `maxLifetimeMs`;
2814
+ missing or invalid lifetimes block subscriptions. Heartbeat loss closes a stream
2815
+ after 55 seconds without data, independently of its lifetime. Planned renewals still invoke `onSync`
2816
+ because events can be missed during reconnection. Terminal 4xx
2817
+ responses block until explicit resume; 429/5xx retry with jitter and respect
2818
+ `Retry-After`. Retry deadlines survive subscription changes. See the
2819
+ [broadcasting guide](https://beignetjs.com/broadcasting).
2820
+
2821
+ ### Diagnose connection changes
2822
+
2823
+ `onSync(info)` and `onStatusChange(status, info)` receive
2824
+ `BroadcastConnectionInfo` from `@beignet/core/broadcasting/client`. Callbacks
2825
+ may omit the metadata argument when they do not need it.
2826
+ `createBroadcastQuerySubscription` forwards status metadata and accepts an
2827
+ optional `onSync(info)` observer; its reconciliation is still scheduled even
2828
+ when that observer throws. This notification describes readiness, not completion
2829
+ of queued or gated query refreshes. Do not refetch from that observer to bypass
2830
+ the gate accidentally.
2831
+
2832
+ | `info.reason` | What Beignet knows |
2833
+ | --- | --- |
2834
+ | `initial` | The first physical connection attempt. |
2835
+ | `planned-renewal` | The server explicitly announced renewal and then ended the stream normally without further traffic. |
2836
+ | `subscription-change` | Adding or removing a distinct channel restarted the multiplexed connection. |
2837
+ | `interruption` | A timeout, transport/protocol failure, or retryable rejection interrupted the connection. |
2838
+ | `unknown` | An unexplained EOF, explicit resume, or browser wakeup has no more precise known cause. |
2839
+
2840
+ Readiness reports the reason for that physical connection; another observer
2841
+ joining an already-ready channel sees the same information. Reconnecting status
2842
+ reports the known reason for restarting. An elapsed lifetime alone never proves
2843
+ planned renewal. Servers send an explicit, connection-wide renewal control frame
2844
+ before closing normally. An unexplained EOF stays `unknown`. A renewal frame
2845
+ followed by a stall produces `interruption` when a watchdog fires. Further traffic
2846
+ after the frame makes a later EOF `unknown`, including an incomplete frame.
2847
+ Reconciliation continues after **every** renewal because events have no replay.
2848
+ Planned renewal is a lifecycle change, not an `onError` failure.
2849
+
2850
+ Routes use the existing `broadcast` instrumentation watcher for admission,
2851
+ connection, and cleanup events. Server closure details report `planned-renewal`,
2852
+ `provider-disconnect`, `buffer-overflow`, `setup-failure`, or `unknown` as known
2853
+ locally; the server cannot infer why a browser cancelled. An optional
2854
+ `instrumentation` sink on `createBroadcastClient` uses the existing
2855
+ `ProviderInstrumentationPort` to record `broadcast.connecting`, `broadcast.ready`,
2856
+ and `broadcast.closed` with browser reasons. These events contain safe counts,
2857
+ durations, and reasons, never headers, credentials, payloads, or channel params.
2858
+ Keep custom logging equally restrained.
2859
+
2860
+
2746
2861
  ## License
2747
2862
 
2748
2863
  MIT
@@ -0,0 +1,45 @@
1
+ import { type ProviderInstrumentationPort } from "../providers/instrumentation.js";
2
+ import { type ChannelDefinition, type InferChannelEvent, type InferChannelParams } from "./index.js";
3
+ export type BroadcastClientStatus = "connecting" | "connected" | "reconnecting" | "blocked" | "closed";
4
+ /** Why this physical connection was opened, or why it is reconnecting. */
5
+ export interface BroadcastConnectionInfo {
6
+ readonly reason: "initial" | "planned-renewal" | "subscription-change" | "interruption" | "unknown";
7
+ }
8
+ /** Safe connection/protocol failure; application callback errors are reported separately. */
9
+ export declare class BroadcastClientError extends Error {
10
+ readonly retryable: boolean;
11
+ readonly status?: number | undefined;
12
+ constructor(message: string, retryable: boolean, status?: number | undefined);
13
+ }
14
+ export interface BroadcastClientSubscription {
15
+ unsubscribe(): void;
16
+ getStatus(): BroadcastClientStatus;
17
+ }
18
+ export interface BroadcastClient {
19
+ subscribe<C extends ChannelDefinition>(channel: C, options: {
20
+ params: InferChannelParams<C>;
21
+ onEvent: (event: InferChannelEvent<C>) => void | Promise<void>;
22
+ /** Refetch authoritative state after initial readiness and every recovered connection. */
23
+ onSync: (info: BroadcastConnectionInfo) => void | Promise<void>;
24
+ onError?: (error: unknown) => void;
25
+ onStatusChange?: (status: BroadcastClientStatus, info: BroadcastConnectionInfo) => void;
26
+ }): BroadcastClientSubscription;
27
+ /** Merge these optional headers into the app's typed HTTP client to opt into exclusion. */
28
+ getRequestHeaders(): Record<string, string>;
29
+ getStatus(): BroadcastClientStatus;
30
+ /** Explicit app action after credentials/access change. Renewals never unblock denied subscriptions. */
31
+ resume(): void;
32
+ close(): void;
33
+ }
34
+ export interface BroadcastClientOptions {
35
+ url: string;
36
+ /** Optional existing instrumentation sink; records causes and counts, never credentials or channel params. */
37
+ instrumentation?: ProviderInstrumentationPort;
38
+ /** Resolved afresh for every request. Never place credentials in the URL. */
39
+ headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
40
+ credentials?: RequestCredentials;
41
+ fetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
42
+ }
43
+ /** One multiplexed streaming Fetch connection per instance. Create an instance per app auth session. */
44
+ export declare function createBroadcastClient(options: BroadcastClientOptions): BroadcastClient;
45
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/broadcasting/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,2BAA2B,EACjC,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAGL,KAAK,iBAAiB,EAEtB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EAGxB,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,qBAAqB,GAC7B,YAAY,GACZ,WAAW,GACX,cAAc,GACd,SAAS,GACT,QAAQ,CAAC;AAEb,0EAA0E;AAC1E,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,MAAM,EACX,SAAS,GACT,iBAAiB,GACjB,qBAAqB,GACrB,cAAc,GACd,SAAS,CAAC;CACf;AAED,6FAA6F;AAC7F,qBAAa,oBAAqB,SAAQ,KAAK;IAG3C,QAAQ,CAAC,SAAS,EAAE,OAAO;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM;gBAFxB,OAAO,EAAE,MAAM,EACN,SAAS,EAAE,OAAO,EAClB,MAAM,CAAC,EAAE,MAAM,YAAA;CAK3B;AAED,MAAM,WAAW,2BAA2B;IAC1C,WAAW,IAAI,IAAI,CAAC;IACpB,SAAS,IAAI,qBAAqB,CAAC;CACpC;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,CAAC,CAAC,SAAS,iBAAiB,EACnC,OAAO,EAAE,CAAC,EACV,OAAO,EAAE;QACP,MAAM,EAAE,kBAAkB,CAAC,CAAC,CAAC,CAAC;QAC9B,OAAO,EAAE,CAAC,KAAK,EAAE,iBAAiB,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAC/D,0FAA0F;QAC1F,MAAM,EAAE,CAAC,IAAI,EAAE,uBAAuB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAChE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;QACnC,cAAc,CAAC,EAAE,CACf,MAAM,EAAE,qBAAqB,EAC7B,IAAI,EAAE,uBAAuB,KAC1B,IAAI,CAAC;KACX,GACA,2BAA2B,CAAC;IAC/B,2FAA2F;IAC3F,iBAAiB,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5C,SAAS,IAAI,qBAAqB,CAAC;IACnC,wGAAwG;IACxG,MAAM,IAAI,IAAI,CAAC;IACf,KAAK,IAAI,IAAI,CAAC;CACf;AAED,MAAM,WAAW,sBAAsB;IACrC,GAAG,EAAE,MAAM,CAAC;IACZ,8GAA8G;IAC9G,eAAe,CAAC,EAAE,2BAA2B,CAAC;IAC9C,6EAA6E;IAC7E,OAAO,CAAC,EAAE,WAAW,GAAG,CAAC,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC;IACnE,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,GAAG,GAAG,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC7E;AA6DD,wGAAwG;AACxG,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,sBAAsB,GAC9B,eAAe,CAusBjB"}