@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.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +11 -0
  3. package/dist/abstract/Cache.d.ts +9 -0
  4. package/dist/abstract/Cache.js +19 -0
  5. package/dist/abstract/Index.d.ts +22 -0
  6. package/dist/abstract/Index.js +81 -0
  7. package/dist/abstract/Job.d.ts +19 -0
  8. package/dist/abstract/Job.js +49 -0
  9. package/dist/abstract/Snapshot.d.ts +22 -0
  10. package/dist/abstract/Snapshot.js +78 -0
  11. package/dist/bin/cli.d.ts +2 -0
  12. package/dist/bin/cli.js +25 -0
  13. package/dist/bin/cron.d.ts +2 -0
  14. package/dist/bin/cron.js +4 -0
  15. package/dist/core/Config.d.ts +30 -0
  16. package/dist/core/Config.js +66 -0
  17. package/dist/core/Cron.d.ts +12 -0
  18. package/dist/core/Cron.js +52 -0
  19. package/dist/core/Fetch.d.ts +28 -0
  20. package/dist/core/Fetch.js +172 -0
  21. package/dist/core/Logger.d.ts +30 -0
  22. package/dist/core/Logger.js +92 -0
  23. package/dist/core/Queue.d.ts +37 -0
  24. package/dist/core/Queue.js +136 -0
  25. package/dist/core/Storage.d.ts +28 -0
  26. package/dist/core/Storage.js +166 -0
  27. package/dist/core/Utils.d.ts +33 -0
  28. package/dist/core/Utils.js +167 -0
  29. package/dist/interfaces/cache.d.ts +6 -0
  30. package/dist/interfaces/config.d.ts +21 -0
  31. package/dist/interfaces/cron.d.ts +3 -0
  32. package/dist/interfaces/fetch.d.ts +13 -0
  33. package/dist/interfaces/filter.d.ts +12 -0
  34. package/dist/interfaces/index.d.ts +30 -0
  35. package/dist/interfaces/job.d.ts +9 -0
  36. package/dist/interfaces/list.d.ts +9 -0
  37. package/dist/interfaces/logger.d.ts +20 -0
  38. package/dist/interfaces/mover.d.ts +7 -0
  39. package/dist/interfaces/parser.d.ts +68 -0
  40. package/dist/interfaces/profile.d.ts +30 -0
  41. package/dist/interfaces/queue.d.ts +17 -0
  42. package/dist/interfaces/snapshot.d.ts +16 -0
  43. package/dist/interfaces/stats.d.ts +45 -0
  44. package/dist/interfaces/storage.d.ts +16 -0
  45. package/dist/job/Alias.d.ts +8 -0
  46. package/dist/job/Alias.js +42 -0
  47. package/dist/job/Annual.d.ts +8 -0
  48. package/dist/job/Annual.js +41 -0
  49. package/dist/job/List.d.ts +11 -0
  50. package/dist/job/List.js +101 -0
  51. package/dist/job/Merge.d.ts +10 -0
  52. package/dist/job/Merge.js +59 -0
  53. package/dist/job/Move.d.ts +7 -0
  54. package/dist/job/Move.js +33 -0
  55. package/dist/job/Performance.d.ts +8 -0
  56. package/dist/job/Performance.js +27 -0
  57. package/dist/job/Profile.d.ts +11 -0
  58. package/dist/job/Profile.js +76 -0
  59. package/dist/job/Queue.d.ts +8 -0
  60. package/dist/job/Queue.js +54 -0
  61. package/dist/job/RTB.d.ts +12 -0
  62. package/dist/job/RTB.js +121 -0
  63. package/dist/job/Stats.d.ts +11 -0
  64. package/dist/job/Stats.js +46 -0
  65. package/dist/job/Top10.d.ts +9 -0
  66. package/dist/job/Top10.js +48 -0
  67. package/dist/job/Wiki.d.ts +9 -0
  68. package/dist/job/Wiki.js +40 -0
  69. package/dist/job/index.d.ts +26 -0
  70. package/dist/job/index.js +26 -0
  71. package/dist/lib/const.d.ts +31 -0
  72. package/dist/lib/const.js +74 -0
  73. package/dist/lib/list.d.ts +90 -0
  74. package/dist/lib/list.js +72 -0
  75. package/dist/lib/regex.d.ts +7 -0
  76. package/dist/lib/regex.js +7 -0
  77. package/dist/model/Filter.d.ts +28 -0
  78. package/dist/model/Filter.js +122 -0
  79. package/dist/model/List.d.ts +12 -0
  80. package/dist/model/List.js +43 -0
  81. package/dist/model/ListIndex.d.ts +8 -0
  82. package/dist/model/ListIndex.js +10 -0
  83. package/dist/model/Mover.d.ts +15 -0
  84. package/dist/model/Mover.js +74 -0
  85. package/dist/model/Profile.d.ts +49 -0
  86. package/dist/model/Profile.js +181 -0
  87. package/dist/model/ProfileIndex.d.ts +20 -0
  88. package/dist/model/ProfileIndex.js +140 -0
  89. package/dist/model/Stats.d.ts +56 -0
  90. package/dist/model/Stats.js +435 -0
  91. package/dist/parser/BillionairesListParser.d.ts +3 -0
  92. package/dist/parser/BillionairesListParser.js +2 -0
  93. package/dist/parser/ListParser.d.ts +7 -0
  94. package/dist/parser/ListParser.js +11 -0
  95. package/dist/parser/Parser.d.ts +43 -0
  96. package/dist/parser/Parser.js +146 -0
  97. package/dist/parser/PersonListParser.d.ts +29 -0
  98. package/dist/parser/PersonListParser.js +111 -0
  99. package/dist/parser/ProfileParser.d.ts +44 -0
  100. package/dist/parser/ProfileParser.js +193 -0
  101. package/dist/parser/RTBListParser.d.ts +15 -0
  102. package/dist/parser/RTBListParser.js +91 -0
  103. package/dist/types/annual.d.ts +7 -0
  104. package/dist/types/config.d.ts +35 -0
  105. package/dist/types/fetch.d.ts +3 -0
  106. package/dist/types/generic.d.ts +10 -0
  107. package/dist/types/job.d.ts +71 -0
  108. package/dist/types/list.d.ts +49 -0
  109. package/dist/types/parser.d.ts +7 -0
  110. package/dist/types/profile.d.ts +9 -0
  111. package/dist/types/queue.d.ts +15 -0
  112. package/dist/types/response.d.ts +183 -0
  113. package/dist/types/storage.d.ts +3 -0
  114. package/dist/types/wiki.d.ts +1 -0
  115. package/dist/utils/Annual.d.ts +7 -0
  116. package/dist/utils/Annual.js +99 -0
  117. package/dist/utils/Performance.d.ts +8 -0
  118. package/dist/utils/Performance.js +39 -0
  119. package/dist/utils/ProfileManager.d.ts +24 -0
  120. package/dist/utils/ProfileManager.js +60 -0
  121. package/dist/utils/ProfileMerger.d.ts +11 -0
  122. package/dist/utils/ProfileMerger.js +67 -0
  123. package/dist/utils/Ranking.d.ts +11 -0
  124. package/dist/utils/Ranking.js +77 -0
  125. package/dist/utils/Wiki.d.ts +11 -0
  126. package/dist/utils/Wiki.js +168 -0
  127. package/package.json +45 -0
@@ -0,0 +1,172 @@
1
+ import axios from 'axios';
2
+ import { Config } from './Config.js';
3
+ import { log } from './Logger.js';
4
+ import { Utils } from './Utils.js';
5
+ import { REGEX_NONUM, REGEX_SPACES } from '../lib/regex.js';
6
+ import { Parser } from '../parser/Parser.js';
7
+ export class Fetch {
8
+ static instance;
9
+ config;
10
+ wikiQuery = { format: 'json', formatversion: 2 };
11
+ lastRequest = 0;
12
+ httpClient;
13
+ constructor() {
14
+ this.config = Config.getInstance().fetch;
15
+ this.httpClient = this.setupHttpClient();
16
+ }
17
+ // --- set up axios ---
18
+ setupHttpClient() {
19
+ const {
20
+ headers,
21
+ rateLimit: { timeout }
22
+ } = this.config;
23
+ return axios.create({ headers, timeout });
24
+ }
25
+ // --- rate limit ---
26
+ getRandomDelay() {
27
+ const { max, min } = this.config.rateLimit.requestDelay;
28
+ const delay = Math.round(Math.random() * (max - min) + min);
29
+ return new Promise(resolve => setTimeout(resolve, delay));
30
+ }
31
+ async applyRateLimit(fn) {
32
+ if (Date.now() - this.lastRequest < this.config.rateLimit.idle) await this.getRandomDelay();
33
+ try {
34
+ return await fn();
35
+ } finally {
36
+ this.lastRequest = Date.now();
37
+ }
38
+ }
39
+ // --- headers ---
40
+ useApiUserAgent() {
41
+ return { 'User-Agent': this.config.apiAgent, 'Api-User-Agent': this.config.apiAgent };
42
+ }
43
+ useRandomUserAgent() {
44
+ return { 'User-Agent': this.config.agentPool[Math.floor(Math.random() * this.config.agentPool.length)] };
45
+ }
46
+ // --- helper ---
47
+ prepQuery(url, replacements) {
48
+ return Object.entries(replacements).reduce((acc, [key, value]) => {
49
+ return acc.replaceAll(`{${key.toUpperCase()}}`, String(value));
50
+ }, url);
51
+ }
52
+ retErr(res, msg, code) {
53
+ return {
54
+ success: false,
55
+ statusCode: code ?? res.statusCode ?? 500,
56
+ error: msg ?? res.error,
57
+ duration: res.duration,
58
+ retries: res.retries
59
+ };
60
+ }
61
+ async fetch(url, method = 'get', headers) {
62
+ log.debug(`Fetching URL: ${url} via ${method.toUpperCase()}`);
63
+ headers = { ...this.config.headers, ...headers };
64
+ const { result: res, ms } = await Utils.measure(async () => {
65
+ let res;
66
+ let retries = 0;
67
+ do {
68
+ res = await this.applyRateLimit(() => this.httpClient[method](url, { headers }));
69
+ if (res.status === 200 && res.data) break;
70
+ log.warn(`Request failed with status: ${res.status}. Retrying ...`);
71
+ } while (++retries < this.config.rateLimit.retries);
72
+ return { ...res, retries };
73
+ });
74
+ log.info(`Fetched URL: ${url} in ${ms} ms`);
75
+ return Object.assign(
76
+ { duration: ms, retries: res.retries },
77
+ res.status === 200 && res.data
78
+ ? { success: true, data: res.data }
79
+ : { success: false, error: `Invalid response status: ${res.status}`, statusCode: res.status }
80
+ );
81
+ }
82
+ // --- fetch methods ---
83
+ async single(url, method = 'get', header) {
84
+ return this.fetch(url, method, header);
85
+ }
86
+ async batch(urls, method = 'get', header) {
87
+ const results = [];
88
+ let url;
89
+ while ((url = urls.shift()) && results.length < this.config.rateLimit.batchSize)
90
+ results.push(await this.fetch(url, method, header));
91
+ if (urls.length) log.warn(`Batch limit reached: ${urls.length} URLs remaining`);
92
+ return results;
93
+ }
94
+ // --- special requests ---
95
+ async wayback(url, ts) {
96
+ const res = await this.single(
97
+ this.prepQuery(this.config.endpoints.wayback, {
98
+ url: encodeURIComponent(url),
99
+ ts: Parser.date(ts, 'ymd').replaceAll(REGEX_NONUM, '')
100
+ }),
101
+ 'get',
102
+ this.useApiUserAgent()
103
+ );
104
+ if (!res?.success || !res.data?.archived_snapshots?.closest?.available)
105
+ return this.retErr(res, 'No archived snapshot found', 404);
106
+ return this.single(
107
+ res.data.archived_snapshots.closest.url.replace('/http', 'if_/http'),
108
+ 'get',
109
+ this.useApiUserAgent()
110
+ );
111
+ }
112
+ async list(uriLike, year) {
113
+ const {
114
+ requests: {
115
+ list: { chunkSize, maxRequests }
116
+ }
117
+ } = this.config;
118
+ const uri = Utils.sanitize(uriLike),
119
+ entries = [];
120
+ let res,
121
+ count = 0,
122
+ start = 0,
123
+ requests = 0;
124
+ do {
125
+ res = await this.single(
126
+ this.prepQuery(this.config.endpoints.list, { uri, year, limit: chunkSize, start }),
127
+ 'get',
128
+ this.useRandomUserAgent()
129
+ );
130
+ if (!res.success) return this.retErr(res);
131
+ if (!res.data?.personList.count) return this.retErr(res, 'Could not fetch list data', 404);
132
+ entries.push(...res.data.personList.personsLists);
133
+ ((count = res.data.personList.count), (start += chunkSize));
134
+ } while (++requests < maxRequests && start < count);
135
+ res.data.personList.personsLists = entries;
136
+ return res;
137
+ }
138
+ async profile(...uriLike) {
139
+ return this.batch(
140
+ uriLike.map(uri => this.prepQuery(this.config.endpoints.profile, { uri: Utils.sanitize(uri) })),
141
+ 'get',
142
+ this.useRandomUserAgent()
143
+ );
144
+ }
145
+ async wikidata(sparql) {
146
+ return this.single(
147
+ this.prepQuery(this.config.endpoints.wikidata, {
148
+ sparql: encodeURIComponent(sparql.replace(REGEX_SPACES, ' ').trim())
149
+ }),
150
+ 'get',
151
+ this.useApiUserAgent()
152
+ );
153
+ }
154
+ async wikipedia(query, lang = 'en') {
155
+ return this.single(
156
+ this.prepQuery(this.config.endpoints.wikipedia, { query: Utils.queryStr({ ...this.wikiQuery, ...query }), lang }),
157
+ 'get',
158
+ this.useApiUserAgent()
159
+ );
160
+ }
161
+ async commons(query) {
162
+ return this.single(
163
+ this.prepQuery(this.config.endpoints.commons, { query: Utils.queryStr({ ...this.wikiQuery, ...query }) }),
164
+ 'get',
165
+ this.useApiUserAgent()
166
+ );
167
+ }
168
+ // --- instantiate ---
169
+ static getInstance() {
170
+ return (Fetch.instance ??= new Fetch());
171
+ }
172
+ }
@@ -0,0 +1,30 @@
1
+ import type { ILogger } from '../interfaces/logger';
2
+ import type { TLoggingLevel } from '../types/config';
3
+ export declare class Logger implements ILogger {
4
+ private static instance;
5
+ private static readonly LEVEL;
6
+ private readonly config;
7
+ private readonly path;
8
+ private constructor();
9
+ private shouldLog;
10
+ private format;
11
+ private log2Console;
12
+ private log2File;
13
+ private log;
14
+ error(msg: string, err?: Error): void;
15
+ errMsg(err: unknown, msg?: string): void;
16
+ exit(msg: string, err?: Error): never;
17
+ warn(msg: string, meta?: unknown): void;
18
+ info(msg: string, meta?: unknown): void;
19
+ debug(msg: string, meta?: unknown): void;
20
+ catch<F extends (...args: any[]) => any, R = ReturnType<F>>(fn: F, msg: string, level?: TLoggingLevel): R | undefined;
21
+ catchAsync<F extends (...args: any[]) => Promise<any>, R = Awaited<ReturnType<F>>>(
22
+ fn: F,
23
+ msg: string,
24
+ level?: TLoggingLevel
25
+ ): Promise<R | undefined>;
26
+ getLogFile(date: string): string | undefined;
27
+ getCurrentLogFile(): string | undefined;
28
+ static getInstance(): ILogger;
29
+ }
30
+ export declare const log: ILogger;
@@ -0,0 +1,92 @@
1
+ import { appendFileSync, mkdirSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { exit } from 'node:process';
4
+ import { Config } from './Config.js';
5
+ import { Utils } from './Utils.js';
6
+ export class Logger {
7
+ static instance;
8
+ static LEVEL = { error: 0, warn: 1, info: 2, debug: 3 };
9
+ config;
10
+ path;
11
+ constructor() {
12
+ const { root, logging } = Config.getInstance();
13
+ this.config = logging;
14
+ this.path = join(root, 'logs');
15
+ mkdirSync(this.path, { recursive: true });
16
+ }
17
+ // --- helper ---
18
+ shouldLog(level) {
19
+ return Logger.LEVEL[level] <= Logger.LEVEL[this.config.level];
20
+ }
21
+ format(level, msg, meta) {
22
+ let entry = `[${Utils.date('iso')}] [${level.toUpperCase()}] ${msg}`;
23
+ if (meta instanceof Error) entry += `: ${meta.message}`;
24
+ else if (meta) entry += `: ${JSON.stringify(meta)}`;
25
+ return entry;
26
+ }
27
+ log2Console(level, entry) {
28
+ (console[level] ?? console.log)(entry);
29
+ }
30
+ log2File(entry) {
31
+ const path = join(this.path, `${Utils.date('ym')}.log`);
32
+ appendFileSync(path, entry + '\n', 'utf8');
33
+ }
34
+ log(level, msg, meta) {
35
+ if (!this.shouldLog(level)) return;
36
+ const entry = this.format(level, msg, meta);
37
+ if (this.config.console) this.log2Console(level, entry);
38
+ if (this.config.file) this.log2File(entry);
39
+ }
40
+ // --- logging methods ---
41
+ error(msg, err) {
42
+ this.log('error', msg, err);
43
+ }
44
+ errMsg(err, msg) {
45
+ const message = err instanceof Error ? err.message : String(err);
46
+ this.log('error', (msg ? `${msg}: ` : '') + message, err);
47
+ }
48
+ exit(msg, err) {
49
+ this.log('error', msg, err);
50
+ exit(1);
51
+ }
52
+ warn(msg, meta) {
53
+ this.log('warn', msg, meta);
54
+ }
55
+ info(msg, meta) {
56
+ this.log('info', msg, meta);
57
+ }
58
+ debug(msg, meta) {
59
+ this.log('debug', msg, meta);
60
+ }
61
+ // --- catch & log errors ---
62
+ catch(fn, msg, level = 'error') {
63
+ try {
64
+ return fn();
65
+ } catch (err) {
66
+ this.log(level, msg, err);
67
+ }
68
+ }
69
+ async catchAsync(fn, msg, level = 'error') {
70
+ try {
71
+ return await fn();
72
+ } catch (err) {
73
+ this.log(level, msg, err);
74
+ }
75
+ }
76
+ // --- get log file ---
77
+ getLogFile(date) {
78
+ return this.catch(
79
+ () => readFileSync(join(this.path, `${date}.log`), 'utf8'),
80
+ `Could not read log file for date ${date}`
81
+ );
82
+ }
83
+ getCurrentLogFile() {
84
+ return this.getLogFile(Utils.date('ym'));
85
+ }
86
+ // --- instantiate ---
87
+ static getInstance() {
88
+ return (Logger.instance ??= new Logger());
89
+ }
90
+ }
91
+ // --- singleton instance ---
92
+ export const log = Logger.getInstance();
@@ -0,0 +1,37 @@
1
+ import type { IQueue } from '../interfaces/queue';
2
+ import type { TQueueConfig } from '../types/config';
3
+ import type { TQueue, TQueueItem, TQueueOptions, TQueueType } from '../types/queue';
4
+ export declare abstract class Queue implements IQueue {
5
+ protected static readonly storage: import('../interfaces/storage').IStorage;
6
+ protected readonly config: TQueueConfig;
7
+ protected readonly type: TQueueType;
8
+ protected readonly path: string;
9
+ protected queue: TQueue;
10
+ protected constructor(type: TQueueType);
11
+ protected loadQueue(): TQueue;
12
+ protected saveQueue(): void;
13
+ protected key(uri: string, args?: unknown): string;
14
+ get size(): number;
15
+ getQueue(): TQueueItem[];
16
+ getByKey(key: string): TQueueItem | undefined;
17
+ hasKey(key: string): boolean;
18
+ getByUri(uriLike: string): TQueueItem[];
19
+ hasUri(uriLike: string): boolean;
20
+ clear(): void;
21
+ add(opt: TQueueOptions, save?: boolean): boolean;
22
+ addMany(items: TQueueOptions[]): number;
23
+ removeByKey(key: string): boolean;
24
+ remove(...uriLike: string[]): number;
25
+ next(n?: number): TQueueItem[];
26
+ nextUri(n?: number): string[];
27
+ }
28
+ export declare class ProfileQueue extends Queue implements IQueue {
29
+ private static instance;
30
+ private constructor();
31
+ static getInstance(): ProfileQueue;
32
+ }
33
+ export declare class ListQueue extends Queue implements IQueue {
34
+ private static instance;
35
+ private constructor();
36
+ static getInstance(): ListQueue;
37
+ }
@@ -0,0 +1,136 @@
1
+ import { sha256 } from 'js-sha256';
2
+ import { Config } from './Config.js';
3
+ import { log } from './Logger.js';
4
+ import { Storage } from './Storage.js';
5
+ import { Utils } from './Utils.js';
6
+ export class Queue {
7
+ static storage = Storage.getInstance();
8
+ config;
9
+ type;
10
+ path;
11
+ queue;
12
+ constructor(type) {
13
+ this.config = Config.getInstance().queue;
14
+ this.type = type;
15
+ this.path = `queue/${this.type}.json`;
16
+ Queue.storage.ensurePath(this.path);
17
+ this.queue = this.loadQueue();
18
+ }
19
+ // --- helper ---
20
+ loadQueue() {
21
+ log.debug(`Load queue [${this.type}]`);
22
+ return new Map((Queue.storage.readJSON(this.path) || []).map(i => [i.key, i]));
23
+ }
24
+ saveQueue() {
25
+ const { defaultPrio = 0 } = this.config;
26
+ Queue.storage.writeJSON(
27
+ this.path,
28
+ [...this.queue.values()].sort(
29
+ (a, b) =>
30
+ (b.prio ?? defaultPrio) - (a.prio ?? defaultPrio) || new Date(a.ts).getTime() - new Date(b.ts).getTime()
31
+ )
32
+ );
33
+ }
34
+ key(uri, args) {
35
+ return sha256(uri + JSON.stringify(args));
36
+ }
37
+ // --- basic operations ---
38
+ get size() {
39
+ return this.queue.size;
40
+ }
41
+ getQueue() {
42
+ return [...this.queue.values()];
43
+ }
44
+ getByKey(key) {
45
+ return this.queue.get(key);
46
+ }
47
+ hasKey(key) {
48
+ return this.queue.has(key);
49
+ }
50
+ getByUri(uriLike) {
51
+ const uri = Utils.sanitize(uriLike);
52
+ return [...this.queue.values()].filter(i => i.uri === uri);
53
+ }
54
+ hasUri(uriLike) {
55
+ return this.getByUri(uriLike).length !== 0;
56
+ }
57
+ clear() {
58
+ log.debug(`Clear queue [${this.type}]`);
59
+ this.queue.clear();
60
+ this.saveQueue();
61
+ }
62
+ // --- add items ---
63
+ add(opt, save = true) {
64
+ const { uriLike, args, prio } = opt;
65
+ return (
66
+ log.catch(() => {
67
+ if (this.queue.size > this.config.maxSize) throw new Error(`Queue size limit reached for type: ${this.type}`);
68
+ const uri = Utils.sanitize(uriLike);
69
+ const key = this.key(uri, args);
70
+ const item = this.queue.get(key);
71
+ const ts = item?.ts ?? Utils.date('iso');
72
+ const data = { key, uri, ts, args, prio };
73
+ if (JSON.stringify(item) === JSON.stringify(data)) return false;
74
+ log.debug(`Add to queue [${this.type}]: ${uri}`, data);
75
+ this.queue.set(key, data);
76
+ if (save) this.saveQueue();
77
+ return true;
78
+ }, `Failed to add item to queue [${this.type}]: ${uriLike}`) ?? false
79
+ );
80
+ }
81
+ addMany(items) {
82
+ const added = items.reduce((acc, item) => acc + +this.add(item, false), 0);
83
+ this.saveQueue();
84
+ return added;
85
+ }
86
+ // --- remove items ---
87
+ removeByKey(key) {
88
+ return (
89
+ this.queue.delete(key) && (log.debug(`Remove from queue [${this.type}] by key: ${key}`), this.saveQueue(), true)
90
+ );
91
+ }
92
+ remove(...uriLike) {
93
+ const keys = [...new Set(uriLike.map(uri => this.getByUri(uri).map(i => i.key)).flat())];
94
+ if (keys && keys.length) {
95
+ keys.forEach(this.queue.delete.bind(this.queue));
96
+ log.debug(`Remove from queue [${this.type}] by URI(s): ${uriLike}`, keys);
97
+ this.saveQueue();
98
+ }
99
+ return keys.length;
100
+ }
101
+ // --- get items from queue (processing) ---
102
+ next(n = 1) {
103
+ const items = [];
104
+ for (const [k, item] of this.queue)
105
+ if (items.length < n) {
106
+ items.push(item);
107
+ this.queue.delete(k);
108
+ } else break;
109
+ this.saveQueue();
110
+ log.debug(`Process ${items.length} item(s) from queue [${this.type}]`, items);
111
+ return items;
112
+ }
113
+ nextUri(n = 1) {
114
+ return this.next(n)
115
+ .filter(Boolean)
116
+ .map(i => i.uri);
117
+ }
118
+ }
119
+ export class ProfileQueue extends Queue {
120
+ static instance;
121
+ constructor() {
122
+ super('profile');
123
+ }
124
+ static getInstance() {
125
+ return (this.instance ??= new ProfileQueue());
126
+ }
127
+ }
128
+ export class ListQueue extends Queue {
129
+ static instance;
130
+ constructor() {
131
+ super('list');
132
+ }
133
+ static getInstance() {
134
+ return (this.instance ??= new ListQueue());
135
+ }
136
+ }
@@ -0,0 +1,28 @@
1
+ import { type Stats } from 'node:fs';
2
+ import type { IStorage } from '../interfaces/storage';
3
+ export declare class Storage implements IStorage {
4
+ private static instance;
5
+ private readonly config;
6
+ private readonly path;
7
+ private constructor();
8
+ private initDB;
9
+ private resolvePath;
10
+ private fileExt;
11
+ private read;
12
+ private write;
13
+ get root(): string;
14
+ exists(path: string): boolean;
15
+ assertPath(path: string): void | never;
16
+ ensurePath(path: string, isDir?: boolean): void;
17
+ stat(path: string): Stats | false;
18
+ scanDir(path: string, ext?: string[]): string[];
19
+ readJSON<T extends object>(path: string): T | false;
20
+ writeJSON<T extends object>(path: string, content: T): boolean;
21
+ readCSV<T extends any[]>(path: string): T | false;
22
+ writeCSV<T extends any[]>(path: string, content: T): boolean;
23
+ appendCSV<T extends any[]>(path: string, content: T, nl?: boolean): boolean;
24
+ datedCSV<T extends any[]>(path: string, content: T, force?: boolean): boolean;
25
+ remove(path: string, force?: boolean): boolean;
26
+ move(from: string, to: string, force?: boolean): boolean;
27
+ static getInstance(): IStorage;
28
+ }
@@ -0,0 +1,166 @@
1
+ import { parse, stringify } from 'csv-string';
2
+ import {
3
+ appendFileSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ readdirSync,
7
+ readFileSync,
8
+ renameSync,
9
+ rmSync,
10
+ statSync,
11
+ writeFileSync
12
+ } from 'node:fs';
13
+ import { dirname, extname, join } from 'node:path';
14
+ import { Config } from './Config.js';
15
+ import { log } from './Logger.js';
16
+ import { Utils } from './Utils.js';
17
+ export class Storage {
18
+ static instance;
19
+ config;
20
+ path;
21
+ constructor() {
22
+ const { root, storage } = Config.getInstance();
23
+ this.config = storage;
24
+ this.path = join(root, this.config.baseDir);
25
+ this.initDB();
26
+ }
27
+ initDB() {
28
+ log.debug(`Initializing storage at ${this.path}`);
29
+ this.ensurePath(this.path);
30
+ ['profile', 'list', 'filter', 'mover', 'stats', 'queue'].forEach(path => this.ensurePath(path, true));
31
+ }
32
+ // --- helper ---
33
+ resolvePath(path) {
34
+ return path.includes(this.path) ? path : join(this.path, path);
35
+ }
36
+ fileExt(path) {
37
+ return extname(this.resolvePath(path)).toLowerCase().slice(1);
38
+ }
39
+ read(path, type) {
40
+ return (
41
+ log.catch(() => {
42
+ this.assertPath((path = this.resolvePath(path)));
43
+ const content = readFileSync(path, 'utf8');
44
+ switch (type ?? this.fileExt(path)) {
45
+ case 'raw':
46
+ return content;
47
+ case 'json':
48
+ return JSON.parse(content);
49
+ case 'csv':
50
+ return parse(content);
51
+ }
52
+ throw new Error(`Unsupported file extension: ${extname(path)}`);
53
+ }, `Failed to read ${path}`) ?? false
54
+ );
55
+ }
56
+ write(path, content, type, options = { append: false, nl: true }) {
57
+ return (
58
+ log.catch(() => {
59
+ this.ensurePath((path = this.resolvePath(path)));
60
+ switch (type ?? this.fileExt(path)) {
61
+ case 'raw':
62
+ content = String(content);
63
+ break;
64
+ case 'json':
65
+ content = JSON.stringify(content, null, this.config.compression ? undefined : 2).trim();
66
+ break;
67
+ case 'csv':
68
+ content = stringify(content).trim();
69
+ break;
70
+ default:
71
+ throw new Error(`Unsupported file extension: ${extname(path)}`);
72
+ }
73
+ if (options.nl && !content.endsWith('\n')) content += '\n';
74
+ (options.append ? appendFileSync : writeFileSync)(path, content, 'utf8');
75
+ log.debug(`Wrote data to ${path}`, options);
76
+ return true;
77
+ }, `Failed to write ${path}`) ?? false
78
+ );
79
+ }
80
+ // --- path operations ---
81
+ get root() {
82
+ return this.path;
83
+ }
84
+ exists(path) {
85
+ return existsSync(this.resolvePath(path));
86
+ }
87
+ assertPath(path) {
88
+ if (!this.exists(path)) throw new Error(`Path ${path} does not exist`);
89
+ }
90
+ ensurePath(path, isDir = false) {
91
+ path = this.resolvePath(path);
92
+ mkdirSync(isDir ? path : dirname(path), { recursive: true });
93
+ }
94
+ stat(path) {
95
+ return (
96
+ log.catch(() => {
97
+ this.assertPath((path = this.resolvePath(path)));
98
+ return statSync(path);
99
+ }, `Failed to stat ${path}`) ?? false
100
+ );
101
+ }
102
+ // --- scan dir ---
103
+ scanDir(path, ext = ['json', 'csv']) {
104
+ return (
105
+ log.catch(() => {
106
+ this.assertPath((path = this.resolvePath(path)));
107
+ return readdirSync(path).filter(f => ext.includes(extname(f).slice(1).toLowerCase()));
108
+ }, `Failed to scan ${path}`) ?? []
109
+ );
110
+ }
111
+ // --- JSON files ---
112
+ readJSON(path) {
113
+ return this.read(path, 'json');
114
+ }
115
+ writeJSON(path, content) {
116
+ return this.write(path, Utils.sortKeysDeep(content), 'json');
117
+ }
118
+ // --- CSV files ---
119
+ readCSV(path) {
120
+ return this.read(path, 'csv');
121
+ }
122
+ writeCSV(path, content) {
123
+ return this.write(path, content, 'csv');
124
+ }
125
+ appendCSV(path, content, nl = true) {
126
+ return this.write(path, content, 'csv', { append: true, nl });
127
+ }
128
+ datedCSV(path, content, force = false) {
129
+ const raw = this.readCSV(path) || [];
130
+ const filtered = raw.filter(r => r[0] !== content[0]);
131
+ if (!force && raw.length !== filtered.length) return false;
132
+ return this.writeCSV(
133
+ path,
134
+ [...filtered, content].sort((a, b) => a[0].localeCompare(b[0]))
135
+ );
136
+ }
137
+ // --- file operations ---
138
+ remove(path, force = true) {
139
+ return (
140
+ log.catch(() => {
141
+ this.assertPath((path = this.resolvePath(path)));
142
+ rmSync(path, { recursive: true, force });
143
+ log.debug(`Removed ${path}`);
144
+ return true;
145
+ }, `Failed to remove ${path}`) ?? false
146
+ );
147
+ }
148
+ move(from, to, force = false) {
149
+ return (
150
+ log.catch(() => {
151
+ this.assertPath((from = this.resolvePath(from)));
152
+ if (this.exists((to = this.resolvePath(to)))) {
153
+ if (force) this.remove(to, true);
154
+ else throw new Error(`Destination path ${to} already exists`);
155
+ }
156
+ renameSync(from, to);
157
+ log.debug(`Moved ${from} to ${to}`);
158
+ return true;
159
+ }, `Failed to move ${from} to ${to}`) ?? false
160
+ );
161
+ }
162
+ // --- instantiate ---
163
+ static getInstance() {
164
+ return (Storage.instance ??= new Storage());
165
+ }
166
+ }
@@ -0,0 +1,33 @@
1
+ import { ArrayMode } from '@komed3/deepmerge';
2
+ import type { TMetaData } from '@rtbnext/schema/src/base/generic';
3
+ import type { ListLike } from 'devtypes/types/list';
4
+ import type { TAggregator, TMeasuredResult, TObjOperator } from '../types/generic';
5
+ import type { TParserDateType } from '../types/parser';
6
+ export declare class Utils {
7
+ private static readonly mergeInstances;
8
+ static sanitize(value: unknown, delimiter?: string): string;
9
+ static hash(value: unknown): string;
10
+ static verifyHash(value: unknown, hash: string): boolean;
11
+ static measure<F extends (...args: any[]) => any, R = Awaited<ReturnType<F>>>(fn: F): Promise<TMeasuredResult<R>>;
12
+ static date(format?: TParserDateType): string;
13
+ static lastMonthDay(month: string | number, year?: string | number): Date;
14
+ static metaData<T extends Record<string, unknown>>(obj?: T): TMetaData<T>;
15
+ static aggregate<T extends Record<PropertyKey, unknown>, K extends keyof T = keyof T, R = unknown>(
16
+ arr: readonly T[],
17
+ key: K,
18
+ aggregator?: TAggregator
19
+ ): T[K] | T[K][] | number | R | undefined;
20
+ static update(operator: TObjOperator, obj: any, path: string, n?: any): void;
21
+ static sort<L extends ListLike>(
22
+ value: L,
23
+ compare?: (a: any, b: any) => number,
24
+ objCompare?: (a: any, b: any) => number
25
+ ): L;
26
+ static sortKeysDeep<T>(value: T, exclude?: ReadonlySet<string>): T;
27
+ static merge<T>(mode: ArrayMode, ...objects: any[]): T;
28
+ static unique<T = unknown>(arr: T[]): T[];
29
+ static mergeArray<T = unknown>(target: T[], source: T[], mode?: ArrayMode): T[];
30
+ static queryStr(query: Record<string, any>): string;
31
+ static buildSearchText(value: unknown, minLength?: number): string;
32
+ static tokenSearch(text: string, tokens: string[], looseMatch?: boolean): boolean;
33
+ }