@rtbnext/core 2.0.0-alpha.1
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/LICENSE +21 -0
- package/README.md +11 -0
- package/dist/abstract/Cache.d.ts +9 -0
- package/dist/abstract/Cache.js +19 -0
- package/dist/abstract/Index.d.ts +22 -0
- package/dist/abstract/Index.js +81 -0
- package/dist/abstract/Job.d.ts +19 -0
- package/dist/abstract/Job.js +49 -0
- package/dist/abstract/Snapshot.d.ts +22 -0
- package/dist/abstract/Snapshot.js +78 -0
- package/dist/bin/cli.d.ts +2 -0
- package/dist/bin/cli.js +25 -0
- package/dist/bin/cron.d.ts +2 -0
- package/dist/bin/cron.js +4 -0
- package/dist/core/Config.d.ts +30 -0
- package/dist/core/Config.js +66 -0
- package/dist/core/Cron.d.ts +12 -0
- package/dist/core/Cron.js +52 -0
- package/dist/core/Fetch.d.ts +28 -0
- package/dist/core/Fetch.js +172 -0
- package/dist/core/Logger.d.ts +30 -0
- package/dist/core/Logger.js +92 -0
- package/dist/core/Queue.d.ts +37 -0
- package/dist/core/Queue.js +136 -0
- package/dist/core/Storage.d.ts +28 -0
- package/dist/core/Storage.js +166 -0
- package/dist/core/Utils.d.ts +33 -0
- package/dist/core/Utils.js +167 -0
- package/dist/interfaces/cache.d.ts +6 -0
- package/dist/interfaces/config.d.ts +21 -0
- package/dist/interfaces/cron.d.ts +3 -0
- package/dist/interfaces/fetch.d.ts +13 -0
- package/dist/interfaces/filter.d.ts +12 -0
- package/dist/interfaces/index.d.ts +30 -0
- package/dist/interfaces/job.d.ts +9 -0
- package/dist/interfaces/list.d.ts +9 -0
- package/dist/interfaces/logger.d.ts +20 -0
- package/dist/interfaces/mover.d.ts +7 -0
- package/dist/interfaces/parser.d.ts +68 -0
- package/dist/interfaces/profile.d.ts +30 -0
- package/dist/interfaces/queue.d.ts +17 -0
- package/dist/interfaces/snapshot.d.ts +16 -0
- package/dist/interfaces/stats.d.ts +45 -0
- package/dist/interfaces/storage.d.ts +16 -0
- package/dist/job/Alias.d.ts +8 -0
- package/dist/job/Alias.js +42 -0
- package/dist/job/Annual.d.ts +8 -0
- package/dist/job/Annual.js +41 -0
- package/dist/job/List.d.ts +11 -0
- package/dist/job/List.js +101 -0
- package/dist/job/Merge.d.ts +10 -0
- package/dist/job/Merge.js +59 -0
- package/dist/job/Move.d.ts +7 -0
- package/dist/job/Move.js +33 -0
- package/dist/job/Performance.d.ts +8 -0
- package/dist/job/Performance.js +27 -0
- package/dist/job/Profile.d.ts +11 -0
- package/dist/job/Profile.js +76 -0
- package/dist/job/Queue.d.ts +8 -0
- package/dist/job/Queue.js +54 -0
- package/dist/job/RTB.d.ts +12 -0
- package/dist/job/RTB.js +121 -0
- package/dist/job/Stats.d.ts +11 -0
- package/dist/job/Stats.js +46 -0
- package/dist/job/Top10.d.ts +9 -0
- package/dist/job/Top10.js +48 -0
- package/dist/job/Wiki.d.ts +9 -0
- package/dist/job/Wiki.js +40 -0
- package/dist/job/index.d.ts +26 -0
- package/dist/job/index.js +26 -0
- package/dist/lib/const.d.ts +31 -0
- package/dist/lib/const.js +74 -0
- package/dist/lib/list.d.ts +90 -0
- package/dist/lib/list.js +72 -0
- package/dist/lib/regex.d.ts +7 -0
- package/dist/lib/regex.js +7 -0
- package/dist/model/Filter.d.ts +28 -0
- package/dist/model/Filter.js +122 -0
- package/dist/model/List.d.ts +12 -0
- package/dist/model/List.js +43 -0
- package/dist/model/ListIndex.d.ts +8 -0
- package/dist/model/ListIndex.js +10 -0
- package/dist/model/Mover.d.ts +15 -0
- package/dist/model/Mover.js +74 -0
- package/dist/model/Profile.d.ts +49 -0
- package/dist/model/Profile.js +181 -0
- package/dist/model/ProfileIndex.d.ts +20 -0
- package/dist/model/ProfileIndex.js +140 -0
- package/dist/model/Stats.d.ts +56 -0
- package/dist/model/Stats.js +435 -0
- package/dist/parser/BillionairesListParser.d.ts +3 -0
- package/dist/parser/BillionairesListParser.js +2 -0
- package/dist/parser/ListParser.d.ts +7 -0
- package/dist/parser/ListParser.js +11 -0
- package/dist/parser/Parser.d.ts +43 -0
- package/dist/parser/Parser.js +146 -0
- package/dist/parser/PersonListParser.d.ts +29 -0
- package/dist/parser/PersonListParser.js +111 -0
- package/dist/parser/ProfileParser.d.ts +44 -0
- package/dist/parser/ProfileParser.js +193 -0
- package/dist/parser/RTBListParser.d.ts +15 -0
- package/dist/parser/RTBListParser.js +91 -0
- package/dist/types/annual.d.ts +7 -0
- package/dist/types/config.d.ts +35 -0
- package/dist/types/fetch.d.ts +3 -0
- package/dist/types/generic.d.ts +10 -0
- package/dist/types/job.d.ts +71 -0
- package/dist/types/list.d.ts +49 -0
- package/dist/types/parser.d.ts +7 -0
- package/dist/types/profile.d.ts +9 -0
- package/dist/types/queue.d.ts +15 -0
- package/dist/types/response.d.ts +183 -0
- package/dist/types/storage.d.ts +3 -0
- package/dist/types/wiki.d.ts +1 -0
- package/dist/utils/Annual.d.ts +7 -0
- package/dist/utils/Annual.js +99 -0
- package/dist/utils/Performance.d.ts +8 -0
- package/dist/utils/Performance.js +39 -0
- package/dist/utils/ProfileManager.d.ts +24 -0
- package/dist/utils/ProfileManager.js +60 -0
- package/dist/utils/ProfileMerger.d.ts +11 -0
- package/dist/utils/ProfileMerger.js +67 -0
- package/dist/utils/Ranking.d.ts +11 -0
- package/dist/utils/Ranking.js +77 -0
- package/dist/utils/Wiki.d.ts +11 -0
- package/dist/utils/Wiki.js +168 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 rtbnext by Paul Köhler (komed3)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# @rtbnext/core
|
|
2
|
+
|
|
3
|
+
Internal core package for the rtbnext project.
|
|
4
|
+
|
|
5
|
+
Implements the [rtbnext database](#) model and provides services for accessing data via the [rtbnext API](#). It is based on and integrates the [@rtbnext/schema](https://github.com/rtbnext/schema).
|
|
6
|
+
|
|
7
|
+
It serves as the backbone across all services in the project.
|
|
8
|
+
|
|
9
|
+
**Copyright © 2026 rtbnext**
|
|
10
|
+
Created and maintained by Paul Köhler (komed3).
|
|
11
|
+
Licensed under the MIT License.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ICache } from '../interfaces/cache';
|
|
2
|
+
export declare abstract class Cache implements ICache {
|
|
3
|
+
protected cachedData: Map<string, unknown>;
|
|
4
|
+
protected cache<T = unknown>(key: string, fn: () => T): T;
|
|
5
|
+
get size(): number;
|
|
6
|
+
get<T = unknown>(key: string): T | undefined;
|
|
7
|
+
has(key: string): boolean;
|
|
8
|
+
clear(): void;
|
|
9
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export class Cache {
|
|
2
|
+
cachedData = new Map();
|
|
3
|
+
cache(key, fn) {
|
|
4
|
+
if (!this.cachedData.has(key)) this.cachedData.set(key, fn());
|
|
5
|
+
return this.cachedData.get(key);
|
|
6
|
+
}
|
|
7
|
+
get size() {
|
|
8
|
+
return this.cachedData.size;
|
|
9
|
+
}
|
|
10
|
+
get(key) {
|
|
11
|
+
return this.cachedData.get(key);
|
|
12
|
+
}
|
|
13
|
+
has(key) {
|
|
14
|
+
return this.cachedData.has(key);
|
|
15
|
+
}
|
|
16
|
+
clear() {
|
|
17
|
+
this.cachedData.clear();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { TIndex } from '@rtbnext/schema/src/base/generic';
|
|
2
|
+
import type { IIndex } from '../interfaces/index';
|
|
3
|
+
export declare abstract class Index<I extends TIndex, T extends Map<string, I>> implements IIndex<I, T> {
|
|
4
|
+
protected static readonly storage: import('../interfaces/storage').IStorage;
|
|
5
|
+
protected readonly type: 'profile' | 'list';
|
|
6
|
+
protected readonly path: string;
|
|
7
|
+
protected index: T;
|
|
8
|
+
protected constructor(type: 'profile' | 'list', path: string);
|
|
9
|
+
protected loadIndex(): T;
|
|
10
|
+
protected saveIndex(): void;
|
|
11
|
+
get size(): number;
|
|
12
|
+
get values(): IterableIterator<I>;
|
|
13
|
+
get keys(): IterableIterator<string>;
|
|
14
|
+
getIndex(): T;
|
|
15
|
+
has(uriLike: string): boolean;
|
|
16
|
+
get(uriLike: string): I | undefined;
|
|
17
|
+
update(uriLike: string, data: Partial<I>, allowUpdate?: boolean, save?: boolean): I | false;
|
|
18
|
+
delta(items: Array<{ uriLike: string; data: Partial<I> }>, allowUpdate?: boolean): number;
|
|
19
|
+
add(uriLike: string, data: I): I | false;
|
|
20
|
+
delete(uriLike: string): void;
|
|
21
|
+
search(query: string, looseMatch?: boolean): T;
|
|
22
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { log } from '../core/Logger.js';
|
|
2
|
+
import { Storage } from '../core/Storage.js';
|
|
3
|
+
import { Utils } from '../core/Utils.js';
|
|
4
|
+
export class Index {
|
|
5
|
+
static storage = Storage.getInstance();
|
|
6
|
+
type;
|
|
7
|
+
path;
|
|
8
|
+
index;
|
|
9
|
+
constructor(type, path) {
|
|
10
|
+
this.type = type;
|
|
11
|
+
this.path = path;
|
|
12
|
+
Index.storage.ensurePath(this.path);
|
|
13
|
+
this.index = this.loadIndex();
|
|
14
|
+
}
|
|
15
|
+
// --- load & save index ---
|
|
16
|
+
loadIndex() {
|
|
17
|
+
const raw = Index.storage.readJSON(this.path) ?? {};
|
|
18
|
+
log.debug(`Index [${this.type}] loaded: ${Object.keys(raw).length} items`);
|
|
19
|
+
return new Map(Object.entries(raw));
|
|
20
|
+
}
|
|
21
|
+
saveIndex() {
|
|
22
|
+
const content = Object.fromEntries(this.index);
|
|
23
|
+
Index.storage.writeJSON(this.path, content);
|
|
24
|
+
}
|
|
25
|
+
// --- basic operations ---
|
|
26
|
+
get size() {
|
|
27
|
+
return this.index.size;
|
|
28
|
+
}
|
|
29
|
+
get values() {
|
|
30
|
+
return this.index.values();
|
|
31
|
+
}
|
|
32
|
+
get keys() {
|
|
33
|
+
return this.index.keys();
|
|
34
|
+
}
|
|
35
|
+
getIndex() {
|
|
36
|
+
return this.index;
|
|
37
|
+
}
|
|
38
|
+
has(uriLike) {
|
|
39
|
+
return this.index.has(Utils.sanitize(uriLike));
|
|
40
|
+
}
|
|
41
|
+
get(uriLike) {
|
|
42
|
+
return this.index.get(Utils.sanitize(uriLike));
|
|
43
|
+
}
|
|
44
|
+
// --- manipulate index (add, update, remove items) ---
|
|
45
|
+
update(uriLike, data, allowUpdate = true, save = true) {
|
|
46
|
+
return (
|
|
47
|
+
log.catch(() => {
|
|
48
|
+
const uri = Utils.sanitize(uriLike);
|
|
49
|
+
if (!allowUpdate && this.index.has(uri)) return false;
|
|
50
|
+
log.debug(`Updating index [${this.type}] item: ${uri}`, data);
|
|
51
|
+
const item = Utils.merge('unique' /* ArrayMode.Unique */, this.index.get(uri) ?? {}, data);
|
|
52
|
+
this.index.set(uri, item);
|
|
53
|
+
if (save) this.saveIndex();
|
|
54
|
+
return item;
|
|
55
|
+
}, `Failed to update index [${this.type}] item: ${uriLike}`) ?? false
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
delta(items, allowUpdate = true) {
|
|
59
|
+
const updated = items.reduce(
|
|
60
|
+
(count, { uriLike, data }) => count + (this.update(uriLike, data, allowUpdate, false) ? 1 : 0),
|
|
61
|
+
0
|
|
62
|
+
);
|
|
63
|
+
this.saveIndex();
|
|
64
|
+
log.debug(`Index [${this.type}] delta applied: ${updated} items updated`);
|
|
65
|
+
return updated;
|
|
66
|
+
}
|
|
67
|
+
add(uriLike, data) {
|
|
68
|
+
log.debug(`Adding index [${this.type}] item: ${uriLike}`);
|
|
69
|
+
return this.update(uriLike, data, false);
|
|
70
|
+
}
|
|
71
|
+
delete(uriLike) {
|
|
72
|
+
log.debug(`Deleting index [${this.type}] item: ${uriLike}`);
|
|
73
|
+
this.index.delete(Utils.sanitize(uriLike));
|
|
74
|
+
this.saveIndex();
|
|
75
|
+
}
|
|
76
|
+
// --- search index items ---
|
|
77
|
+
search(query, looseMatch = false) {
|
|
78
|
+
const tokens = Utils.buildSearchText(query).split(' ').filter(Boolean);
|
|
79
|
+
return new Map([...this.index].filter(([_, { text }]) => Utils.tokenSearch(text, tokens, looseMatch)));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { IJob } from '../interfaces/job';
|
|
2
|
+
import type { TLoggingLevel } from '../types/config';
|
|
3
|
+
import type { TJobClsOptions } from '../types/job';
|
|
4
|
+
export declare abstract class Job<T extends TJobClsOptions = TJobClsOptions> implements IJob<T> {
|
|
5
|
+
protected static readonly config: import('../interfaces/config').IConfig;
|
|
6
|
+
protected readonly options: T;
|
|
7
|
+
protected readonly job: string;
|
|
8
|
+
protected readonly silent: boolean;
|
|
9
|
+
protected readonly safeMode: boolean;
|
|
10
|
+
constructor(options: T, job: string);
|
|
11
|
+
protected log(msg: string, meta?: any, as?: TLoggingLevel): void;
|
|
12
|
+
protected err(err: unknown, msg?: string): void;
|
|
13
|
+
protected protect<F extends (...args: any[]) => any, R = Awaited<ReturnType<F>>>(fn: F): Promise<R | undefined>;
|
|
14
|
+
getJobName(): string;
|
|
15
|
+
getOptions(): T;
|
|
16
|
+
isSilent(): boolean;
|
|
17
|
+
isSafeMode(): boolean;
|
|
18
|
+
abstract run(): void | Promise<void>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Config } from '../core/Config.js';
|
|
2
|
+
import { log } from '../core/Logger.js';
|
|
3
|
+
import { Utils } from '../core/Utils.js';
|
|
4
|
+
import { Parser } from '../parser/Parser.js';
|
|
5
|
+
export class Job {
|
|
6
|
+
static config = Config.getInstance();
|
|
7
|
+
options;
|
|
8
|
+
job;
|
|
9
|
+
silent;
|
|
10
|
+
safeMode;
|
|
11
|
+
constructor(options, job) {
|
|
12
|
+
this.options = options;
|
|
13
|
+
this.job = job;
|
|
14
|
+
const { silent, safeMode } = Job.config.job;
|
|
15
|
+
this.silent = this.options.silent !== undefined ? Parser.boolean(this.options.silent) : silent;
|
|
16
|
+
this.safeMode = this.options.safeMode !== undefined ? Parser.boolean(this.options.safeMode) : safeMode;
|
|
17
|
+
this.log('Run job', this.options);
|
|
18
|
+
}
|
|
19
|
+
// --- helper ---
|
|
20
|
+
log(msg, meta, as = 'info') {
|
|
21
|
+
if (!this.silent) log[as](`[${this.job.toUpperCase()}] ${msg}`, meta);
|
|
22
|
+
}
|
|
23
|
+
err(err, msg) {
|
|
24
|
+
if (!this.silent) log.errMsg(err, msg ? `[${this.job.toUpperCase()}] ${msg}` : undefined);
|
|
25
|
+
}
|
|
26
|
+
async protect(fn) {
|
|
27
|
+
try {
|
|
28
|
+
const res = await Utils.measure(fn);
|
|
29
|
+
this.log(`Finished in ${(res.ms / 1000).toFixed(3)} sec`);
|
|
30
|
+
return res.result;
|
|
31
|
+
} catch (err) {
|
|
32
|
+
this.err(err);
|
|
33
|
+
if (!this.safeMode) throw err;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// --- getter ---
|
|
37
|
+
getJobName() {
|
|
38
|
+
return this.job;
|
|
39
|
+
}
|
|
40
|
+
getOptions() {
|
|
41
|
+
return this.options;
|
|
42
|
+
}
|
|
43
|
+
isSilent() {
|
|
44
|
+
return this.silent;
|
|
45
|
+
}
|
|
46
|
+
isSafeMode() {
|
|
47
|
+
return this.safeMode;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { TSnapshot } from '@rtbnext/schema/src/base/generic';
|
|
2
|
+
import type { ISnapshot } from '../interfaces/snapshot';
|
|
3
|
+
export declare abstract class Snapshot<T extends TSnapshot> implements ISnapshot<T> {
|
|
4
|
+
protected static readonly storage: import('../interfaces/storage').IStorage;
|
|
5
|
+
protected readonly path: string;
|
|
6
|
+
protected dates: string[];
|
|
7
|
+
protected constructor(path: string);
|
|
8
|
+
protected scanDates(): string[];
|
|
9
|
+
protected datedPath(date: string): string;
|
|
10
|
+
getDates(): string[];
|
|
11
|
+
hasDate(dateLike: string): boolean;
|
|
12
|
+
firstDate(): string | undefined;
|
|
13
|
+
latestDate(): string | undefined;
|
|
14
|
+
nearestDate(dateLike: string): string | undefined;
|
|
15
|
+
datesInRange(from: string, to: string): string[];
|
|
16
|
+
datesInYear(year: string | number): string[];
|
|
17
|
+
firstInYear(year: string | number): string | undefined;
|
|
18
|
+
latestInYear(year: string | number): string | undefined;
|
|
19
|
+
getSnapshot(dateLike: string, exactMatch?: boolean): T | false;
|
|
20
|
+
getLatest(): T | false;
|
|
21
|
+
saveSnapshot(snapshot: T, force?: boolean): boolean;
|
|
22
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { log } from '../core/Logger.js';
|
|
3
|
+
import { Storage } from '../core/Storage.js';
|
|
4
|
+
import { Utils } from '../core/Utils.js';
|
|
5
|
+
import { Parser } from '../parser/Parser.js';
|
|
6
|
+
export class Snapshot {
|
|
7
|
+
static storage = Storage.getInstance();
|
|
8
|
+
path;
|
|
9
|
+
dates;
|
|
10
|
+
constructor(path) {
|
|
11
|
+
this.path = path;
|
|
12
|
+
Snapshot.storage.ensurePath(this.path, true);
|
|
13
|
+
this.dates = this.scanDates();
|
|
14
|
+
}
|
|
15
|
+
// --- helper ---
|
|
16
|
+
scanDates() {
|
|
17
|
+
return Utils.sort(Snapshot.storage.scanDir(this.path));
|
|
18
|
+
}
|
|
19
|
+
datedPath(date) {
|
|
20
|
+
return join(this.path, `${date}`);
|
|
21
|
+
}
|
|
22
|
+
// --- basic getter ---
|
|
23
|
+
getDates() {
|
|
24
|
+
return this.dates;
|
|
25
|
+
}
|
|
26
|
+
hasDate(dateLike) {
|
|
27
|
+
return this.dates.includes(Parser.date(dateLike, 'ymd'));
|
|
28
|
+
}
|
|
29
|
+
firstDate() {
|
|
30
|
+
return this.dates[0];
|
|
31
|
+
}
|
|
32
|
+
latestDate() {
|
|
33
|
+
return this.dates.at(-1);
|
|
34
|
+
}
|
|
35
|
+
// --- special getter ---
|
|
36
|
+
nearestDate(dateLike) {
|
|
37
|
+
const target = Parser.date(dateLike);
|
|
38
|
+
return this.dates.slice().reduce((nearest, date) => (date > target ? nearest : date));
|
|
39
|
+
}
|
|
40
|
+
datesInRange(from, to) {
|
|
41
|
+
const fromDate = Parser.date(from),
|
|
42
|
+
toDate = Parser.date(to);
|
|
43
|
+
return this.dates.filter(date => date >= fromDate && date <= toDate);
|
|
44
|
+
}
|
|
45
|
+
datesInYear(year) {
|
|
46
|
+
return this.datesInRange(`${year}-01-01`, `${year}-12-31`);
|
|
47
|
+
}
|
|
48
|
+
firstInYear(year) {
|
|
49
|
+
const target = Parser.string(year);
|
|
50
|
+
return this.dates.find(date => date.substring(0, 4) === target);
|
|
51
|
+
}
|
|
52
|
+
latestInYear(year) {
|
|
53
|
+
const target = Parser.string(year);
|
|
54
|
+
return this.dates.filter(date => date.substring(0, 4) === target).at(-1);
|
|
55
|
+
}
|
|
56
|
+
// --- get snapshot data ---
|
|
57
|
+
getSnapshot(dateLike, exactMatch = true) {
|
|
58
|
+
const target = Parser.date(dateLike);
|
|
59
|
+
const date = this.hasDate(target) ? target : exactMatch ? undefined : this.nearestDate(target);
|
|
60
|
+
return date ? Snapshot.storage.readJSON(this.datedPath(date)) : false;
|
|
61
|
+
}
|
|
62
|
+
getLatest() {
|
|
63
|
+
return this.dates.length ? this.getSnapshot(this.latestDate()) : false;
|
|
64
|
+
}
|
|
65
|
+
// --- save snapshot ---
|
|
66
|
+
saveSnapshot(snapshot, force = false) {
|
|
67
|
+
log.debug(`Saving snapshot for date ${snapshot.date}`);
|
|
68
|
+
return (
|
|
69
|
+
log.catch(() => {
|
|
70
|
+
if (!force && this.hasDate(snapshot.date)) throw new Error(`Snapshot for date ${snapshot.date} already exists`);
|
|
71
|
+
const path = this.datedPath(snapshot.date);
|
|
72
|
+
if (!Snapshot.storage.writeJSON(path, snapshot)) throw new Error(`Failed to write snapshot to ${path}`);
|
|
73
|
+
this.dates = this.scanDates();
|
|
74
|
+
return true;
|
|
75
|
+
}, `Failed to save snapshot for date ${snapshot.date}`) ?? false
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
package/dist/bin/cli.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { JOBS } from '../job/index.js';
|
|
4
|
+
const addGlobalOptions = command =>
|
|
5
|
+
void command
|
|
6
|
+
.optionsGroup('Global Options:')
|
|
7
|
+
.option('--silent', 'Disable logging')
|
|
8
|
+
.option('--safe-mode', 'Enable safe mode');
|
|
9
|
+
const program = new Command();
|
|
10
|
+
program
|
|
11
|
+
.name('ntbnext-cli')
|
|
12
|
+
.description('CLI to maintain the @rtbnext billionaires database')
|
|
13
|
+
.version('2.0.0', '-v, --version');
|
|
14
|
+
for (const JobClass of JOBS) {
|
|
15
|
+
const { id, desc, options } = JobClass.command;
|
|
16
|
+
const command = program.command(id).description(desc);
|
|
17
|
+
for (const { name, desc, parser, required } of options ?? []) {
|
|
18
|
+
const fn = required ? command.requiredOption.bind(command) : command.option.bind(command);
|
|
19
|
+
if (parser) fn(name, desc, parser);
|
|
20
|
+
else fn(name, desc);
|
|
21
|
+
}
|
|
22
|
+
addGlobalOptions(command);
|
|
23
|
+
command.action(async options => await new JobClass(options).run());
|
|
24
|
+
}
|
|
25
|
+
await program.parseAsync();
|
package/dist/bin/cron.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { IConfig } from '../interfaces/config';
|
|
2
|
+
import type {
|
|
3
|
+
TConfigObject,
|
|
4
|
+
TCronConfig,
|
|
5
|
+
TFetchConfig,
|
|
6
|
+
TJobConfig,
|
|
7
|
+
TLoggingConfig,
|
|
8
|
+
TQueueConfig,
|
|
9
|
+
TStorageConfig
|
|
10
|
+
} from '../types/config';
|
|
11
|
+
export declare class Config implements IConfig {
|
|
12
|
+
private static instance;
|
|
13
|
+
private readonly cwd;
|
|
14
|
+
private readonly path;
|
|
15
|
+
private readonly env;
|
|
16
|
+
private readonly cfg;
|
|
17
|
+
private constructor();
|
|
18
|
+
private loadConfigFile;
|
|
19
|
+
private loadConfig;
|
|
20
|
+
get root(): string;
|
|
21
|
+
get environment(): string;
|
|
22
|
+
get config(): TConfigObject;
|
|
23
|
+
get logging(): TLoggingConfig;
|
|
24
|
+
get job(): TJobConfig;
|
|
25
|
+
get cron(): TCronConfig;
|
|
26
|
+
get storage(): TStorageConfig;
|
|
27
|
+
get fetch(): TFetchConfig;
|
|
28
|
+
get queue(): TQueueConfig;
|
|
29
|
+
static getInstance(): IConfig;
|
|
30
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import process, { cwd } from 'node:process';
|
|
4
|
+
import { parse } from 'yaml';
|
|
5
|
+
import { Utils } from './Utils.js';
|
|
6
|
+
export class Config {
|
|
7
|
+
static instance;
|
|
8
|
+
cwd;
|
|
9
|
+
path;
|
|
10
|
+
env;
|
|
11
|
+
cfg;
|
|
12
|
+
constructor() {
|
|
13
|
+
this.cwd = cwd();
|
|
14
|
+
this.path = join(this.cwd, 'config');
|
|
15
|
+
this.env = process.env.NODE_ENV ?? 'production';
|
|
16
|
+
this.cfg = this.loadConfig();
|
|
17
|
+
}
|
|
18
|
+
// --- config loader ---
|
|
19
|
+
loadConfigFile(path) {
|
|
20
|
+
if (!existsSync((path = join(this.path, path)))) return {};
|
|
21
|
+
try {
|
|
22
|
+
return parse(readFileSync(path, 'utf8'));
|
|
23
|
+
} catch {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
loadConfig() {
|
|
28
|
+
return Utils.merge(
|
|
29
|
+
'replace' /* ArrayMode.Replace */,
|
|
30
|
+
this.loadConfigFile('default.yml'),
|
|
31
|
+
this.loadConfigFile(`${this.env}.yml`)
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
// --- public getter ---
|
|
35
|
+
get root() {
|
|
36
|
+
return this.cwd;
|
|
37
|
+
}
|
|
38
|
+
get environment() {
|
|
39
|
+
return this.env;
|
|
40
|
+
}
|
|
41
|
+
get config() {
|
|
42
|
+
return this.cfg;
|
|
43
|
+
}
|
|
44
|
+
get logging() {
|
|
45
|
+
return this.cfg.logging;
|
|
46
|
+
}
|
|
47
|
+
get job() {
|
|
48
|
+
return this.cfg.job;
|
|
49
|
+
}
|
|
50
|
+
get cron() {
|
|
51
|
+
return this.cfg.cron;
|
|
52
|
+
}
|
|
53
|
+
get storage() {
|
|
54
|
+
return this.cfg.storage;
|
|
55
|
+
}
|
|
56
|
+
get fetch() {
|
|
57
|
+
return this.cfg.fetch;
|
|
58
|
+
}
|
|
59
|
+
get queue() {
|
|
60
|
+
return this.cfg.queue;
|
|
61
|
+
}
|
|
62
|
+
// --- instantiate ---
|
|
63
|
+
static getInstance() {
|
|
64
|
+
return (Config.instance ??= new Config());
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ICron } from '../interfaces/cron';
|
|
2
|
+
export declare class Cron implements ICron {
|
|
3
|
+
private static readonly storage;
|
|
4
|
+
private static instance;
|
|
5
|
+
private readonly config;
|
|
6
|
+
private constructor();
|
|
7
|
+
private getLastRun;
|
|
8
|
+
private setLastRun;
|
|
9
|
+
private ensureLastRun;
|
|
10
|
+
run(): Promise<void>;
|
|
11
|
+
static getInstance(): ICron;
|
|
12
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { prev } from 'nxtcron';
|
|
2
|
+
import { Config } from './Config.js';
|
|
3
|
+
import { log } from './Logger.js';
|
|
4
|
+
import { Storage } from './Storage.js';
|
|
5
|
+
import { JOBS } from '../job/index.js';
|
|
6
|
+
export class Cron {
|
|
7
|
+
static storage = Storage.getInstance();
|
|
8
|
+
static instance;
|
|
9
|
+
config;
|
|
10
|
+
constructor() {
|
|
11
|
+
this.config = Config.getInstance().cron;
|
|
12
|
+
}
|
|
13
|
+
// --- helper ---
|
|
14
|
+
getLastRun() {
|
|
15
|
+
const lastRun = Cron.storage.readJSON('cron.json');
|
|
16
|
+
return lastRun && lastRun.lastRun ? new Date(lastRun.lastRun) : undefined;
|
|
17
|
+
}
|
|
18
|
+
setLastRun(date) {
|
|
19
|
+
return Cron.storage.writeJSON('cron.json', { lastRun: date.toISOString() });
|
|
20
|
+
}
|
|
21
|
+
ensureLastRun() {
|
|
22
|
+
const lastRun = this.getLastRun();
|
|
23
|
+
if (lastRun instanceof Date && !Number.isNaN(lastRun.getTime())) return lastRun;
|
|
24
|
+
this.setLastRun(new Date());
|
|
25
|
+
throw new Error('Initial Cron job run - will not execute any scheduled events');
|
|
26
|
+
}
|
|
27
|
+
// --- cron runner ---
|
|
28
|
+
async run() {
|
|
29
|
+
await log.catchAsync(async () => {
|
|
30
|
+
log.debug('Run scheduled Cron jobs ...');
|
|
31
|
+
const after = this.ensureLastRun(),
|
|
32
|
+
before = new Date();
|
|
33
|
+
const cronOptions = { timezone: this.config.timezone, count: 1, after, before };
|
|
34
|
+
for (const JobClass of JOBS) {
|
|
35
|
+
if (!('cron' in JobClass)) continue;
|
|
36
|
+
for (const { cronexpr, options } of JobClass.cron) {
|
|
37
|
+
const [date] = prev(cronexpr, cronOptions);
|
|
38
|
+
if (date === undefined || !(date instanceof Date)) continue;
|
|
39
|
+
log.info(`[CRON] Run Cron job ${JobClass.command.id} scheduled @ ${date.toISOString()}`);
|
|
40
|
+
await new JobClass(options?.(date) ?? {}).run();
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
log.debug(`Shut down Cron job runner, set last run time to ${before.toISOString()}`);
|
|
45
|
+
this.setLastRun(before);
|
|
46
|
+
}, 'Failed to run cron jobs');
|
|
47
|
+
}
|
|
48
|
+
// --- instantiate ---
|
|
49
|
+
static getInstance() {
|
|
50
|
+
return (Cron.instance ??= new Cron());
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { IFetch } from '../interfaces/fetch';
|
|
2
|
+
import type { TFetchMethod, THeader } from '../types/fetch';
|
|
3
|
+
import type { TListResponse, TProfileResponse, TResponse } from '../types/response';
|
|
4
|
+
export declare class Fetch implements IFetch {
|
|
5
|
+
private static instance;
|
|
6
|
+
private readonly config;
|
|
7
|
+
private readonly wikiQuery;
|
|
8
|
+
private lastRequest;
|
|
9
|
+
private httpClient;
|
|
10
|
+
private constructor();
|
|
11
|
+
private setupHttpClient;
|
|
12
|
+
private getRandomDelay;
|
|
13
|
+
private applyRateLimit;
|
|
14
|
+
private useApiUserAgent;
|
|
15
|
+
private useRandomUserAgent;
|
|
16
|
+
private prepQuery;
|
|
17
|
+
private retErr;
|
|
18
|
+
private fetch;
|
|
19
|
+
single<T>(url: string, method?: TFetchMethod, header?: THeader): Promise<TResponse<T>>;
|
|
20
|
+
batch<T>(urls: string[], method?: TFetchMethod, header?: THeader): Promise<TResponse<T>[]>;
|
|
21
|
+
wayback<T>(url: string, ts: unknown): Promise<TResponse<T>>;
|
|
22
|
+
list<T extends object>(uriLike: string, year: string): Promise<TResponse<TListResponse<T>>>;
|
|
23
|
+
profile(...uriLike: string[]): Promise<TResponse<TProfileResponse>[]>;
|
|
24
|
+
wikidata<T>(sparql: string): Promise<TResponse<T>>;
|
|
25
|
+
wikipedia<T>(query: Record<string, unknown>, lang?: string): Promise<TResponse<T>>;
|
|
26
|
+
commons<T>(query: Record<string, unknown>): Promise<TResponse<T>>;
|
|
27
|
+
static getInstance(): IFetch;
|
|
28
|
+
}
|