@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,327 @@
1
+ /**
2
+ * Type layer for fallback bindings.
3
+ *
4
+ * Everything here is type-only or plain data — the package has no runtime
5
+ * dependencies. Zod and @lokalise/api-contracts appear as type-only imports
6
+ * (optional peers); runtime introspection of contracts is structural.
7
+ */
8
+ import type { z } from 'zod/v4';
9
+ /** A comparable version: a monotonic number or a comparable string. */
10
+ export type Version = number | string;
11
+ /**
12
+ * Declares how versions are extracted and ordered.
13
+ *
14
+ * The version is the correctness backbone of the fallback pattern: a single
15
+ * "deliver iff newer than the high-watermark" rule handles duplicate delivery
16
+ * (SSE + poll of the same update), the stale-poll race (a slow poll response
17
+ * arriving after a newer pushed event), and replay overlap after reconnect.
18
+ */
19
+ export type VersionConfig<Snapshot, Event> = {
20
+ /** Extract the version from a poll snapshot body. */
21
+ ofSnapshot: (snapshot: Snapshot) => Version;
22
+ /**
23
+ * Extract the version from a delivered event. The default reads the SSE
24
+ * `id:`, accepting a bare integer (`"42"`) or the
25
+ * `"<numeric epoch>-<counter>"` shape produced by the server-side
26
+ * `createEventIdSequence()`; ids in any other shape (e.g. UUIDs) carry
27
+ * no version. Return `undefined` for events that carry no version —
28
+ * they are delivered without advancing the watermark (at-least-once).
29
+ */
30
+ ofEvent?: (event: Event) => Version | undefined;
31
+ /** Custom comparator. Defaults to numeric, falling back to lexicographic. */
32
+ compare?: (a: Version, b: Version) => number;
33
+ /**
34
+ * Versions are consecutive integers per subscription scope. Enables
35
+ * client-side gap detection (seq N then N+2 → a repair poll is issued
36
+ * immediately instead of waiting for the deadman timer).
37
+ * @default false
38
+ */
39
+ dense?: boolean;
40
+ }
41
+ /**
42
+ * Versionless mode: at-least-once, last-writer-wins. No dedup between
43
+ * channels beyond subscription termination, no gap detection, and the
44
+ * state layer is limited to full snapshot replacement. An adoption bridge
45
+ * for backends without versions — strongly prefer declaring versions.
46
+ */
47
+ | 'none';
48
+ /** Maps event name → parsed payload type. */
49
+ export type EventPayloadMap = Record<string, unknown>;
50
+ /** An event as delivered to app code, regardless of the channel it used. */
51
+ export type FallbackEvent<Events extends EventPayloadMap = EventPayloadMap> = {
52
+ [K in keyof Events & string]: {
53
+ event: K;
54
+ data: Events[K];
55
+ /** SSE id when the event arrived over the stream. */
56
+ id?: string;
57
+ /**
58
+ * Which channel produced the event. Present for observability — app
59
+ * logic SHOULD ignore it; channel transparency is the whole point.
60
+ */
61
+ origin: FallbackEventOrigin;
62
+ };
63
+ }[keyof Events & string];
64
+ export type FallbackEventOrigin = 'sse' | 'poll';
65
+ /** An event synthesized from a snapshot by `snapshotToEvents`. */
66
+ export type SyntheticEvent<Events extends EventPayloadMap = EventPayloadMap> = {
67
+ [K in keyof Events & string]: {
68
+ event: K;
69
+ data: Events[K];
70
+ };
71
+ }[keyof Events & string];
72
+ /**
73
+ * Reconciliation + policy declaration for one dual-mode (or bound-pair)
74
+ * subscription. This is the one thing the framework cannot infer: how a REST
75
+ * snapshot relates to the SSE events. Everything downstream of this
76
+ * declaration — racing, canceling, dedup, reconnect, degradation — is
77
+ * transparent to app code.
78
+ */
79
+ export type FallbackBindingConfig<Snapshot, Events extends EventPayloadMap, State = undefined> = {
80
+ /**
81
+ * Translate a poll snapshot into zero or more events, making poll results
82
+ * indistinguishable from pushed events for app code. Must be PURE — a
83
+ * function of the snapshot alone. Return `[]` when the snapshot carries no
84
+ * news (e.g. a job still pending); the poll still advances the watermark.
85
+ *
86
+ * Exactly one of `snapshotToEvents` / `snapshotEvent` is required.
87
+ */
88
+ snapshotToEvents?: (snapshot: Snapshot) => ReadonlyArray<SyntheticEvent<Events>>;
89
+ /**
90
+ * Shorthand for the common case where the JSON snapshot body IS the
91
+ * payload of one state-carrying event: expands to
92
+ * `snapshotToEvents: (s) => [{ event: snapshotEvent, data: s }]`.
93
+ */
94
+ snapshotEvent?: keyof Events & string;
95
+ /** Version extraction/ordering, or `'none'` (see {@link VersionConfig}). */
96
+ version: VersionConfig<Snapshot, FallbackEvent<Events>>;
97
+ /**
98
+ * Events whose delivery completes the subscription (use case: await async
99
+ * completion). Timers are cancelled, in-flight requests aborted, and
100
+ * `events()` iterators complete.
101
+ */
102
+ terminalEvents?: ReadonlyArray<keyof Events & string>;
103
+ /**
104
+ * Optional state layer (use case: initial load + hydration). When present,
105
+ * the subscription handle exposes `getState()` / `onStateChange()`.
106
+ * Snapshots REPLACE state via `init`; live events update it via `apply`.
107
+ * Events synthesized from a snapshot are not applied (the snapshot already
108
+ * subsumes them).
109
+ */
110
+ state?: {
111
+ init: (snapshot: Snapshot) => State;
112
+ apply: (state: State, event: FallbackEvent<Events>) => State;
113
+ /**
114
+ * On gap detection (dense versions): suppress `apply` until the next
115
+ * snapshot re-initializes state, so deltas are never applied out of
116
+ * order. Events still flow to event listeners.
117
+ * @default true
118
+ */
119
+ reinitOnGap?: boolean;
120
+ };
121
+ /**
122
+ * Whether the server's Last-Event-ID replay is complete ('trusted') —
123
+ * skipping the reconciliation poll after a reconnect — or best-effort
124
+ * ('untrusted', default) — always polling after the stream reopens.
125
+ * @default 'untrusted'
126
+ */
127
+ replay?: 'trusted' | 'untrusted';
128
+ /** Per-binding policy defaults; overridable per subscription. */
129
+ policy?: Partial<FallbackPolicy>;
130
+ };
131
+ export type BackoffConfig = {
132
+ baseMs: number;
133
+ factor: number;
134
+ maxMs: number;
135
+ };
136
+ /**
137
+ * A hard ceiling on how long a subscription may keep working before it gives
138
+ * up. Unset by default: a live-state subscription is supposed to run for as
139
+ * long as the surface is open.
140
+ *
141
+ * Set it for pending-completion subscriptions. Every individual wait in this
142
+ * package is bounded, but the subscription as a whole is not: a backend stuck
143
+ * in a pending state deadman-polls forever (backing off to
144
+ * `deadmanIdleBackoff.maxMs`, never giving up), so a permanently pending
145
+ * operation polls until the tab closes. With a budget the subscription stops
146
+ * with a `'budget-exhausted'` reason, which is what a UI needs to show an
147
+ * actionable error and offer a manual retry.
148
+ */
149
+ export type SubscriptionBudget = {
150
+ /** Wall-clock ceiling, measured from subscription creation. */
151
+ maxDurationMs?: number;
152
+ /** Ceiling on snapshot polls attempted, successful or not. */
153
+ maxPolls?: number;
154
+ };
155
+ export type FallbackPolicy = {
156
+ /**
157
+ * Which channels the subscription uses:
158
+ * - `'dual'` (default): SSE as the low-latency channel, polls as the
159
+ * correctness backbone, degrading to polling after repeated connect
160
+ * failures.
161
+ * - `'poll-only'`: never open a stream at all. The subscription starts in
162
+ * `'polling'` and runs the degraded cadence
163
+ * (`degradedPollIntervalMs`) from the first tick.
164
+ *
165
+ * `'poll-only'` exists so a backend can adopt this package BEFORE it has an
166
+ * SSE endpoint: the binding, the version gate, the reconciler and the state
167
+ * machine are the same ones the streaming rollout will use, so turning the
168
+ * stream on later is a config change on an already-integrated subscription
169
+ * rather than a second migration. Without it, poll-only delivery is only
170
+ * reachable after `degradedAfterFailures` connect failures against an
171
+ * endpoint that must already exist.
172
+ * @default 'dual'
173
+ */
174
+ mode: 'dual' | 'poll-only';
175
+ /**
176
+ * Overall ceiling for the subscription — see {@link SubscriptionBudget}.
177
+ * Unset by default.
178
+ */
179
+ subscriptionBudget?: SubscriptionBudget;
180
+ /**
181
+ * Initial snapshot poll behavior:
182
+ * - `'eager'` (default): fetch a snapshot as soon as the stream opens
183
+ * (hydration; also closes the startup race where the awaited activity
184
+ * completed before the stream was established).
185
+ * - `'delayed'`: no initial poll; the first poll happens when the deadman
186
+ * timer fires.
187
+ * - `'none'`: same as 'delayed' (kept distinct for future use).
188
+ */
189
+ initialPoll: 'eager' | 'delayed' | 'none';
190
+ /**
191
+ * Deadman delay: how long after the last DELIVERED EVENT before a
192
+ * reconciliation poll fires. Heartbeats do not count, and neither do
193
+ * duplicates below the watermark: both prove transport liveness, not
194
+ * delivery.
195
+ */
196
+ deadmanDelayMs: number;
197
+ /**
198
+ * Consecutive no-news polls stretch the deadman interval, and delivered
199
+ * events do NOT shrink it back: a stream that keeps delivering is proving
200
+ * it needs less reconciliation, not more. Only a poll that finds something
201
+ * the stream missed resets the interval to {@link deadmanDelayMs}.
202
+ */
203
+ deadmanIdleBackoff: {
204
+ factor: number;
205
+ maxMs: number;
206
+ };
207
+ /**
208
+ * Force-close the stream when NO BYTES (events, comments, heartbeats)
209
+ * arrive within this window — catches silently dead connections.
210
+ * `'off'` disables byte-level liveness (rely on the deadman alone).
211
+ */
212
+ staleConnectionTimeoutMs: number | 'off';
213
+ /**
214
+ * Abort a stream connect that has not produced response headers within this
215
+ * window, so a hung connect is counted as a connect failure (backoff,
216
+ * degradation) instead of stalling the subscription indefinitely.
217
+ * `'off'` waits forever — only safe with a transport that times out itself.
218
+ */
219
+ connectTimeoutMs: number | 'off';
220
+ /**
221
+ * Abort a snapshot poll that has not settled within this window. Without a
222
+ * bound, one never-settling poll would hold the in-flight latch and leave
223
+ * the deadman unarmed — disabling the correctness backbone entirely.
224
+ * `'off'` waits forever — only safe with a transport that times out itself.
225
+ */
226
+ pollTimeoutMs: number | 'off';
227
+ /** Backoff (full jitter) for failed polls. */
228
+ pollFailureBackoff: BackoffConfig;
229
+ /** Backoff (full jitter) for SSE reconnect attempts; `retry:` hints override the base. */
230
+ sseRetryBackoff: BackoffConfig;
231
+ /**
232
+ * Bounds applied to a server-sent `retry:` reconnection hint. The hint is
233
+ * attacker- or bug-reachable input: `retry: 0` would spin a zero-delay
234
+ * reconnect loop and a very large value would park reconnection for hours,
235
+ * so it is clamped into this range rather than used verbatim.
236
+ */
237
+ serverRetryHintBounds: {
238
+ minMs: number;
239
+ maxMs: number;
240
+ };
241
+ /**
242
+ * Consecutive snapshot-poll failures after which subscribe-first hydration
243
+ * is abandoned: the buffered live events are flushed to the application and
244
+ * the stream resumes delivering directly. Without this, an endpoint that
245
+ * keeps failing its snapshot poll would hold the hydration buffer open and
246
+ * a perfectly healthy stream would deliver nothing.
247
+ */
248
+ hydrationAbandonAfterFailures: number;
249
+ /** Consecutive SSE connect failures before degrading to POLLING_ONLY. */
250
+ degradedAfterFailures: number;
251
+ /** Poll cadence while degraded (SSE unavailable). */
252
+ degradedPollIntervalMs: number;
253
+ /** Cap for background SSE retry while degraded. */
254
+ degradedSseRetryMaxMs: number;
255
+ /** Max events buffered during hydration; overflow drops the buffer and refetches. */
256
+ hydrationBufferLimit: number;
257
+ /**
258
+ * HTTP statuses on the stream or poll that stop the subscription instead
259
+ * of retrying (auth/authz/not-found are not transient).
260
+ */
261
+ unretryableStatuses: ReadonlyArray<number>;
262
+ /**
263
+ * Statuses that are offered to `onAuthChallenge` before the subscription
264
+ * gives up on them. Only meaningful for statuses that are also in
265
+ * {@link unretryableStatuses}.
266
+ *
267
+ * In a SPA a 401 is usually an expired token rather than a genuinely
268
+ * unauthorized caller, and this package exists to recover without a page
269
+ * reload. With an `onAuthChallenge` hook configured, the first auth refusal
270
+ * on either channel is handed to the application, which refreshes
271
+ * credentials and lets the request run once more; a second refusal without
272
+ * an intervening success stops the subscription.
273
+ * @default [401]
274
+ */
275
+ authChallengeStatuses: ReadonlyArray<number>;
276
+ };
277
+ export declare const DEFAULT_POLICY: FallbackPolicy;
278
+ /**
279
+ * Preset for use case A — await async completion.
280
+ *
281
+ * Set `subscriptionBudget` on top of this for operations that must not poll
282
+ * forever if the backend never leaves its pending state.
283
+ */
284
+ export declare const COMPLETION_POLICY: FallbackPolicy;
285
+ /** Preset for use case B — initial state load + live hydration. */
286
+ export declare const LIVE_STATE_POLICY: FallbackPolicy;
287
+ /**
288
+ * Preset for adopters with no SSE endpoint yet: same binding, version gate and
289
+ * state machine as the dual-mode presets, polling only. Flip `mode` back to
290
+ * `'dual'` once the stream exists.
291
+ */
292
+ export declare const POLL_ONLY_POLICY: FallbackPolicy;
293
+ type SseBodyLike<TSchemas> = {
294
+ readonly _tag: 'SseBody';
295
+ readonly schemaByEventName: TSchemas;
296
+ };
297
+ type EventsFromSchemaMap<TSchemas> = {
298
+ [K in keyof TSchemas]: TSchemas[K] extends z.ZodType ? z.output<TSchemas[K]> : never;
299
+ };
300
+ type SseEventsOfContentMap<TContent> = {
301
+ [K in keyof TContent]: TContent[K] extends SseBodyLike<infer TSchemas> ? EventsFromSchemaMap<TSchemas> : never;
302
+ }[keyof TContent];
303
+ type NonSseBodyOfContentMap<TContent> = {
304
+ [K in keyof TContent]: TContent[K] extends SseBodyLike<unknown> ? never : TContent[K] extends z.ZodType ? z.output<TContent[K]> : never;
305
+ }[keyof TContent];
306
+ type SuccessStatusKey = 200 | 201 | 202 | 203 | 206 | 207 | 208 | 226;
307
+ type SuccessEntries<TContract> = TContract extends {
308
+ responsesByStatusCode: infer TResponses;
309
+ } ? TResponses[Extract<keyof TResponses, SuccessStatusKey>] : never;
310
+ /** Event payload map inferred from a `defineApiContract` contract's sseBody. */
311
+ export type InferContractEvents<TContract> = Extract<SuccessEntries<TContract> extends infer TEntry ? TEntry extends {
312
+ content: infer TContent;
313
+ } ? SseEventsOfContentMap<TContent> : never : never, EventPayloadMap>;
314
+ /** Snapshot (non-SSE JSON success body) inferred from a `defineApiContract` contract. */
315
+ export type InferContractSnapshot<TContract> = SuccessEntries<TContract> extends infer TEntry ? TEntry extends {
316
+ content: infer TContent;
317
+ } ? NonSseBodyOfContentMap<TContent> : TEntry extends z.ZodType ? z.output<TEntry> : never : never;
318
+ /** Event payload map inferred from a legacy `buildSseContract` dual-mode contract. */
319
+ export type InferLegacyEvents<TContract> = TContract extends {
320
+ serverSentEventSchemas: infer TSchemas;
321
+ } ? EventsFromSchemaMap<TSchemas> : never;
322
+ /** Snapshot inferred from a legacy dual-mode contract's success schema. */
323
+ export type InferLegacySnapshot<TContract> = TContract extends {
324
+ successResponseBodySchema: infer TSchema;
325
+ } ? TSchema extends z.ZodType ? z.output<TSchema> : never : never;
326
+ export {};
327
+ //# sourceMappingURL=bindingTypes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bindingTypes.d.ts","sourceRoot":"","sources":["../src/bindingTypes.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,QAAQ,CAAA;AAM/B,uEAAuE;AACvE,MAAM,MAAM,OAAO,GAAG,MAAM,GAAG,MAAM,CAAA;AAErC;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,CAAC,QAAQ,EAAE,KAAK,IACrC;IACE,qDAAqD;IACrD,UAAU,EAAE,CAAC,QAAQ,EAAE,QAAQ,KAAK,OAAO,CAAA;IAC3C;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,GAAG,SAAS,CAAA;IAC/C,6EAA6E;IAC7E,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,KAAK,MAAM,CAAA;IAC5C;;;;;OAKG;IACH,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AACH;;;;;GAKG;GACD,MAAM,CAAA;AAMV,6CAA6C;AAC7C,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAErD,4EAA4E;AAC5E,MAAM,MAAM,aAAa,CAAC,MAAM,SAAS,eAAe,GAAG,eAAe,IAAI;KAC3E,CAAC,IAAI,MAAM,MAAM,GAAG,MAAM,GAAG;QAC5B,KAAK,EAAE,CAAC,CAAA;QACR,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAA;QACf,qDAAqD;QACrD,EAAE,CAAC,EAAE,MAAM,CAAA;QACX;;;WAGG;QACH,MAAM,EAAE,mBAAmB,CAAA;KAC5B;CACF,CAAC,MAAM,MAAM,GAAG,MAAM,CAAC,CAAA;AAExB,MAAM,MAAM,mBAAmB,GAAG,KAAK,GAAG,MAAM,CAAA;AAEhD,kEAAkE;AAClE,MAAM,MAAM,cAAc,CAAC,MAAM,SAAS,eAAe,GAAG,eAAe,IAAI;KAC5E,CAAC,IAAI,MAAM,MAAM,GAAG,MAAM,GAAG;QAAE,KAAK,EAAE,CAAC,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAA;KAAE;CAC5D,CAAC,MAAM,MAAM,GAAG,MAAM,CAAC,CAAA;AAMxB;;;;;;GAMG;AACH,MAAM,MAAM,qBAAqB,CAAC,QAAQ,EAAE,MAAM,SAAS,eAAe,EAAE,KAAK,GAAG,SAAS,IAAI;IAC/F;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,KAAK,aAAa,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAA;IAChF;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,MAAM,GAAG,MAAM,CAAA;IAErC,4EAA4E;IAC5E,OAAO,EAAE,aAAa,CAAC,QAAQ,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,CAAA;IAEvD;;;;OAIG;IACH,cAAc,CAAC,EAAE,aAAa,CAAC,MAAM,MAAM,GAAG,MAAM,CAAC,CAAA;IAErD;;;;;;OAMG;IACH,KAAK,CAAC,EAAE;QACN,IAAI,EAAE,CAAC,QAAQ,EAAE,QAAQ,KAAK,KAAK,CAAA;QACnC,KAAK,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,aAAa,CAAC,MAAM,CAAC,KAAK,KAAK,CAAA;QAC5D;;;;;WAKG;QACH,WAAW,CAAC,EAAE,OAAO,CAAA;KACtB,CAAA;IAED;;;;;OAKG;IACH,MAAM,CAAC,EAAE,SAAS,GAAG,WAAW,CAAA;IAEhC,iEAAiE;IACjE,MAAM,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAA;CACjC,CAAA;AAMD,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAC/B,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B;;;;;;;;;;;;;;;;;OAiBG;IACH,IAAI,EAAE,MAAM,GAAG,WAAW,CAAA;IAC1B;;;OAGG;IACH,kBAAkB,CAAC,EAAE,kBAAkB,CAAA;IACvC;;;;;;;;OAQG;IACH,WAAW,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAA;IACzC;;;;;OAKG;IACH,cAAc,EAAE,MAAM,CAAA;IACtB;;;;;OAKG;IACH,kBAAkB,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;IACrD;;;;OAIG;IACH,wBAAwB,EAAE,MAAM,GAAG,KAAK,CAAA;IACxC;;;;;OAKG;IACH,gBAAgB,EAAE,MAAM,GAAG,KAAK,CAAA;IAChC;;;;;OAKG;IACH,aAAa,EAAE,MAAM,GAAG,KAAK,CAAA;IAC7B,8CAA8C;IAC9C,kBAAkB,EAAE,aAAa,CAAA;IACjC,0FAA0F;IAC1F,eAAe,EAAE,aAAa,CAAA;IAC9B;;;;;OAKG;IACH,qBAAqB,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;IACvD;;;;;;OAMG;IACH,6BAA6B,EAAE,MAAM,CAAA;IACrC,yEAAyE;IACzE,qBAAqB,EAAE,MAAM,CAAA;IAC7B,qDAAqD;IACrD,sBAAsB,EAAE,MAAM,CAAA;IAC9B,mDAAmD;IACnD,qBAAqB,EAAE,MAAM,CAAA;IAC7B,qFAAqF;IACrF,oBAAoB,EAAE,MAAM,CAAA;IAC5B;;;OAGG;IACH,mBAAmB,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IAC1C;;;;;;;;;;;;OAYG;IACH,qBAAqB,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;CAC7C,CAAA;AAED,eAAO,MAAM,cAAc,EAAE,cAkB5B,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,EAAE,cAA+B,CAAA;AAE/D,mEAAmE;AACnE,eAAO,MAAM,iBAAiB,EAAE,cAK/B,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,EAAE,cAG9B,CAAA;AAMD,KAAK,WAAW,CAAC,QAAQ,IAAI;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,iBAAiB,EAAE,QAAQ,CAAA;CAAE,CAAA;AAE/F,KAAK,mBAAmB,CAAC,QAAQ,IAAI;KAClC,CAAC,IAAI,MAAM,QAAQ,GAAG,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK;CACrF,CAAA;AAED,KAAK,qBAAqB,CAAC,QAAQ,IAAI;KACpC,CAAC,IAAI,MAAM,QAAQ,GAAG,QAAQ,CAAC,CAAC,CAAC,SAAS,WAAW,CAAC,MAAM,QAAQ,CAAC,GAClE,mBAAmB,CAAC,QAAQ,CAAC,GAC7B,KAAK;CACV,CAAC,MAAM,QAAQ,CAAC,CAAA;AAEjB,KAAK,sBAAsB,CAAC,QAAQ,IAAI;KACrC,CAAC,IAAI,MAAM,QAAQ,GAAG,QAAQ,CAAC,CAAC,CAAC,SAAS,WAAW,CAAC,OAAO,CAAC,GAC3D,KAAK,GACL,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,GAC3B,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,GACrB,KAAK;CACZ,CAAC,MAAM,QAAQ,CAAC,CAAA;AAEjB,KAAK,gBAAgB,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAA;AAErE,KAAK,cAAc,CAAC,SAAS,IAAI,SAAS,SAAS;IACjD,qBAAqB,EAAE,MAAM,UAAU,CAAA;CACxC,GACG,UAAU,CAAC,OAAO,CAAC,MAAM,UAAU,EAAE,gBAAgB,CAAC,CAAC,GACvD,KAAK,CAAA;AAET,gFAAgF;AAChF,MAAM,MAAM,mBAAmB,CAAC,SAAS,IAAI,OAAO,CAClD,cAAc,CAAC,SAAS,CAAC,SAAS,MAAM,MAAM,GAC1C,MAAM,SAAS;IAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;CAAE,GACxC,qBAAqB,CAAC,QAAQ,CAAC,GAC/B,KAAK,GACP,KAAK,EACT,eAAe,CAChB,CAAA;AAED,yFAAyF;AACzF,MAAM,MAAM,qBAAqB,CAAC,SAAS,IACzC,cAAc,CAAC,SAAS,CAAC,SAAS,MAAM,MAAM,GAC1C,MAAM,SAAS;IAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;CAAE,GACxC,sBAAsB,CAAC,QAAQ,CAAC,GAChC,MAAM,SAAS,CAAC,CAAC,OAAO,GACtB,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,GAChB,KAAK,GACT,KAAK,CAAA;AAEX,sFAAsF;AACtF,MAAM,MAAM,iBAAiB,CAAC,SAAS,IAAI,SAAS,SAAS;IAC3D,sBAAsB,EAAE,MAAM,QAAQ,CAAA;CACvC,GACG,mBAAmB,CAAC,QAAQ,CAAC,GAC7B,KAAK,CAAA;AAET,2EAA2E;AAC3E,MAAM,MAAM,mBAAmB,CAAC,SAAS,IAAI,SAAS,SAAS;IAC7D,yBAAyB,EAAE,MAAM,OAAO,CAAA;CACzC,GACG,OAAO,SAAS,CAAC,CAAC,OAAO,GACvB,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,GACjB,KAAK,GACP,KAAK,CAAA"}
@@ -0,0 +1,43 @@
1
+ export const DEFAULT_POLICY = {
2
+ mode: 'dual',
3
+ initialPoll: 'eager',
4
+ deadmanDelayMs: 10_000,
5
+ deadmanIdleBackoff: { factor: 1.5, maxMs: 60_000 },
6
+ staleConnectionTimeoutMs: 60_000,
7
+ connectTimeoutMs: 15_000,
8
+ pollTimeoutMs: 10_000,
9
+ pollFailureBackoff: { baseMs: 1_000, factor: 2, maxMs: 30_000 },
10
+ sseRetryBackoff: { baseMs: 1_000, factor: 2, maxMs: 30_000 },
11
+ serverRetryHintBounds: { minMs: 250, maxMs: 60_000 },
12
+ hydrationAbandonAfterFailures: 3,
13
+ degradedAfterFailures: 3,
14
+ degradedPollIntervalMs: 15_000,
15
+ degradedSseRetryMaxMs: 60_000,
16
+ hydrationBufferLimit: 1_000,
17
+ unretryableStatuses: [401, 403, 404],
18
+ authChallengeStatuses: [401],
19
+ };
20
+ /**
21
+ * Preset for use case A — await async completion.
22
+ *
23
+ * Set `subscriptionBudget` on top of this for operations that must not poll
24
+ * forever if the backend never leaves its pending state.
25
+ */
26
+ export const COMPLETION_POLICY = DEFAULT_POLICY;
27
+ /** Preset for use case B — initial state load + live hydration. */
28
+ export const LIVE_STATE_POLICY = {
29
+ ...DEFAULT_POLICY,
30
+ deadmanDelayMs: 120_000,
31
+ deadmanIdleBackoff: { factor: 1.5, maxMs: 300_000 },
32
+ degradedPollIntervalMs: 60_000,
33
+ };
34
+ /**
35
+ * Preset for adopters with no SSE endpoint yet: same binding, version gate and
36
+ * state machine as the dual-mode presets, polling only. Flip `mode` back to
37
+ * `'dual'` once the stream exists.
38
+ */
39
+ export const POLL_ONLY_POLICY = {
40
+ ...DEFAULT_POLICY,
41
+ mode: 'poll-only',
42
+ };
43
+ //# sourceMappingURL=bindingTypes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bindingTypes.js","sourceRoot":"","sources":["../src/bindingTypes.ts"],"names":[],"mappings":"AA4SA,MAAM,CAAC,MAAM,cAAc,GAAmB;IAC5C,IAAI,EAAE,MAAM;IACZ,WAAW,EAAE,OAAO;IACpB,cAAc,EAAE,MAAM;IACtB,kBAAkB,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE;IAClD,wBAAwB,EAAE,MAAM;IAChC,gBAAgB,EAAE,MAAM;IACxB,aAAa,EAAE,MAAM;IACrB,kBAAkB,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE;IAC/D,eAAe,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE;IAC5D,qBAAqB,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE;IACpD,6BAA6B,EAAE,CAAC;IAChC,qBAAqB,EAAE,CAAC;IACxB,sBAAsB,EAAE,MAAM;IAC9B,qBAAqB,EAAE,MAAM;IAC7B,oBAAoB,EAAE,KAAK;IAC3B,mBAAmB,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC;IACpC,qBAAqB,EAAE,CAAC,GAAG,CAAC;CAC7B,CAAA;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAmB,cAAc,CAAA;AAE/D,mEAAmE;AACnE,MAAM,CAAC,MAAM,iBAAiB,GAAmB;IAC/C,GAAG,cAAc;IACjB,cAAc,EAAE,OAAO;IACvB,kBAAkB,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE;IACnD,sBAAsB,EAAE,MAAM;CAC/B,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAmB;IAC9C,GAAG,cAAc;IACjB,IAAI,EAAE,WAAW;CAClB,CAAA"}
@@ -0,0 +1,9 @@
1
+ export { createSSEStreamParser, type ParsedSSEEvent, type ParseSSEBufferResult, type ParseSSEStreamOptions, parseSSEBuffer, parseSSEResponse, parseSSEStream, type SSEStreamParser, } from '@opinionated-machine/sse-parser';
2
+ export { type BindFallbackContractsOptions, bindFallbackContracts, defineFallbackBinding, FALLBACK_BINDING_SYMBOL, type FallbackBinding, type FallbackRequestParams, fromLegacyDualModeContract, readFallbackBinding, } from './binding.ts';
3
+ export { type BackoffConfig, COMPLETION_POLICY, DEFAULT_POLICY, type EventPayloadMap, type FallbackBindingConfig, type FallbackEvent, type FallbackEventOrigin, type FallbackPolicy, type InferContractEvents, type InferContractSnapshot, type InferLegacyEvents, type InferLegacySnapshot, LIVE_STATE_POLICY, POLL_ONLY_POLICY, type SubscriptionBudget, type SyntheticEvent, type Version, type VersionConfig, } from './bindingTypes.ts';
4
+ export { createPollGate, type PollGate, type PollGateConfig } from './pollGate.ts';
5
+ export { defaultCompareVersions, type EventOutcome, type IncomingEvent, type InvalidVersionInfo, Reconciler, type SnapshotOutcome, type VersionGap, } from './reconciler.ts';
6
+ export { backoffDelay, ResettableTimer, sleep } from './scheduler.ts';
7
+ export { type CreateResilientSubscriptionOptions, createResilientSubscription, type FallbackDiagnostics, type ResilientSubscription, type StopReason, type SubscriptionStatus, type SubscriptionStopDetail, SubscriptionStoppedError, } from './subscription.ts';
8
+ export { type FallbackTransport, isParsedStreamResponse, type ParsedSseFrame, type ParsedStreamResponse, type RawStreamResponse, type SnapshotResponse, type StreamResponse, type TestSnapshotCall, type TestStreamHandle, TestTransport, type TransportRequest, } from './transport.ts';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,qBAAqB,EACrB,KAAK,cAAc,EACnB,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,cAAc,EACd,gBAAgB,EAChB,cAAc,EACd,KAAK,eAAe,GACrB,MAAM,iCAAiC,CAAA;AACxC,OAAO,EACL,KAAK,4BAA4B,EACjC,qBAAqB,EACrB,qBAAqB,EACrB,uBAAuB,EACvB,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,0BAA0B,EAC1B,mBAAmB,GACpB,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,KAAK,aAAa,EAClB,iBAAiB,EACjB,cAAc,EACd,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,aAAa,EAClB,KAAK,mBAAmB,EACxB,KAAK,cAAc,EACnB,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,iBAAiB,EACjB,gBAAgB,EAChB,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,OAAO,EACZ,KAAK,aAAa,GACnB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,cAAc,EAAE,KAAK,QAAQ,EAAE,KAAK,cAAc,EAAE,MAAM,eAAe,CAAA;AAClF,OAAO,EACL,sBAAsB,EACtB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,UAAU,EACV,KAAK,eAAe,EACpB,KAAK,UAAU,GAChB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AACrE,OAAO,EACL,KAAK,kCAAkC,EACvC,2BAA2B,EAC3B,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,UAAU,EACf,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,EAC3B,wBAAwB,GACzB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EACL,KAAK,iBAAiB,EACtB,sBAAsB,EACtB,KAAK,cAAc,EACnB,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,aAAa,EACb,KAAK,gBAAgB,GACtB,MAAM,gBAAgB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ // Re-exported so a transport author does not have to add the parser package
2
+ // as a second dependency to frame a stream by hand.
3
+ export { createSSEStreamParser, parseSSEBuffer, parseSSEResponse, parseSSEStream, } from '@opinionated-machine/sse-parser';
4
+ export { bindFallbackContracts, defineFallbackBinding, FALLBACK_BINDING_SYMBOL, fromLegacyDualModeContract, readFallbackBinding, } from "./binding.js";
5
+ export { COMPLETION_POLICY, DEFAULT_POLICY, LIVE_STATE_POLICY, POLL_ONLY_POLICY, } from "./bindingTypes.js";
6
+ export { createPollGate } from "./pollGate.js";
7
+ export { defaultCompareVersions, Reconciler, } from "./reconciler.js";
8
+ export { backoffDelay, ResettableTimer, sleep } from "./scheduler.js";
9
+ export { createResilientSubscription, SubscriptionStoppedError, } from "./subscription.js";
10
+ export { isParsedStreamResponse, TestTransport, } from "./transport.js";
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,oDAAoD;AACpD,OAAO,EACL,qBAAqB,EAIrB,cAAc,EACd,gBAAgB,EAChB,cAAc,GAEf,MAAM,iCAAiC,CAAA;AACxC,OAAO,EAEL,qBAAqB,EACrB,qBAAqB,EACrB,uBAAuB,EAGvB,0BAA0B,EAC1B,mBAAmB,GACpB,MAAM,cAAc,CAAA;AACrB,OAAO,EAEL,iBAAiB,EACjB,cAAc,EAUd,iBAAiB,EACjB,gBAAgB,GAKjB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,cAAc,EAAsC,MAAM,eAAe,CAAA;AAClF,OAAO,EACL,sBAAsB,EAItB,UAAU,GAGX,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AACrE,OAAO,EAEL,2BAA2B,EAM3B,wBAAwB,GACzB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAEL,sBAAsB,EAQtB,aAAa,GAEd,MAAM,gBAAgB,CAAA"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Cross-subscription coordination for reconciliation polls.
3
+ *
4
+ * Each subscription jitters its own backoff, which spreads out ITS retries but
5
+ * says nothing about the other subscriptions in the same tab. After a server
6
+ * blip every live subscription reconnects at once and each fires its
7
+ * post-reconnect reconciliation poll, so an app running dozens of
8
+ * subscriptions turns one outage into a burst of dozens of simultaneous
9
+ * requests against one origin.
10
+ *
11
+ * A gate is shared by every subscription that should be capped together
12
+ * (normally one per origin). It bounds how many reconciliation polls run at
13
+ * the same time and, with `staggerMs`, spreads the release of queued polls
14
+ * over a window instead of letting them all start on the same tick.
15
+ *
16
+ * ```ts
17
+ * const gate = createPollGate({ maxConcurrent: 4, staggerMs: 2_000 })
18
+ * const sub = createResilientSubscription(binding, { transport, pollGate: gate })
19
+ * ```
20
+ *
21
+ * A gate delays polls; it never cancels them. A subscription waiting for a
22
+ * slot keeps its in-flight latch, so its deadman does not fire a second poll
23
+ * behind the first.
24
+ */
25
+ export type PollGate = {
26
+ /**
27
+ * Wait for permission to run a reconciliation poll. Resolves with a release
28
+ * function that MUST be called when the poll settles.
29
+ *
30
+ * Rejects when `signal` aborts while waiting; nothing needs releasing then.
31
+ */
32
+ acquire(opts: {
33
+ signal: AbortSignal;
34
+ }): Promise<() => void>;
35
+ };
36
+ export type PollGateConfig = {
37
+ /**
38
+ * Maximum reconciliation polls in flight across every subscription sharing
39
+ * this gate.
40
+ * @default 6
41
+ */
42
+ maxConcurrent?: number;
43
+ /**
44
+ * Spread window. Each poll waits a uniformly random delay in
45
+ * `[0, staggerMs)` after taking a slot, so a fleet-wide reconnect does not
46
+ * put every request on the same tick.
47
+ * @default 0 (no stagger)
48
+ */
49
+ staggerMs?: number;
50
+ /** Injectable randomness for deterministic tests. */
51
+ random?: () => number;
52
+ };
53
+ /**
54
+ * Create a {@link PollGate}: a shared concurrency cap plus an optional
55
+ * randomized stagger for reconciliation polls.
56
+ */
57
+ export declare function createPollGate(config?: PollGateConfig): PollGate;
58
+ //# sourceMappingURL=pollGate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pollGate.d.ts","sourceRoot":"","sources":["../src/pollGate.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,MAAM,QAAQ,GAAG;IACrB;;;;;OAKG;IACH,OAAO,CAAC,IAAI,EAAE;QAAE,MAAM,EAAE,WAAW,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,CAAA;CAC5D,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,qDAAqD;IACrD,MAAM,CAAC,EAAE,MAAM,MAAM,CAAA;CACtB,CAAA;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,MAAM,CAAC,EAAE,cAAc,GAAG,QAAQ,CA4DhE"}
@@ -0,0 +1,64 @@
1
+ import { sleep } from "./scheduler.js";
2
+ /**
3
+ * Create a {@link PollGate}: a shared concurrency cap plus an optional
4
+ * randomized stagger for reconciliation polls.
5
+ */
6
+ export function createPollGate(config) {
7
+ const maxConcurrent = Math.max(1, Math.floor(config?.maxConcurrent ?? 6));
8
+ const staggerMs = Math.max(0, config?.staggerMs ?? 0);
9
+ const random = config?.random ?? Math.random;
10
+ let active = 0;
11
+ const waiting = [];
12
+ const releaseSlot = () => {
13
+ // Hand the slot straight to the next waiter rather than decrementing and
14
+ // letting a fresh caller race in ahead of a subscription already queued.
15
+ const next = waiting.shift();
16
+ if (next) {
17
+ next();
18
+ return;
19
+ }
20
+ active -= 1;
21
+ };
22
+ const takeSlot = (signal) => {
23
+ if (active < maxConcurrent) {
24
+ active += 1;
25
+ return Promise.resolve();
26
+ }
27
+ return new Promise((resolve, reject) => {
28
+ const grant = () => {
29
+ signal.removeEventListener('abort', onAbort);
30
+ resolve();
31
+ };
32
+ const onAbort = () => {
33
+ const index = waiting.indexOf(grant);
34
+ if (index >= 0)
35
+ waiting.splice(index, 1);
36
+ reject(new Error('Poll gate wait aborted'));
37
+ };
38
+ waiting.push(grant);
39
+ signal.addEventListener('abort', onAbort, { once: true });
40
+ });
41
+ };
42
+ return {
43
+ async acquire(opts) {
44
+ if (opts.signal.aborted)
45
+ throw new Error('Poll gate wait aborted');
46
+ await takeSlot(opts.signal);
47
+ if (staggerMs > 0) {
48
+ const proceeded = await sleep(Math.floor(random() * staggerMs), opts.signal);
49
+ if (!proceeded) {
50
+ releaseSlot();
51
+ throw new Error('Poll gate wait aborted');
52
+ }
53
+ }
54
+ let released = false;
55
+ return () => {
56
+ if (released)
57
+ return;
58
+ released = true;
59
+ releaseSlot();
60
+ };
61
+ },
62
+ };
63
+ }
64
+ //# sourceMappingURL=pollGate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pollGate.js","sourceRoot":"","sources":["../src/pollGate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAsDtC;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,MAAuB;IACpD,MAAM,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,aAAa,IAAI,CAAC,CAAC,CAAC,CAAA;IACzE,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,IAAI,CAAC,CAAC,CAAA;IACrD,MAAM,MAAM,GAAG,MAAM,EAAE,MAAM,IAAI,IAAI,CAAC,MAAM,CAAA;IAE5C,IAAI,MAAM,GAAG,CAAC,CAAA;IACd,MAAM,OAAO,GAAsB,EAAE,CAAA;IAErC,MAAM,WAAW,GAAG,GAAS,EAAE;QAC7B,yEAAyE;QACzE,yEAAyE;QACzE,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,EAAE,CAAA;QAC5B,IAAI,IAAI,EAAE,CAAC;YACT,IAAI,EAAE,CAAA;YACN,OAAM;QACR,CAAC;QACD,MAAM,IAAI,CAAC,CAAA;IACb,CAAC,CAAA;IAED,MAAM,QAAQ,GAAG,CAAC,MAAmB,EAAiB,EAAE;QACtD,IAAI,MAAM,GAAG,aAAa,EAAE,CAAC;YAC3B,MAAM,IAAI,CAAC,CAAA;YACX,OAAO,OAAO,CAAC,OAAO,EAAE,CAAA;QAC1B,CAAC;QACD,OAAO,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YAC3C,MAAM,KAAK,GAAG,GAAS,EAAE;gBACvB,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;gBAC5C,OAAO,EAAE,CAAA;YACX,CAAC,CAAA;YACD,MAAM,OAAO,GAAG,GAAS,EAAE;gBACzB,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;gBACpC,IAAI,KAAK,IAAI,CAAC;oBAAE,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAA;gBACxC,MAAM,CAAC,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAC,CAAA;YAC7C,CAAC,CAAA;YACD,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;YACnB,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAA;QAC3D,CAAC,CAAC,CAAA;IACJ,CAAC,CAAA;IAED,OAAO;QACL,KAAK,CAAC,OAAO,CAAC,IAA6B;YACzC,IAAI,IAAI,CAAC,MAAM,CAAC,OAAO;gBAAE,MAAM,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAA;YAClE,MAAM,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;YAE3B,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;gBAClB,MAAM,SAAS,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,GAAG,SAAS,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAA;gBAC5E,IAAI,CAAC,SAAS,EAAE,CAAC;oBACf,WAAW,EAAE,CAAA;oBACb,MAAM,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAA;gBAC3C,CAAC;YACH,CAAC;YAED,IAAI,QAAQ,GAAG,KAAK,CAAA;YACpB,OAAO,GAAG,EAAE;gBACV,IAAI,QAAQ;oBAAE,OAAM;gBACpB,QAAQ,GAAG,IAAI,CAAA;gBACf,WAAW,EAAE,CAAA;YACf,CAAC,CAAA;QACH,CAAC;KACF,CAAA;AACH,CAAC"}