@deadair/plugin-sdk 0.86.0 → 0.86.2

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.
@@ -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 and conditional GET (`ETag`, `Last-Modified`) are NOT here. They
22
- * belong to whoever is polling — a plugin knows how often its own feeds are
23
- * worth asking and this file does not — and putting a cache behind a pure
24
- * parser would hide it from the one caller that must not be surprised by it.
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
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;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;;;;;;;;;GASG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,UAAU,CAAC,CAoBxG"}
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 response = await host.fetch(url, init);
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
- return parseFeed(await response.text());
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;
@@ -1275,6 +1329,7 @@ var configFieldOptionSourceSchema = z.enum([
1275
1329
  "station.newsFeeds",
1276
1330
  "station.podcastShows",
1277
1331
  "station.narrationSeries",
1332
+ "station.ownShows",
1278
1333
  "intl.timeZones",
1279
1334
  "plugins.speech",
1280
1335
  "plugins.llm",
@@ -1565,6 +1620,7 @@ export {
1565
1620
  ENRICHMENT_MATCH_KEY_ARTIST_TITLE,
1566
1621
  ENRICHMENT_MATCH_KEY_ISRC,
1567
1622
  FEED_SUMMARY_MAX_CHARS,
1623
+ FEED_VALIDATORS_MAX,
1568
1624
  HOST_FETCH_METHODS,
1569
1625
  JSON_SAFE_PAYLOAD_TYPES,
1570
1626
  KNOWN_ENRICHMENT_MATCH_KEYS,