@@ -5,14 +5,54 @@ import Extractor from './services/extractor/extractor.js';
55import urlModifier from './services/queryStringMutator.js' ;
66import logger from './services/logger.js' ;
77
8+ /**
9+ * @typedef {Object } Listing
10+ * @property {string } id Stable unique identifier (hash) of the listing.
11+ * @property {string } title Title or headline of the listing.
12+ * @property {string } [address] Optional address/location text.
13+ * @property {string } [price] Optional price text/value.
14+ * @property {string } [url] Link to the listing detail page.
15+ * @property {any } [meta] Provider-specific additional metadata.
16+ */
17+
18+ /**
19+ * @typedef {Object } SimilarityCache
20+ * @property {(title:string, address?:string)=>boolean } hasSimilarEntries Returns true if a similar entry is known.
21+ * @property {(title:string, address?:string)=>void } addCacheEntry Adds a new entry to the similarity cache.
22+ */
23+
24+ /**
25+ * Runtime orchestrator for fetching, normalizing, filtering, deduplicating, storing,
26+ * and notifying about new listings from a configured provider.
27+ *
28+ * The execution flow is:
29+ * 1) Prepare provider URL (sorting, etc.)
30+ * 2) Extract raw listings from the provider
31+ * 3) Normalize listings to the provider schema
32+ * 4) Filter out incomplete/blacklisted listings
33+ * 5) Identify new listings (vs. previously stored hashes)
34+ * 6) Persist new listings
35+ * 7) Filter out entries similar to already seen ones
36+ * 8) Dispatch notifications
37+ */
838class FredyRuntime {
939 /**
40+ * Create a new runtime instance for a single provider/job execution.
1041 *
11- * @param providerConfig the config for the specific provider, we're going to query at the moment
12- * @param notificationConfig the config for all notifications
13- * @param providerId the id of the provider currently in use
14- * @param jobKey key of the job that is currently running (from within the config)
15- * @param similarityCache cache instance holding values to check for similarity of entries
42+ * @param {Object } providerConfig Provider configuration.
43+ * @param {string } providerConfig.url Base URL to crawl.
44+ * @param {string } [providerConfig.sortByDateParam] Query parameter used to enforce sorting by date (provider-specific).
45+ * @param {string } [providerConfig.waitForSelector] CSS selector to wait for before parsing content.
46+ * @param {Object.<string, string> } providerConfig.crawlFields Mapping of field names to selectors/paths to extract.
47+ * @param {string } providerConfig.crawlContainer CSS selector for the container holding listing items.
48+ * @param {(raw:any)=>Listing } providerConfig.normalize Function to convert raw scraped data into a Listing shape.
49+ * @param {(listing:Listing)=>boolean } providerConfig.filter Function to filter out unwanted listings.
50+ * @param {(url:string, waitForSelector?:string)=>Promise<void>|Promise<Listing[]> } [providerConfig.getListings] Optional override to fetch listings.
51+ *
52+ * @param {Object } notificationConfig Notification configuration passed to notification adapters.
53+ * @param {string } providerId The ID of the provider currently in use.
54+ * @param {string } jobKey Key of the job that is currently running (from within the config).
55+ * @param {SimilarityCache } similarityCache Cache instance for checking similar entries.
1656 */
1757 constructor ( providerConfig , notificationConfig , providerId , jobKey , similarityCache ) {
1858 this . _providerConfig = providerConfig ;
@@ -22,29 +62,31 @@ class FredyRuntime {
2262 this . _similarityCache = similarityCache ;
2363 }
2464
65+ /**
66+ * Execute the end-to-end pipeline for a single provider run.
67+ *
68+ * @returns {Promise<Listing[]|void> } Resolves to the list of new (and similarity-filtered) listings
69+ * after notifications have been sent; resolves to void when there are no new listings.
70+ */
2571 execute ( ) {
26- return (
27- //modify the url to make sure search order is correctly set
28- Promise . resolve ( urlModifier ( this . _providerConfig . url , this . _providerConfig . sortByDateParam ) )
29- //scraping the site and try finding new listings
30- . then ( this . _providerConfig . getListings ?. bind ( this ) ?? this . _getListings . bind ( this ) )
31- //bring them in a proper form (dictated by the provider)
32- . then ( this . _normalize . bind ( this ) )
33- //filter listings with stuff tagged by the blacklist of the provider
34- . then ( this . _filter . bind ( this ) )
35- //check if new listings available. if so proceed
36- . then ( this . _findNew . bind ( this ) )
37- //store everything in db
38- . then ( this . _save . bind ( this ) )
39- //check for similar listings. if found, remove them before notifying
40- . then ( this . _filterBySimilarListings . bind ( this ) )
41- //notify the user using the configured notification adapter
42- . then ( this . _notify . bind ( this ) )
43- //if an error occurred on the way, handle it here.
44- . catch ( this . _handleError . bind ( this ) )
45- ) ;
72+ return Promise . resolve ( urlModifier ( this . _providerConfig . url , this . _providerConfig . sortByDateParam ) )
73+ . then ( this . _providerConfig . getListings ?. bind ( this ) ?? this . _getListings . bind ( this ) )
74+ . then ( this . _normalize . bind ( this ) )
75+ . then ( this . _filter . bind ( this ) )
76+ . then ( this . _findNew . bind ( this ) )
77+ . then ( this . _save . bind ( this ) )
78+ . then ( this . _filterBySimilarListings . bind ( this ) )
79+ . then ( this . _notify . bind ( this ) )
80+ . catch ( this . _handleError . bind ( this ) ) ;
4681 }
4782
83+ /**
84+ * Fetch listings from the provider, using the default Extractor flow unless
85+ * a provider-specific getListings override is supplied.
86+ *
87+ * @param {string } url The provider URL to fetch from.
88+ * @returns {Promise<Listing[]> } Resolves with an array of listings (empty when none found).
89+ */
4890 _getListings ( url ) {
4991 const extractor = new Extractor ( ) ;
5092 return new Promise ( ( resolve , reject ) => {
@@ -65,17 +107,36 @@ class FredyRuntime {
65107 } ) ;
66108 }
67109
110+ /**
111+ * Normalize raw listings into the provider-specific Listing shape.
112+ *
113+ * @param {any[] } listings Raw listing entries from the extractor or override.
114+ * @returns {Listing[] } Normalized listings.
115+ */
68116 _normalize ( listings ) {
69117 return listings . map ( this . _providerConfig . normalize ) ;
70118 }
71119
120+ /**
121+ * Filter out listings that are missing required fields and those rejected by the
122+ * provider's blacklist/filter function.
123+ *
124+ * @param {Listing[] } listings Listings to filter.
125+ * @returns {Listing[] } Filtered listings that pass validation and provider filter.
126+ */
72127 _filter ( listings ) {
73- //only return those where all the fields have been found
74128 const keys = Object . keys ( this . _providerConfig . crawlFields ) ;
75129 const filteredListings = listings . filter ( ( item ) => keys . every ( ( key ) => key in item ) ) ;
76130 return filteredListings . filter ( this . _providerConfig . filter ) ;
77131 }
78132
133+ /**
134+ * Determine which listings are new by comparing their IDs against stored hashes.
135+ *
136+ * @param {Listing[] } listings Listings to evaluate for novelty.
137+ * @returns {Listing[] } New listings not seen before.
138+ * @throws {NoNewListingsWarning } When no new listings are found.
139+ */
79140 _findNew ( listings ) {
80141 logger . debug ( `Checking ${ listings . length } listings for new entries (Provider: '${ this . _providerId } ')` ) ;
81142 const hashes = getKnownListingHashesForJobAndProvider ( this . _jobKey , this . _providerId ) || [ ] ;
@@ -87,6 +148,13 @@ class FredyRuntime {
87148 return newListings ;
88149 }
89150
151+ /**
152+ * Send notifications for new listings using the configured notification adapter(s).
153+ *
154+ * @param {Listing[] } newListings New listings to notify about.
155+ * @returns {Promise<Listing[]> } Resolves to the provided listings after notifications complete.
156+ * @throws {NoNewListingsWarning } When there are no listings to notify about.
157+ */
90158 _notify ( newListings ) {
91159 if ( newListings . length === 0 ) {
92160 throw new NoNewListingsWarning ( ) ;
@@ -95,12 +163,25 @@ class FredyRuntime {
95163 return Promise . all ( sendNotifications ) . then ( ( ) => newListings ) ;
96164 }
97165
166+ /**
167+ * Persist new listings and pass them through.
168+ *
169+ * @param {Listing[] } newListings Listings to store.
170+ * @returns {Listing[] } The same listings, unchanged.
171+ */
98172 _save ( newListings ) {
99173 logger . debug ( `Storing ${ newListings . length } new listings (Provider: '${ this . _providerId } ')` ) ;
100174 storeListings ( this . _jobKey , this . _providerId , newListings ) ;
101175 return newListings ;
102176 }
103177
178+ /**
179+ * Remove listings that are similar to already known entries according to the similarity cache.
180+ * Adds the remaining listings to the cache.
181+ *
182+ * @param {Listing[] } listings Listings to filter by similarity.
183+ * @returns {Listing[] } Listings considered unique enough to keep.
184+ */
104185 _filterBySimilarListings ( listings ) {
105186 const filteredList = listings . filter ( ( listing ) => {
106187 const similar = this . _similarityCache . hasSimilarEntries ( listing . title , listing . address ) ;
@@ -115,6 +196,12 @@ class FredyRuntime {
115196 return filteredList ;
116197 }
117198
199+ /**
200+ * Handle errors occurring in the pipeline, logging levels depending on type.
201+ *
202+ * @param {Error } err Error instance thrown by previous steps.
203+ * @returns {void }
204+ */
118205 _handleError ( err ) {
119206 if ( err . name === 'NoNewListingsWarning' ) {
120207 logger . debug ( `No new listings found (Provider: '${ this . _providerId } ').` ) ;
0 commit comments