@opinionated-machine/sse-fallback 0.1.1

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scheduler.js","sourceRoot":"","sources":["../src/scheduler.ts"],"names":[],"mappings":"AASA,uCAAuC;AACvC,MAAM,OAAO,eAAe;IAClB,MAAM,CAAyB;IACtB,MAAM,CAAY;IAEnC,YAAY,MAAkB;QAC5B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;IACtB,CAAC;IAED,kEAAkE;IAClE,GAAG,CAAC,OAAe;QACjB,IAAI,CAAC,KAAK,EAAE,CAAA;QACZ,IAAI,CAAC,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,CAE7C;QAAC,IAAI,CAAC,MAAiC,CAAC,KAAK,EAAE,EAAE,CAAA;IACpD,CAAC;IAED,KAAK;QACH,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;YAC9B,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;YACzB,IAAI,CAAC,MAAM,GAAG,SAAS,CAAA;QACzB,CAAC;IACH,CAAC;IAED,IAAI,OAAO;QACT,OAAO,IAAI,CAAC,MAAM,KAAK,SAAS,CAAA;IAClC,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,MAAqB,EAAE,OAAe,EAAE,MAAoB;IACvF,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,IAAI,OAAO,CAAC,CAAA;IAChF,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,GAAG,OAAO,CAAC,CAAC,CAAA;AACpD,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,KAAK,CAAC,OAAe,EAAE,MAAmB;IACxD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACnB,OAAO,CAAC,KAAK,CAAC,CAAA;YACd,OAAM;QACR,CAAC;QACD,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YAC5B,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;YAC5C,OAAO,CAAC,IAAI,CAAC,CAAA;QACf,CAAC,EAAE,OAAO,CAAC,CACV;QAAC,KAAgC,CAAC,KAAK,EAAE,EAAE,CAAA;QAC5C,MAAM,OAAO,GAAG,GAAG,EAAE;YACnB,YAAY,CAAC,KAAK,CAAC,CAAA;YACnB,OAAO,CAAC,KAAK,CAAC,CAAA;QAChB,CAAC,CAAA;QACD,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAA;IAC3D,CAAC,CAAC,CAAA;AACJ,CAAC"}
@@ -0,0 +1,164 @@
1
+ import type { FallbackBinding, FallbackRequestParams } from './binding.ts';
2
+ import type { EventPayloadMap, FallbackEvent, FallbackPolicy } from './bindingTypes.ts';
3
+ import type { PollGate } from './pollGate.ts';
4
+ import type { InvalidVersionInfo, VersionGap } from './reconciler.ts';
5
+ import type { FallbackTransport } from './transport.ts';
6
+ export type SubscriptionStatus = 'connecting' | 'live' | 'reconnecting' | 'polling' | 'stopped';
7
+ /**
8
+ * Why a subscription reached `'stopped'`.
9
+ *
10
+ * `'stopped'` alone cannot be acted on: a completed job, an auth failure and a
11
+ * caller's own `stop()` all land there. The reason is what lets a UI tell
12
+ * success from give-up, and give-up from "the user navigated away".
13
+ *
14
+ * - `'terminal-event'` — a terminal event was delivered. Success.
15
+ * - `'unretryable-status'` — the stream or a poll was refused with a status in
16
+ * `unretryableStatuses` (and `onAuthChallenge`, if any, did not recover).
17
+ * `status` carries which one.
18
+ * - `'budget-exhausted'` — `subscriptionBudget` ran out. `limit` says which
19
+ * half. Show an actionable error and offer a manual retry.
20
+ * - `'manual'` — the caller called `stop()`, or the `signal` passed at
21
+ * creation aborted.
22
+ */
23
+ export type StopReason = 'terminal-event' | 'unretryable-status' | 'budget-exhausted' | 'manual';
24
+ /** The reason a subscription stopped, plus whatever detail that reason carries. */
25
+ export type SubscriptionStopDetail = {
26
+ reason: StopReason;
27
+ /** The refusing HTTP status, for `'unretryable-status'`. */
28
+ status?: number;
29
+ /** Which half of the budget ran out, for `'budget-exhausted'`. */
30
+ limit?: 'maxDurationMs' | 'maxPolls';
31
+ /** Which channel hit the refusal, for `'unretryable-status'`. */
32
+ channel?: 'poll' | 'stream';
33
+ };
34
+ /**
35
+ * Rejection thrown by `waitFor` / `waitForTerminal` when the subscription
36
+ * stops before the awaited event arrives. Carries the stop reason so the
37
+ * caller can branch without inspecting the message.
38
+ */
39
+ export declare class SubscriptionStoppedError extends Error {
40
+ readonly reason: StopReason;
41
+ readonly status: number | undefined;
42
+ readonly limit: 'maxDurationMs' | 'maxPolls' | undefined;
43
+ readonly channel: 'poll' | 'stream' | undefined;
44
+ constructor(detail: SubscriptionStopDetail);
45
+ }
46
+ /**
47
+ * Observability hooks — all optional, all no-ops by default. None of these
48
+ * affect delivery semantics; they exist so applications can meter the
49
+ * fallback machinery (gap rate, duplicate rate, poll errors).
50
+ */
51
+ export type FallbackDiagnostics = {
52
+ onGap?: (gap: VersionGap) => void;
53
+ onDuplicate?: (event: string) => void;
54
+ onStaleSnapshot?: () => void;
55
+ onPollError?: (error: unknown) => void;
56
+ onStreamError?: (error: unknown) => void;
57
+ /**
58
+ * A gap suspended the state layer: `getState()` is frozen at its pre-gap
59
+ * value until a snapshot repairs it, even though events keep flowing.
60
+ */
61
+ onStateSuspended?: (gap: VersionGap) => void;
62
+ /** A snapshot lifted the suspension and re-initialized state. */
63
+ onStateRepaired?: () => void;
64
+ /** A listener passed to `onEvent` / `onStateChange` / `onStatusChange` threw. */
65
+ onListenerError?: (error: unknown) => void;
66
+ /**
67
+ * A version extractor returned something the gate cannot order, so the item
68
+ * was delivered without advancing the watermark. Delivery keeps working
69
+ * (at-least-once, no deduplication between the channels), but the binding
70
+ * is misconfigured or the payload is missing its version field — this hook
71
+ * is the only signal that says so.
72
+ */
73
+ onInvalidVersion?: (info: InvalidVersionInfo) => void;
74
+ };
75
+ export type CreateResilientSubscriptionOptions = {
76
+ transport: FallbackTransport;
77
+ params?: FallbackRequestParams;
78
+ policy?: Partial<FallbackPolicy>;
79
+ diagnostics?: FallbackDiagnostics;
80
+ signal?: AbortSignal;
81
+ /**
82
+ * Shared cap and stagger for reconciliation polls across subscriptions —
83
+ * see `createPollGate`. Without one, a fleet-wide reconnect fires every
84
+ * subscription's reconciliation poll on the same tick.
85
+ */
86
+ pollGate?: PollGate;
87
+ /**
88
+ * Called when a poll or stream connect is refused with a status in
89
+ * `policy.authChallengeStatuses` (default `[401]`). Refresh credentials
90
+ * here — the transport builds every request fresh, so a token stored on the
91
+ * transport is picked up by the retry.
92
+ *
93
+ * Resolve `true` to run the refused request once more; resolve `false`, or
94
+ * throw, to stop the subscription with `'unretryable-status'`. The retry is
95
+ * granted once per auth failure streak: a second refusal with no successful
96
+ * request in between stops the subscription.
97
+ */
98
+ onAuthChallenge?: (challenge: {
99
+ status: number;
100
+ channel: 'poll' | 'stream';
101
+ }) => boolean | Promise<boolean>;
102
+ /**
103
+ * Decode an SSE `data:` payload into the value handed to the reconciler.
104
+ * Defaults to `JSON.parse`, matching the framework's default serializer.
105
+ * Routes that configure a custom `serializer` (or send raw strings) declare
106
+ * the matching decoder here — otherwise their frames cannot be read, and a
107
+ * frame that cannot be read is a lost event, repaired by a poll.
108
+ */
109
+ parseEventData?: (raw: string) => unknown;
110
+ /** Injectable randomness for deterministic backoff in tests. */
111
+ random?: () => number;
112
+ };
113
+ export type ResilientSubscription<Events extends EventPayloadMap = EventPayloadMap, State = undefined> = {
114
+ /** Uniform event stream — SSE-pushed, replayed, and poll-synthesized alike. */
115
+ events(): AsyncIterable<FallbackEvent<Events>>;
116
+ /** Callback-style event consumption; returns an unsubscribe function. */
117
+ onEvent(listener: (event: FallbackEvent<Events>) => void): () => void;
118
+ /** Latest reduced state — only meaningful when the binding declares `state`. */
119
+ getState(): State | undefined;
120
+ onStateChange(listener: (state: State) => void): () => void;
121
+ readonly status: SubscriptionStatus;
122
+ /**
123
+ * Observe status transitions. `detail` is present exactly when `status` is
124
+ * `'stopped'`, and says why — see {@link StopReason}.
125
+ */
126
+ onStatusChange(listener: (status: SubscriptionStatus, detail?: SubscriptionStopDetail) => void): () => void;
127
+ /**
128
+ * Why the subscription stopped, or `undefined` while it is still running.
129
+ * Also delivered to `onStop` and `onStatusChange` at the moment it stops.
130
+ */
131
+ readonly result: SubscriptionStopDetail | undefined;
132
+ /**
133
+ * Run a listener when the subscription stops. Registering after it has
134
+ * already stopped calls the listener immediately, so there is no race
135
+ * between subscribing and a terminal event that arrived first.
136
+ */
137
+ onStop(listener: (detail: SubscriptionStopDetail) => void): () => void;
138
+ /** Force an immediate reconciliation poll + connection check. */
139
+ nudge(): void;
140
+ /** Stop the subscription: cancel timers, abort in-flight requests. */
141
+ stop(): void;
142
+ /**
143
+ * Await the first delivery of a specific event (use case: await async
144
+ * completion). Resolves identically whether the event traveled over SSE,
145
+ * replay, or a fallback poll.
146
+ *
147
+ * Rejects with {@link SubscriptionStoppedError} if the subscription stops
148
+ * first, so the caller can tell an auth failure from an exhausted budget.
149
+ */
150
+ waitFor<K extends keyof Events & string>(event: K, opts?: {
151
+ timeoutMs?: number;
152
+ }): Promise<Events[K]>;
153
+ /** Await the first terminal event (any of the binding's terminalEvents). */
154
+ waitForTerminal(opts?: {
155
+ timeoutMs?: number;
156
+ }): Promise<FallbackEvent<Events>>;
157
+ };
158
+ /**
159
+ * Create a resilient subscription: SSE as the low-latency channel, short
160
+ * polls as the correctness backbone. See the package README for the state
161
+ * machine and reconciliation semantics.
162
+ */
163
+ export declare function createResilientSubscription<Snapshot, Events extends EventPayloadMap, State = undefined>(binding: FallbackBinding<Snapshot, Events, State>, options: CreateResilientSubscriptionOptions): ResilientSubscription<Events, State>;
164
+ //# sourceMappingURL=subscription.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subscription.d.ts","sourceRoot":"","sources":["../src/subscription.ts"],"names":[],"mappings":"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"}