@khorsheed/dsh-reader 0.2.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.en.md +90 -0
- package/README.i18n.yaml +6 -0
- package/README.md +90 -0
- package/cordis.patch.yml +13 -0
- package/lib/client.js +15119 -0
- package/lib/index.js +3660 -0
- package/lib/invariant.js +38 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +1738 -0
- package/lib/typert.remote-client.d.ts +82 -0
- package/lib/typert.remote-client.js +1115 -0
- package/lib/types/arxiv.d.ts +47 -0
- package/lib/types/arxiv.js +76 -0
- package/lib/types/client/ReaderPane.d.ts +71 -0
- package/lib/types/client/ReaderPane.js +3135 -0
- package/lib/types/client/contract.d.ts +254 -0
- package/lib/types/client/contract.js +1 -0
- package/lib/types/client/definition.d.ts +27 -0
- package/lib/types/client/definition.js +32 -0
- package/lib/types/client/extract-article.d.ts +102 -0
- package/lib/types/client/extract-article.js +1024 -0
- package/lib/types/client/icons.d.ts +27 -0
- package/lib/types/client/icons.js +11 -0
- package/lib/types/client/index.d.ts +34 -0
- package/lib/types/client/index.js +222 -0
- package/lib/types/client/locales.d.ts +391 -0
- package/lib/types/client/locales.js +382 -0
- package/lib/types/client/parse-rss.d.ts +69 -0
- package/lib/types/client/parse-rss.js +656 -0
- package/lib/types/client/quote.d.ts +89 -0
- package/lib/types/client/quote.js +119 -0
- package/lib/types/client/selectors.d.ts +167 -0
- package/lib/types/client/selectors.js +413 -0
- package/lib/types/client/session.d.ts +171 -0
- package/lib/types/client/session.js +223 -0
- package/lib/types/client/store.d.ts +181 -0
- package/lib/types/client/store.js +226 -0
- package/lib/types/client/translate.d.ts +379 -0
- package/lib/types/client/translate.js +873 -0
- package/lib/types/index.d.ts +48 -0
- package/lib/types/index.js +25 -0
- package/lib/types/invariant.d.ts +29 -0
- package/lib/types/invariant.js +38 -0
- package/lib/types/link-resolvers.d.ts +107 -0
- package/lib/types/link-resolvers.js +299 -0
- package/lib/types/remote.d.ts +224 -0
- package/lib/types/remote.js +275 -0
- package/lib/types/schedule.d.ts +78 -0
- package/lib/types/schedule.js +114 -0
- package/lib/types/service.d.ts +633 -0
- package/lib/types/service.js +1697 -0
- package/lib/types/store.d.ts +224 -0
- package/lib/types/store.js +916 -0
- package/lib/types/types.d.ts +710 -0
- package/lib/types/types.js +182 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +128 -0
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared vocabulary of `@khorsheed/dsh-reader`: the persisted state document,
|
|
3
|
+
* the Remote wire payloads, and the pure helpers both faces share.
|
|
4
|
+
*
|
|
5
|
+
* Two responsibilities are deliberately split across the process boundary:
|
|
6
|
+
*
|
|
7
|
+
* - the HOST half owns the network (the sanctioned `ctx.web` fetch seam) and
|
|
8
|
+
* the disk (one version-guarded JSON document over `ctx.fs`). It stores the
|
|
9
|
+
* LAST RAW PAYLOAD per source — feed XML or article HTML — plus fetch
|
|
10
|
+
* metadata, and never parses either.
|
|
11
|
+
* - the BROWSER half owns parsing. The host runtime has no XML/DOM parser at
|
|
12
|
+
* all (Node's `globalThis.DOMParser` is absent), so the single parser lives
|
|
13
|
+
* in the browser where `DOMParser` is native, and its output never needs to
|
|
14
|
+
* be persisted: entries and extracted article bodies are session state.
|
|
15
|
+
*
|
|
16
|
+
* @module @khorsheed/dsh-reader/types
|
|
17
|
+
*/
|
|
18
|
+
/** What a source is: a syndication feed, or one manually saved article link. */
|
|
19
|
+
export type ReaderSourceKind = 'rss' | 'link';
|
|
20
|
+
/** Fetch state of one source's most recent attempt. */
|
|
21
|
+
export type ReaderFetchStatus = 'ok' | 'fetching' | 'error';
|
|
22
|
+
/**
|
|
23
|
+
* Why a source has no previewable body.
|
|
24
|
+
*
|
|
25
|
+
* A saved link is a link the reader OWNS even when its body cannot be read
|
|
26
|
+
* here, so these are not add failures: they are the reason the card says
|
|
27
|
+
* "link only" and the detail view offers the original instead of a blank
|
|
28
|
+
* page. The set is deliberately small and each member has its own sentence —
|
|
29
|
+
* "collected but not previewable" with no cause is a worse answer than a
|
|
30
|
+
* wrong one, and the three real causes (a bot wall, a login wall, a
|
|
31
|
+
* non-web file) send the reader to different actions.
|
|
32
|
+
*/
|
|
33
|
+
export type ReaderPreviewFailureCode =
|
|
34
|
+
/** The site refuses non-browser requests, or answered with a bot challenge. */
|
|
35
|
+
'blocked'
|
|
36
|
+
/** The target sits behind an authentication wall (institutional proxy, SSO). */
|
|
37
|
+
| 'login'
|
|
38
|
+
/** The response is not a web page at all (a PDF or another file type). */
|
|
39
|
+
| 'unsupported-type'
|
|
40
|
+
/** The address hops to a different site, which the host seam refuses to follow. */
|
|
41
|
+
| 'redirected'
|
|
42
|
+
/** The page yielded nothing to read (an interstitial, an empty shell). */
|
|
43
|
+
| 'empty'
|
|
44
|
+
/** The request never completed: network, timeout, or a 5xx. The retryable one. */
|
|
45
|
+
| 'unreachable'
|
|
46
|
+
/** The address answered with an HTTP error status (404 and friends). */
|
|
47
|
+
| 'http'
|
|
48
|
+
/**
|
|
49
|
+
* The host is KNOWN to be unreadable server-side without a request (a
|
|
50
|
+
* script-rendered app behind an anti-bot wall — OpenReview, measured). The
|
|
51
|
+
* sentence for it carries the way out (paste the arXiv version / open in a
|
|
52
|
+
* browser), so it is not folded into `blocked`, which means "we tried and
|
|
53
|
+
* the site answered a wall".
|
|
54
|
+
*/
|
|
55
|
+
| 'unreadable';
|
|
56
|
+
/**
|
|
57
|
+
* Whether an automatic retry is worth the request.
|
|
58
|
+
*
|
|
59
|
+
* Only a transport failure is: a bot wall, a login wall, a PDF and a 404 all
|
|
60
|
+
* answer the same way forever, so retrying them on a timer would be a crawler
|
|
61
|
+
* with a grudge rather than a reader. The manual refresh stays available.
|
|
62
|
+
*
|
|
63
|
+
* @param code - the recorded failure.
|
|
64
|
+
* @returns true when the entry may be retried automatically.
|
|
65
|
+
*/
|
|
66
|
+
export declare function isRetryablePreviewFailure(code: ReaderPreviewFailureCode): boolean;
|
|
67
|
+
/** A recorded reason that a source has no previewable body. */
|
|
68
|
+
export interface ReaderPreviewFailure {
|
|
69
|
+
readonly code: ReaderPreviewFailureCode;
|
|
70
|
+
/** The seam's or the classifier's own words, kept for diagnosis. */
|
|
71
|
+
readonly message: string;
|
|
72
|
+
/** When the failure was recorded (ISO-8601). */
|
|
73
|
+
readonly at: string;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* One source of entries.
|
|
77
|
+
*
|
|
78
|
+
* A `link` source always holds exactly one entry (the article itself) and is
|
|
79
|
+
* created by pasting a URL; an `rss` source holds however many items its feed
|
|
80
|
+
* declares.
|
|
81
|
+
*/
|
|
82
|
+
export interface ReaderSource {
|
|
83
|
+
readonly id: string;
|
|
84
|
+
readonly kind: ReaderSourceKind;
|
|
85
|
+
/** Feed URL (normalized after redirects), or the article URL for a link. */
|
|
86
|
+
readonly url: string;
|
|
87
|
+
/** User-facing name; falls back to the URL's host plus last path segment. */
|
|
88
|
+
readonly label?: string;
|
|
89
|
+
readonly enabled: boolean;
|
|
90
|
+
/** ISO-8601 timestamp of when the user added it. */
|
|
91
|
+
readonly addedAt: string;
|
|
92
|
+
/** ISO-8601 timestamp of the last completed fetch attempt. */
|
|
93
|
+
readonly fetchedAt?: string;
|
|
94
|
+
readonly status?: ReaderFetchStatus;
|
|
95
|
+
/** Human-readable failure of the last attempt, when `status` is `error`. */
|
|
96
|
+
readonly error?: string;
|
|
97
|
+
/**
|
|
98
|
+
* True when the host's fetch seam cut the payload at its size limit
|
|
99
|
+
* (`maxBodyChars`, 100 000 by default). A truncated XML document cannot be
|
|
100
|
+
* parsed and a truncated HTML page yields a partial article, so every
|
|
101
|
+
* consumer treats this as "incomplete" and says so rather than guessing.
|
|
102
|
+
*/
|
|
103
|
+
readonly truncated?: boolean;
|
|
104
|
+
/**
|
|
105
|
+
* Why this source has no previewable body, when it has none.
|
|
106
|
+
*
|
|
107
|
+
* A saved link keeps its identity and its URL even when the body cannot be
|
|
108
|
+
* read, so this is recorded instead of the source being dropped: losing a
|
|
109
|
+
* link the reader deliberately saved is a worse outcome than a card that
|
|
110
|
+
* says it can only be opened in a browser.
|
|
111
|
+
*/
|
|
112
|
+
readonly failure?: ReaderPreviewFailure;
|
|
113
|
+
/**
|
|
114
|
+
* The URL the reader pasted, when this source was resolved to a canonical
|
|
115
|
+
* version of the same work (a DOI that gated into its arXiv version). The
|
|
116
|
+
* source's own `url` is the resolved one — it is what refreshes fetch — and
|
|
117
|
+
* this keeps the provenance visible.
|
|
118
|
+
*/
|
|
119
|
+
readonly resolvedFrom?: string;
|
|
120
|
+
/** The last raw payload: feed XML for `rss`, article HTML for `link`. */
|
|
121
|
+
readonly raw?: string;
|
|
122
|
+
}
|
|
123
|
+
/** The whole persisted document: one JSON file under the plugin's state root. */
|
|
124
|
+
export interface ReaderStateDoc {
|
|
125
|
+
readonly version: 1;
|
|
126
|
+
readonly sources: readonly ReaderSource[];
|
|
127
|
+
readonly refresh: ReaderRefreshConfig;
|
|
128
|
+
/** How long fetched article bodies are served, and how many are kept. */
|
|
129
|
+
readonly cache?: ReaderCachePolicy;
|
|
130
|
+
/** The reader's tag vocabulary, keyed by tag id. */
|
|
131
|
+
readonly tags?: Readonly<Record<string, ReaderTag>>;
|
|
132
|
+
/** ISO-8601 timestamp of the last completed refresh run. */
|
|
133
|
+
readonly lastRefreshAt?: string;
|
|
134
|
+
/**
|
|
135
|
+
* The reader's own annotations, keyed by ENTRY id.
|
|
136
|
+
*
|
|
137
|
+
* Deliberately a separate table from the sources: an entry belongs to a feed
|
|
138
|
+
* that may drop it tomorrow, while a tag the reader put on it is theirs and
|
|
139
|
+
* must outlive the feed's window. It is also the only place a fetched article
|
|
140
|
+
* body can live — the sources hold raw payloads, not per-entry markup.
|
|
141
|
+
*/
|
|
142
|
+
readonly annotations?: Readonly<Record<string, ReaderEntryAnnotation>>;
|
|
143
|
+
/**
|
|
144
|
+
* The global sentence memory's manifest; the TABLE lives in its own
|
|
145
|
+
* `bodies/` file.
|
|
146
|
+
*
|
|
147
|
+
* Kept out of the document body on purpose: the table is bounded at 50 000
|
|
148
|
+
* sentences (several MB of JSON), and this document is read and rewritten
|
|
149
|
+
* whole on every commit — including one per article opened (`recordRead`).
|
|
150
|
+
* The manifest is all the commit path needs.
|
|
151
|
+
*/
|
|
152
|
+
readonly translationMemory?: ReaderTranslationMemoryManifest;
|
|
153
|
+
/**
|
|
154
|
+
* What the reader opened, newest first (the 「最近阅读」 page).
|
|
155
|
+
*
|
|
156
|
+
* Persisted, unlike the pane's session memory: "what was I reading" is a fact
|
|
157
|
+
* about the reader's own behaviour that has to outlive a reload and a restart,
|
|
158
|
+
* and it holds no third-party text — a title and a URL the reader already
|
|
159
|
+
* asked for. It is NOT the read cursor (that stays per session, in the pane).
|
|
160
|
+
*/
|
|
161
|
+
readonly recent?: readonly ReaderRecentEntry[];
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* One entry the reader opened, as the 「最近阅读」 page lists it.
|
|
165
|
+
*
|
|
166
|
+
* The title and URL are stored rather than looked up because the feed that
|
|
167
|
+
* published the entry may have rolled it out of its window: a recent list that
|
|
168
|
+
* silently drops yesterday's article is not a record of what was read.
|
|
169
|
+
*/
|
|
170
|
+
export interface ReaderRecentEntry {
|
|
171
|
+
readonly entryId: string;
|
|
172
|
+
readonly sourceId: string;
|
|
173
|
+
readonly title: string;
|
|
174
|
+
/** The article URL, for reopening an entry the feed no longer publishes. */
|
|
175
|
+
readonly url?: string;
|
|
176
|
+
/** When it was opened (ISO-8601). */
|
|
177
|
+
readonly readAt: string;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* What the plugin holds for one entry: a body, a stored raw payload, or why not.
|
|
181
|
+
*
|
|
182
|
+
* `raw` is the state that makes a fetch resumable: the payload is on disk and the
|
|
183
|
+
* browser half has not extracted it yet, so closing the page costs nothing.
|
|
184
|
+
*/
|
|
185
|
+
export type ReaderEntryFetchState = {
|
|
186
|
+
readonly state: 'none';
|
|
187
|
+
} | {
|
|
188
|
+
readonly state: 'fetching';
|
|
189
|
+
readonly at: string;
|
|
190
|
+
} | {
|
|
191
|
+
readonly state: 'raw';
|
|
192
|
+
readonly at: string;
|
|
193
|
+
} | {
|
|
194
|
+
readonly state: 'ready';
|
|
195
|
+
readonly at?: string;
|
|
196
|
+
} | {
|
|
197
|
+
readonly state: 'failed';
|
|
198
|
+
readonly at: string;
|
|
199
|
+
readonly message: string;
|
|
200
|
+
readonly code?: ReaderPreviewFailureCode;
|
|
201
|
+
};
|
|
202
|
+
/** The persisted twin of the in-flight/raw half of {@link ReaderEntryFetchState}. */
|
|
203
|
+
export interface ReaderEntryFetchRecord {
|
|
204
|
+
readonly state: 'fetching' | 'raw';
|
|
205
|
+
readonly at: string;
|
|
206
|
+
/** The `bodies/` file holding the raw payload, once it has been written. */
|
|
207
|
+
readonly rawFile?: string;
|
|
208
|
+
/** The payload's character count (the document itself stays out of `state.json`). */
|
|
209
|
+
readonly chars?: number;
|
|
210
|
+
/** Where the payload came from, so an edited URL is detectable. */
|
|
211
|
+
readonly url?: string;
|
|
212
|
+
/** True when the seam capped the payload. */
|
|
213
|
+
readonly truncated?: boolean;
|
|
214
|
+
}
|
|
215
|
+
/** One entry's reader-authored state (see the annotations section below). */
|
|
216
|
+
export interface ReaderEntryAnnotation {
|
|
217
|
+
/** The fetched full text, when one was asked for and arrived. */
|
|
218
|
+
readonly body?: ReaderEntryBody;
|
|
219
|
+
/** Tag ids on this entry, in the order they were applied. */
|
|
220
|
+
readonly tagIds?: readonly string[];
|
|
221
|
+
/** A fetch in flight, or a payload fetched and still waiting to be extracted. */
|
|
222
|
+
readonly fetch?: ReaderEntryFetchRecord;
|
|
223
|
+
/** Why the last fetch failed, so a retry is a decision and not a loop. */
|
|
224
|
+
readonly error?: string;
|
|
225
|
+
/** The classified reason, so the wall can show the same sentence as the detail. */
|
|
226
|
+
readonly failureCode?: ReaderPreviewFailureCode;
|
|
227
|
+
/** When that failure was recorded (ISO-8601). */
|
|
228
|
+
readonly failedAt?: string;
|
|
229
|
+
/**
|
|
230
|
+
* The article's own title, captured at extraction.
|
|
231
|
+
*
|
|
232
|
+
* A saved link's entry is synthesized from the source's URL-derived label;
|
|
233
|
+
* once the body has been extracted the paper's real name lives here, and the
|
|
234
|
+
* wall card prefers it. Deliberately survives the body: past eviction it is
|
|
235
|
+
* the card's memory of what the paper was. Only ever written for a link
|
|
236
|
+
* source's single entry — a feed entry's title is already the publisher's.
|
|
237
|
+
*/
|
|
238
|
+
readonly title?: string;
|
|
239
|
+
/** A short excerpt (the abstract / first paragraph), captured with the title. */
|
|
240
|
+
readonly excerpt?: string;
|
|
241
|
+
/**
|
|
242
|
+
* This entry's translated segment map (a hash→translation table, never
|
|
243
|
+
* markup). No TTL: a translation costs a gesture plus per-sentence model work
|
|
244
|
+
* to rebuild, so only the shared translation budget evicts it.
|
|
245
|
+
*/
|
|
246
|
+
readonly translation?: ReaderEntryTranslation;
|
|
247
|
+
}
|
|
248
|
+
/** How long a fetched article body is served before the reader is offered a refetch. */
|
|
249
|
+
export interface ReaderCachePolicy {
|
|
250
|
+
/** 0 means "never expires" (the body is kept until the budget evicts it). */
|
|
251
|
+
readonly ttlHours: number;
|
|
252
|
+
/** How many entry bodies may be retained at once (oldest evicted first). */
|
|
253
|
+
readonly maxEntries: number;
|
|
254
|
+
/**
|
|
255
|
+
* How large the two translation tiers may grow together, in characters.
|
|
256
|
+
*
|
|
257
|
+
* Absent means {@link DEFAULT_TRANSLATION_BUDGET_CHARS}. Translations carry no
|
|
258
|
+
* TTL (rebuilding one costs a user gesture plus per-sentence model work, so
|
|
259
|
+
* they must not inherit the body TTL) — this budget, LRU by last use across
|
|
260
|
+
* both tiers, is the only eviction.
|
|
261
|
+
*/
|
|
262
|
+
readonly translationBudgetChars?: number;
|
|
263
|
+
}
|
|
264
|
+
/** The default cache policy: a day, and a few hundred articles. */
|
|
265
|
+
export declare const DEFAULT_CACHE_POLICY: ReaderCachePolicy;
|
|
266
|
+
/**
|
|
267
|
+
* The schema version of the persisted translation tiers (the global sentence
|
|
268
|
+
* memory's table, and each entry's segment map). A browser Translator model
|
|
269
|
+
* upgrade quietly changes what the same sentence translates to, and a
|
|
270
|
+
* segmentation change here changes what a "sentence" is — bumping this turns
|
|
271
|
+
* every old record into a MISS. Records are evicted LAZILY (the next write or
|
|
272
|
+
* budget pass drops them), never wiped eagerly.
|
|
273
|
+
*/
|
|
274
|
+
export declare const TRANSLATION_STORAGE_VERSION = 1;
|
|
275
|
+
/**
|
|
276
|
+
* The default total budget for both translation tiers, in characters — 64 MB
|
|
277
|
+
* worth of text. Counted in characters, like every other budget in this
|
|
278
|
+
* document (a CJK sentence costs more bytes than that reads; the approximation
|
|
279
|
+
* is the store's existing convention).
|
|
280
|
+
*/
|
|
281
|
+
export declare const DEFAULT_TRANSLATION_BUDGET_CHARS: number;
|
|
282
|
+
/** How many sentences the global memory keeps before its own LRU end is evicted. */
|
|
283
|
+
export declare const MAX_TRANSLATION_MEMORY_ENTRIES = 50000;
|
|
284
|
+
/** One remembered sentence translation — the global tier's value. */
|
|
285
|
+
export interface ReaderTranslationMemoryEntry {
|
|
286
|
+
/** The source sentence, verbatim (the hash in the entry's key is over this). */
|
|
287
|
+
readonly source: string;
|
|
288
|
+
/** What the translator answered. */
|
|
289
|
+
readonly target: string;
|
|
290
|
+
/** The LRU clock: when the entry last served a translation (or was written). */
|
|
291
|
+
readonly lastUsedAt: string;
|
|
292
|
+
}
|
|
293
|
+
/** The global memory file's content; the table lives in `bodies/`, not inline. */
|
|
294
|
+
export interface ReaderTranslationMemoryTable {
|
|
295
|
+
readonly version: number;
|
|
296
|
+
readonly entries: Readonly<Record<string, ReaderTranslationMemoryEntry>>;
|
|
297
|
+
}
|
|
298
|
+
/** The document's pointer at the global memory file. */
|
|
299
|
+
export interface ReaderTranslationMemoryManifest {
|
|
300
|
+
readonly version: number;
|
|
301
|
+
/** The `bodies/` file holding the table (a fixed, safe name). */
|
|
302
|
+
readonly file: string;
|
|
303
|
+
readonly entries: number;
|
|
304
|
+
readonly chars: number;
|
|
305
|
+
readonly updatedAt: string;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* One entry's translation of its body: a map of sentence hash to translation —
|
|
309
|
+
* never translated markup, so a body re-fetched with minor edits still reuses
|
|
310
|
+
* every unchanged sentence through the global tier, while a `bodyHash`
|
|
311
|
+
* mismatch invalidates only this exact-fit record.
|
|
312
|
+
*/
|
|
313
|
+
export interface ReaderEntryTranslation {
|
|
314
|
+
readonly version: number;
|
|
315
|
+
/** The `<src>→<tgt>` label the map was built under. */
|
|
316
|
+
readonly pair: string;
|
|
317
|
+
/** Hash of the normalized body the segments were cut from. */
|
|
318
|
+
readonly bodyHash: string;
|
|
319
|
+
/** sentenceHash → translation, inline while the map is small. */
|
|
320
|
+
readonly segments?: Readonly<Record<string, string>>;
|
|
321
|
+
/** The `bodies/` file holding the segment map once it is large. */
|
|
322
|
+
readonly file?: string;
|
|
323
|
+
/** The map's serialized size, for the shared budget. */
|
|
324
|
+
readonly chars?: number;
|
|
325
|
+
readonly translatedAt: string;
|
|
326
|
+
/** The shared-budget LRU clock. */
|
|
327
|
+
readonly lastUsedAt: string;
|
|
328
|
+
}
|
|
329
|
+
/** One sentence the browser half learned (or was served), as it reports it. */
|
|
330
|
+
export interface ReaderSentenceLearn {
|
|
331
|
+
/** The client's `translationHash(source)`, stored opaquely. */
|
|
332
|
+
readonly hash: string;
|
|
333
|
+
readonly source: string;
|
|
334
|
+
readonly target: string;
|
|
335
|
+
}
|
|
336
|
+
/** An entry translation as the wire serves it: the segment map resolved. */
|
|
337
|
+
export interface ReaderEntryTranslationView {
|
|
338
|
+
readonly pair: string;
|
|
339
|
+
readonly bodyHash: string;
|
|
340
|
+
readonly segments: Record<string, string>;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* How much the two caches currently hold, aggregated on the host.
|
|
344
|
+
*
|
|
345
|
+
* Counts and characters only — the tables themselves never cross the wire for
|
|
346
|
+
* a settings readout. `chars` is the store's accounting unit (characters, the
|
|
347
|
+
* same unit the budgets are written in).
|
|
348
|
+
*/
|
|
349
|
+
export interface ReaderStorageStats {
|
|
350
|
+
/** The cached article bodies (what TTL + maxEntries govern). */
|
|
351
|
+
readonly bodies: {
|
|
352
|
+
readonly entries: number;
|
|
353
|
+
readonly chars: number;
|
|
354
|
+
};
|
|
355
|
+
/** The two translation tiers: per-entry maps, and the global sentence memory. */
|
|
356
|
+
readonly translations: {
|
|
357
|
+
/** How many entries carry a segment map. */
|
|
358
|
+
readonly entries: number;
|
|
359
|
+
/** The maps' combined size. */
|
|
360
|
+
readonly chars: number;
|
|
361
|
+
/** The global memory's sentence count (0 when no table exists). */
|
|
362
|
+
readonly memoryEntries: number;
|
|
363
|
+
/** The global memory's size (0 when no table exists). */
|
|
364
|
+
readonly memoryChars: number;
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
export interface ReaderRefreshConfig {
|
|
368
|
+
readonly enabled: boolean;
|
|
369
|
+
/** Local time of day as `HH:MM`; defaults to `10:00`. */
|
|
370
|
+
readonly timeOfDay: string;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* One parsed entry. Produced by the browser half and never persisted.
|
|
374
|
+
*
|
|
375
|
+
* `contentHtml` is the best body the reader could assemble: a feed's
|
|
376
|
+
* `<content:encoded>` / `<content>` when it ships full text, otherwise the
|
|
377
|
+
* article body extracted from the fetched page. It is always the output of the
|
|
378
|
+
* whitelist normalizer, never raw third-party markup.
|
|
379
|
+
*/
|
|
380
|
+
export interface ReaderEntry {
|
|
381
|
+
/** Stable id: the feed's guid/id, else the link, else a title+link hash. */
|
|
382
|
+
readonly id: string;
|
|
383
|
+
readonly sourceId: string;
|
|
384
|
+
readonly title: string;
|
|
385
|
+
readonly link?: string;
|
|
386
|
+
readonly author?: string;
|
|
387
|
+
/** ISO-8601 publication timestamp, when the feed declares one. */
|
|
388
|
+
readonly publishedAt?: string;
|
|
389
|
+
readonly tags?: readonly string[];
|
|
390
|
+
/** Short plain-text excerpt for the card. */
|
|
391
|
+
readonly summary?: string;
|
|
392
|
+
/** Normalized article body for the detail view. */
|
|
393
|
+
readonly contentHtml?: string;
|
|
394
|
+
/**
|
|
395
|
+
* True when {@link contentHtml} is only the FEED's own summary.
|
|
396
|
+
*
|
|
397
|
+
* A feed that publishes no `content:encoded` / `<content>` still gets its
|
|
398
|
+
* description rendered in the detail view (an empty page would be worse), so
|
|
399
|
+
* the browser half has to be able to tell "the feed gave me its summary" from
|
|
400
|
+
* "this is the article" — otherwise the entry looks complete and nothing ever
|
|
401
|
+
* fetches the page behind it. The automatic backfill and opening an entry
|
|
402
|
+
* both treat this as a body still owed.
|
|
403
|
+
*/
|
|
404
|
+
readonly summaryOnly?: boolean;
|
|
405
|
+
/** True when the body is known to be incomplete (see {@link ReaderSource.truncated}). */
|
|
406
|
+
readonly truncated?: boolean;
|
|
407
|
+
/**
|
|
408
|
+
* Figures the page draws with its own scripts, counted at extraction. Saved
|
|
409
|
+
* links extract at load time, so the count rides the entry; the detail view
|
|
410
|
+
* shows the notice and the 「渲染抓取」 action from it.
|
|
411
|
+
*/
|
|
412
|
+
readonly scriptFigures?: number;
|
|
413
|
+
/**
|
|
414
|
+
* True when the ENTRY itself is a salvage of a payload the host capped: its
|
|
415
|
+
* text is real but stops where the cap landed. The detail view says so.
|
|
416
|
+
*/
|
|
417
|
+
readonly partial?: boolean;
|
|
418
|
+
}
|
|
419
|
+
/** One source as the browser sees it (no payload: bodies are fetched on demand). */
|
|
420
|
+
export interface ReaderSourceSummary {
|
|
421
|
+
readonly id: string;
|
|
422
|
+
readonly kind: ReaderSourceKind;
|
|
423
|
+
readonly url: string;
|
|
424
|
+
readonly label: string;
|
|
425
|
+
readonly enabled: boolean;
|
|
426
|
+
/**
|
|
427
|
+
* When the reader added it (ISO-8601).
|
|
428
|
+
*
|
|
429
|
+
* The browser needs this to order the wall and the management list by
|
|
430
|
+
* "when did this arrive": a saved link carries no publication date at all,
|
|
431
|
+
* so without this field it sorts as the oldest thing on the wall — which is
|
|
432
|
+
* exactly where a reader who just added it will not look.
|
|
433
|
+
*/
|
|
434
|
+
readonly addedAt: string;
|
|
435
|
+
readonly fetchedAt?: string;
|
|
436
|
+
readonly status?: ReaderFetchStatus;
|
|
437
|
+
readonly error?: string;
|
|
438
|
+
readonly truncated?: boolean;
|
|
439
|
+
readonly hasBody: boolean;
|
|
440
|
+
/** Present when this source has no previewable body (see {@link ReaderPreviewFailure}). */
|
|
441
|
+
readonly failure?: {
|
|
442
|
+
readonly code: ReaderPreviewFailureCode;
|
|
443
|
+
readonly message: string;
|
|
444
|
+
};
|
|
445
|
+
/** The URL the reader pasted, when the source was resolved to a canonical version (see {@link ReaderSource.resolvedFrom}). */
|
|
446
|
+
readonly resolvedFrom?: string;
|
|
447
|
+
}
|
|
448
|
+
/** Capability handshake: what the browser may rely on in this composition. */
|
|
449
|
+
export interface ReaderCapabilities {
|
|
450
|
+
readonly protocolVersion: 1;
|
|
451
|
+
/** False when no filesystem is mounted: state lives in memory and is lost on restart. */
|
|
452
|
+
readonly hasFs: boolean;
|
|
453
|
+
/** False when no side-chat service is mounted: the side-chat gesture must hide. */
|
|
454
|
+
readonly hasSideChat: boolean;
|
|
455
|
+
/** ISO-8601 timestamp of the next scheduled refresh, when one is armed. */
|
|
456
|
+
readonly nextRefreshAt?: string;
|
|
457
|
+
/**
|
|
458
|
+
* ISO-8601 timestamp of the last completed refresh run, when there was one.
|
|
459
|
+
* The list shows it so a reader can decide whether the snapshot is stale
|
|
460
|
+
* enough to be worth a fetch.
|
|
461
|
+
*/
|
|
462
|
+
readonly lastRefreshAt?: string;
|
|
463
|
+
}
|
|
464
|
+
/** Outcome of a mutating source verb. A domain refusal, never an exception. */
|
|
465
|
+
export type ReaderMutationOutcome = 'ok' | 'not-found' | 'invalid-url' | 'invalid-time' | 'duplicate' | 'unavailable';
|
|
466
|
+
/** What `addSource` decided a pasted URL was. */
|
|
467
|
+
export interface ReaderAddOutcome {
|
|
468
|
+
readonly outcome: 'subscribed' | 'saved-link';
|
|
469
|
+
readonly kind: ReaderSourceKind;
|
|
470
|
+
readonly id: string;
|
|
471
|
+
readonly label: string;
|
|
472
|
+
/**
|
|
473
|
+
* Present when the source was saved but its body cannot be previewed.
|
|
474
|
+
*
|
|
475
|
+
* The add still succeeded: the URL is a link the reader now owns. This is
|
|
476
|
+
* the verdict's reason, not a failure — which is why it rides the outcome
|
|
477
|
+
* instead of becoming one more refusal that loses the link.
|
|
478
|
+
*/
|
|
479
|
+
readonly failure?: {
|
|
480
|
+
readonly code: ReaderPreviewFailureCode;
|
|
481
|
+
readonly message: string;
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* A refusal from `addSource`: the mutation outcomes that make sense for "turn
|
|
486
|
+
* this URL into a source", plus the two fetch-stage refusals that have no
|
|
487
|
+
* meaning for the other verbs. Kept as its own union so the wire type states
|
|
488
|
+
* exactly what this verb can answer.
|
|
489
|
+
*/
|
|
490
|
+
export type ReaderAddRefusal = Extract<ReaderMutationOutcome, 'invalid-url' | 'duplicate' | 'unavailable'> | 'unsupported-content' | 'fetch-failed';
|
|
491
|
+
/**
|
|
492
|
+
* The fetch itself failed, with the seam's own reason attached.
|
|
493
|
+
*
|
|
494
|
+
* A separate shape rather than one more string in {@link ReaderAddRefusal}:
|
|
495
|
+
* "the request did not complete" is a different conversation from "that URL
|
|
496
|
+
* cannot be a source", and only the first one has something to show the reader
|
|
497
|
+
* beyond the verdict. Without the reason the UI can only say "failed", which
|
|
498
|
+
* is not a diagnosis.
|
|
499
|
+
*/
|
|
500
|
+
export interface ReaderAddFailure {
|
|
501
|
+
readonly outcome: 'fetch-failed';
|
|
502
|
+
/** The fetch seam's message, verbatim — a refused connection, a timeout, a byte cap. */
|
|
503
|
+
readonly reason: string;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* One entry the host would like full text for.
|
|
507
|
+
*
|
|
508
|
+
* The host decides WHICH entries need it (it owns the cache, the failures and
|
|
509
|
+
* the policy); the browser decides what the page says (it owns the DOM). This
|
|
510
|
+
* is the split that makes automatic backfill possible at all.
|
|
511
|
+
*/
|
|
512
|
+
export interface ReaderBackfillCandidate {
|
|
513
|
+
readonly entryId: string;
|
|
514
|
+
readonly url: string;
|
|
515
|
+
readonly label: string;
|
|
516
|
+
/** Why a previous attempt failed, when one did (and when it is worth retrying). */
|
|
517
|
+
readonly lastError?: string;
|
|
518
|
+
}
|
|
519
|
+
/** Per-source result of a refresh run. */
|
|
520
|
+
export interface ReaderRefreshResult {
|
|
521
|
+
readonly id: string;
|
|
522
|
+
readonly status: 'ok' | 'fetch-failed' | 'truncated' | 'unavailable';
|
|
523
|
+
readonly message?: string;
|
|
524
|
+
}
|
|
525
|
+
/** One raw payload, requested on demand for parsing in the browser. */
|
|
526
|
+
export interface ReaderBody {
|
|
527
|
+
readonly id: string;
|
|
528
|
+
readonly raw?: string;
|
|
529
|
+
readonly truncated?: boolean;
|
|
530
|
+
/** Present when the payload could not be produced (fetch failed, no fs, …). */
|
|
531
|
+
readonly error?: string;
|
|
532
|
+
/**
|
|
533
|
+
* The captured article title for a link source's single entry, when a past
|
|
534
|
+
* extraction stored it (see {@link ReaderEntryAnnotation.title}). The wall
|
|
535
|
+
* prefers it over the URL-derived label.
|
|
536
|
+
*/
|
|
537
|
+
readonly title?: string;
|
|
538
|
+
/** The captured excerpt, with the title. */
|
|
539
|
+
readonly excerpt?: string;
|
|
540
|
+
}
|
|
541
|
+
/** Default state root segment under `$DSH_HOME/state`. */
|
|
542
|
+
export declare const STATE_ROOT_SEGMENT = "dsh-reader";
|
|
543
|
+
/** Default daily refresh time. */
|
|
544
|
+
export declare const DEFAULT_REFRESH_TIME = "10:00";
|
|
545
|
+
/**
|
|
546
|
+
* Per-source raw-payload budget in characters.
|
|
547
|
+
*
|
|
548
|
+
* Sized to the HOST's egress cap rather than below it: a deployment can raise
|
|
549
|
+
* `web-fetch-http`'s `maxBodyChars` (the acceptance instance runs it at
|
|
550
|
+
* 2,000,000), and a per-source budget smaller than what the seam is willing to
|
|
551
|
+
* deliver would silently re-truncate every large feed here — the reader would
|
|
552
|
+
* pay for the fetch and still get half a document, with the note blaming the
|
|
553
|
+
* fetch. The fetch cap is the outer bound; this one must not be the inner one.
|
|
554
|
+
*/
|
|
555
|
+
export declare const MAX_BODY_CHARS_PER_SOURCE: number;
|
|
556
|
+
/** Total raw-payload budget in characters across all sources (newest kept). */
|
|
557
|
+
export declare const MAX_TOTAL_BODY_CHARS: number;
|
|
558
|
+
/**
|
|
559
|
+
* How large an extracted body may be before it lives in its own file.
|
|
560
|
+
*
|
|
561
|
+
* `state.json` is read and rewritten as a whole on every mutation, so a research
|
|
562
|
+
* paper whose figures are inlined as base64 (transformer-circuits.pub's emotions
|
|
563
|
+
* paper is a 41.8 MB HTML document and extracts to tens of megabytes) would make
|
|
564
|
+
* every reader operation pay a multi-megabyte JSON round trip. Above this
|
|
565
|
+
* threshold the body is written to one file under `bodies/` and the document
|
|
566
|
+
* keeps only its metadata — so the cache policy (`ttlHours`, `maxEntries`) still
|
|
567
|
+
* governs it exactly like an inline body, and eviction deletes the file.
|
|
568
|
+
*/
|
|
569
|
+
export declare const INLINE_BODY_MAX_CHARS: number;
|
|
570
|
+
/**
|
|
571
|
+
* Build a display label for a source from its URL: the registrable-ish host
|
|
572
|
+
* plus the last meaningful path segment, which is what distinguishes several
|
|
573
|
+
* feeds from one publisher.
|
|
574
|
+
*
|
|
575
|
+
* @param url - the source URL (any string; a malformed one degrades to its own head).
|
|
576
|
+
* @returns a short human label.
|
|
577
|
+
*/
|
|
578
|
+
export declare function defaultSourceLabel(url: string): string;
|
|
579
|
+
/**
|
|
580
|
+
* Build a stable entry id. Feeds are inconsistent about identifiers, so the
|
|
581
|
+
* order is: explicit guid/id, then the entry link, then a hash over title and
|
|
582
|
+
* link together (a feed with neither must still deduplicate across refreshes).
|
|
583
|
+
*
|
|
584
|
+
* @param entry - the identifying fields an entry may carry.
|
|
585
|
+
* @returns an id stable across refreshes of the same feed content.
|
|
586
|
+
*/
|
|
587
|
+
export declare function stableEntryId(entry: {
|
|
588
|
+
title: string;
|
|
589
|
+
link?: string;
|
|
590
|
+
guid?: string;
|
|
591
|
+
}): string;
|
|
592
|
+
/**
|
|
593
|
+
* The entry id a saved link's single entry carries.
|
|
594
|
+
*
|
|
595
|
+
* A `link` source is exactly one entry, so the host must be able to name that
|
|
596
|
+
* entry when it records a per-entry annotation (the fetch failure the detail
|
|
597
|
+
* view reads). Both halves use this one function: a magic string duplicated
|
|
598
|
+
* across the process boundary is a bug waiting for a rename.
|
|
599
|
+
*
|
|
600
|
+
* @param sourceId - the link source's id.
|
|
601
|
+
* @returns the entry id the browser will build for it.
|
|
602
|
+
*/
|
|
603
|
+
export declare function linkEntryId(sourceId: string): string;
|
|
604
|
+
/**
|
|
605
|
+
* The query that selects every entry of one source KIND.
|
|
606
|
+
*
|
|
607
|
+
* The same local-predicate mechanism as a source or tag filter, so a kind
|
|
608
|
+
* narrowing is visible in the search box and can be cleared there. The prefix
|
|
609
|
+
* cannot collide with a real source id: ids are always `<kind>-<hash>`.
|
|
610
|
+
*
|
|
611
|
+
* @param kind - the source kind to select.
|
|
612
|
+
* @returns the query string.
|
|
613
|
+
*/
|
|
614
|
+
export declare function kindQuery(kind: ReaderSourceKind): string;
|
|
615
|
+
/** The source kinds a kind query can name, for the predicate that reads one. */
|
|
616
|
+
export declare const READER_SOURCE_KINDS: readonly ReaderSourceKind[];
|
|
617
|
+
/**
|
|
618
|
+
* Shorten a summary for a card, collapsing whitespace so multi-line feed
|
|
619
|
+
* descriptions do not blow up the layout.
|
|
620
|
+
*
|
|
621
|
+
* @param text - the raw summary or content text.
|
|
622
|
+
* @param maxLength - the maximum returned length, ellipsis included.
|
|
623
|
+
* @returns the collapsed, shortened text, or `undefined` when there is none.
|
|
624
|
+
*/
|
|
625
|
+
export declare function summarize(text: string | undefined, maxLength?: number): string | undefined;
|
|
626
|
+
/** Coerce an unknown thrown value into a message string. */
|
|
627
|
+
export declare function errorMessage(error: unknown): string;
|
|
628
|
+
/**
|
|
629
|
+
* One entry's cached article body, keyed by the entry's stable id.
|
|
630
|
+
*
|
|
631
|
+
* A feed may publish full text for some entries and only a summary for others
|
|
632
|
+
* (measured: the OpenAI alignment feed runs 155 to 62,044 characters entry by
|
|
633
|
+
* entry), so this is where the full text of a summary-only entry lives once the
|
|
634
|
+
* reader has asked for it. Keyed by entry id — NOT by the source — because the
|
|
635
|
+
* id is derived from the entry's guid/link and therefore survives every
|
|
636
|
+
* refresh; keying by "position in the feed" would mix entries up on the next
|
|
637
|
+
* fetch.
|
|
638
|
+
*/
|
|
639
|
+
export interface ReaderEntryBody {
|
|
640
|
+
/** Whitelist-normalized article markup, when it is small enough to inline. */
|
|
641
|
+
readonly html?: string;
|
|
642
|
+
/**
|
|
643
|
+
* The `bodies/` file holding this body, when it is too large to inline.
|
|
644
|
+
*
|
|
645
|
+
* A bare file name, never a path: the store owns the directory, and a document
|
|
646
|
+
* that carried an absolute path would break the moment the state root moves.
|
|
647
|
+
*/
|
|
648
|
+
readonly file?: string;
|
|
649
|
+
/** The body's character count, so the document can report its size without it. */
|
|
650
|
+
readonly chars?: number;
|
|
651
|
+
/**
|
|
652
|
+
* True when this body came from the capture package's rendered fetch rather
|
|
653
|
+
* than the plain fetch. The detail view's 「重新抓取」 routes on it: a
|
|
654
|
+
* rendered entry re-renders (a plain fetch would clobber the rendered body
|
|
655
|
+
* with the page's pre-JS shell).
|
|
656
|
+
*/
|
|
657
|
+
readonly rendered?: boolean;
|
|
658
|
+
/** When it was fetched (ISO-8601), for the "fetched just now" line and pruning. */
|
|
659
|
+
readonly fetchedAt: string;
|
|
660
|
+
/**
|
|
661
|
+
* When it stops being served without asking again (ISO-8601).
|
|
662
|
+
*
|
|
663
|
+
* A cached body is an optimization, not an archive: the acceptance decision
|
|
664
|
+
* was 24h, because a reader re-opens an article within the day or not at all.
|
|
665
|
+
* Past the deadline the detail view offers the fetch again rather than
|
|
666
|
+
* silently serving yesterday's copy.
|
|
667
|
+
*/
|
|
668
|
+
readonly expiresAt: string;
|
|
669
|
+
/** The URL it was fetched from, so a source edit is detectable. */
|
|
670
|
+
readonly url: string;
|
|
671
|
+
/** True when the fetch hit the seam's cap (the note then says so honestly). */
|
|
672
|
+
readonly truncated?: boolean;
|
|
673
|
+
/** Figures the page draws with its own scripts (dropped; the note counts them). */
|
|
674
|
+
readonly scriptFigures?: number;
|
|
675
|
+
}
|
|
676
|
+
/** One user-defined tag. */
|
|
677
|
+
export interface ReaderTag {
|
|
678
|
+
/** Stable id — renaming a tag must not rewrite every entry that carries it. */
|
|
679
|
+
readonly id: string;
|
|
680
|
+
/** The reader-facing name, unique case-insensitively. */
|
|
681
|
+
readonly name: string;
|
|
682
|
+
readonly createdAt: string;
|
|
683
|
+
}
|
|
684
|
+
/** A mutation on the annotation face, as a bare domain value. */
|
|
685
|
+
export type ReaderAnnotationOutcome = 'ok' | 'not-found' | 'invalid' | 'duplicate' | 'unavailable' | 'empty';
|
|
686
|
+
/**
|
|
687
|
+
* What the pane asks for when it opens one entry.
|
|
688
|
+
*
|
|
689
|
+
* `html` present means the detail view can render immediately. Absent means
|
|
690
|
+
* there is nothing beyond the feed summary and the view should offer the fetch.
|
|
691
|
+
*/
|
|
692
|
+
export interface ReaderEntryBodyView {
|
|
693
|
+
readonly entryId: string;
|
|
694
|
+
/** True when this body is a cached fetch rather than the feed's own payload. */
|
|
695
|
+
readonly cached: boolean;
|
|
696
|
+
/** False when the cache's deadline has passed: the view offers the fetch. */
|
|
697
|
+
readonly fresh: boolean;
|
|
698
|
+
/** True when the feed's payload already carries full text for this entry. */
|
|
699
|
+
readonly fromFeed: boolean;
|
|
700
|
+
readonly html?: string;
|
|
701
|
+
readonly fetchedAt?: string;
|
|
702
|
+
readonly truncated?: boolean;
|
|
703
|
+
/** Figures this page draws with scripts — the detail view says so. */
|
|
704
|
+
readonly scriptFigures?: number;
|
|
705
|
+
/** True when this body came from a rendered fetch (the「重新抓取」route reads it). */
|
|
706
|
+
readonly rendered?: boolean;
|
|
707
|
+
/** Why a fetch could not produce a body, when one was attempted. */
|
|
708
|
+
readonly error?: string;
|
|
709
|
+
}
|
|
710
|
+
//# sourceMappingURL=types.d.ts.map
|