webring 1.0.3 → 1.1.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/CHANGELOG.md CHANGED
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.1.0](https://github.com/shepherdjerred/webring/compare/v1.0.3...v1.1.0) (2024-06-28)
9
+
10
+
11
+ ### Features
12
+
13
+ * source mappings ([095fe2b](https://github.com/shepherdjerred/webring/commit/095fe2be44e25547271730a5611d4710609cdf8d))
14
+
8
15
  ## [1.0.3](https://github.com/shepherdjerred/webring/compare/v1.0.2...v1.0.3) (2024-06-24)
9
16
 
10
17
 
package/dist/cache.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  import { type Cache, type Result, type ResultEntry, type Source, type CachedConfiguration } from "./types.js";
2
2
  export declare function fetchAllCached(config: CachedConfiguration): Promise<Result>;
3
3
  export declare function fetchWithCache(source: Source, cache: Cache, config: CachedConfiguration): Promise<ResultEntry | undefined>;
4
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,KAAK,EACV,KAAK,MAAM,EACX,KAAK,WAAW,EAEhB,KAAK,MAAM,EACX,KAAK,mBAAmB,EAGzB,MAAM,YAAY,CAAC;AAsCpB,wBAAsB,cAAc,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,CAQjF;AAED,wBAAsB,cAAc,CAClC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,mBAAmB,GAC1B,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAgBlC"}
package/dist/cache.js CHANGED
@@ -52,3 +52,4 @@ export async function fetchWithCache(source, cache, config) {
52
52
  console.log(`Fetching ${source.url}`);
53
53
  return fetch(source, config.truncate);
54
54
  }
55
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"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"}
package/dist/fetch.d.ts CHANGED
@@ -10,3 +10,4 @@ export declare function fetchAll(config: Configuration): Promise<{
10
10
  preview?: string | undefined;
11
11
  }[]>;
12
12
  export declare function fetch(source: Source, length: number): Promise<ResultEntry | undefined>;
13
+ //# sourceMappingURL=fetch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../src/fetch.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,MAAM,EAAE,KAAK,WAAW,EAAmB,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAOhG,wBAAsB,QAAQ,CAAC,MAAM,EAAE,aAAa;;;;;;;;;KAEnD;AAED,wBAAsB,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAsC5F"}
package/dist/fetch.js CHANGED
@@ -35,3 +35,4 @@ export async function fetch(source, length) {
35
35
  return undefined;
36
36
  }
37
37
  }
38
+ //# sourceMappingURL=fetch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch.js","sourceRoot":"","sources":["../src/fetch.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,YAAY,CAAC;AAChC,OAAO,YAAY,MAAM,eAAe,CAAC;AACzC,OAAO,QAAQ,MAAM,eAAe,CAAC;AACrC,OAAO,EAAiC,eAAe,EAAsB,MAAM,YAAY,CAAC;AAChG,OAAO,KAAK,CAAC,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAC;AAEpD,sFAAsF;AACtF,MAAM,UAAU,GAA4B,QAA8C,CAAC;AAE3F,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,MAAqB;IAClD,OAAO,MAAM,uBAAuB,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;AACnG,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,KAAK,CAAC,MAAc,EAAE,MAAc;IACxD,MAAM,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;IAC5B,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAE/C,MAAM,SAAS,GAAG,CAAC,CAAC,IAAI,CACtB,IAAI,CAAC,KAAK,EACV,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,eAAe,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,EAC5C,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC,EACjD,CAAC,CAAC,OAAO,EAAE,EACX,CAAC,CAAC,KAAK,EAAE,CACV,CAAC;QAEF,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAC;QAC5C,CAAC;QAED,MAAM,OAAO,GACX,SAAS,CAAC,cAAc,IAAI,SAAS,CAAC,OAAO,IAAI,SAAS,CAAC,WAAW,IAAI,SAAS,CAAC,iBAAiB,CAAC,CAAC;QAEzG,OAAO;YACL,KAAK,EAAE,SAAS,CAAC,KAAK;YACtB,GAAG,EAAE,SAAS,CAAC,IAAI;YACnB,IAAI,EAAE,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;YAC9B,MAAM;YACN,OAAO,EAAE,OAAO;gBACd,CAAC,CAAC,UAAU,CACR,YAAY,CAAC,OAAO,EAAE;oBACpB,oBAAoB,EAAE,KAAK;iBAC5B,CAAC,EACF,MAAM,CACP;gBACH,CAAC,CAAC,SAAS;SACd,CAAC;IACJ,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,CAAC,KAAK,CAAC,kBAAkB,MAAM,CAAC,GAAG,KAAK,CAAW,EAAE,CAAC,CAAC;QAC9D,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  import { type Configuration, type Result } from "./types.js";
2
2
  export declare function run(config: Configuration): Promise<Result>;
3
3
  export * from "./types.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,MAAM,EAA6B,MAAM,YAAY,CAAC;AAExF,wBAAsB,GAAG,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAoBhE;AAED,cAAc,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -17,3 +17,4 @@ export async function run(config) {
17
17
  return topResults;
18
18
  }
19
19
  export * from "./types.js";
20
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAE,cAAc,IAAI,cAAc,EAAE,MAAM,YAAY,CAAC;AAC9D,OAAO,EAAE,QAAQ,IAAI,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC1D,OAAO,EAAmC,yBAAyB,EAAE,MAAM,YAAY,CAAC;AAExF,MAAM,CAAC,KAAK,UAAU,GAAG,CAAC,MAAqB;IAC7C,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,yBAAyB,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAEtE,IAAI,MAAc,CAAC;IACnB,IAAI,OAAO,EAAE,CAAC;QACZ,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC,CAAC;QACxD,MAAM,GAAG,MAAM,cAAc,CAAC,IAAI,CAAC,CAAC;IACtC,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;QAC/B,MAAM,GAAG,MAAM,gBAAgB,CAAC,MAAM,CAAC,CAAC;IAC1C,CAAC;IAED,MAAM,UAAU,GAAG,CAAC,CAAC,IAAI,CACvB,MAAM,EACN,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,EAC3C,CAAC,CAAC,OAAO,EAAE,EACX,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CACtB,CAAC;IAEF,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,cAAc,YAAY,CAAC"}
@@ -1 +1,2 @@
1
1
  export {};
2
+ //# sourceMappingURL=index.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":""}
@@ -104,3 +104,4 @@ async function createTempDir() {
104
104
  const dir = join(ostmpdir, "unit-test-");
105
105
  return await mkdtemp(dir);
106
106
  }
107
+ //# sourceMappingURL=index.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"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"}
package/dist/types.d.ts CHANGED
@@ -366,3 +366,4 @@ export declare const FeedEntrySchema: z.ZodEffects<z.ZodObject<{
366
366
  description?: string | undefined;
367
367
  }>;
368
368
  export {};
369
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAClD,QAAA,MAAM,YAAY;;;;;;;;;EAKhB,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAC1E,QAAA,MAAM,wBAAwB;;;;;;;;;EAI5B,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAChE,QAAA,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAQvB,CAAC;AAGH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAC5E,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAEpC,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC5D,QAAA,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAMrB,CAAC;AAEH,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAClD,QAAA,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;WAA6B,CAAC;AAEhD,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC1D,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAG3B,CAAC;AAEH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAChD,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAA6B,CAAC;AAEtD,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAyBxB,CAAC"}
package/dist/types.js CHANGED
@@ -61,3 +61,4 @@ export const FeedEntrySchema = z
61
61
  "content:encoded": entry["content:encoded"],
62
62
  };
63
63
  });
64
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/util.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export declare function asyncMapFilterUndefined<T, U>(input: T[], fn: (x: T) => Promise<U | undefined>): Promise<U[]>;
2
2
  export declare function asyncMap<T, U>(input: T[], fn: (x: T) => Promise<U>): Promise<U[]>;
3
3
  export declare function filterUndefined<T>(input: (T | undefined)[]): T[];
4
+ //# sourceMappingURL=util.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAGA,wBAAsB,uBAAuB,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC,CAGlH;AAED,wBAAsB,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC,CAOvF;AAED,wBAAgB,eAAe,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,GAAG,SAAS,CAAC,EAAE,GAAG,CAAC,EAAE,CAKhE"}
package/dist/util.js CHANGED
@@ -11,3 +11,4 @@ export async function asyncMap(input, fn) {
11
11
  export function filterUndefined(input) {
12
12
  return R.pipe(input, R.filter((result) => result !== undefined));
13
13
  }
14
+ //# sourceMappingURL=util.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,QAAQ,CAAC;AAE5B,8DAA8D;AAC9D,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAO,KAAU,EAAE,EAAoC;IAClG,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAC1C,OAAO,eAAe,CAAC,OAAO,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAO,KAAU,EAAE,EAAwB;IACvE,MAAM,QAAQ,GAAG,CAAC,CAAC,IAAI,CACrB,KAAK,EACL,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAC1B,CAAC;IAEF,OAAO,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;AAC/B,CAAC;AAED,MAAM,UAAU,eAAe,CAAI,KAAwB;IACzD,OAAO,CAAC,CAAC,IAAI,CACX,KAAK,EACL,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,KAAK,SAAS,CAAC,CACpC,CAAC;AACX,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "webring",
3
3
  "type": "module",
4
- "version": "1.0.3",
4
+ "version": "1.1.0",
5
5
  "scripts": {
6
6
  "prepare": "husky",
7
7
  "lint": "eslint src",
@@ -23,7 +23,6 @@
23
23
  "@eslint/js": "^9.4.0",
24
24
  "@tsconfig/node20": "^20.1.4",
25
25
  "@tsconfig/strictest": "^2.0.5",
26
- "@types/eslint__js": "^8.42.3",
27
26
  "@types/node": "^20.14.0",
28
27
  "@types/sanitize-html": "^2.11.0",
29
28
  "@typescript-eslint/eslint-plugin": "^7.11.0",
@@ -50,6 +49,7 @@
50
49
  },
51
50
  "files": [
52
51
  "dist",
52
+ "src",
53
53
  "package.json",
54
54
  "README.md",
55
55
  "LICENSE",
@@ -0,0 +1,146 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`it should fetch an RSS feed with caching 1`] = `
4
+ [
5
+ {
6
+ "date": 2024-06-05T00:00:00.000Z,
7
+ "preview": "TIL: Asymmetric Cryptography in Go",
8
+ "source": {
9
+ "title": "Jerred Shepherd",
10
+ "url": "https://sjer.red/rss.xml",
11
+ },
12
+ "title": "TIL: Asymmetric Cryptography in Go",
13
+ "url": "https://sjer.red/blog/til/2024-06-05/",
14
+ },
15
+ ]
16
+ `;
17
+
18
+ exports[`it should fetch an RSS feed without caching 1`] = `
19
+ [
20
+ {
21
+ "date": 2024-06-05T00:00:00.000Z,
22
+ "preview": "TIL: Asymmetric Cryptography in Go",
23
+ "source": {
24
+ "title": "Jerred Shepherd",
25
+ "url": "https://sjer.red/rss.xml",
26
+ },
27
+ "title": "TIL: Asymmetric Cryptography in Go",
28
+ "url": "https://sjer.red/blog/til/2024-06-05/",
29
+ },
30
+ ]
31
+ `;
32
+
33
+ exports[`it should fetch several RSS feeds 1`] = `
34
+ [
35
+ {
36
+ "date": 2024-06-20T14:00:00.000Z,
37
+ "preview": "Looking for constructors that take a character count. The post How to convert between different types of counted-string string types appeared first on The Old New Thing.",
38
+ "source": {
39
+ "title": "The Old New Thing",
40
+ "url": "https://devblogs.microsoft.com/oldnewthing/feed",
41
+ },
42
+ "title": "How to convert between different types of counted-string string types",
43
+ "url": "https://devblogs.microsoft.com/oldnewthing/20240620-00/?p=109922",
44
+ },
45
+ {
46
+ "date": 2024-06-19T00:00:00.000Z,
47
+ "preview": "The recent innovations in the AI space, most notably those such as GPT-4, obviously have far-reaching implications for society, ranging from the utopian eliminating of drudgery, to the dystopian damage to the livelihood of artists in a capitalist society, to existential threats to humanity itself. I...",
48
+ "source": {
49
+ "title": "Ludicity",
50
+ "url": "https://ludic.mataroa.blog/rss/",
51
+ },
52
+ "title": "I Will Fucking Piledrive You If You Mention AI Again",
53
+ "url": "https://ludic.mataroa.blog/blog/i-will-fucking-piledrive-you-if-you-mention-ai-again/",
54
+ },
55
+ {
56
+ "date": 2024-06-19T00:00:00.000Z,
57
+ "preview": "I love this meetup so much",
58
+ "source": {
59
+ "title": "Xe Iaso",
60
+ "url": "https://xeiaso.net/blog.rss",
61
+ },
62
+ "title": "AI Tinkerers Ottawa v2.5.0 trip report",
63
+ "url": "https://xeiaso.net/notes/2024/ait-ottawa-2.5.0/",
64
+ },
65
+ {
66
+ "date": 2024-06-08T04:30:00.000Z,
67
+ "preview": "Fresh water from snow, at 70 below!",
68
+ "source": {
69
+ "title": "brr.fyi",
70
+ "url": "https://brr.fyi/feed.xml",
71
+ },
72
+ "title": "South Pole Water Infrastructure",
73
+ "url": "https://brr.fyi/posts/south-pole-water-infrastructure",
74
+ },
75
+ {
76
+ "date": 2024-06-01T00:00:00.000Z,
77
+ "preview": ".dog-line { display: flex; flex-wrap: nowrap; flex-direction: row; width: 100%; height: 10rem; margin-top: 2rem; margin-bottom: 2rem; } .dog-line img { flex-grow: 1; height: auto; margin: 0; padding: 0; object-fit: contain; } .dog-grid { display: grid; grid-template-columns: repeat(4, 1fr); grid-gap...",
78
+ "source": {
79
+ "title": "Sam Rose",
80
+ "url": "https://samwho.dev/rss.xml",
81
+ },
82
+ "title": "A Commitment to Art and Dogs",
83
+ "url": "https://samwho.dev/dogs/",
84
+ },
85
+ {
86
+ "date": 2024-05-31T00:00:00.000Z,
87
+ "preview": "This is an archive of some posts in a forum thread titled "Beware of Bioware" in a now defunct forum, with comments from that forum as well as blog comments from a now defunct blog that archived that made the first attempt to archive this content. The original posts were deleted shortly after being ...",
88
+ "source": {
89
+ "title": "Dan Luu",
90
+ "url": "https://danluu.com/atom.xml",
91
+ },
92
+ "title": "Work-life balance at Bioware",
93
+ "url": "https://danluu.com/bioware/",
94
+ },
95
+ {
96
+ "date": 2024-05-24T00:00:00.000Z,
97
+ "preview": "I needed a bit of a break from “real work” recently, so I started a new programming project that was low-stakes and purely recreational. On April 21st, I set out to see how much of a Unix-like operating system for x86_64 targets that I could put together in about a month. The result is Bunnix. Not i...",
98
+ "source": {
99
+ "title": "Drew DeVault",
100
+ "url": "https://drewdevault.com/blog/index.xml",
101
+ },
102
+ "title": "Writing a Unix clone in about a month",
103
+ "url": "https://drewdevault.com/2024/05/24/2024-05-24-Bunnix.html",
104
+ },
105
+ {
106
+ "date": 2024-02-27T12:00:00.000Z,
107
+ "preview": "The dream of soaring in the sky like a bird has captivated the human mind for ages. Although many failed, some eventually succeeded in achieving that goal. These days we take air transportation for granted, but the physics of flight can still be puzzling. In this article we’ll investigate what makes...",
108
+ "source": {
109
+ "title": "Bartosz Ciechanowski",
110
+ "url": "https://ciechanow.ski/atom.xml",
111
+ },
112
+ "title": "Airfoil",
113
+ "url": "https://ciechanow.ski/airfoil/",
114
+ },
115
+ {
116
+ "date": 2024-01-29T00:00:00.000Z,
117
+ "preview": "Web components won't take web development by storm, or show us the One True Way to build websites. What they will do is let us collectively build a rich ecosystem of dynamic components that work with any web stack.",
118
+ "source": {
119
+ "title": "Jake Lazaroff",
120
+ "url": "https://jakelazaroff.com/rss.xml",
121
+ },
122
+ "title": "The Web Component Success Story",
123
+ "url": "https://jakelazaroff.com/words/the-web-component-success-story/",
124
+ },
125
+ {
126
+ "date": 2023-04-07T00:00:00.000Z,
127
+ "preview": "In early 2019, some months after completing a rehab program for drug addiction, I was in a very open-minded headspace where I wanted to challenge myself and find ways to improve as a person. Drugs had filled my life with secrecy and lies, but that life was over. Although I was unsure of my next step...",
128
+ "source": {
129
+ "title": "Andreas Kling",
130
+ "url": "https://awesomekling.github.io/feed.xml",
131
+ },
132
+ "title": "Making myself uncomfortable again",
133
+ "url": "https://awesomekling.github.io/Making-myself-uncomfortable-again/",
134
+ },
135
+ {
136
+ "date": 2022-09-17T00:00:00.000Z,
137
+ "preview": undefined,
138
+ "source": {
139
+ "title": "Explained From First Principles",
140
+ "url": "https://explained-from-first-principles.com/feed.xml",
141
+ },
142
+ "title": "Number theory explained from first principles",
143
+ "url": "https://explained-from-first-principles.com/number-theory/",
144
+ },
145
+ ]
146
+ `;
package/src/cache.ts ADDED
@@ -0,0 +1,79 @@
1
+ import * as R from "remeda";
2
+ import {
3
+ type Cache,
4
+ type Result,
5
+ type ResultEntry,
6
+ type CacheEntry,
7
+ type Source,
8
+ type CachedConfiguration,
9
+ CacheSchema,
10
+ type CacheConfiguration,
11
+ } from "./types.js";
12
+ import { fetch } from "./fetch.js";
13
+ import fs from "fs/promises";
14
+ import { asyncMapFilterUndefined } from "./util.js";
15
+
16
+ async function loadCache({ cache_file }: CacheConfiguration): Promise<Cache> {
17
+ try {
18
+ await fs.access(cache_file);
19
+ const cacheFile = await fs.readFile(cache_file);
20
+ return CacheSchema.parse(JSON.parse(cacheFile.toString()));
21
+ } catch (e) {
22
+ return {};
23
+ }
24
+ }
25
+
26
+ async function saveCache({ cache_file }: CacheConfiguration, cache: Cache) {
27
+ await fs.mkdir(cache_file.split("/").slice(0, -1).join("/"), { recursive: true });
28
+ await fs.writeFile(cache_file, JSON.stringify(cache));
29
+ }
30
+
31
+ function toCacheEntry(result: ResultEntry, now: Date): [string, CacheEntry] {
32
+ return [result.source.url, { timestamp: now, data: result }];
33
+ }
34
+
35
+ function toCache(results: ResultEntry[], now: Date): Cache {
36
+ return R.pipe(
37
+ results,
38
+ R.map((result) => toCacheEntry(result, now)),
39
+ R.fromEntries(),
40
+ );
41
+ }
42
+
43
+ function updateCache(results: ResultEntry[], config: CachedConfiguration): Promise<void> {
44
+ const now = new Date();
45
+ const updatedCache = toCache(results, now);
46
+ return saveCache(config.cache, updatedCache);
47
+ }
48
+
49
+ export async function fetchAllCached(config: CachedConfiguration): Promise<Result> {
50
+ const cache = await loadCache(config.cache);
51
+
52
+ const results = await asyncMapFilterUndefined(config.sources, (source) => fetchWithCache(source, cache, config));
53
+
54
+ await updateCache(results, config);
55
+
56
+ return results;
57
+ }
58
+
59
+ export async function fetchWithCache(
60
+ source: Source,
61
+ cache: Cache,
62
+ config: CachedConfiguration,
63
+ ): Promise<ResultEntry | undefined> {
64
+ const cacheEntry = cache[source.url];
65
+ if (cacheEntry) {
66
+ const now = new Date();
67
+ if (now.getTime() - cacheEntry.timestamp.getTime() < config.cache.cache_duration_minutes * 60 * 1000) {
68
+ console.log(`Cache entry found for ${source.url}.`);
69
+ return Promise.resolve(cacheEntry.data);
70
+ } else {
71
+ console.log(`Cache entry for ${source.url} is too old.`);
72
+ }
73
+ } else {
74
+ console.log(`No cache entry for ${source.url}.`);
75
+ }
76
+
77
+ console.log(`Fetching ${source.url}`);
78
+ return fetch(source, config.truncate);
79
+ }
package/src/fetch.ts ADDED
@@ -0,0 +1,53 @@
1
+ import Parser from "rss-parser";
2
+ import sanitizeHtml from "sanitize-html";
3
+ import truncate from "truncate-html";
4
+ import { type Source, type ResultEntry, FeedEntrySchema, type Configuration } from "./types.js";
5
+ import * as R from "remeda";
6
+ import { asyncMapFilterUndefined } from "./util.js";
7
+
8
+ // for some reason, TypeScript does not infer the type of the default export correctly
9
+ const truncateFn: typeof truncate.default = truncate as unknown as typeof truncate.default;
10
+
11
+ export async function fetchAll(config: Configuration) {
12
+ return await asyncMapFilterUndefined(config.sources, (source) => fetch(source, config.truncate));
13
+ }
14
+
15
+ export async function fetch(source: Source, length: number): Promise<ResultEntry | undefined> {
16
+ const parser = new Parser();
17
+ try {
18
+ const feed = await parser.parseURL(source.url);
19
+
20
+ const firstItem = R.pipe(
21
+ feed.items,
22
+ R.map((item) => FeedEntrySchema.parse(item)),
23
+ R.sortBy((item) => new Date(item.date).getTime()),
24
+ R.reverse(),
25
+ R.first(),
26
+ );
27
+
28
+ if (!firstItem) {
29
+ throw new Error("no items found in feed");
30
+ }
31
+
32
+ const preview =
33
+ firstItem.contentSnippet ?? firstItem.content ?? firstItem.description ?? firstItem["content:encoded"];
34
+
35
+ return {
36
+ title: firstItem.title,
37
+ url: firstItem.link,
38
+ date: new Date(firstItem.date),
39
+ source,
40
+ preview: preview
41
+ ? truncateFn(
42
+ sanitizeHtml(preview, {
43
+ parseStyleAttributes: false,
44
+ }),
45
+ length,
46
+ )
47
+ : undefined,
48
+ };
49
+ } catch (e) {
50
+ console.error(`Error fetching ${source.url}: ${e as string}`);
51
+ return undefined;
52
+ }
53
+ }
@@ -0,0 +1,114 @@
1
+ import { expect, test } from "vitest";
2
+ import type { Configuration } from "./types.js";
3
+ import { run } from "./index.js";
4
+ import { tmpdir } from "os";
5
+ import { mkdtemp } from "fs/promises";
6
+ import { join } from "path";
7
+
8
+ // TODO: intercept network requests
9
+ test("it should fetch an RSS feed without caching", async () => {
10
+ const config: Configuration = {
11
+ sources: [
12
+ {
13
+ title: "Jerred Shepherd",
14
+ url: "https://sjer.red/rss.xml",
15
+ },
16
+ ],
17
+ number: 1,
18
+ truncate: 300,
19
+ };
20
+
21
+ const result = await run(config);
22
+ expect(result).toMatchSnapshot();
23
+ });
24
+
25
+ test("it should fetch several RSS feeds", async () => {
26
+ const config: Configuration = {
27
+ sources: [
28
+ {
29
+ url: "https://drewdevault.com/blog/index.xml",
30
+ title: "Drew DeVault",
31
+ },
32
+ {
33
+ url: "https://danluu.com/atom.xml",
34
+ title: "Dan Luu",
35
+ },
36
+ {
37
+ url: "https://jakelazaroff.com/rss.xml",
38
+ title: "Jake Lazaroff",
39
+ },
40
+ {
41
+ url: "https://awesomekling.github.io/feed.xml",
42
+ title: "Andreas Kling",
43
+ },
44
+ {
45
+ url: "https://xeiaso.net/blog.rss",
46
+ title: "Xe Iaso",
47
+ },
48
+ {
49
+ url: "https://ciechanow.ski/atom.xml",
50
+ title: "Bartosz Ciechanowski",
51
+ },
52
+ {
53
+ url: "https://explained-from-first-principles.com/feed.xml",
54
+ title: "Explained From First Principles",
55
+ },
56
+ {
57
+ url: "http://www.aaronsw.com/2002/feeds/pgessays.rss",
58
+ title: "Paul Graham",
59
+ },
60
+ {
61
+ url: "https://samwho.dev/rss.xml",
62
+ title: "Sam Rose",
63
+ },
64
+ {
65
+ url: "https://rachelbythebay.com/w/atom.xml",
66
+ title: "Rachel Kroll",
67
+ },
68
+ {
69
+ url: "https://brr.fyi/feed.xml",
70
+ title: "brr.fyi",
71
+ },
72
+ {
73
+ url: "https://devblogs.microsoft.com/oldnewthing/feed",
74
+ title: "The Old New Thing",
75
+ },
76
+ {
77
+ url: "https://ludic.mataroa.blog/rss/",
78
+ title: "Ludicity",
79
+ },
80
+ ],
81
+ number: 20,
82
+ truncate: 300,
83
+ };
84
+
85
+ const result = await run(config);
86
+ expect(result).toMatchSnapshot();
87
+ });
88
+
89
+ test("it should fetch an RSS feed with caching", async () => {
90
+ const config: Configuration = {
91
+ sources: [
92
+ {
93
+ title: "Jerred Shepherd",
94
+ url: "https://sjer.red/rss.xml",
95
+ },
96
+ ],
97
+ number: 1,
98
+ truncate: 300,
99
+ cache: {
100
+ cache_file: `${await createTempDir()}/cache.json`,
101
+ cache_duration_minutes: 1,
102
+ },
103
+ };
104
+
105
+ const result = await run(config);
106
+ expect(result).toMatchSnapshot();
107
+ });
108
+
109
+ // https://sdorra.dev/posts/2024-02-12-vitest-tmpdir
110
+ async function createTempDir() {
111
+ const ostmpdir = tmpdir();
112
+ const dir = join(ostmpdir, "unit-test-");
113
+ return await mkdtemp(dir);
114
+ }
package/src/index.ts ADDED
@@ -0,0 +1,28 @@
1
+ import * as R from "remeda";
2
+ import { fetchAllCached as fetchAllCached } from "./cache.js";
3
+ import { fetchAll as fetchAllUncached } from "./fetch.js";
4
+ import { type Configuration, type Result, CachedConfigurationSchema } from "./types.js";
5
+
6
+ export async function run(config: Configuration): Promise<Result> {
7
+ const { success, data } = CachedConfigurationSchema.safeParse(config);
8
+
9
+ let result: Result;
10
+ if (success) {
11
+ console.log(`Using cache at ${data.cache.cache_file}.`);
12
+ result = await fetchAllCached(data);
13
+ } else {
14
+ console.log("Cache disabled.");
15
+ result = await fetchAllUncached(config);
16
+ }
17
+
18
+ const topResults = R.pipe(
19
+ result,
20
+ R.sortBy((result) => result.date.getTime()),
21
+ R.reverse(),
22
+ R.take(config.number),
23
+ );
24
+
25
+ return topResults;
26
+ }
27
+
28
+ export * from "./types.js";
package/src/types.ts ADDED
@@ -0,0 +1,81 @@
1
+ import { z } from "zod";
2
+
3
+ export type Source = z.infer<typeof SourceSchema>;
4
+ const SourceSchema = z.object({
5
+ // the url of the feed
6
+ url: z.string(),
7
+ // a title for the feed
8
+ title: z.string(),
9
+ });
10
+
11
+ export type CacheConfiguration = z.infer<typeof CacheConfigurationSchema>;
12
+ const CacheConfigurationSchema = z.object({
13
+ // how long to cache a results for
14
+ cache_duration_minutes: z.number().default(60),
15
+ cache_file: z.string().default("cache.json"),
16
+ });
17
+
18
+ export type Configuration = z.infer<typeof ConfigurationSchema>;
19
+ const ConfigurationSchema = z.object({
20
+ // list of sources to fetch
21
+ sources: SourceSchema.array(),
22
+ // how many entries to return
23
+ number: z.number().default(3),
24
+ // how many words to truncate the preview to
25
+ truncate: z.number().default(300),
26
+ cache: CacheConfigurationSchema.optional(),
27
+ });
28
+
29
+ // CachedConfiguration is the same as Configuration but cache is not optional
30
+ export type CachedConfiguration = z.infer<typeof CachedConfigurationSchema>;
31
+ export const CachedConfigurationSchema = ConfigurationSchema.extend({
32
+ cache: CacheConfigurationSchema,
33
+ });
34
+
35
+ export type ResultEntry = z.infer<typeof ResultEntrySchema>;
36
+ const ResultEntrySchema = z.object({
37
+ title: z.string(),
38
+ url: z.string(),
39
+ date: z.coerce.date(),
40
+ source: SourceSchema,
41
+ preview: z.string().optional(),
42
+ });
43
+
44
+ export type Result = z.infer<typeof ResultSchema>;
45
+ const ResultSchema = z.array(ResultEntrySchema);
46
+
47
+ export type CacheEntry = z.infer<typeof CacheEntrySchema>;
48
+ export const CacheEntrySchema = z.object({
49
+ timestamp: z.coerce.date(),
50
+ data: ResultEntrySchema,
51
+ });
52
+
53
+ export type Cache = z.infer<typeof CacheSchema>;
54
+ export const CacheSchema = z.record(CacheEntrySchema);
55
+
56
+ export const FeedEntrySchema = z
57
+ .object({
58
+ title: z.string(),
59
+ link: z.string(),
60
+ isoDate: z.coerce.date().optional(),
61
+ pubDate: z.coerce.date().optional(),
62
+ content: z.string().optional(),
63
+ contentSnippet: z.string().optional(),
64
+ "content:encoded": z.string().optional(),
65
+ description: z.string().optional(),
66
+ })
67
+ .transform((entry) => {
68
+ const date = entry.isoDate ?? entry.pubDate;
69
+ if (!date) {
70
+ throw new Error("no date found in feed entry");
71
+ }
72
+ return {
73
+ title: entry.title,
74
+ link: entry.link,
75
+ date,
76
+ content: entry.content,
77
+ contentSnippet: entry.contentSnippet,
78
+ description: entry.description,
79
+ "content:encoded": entry["content:encoded"],
80
+ };
81
+ });
package/src/util.ts ADDED
@@ -0,0 +1,23 @@
1
+ import * as R from "remeda";
2
+
3
+ // run an async map operation, filtering out undefined results
4
+ export async function asyncMapFilterUndefined<T, U>(input: T[], fn: (x: T) => Promise<U | undefined>): Promise<U[]> {
5
+ const results = await asyncMap(input, fn);
6
+ return filterUndefined(results);
7
+ }
8
+
9
+ export async function asyncMap<T, U>(input: T[], fn: (x: T) => Promise<U>): Promise<U[]> {
10
+ const promises = R.pipe(
11
+ input,
12
+ R.map((item) => fn(item)),
13
+ );
14
+
15
+ return Promise.all(promises);
16
+ }
17
+
18
+ export function filterUndefined<T>(input: (T | undefined)[]): T[] {
19
+ return R.pipe(
20
+ input,
21
+ R.filter((result) => result !== undefined),
22
+ ) as T[];
23
+ }