@hatk/hatk 0.0.1-alpha.79 → 0.0.1-alpha.80
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/dist/jetstream.d.ts +37 -1
- package/dist/jetstream.d.ts.map +1 -1
- package/dist/jetstream.js +72 -8
- package/package.json +1 -1
package/dist/jetstream.d.ts
CHANGED
|
@@ -18,6 +18,24 @@ import { type CommitOp, type IndexerCoreOpts } from './indexer.ts';
|
|
|
18
18
|
/** Server-side filter caps, rejected before the WebSocket upgrade. */
|
|
19
19
|
export declare const MAX_COLLECTIONS = 100;
|
|
20
20
|
export declare const MAX_DIDS = 10000;
|
|
21
|
+
/**
|
|
22
|
+
* Refused handshakes before the next attempt drops its cursor and probes live.
|
|
23
|
+
*
|
|
24
|
+
* Jetstream retains roughly a day of seqs. A cursor older than that is refused
|
|
25
|
+
* at the handshake — a bare close 1006, no `open`, no message. Nothing arrives,
|
|
26
|
+
* so {@link getLastSeq} stays null and a plain resume offers the same dead
|
|
27
|
+
* cursor forever. Restarting does not clear it either: the boot cursor is read
|
|
28
|
+
* back from the row this stream never got to advance. The stream wedges for
|
|
29
|
+
* good, while records written through the AppView's own path keep indexing, so
|
|
30
|
+
* nothing looks broken from inside the app.
|
|
31
|
+
*
|
|
32
|
+
* A cursorless probe tells a dead cursor from a dead instance without guessing:
|
|
33
|
+
* a reachable instance always accepts a cursorless subscribe, so a probe that
|
|
34
|
+
* opens means the cursor was the problem, and a probe that is refused too means
|
|
35
|
+
* the instance is down and the cursor is still worth keeping. Probing every Nth
|
|
36
|
+
* attempt rather than once keeps that true across an outage of any length.
|
|
37
|
+
*/
|
|
38
|
+
export declare const CURSOR_PROBE_EVERY = 3;
|
|
21
39
|
export interface JetstreamOpts extends IndexerCoreOpts {
|
|
22
40
|
/** Instance base URL, e.g. `wss://jetstream.us-east.bsky.network`. */
|
|
23
41
|
jetstreamUrl: string;
|
|
@@ -67,6 +85,18 @@ export declare function commitToOp(commit: JetstreamCommit, collections: Set<str
|
|
|
67
85
|
* Exported for tests.
|
|
68
86
|
*/
|
|
69
87
|
export declare function processEvent(payload: any, collections: Set<string>): void;
|
|
88
|
+
/**
|
|
89
|
+
* The cursor a connection attempt should offer, given how many attempts in a
|
|
90
|
+
* row were refused before it.
|
|
91
|
+
*
|
|
92
|
+
* `refusals` counts consecutive closes that never reached `open`; it resets the
|
|
93
|
+
* moment one does. Every {@link CURSOR_PROBE_EVERY}th refusal answers null so
|
|
94
|
+
* the attempt subscribes to the live tip instead — see the constant for why
|
|
95
|
+
* that is the discriminator.
|
|
96
|
+
*
|
|
97
|
+
* Exported for tests.
|
|
98
|
+
*/
|
|
99
|
+
export declare function reconnectCursor(refusals: number, liveSeq: number | null, bootCursor: string | null | undefined): string | null | undefined;
|
|
70
100
|
/**
|
|
71
101
|
* Connect to a Jetstream v2 instance and begin indexing.
|
|
72
102
|
*
|
|
@@ -75,8 +105,14 @@ export declare function processEvent(payload: any, collections: Set<string>): vo
|
|
|
75
105
|
* inclusive and delivery is at-least-once, so the event at the resume point
|
|
76
106
|
* arrives again — harmless, since writes upsert on the record's `at://` URI.
|
|
77
107
|
*
|
|
108
|
+
* `opts.cursor` is the boot-time cursor and stays fixed across reconnects;
|
|
109
|
+
* what each attempt actually offers comes from {@link reconnectCursor}, so a
|
|
110
|
+
* live probe can drop the cursor for one attempt without losing it.
|
|
111
|
+
*
|
|
112
|
+
* @param refusals Consecutive refused handshakes so far. Internal — reconnects
|
|
113
|
+
* pass their own count; callers start at 0.
|
|
78
114
|
* @returns The WebSocket connection (for shutdown coordination)
|
|
79
115
|
*/
|
|
80
|
-
export declare function startJetstreamIndexer(opts: JetstreamOpts): Promise<WebSocket>;
|
|
116
|
+
export declare function startJetstreamIndexer(opts: JetstreamOpts, refusals?: number): Promise<WebSocket>;
|
|
81
117
|
export {};
|
|
82
118
|
//# sourceMappingURL=jetstream.d.ts.map
|
package/dist/jetstream.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jetstream.d.ts","sourceRoot":"","sources":["../src/jetstream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EASL,KAAK,QAAQ,EACb,KAAK,eAAe,EACrB,MAAM,cAAc,CAAA;AAGrB,sEAAsE;AACtE,eAAO,MAAM,eAAe,MAAM,CAAA;AAClC,eAAO,MAAM,QAAQ,QAAS,CAAA;AAO9B,MAAM,WAAW,aAAc,SAAQ,eAAe;IACpD,sEAAsE;IACtE,YAAY,EAAE,MAAM,CAAA;IACpB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB;AAED,wDAAwD;AACxD,UAAU,eAAe;IACvB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CAC7B;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,EACxB,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,EAC/B,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,GACrB,MAAM,CASR;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,GAAG,IAAI,CAalG;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,eAAe,EAAE,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,QAAQ,GAAG,IAAI,CAY7F;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CAuBzE;AAED
|
|
1
|
+
{"version":3,"file":"jetstream.d.ts","sourceRoot":"","sources":["../src/jetstream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EASL,KAAK,QAAQ,EACb,KAAK,eAAe,EACrB,MAAM,cAAc,CAAA;AAGrB,sEAAsE;AACtE,eAAO,MAAM,eAAe,MAAM,CAAA;AAClC,eAAO,MAAM,QAAQ,QAAS,CAAA;AAO9B;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,kBAAkB,IAAI,CAAA;AAEnC,MAAM,WAAW,aAAc,SAAQ,eAAe;IACpD,sEAAsE;IACtE,YAAY,EAAE,MAAM,CAAA;IACpB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB;AAED,wDAAwD;AACxD,UAAU,eAAe;IACvB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CAC7B;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,EACxB,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,EAC/B,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,GACrB,MAAM,CASR;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,GAAG,IAAI,CAalG;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,eAAe,EAAE,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,QAAQ,GAAG,IAAI,CAY7F;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CAuBzE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GAAG,IAAI,EACtB,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACpC,MAAM,GAAG,IAAI,GAAG,SAAS,CAK3B;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,aAAa,EAAE,QAAQ,SAAI,GAAG,OAAO,CAAC,SAAS,CAAC,CA0DjG"}
|
package/dist/jetstream.js
CHANGED
|
@@ -22,6 +22,24 @@ export const MAX_DIDS = 10_000;
|
|
|
22
22
|
/** Event kinds hatk consumes. `commit` carries records; `identity` drives handle renames. */
|
|
23
23
|
const KINDS = ['commit', 'identity'];
|
|
24
24
|
const RECONNECT_DELAY_MS = 3000;
|
|
25
|
+
/**
|
|
26
|
+
* Refused handshakes before the next attempt drops its cursor and probes live.
|
|
27
|
+
*
|
|
28
|
+
* Jetstream retains roughly a day of seqs. A cursor older than that is refused
|
|
29
|
+
* at the handshake — a bare close 1006, no `open`, no message. Nothing arrives,
|
|
30
|
+
* so {@link getLastSeq} stays null and a plain resume offers the same dead
|
|
31
|
+
* cursor forever. Restarting does not clear it either: the boot cursor is read
|
|
32
|
+
* back from the row this stream never got to advance. The stream wedges for
|
|
33
|
+
* good, while records written through the AppView's own path keep indexing, so
|
|
34
|
+
* nothing looks broken from inside the app.
|
|
35
|
+
*
|
|
36
|
+
* A cursorless probe tells a dead cursor from a dead instance without guessing:
|
|
37
|
+
* a reachable instance always accepts a cursorless subscribe, so a probe that
|
|
38
|
+
* opens means the cursor was the problem, and a probe that is refused too means
|
|
39
|
+
* the instance is down and the cursor is still worth keeping. Probing every Nth
|
|
40
|
+
* attempt rather than once keeps that true across an outage of any length.
|
|
41
|
+
*/
|
|
42
|
+
export const CURSOR_PROBE_EVERY = 3;
|
|
25
43
|
/**
|
|
26
44
|
* Build the subscribe URL.
|
|
27
45
|
*
|
|
@@ -114,6 +132,24 @@ export function processEvent(payload, collections) {
|
|
|
114
132
|
return;
|
|
115
133
|
applyCommit(did, [op]);
|
|
116
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* The cursor a connection attempt should offer, given how many attempts in a
|
|
137
|
+
* row were refused before it.
|
|
138
|
+
*
|
|
139
|
+
* `refusals` counts consecutive closes that never reached `open`; it resets the
|
|
140
|
+
* moment one does. Every {@link CURSOR_PROBE_EVERY}th refusal answers null so
|
|
141
|
+
* the attempt subscribes to the live tip instead — see the constant for why
|
|
142
|
+
* that is the discriminator.
|
|
143
|
+
*
|
|
144
|
+
* Exported for tests.
|
|
145
|
+
*/
|
|
146
|
+
export function reconnectCursor(refusals, liveSeq, bootCursor) {
|
|
147
|
+
const resume = resumeCursor(liveSeq, bootCursor);
|
|
148
|
+
// Nothing to abandon — an attempt with no cursor is already a live tail.
|
|
149
|
+
if (!resume)
|
|
150
|
+
return resume;
|
|
151
|
+
return refusals > 0 && refusals % CURSOR_PROBE_EVERY === 0 ? null : resume;
|
|
152
|
+
}
|
|
117
153
|
/**
|
|
118
154
|
* Connect to a Jetstream v2 instance and begin indexing.
|
|
119
155
|
*
|
|
@@ -122,17 +158,26 @@ export function processEvent(payload, collections) {
|
|
|
122
158
|
* inclusive and delivery is at-least-once, so the event at the resume point
|
|
123
159
|
* arrives again — harmless, since writes upsert on the record's `at://` URI.
|
|
124
160
|
*
|
|
161
|
+
* `opts.cursor` is the boot-time cursor and stays fixed across reconnects;
|
|
162
|
+
* what each attempt actually offers comes from {@link reconnectCursor}, so a
|
|
163
|
+
* live probe can drop the cursor for one attempt without losing it.
|
|
164
|
+
*
|
|
165
|
+
* @param refusals Consecutive refused handshakes so far. Internal — reconnects
|
|
166
|
+
* pass their own count; callers start at 0.
|
|
125
167
|
* @returns The WebSocket connection (for shutdown coordination)
|
|
126
168
|
*/
|
|
127
|
-
export async function startJetstreamIndexer(opts) {
|
|
128
|
-
const { jetstreamUrl, collections
|
|
169
|
+
export async function startJetstreamIndexer(opts, refusals = 0) {
|
|
170
|
+
const { jetstreamUrl, collections } = opts;
|
|
129
171
|
const pinnedRepos = opts.pinnedRepos || null;
|
|
130
172
|
assertFilterLimits(collections, pinnedRepos);
|
|
131
173
|
setCursorKey('jetstream');
|
|
132
174
|
await configureIndexer(opts);
|
|
175
|
+
const cursor = reconnectCursor(refusals, getLastSeq(), opts.cursor);
|
|
133
176
|
const wsUrl = buildSubscribeUrl(jetstreamUrl, collections, pinnedRepos, cursor);
|
|
134
177
|
if (cursor)
|
|
135
178
|
log(`[jetstream] Resuming from cursor ${cursor}`);
|
|
179
|
+
else if (refusals > 0)
|
|
180
|
+
log('[jetstream] Probing the live tip — the resume cursor keeps being refused');
|
|
136
181
|
log(`[jetstream] Connecting to ${jetstreamUrl} (${collections.size} collections)...`);
|
|
137
182
|
// The lexicon default is identical framing, so an empty subprotocol echo is
|
|
138
183
|
// fine; we offer it so the server can pick the JSON arm explicitly.
|
|
@@ -150,12 +195,31 @@ export async function startJetstreamIndexer(opts) {
|
|
|
150
195
|
emit('jetstream', 'decode_error', { error: err instanceof Error ? err.message : String(err) });
|
|
151
196
|
}
|
|
152
197
|
});
|
|
153
|
-
|
|
154
|
-
ws.addEventListener('
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
198
|
+
let opened = false;
|
|
199
|
+
ws.addEventListener('open', () => {
|
|
200
|
+
opened = true;
|
|
201
|
+
log('[jetstream] Connected');
|
|
202
|
+
});
|
|
203
|
+
ws.addEventListener('close', (event) => {
|
|
204
|
+
// A close that never reached `open` is a refused handshake, not a dropped
|
|
205
|
+
// stream, and the two want opposite things from the cursor.
|
|
206
|
+
const nextRefusals = opened ? 0 : refusals + 1;
|
|
207
|
+
if (opened) {
|
|
208
|
+
log(`[jetstream] Disconnected (${event.code}), reconnecting in ${RECONNECT_DELAY_MS / 1000}s...`);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
log(`[jetstream] Handshake refused (${event.code}${event.reason ? `: ${event.reason}` : ''}) ` +
|
|
212
|
+
`x${nextRefusals}, retrying in ${RECONNECT_DELAY_MS / 1000}s...`);
|
|
213
|
+
emit('jetstream', 'handshake_refused', {
|
|
214
|
+
code: event.code,
|
|
215
|
+
reason: event.reason || undefined,
|
|
216
|
+
cursor: cursor ?? null,
|
|
217
|
+
refusals: nextRefusals,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
// `opts` goes back unchanged so the boot cursor survives a probe; the seq
|
|
221
|
+
// is read at reconnect time, not here, so a resume uses everything seen.
|
|
222
|
+
setTimeout(() => startJetstreamIndexer(opts, nextRefusals), RECONNECT_DELAY_MS);
|
|
159
223
|
});
|
|
160
224
|
return ws;
|
|
161
225
|
}
|