@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,633 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The reader service core: owns the fetch, the state document, and the daily
|
|
3
|
+
* refresh. Everything the browser can ask for is implemented here; the Remote
|
|
4
|
+
* class next door is a thin adapter with no logic of its own.
|
|
5
|
+
*
|
|
6
|
+
* Two seams shape the whole module:
|
|
7
|
+
*
|
|
8
|
+
* - **Fetch goes through `ctx.web.fetch`** — the host's sanctioned outbound
|
|
9
|
+
* HTTP face, which brings public-address resolution, connection pinning, a
|
|
10
|
+
* byte cap and a timeout. Its provider caps the decoded body at 100 000
|
|
11
|
+
* characters by default, so a payload that comes back `truncated` is stored
|
|
12
|
+
* as-is and FLAGGED; nothing here pretends it is a whole document. A
|
|
13
|
+
* truncated feed cannot be parsed and a truncated page yields half an
|
|
14
|
+
* article, so the browser half reports "incomplete" rather than guessing.
|
|
15
|
+
* - **The provider follows same-origin redirects only** — a cross-origin hop is
|
|
16
|
+
* refused with `WEB_REDIRECT_BLOCKED` by design (each new origin must be
|
|
17
|
+
* validated afresh), and the refusal names the target ORIGIN alone, never the
|
|
18
|
+
* path it was headed for. So the service re-enters the seam only for a hop it
|
|
19
|
+
* can reconstruct faithfully — the same host under a different scheme or port
|
|
20
|
+
* (`http://host/feed` → `https://host/feed`, the common upgrade). A hop that
|
|
21
|
+
* changes the HOST is reported as such instead of being followed to the bare
|
|
22
|
+
* origin, which would fetch some other page and file it under the reader's
|
|
23
|
+
* URL. The final URL becomes the source's canonical URL.
|
|
24
|
+
* - **A failed fetch is still a saved link.** A URL the reader pasted is one
|
|
25
|
+
* they own; the classification below records WHY there is no readable body
|
|
26
|
+
* (bot wall, login wall, non-web file, redirect to another site, empty page,
|
|
27
|
+
* transport error) instead of discarding the URL, and only the transport
|
|
28
|
+
* failure is retried automatically. Losing the link is worse than a card
|
|
29
|
+
* that says it can only be opened in a browser.
|
|
30
|
+
*
|
|
31
|
+
* With no filesystem mounted the service stays fully functional in memory
|
|
32
|
+
* (sources still add, refresh and search) and simply does not survive a
|
|
33
|
+
* restart — a composition must never fail because a deployment has no fs.
|
|
34
|
+
*
|
|
35
|
+
* @module @khorsheed/dsh-reader/service
|
|
36
|
+
*/
|
|
37
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
38
|
+
import { type ReaderAddFailure, type ReaderAddOutcome, type ReaderAddRefusal, type ReaderAnnotationOutcome, type ReaderBackfillCandidate, type ReaderEntryBodyView, type ReaderBody, type ReaderCapabilities, type ReaderMutationOutcome, type ReaderEntryFetchState, type ReaderEntryTranslationView, type ReaderPreviewFailure, type ReaderPreviewFailureCode, type ReaderRecentEntry, type ReaderRefreshResult, type ReaderSentenceLearn, type ReaderSourceSummary, type ReaderStorageStats, type ReaderTag } from './types.ts';
|
|
39
|
+
/**
|
|
40
|
+
* How many entries one automatic backfill run may fetch.
|
|
41
|
+
*
|
|
42
|
+
* A wall of 300 summary-only entries must not turn one refresh into 300
|
|
43
|
+
* requests to other people's servers; the run takes a slice and the next
|
|
44
|
+
* refresh takes the next one.
|
|
45
|
+
*/
|
|
46
|
+
export declare const BACKFILL_MAX_PER_RUN = 8;
|
|
47
|
+
/** How long a FAILED fetch is left alone before it may be retried (6 hours). */
|
|
48
|
+
export declare const BACKFILL_RETRY_MS: number;
|
|
49
|
+
/**
|
|
50
|
+
* Thrown when the FETCH itself failed — a refused connection, a timeout, a
|
|
51
|
+
* response over the seam's byte cap. Carries the seam's own message: the
|
|
52
|
+
* difference between "the host has no egress" and "that host timed out" is the
|
|
53
|
+
* whole diagnosis, and a bare "failed" tells the reader neither.
|
|
54
|
+
*/
|
|
55
|
+
export declare class FetchFailure extends Error {
|
|
56
|
+
/**
|
|
57
|
+
* @param message - the seam's message, verbatim.
|
|
58
|
+
* @param cause - the original error, when there was one.
|
|
59
|
+
*/
|
|
60
|
+
constructor(message: string, cause?: unknown);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Classify a fetched payload as a feed or an article page.
|
|
64
|
+
*
|
|
65
|
+
* The decision comes from the CONTENT, not from asking the user: nobody pasting
|
|
66
|
+
* a URL can be expected to know whether it is a feed, and guessing wrong used
|
|
67
|
+
* to mean silently creating an empty subscription.
|
|
68
|
+
*
|
|
69
|
+
* @param raw - the decoded payload.
|
|
70
|
+
* @returns which kind of source this payload can become.
|
|
71
|
+
*/
|
|
72
|
+
export declare function classifyPayload(raw: string): 'feed' | 'page';
|
|
73
|
+
/** The reader service. */
|
|
74
|
+
export declare class ReaderService {
|
|
75
|
+
private readonly store;
|
|
76
|
+
private readonly ctx;
|
|
77
|
+
private timer;
|
|
78
|
+
private disposed;
|
|
79
|
+
/** The document of record when no filesystem is mounted. */
|
|
80
|
+
private memoryDoc;
|
|
81
|
+
/**
|
|
82
|
+
* @param ctx - owning context.
|
|
83
|
+
* @param config - optional state-root override.
|
|
84
|
+
*/
|
|
85
|
+
constructor(ctx: Context, config?: {
|
|
86
|
+
stateRoot?: string;
|
|
87
|
+
});
|
|
88
|
+
/** Capabilities the browser may rely on. */
|
|
89
|
+
capabilities(): Promise<ReaderCapabilities>;
|
|
90
|
+
/** The source list, without payloads. */
|
|
91
|
+
listSources(): Promise<{
|
|
92
|
+
sources: ReaderSourceSummary[];
|
|
93
|
+
}>;
|
|
94
|
+
/**
|
|
95
|
+
* Add a source, deciding what it is from what comes back.
|
|
96
|
+
*
|
|
97
|
+
* The one rule this method exists to enforce: **a URL the reader pasted is
|
|
98
|
+
* never thrown away.** A fetch that fails, a bot challenge, a login wall and
|
|
99
|
+
* a PDF all end the same way — a `link` source that exists, carries the
|
|
100
|
+
* reason it has no previewable body, and can be opened, tagged, refreshed
|
|
101
|
+
* manually or deleted. Only a URL that cannot be a source at all
|
|
102
|
+
* (`invalid-url`) or one already present (`duplicate`) is refused.
|
|
103
|
+
*
|
|
104
|
+
* @param request - the pasted URL and an optional label.
|
|
105
|
+
* @returns the decision, or a domain refusal.
|
|
106
|
+
*/
|
|
107
|
+
addSource(request: {
|
|
108
|
+
url: string;
|
|
109
|
+
label?: string;
|
|
110
|
+
}): Promise<ReaderAddOutcome | ReaderAddRefusal | ReaderAddFailure>;
|
|
111
|
+
/**
|
|
112
|
+
* Persist a link the reader owns even though its body cannot be previewed.
|
|
113
|
+
*
|
|
114
|
+
* Two records, because two consumers ask different questions: the SOURCE
|
|
115
|
+
* carries the failure so the wall can mark the card and the backfill can
|
|
116
|
+
* leave a permanent wall alone, while the ENTRY annotation carries it so
|
|
117
|
+
* `getEntryBody` — the one call the detail view makes — explains itself
|
|
118
|
+
* instead of answering with a blank article.
|
|
119
|
+
*
|
|
120
|
+
* @param request - the final URL, an optional label, and the pasted URL when
|
|
121
|
+
* this link was resolved to a canonical version.
|
|
122
|
+
* @param failure - the classified reason, with its timestamp.
|
|
123
|
+
* @param now - the commit instant.
|
|
124
|
+
* @returns the add outcome the browser reports.
|
|
125
|
+
*/
|
|
126
|
+
private saveLinkOnly;
|
|
127
|
+
/**
|
|
128
|
+
* Update a source's mutable fields, or the global refresh time.
|
|
129
|
+
*
|
|
130
|
+
* @param request - the id plus the fields to change.
|
|
131
|
+
* @returns the outcome, or a domain refusal.
|
|
132
|
+
*/
|
|
133
|
+
updateSource(request: {
|
|
134
|
+
id: string;
|
|
135
|
+
enabled?: boolean;
|
|
136
|
+
label?: string;
|
|
137
|
+
url?: string;
|
|
138
|
+
timeOfDay?: string;
|
|
139
|
+
}): Promise<ReaderMutationOutcome>;
|
|
140
|
+
/**
|
|
141
|
+
* Remove a source.
|
|
142
|
+
*
|
|
143
|
+
* Its entries' TRANSLATIONS go with it: the entries themselves are unreachable
|
|
144
|
+
* once the source is gone (the recent page disables them), so their exact-fit
|
|
145
|
+
* maps are dead weight. The host never parses feeds, so the caller passes the
|
|
146
|
+
* entry ids it has parsed; a link source's single entry id is derivable here
|
|
147
|
+
* regardless. The cached BODIES stay — the body budget owns their lifetime,
|
|
148
|
+
* unchanged.
|
|
149
|
+
*
|
|
150
|
+
* @param request - the source id, plus the entries the caller knows belong to it.
|
|
151
|
+
* @returns the outcome, or a domain refusal.
|
|
152
|
+
*/
|
|
153
|
+
removeSource(request: {
|
|
154
|
+
id: string;
|
|
155
|
+
entryIds?: readonly string[];
|
|
156
|
+
}): Promise<ReaderMutationOutcome>;
|
|
157
|
+
/**
|
|
158
|
+
* Refresh the enabled sources, or the named ones.
|
|
159
|
+
*
|
|
160
|
+
* @param request - optional id list.
|
|
161
|
+
* @returns one result per attempted source.
|
|
162
|
+
*/
|
|
163
|
+
refresh(request?: {
|
|
164
|
+
ids?: string[];
|
|
165
|
+
}): Promise<{
|
|
166
|
+
results: ReaderRefreshResult[];
|
|
167
|
+
}>;
|
|
168
|
+
/**
|
|
169
|
+
* Hand back raw payloads for the browser to parse.
|
|
170
|
+
*
|
|
171
|
+
* @param request - the source ids whose payloads are wanted.
|
|
172
|
+
* @returns one body per requested id; an error instead of a payload when
|
|
173
|
+
* nothing usable is stored.
|
|
174
|
+
*/
|
|
175
|
+
getBodies(request: {
|
|
176
|
+
ids: string[];
|
|
177
|
+
}): Promise<{
|
|
178
|
+
bodies: ReaderBody[];
|
|
179
|
+
}>;
|
|
180
|
+
/**
|
|
181
|
+
* Pass a ref block to the side-chat service when one is composed.
|
|
182
|
+
*
|
|
183
|
+
* @param request - the target context, its label, and the ref text.
|
|
184
|
+
* @returns `'ok'`, or `'unavailable'` when no side chat is mounted.
|
|
185
|
+
*/
|
|
186
|
+
quoteToSideChat(request: {
|
|
187
|
+
contextKey: string;
|
|
188
|
+
label: string;
|
|
189
|
+
text: string;
|
|
190
|
+
}): Promise<'ok' | 'unavailable'>;
|
|
191
|
+
/**
|
|
192
|
+
* The body the detail view should render for one entry, if any.
|
|
193
|
+
*
|
|
194
|
+
* The order is the whole contract: a fresh cached fetch wins (it is full text
|
|
195
|
+
* the reader already paid for), then the feed's own payload, and only when
|
|
196
|
+
* neither exists does the view offer to fetch. A STALE cache entry reports
|
|
197
|
+
* `fresh: false` and no html — the detail view then offers the fetch again
|
|
198
|
+
* rather than quietly serving yesterday's copy.
|
|
199
|
+
*
|
|
200
|
+
* @param request - the entry and the payload the feed gave for it.
|
|
201
|
+
* @returns what there is to show, and whether a fetch is worth offering.
|
|
202
|
+
*/
|
|
203
|
+
getEntryBody(request: {
|
|
204
|
+
entryId: string;
|
|
205
|
+
url: string;
|
|
206
|
+
feedHtml?: string;
|
|
207
|
+
}): Promise<ReaderEntryBodyView>;
|
|
208
|
+
/**
|
|
209
|
+
* Fetch one entry's article and hand the raw HTML to the browser.
|
|
210
|
+
*
|
|
211
|
+
* The HOST does not extract: extraction needs a DOM, and this runtime has no
|
|
212
|
+
* XML/HTML parser at all (the same reason feed parsing lives in the browser
|
|
213
|
+
* half). So this verb is the network half only — it returns what came back,
|
|
214
|
+
* and `storeEntryBody` persists what the browser made of it.
|
|
215
|
+
*
|
|
216
|
+
* Fetching here is deliberately NOT automatic on refresh: doing it for every
|
|
217
|
+
* summary-only entry would make this a crawler of other people's sites, which
|
|
218
|
+
* the design refuses. One entry, one fetch, on the reader's request.
|
|
219
|
+
*
|
|
220
|
+
* @param request - the entry and its article URL.
|
|
221
|
+
* @returns the raw payload, or why there is none.
|
|
222
|
+
*/
|
|
223
|
+
fetchEntryBody(request: {
|
|
224
|
+
entryId: string;
|
|
225
|
+
url: string;
|
|
226
|
+
}): Promise<{
|
|
227
|
+
entryId: string;
|
|
228
|
+
url?: string;
|
|
229
|
+
raw?: string;
|
|
230
|
+
truncated?: boolean;
|
|
231
|
+
error?: string;
|
|
232
|
+
}>;
|
|
233
|
+
/**
|
|
234
|
+
* Which entries have a body, a stored raw payload, or a recorded failure.
|
|
235
|
+
*
|
|
236
|
+
* The wall needs this per ENTRY (entries come from the browser's parse of a
|
|
237
|
+
* feed), so it cannot ride the source summaries. `raw` means "fetched, not yet
|
|
238
|
+
* extracted" — the browser half turns those into bodies on its next load.
|
|
239
|
+
*
|
|
240
|
+
* @param request - the entry ids the caller is showing.
|
|
241
|
+
* @returns one state per requested id.
|
|
242
|
+
*/
|
|
243
|
+
entryFetchStates(request: {
|
|
244
|
+
entryIds: readonly string[];
|
|
245
|
+
}): Promise<{
|
|
246
|
+
states: Record<string, ReaderEntryFetchState>;
|
|
247
|
+
}>;
|
|
248
|
+
/**
|
|
249
|
+
* Hand back a stored raw payload so the browser can extract it.
|
|
250
|
+
*
|
|
251
|
+
* @param request - the entry whose payload is wanted.
|
|
252
|
+
* @returns the payload, or why there is none.
|
|
253
|
+
*/
|
|
254
|
+
getRawBody(request: {
|
|
255
|
+
entryId: string;
|
|
256
|
+
}): Promise<{
|
|
257
|
+
entryId: string;
|
|
258
|
+
raw?: string;
|
|
259
|
+
url?: string;
|
|
260
|
+
truncated?: boolean;
|
|
261
|
+
error?: string;
|
|
262
|
+
}>;
|
|
263
|
+
/** Record that a fetch is in flight, so a wall reopened later can say so. */
|
|
264
|
+
private markFetching;
|
|
265
|
+
/**
|
|
266
|
+
* Store one raw payload for later extraction.
|
|
267
|
+
*
|
|
268
|
+
* @param entryId - the entry it belongs to.
|
|
269
|
+
* @param raw - the payload.
|
|
270
|
+
* @returns the file name, or `undefined` when the deployment has no fs.
|
|
271
|
+
*/
|
|
272
|
+
private storeRaw;
|
|
273
|
+
/**
|
|
274
|
+
* Cache the markup the browser extracted for one entry.
|
|
275
|
+
*
|
|
276
|
+
* The deadline is computed here, not on the client, so the policy has exactly
|
|
277
|
+
* one owner (this service's document) — a client that guessed the TTL would
|
|
278
|
+
* be a second implementation of it.
|
|
279
|
+
*
|
|
280
|
+
* @param request - the entry, the markup, and the URL it came from.
|
|
281
|
+
* @returns the stored view.
|
|
282
|
+
*/
|
|
283
|
+
storeEntryBody(request: {
|
|
284
|
+
entryId: string;
|
|
285
|
+
url: string;
|
|
286
|
+
html: string;
|
|
287
|
+
truncated?: boolean;
|
|
288
|
+
scriptFigures?: number;
|
|
289
|
+
/** True when this body came from the capture rendered fetch — 「重新抓取」 re-renders it. */
|
|
290
|
+
rendered?: boolean;
|
|
291
|
+
/** The caller's hash of `html`; the entry's translation map dies with a body it no longer matches. */
|
|
292
|
+
bodyHash?: string;
|
|
293
|
+
/** The article's own title, as the extraction read it — a saved link's card upgrade. */
|
|
294
|
+
title?: string;
|
|
295
|
+
/** A short excerpt (abstract / first paragraph), with the title. */
|
|
296
|
+
excerpt?: string;
|
|
297
|
+
}): Promise<ReaderEntryBodyView>;
|
|
298
|
+
/**
|
|
299
|
+
* Which entries still need their full text, and which may be retried.
|
|
300
|
+
*
|
|
301
|
+
* The host is the only side that can answer this: it holds the cache, the
|
|
302
|
+
* recorded failures and the policy. A failure is retried only after
|
|
303
|
+
* `BACKFILL_RETRY_MS` — an entry whose publisher refuses us must not be
|
|
304
|
+
* hammered once per refresh, which would make this a crawler with a grudge.
|
|
305
|
+
*
|
|
306
|
+
* @param request - the entries the caller is looking at (id, url, label) and
|
|
307
|
+
* whether the caller already has full text for them (the feed's own payload).
|
|
308
|
+
* @returns the candidates, capped so one refresh cannot start a stampede.
|
|
309
|
+
*/
|
|
310
|
+
listBackfillCandidates(request: {
|
|
311
|
+
entries: readonly {
|
|
312
|
+
entryId: string;
|
|
313
|
+
url: string;
|
|
314
|
+
label: string;
|
|
315
|
+
hasBody: boolean;
|
|
316
|
+
}[];
|
|
317
|
+
limit?: number;
|
|
318
|
+
}): Promise<{
|
|
319
|
+
candidates: ReaderBackfillCandidate[];
|
|
320
|
+
}>;
|
|
321
|
+
/**
|
|
322
|
+
* One entry's exact-fit translation record, with its segment map resolved.
|
|
323
|
+
*
|
|
324
|
+
* A record from an older storage schema, or one whose sidecar file is gone,
|
|
325
|
+
* reads as absent — the caller falls back to the global memory, and the stale
|
|
326
|
+
* record is carried out by the next write or budget pass (lazy, never wiped).
|
|
327
|
+
*
|
|
328
|
+
* @param request - the entry.
|
|
329
|
+
* @returns the record, or nothing usable.
|
|
330
|
+
*/
|
|
331
|
+
getEntryTranslation(request: {
|
|
332
|
+
entryId: string;
|
|
333
|
+
}): Promise<{
|
|
334
|
+
translation?: ReaderEntryTranslationView;
|
|
335
|
+
}>;
|
|
336
|
+
/**
|
|
337
|
+
* The translations for exactly the sentences asked about, of one pair.
|
|
338
|
+
*
|
|
339
|
+
* The client hashes the body's sentences itself, so the slice is precise and
|
|
340
|
+
* the whole table never crosses the wire. Reads do not rewrite the file:
|
|
341
|
+
* the LRU stamp moves when the sentence next serves (the client's batched
|
|
342
|
+
* report of what it reused), not when a list of hashes arrives.
|
|
343
|
+
*
|
|
344
|
+
* @param request - the pair label and the sentence hashes wanted.
|
|
345
|
+
* @returns hash → translation for the hits.
|
|
346
|
+
*/
|
|
347
|
+
getSentenceTranslations(request: {
|
|
348
|
+
pair: string;
|
|
349
|
+
hashes: readonly string[];
|
|
350
|
+
}): Promise<{
|
|
351
|
+
translations: Record<string, string>;
|
|
352
|
+
}>;
|
|
353
|
+
/**
|
|
354
|
+
* Persist what one translation run produced, in one batch.
|
|
355
|
+
*
|
|
356
|
+
* `entries` are the sentences the model just translated: they upsert into the
|
|
357
|
+
* global memory AND the entry's exact-fit map. `recalled` are the sentences
|
|
358
|
+
* the run was served from memory: their lastUsedAt bumps (that is what makes
|
|
359
|
+
* the budget's LRU a use-clock rather than a write-clock), and they join the
|
|
360
|
+
* entry map too, so the map stays the complete record of this body. When
|
|
361
|
+
* `entryId` + `bodyHash` are present the entry map is REWRITTEN to the run's
|
|
362
|
+
* full sentence set — the run saw the whole body, so a merge would only
|
|
363
|
+
* preserve sentences the current body no longer has.
|
|
364
|
+
*
|
|
365
|
+
* @param request - the pair, the learned and recalled sentences, and the
|
|
366
|
+
* entry identity when this run was an article's.
|
|
367
|
+
* @returns how many sentences were newly learned.
|
|
368
|
+
*/
|
|
369
|
+
rememberSentences(request: {
|
|
370
|
+
pair: string;
|
|
371
|
+
entries: readonly ReaderSentenceLearn[];
|
|
372
|
+
recalled?: readonly ReaderSentenceLearn[];
|
|
373
|
+
entryId?: string;
|
|
374
|
+
bodyHash?: string;
|
|
375
|
+
}): Promise<{
|
|
376
|
+
stored: number;
|
|
377
|
+
}>;
|
|
378
|
+
/**
|
|
379
|
+
* How much the two caches currently hold, aggregated on the host.
|
|
380
|
+
*
|
|
381
|
+
* The tables themselves never cross the wire for a settings readout: counts
|
|
382
|
+
* and characters only. The characters are the store's own accounting unit —
|
|
383
|
+
* the same numbers the budgets bound.
|
|
384
|
+
*
|
|
385
|
+
* @returns per-tier usage.
|
|
386
|
+
*/
|
|
387
|
+
getStorageStats(): Promise<ReaderStorageStats>;
|
|
388
|
+
/**
|
|
389
|
+
* Forget every translation: the global sentence memory AND every entry map.
|
|
390
|
+
*
|
|
391
|
+
* The manage page's one gesture for it, after its own confirm. Bodies, tags,
|
|
392
|
+
* the recent list and the sources are untouched — the reader is clearing a
|
|
393
|
+
* derived cache, not their library. The commit's own `pruneBodies` sweep
|
|
394
|
+
* takes the files once the document no longer names them.
|
|
395
|
+
*
|
|
396
|
+
* @returns how many entry maps went, and whether the memory table went.
|
|
397
|
+
*/
|
|
398
|
+
clearTranslations(): Promise<{
|
|
399
|
+
clearedEntries: number;
|
|
400
|
+
clearedMemory: boolean;
|
|
401
|
+
}>;
|
|
402
|
+
/**
|
|
403
|
+
* The global sentence memory's table, from its sidecar file.
|
|
404
|
+
*
|
|
405
|
+
* Read per call, not cached: the file is the truth, and the service's own
|
|
406
|
+
* commits are the only writes — a cache keyed on the manifest would save a
|
|
407
|
+
* few MB of JSON parsing per translation run at the price of a coherence
|
|
408
|
+
* story. An absent, unreadable, or stale-version file reads as empty (the
|
|
409
|
+
* lazy half of the schema version).
|
|
410
|
+
*/
|
|
411
|
+
private translationMemory;
|
|
412
|
+
/** The tag vocabulary plus which entry ids carry each tag. */
|
|
413
|
+
listTags(): Promise<{
|
|
414
|
+
tags: ReaderTag[];
|
|
415
|
+
counts: Record<string, number>;
|
|
416
|
+
}>;
|
|
417
|
+
/**
|
|
418
|
+
* Create a tag, or return the existing one with the same name.
|
|
419
|
+
*
|
|
420
|
+
* Names are matched case-insensitively on purpose: "AI" and "ai" becoming two
|
|
421
|
+
* tags is what makes a tag system useless for filtering, and the reader
|
|
422
|
+
* cannot be expected to remember which spelling they used.
|
|
423
|
+
*
|
|
424
|
+
* @param request - the name to create or find.
|
|
425
|
+
* @returns the tag id, or a refusal.
|
|
426
|
+
*/
|
|
427
|
+
createTag(request: {
|
|
428
|
+
name: string;
|
|
429
|
+
}): Promise<ReaderTag | ReaderAnnotationOutcome>;
|
|
430
|
+
/** Rename a tag (its id, and therefore every entry that carries it, is untouched). */
|
|
431
|
+
renameTag(request: {
|
|
432
|
+
id: string;
|
|
433
|
+
name: string;
|
|
434
|
+
}): Promise<ReaderAnnotationOutcome>;
|
|
435
|
+
/** Drop a tag from the vocabulary and from every entry that carried it. */
|
|
436
|
+
deleteTag(request: {
|
|
437
|
+
id: string;
|
|
438
|
+
}): Promise<ReaderAnnotationOutcome>;
|
|
439
|
+
/** Add or remove one tag on one entry. */
|
|
440
|
+
tagEntry(request: {
|
|
441
|
+
entryId: string;
|
|
442
|
+
tagId: string;
|
|
443
|
+
on: boolean;
|
|
444
|
+
}): Promise<ReaderAnnotationOutcome>;
|
|
445
|
+
/** The tags on one entry. */
|
|
446
|
+
entryTags(request: {
|
|
447
|
+
entryId: string;
|
|
448
|
+
}): Promise<{
|
|
449
|
+
tags: ReaderTag[];
|
|
450
|
+
}>;
|
|
451
|
+
/** How long a fetched body is served, and how large the translation store may grow. */
|
|
452
|
+
getCachePolicy(): Promise<{
|
|
453
|
+
ttlHours: number;
|
|
454
|
+
maxEntries: number;
|
|
455
|
+
translationBudgetChars: number;
|
|
456
|
+
}>;
|
|
457
|
+
/** Change the cache policy (`ttlHours: 0` = keep until the budget evicts). */
|
|
458
|
+
setCachePolicy(request: {
|
|
459
|
+
ttlHours: number;
|
|
460
|
+
maxEntries?: number;
|
|
461
|
+
translationBudgetChars?: number;
|
|
462
|
+
}): Promise<ReaderAnnotationOutcome>;
|
|
463
|
+
/** Drop tags no entry references any more. */
|
|
464
|
+
pruneTags(): Promise<{
|
|
465
|
+
removed: number;
|
|
466
|
+
}>;
|
|
467
|
+
/**
|
|
468
|
+
* Record that the reader opened one entry.
|
|
469
|
+
*
|
|
470
|
+
* One row per ENTRY (not per source): the question the recent page answers is
|
|
471
|
+
* about the articles that were read, and an RSS entry has no source-level
|
|
472
|
+
* page to point at. Reopening an entry moves it to the top rather than
|
|
473
|
+
* duplicating it, and the list is capped — see `MAX_RECENT_ENTRIES`.
|
|
474
|
+
*
|
|
475
|
+
* The write is deliberately not batched: opening is a human-paced gesture,
|
|
476
|
+
* and a queue that had not flushed yet would lose the item on a restart.
|
|
477
|
+
*
|
|
478
|
+
* @param request - the entry, its source, and what to call it in the list.
|
|
479
|
+
* @returns how many entries the list holds afterwards.
|
|
480
|
+
*/
|
|
481
|
+
recordRead(request: {
|
|
482
|
+
entryId: string;
|
|
483
|
+
sourceId: string;
|
|
484
|
+
title: string;
|
|
485
|
+
url?: string;
|
|
486
|
+
}): Promise<{
|
|
487
|
+
entries: number;
|
|
488
|
+
}>;
|
|
489
|
+
/**
|
|
490
|
+
* The entries the reader opened, newest first.
|
|
491
|
+
*
|
|
492
|
+
* @returns the recent list as the page renders it.
|
|
493
|
+
*/
|
|
494
|
+
listRecent(): Promise<{
|
|
495
|
+
entries: ReaderRecentEntry[];
|
|
496
|
+
}>;
|
|
497
|
+
/**
|
|
498
|
+
* Forget every recent entry.
|
|
499
|
+
*
|
|
500
|
+
* The reader's own list is theirs to erase: a "recently read" page that can
|
|
501
|
+
* only grow is one nobody wants to open in company.
|
|
502
|
+
*
|
|
503
|
+
* @returns how many entries were dropped.
|
|
504
|
+
*/
|
|
505
|
+
clearRecent(): Promise<{
|
|
506
|
+
removed: number;
|
|
507
|
+
}>;
|
|
508
|
+
/**
|
|
509
|
+
* Record why a fetch produced nothing, so the retry is a decision.
|
|
510
|
+
*
|
|
511
|
+
* The failure also carries its classified CODE when there is one: the wall's
|
|
512
|
+
* card button explains a red state with the same sentence the detail view
|
|
513
|
+
* uses, instead of the seam's raw words.
|
|
514
|
+
*
|
|
515
|
+
* @param entryId - the entry.
|
|
516
|
+
* @param error - the message to keep, verbatim.
|
|
517
|
+
* @param code - the classified reason, when one was derived.
|
|
518
|
+
*/
|
|
519
|
+
private recordFetchFailure;
|
|
520
|
+
/**
|
|
521
|
+
* Ask the link-resolver table what a pasted URL names.
|
|
522
|
+
*
|
|
523
|
+
* Runs on the ADD path only, and never breaks it: the table's own failure
|
|
524
|
+
* (a resolver bug, an answer shape nobody planned for) reads as "no answer"
|
|
525
|
+
* and the ordinary fetch path takes over. Resolvers that need the network
|
|
526
|
+
* ride the same sanctioned seam as every fetch here; with no seam mounted
|
|
527
|
+
* their fetch throws, they answer `null`, and the ordinary path reports the
|
|
528
|
+
* missing capability — while a resolver that needs no network (OpenReview's
|
|
529
|
+
* known-unreadable verdict) still answers.
|
|
530
|
+
*/
|
|
531
|
+
private resolvePaper;
|
|
532
|
+
/** The probed web seam (no host import, no boot-time dependency). */
|
|
533
|
+
private web;
|
|
534
|
+
/** The probed side-chat service. */
|
|
535
|
+
private sideChat;
|
|
536
|
+
/**
|
|
537
|
+
* The document of record: from disk when one is mounted, else the in-memory
|
|
538
|
+
* copy the service has been accumulating.
|
|
539
|
+
*/
|
|
540
|
+
private currentDoc;
|
|
541
|
+
/**
|
|
542
|
+
* Apply a mutation to the document of record.
|
|
543
|
+
*
|
|
544
|
+
* Without a filesystem the mutation lands in memory so the session still
|
|
545
|
+
* works; with one it goes through the store's version guard.
|
|
546
|
+
*
|
|
547
|
+
* @param mutate - pure function from the current document to the next.
|
|
548
|
+
*/
|
|
549
|
+
private commit;
|
|
550
|
+
/**
|
|
551
|
+
* Fetch a URL, following the cross-origin hops that can be reconstructed.
|
|
552
|
+
*
|
|
553
|
+
* The seam refuses a cross-origin hop and names only the target ORIGIN, so
|
|
554
|
+
* there is exactly one hop this method can follow without inventing a URL:
|
|
555
|
+
* a same-host scheme/port change, which keeps the path and query
|
|
556
|
+
* (`http://host/feed` → `https://host/feed`). A hop to a DIFFERENT host is
|
|
557
|
+
* re-thrown untouched — the caller classifies it, and the reader is told the
|
|
558
|
+
* address moves to another site rather than being served some other page
|
|
559
|
+
* under the URL they pasted.
|
|
560
|
+
*
|
|
561
|
+
* @param url - the initial URL.
|
|
562
|
+
* @returns the final URL and its decoded payload.
|
|
563
|
+
*/
|
|
564
|
+
private fetchFollowing;
|
|
565
|
+
/** Arm (or re-arm) the daily refresh timer against the current document. */
|
|
566
|
+
private armSchedule;
|
|
567
|
+
}
|
|
568
|
+
/** Parse and normalize a pasted URL; reject anything that is not http(s). */
|
|
569
|
+
export declare function normalizeUrl(input: string): string | undefined;
|
|
570
|
+
/**
|
|
571
|
+
* The next URL a cross-origin refusal can be followed to — or `undefined`.
|
|
572
|
+
*
|
|
573
|
+
* The host seams throw a `WEB_REDIRECT_BLOCKED` error whose CODE is a property
|
|
574
|
+
* and whose message names only the target origin. The original implementation
|
|
575
|
+
* tested the message for the code text, which it never contains, so the whole
|
|
576
|
+
* follow path was dead code and every cross-origin redirect surfaced as a raw
|
|
577
|
+
* seam error. This reads the code where it actually lives.
|
|
578
|
+
*
|
|
579
|
+
* @param error - the value the seam threw.
|
|
580
|
+
* @param current - the URL whose request was refused.
|
|
581
|
+
* @returns the same path and query on the new origin, when the HOST is
|
|
582
|
+
* unchanged (a scheme or port upgrade); otherwise `undefined`.
|
|
583
|
+
*/
|
|
584
|
+
export declare function crossOriginRetarget(error: unknown, current: string): string | undefined;
|
|
585
|
+
/**
|
|
586
|
+
* The smallest amount of visible text that still counts as a readable page.
|
|
587
|
+
*
|
|
588
|
+
* Deliberately small: this only runs on a freshly fetched page, and its job is
|
|
589
|
+
* to catch interstitials — a bot challenge, a cookie notice, an empty shell —
|
|
590
|
+
* not to judge whether an article is long enough to be worth reading.
|
|
591
|
+
*/
|
|
592
|
+
export declare const MIN_PREVIEW_TEXT_CHARS = 200;
|
|
593
|
+
/**
|
|
594
|
+
* Whether a fetched page can be previewed at all, and why not when it cannot.
|
|
595
|
+
*
|
|
596
|
+
* The host has no DOM, so this is a deliberately crude reading of the payload:
|
|
597
|
+
* strip script/style/tags, count what is left, and only when that is
|
|
598
|
+
* vanishingly small look for the words a wall leaves behind. A page with real
|
|
599
|
+
* text is never second-guessed.
|
|
600
|
+
*
|
|
601
|
+
* @param raw - the decoded payload.
|
|
602
|
+
* @param finalUrl - where the fetch actually landed.
|
|
603
|
+
* @param requestedUrl - the URL the reader pasted.
|
|
604
|
+
* @returns the reason when there is nothing to preview, else `undefined`.
|
|
605
|
+
*/
|
|
606
|
+
export declare function inspectPreview(raw: string, finalUrl: string, requestedUrl: string): {
|
|
607
|
+
code: ReaderPreviewFailureCode;
|
|
608
|
+
message: string;
|
|
609
|
+
} | undefined;
|
|
610
|
+
/**
|
|
611
|
+
* Classify a thrown fetch failure into the reason the reader sees.
|
|
612
|
+
*
|
|
613
|
+
* Everything here is per-case diagnosable: the seam's own message is kept
|
|
614
|
+
* verbatim, and only the CODE decides which sentence the browser shows and
|
|
615
|
+
* whether an automatic retry is allowed.
|
|
616
|
+
*
|
|
617
|
+
* @param error - the value the fetch threw.
|
|
618
|
+
* @param at - the failure instant (ISO-8601).
|
|
619
|
+
* @returns the recorded failure.
|
|
620
|
+
*/
|
|
621
|
+
export declare function classifyFetchFailure(error: unknown, at: string): ReaderPreviewFailure;
|
|
622
|
+
/**
|
|
623
|
+
* How much visible text a payload carries.
|
|
624
|
+
*
|
|
625
|
+
* Script, style and markup are stripped; entities collapse to a single space.
|
|
626
|
+
* This is a measurement, not a parser: its only job is to separate "a page"
|
|
627
|
+
* from "an interstitial that is technically HTML".
|
|
628
|
+
*
|
|
629
|
+
* @param raw - the decoded payload.
|
|
630
|
+
* @returns the visible character count.
|
|
631
|
+
*/
|
|
632
|
+
export declare function visibleTextLength(raw: string): number;
|
|
633
|
+
//# sourceMappingURL=service.d.ts.map
|