@connextar/house 0.10.0 → 0.12.0
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/README.md +590 -0
- package/dist/jobs/counts.d.ts +82 -0
- package/dist/jobs/counts.d.ts.map +1 -0
- package/dist/jobs/counts.js +112 -0
- package/dist/jobs/counts.js.map +1 -0
- package/dist/jobs/cron-auth.d.ts +44 -0
- package/dist/jobs/cron-auth.d.ts.map +1 -0
- package/dist/jobs/cron-auth.js +20 -0
- package/dist/jobs/cron-auth.js.map +1 -0
- package/dist/jobs/health.d.ts +170 -0
- package/dist/jobs/health.d.ts.map +1 -0
- package/dist/jobs/health.js +149 -0
- package/dist/jobs/health.js.map +1 -0
- package/dist/jobs/index.d.ts +27 -0
- package/dist/jobs/index.d.ts.map +1 -0
- package/dist/jobs/index.js +27 -0
- package/dist/jobs/index.js.map +1 -0
- package/dist/jobs/job-health-list.d.ts +78 -0
- package/dist/jobs/job-health-list.d.ts.map +1 -0
- package/dist/jobs/job-health-list.js +119 -0
- package/dist/jobs/job-health-list.js.map +1 -0
- package/dist/jobs/prune.d.ts +45 -0
- package/dist/jobs/prune.d.ts.map +1 -0
- package/dist/jobs/prune.js +58 -0
- package/dist/jobs/prune.js.map +1 -0
- package/dist/jobs/react-index.d.ts +19 -0
- package/dist/jobs/react-index.d.ts.map +1 -0
- package/dist/jobs/react-index.js +18 -0
- package/dist/jobs/react-index.js.map +1 -0
- package/dist/jobs/registry.d.ts +119 -0
- package/dist/jobs/registry.d.ts.map +1 -0
- package/dist/jobs/registry.js +87 -0
- package/dist/jobs/registry.js.map +1 -0
- package/dist/jobs/scheduler.d.ts +100 -0
- package/dist/jobs/scheduler.d.ts.map +1 -0
- package/dist/jobs/scheduler.js +297 -0
- package/dist/jobs/scheduler.js.map +1 -0
- package/dist/jobs/store.d.ts +119 -0
- package/dist/jobs/store.d.ts.map +1 -0
- package/dist/jobs/store.js +87 -0
- package/dist/jobs/store.js.map +1 -0
- package/dist/jobs/windows.d.ts +66 -0
- package/dist/jobs/windows.d.ts.map +1 -0
- package/dist/jobs/windows.js +130 -0
- package/dist/jobs/windows.js.map +1 -0
- package/package.json +9 -1
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { isManualKey } from "./windows.js";
|
|
2
|
+
/**
|
|
3
|
+
* A stored switch value, read the way AltEd learnt to: a row that exists but
|
|
4
|
+
* does not read as `{ paused: boolean }` is **paused**. Somebody stored it, and a
|
|
5
|
+
* switch nobody can read should not send a job out spending.
|
|
6
|
+
*/
|
|
7
|
+
export function pausedFromStored(value) {
|
|
8
|
+
if (value && typeof value === "object" && typeof value.paused === "boolean") {
|
|
9
|
+
return value.paused;
|
|
10
|
+
}
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The whole contract in memory — for tests, the app's included. It keeps the
|
|
15
|
+
* unique `(job, windowKey)` rule exactly as the database would.
|
|
16
|
+
*/
|
|
17
|
+
export function createMemoryJobStore() {
|
|
18
|
+
const runs = [];
|
|
19
|
+
const switches = new Map();
|
|
20
|
+
const switchLog = [];
|
|
21
|
+
let next = 0;
|
|
22
|
+
return {
|
|
23
|
+
runs,
|
|
24
|
+
switches,
|
|
25
|
+
switchLog,
|
|
26
|
+
async claimRun(claim) {
|
|
27
|
+
if (runs.some((run) => run.job === claim.job && run.windowKey === claim.windowKey))
|
|
28
|
+
return null;
|
|
29
|
+
const id = `run-${++next}`;
|
|
30
|
+
runs.push({ id, ...claim, finishedAt: null, ok: false, counts: null, error: null });
|
|
31
|
+
return id;
|
|
32
|
+
},
|
|
33
|
+
async finishRun(id, outcome) {
|
|
34
|
+
const run = runs.find((row) => row.id === id);
|
|
35
|
+
if (!run)
|
|
36
|
+
throw new Error(`No run ${id}`);
|
|
37
|
+
Object.assign(run, outcome);
|
|
38
|
+
},
|
|
39
|
+
async latestRun(query) {
|
|
40
|
+
return latestOf(runs.filter((run) => run.job === query.job &&
|
|
41
|
+
query.tenants.includes(run.tenantId) &&
|
|
42
|
+
(!query.ok || run.ok) &&
|
|
43
|
+
(!query.manualOnly || isManualKey(run.windowKey))));
|
|
44
|
+
},
|
|
45
|
+
async latestUnfinishedRun(job) {
|
|
46
|
+
return latestOf(runs.filter((run) => run.job === job && run.finishedAt === null));
|
|
47
|
+
},
|
|
48
|
+
async pruneRuns({ startedBefore, limit }) {
|
|
49
|
+
// What health reads: per job and tenant, the latest run and the latest success.
|
|
50
|
+
const kept = new Set();
|
|
51
|
+
const groups = new Map();
|
|
52
|
+
for (const run of runs) {
|
|
53
|
+
const key = JSON.stringify([run.job, run.tenantId]);
|
|
54
|
+
groups.set(key, [...(groups.get(key) ?? []), run]);
|
|
55
|
+
}
|
|
56
|
+
for (const group of groups.values()) {
|
|
57
|
+
for (const latest of [latestOf(group), latestOf(group.filter((run) => run.ok))]) {
|
|
58
|
+
if (latest)
|
|
59
|
+
kept.add(latest.id);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const doomed = runs
|
|
63
|
+
.map((run, index) => ({ run, index }))
|
|
64
|
+
.filter(({ run }) => run.finishedAt !== null && run.startedAt.getTime() < startedBefore.getTime() && !kept.has(run.id))
|
|
65
|
+
.sort((a, b) => a.run.startedAt.getTime() - b.run.startedAt.getTime() || a.index - b.index)
|
|
66
|
+
.slice(0, limit)
|
|
67
|
+
.map(({ run }) => run.id);
|
|
68
|
+
const gone = new Set(doomed);
|
|
69
|
+
for (let i = runs.length - 1; i >= 0; i--)
|
|
70
|
+
if (gone.has(runs[i].id))
|
|
71
|
+
runs.splice(i, 1);
|
|
72
|
+
return gone.size;
|
|
73
|
+
},
|
|
74
|
+
async readSwitches() {
|
|
75
|
+
return new Map(switches);
|
|
76
|
+
},
|
|
77
|
+
async writeSwitch(change) {
|
|
78
|
+
switches.set(change.job, { paused: change.paused, at: change.at, by: change.by.name });
|
|
79
|
+
switchLog.push(change);
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/** Latest by start; on a tie, the one inserted last — what a database's `ORDER BY started_at DESC, id DESC` gives. */
|
|
84
|
+
function latestOf(runs) {
|
|
85
|
+
return runs.reduce((latest, run) => (!latest || run.startedAt.getTime() >= latest.startedAt.getTime() ? run : latest), null);
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/jobs/store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run windows: which window a moment falls in, or none.
|
|
3
|
+
*
|
|
4
|
+
* A window is a function from "now" to a key, or `null` when the job is not due
|
|
5
|
+
* at all. The key is what the ledger's unique index is on, together with the
|
|
6
|
+
* job, so **one run per job per key** is the whole of idempotency: a second tick
|
|
7
|
+
* in the same window finds the key taken and does nothing.
|
|
8
|
+
*
|
|
9
|
+
* Both apps wrote this as two functions — `attempt(now)` and `windowKey(now)` —
|
|
10
|
+
* which could disagree. One function cannot.
|
|
11
|
+
*
|
|
12
|
+
* The helpers carry one lesson each:
|
|
13
|
+
*
|
|
14
|
+
* - **A day is a day somewhere.** AltEd keyed its tracker job on the UTC date
|
|
15
|
+
* while the job took "yesterday" from the UK calendar; in summer a tick after
|
|
16
|
+
* 23:00 UTC and the next at 00:00 UTC fell in different windows but the same
|
|
17
|
+
* UK day, so that day's misses were sent twice and the next day's never. Every
|
|
18
|
+
* calendar helper takes the `timeZone` the job's own idea of a day comes from.
|
|
19
|
+
* - **Not before a civil hour.** `from` keeps a morning email out of midnight —
|
|
20
|
+
* AltEd's weekly digest waits for 07:00 "so nobody's phone lights up".
|
|
21
|
+
* - **A missed start is caught up, not lost.** Quickie's monthly report ran only
|
|
22
|
+
* on the 1st, so a day of failed ticks on the 1st lost the month. `weekly` and
|
|
23
|
+
* `monthly` stay open from their start to the end of their period (after
|
|
24
|
+
* `from` each day), and the key keeps it to one run.
|
|
25
|
+
*
|
|
26
|
+
* Keys match what both apps already store (`2026-09-30`, `2026-09-30T14`,
|
|
27
|
+
* `2026-09-30Q2`, `month:2026-09`), so moving a job onto a helper does not run
|
|
28
|
+
* it a second time in the window it moves in.
|
|
29
|
+
*/
|
|
30
|
+
export type JobWindow = (now: Date) => string | null;
|
|
31
|
+
export interface CalendarWindowOptions {
|
|
32
|
+
/** The first hour of the day the window is open, in `timeZone`. Default 0. */
|
|
33
|
+
from?: number;
|
|
34
|
+
/** Whose calendar. Default `UTC`. A job that reasons about "today" in the UK says `Europe/London`. */
|
|
35
|
+
timeZone?: string;
|
|
36
|
+
}
|
|
37
|
+
/** Once an hour. Key `2026-09-30T14` (UTC) — Quickie's hourly key. */
|
|
38
|
+
export declare function hourly(): JobWindow;
|
|
39
|
+
/**
|
|
40
|
+
* Once every `hours` hours, in blocks from UTC midnight. Key `2026-09-30Q2` —
|
|
41
|
+
* with 6, exactly Quickie's four-a-day bank feed key.
|
|
42
|
+
*/
|
|
43
|
+
export declare function everyHours(hours: number): JobWindow;
|
|
44
|
+
/** Once a day, from `from` o'clock to midnight in `timeZone`. Key `2026-09-30`, the date there. */
|
|
45
|
+
export declare function daily(options?: CalendarWindowOptions): JobWindow;
|
|
46
|
+
/**
|
|
47
|
+
* Once a week, opening on `weekday` (1 = Monday) at `from` and staying open —
|
|
48
|
+
* from `from` each day — until the next one opens. Key `week:2026-09-28`, the
|
|
49
|
+
* date it opened.
|
|
50
|
+
*/
|
|
51
|
+
export declare function weekly(options?: CalendarWindowOptions & {
|
|
52
|
+
weekday?: number;
|
|
53
|
+
}): JobWindow;
|
|
54
|
+
/**
|
|
55
|
+
* Once a month, opening on `day` at `from` and staying open — from `from` each
|
|
56
|
+
* day — to the month's end. Key `month:2026-09` — Quickie's monthly key.
|
|
57
|
+
* `day` is 1–28, so the window opens in February too.
|
|
58
|
+
*/
|
|
59
|
+
export declare function monthly(options?: CalendarWindowOptions & {
|
|
60
|
+
day?: number;
|
|
61
|
+
}): JobWindow;
|
|
62
|
+
/** The prefix every Run now key starts with, in both apps' existing rows and in this module's. */
|
|
63
|
+
export declare const MANUAL_KEY_PREFIX = "manual:";
|
|
64
|
+
/** Whether a stored window key was a Run now rather than the schedule. */
|
|
65
|
+
export declare function isManualKey(windowKey: string): boolean;
|
|
66
|
+
//# sourceMappingURL=windows.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"windows.d.ts","sourceRoot":"","sources":["../../src/jobs/windows.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,MAAM,MAAM,SAAS,GAAG,CAAC,GAAG,EAAE,IAAI,KAAK,MAAM,GAAG,IAAI,CAAC;AAoDrD,MAAM,WAAW,qBAAqB;IACpC,8EAA8E;IAC9E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sGAAsG;IACtG,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,sEAAsE;AACtE,wBAAgB,MAAM,IAAI,SAAS,CAElC;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAKnD;AAED,mGAAmG;AACnG,wBAAgB,KAAK,CAAC,OAAO,GAAE,qBAA0B,GAAG,SAAS,CAOpE;AAED;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,OAAO,GAAE,qBAAqB,GAAG;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,SAAS,CAY5F;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,OAAO,GAAE,qBAAqB,GAAG;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,SAAS,CASzF;AAED,kGAAkG;AAClG,eAAO,MAAM,iBAAiB,YAAY,CAAC;AAE3C,0EAA0E;AAC1E,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAEtD"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run windows: which window a moment falls in, or none.
|
|
3
|
+
*
|
|
4
|
+
* A window is a function from "now" to a key, or `null` when the job is not due
|
|
5
|
+
* at all. The key is what the ledger's unique index is on, together with the
|
|
6
|
+
* job, so **one run per job per key** is the whole of idempotency: a second tick
|
|
7
|
+
* in the same window finds the key taken and does nothing.
|
|
8
|
+
*
|
|
9
|
+
* Both apps wrote this as two functions — `attempt(now)` and `windowKey(now)` —
|
|
10
|
+
* which could disagree. One function cannot.
|
|
11
|
+
*
|
|
12
|
+
* The helpers carry one lesson each:
|
|
13
|
+
*
|
|
14
|
+
* - **A day is a day somewhere.** AltEd keyed its tracker job on the UTC date
|
|
15
|
+
* while the job took "yesterday" from the UK calendar; in summer a tick after
|
|
16
|
+
* 23:00 UTC and the next at 00:00 UTC fell in different windows but the same
|
|
17
|
+
* UK day, so that day's misses were sent twice and the next day's never. Every
|
|
18
|
+
* calendar helper takes the `timeZone` the job's own idea of a day comes from.
|
|
19
|
+
* - **Not before a civil hour.** `from` keeps a morning email out of midnight —
|
|
20
|
+
* AltEd's weekly digest waits for 07:00 "so nobody's phone lights up".
|
|
21
|
+
* - **A missed start is caught up, not lost.** Quickie's monthly report ran only
|
|
22
|
+
* on the 1st, so a day of failed ticks on the 1st lost the month. `weekly` and
|
|
23
|
+
* `monthly` stay open from their start to the end of their period (after
|
|
24
|
+
* `from` each day), and the key keeps it to one run.
|
|
25
|
+
*
|
|
26
|
+
* Keys match what both apps already store (`2026-09-30`, `2026-09-30T14`,
|
|
27
|
+
* `2026-09-30Q2`, `month:2026-09`), so moving a job onto a helper does not run
|
|
28
|
+
* it a second time in the window it moves in.
|
|
29
|
+
*/
|
|
30
|
+
const WEEKDAYS = { Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6, Sun: 7 };
|
|
31
|
+
const formatters = new Map();
|
|
32
|
+
function zoned(now, timeZone) {
|
|
33
|
+
let format = formatters.get(timeZone);
|
|
34
|
+
if (!format) {
|
|
35
|
+
format = new Intl.DateTimeFormat("en-US", {
|
|
36
|
+
timeZone,
|
|
37
|
+
year: "numeric",
|
|
38
|
+
month: "2-digit",
|
|
39
|
+
day: "2-digit",
|
|
40
|
+
hour: "2-digit",
|
|
41
|
+
hourCycle: "h23",
|
|
42
|
+
weekday: "short",
|
|
43
|
+
});
|
|
44
|
+
formatters.set(timeZone, format);
|
|
45
|
+
}
|
|
46
|
+
const parts = Object.fromEntries(format.formatToParts(now).map((part) => [part.type, part.value]));
|
|
47
|
+
return {
|
|
48
|
+
year: Number(parts.year),
|
|
49
|
+
month: Number(parts.month),
|
|
50
|
+
day: Number(parts.day),
|
|
51
|
+
hour: Number(parts.hour),
|
|
52
|
+
weekday: WEEKDAYS[parts.weekday ?? ""] ?? 1,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const pad = (value, width = 2) => String(value).padStart(width, "0");
|
|
56
|
+
/** `YYYY-MM-DD` for a calendar date, moved by whole days without touching a clock. */
|
|
57
|
+
function dateKey(year, month, day, plusDays = 0) {
|
|
58
|
+
const date = new Date(Date.UTC(year, month - 1, day + plusDays));
|
|
59
|
+
return `${pad(date.getUTCFullYear(), 4)}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())}`;
|
|
60
|
+
}
|
|
61
|
+
function checkHour(from) {
|
|
62
|
+
if (!Number.isInteger(from) || from < 0 || from > 23)
|
|
63
|
+
throw new RangeError(`"from" must be an hour, 0–23 (got ${from}).`);
|
|
64
|
+
}
|
|
65
|
+
/** Once an hour. Key `2026-09-30T14` (UTC) — Quickie's hourly key. */
|
|
66
|
+
export function hourly() {
|
|
67
|
+
return (now) => `${now.toISOString().slice(0, 13)}`;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Once every `hours` hours, in blocks from UTC midnight. Key `2026-09-30Q2` —
|
|
71
|
+
* with 6, exactly Quickie's four-a-day bank feed key.
|
|
72
|
+
*/
|
|
73
|
+
export function everyHours(hours) {
|
|
74
|
+
if (!Number.isInteger(hours) || hours < 1 || 24 % hours !== 0) {
|
|
75
|
+
throw new RangeError(`everyHours takes a whole number of hours that divides the day (got ${hours}).`);
|
|
76
|
+
}
|
|
77
|
+
return (now) => `${now.toISOString().slice(0, 10)}Q${Math.floor(now.getUTCHours() / hours)}`;
|
|
78
|
+
}
|
|
79
|
+
/** Once a day, from `from` o'clock to midnight in `timeZone`. Key `2026-09-30`, the date there. */
|
|
80
|
+
export function daily(options = {}) {
|
|
81
|
+
const { from = 0, timeZone = "UTC" } = options;
|
|
82
|
+
checkHour(from);
|
|
83
|
+
return (now) => {
|
|
84
|
+
const at = zoned(now, timeZone);
|
|
85
|
+
return at.hour >= from ? dateKey(at.year, at.month, at.day) : null;
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Once a week, opening on `weekday` (1 = Monday) at `from` and staying open —
|
|
90
|
+
* from `from` each day — until the next one opens. Key `week:2026-09-28`, the
|
|
91
|
+
* date it opened.
|
|
92
|
+
*/
|
|
93
|
+
export function weekly(options = {}) {
|
|
94
|
+
const { weekday = 1, from = 0, timeZone = "UTC" } = options;
|
|
95
|
+
checkHour(from);
|
|
96
|
+
if (!Number.isInteger(weekday) || weekday < 1 || weekday > 7) {
|
|
97
|
+
throw new RangeError(`weekday is 1 (Monday) to 7 (Sunday) (got ${weekday}).`);
|
|
98
|
+
}
|
|
99
|
+
return (now) => {
|
|
100
|
+
const at = zoned(now, timeZone);
|
|
101
|
+
if (at.hour < from)
|
|
102
|
+
return null;
|
|
103
|
+
const since = (at.weekday - weekday + 7) % 7;
|
|
104
|
+
return `week:${dateKey(at.year, at.month, at.day, -since)}`;
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Once a month, opening on `day` at `from` and staying open — from `from` each
|
|
109
|
+
* day — to the month's end. Key `month:2026-09` — Quickie's monthly key.
|
|
110
|
+
* `day` is 1–28, so the window opens in February too.
|
|
111
|
+
*/
|
|
112
|
+
export function monthly(options = {}) {
|
|
113
|
+
const { day = 1, from = 0, timeZone = "UTC" } = options;
|
|
114
|
+
checkHour(from);
|
|
115
|
+
if (!Number.isInteger(day) || day < 1 || day > 28)
|
|
116
|
+
throw new RangeError(`day is 1–28 (got ${day}).`);
|
|
117
|
+
return (now) => {
|
|
118
|
+
const at = zoned(now, timeZone);
|
|
119
|
+
if (at.day < day || at.hour < from)
|
|
120
|
+
return null;
|
|
121
|
+
return `month:${pad(at.year, 4)}-${pad(at.month)}`;
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/** The prefix every Run now key starts with, in both apps' existing rows and in this module's. */
|
|
125
|
+
export const MANUAL_KEY_PREFIX = "manual:";
|
|
126
|
+
/** Whether a stored window key was a Run now rather than the schedule. */
|
|
127
|
+
export function isManualKey(windowKey) {
|
|
128
|
+
return windowKey.startsWith(MANUAL_KEY_PREFIX);
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=windows.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"windows.js","sourceRoot":"","sources":["../../src/jobs/windows.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@connextar/house",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "The pieces every app we build needs and none of them should own a copy of.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -62,6 +62,14 @@
|
|
|
62
62
|
"types": "./dist/grid/index.d.ts",
|
|
63
63
|
"default": "./dist/grid/index.js"
|
|
64
64
|
},
|
|
65
|
+
"./jobs": {
|
|
66
|
+
"types": "./dist/jobs/index.d.ts",
|
|
67
|
+
"default": "./dist/jobs/index.js"
|
|
68
|
+
},
|
|
69
|
+
"./jobs/react": {
|
|
70
|
+
"types": "./dist/jobs/react-index.d.ts",
|
|
71
|
+
"default": "./dist/jobs/react-index.js"
|
|
72
|
+
},
|
|
65
73
|
"./web-search": {
|
|
66
74
|
"types": "./dist/web-search/index.d.ts",
|
|
67
75
|
"default": "./dist/web-search/index.js"
|