@@ -8,6 +8,11 @@ import {
8
8
InvalidPollingIntervalError ,
9
9
UnsupportedFeedFormatError ,
10
10
} from "./errors"
11
+ import {
12
+ detectSmartPollingInterval ,
13
+ getContentHash ,
14
+ parseFeedDate ,
15
+ } from "./utils"
11
16
import type {
12
17
CollectionConfig ,
13
18
DeleteMutationFnParams ,
@@ -17,66 +22,10 @@ import type {
17
22
UtilsRecord ,
18
23
} from "@tanstack/db"
19
24
import type { StandardSchemaV1 } from "@standard-schema/spec"
25
+ import type { AtomItem , FeedItem , HTTPOptions , RSSItem } from "./types"
20
26
21
27
const debug = DebugModule . debug ( `ts/db:rss` )
22
28
23
- /**
24
- * Types for RSS feed items
25
- */
26
- export interface RSSItem {
27
- title ?: string
28
- description ?: string
29
- link ?: string
30
- guid ?: string
31
- pubDate ?: string | Date
32
- author ?: string
33
- category ?: string | Array < string >
34
- enclosure ?: {
35
- url : string
36
- type ?: string
37
- length ?: string
38
- }
39
- [ key : string ] : any
40
- }
41
-
42
- /**
43
- * Types for Atom feed items
44
- */
45
- export interface AtomItem {
46
- title ?: string | { $text ?: string ; type ?: string }
47
- summary ?: string | { $text ?: string ; type ?: string }
48
- content ?: string | { $text ?: string ; type ?: string }
49
- link ?:
50
- | string
51
- | { href ?: string ; rel ?: string ; type ?: string }
52
- | Array < { href ?: string ; rel ?: string ; type ?: string } >
53
- id ?: string
54
- updated ?: string | Date
55
- published ?: string | Date
56
- author ?: string | { name ?: string ; email ?: string ; uri ?: string }
57
- category ?:
58
- | string
59
- | { term ?: string ; label ?: string }
60
- | Array < { term ?: string ; label ?: string } >
61
- [ key : string ] : any
62
- }
63
-
64
- export type FeedItem = RSSItem | AtomItem
65
-
66
- /**
67
- * Feed type detection
68
- */
69
- export type FeedType = `rss` | `atom` | `auto`
70
-
71
- /**
72
- * HTTP options for fetching feeds
73
- */
74
- export interface HTTPOptions {
75
- timeout ?: number
76
- headers ?: Record < string , string >
77
- userAgent ?: string
78
- }
79
-
80
29
/**
81
30
* Base configuration interface for feed collection options
82
31
*/
@@ -305,7 +254,7 @@ function parseFeed(xmlContent: string, parserOptions: any = {}): ParsedFeed {
305
254
function defaultRSSTransform ( item : RSSItem ) : RSSItem {
306
255
return {
307
256
...item ,
308
- pubDate : item . pubDate ? new Date ( item . pubDate ) : undefined ,
257
+ pubDate : item . pubDate ? parseFeedDate ( item . pubDate ) : undefined ,
309
258
}
310
259
}
311
260
@@ -340,10 +289,10 @@ function defaultAtomTransform(item: AtomItem): AtomItem {
340
289
341
290
// Handle dates
342
291
if ( item . updated ) {
343
- normalized . updated = new Date ( item . updated )
292
+ normalized . updated = parseFeedDate ( item . updated )
344
293
}
345
294
if ( item . published ) {
346
- normalized . published = new Date ( item . published )
295
+ normalized . published = parseFeedDate ( item . published )
347
296
}
348
297
349
298
// Handle author
@@ -447,7 +396,7 @@ function createFeedCollectionOptions<
447
396
) {
448
397
const {
449
398
feedUrl,
450
- pollingInterval = 300000 , // 5 minutes default
399
+ pollingInterval : userPollingInterval ,
451
400
httpOptions = { } ,
452
401
startPolling = true ,
453
402
maxSeenItems = 1000 ,
@@ -461,6 +410,10 @@ function createFeedCollectionOptions<
461
410
...restConfig
462
411
} = config
463
412
413
+ // Smart polling interval detection
414
+ let pollingInterval =
415
+ userPollingInterval !== undefined ? userPollingInterval : 300000 // Default 5 minutes
416
+
464
417
// Validation
465
418
if ( ! feedUrl ) {
466
419
throw new FeedURLRequiredError ( )
@@ -470,7 +423,10 @@ function createFeedCollectionOptions<
470
423
}
471
424
472
425
// State management
473
- let seenItems = new Map < string , { id : string ; lastSeen : number } > ( )
426
+ let seenItems = new Map <
427
+ string ,
428
+ { id : string ; lastSeen : number ; contentHash : string }
429
+ > ( )
474
430
let syncParams :
475
431
| Parameters <
476
432
SyncConfig < ResolveType < TExplicit , TSchema , TFallback > , TKey > [ `sync`]
@@ -544,10 +500,22 @@ function createFeedCollectionOptions<
544
500
throw new UnsupportedFeedFormatError ( feedUrl )
545
501
}
546
502
503
+ // Detect smart polling interval on first fetch
504
+ if ( ! userPollingInterval ) {
505
+ const parser = new XMLParser ( parserOptions )
506
+ const feedData = parser . parse ( xmlContent )
507
+ const smartInterval = detectSmartPollingInterval ( feedData )
508
+ if ( smartInterval !== pollingInterval ) {
509
+ pollingInterval = smartInterval
510
+ debug ( `Updated polling interval to ${ pollingInterval } ms` )
511
+ }
512
+ }
513
+
547
514
const { begin, write, commit } = params
548
515
begin ( )
549
516
550
517
let newItemsCount = 0
518
+ let updatedItemsCount = 0
551
519
const currentTime = Date . now ( )
552
520
553
521
for ( const rawItem of parsedFeed . items ) {
@@ -572,22 +540,41 @@ function createFeedCollectionOptions<
572
540
573
541
// Generate unique ID for deduplication
574
542
const itemId = getItemId ( rawItem , parsedFeed . type )
543
+ const contentHash = getContentHash ( rawItem )
575
544
576
545
// Check if we've seen this item before
577
546
const seen = seenItems . get ( itemId )
578
547
579
548
if ( ! seen ) {
580
549
// New item
581
- seenItems . set ( itemId , { id : itemId , lastSeen : currentTime } )
550
+ seenItems . set ( itemId , {
551
+ id : itemId ,
552
+ lastSeen : currentTime ,
553
+ contentHash,
554
+ } )
582
555
583
556
write ( {
584
557
type : `insert` ,
585
558
value : transformedItem ,
586
559
} )
587
560
588
561
newItemsCount ++
562
+ } else if ( seen . contentHash !== contentHash ) {
563
+ // Item exists but content has changed - treat as update
564
+ seenItems . set ( itemId , {
565
+ ...seen ,
566
+ lastSeen : currentTime ,
567
+ contentHash,
568
+ } )
569
+
570
+ write ( {
571
+ type : `update` ,
572
+ value : transformedItem ,
573
+ } )
574
+
575
+ updatedItemsCount ++
589
576
} else {
590
- // Update last seen time
577
+ // Item exists and content hasn't changed - just update last seen time
591
578
seenItems . set ( itemId , { ...seen , lastSeen : currentTime } )
592
579
}
593
580
}
@@ -597,6 +584,9 @@ function createFeedCollectionOptions<
597
584
if ( newItemsCount > 0 ) {
598
585
debug ( `Added ${ newItemsCount } new items from feed` )
599
586
}
587
+ if ( updatedItemsCount > 0 ) {
588
+ debug ( `Updated ${ updatedItemsCount } existing items from feed` )
589
+ }
600
590
601
591
// Clean up old items periodically
602
592
cleanupSeenItems ( )
@@ -694,6 +684,7 @@ function createFeedCollectionOptions<
694
684
getKey,
695
685
sync,
696
686
startSync : true ,
687
+ rowUpdateMode : `full` ,
697
688
onInsert,
698
689
onUpdate,
699
690
onDelete,
0 commit comments