@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.
- package/LICENSE +21 -0
- package/README.md +365 -0
- package/dist/binding.d.ts +82 -0
- package/dist/binding.d.ts.map +1 -0
- package/dist/binding.js +181 -0
- package/dist/binding.js.map +1 -0
- package/dist/bindingTypes.d.ts +327 -0
- package/dist/bindingTypes.d.ts.map +1 -0
- package/dist/bindingTypes.js +43 -0
- package/dist/bindingTypes.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/pollGate.d.ts +58 -0
- package/dist/pollGate.d.ts.map +1 -0
- package/dist/pollGate.js +64 -0
- package/dist/pollGate.js.map +1 -0
- package/dist/reconciler.d.ts +245 -0
- package/dist/reconciler.d.ts.map +1 -0
- package/dist/reconciler.js +568 -0
- package/dist/reconciler.js.map +1 -0
- package/dist/scheduler.d.ts +19 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +51 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/subscription.d.ts +164 -0
- package/dist/subscription.d.ts.map +1 -0
- package/dist/subscription.js +878 -0
- package/dist/subscription.js.map +1 -0
- package/dist/transport.d.ts +177 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +168 -0
- package/dist/transport.js.map +1 -0
- package/package.json +76 -0
|
@@ -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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"}
|