Skip to content

Commit e56c792

Browse files
Add RSS/Atom feed collection package for TanStack DB
Co-authored-by: sam.willis <[email protected]>
1 parent 233a60e commit e56c792

File tree

11 files changed

+2939
-0
lines changed

11 files changed

+2939
-0
lines changed

packages/rss-db-collection/README.md

Lines changed: 442 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
{
2+
"name": "@tanstack/rss-db-collection",
3+
"description": "RSS/Atom feed collection for TanStack DB",
4+
"version": "0.1.0",
5+
"dependencies": {
6+
"@standard-schema/spec": "^1.0.0",
7+
"@tanstack/db": "workspace:*",
8+
"debug": "^4.4.1",
9+
"fast-xml-parser": "^4.5.0"
10+
},
11+
"devDependencies": {
12+
"@types/debug": "^4.1.12",
13+
"@vitest/coverage-istanbul": "^3.0.9"
14+
},
15+
"exports": {
16+
".": {
17+
"import": {
18+
"types": "./dist/esm/index.d.ts",
19+
"default": "./dist/esm/index.js"
20+
},
21+
"require": {
22+
"types": "./dist/cjs/index.d.cts",
23+
"default": "./dist/cjs/index.cjs"
24+
}
25+
},
26+
"./package.json": "./package.json"
27+
},
28+
"files": [
29+
"dist",
30+
"src"
31+
],
32+
"main": "dist/cjs/index.cjs",
33+
"module": "dist/esm/index.js",
34+
"packageManager": "[email protected]",
35+
"peerDependencies": {
36+
"typescript": ">=4.7"
37+
},
38+
"author": "Claude AI",
39+
"license": "MIT",
40+
"repository": {
41+
"type": "git",
42+
"url": "https://github.com/TanStack/db.git",
43+
"directory": "packages/rss-db-collection"
44+
},
45+
"homepage": "https://tanstack.com/db",
46+
"keywords": [
47+
"rss",
48+
"atom",
49+
"feed",
50+
"polling",
51+
"typescript"
52+
],
53+
"scripts": {
54+
"build": "vite build",
55+
"dev": "vite build --watch",
56+
"lint": "eslint . --fix",
57+
"test": "npx vitest --run"
58+
},
59+
"sideEffects": false,
60+
"type": "module",
61+
"types": "dist/esm/index.d.ts"
62+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* Base error class for RSS Collection errors
3+
*/
4+
export abstract class RSSCollectionError extends Error {
5+
constructor(message: string) {
6+
super(message)
7+
this.name = this.constructor.name
8+
}
9+
}
10+
11+
/**
12+
* Error thrown when feed URL is required but not provided
13+
*/
14+
export class FeedURLRequiredError extends RSSCollectionError {
15+
constructor() {
16+
super(`Feed URL is required for RSS collection`)
17+
}
18+
}
19+
20+
/**
21+
* Error thrown when polling interval is invalid
22+
*/
23+
export class InvalidPollingIntervalError extends RSSCollectionError {
24+
constructor(interval: number) {
25+
super(
26+
`Invalid polling interval: ${interval}. Must be a positive number in milliseconds.`
27+
)
28+
}
29+
}
30+
31+
/**
32+
* Error thrown when feed parsing fails
33+
*/
34+
export class FeedParsingError extends RSSCollectionError {
35+
constructor(url: string, originalError: Error) {
36+
super(`Failed to parse feed from ${url}: ${originalError.message}`)
37+
this.cause = originalError
38+
}
39+
}
40+
41+
/**
42+
* Error thrown when feed fetch fails
43+
*/
44+
export class FeedFetchError extends RSSCollectionError {
45+
constructor(url: string, status?: number) {
46+
super(
47+
status
48+
? `Failed to fetch feed from ${url}: HTTP ${status}`
49+
: `Failed to fetch feed from ${url}`
50+
)
51+
}
52+
}
53+
54+
/**
55+
* Error thrown when timeout occurs while fetching feed
56+
*/
57+
export class FeedTimeoutError extends RSSCollectionError {
58+
constructor(url: string, timeout: number) {
59+
super(`Timeout after ${timeout}ms while fetching feed from ${url}`)
60+
}
61+
}
62+
63+
/**
64+
* Error thrown when feed format is not supported
65+
*/
66+
export class UnsupportedFeedFormatError extends RSSCollectionError {
67+
constructor(url: string) {
68+
super(
69+
`Unsupported feed format from ${url}. Only RSS and Atom feeds are supported.`
70+
)
71+
}
72+
}
73+
74+
/**
75+
* Error thrown when required getKey function is not provided
76+
*/
77+
export class GetKeyRequiredError extends RSSCollectionError {
78+
constructor() {
79+
super(`getKey function is required for RSS collection`)
80+
}
81+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/**
2+
* RSS/Atom Feed Collection for TanStack DB
3+
*
4+
* This package provides RSS and Atom feed collection capabilities with:
5+
* - Automatic feed type detection (RSS/Atom)
6+
* - Configurable polling intervals
7+
* - Built-in deduplication
8+
* - Custom transform functions
9+
* - Full TypeScript support
10+
*
11+
* @example RSS Collection
12+
* ```typescript
13+
* import { createCollection } from '@tanstack/db'
14+
* import { rssCollectionOptions } from '@tanstack/rss-db-collection'
15+
*
16+
* interface BlogPost {
17+
* id: string
18+
* title: string
19+
* description: string
20+
* link: string
21+
* publishedAt: Date
22+
* }
23+
*
24+
* const blogFeed = createCollection({
25+
* ...rssCollectionOptions<BlogPost>({
26+
* feedUrl: 'https://blog.example.com/rss.xml',
27+
* pollingInterval: 5 * 60 * 1000, // 5 minutes
28+
* getKey: (item) => item.id,
29+
* transform: (item) => ({
30+
* id: item.guid || item.link || '',
31+
* title: item.title || '',
32+
* description: item.description || '',
33+
* link: item.link || '',
34+
* publishedAt: new Date(item.pubDate || Date.now())
35+
* })
36+
* })
37+
* })
38+
* ```
39+
*
40+
* @example Atom Collection
41+
* ```typescript
42+
* import { createCollection } from '@tanstack/db'
43+
* import { atomCollectionOptions } from '@tanstack/rss-db-collection'
44+
*
45+
* const atomFeed = createCollection({
46+
* ...atomCollectionOptions<BlogPost>({
47+
* feedUrl: 'https://blog.example.com/atom.xml',
48+
* pollingInterval: 5 * 60 * 1000, // 5 minutes
49+
* getKey: (item) => item.id,
50+
* transform: (item) => ({
51+
* id: item.id || '',
52+
* title: typeof item.title === 'string' ? item.title : item.title?.$text || '',
53+
* description: typeof item.summary === 'string' ? item.summary : item.summary?.$text || '',
54+
* link: typeof item.link === 'string' ? item.link : item.link?.href || '',
55+
* publishedAt: new Date(item.published || item.updated || Date.now())
56+
* })
57+
* })
58+
* })
59+
* ```
60+
*/
61+
62+
// RSS collection functionality
63+
export {
64+
rssCollectionOptions,
65+
type RSSCollectionConfig,
66+
type RSSItem,
67+
} from "./rss"
68+
69+
// Atom collection functionality
70+
export {
71+
atomCollectionOptions,
72+
type AtomCollectionConfig,
73+
type AtomItem,
74+
} from "./rss"
75+
76+
// Shared types and utilities
77+
export {
78+
type FeedItem,
79+
type FeedType,
80+
type HTTPOptions,
81+
type FeedCollectionUtils,
82+
} from "./rss"
83+
84+
// Error types
85+
export {
86+
RSSCollectionError,
87+
FeedURLRequiredError,
88+
InvalidPollingIntervalError,
89+
FeedParsingError,
90+
FeedFetchError,
91+
FeedTimeoutError,
92+
UnsupportedFeedFormatError,
93+
GetKeyRequiredError,
94+
} from "./errors"

0 commit comments

Comments
 (0)