Skip to content

Commit b48f786

Browse files
committed
improve docu
1 parent 9c74129 commit b48f786

File tree

2 files changed

+114
-27
lines changed

2 files changed

+114
-27
lines changed

lib/FredyRuntime.js

Lines changed: 113 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,54 @@ import Extractor from './services/extractor/extractor.js';
55
import urlModifier from './services/queryStringMutator.js';
66
import 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+
*/
838
class 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}').`);

lib/services/storage/migrations/sql/5.job-sharing.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Migration: Adding a new table to store if somebody "watches" (a.k.a favorite) a listing
1+
// Migration: Adding a new table to store if somebody shared a job with someone
22

33
export function up(db) {
44
db.exec(`

0 commit comments

Comments
 (0)