openings 0.1.61 → 0.1.62
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/package.json +1 -1
- package/src/common-crawl-discovery.ts +28 -4
- package/src/locations.ts +7 -1
- package/src/providers.ts +9 -1
- package/src/version.ts +1 -1
package/package.json
CHANGED
|
@@ -8,7 +8,9 @@ import type { Ats } from "./types.ts";
|
|
|
8
8
|
import { assertArtifactFile } from "./artifact-path.ts";
|
|
9
9
|
|
|
10
10
|
type Fetch = (input: string | URL, init?: RequestInit) => Promise<Response>;
|
|
11
|
-
interface Options {
|
|
11
|
+
interface Options {
|
|
12
|
+
/** Backoff between index retries. Only set in tests, where real seconds are dead time. */
|
|
13
|
+
retryDelayMs?: number; country?: string; fetch?: Fetch; indexUrl?: string; registryPath?: string; provider?: Ats; indexRecordLimit?: number; sampleTokenLimit?: number; excludeTokens?: string[] }
|
|
12
14
|
interface Lead { sourceUrl: string; ats: string; token: string; discoveredFrom: { channel: "dataset"; reference: string } }
|
|
13
15
|
interface Collection { id?: unknown; "cdx-api"?: unknown }
|
|
14
16
|
|
|
@@ -26,6 +28,10 @@ export interface CommonCrawlDiscoveryReport extends ReportMeta {
|
|
|
26
28
|
rejected: number;
|
|
27
29
|
truncated: boolean;
|
|
28
30
|
truncatedPatterns: string[];
|
|
31
|
+
/** Patterns the index had no captures for, so a later run can tell an empty region from a missing one. */
|
|
32
|
+
emptyPatterns: string[];
|
|
33
|
+
/** Patterns the index refused, with the status it gave. Reported rather than thrown, so one bad pattern cannot discard the sweep. */
|
|
34
|
+
failedPatterns: Array<{ pattern: string; status: number }>;
|
|
29
35
|
candidatesPath: string;
|
|
30
36
|
reportPath: string;
|
|
31
37
|
registryPath?: string;
|
|
@@ -62,6 +68,8 @@ export async function discoverCommonCrawlSources(candidatesPath: string, reportP
|
|
|
62
68
|
const seenUrls = new Set<string>();
|
|
63
69
|
const rejections: Array<{ value: string; reason: string }> = [];
|
|
64
70
|
const truncatedPatterns: string[] = [];
|
|
71
|
+
const emptyPatterns: string[] = [];
|
|
72
|
+
const failedPatterns: Array<{ pattern: string; status: number }> = [];
|
|
65
73
|
let indexRecordsExamined = 0;
|
|
66
74
|
|
|
67
75
|
for (const pattern of options.provider ? providerPatterns[options.provider] : patterns) {
|
|
@@ -75,10 +83,20 @@ export async function discoverCommonCrawlSources(candidatesPath: string, reportP
|
|
|
75
83
|
let response = await fetcher(query);
|
|
76
84
|
for (let attempt = 1; !response.ok && response.status >= 500 && attempt <= 3; attempt += 1) { // the index server sheds load with 502s; back off and retry
|
|
77
85
|
await response.body?.cancel().catch(() => undefined);
|
|
78
|
-
await new Promise((resolve) => setTimeout(resolve, attempt * 5_000));
|
|
86
|
+
await new Promise((resolve) => setTimeout(resolve, attempt * (options.retryDelayMs ?? 5_000)));
|
|
79
87
|
response = await fetcher(query);
|
|
80
88
|
}
|
|
81
|
-
|
|
89
|
+
// The index answers 404 when a pattern matched nothing at all. That is an empty region, not a broken run.
|
|
90
|
+
if (response.status === 404) { await response.body?.cancel().catch(() => undefined); emptyPatterns.push(pattern); continue; }
|
|
91
|
+
// Anything else that survived the retries is this pattern's problem, not the run's. A sparse pattern asked
|
|
92
|
+
// for too many records times out (collapse and filter make the index do real work), and throwing there
|
|
93
|
+
// discarded every pattern that had already answered. A partial sweep is worth keeping; only a sweep where
|
|
94
|
+
// nothing at all succeeded is a failure.
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
await response.body?.cancel().catch(() => undefined);
|
|
97
|
+
failedPatterns.push({ pattern, status: response.status });
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
82
100
|
const lines = (await response.text()).split(/\r?\n/).filter(Boolean).slice(0, indexRecordLimit);
|
|
83
101
|
indexRecordsExamined += lines.length;
|
|
84
102
|
if (lines.length >= indexRecordLimit) truncatedPatterns.push(pattern);
|
|
@@ -93,6 +111,12 @@ export async function discoverCommonCrawlSources(candidatesPath: string, reportP
|
|
|
93
111
|
}
|
|
94
112
|
}
|
|
95
113
|
|
|
114
|
+
// Every pattern refused is a broken sweep, not an empty one, and must not be reported as a quiet zero.
|
|
115
|
+
const attempted = options.provider ? providerPatterns[options.provider] : patterns;
|
|
116
|
+
if (failedPatterns.length === attempted.length && attempted.length > 0) {
|
|
117
|
+
throw new Error(`Common Crawl index refused every pattern: ${failedPatterns.map((f) => `${f.pattern} HTTP ${f.status}`).join(", ")}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
96
120
|
const availableLeads: Lead[] = [];
|
|
97
121
|
const excludedTokens = new Set((options.excludeTokens ?? []).map((token) => token.toLowerCase()));
|
|
98
122
|
let alreadyKnown = 0;
|
|
@@ -113,7 +137,7 @@ export async function discoverCommonCrawlSources(candidatesPath: string, reportP
|
|
|
113
137
|
const registry = options.registryPath ? await mergeEnrichmentLeads(options.registryPath, registryLeads) : { added: 0, bytes: undefined, lockHeldMs: undefined };
|
|
114
138
|
const report: CommonCrawlDiscoveryReport = stampReport("common-crawl-discovery:1", 1, {
|
|
115
139
|
country, provider: options.provider, index, indexRecordsExamined, sampleTokenLimit, sampleShortfall: sampleTokenLimit === undefined ? 0 : Math.max(0, sampleTokenLimit - leads.length), urlsSeen: seenUrls.size, sourcesFound: found.size, alreadyKnown, unresolved: leads.length, rejected: rejections.length,
|
|
116
|
-
truncated: truncatedPatterns.length > 0, truncatedPatterns,
|
|
140
|
+
truncated: truncatedPatterns.length > 0, truncatedPatterns, emptyPatterns, failedPatterns,
|
|
117
141
|
candidatesPath, reportPath, registryPath: options.registryPath, registryAdded: registry.added, registryBytes: registry.bytes, registryLockHeldMs: registry.lockHeldMs, availableLeads, leads, rejections,
|
|
118
142
|
});
|
|
119
143
|
await atomicJson(reportPath, report);
|
package/src/locations.ts
CHANGED
|
@@ -106,6 +106,12 @@ const US_STATE_CODES = new Set(["AL", "AK", "AZ", "AR", "CA", "CO", "CT", "DE",
|
|
|
106
106
|
const US_CITY_STATE = /,\s*([A-Z]{2})(?=\s*(?:$|,|\d{5}|\(|\/|-))/g;
|
|
107
107
|
/** Workday's "ST-CITY, street" and "ST - City" shapes, e.g. "IN-INDIANAPOLIS, 220 VIRGINIA AVE" or "IN - Indianapolis". */
|
|
108
108
|
const US_STATE_PREFIX = /^([A-Z]{2})\s*-\s*(?=[A-Za-z])/;
|
|
109
|
+
/**
|
|
110
|
+
* Workday also writes the country in front of the state: "US-IN-FT WAYNE-150A ~ 1010 Production Rd".
|
|
111
|
+
* US_STATE_PREFIX reads the leading "US" as the state, finds it is not one, and gives up, so Fort Wayne
|
|
112
|
+
* was filed under India on the strength of a bare "IN" further along the string.
|
|
113
|
+
*/
|
|
114
|
+
const US_COUNTRY_STATE_PREFIX = /^US\s*-\s*([A-Z]{2})\s*-\s*(?=[A-Za-z0-9])/;
|
|
109
115
|
|
|
110
116
|
function detectCountries(location: string): string[] {
|
|
111
117
|
const byName: string[] = [];
|
|
@@ -117,7 +123,7 @@ function detectCountries(location: string): string[] {
|
|
|
117
123
|
// "Indianapolis, IN" is Indiana, not India: a bare code in the US "City, ST" position is a state unless a country name
|
|
118
124
|
// or a known Indian place says otherwise.
|
|
119
125
|
// Case matters: "Berlin, de" is a lowercase country code, "Gary, IN" is a state.
|
|
120
|
-
const prefix = US_STATE_PREFIX.exec(location)?.[1];
|
|
126
|
+
const prefix = US_COUNTRY_STATE_PREFIX.exec(location)?.[1] ?? US_STATE_PREFIX.exec(location)?.[1];
|
|
121
127
|
const stateCodes = [...[...location.matchAll(US_CITY_STATE)].map((match) => match[1]!), ...(prefix ? [prefix] : [])].filter((code) => US_STATE_CODES.has(code));
|
|
122
128
|
if (stateCodes.length && byName.length === 0 && !INDIA_PATTERN.test(location)) {
|
|
123
129
|
return [...new Set(["US", ...byCode.filter((code) => !US_STATE_CODES.has(code))])];
|
package/src/providers.ts
CHANGED
|
@@ -210,7 +210,15 @@ const oraclecloud: ProviderSpec = {
|
|
|
210
210
|
ats: "oraclecloud",
|
|
211
211
|
label: "Oracle Cloud Recruiting",
|
|
212
212
|
hosts: ["oraclecloud.com"],
|
|
213
|
-
|
|
213
|
+
// One pattern per Fusion region. A wildcard in the middle of the host plus a deep path suffix is more than
|
|
214
|
+
// the Common Crawl index will serve: "*.fa.oraclecloud.com/hcmUI/CandidateExperience*" answered 504 every
|
|
215
|
+
// time, which is why Oracle had no discovered tenants at all despite being supported. Per-region host
|
|
216
|
+
// patterns answer 200, and the CandidateExperience path is filtered from the returned URLs instead.
|
|
217
|
+
crawlPatterns: ["*.fa.us2.oraclecloud.com/*", "*.fa.us6.oraclecloud.com/*", "*.fa.us9.oraclecloud.com/*",
|
|
218
|
+
"*.fa.em2.oraclecloud.com/*", "*.fa.em3.oraclecloud.com/*", "*.fa.em4.oraclecloud.com/*",
|
|
219
|
+
"*.fa.ap1.oraclecloud.com/*", "*.fa.ap2.oraclecloud.com/*", "*.fa.ap3.oraclecloud.com/*",
|
|
220
|
+
"*.fa.ca2.oraclecloud.com/*", "*.fa.ca3.oraclecloud.com/*", "*.fa.uk1.oraclecloud.com/*",
|
|
221
|
+
"*.fa.sa1.oraclecloud.com/*"],
|
|
214
222
|
resolve(url) {
|
|
215
223
|
if (!ORACLE_HOST.test(url.hostname)) return null;
|
|
216
224
|
const site = /\/sites\/([A-Za-z0-9_]{2,32})/.exec(url.pathname)?.[1] ?? url.searchParams.get("siteNumber");
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.1.
|
|
1
|
+
export const VERSION = "0.1.62";
|