@cplieger/sse 1.5.0-dev.8 → 2.0.0
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/README.md +4 -2
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/digest.ts +5 -5
- package/src/fold.ts +1 -1
- package/src/index.ts +11 -36
- package/src/lifecycle.ts +4 -3
- package/src/port-protocol.ts +4 -4
- package/src/reducer.ts +1 -1
- package/src/state.ts +89 -0
- package/src/stream.ts +4 -2
- package/src/tab.ts +3 -3
- package/src/transport.ts +1 -1
- package/src/versions.ts +2 -2
- package/src/wire.ts +5 -1
- package/src/worker.ts +2 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> A Server-Sent-Events client that resumes exactly where it stopped, notices a dead stream, and reconciles after a sleep.
|
|
4
4
|
|
|
5
|
-
The TypeScript half of [github.com/cplieger/go/sse](https://github.com/cplieger/go/tree/main/sse), speaking the wire the Go `sse` package serves: every frame carries an `epoch:offset` cursor, every connection opens with a hello that says whether that cursor was honoured, and the client turns both into a projection an application can trust across a page hide, a phone lock or a network change. A native `EventSource` cannot set a request header, cannot tell a silent stream from an idle one, and reconnects on its own schedule with whatever `Last-Event-ID` it last saw; this client owns the connection over `fetch` and `ReadableStream` instead, presents the cursor it holds, measures liveness on bytes, closes a hidden tab's stream after a minute and reopens it on return, backs off with full jitter, and holds incoming frames while the application asks the server what changed. It has no runtime dependencies, and the DOM readers (`document.visibilityState`, `navigator.onLine`) are injectable, so the same runtime runs in Node against the Go test fixture.
|
|
5
|
+
The TypeScript half of [github.com/cplieger/go/sse/v2](https://github.com/cplieger/go/tree/main/sse), speaking the wire the Go `sse` package serves: every frame carries an `epoch:offset` cursor, every connection opens with a hello that says whether that cursor was honoured, and the client turns both into a projection an application can trust across a page hide, a phone lock or a network change. A native `EventSource` cannot set a request header, cannot tell a silent stream from an idle one, and reconnects on its own schedule with whatever `Last-Event-ID` it last saw; this client owns the connection over `fetch` and `ReadableStream` instead, presents the cursor it holds, measures liveness on bytes, closes a hidden tab's stream after a minute and reopens it on return, backs off with full jitter, and holds incoming frames while the application asks the server what changed. It has no runtime dependencies, and the DOM readers (`document.visibilityState`, `navigator.onLine`) are injectable, so the same runtime runs in Node against the Go test fixture.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -44,6 +44,8 @@ The first frame of every connection is a hello: it names the server epoch, the r
|
|
|
44
44
|
|
|
45
45
|
The client keeps its cursor as `epoch:offset`, presents it as `Last-Event-ID` on every connect, and advances it only as frames are delivered to `onFrame`, so a reconnect after a gap either replays exactly the missed frames or reports through the hello that the gap could not be covered, and the application reconciles instead of trusting the replay.
|
|
46
46
|
|
|
47
|
+
`stream.state()` returns a `StreamState`: the connection kind (`stopped`, `connecting`, `open`, `backoff`, `offline` or `hidden_closed`), its generation and attempt, and the readings that kind acts on, such as `visible`, `online`, the hello's `keepaliveMs` and when a backoff ends. The package exports the factories, the wire helpers and the types they take and return; its parser, reducer and tab fold are internal.
|
|
48
|
+
|
|
47
49
|
While `revalidate` runs, incoming frames are held and delivered in order once it settles; runs are single-flight, a second cause queues at most one more run, and a held queue past 2000 frames or 64 MiB ends the connection so the next connect resumes from the cursor instead.
|
|
48
50
|
|
|
49
51
|
With `alive` set, every keepalive (or every `everyBeats`-th) is acknowledged with a `POST` to that URL carrying the stream's headers, so a server-side presence table keyed on `SSE-Client` sees which clients are still reading; a failed acknowledgement is reported as an `alive_ack` record.
|
|
@@ -54,7 +56,7 @@ A 401 ends the session's stream instead of retrying it: a 401 on the stream requ
|
|
|
54
56
|
|
|
55
57
|
The host presents the first non-empty tag a tab attaches with and replaces it, reconnecting once, when a later attach or `setTag` carries a different one; the empty tag is ignored.
|
|
56
58
|
|
|
57
|
-
The host's `revalidate(ctx, tabs)` body performs one digest for the profile and fans the verdict to every tab through `tabs.run(ctx, { changed, removed })`, so each tab's `revalidate` receives a `TabRevalidateContext` carrying `changed` and `removed` beside its own `signal`. A `state` message reaches a tab before the `lifecycle` record of the same transition, so `attachment.state()` is current inside the handler, and a tab attaching to a live stream receives `state` and then a `tab_attached` record naming that state.
|
|
59
|
+
The host's `revalidate(ctx, tabs)` body performs one digest for the profile and fans the verdict to every tab through `tabs.run(ctx, { changed, removed })`, so each tab's `revalidate` receives a `TabRevalidateContext` carrying `changed` and `removed` beside its own `signal`. A `state` message reaches a tab before the `lifecycle` record of the same transition, so `attachment.state()`, a `StreamState`, is current inside the handler, and a tab attaching to a live stream receives `state` and then a `tab_attached` record naming that state.
|
|
58
60
|
|
|
59
61
|
The package typechecks under `lib: ["ESNext", "WebWorker"]` as well as under the DOM lib (`tsconfig.worker.json` is the check), so a consumer's worker entry can import `createWorkerHost` from a worker-only program: the DOM visibility and online sources read `document` and `window` through `globalThis` and are inert where they are absent.
|
|
60
62
|
|
package/jsr.json
CHANGED
package/package.json
CHANGED
package/src/digest.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { Held, Subject } from "./versions.js";
|
|
|
3
3
|
import { compareOffset } from "./wire.js";
|
|
4
4
|
|
|
5
5
|
/** A subject the server reports at a version other than the presented one: a fetch instruction. */
|
|
6
|
-
export type
|
|
6
|
+
export type Changed = Held;
|
|
7
7
|
|
|
8
8
|
/** A subject the server no longer answers for. */
|
|
9
9
|
export interface Removed extends Subject {
|
|
@@ -19,7 +19,7 @@ export interface Removed extends Subject {
|
|
|
19
19
|
*/
|
|
20
20
|
export type DigestResult =
|
|
21
21
|
| { kind: "must_refetch"; epoch: string; head: string }
|
|
22
|
-
| { kind: "ok"; epoch: string; head: string; changed:
|
|
22
|
+
| { kind: "ok"; epoch: string; head: string; changed: Changed[]; removed: Removed[] };
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* What one check asks about: the epoch the held versions were minted under, and the versions
|
|
@@ -100,7 +100,7 @@ interface Position {
|
|
|
100
100
|
|
|
101
101
|
type BatchAnswer =
|
|
102
102
|
| { kind: "must_refetch"; position: Position }
|
|
103
|
-
| { kind: "ok"; position: Position; changed:
|
|
103
|
+
| { kind: "ok"; position: Position; changed: Changed[]; removed: Removed[] };
|
|
104
104
|
|
|
105
105
|
/** Reads the position; null when the body is not even a positioned digest answer. */
|
|
106
106
|
function readPosition(body: unknown): Position | null {
|
|
@@ -134,7 +134,7 @@ function readAnswer(
|
|
|
134
134
|
}
|
|
135
135
|
const requested = new Set(batch.map(subjectKey));
|
|
136
136
|
const seen = new Set<string>();
|
|
137
|
-
const changed:
|
|
137
|
+
const changed: Changed[] = [];
|
|
138
138
|
const removed: Removed[] = [];
|
|
139
139
|
for (const entry of changedRaw as unknown[]) {
|
|
140
140
|
if (!isRecord(entry)) {
|
|
@@ -259,7 +259,7 @@ export function createDigestClient(opts: DigestClientOptions): DigestClient {
|
|
|
259
259
|
batches.push(snapshot.held.slice(i, i + maxSubjects));
|
|
260
260
|
}
|
|
261
261
|
const [first = [], ...rest] = batches;
|
|
262
|
-
const changed:
|
|
262
|
+
const changed: Changed[] = [];
|
|
263
263
|
const removed: Removed[] = [];
|
|
264
264
|
let answer = await post(snapshot.epoch, first, signal);
|
|
265
265
|
for (let i = 0; ; i++) {
|
package/src/fold.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type { VisibilityEvent, VisibilityInput, VisibilitySource } from "./visib
|
|
|
7
7
|
* it leaves; the profile's network reading is not recomputed on a departure and keeps the last value
|
|
8
8
|
* reported, by whichever tab.
|
|
9
9
|
*/
|
|
10
|
-
|
|
10
|
+
interface TabState {
|
|
11
11
|
readonly visible: boolean;
|
|
12
12
|
readonly online: boolean;
|
|
13
13
|
/** A freshly attached tab may earn one pageshow; cleared by its first. */
|
package/src/index.ts
CHANGED
|
@@ -23,19 +23,9 @@ export {
|
|
|
23
23
|
validateHello,
|
|
24
24
|
} from "./wire.js";
|
|
25
25
|
export type { Cursor, Hello, HelloRefusal, HelloResult, ResetReason } from "./wire.js";
|
|
26
|
-
export {
|
|
27
|
-
export type { ParsedEvent, Parser, ParserHandlers, ParserOptions } from "./parser.js";
|
|
28
|
-
export { initialState, jitter, reduce } from "./reducer.js";
|
|
29
|
-
export type {
|
|
30
|
-
ClientEvent,
|
|
31
|
-
ClientState,
|
|
32
|
-
ConnectFailure,
|
|
33
|
-
Effect,
|
|
34
|
-
Reduction,
|
|
35
|
-
StreamEnd,
|
|
36
|
-
Timer,
|
|
37
|
-
} from "./reducer.js";
|
|
26
|
+
export type { ConnectFailure, StreamEnd } from "./reducer.js";
|
|
38
27
|
export type { HeldDiscardCause, LifecycleEvent, RevalidateCause } from "./lifecycle.js";
|
|
28
|
+
export type { StreamState, StreamStateKind } from "./state.js";
|
|
39
29
|
export {
|
|
40
30
|
adaptVisibility,
|
|
41
31
|
createDOMVisibilitySource,
|
|
@@ -50,40 +40,25 @@ export type {
|
|
|
50
40
|
export { createDOMOnlineSource, createOnlineManager } from "./online.js";
|
|
51
41
|
export type { OnlineManager, OnlineSource } from "./online.js";
|
|
52
42
|
export { bindListener, createVersionMap, staleListener } from "./versions.js";
|
|
53
|
-
export type {
|
|
43
|
+
export type {
|
|
44
|
+
BindListener,
|
|
45
|
+
Held,
|
|
46
|
+
StaleListener,
|
|
47
|
+
Subject,
|
|
48
|
+
VersionMap,
|
|
49
|
+
VersionMapOptions,
|
|
50
|
+
} from "./versions.js";
|
|
54
51
|
export { DigestStatusError, createDigestClient } from "./digest.js";
|
|
55
52
|
export type {
|
|
53
|
+
Changed,
|
|
56
54
|
DigestClient,
|
|
57
55
|
DigestClientOptions,
|
|
58
56
|
DigestResult,
|
|
59
57
|
DigestSnapshot,
|
|
60
58
|
Removed,
|
|
61
|
-
State,
|
|
62
59
|
} from "./digest.js";
|
|
63
60
|
export { createStream } from "./stream.js";
|
|
64
61
|
export type { Frame, RevalidateContext, Stream, StreamOptions } from "./stream.js";
|
|
65
|
-
export type {
|
|
66
|
-
Connection,
|
|
67
|
-
ConnectOptions,
|
|
68
|
-
TransportCallbacks,
|
|
69
|
-
TransportFrame,
|
|
70
|
-
} from "./transport.js";
|
|
71
|
-
export {
|
|
72
|
-
anyVisible,
|
|
73
|
-
createFoldState,
|
|
74
|
-
createProfileOnlineSource,
|
|
75
|
-
createProfileVisibilitySource,
|
|
76
|
-
foldTabs,
|
|
77
|
-
} from "./fold.js";
|
|
78
|
-
export type {
|
|
79
|
-
FoldInput,
|
|
80
|
-
FoldOutput,
|
|
81
|
-
FoldState,
|
|
82
|
-
ProfileOnlineSource,
|
|
83
|
-
ProfileVisibilitySource,
|
|
84
|
-
TabState,
|
|
85
|
-
} from "./fold.js";
|
|
86
|
-
export { isTabMessage, isWorkerMessage } from "./port-protocol.js";
|
|
87
62
|
export type {
|
|
88
63
|
DetachCause,
|
|
89
64
|
PortRevalidateContext,
|
package/src/lifecycle.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ConnectFailure } from "./reducer.js";
|
|
2
|
+
import type { StreamStateKind } from "./state.js";
|
|
2
3
|
import type { Subject } from "./versions.js";
|
|
3
4
|
import type { ResetReason } from "./wire.js";
|
|
4
5
|
|
|
@@ -22,7 +23,7 @@ export type HeldDiscardCause =
|
|
|
22
23
|
|
|
23
24
|
/** Every record the observability feed (`onLifecycle`) can carry. Nothing branches on one. */
|
|
24
25
|
export type LifecycleEvent =
|
|
25
|
-
| { kind: "state"; from:
|
|
26
|
+
| { kind: "state"; from: StreamStateKind; to: StreamStateKind; generation: number }
|
|
26
27
|
| {
|
|
27
28
|
kind: "hello";
|
|
28
29
|
verdict: string;
|
|
@@ -55,7 +56,7 @@ export type LifecycleEvent =
|
|
|
55
56
|
| { kind: "worker_unavailable"; cause: "silent" | "error" }
|
|
56
57
|
| { kind: "worker_recovered" }
|
|
57
58
|
| { kind: "auth_lost"; route: "stream" | "alive" | "digest"; status: 401 }
|
|
58
|
-
| { kind: "tab_attached"; replaced: boolean; state:
|
|
59
|
+
| { kind: "tab_attached"; replaced: boolean; state: StreamStateKind }
|
|
59
60
|
| { kind: "tab_detached"; cause: string }
|
|
60
61
|
| { kind: "port_expired"; sinceLastAckMs: number }
|
|
61
62
|
| { kind: "port_repaired" };
|
package/src/port-protocol.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Changed, Removed } from "./digest.js";
|
|
2
2
|
import type { LifecycleEvent, RevalidateCause } from "./lifecycle.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type { StreamState } from "./state.js";
|
|
4
4
|
import type { Frame } from "./stream.js";
|
|
5
5
|
import type { Subject } from "./versions.js";
|
|
6
6
|
import type { VisibilityInput } from "./visibility.js";
|
|
@@ -21,7 +21,7 @@ export interface PortRevalidateContext {
|
|
|
21
21
|
readonly generation: number;
|
|
22
22
|
readonly full: boolean;
|
|
23
23
|
/** The host's digest verdict for this run, when its `revalidate` body performed one. */
|
|
24
|
-
readonly changed?: readonly
|
|
24
|
+
readonly changed?: readonly Changed[];
|
|
25
25
|
readonly removed?: readonly Removed[];
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -80,7 +80,7 @@ export type WorkerToTab =
|
|
|
80
80
|
| { readonly type: "pong"; readonly seq: number }
|
|
81
81
|
| { readonly type: "revalidate_run"; readonly runId: number; readonly ctx: PortRevalidateContext }
|
|
82
82
|
| { readonly type: "stale" }
|
|
83
|
-
| { readonly type: "state"; readonly state:
|
|
83
|
+
| { readonly type: "state"; readonly state: StreamState };
|
|
84
84
|
|
|
85
85
|
const TAB_TYPES: ReadonlySet<string> = new Set<TabToWorker["type"]>([
|
|
86
86
|
"attach",
|
package/src/reducer.ts
CHANGED
|
@@ -157,7 +157,7 @@ export function initialState(): ClientState {
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
/** Full-jitter backoff delay for `attempt`; `rand` is drawn in [0, 1) by the runtime. */
|
|
160
|
-
|
|
160
|
+
function jitter(attempt: number, rand: number, cfg: TimingConfig): number {
|
|
161
161
|
return rand * Math.min(cfg.capMs, cfg.baseMs * 2 ** attempt);
|
|
162
162
|
}
|
|
163
163
|
|
package/src/state.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { ClientState } from "./reducer.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A stream's connection state as an application reads it: the kind, the
|
|
5
|
+
* generation, and the readings each kind acts on, including when a backoff
|
|
6
|
+
* ends. It leaves out the timers the stream keeps for itself (when it opened,
|
|
7
|
+
* its last byte, when it went hidden), so those can change without a breaking
|
|
8
|
+
* release.
|
|
9
|
+
*/
|
|
10
|
+
export type StreamState =
|
|
11
|
+
| { readonly kind: "stopped"; readonly generation: number }
|
|
12
|
+
| {
|
|
13
|
+
readonly kind: "connecting";
|
|
14
|
+
readonly generation: number;
|
|
15
|
+
readonly attempt: number;
|
|
16
|
+
readonly visible: boolean;
|
|
17
|
+
readonly online: boolean;
|
|
18
|
+
}
|
|
19
|
+
| {
|
|
20
|
+
readonly kind: "open";
|
|
21
|
+
readonly generation: number;
|
|
22
|
+
readonly attempt: number;
|
|
23
|
+
/** The hello's keepalive_ms. */
|
|
24
|
+
readonly keepaliveMs: number;
|
|
25
|
+
readonly visible: boolean;
|
|
26
|
+
readonly online: boolean;
|
|
27
|
+
}
|
|
28
|
+
| {
|
|
29
|
+
readonly kind: "backoff";
|
|
30
|
+
readonly generation: number;
|
|
31
|
+
readonly attempt: number;
|
|
32
|
+
/** When the next attempt starts, in milliseconds on the stream's clock. */
|
|
33
|
+
readonly until: number;
|
|
34
|
+
readonly visible: boolean;
|
|
35
|
+
readonly online: boolean;
|
|
36
|
+
}
|
|
37
|
+
| {
|
|
38
|
+
readonly kind: "offline";
|
|
39
|
+
readonly generation: number;
|
|
40
|
+
readonly attempt: number;
|
|
41
|
+
readonly visible: boolean;
|
|
42
|
+
}
|
|
43
|
+
| {
|
|
44
|
+
readonly kind: "hidden_closed";
|
|
45
|
+
readonly generation: number;
|
|
46
|
+
readonly attempt: number;
|
|
47
|
+
readonly online: boolean;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** The kinds a {@link StreamState} can take, as lifecycle `state` records name them. */
|
|
51
|
+
export type StreamStateKind = StreamState["kind"];
|
|
52
|
+
|
|
53
|
+
/** The application's view of the reducer's state: the same kind with its timers left out. */
|
|
54
|
+
export function toStreamState(s: ClientState): StreamState {
|
|
55
|
+
switch (s.kind) {
|
|
56
|
+
case "stopped":
|
|
57
|
+
return { kind: s.kind, generation: s.generation };
|
|
58
|
+
case "connecting":
|
|
59
|
+
return {
|
|
60
|
+
kind: s.kind,
|
|
61
|
+
generation: s.generation,
|
|
62
|
+
attempt: s.attempt,
|
|
63
|
+
visible: s.visible,
|
|
64
|
+
online: s.online,
|
|
65
|
+
};
|
|
66
|
+
case "backoff":
|
|
67
|
+
return {
|
|
68
|
+
kind: s.kind,
|
|
69
|
+
generation: s.generation,
|
|
70
|
+
attempt: s.attempt,
|
|
71
|
+
until: s.until,
|
|
72
|
+
visible: s.visible,
|
|
73
|
+
online: s.online,
|
|
74
|
+
};
|
|
75
|
+
case "open":
|
|
76
|
+
return {
|
|
77
|
+
kind: s.kind,
|
|
78
|
+
generation: s.generation,
|
|
79
|
+
attempt: s.attempt,
|
|
80
|
+
keepaliveMs: s.keepaliveMs,
|
|
81
|
+
visible: s.visible,
|
|
82
|
+
online: s.online,
|
|
83
|
+
};
|
|
84
|
+
case "offline":
|
|
85
|
+
return { kind: s.kind, generation: s.generation, attempt: s.attempt, visible: s.visible };
|
|
86
|
+
case "hidden_closed":
|
|
87
|
+
return { kind: s.kind, generation: s.generation, attempt: s.attempt, online: s.online };
|
|
88
|
+
}
|
|
89
|
+
}
|
package/src/stream.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
initialState,
|
|
10
10
|
reduce,
|
|
11
11
|
} from "./reducer.js";
|
|
12
|
+
import { type StreamState, toStreamState } from "./state.js";
|
|
12
13
|
import { DEFAULT_RETRY_MS, type TimingConfig, WIRE, resolveTiming } from "./timing.js";
|
|
13
14
|
import { type Connection, type TransportFrame, connect } from "./transport.js";
|
|
14
15
|
import { type VersionMap, bindListener, staleListener } from "./versions.js";
|
|
@@ -93,7 +94,8 @@ export interface Stream {
|
|
|
93
94
|
stop(): void;
|
|
94
95
|
/** Aborts the attempt and connects again in place; ignored in hidden_closed, offline and stopped. */
|
|
95
96
|
reconnect(): void;
|
|
96
|
-
state
|
|
97
|
+
/** The connection state, without the timers the stream keeps for itself. */
|
|
98
|
+
state(): StreamState;
|
|
97
99
|
cursor(): Cursor | null;
|
|
98
100
|
/** Drops the cursor; the next connect presents none and earns a fresh hello. */
|
|
99
101
|
resetCursor(): void;
|
|
@@ -769,7 +771,7 @@ export function createStream(opts: StreamOptions): Stream {
|
|
|
769
771
|
reconnect() {
|
|
770
772
|
dispatch({ type: "reconnect", now: now() });
|
|
771
773
|
},
|
|
772
|
-
state: () => state,
|
|
774
|
+
state: () => toStreamState(state),
|
|
773
775
|
cursor: () => cursor,
|
|
774
776
|
resetCursor() {
|
|
775
777
|
cursor = null;
|
package/src/tab.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
type WorkerToTab,
|
|
7
7
|
isWorkerMessage,
|
|
8
8
|
} from "./port-protocol.js";
|
|
9
|
-
import type {
|
|
9
|
+
import type { StreamState } from "./state.js";
|
|
10
10
|
import { type Frame, type Stream, statusOf } from "./stream.js";
|
|
11
11
|
import { DEFAULT_TIMING } from "./timing.js";
|
|
12
12
|
import type { Subject, VersionMap } from "./versions.js";
|
|
@@ -75,7 +75,7 @@ export interface TabAttachment {
|
|
|
75
75
|
/** Leaves the host; `logout` also ends the profile's stream, or this tab's fallback stream. */
|
|
76
76
|
detach(cause?: "unload" | "logout"): void;
|
|
77
77
|
/** The host's last state broadcast, or the fallback stream's own state. */
|
|
78
|
-
state():
|
|
78
|
+
state(): StreamState | null;
|
|
79
79
|
mode(): "worker" | "fallback";
|
|
80
80
|
/** Reports a stamp this tab applied, into the host's version map or the fallback's. */
|
|
81
81
|
observe(subject: Subject, version: string, epoch: string): void;
|
|
@@ -113,7 +113,7 @@ export function attachToWorker(opts: AttachOptions): TabAttachment {
|
|
|
113
113
|
let mode: "worker" | "fallback" = "worker";
|
|
114
114
|
let link: Link | null = null;
|
|
115
115
|
let fallback: TabFallback | null = null;
|
|
116
|
-
let lastState:
|
|
116
|
+
let lastState: StreamState | null = null;
|
|
117
117
|
let watchdog: ReturnType<typeof setTimeout> | null = null;
|
|
118
118
|
let lastPortMessageAt = Date.now();
|
|
119
119
|
let heartbeatSeen = false;
|
package/src/transport.ts
CHANGED
|
@@ -140,7 +140,7 @@ export function connect(opts: ConnectOptions): Connection {
|
|
|
140
140
|
fail({ kind: "bad_hello" });
|
|
141
141
|
return;
|
|
142
142
|
}
|
|
143
|
-
const result = validateHello(data, opts
|
|
143
|
+
const result = validateHello(data, opts);
|
|
144
144
|
if (!result.ok) {
|
|
145
145
|
fail(result.reason);
|
|
146
146
|
return;
|
package/src/versions.ts
CHANGED
|
@@ -40,8 +40,8 @@ export interface VersionMapOptions {
|
|
|
40
40
|
readonly onStale?: (subject: Subject, epoch: string) => void;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
type BindListener = (epoch: string, dropped: number) => void;
|
|
44
|
-
type StaleListener = (subject: Subject, epoch: string) => void;
|
|
43
|
+
export type BindListener = (epoch: string, dropped: number) => void;
|
|
44
|
+
export type StaleListener = (subject: Subject, epoch: string) => void;
|
|
45
45
|
|
|
46
46
|
interface Listeners {
|
|
47
47
|
readonly bound: Set<BindListener>;
|
package/src/wire.ts
CHANGED
|
@@ -94,7 +94,11 @@ const BAD_HELLO: HelloResult = { ok: false, reason: { kind: "bad_hello" } };
|
|
|
94
94
|
|
|
95
95
|
// Structural pessimism follows Centrifugo's recovered/was_recovering: only `resumed === true` resumes.
|
|
96
96
|
/** Validates a decoded hello payload against the wire contract and the accepted revision range. */
|
|
97
|
-
export function validateHello(
|
|
97
|
+
export function validateHello(
|
|
98
|
+
data: unknown,
|
|
99
|
+
range: { readonly minWire: number; readonly maxWire: number },
|
|
100
|
+
): HelloResult {
|
|
101
|
+
const { minWire, maxWire } = range;
|
|
98
102
|
if (typeof data !== "object" || data === null) {
|
|
99
103
|
return BAD_HELLO;
|
|
100
104
|
}
|
package/src/worker.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Changed, Removed } from "./digest.js";
|
|
2
2
|
import { createProfileOnlineSource, createProfileVisibilitySource } from "./fold.js";
|
|
3
3
|
import type { LifecycleEvent } from "./lifecycle.js";
|
|
4
4
|
import { createOnlineManager } from "./online.js";
|
|
@@ -20,7 +20,7 @@ import { createVisibilityManager } from "./visibility.js";
|
|
|
20
20
|
|
|
21
21
|
/** The changed and removed sets of one host-side digest, fanned to the tabs with the run. */
|
|
22
22
|
export interface DigestVerdict {
|
|
23
|
-
readonly changed: readonly
|
|
23
|
+
readonly changed: readonly Changed[];
|
|
24
24
|
readonly removed: readonly Removed[];
|
|
25
25
|
}
|
|
26
26
|
|