@deadair/plugin-sdk 0.86.0 → 0.86.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/feed.parse.d.ts +25 -4
- package/dist/feed.parse.d.ts.map +1 -1
- package/dist/index.js +57 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/feed.parse.d.ts
CHANGED
|
@@ -18,10 +18,16 @@ import type { HostFetchInit, PluginHost } from './plugin.host.js';
|
|
|
18
18
|
* thin deliberately: `host.fetch` already carries the allowlist, the pacing and
|
|
19
19
|
* the `Retry-After` back-off, so there is nothing left here but a status check.
|
|
20
20
|
*
|
|
21
|
-
* Caching
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
21
|
+
* Caching is NOT here: how often a feed is worth asking belongs to whoever is
|
|
22
|
+
* polling, and a plugin knows that about its own feeds where this file does
|
|
23
|
+
* not. Conditional GET IS here, because it is not a cache in the sense that
|
|
24
|
+
* matters. {@link fetchFeed} remembers the `ETag` and `Last-Modified` the
|
|
25
|
+
* publisher sent with the last document it parsed for a URL, sends them back
|
|
26
|
+
* as `If-None-Match` and `If-Modified-Since`, and answers a 304 with that
|
|
27
|
+
* document. Every call still goes on the wire and the publisher still decides
|
|
28
|
+
* whether anything changed, so no caller can be handed a stale feed by it;
|
|
29
|
+
* what an unchanged feed costs becomes a status line instead of a document.
|
|
30
|
+
* See {@link FEED_VALIDATORS_MAX} for the bound.
|
|
25
31
|
*
|
|
26
32
|
* ## A podcast is a feed with an attachment
|
|
27
33
|
*
|
|
@@ -176,6 +182,21 @@ export declare function parseFeed(xml: string): ParsedFeed;
|
|
|
176
182
|
* neighbours — which {@link PluginErrorCode} a status means — and that is
|
|
177
183
|
* `plugin.http.ts`'s ladder, unmodified. A service with its own reading of a
|
|
178
184
|
* status layers that in front by calling {@link parseFeed} itself.
|
|
185
|
+
*
|
|
186
|
+
* The one thing it adds to the request is conditional GET: the validators the
|
|
187
|
+
* publisher sent last time go back with this request, and a 304 is answered
|
|
188
|
+
* with the document they came from (a copy, so a caller that edits what it was
|
|
189
|
+
* handed cannot edit what the next caller gets). A 304 this did not ask for is
|
|
190
|
+
* the caller's own and fails the status check like any other non-2xx.
|
|
179
191
|
*/
|
|
180
192
|
export declare function fetchFeed(host: PluginHost, url: string, init?: HostFetchInit): Promise<ParsedFeed>;
|
|
193
|
+
/**
|
|
194
|
+
* How many feeds one host remembers validators for, oldest dropped first.
|
|
195
|
+
*
|
|
196
|
+
* A station reads tens of feeds rather than thousands, so this is a ceiling a
|
|
197
|
+
* plugin that builds URLs from user input cannot grow past rather than a size
|
|
198
|
+
* anybody is expected to reach. Losing an entry costs one full document the
|
|
199
|
+
* next time that feed is asked, and nothing else.
|
|
200
|
+
*/
|
|
201
|
+
export declare const FEED_VALIDATORS_MAX = 256;
|
|
181
202
|
//# sourceMappingURL=feed.parse.d.ts.map
|
package/dist/feed.parse.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"feed.parse.d.ts","sourceRoot":"","sources":["../src/feed.parse.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAElE
|
|
1
|
+
{"version":3,"file":"feed.parse.d.ts","sourceRoot":"","sources":["../src/feed.parse.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAElE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AAEH,8CAA8C;AAC9C,MAAM,WAAW,QAAQ;IACrB;;;;;;;;;;;;OAYG;IACH,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,gFAAgF;IAChF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,+EAA+E;IAC/E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,aAAa,CAAC;IAC1B;;;;;;;;OAQG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oFAAoF;IACpF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,iFAAiF;IACjF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kFAAkF;IAClF,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,aAAa;IAC1B,gFAAgF;IAChF,GAAG,EAAE,MAAM,CAAC;IACZ,+DAA+D;IAC/D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,iCAAiC;AACjC,MAAM,WAAW,UAAU;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iGAAiG;IACjG,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8FAA8F;IAC9F,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2FAA2F;IAC3F,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,kGAAkG;IAClG,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,mFAAmF;IACnF,KAAK,EAAE,QAAQ,EAAE,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAuB1C;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAqDjD;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,UAAU,CAAC,CAiCxG;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,MAAM,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -786,7 +786,16 @@ function parseFeed(xml) {
|
|
|
786
786
|
}
|
|
787
787
|
__name(parseFeed, "parseFeed");
|
|
788
788
|
async function fetchFeed(host, url, init) {
|
|
789
|
-
const
|
|
789
|
+
const known = validatorsFor(host);
|
|
790
|
+
const remembered = known.get(url);
|
|
791
|
+
const asked = conditional(init, remembered);
|
|
792
|
+
const response = await host.fetch(url, asked ?? init);
|
|
793
|
+
if (response.status === 304 && remembered !== void 0 && asked !== void 0) {
|
|
794
|
+
await response.body?.cancel();
|
|
795
|
+
known.delete(url);
|
|
796
|
+
known.set(url, remembered);
|
|
797
|
+
return structuredClone(remembered.feed);
|
|
798
|
+
}
|
|
790
799
|
if (!response.ok) {
|
|
791
800
|
await response.body?.cancel();
|
|
792
801
|
const error = new PluginError(`feed request failed: ${upstreamDetail(response.status, response.statusText)}`).withCode(pluginCodeForStatus(response.status));
|
|
@@ -795,9 +804,54 @@ async function fetchFeed(host, url, init) {
|
|
|
795
804
|
if (advice !== void 0) error.withRetry(advice);
|
|
796
805
|
throw error;
|
|
797
806
|
}
|
|
798
|
-
|
|
807
|
+
const feed = parseFeed(await response.text());
|
|
808
|
+
remember(known, url, response.headers, feed);
|
|
809
|
+
return feed;
|
|
799
810
|
}
|
|
800
811
|
__name(fetchFeed, "fetchFeed");
|
|
812
|
+
var FEED_VALIDATORS_MAX = 256;
|
|
813
|
+
var validatorsByHost = /* @__PURE__ */ new WeakMap();
|
|
814
|
+
function validatorsFor(host) {
|
|
815
|
+
let known = validatorsByHost.get(host);
|
|
816
|
+
if (known === void 0) {
|
|
817
|
+
known = /* @__PURE__ */ new Map();
|
|
818
|
+
validatorsByHost.set(host, known);
|
|
819
|
+
}
|
|
820
|
+
return known;
|
|
821
|
+
}
|
|
822
|
+
__name(validatorsFor, "validatorsFor");
|
|
823
|
+
function conditional(init, remembered) {
|
|
824
|
+
if (remembered === void 0) return void 0;
|
|
825
|
+
const headers = {
|
|
826
|
+
...init?.headers
|
|
827
|
+
};
|
|
828
|
+
const named = new Set(Object.keys(headers).map((name) => name.toLowerCase()));
|
|
829
|
+
if (named.has("if-none-match") || named.has("if-modified-since")) return void 0;
|
|
830
|
+
if (remembered.etag !== void 0) headers["if-none-match"] = remembered.etag;
|
|
831
|
+
if (remembered.lastModified !== void 0) headers["if-modified-since"] = remembered.lastModified;
|
|
832
|
+
return {
|
|
833
|
+
...init,
|
|
834
|
+
headers
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
__name(conditional, "conditional");
|
|
838
|
+
function remember(known, url, headers, feed) {
|
|
839
|
+
known.delete(url);
|
|
840
|
+
const etag = headers.get("etag") ?? void 0;
|
|
841
|
+
const lastModified = headers.get("last-modified") ?? void 0;
|
|
842
|
+
if (etag === void 0 && lastModified === void 0) return;
|
|
843
|
+
const entry = {
|
|
844
|
+
feed: structuredClone(feed)
|
|
845
|
+
};
|
|
846
|
+
if (etag !== void 0) entry.etag = etag;
|
|
847
|
+
if (lastModified !== void 0) entry.lastModified = lastModified;
|
|
848
|
+
known.set(url, entry);
|
|
849
|
+
if (known.size > FEED_VALIDATORS_MAX) {
|
|
850
|
+
const oldest = known.keys().next().value;
|
|
851
|
+
if (oldest !== void 0) known.delete(oldest);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
__name(remember, "remember");
|
|
801
855
|
function readItem(entry) {
|
|
802
856
|
const title = speakable(entry.title);
|
|
803
857
|
if (title === void 0) return void 0;
|
|
@@ -1565,6 +1619,7 @@ export {
|
|
|
1565
1619
|
ENRICHMENT_MATCH_KEY_ARTIST_TITLE,
|
|
1566
1620
|
ENRICHMENT_MATCH_KEY_ISRC,
|
|
1567
1621
|
FEED_SUMMARY_MAX_CHARS,
|
|
1622
|
+
FEED_VALIDATORS_MAX,
|
|
1568
1623
|
HOST_FETCH_METHODS,
|
|
1569
1624
|
JSON_SAFE_PAYLOAD_TYPES,
|
|
1570
1625
|
KNOWN_ENRICHMENT_MATCH_KEYS,
|