ts-server-lib 0.0.48

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 (46) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +8 -0
  3. package/db/TSJournal.d.ts +108 -0
  4. package/db/TSJournal.js +229 -0
  5. package/db/TSMongo.d.ts +103 -0
  6. package/db/TSMongo.js +516 -0
  7. package/db/TSRQW.d.ts +625 -0
  8. package/db/TSRQW.js +1204 -0
  9. package/db/TSRedis.d.ts +530 -0
  10. package/db/TSRedis.js +1368 -0
  11. package/db/TSRedisTB.d.ts +80 -0
  12. package/db/TSRedisTB.js +178 -0
  13. package/package.json +85 -0
  14. package/ussd/TSUssdMenu.d.ts +139 -0
  15. package/ussd/TSUssdMenu.js +368 -0
  16. package/ussd/TSUssdScreen.d.ts +58 -0
  17. package/ussd/TSUssdScreen.js +218 -0
  18. package/ussd/index.d.ts +3 -0
  19. package/ussd/index.js +19 -0
  20. package/ussd/providers/AfricasTalking.d.ts +3 -0
  21. package/ussd/providers/AfricasTalking.js +17 -0
  22. package/ussd/providers/AirtelDRC.d.ts +9 -0
  23. package/ussd/providers/AirtelDRC.js +31 -0
  24. package/ussd/providers/OrangeDRC.d.ts +5 -0
  25. package/ussd/providers/OrangeDRC.js +213 -0
  26. package/ussd/providers/VodacomDRC.d.ts +9 -0
  27. package/ussd/providers/VodacomDRC.js +48 -0
  28. package/ussd/providers/_.d.ts +55 -0
  29. package/ussd/providers/_.js +83 -0
  30. package/ussd/providers/index.d.ts +13 -0
  31. package/ussd/providers/index.js +56 -0
  32. package/utils/TSFifo.d.ts +109 -0
  33. package/utils/TSFifo.js +145 -0
  34. package/utils/TSFile.d.ts +36 -0
  35. package/utils/TSFile.js +244 -0
  36. package/utils/TSHash.d.ts +19 -0
  37. package/utils/TSHash.js +71 -0
  38. package/utils/TSRequest.d.ts +248 -0
  39. package/utils/TSRequest.js +689 -0
  40. package/utils/TSStub.d.ts +159 -0
  41. package/utils/TSStub.js +296 -0
  42. package/utils/abort.d.ts +18 -0
  43. package/utils/abort.js +97 -0
  44. package/utils/mime.json +11358 -0
  45. package/utils/object-keys.d.ts +39 -0
  46. package/utils/object-keys.js +52 -0
@@ -0,0 +1,248 @@
1
+ export type ITSResponse = {
2
+ data?: any;
3
+ kind: string;
4
+ status: number;
5
+ message: string;
6
+ } | void;
7
+ /** Options for {@link TSRequest.raw} / form / json / xml — `signal` aborts the in-flight HTTP request. */
8
+ export type TSRequestHttpOptions = Record<string, unknown> & {
9
+ signal?: AbortSignal;
10
+ };
11
+ /**
12
+ * Body size at or above which XML parsing is moved off the main thread.
13
+ *
14
+ * `fast-xml-parser` is synchronous and un-interruptible, so a large REST response blocks the event
15
+ * loop for the whole parse. Measured in a sports feed replay: a `matches` schedule sweep (~30 day
16
+ * schedules plus tournament schedules, one 16-tournament chunk holding 14,266 events) produced
17
+ * event-loop stalls of ~364 ms and periodic >1 s AMQP dispatch breaches on every `matches` cron minute.
18
+ * The AMQP path never had this problem because it already parses inside a TSRQW worker; only REST
19
+ * parsed inline.
20
+ *
21
+ * Below this size, offloading is a net loss: the structured-clone round-trip to a worker costs more
22
+ * than parsing a small document. Tuned to sit above ordinary API replies and below the multi-hundred-KB
23
+ * schedule documents that actually stall the loop.
24
+ *
25
+ * ─────────────────────────────────────────────────────────────────────────────────────────────────────────────
26
+ * 🔴 **128 KB -> 64 KB, 2026-08-14. The schedule documents were missing the worker by 2.8 KB.**
27
+ *
28
+ * The paragraph above predicted "multi-hundred-KB schedule documents". Measured, they are not: the largest
29
+ * inline document is **125.2 KB**, i.e. **2.8 KB under the old 128 KB cutoff**, so the exact population this
30
+ * threshold exists to offload was taking the inline path every time. The same near-miss was recorded once
31
+ * before at 130,676 bytes against 131,072 (396 bytes under) — twice is a pattern, not a coincidence: UOF
32
+ * schedule responses cluster just below 128 KB.
33
+ *
34
+ * Measured on the sports feed, 6,124 inline parses over one catalog window:
35
+ *
36
+ * mean inline document 5.4 KB -> 1.14 ms <- thousands of small entity fetches
37
+ * MAX inline document 125.2 KB -> 42 ms <- the schedule documents
38
+ * aggregate inline time 6,995 ms (~2 % duty)
39
+ *
40
+ * ⚠ **Do NOT size this from the mean.** 5.4 KB / 1.14 ms makes inline parsing look free, and that average is
41
+ * what hid the problem: it mixes two populations — many tiny documents that dominate the COUNT and a few large
42
+ * ones that dominate the COST. The relevant statistic is `inlineMaxMs` x caller concurrency.
43
+ *
44
+ * Why it mattered: the REST callers fetch N documents through one `Promise.all`, so N parses resolve
45
+ * back-to-back with no yield between them. At N=8 that is 8 x 42 ms = **336 ms** of un-yieldable main-thread
46
+ * time; observed `lagPeakMs` on those stages was 338 / 368 / 548 / 676 / 1,301 / 1,640 ms **with dispatch at
47
+ * zero**. The consuming service's background contract is a 4 ms cooperative slice, and a monolithic
48
+ * `XMLParser().parse()` cannot be sliced at all — so the only way background XML honours that contract is to
49
+ * leave this thread.
50
+ *
51
+ * 64 KB is chosen to move that population and nothing else: it is ~12x the 5.4 KB mean, so small documents keep
52
+ * the inline path where the round trip genuinely costs more than the parse. `TSRequestXml.spec.ts` already
53
+ * bounds this constant to [64 KB, 512 KB]; this sits at that floor deliberately.
54
+ *
55
+ * 🟢 **Safe for the money path by construction:** AMQP messages never reach this function — they parse inside a
56
+ * TSRQW worker pool (see the note above, and `threadFactor` in the atfeed betradar driver). Only REST does, and
57
+ * in the sports feed REST is exclusively the catalog/schedule sync, i.e. background work.
58
+ *
59
+ * Watch `offThread` count after this change: the pool is ONE worker, so if the 64-128 KB band turns out to hold
60
+ * thousands of documents rather than dozens they will serialise behind each other. That is still the right place
61
+ * for them — a queued worker does not block the loop — but it makes the catalog sweep slower, which is the
62
+ * trade being accepted here.
63
+ */
64
+ /**
65
+ * DEFAULT threshold. The authority for this number is this library's own measurement,
66
+ * `test/utils/TSRequestXmlCrossover.spec.ts`, which measures the main-thread BLOCK per mode across a size
67
+ * ladder. Change it against that spec's output, not against reasoning: it has been wrong twice before, in
68
+ * opposite directions, both times from reasoning alone.
69
+ *
70
+ * Two specs constrain it and the effective bound is their INTERSECTION, [64 KB, 128 KB] —
71
+ * `TSRequestXml.spec.ts` asserts [64 KB, 512 KB], the crossover spec asserts [32 KB, 128 KB]. 64 KB therefore
72
+ * sits exactly on the floor: it cannot be lowered by editing this line alone, which is deliberate.
73
+ *
74
+ * A consumer that wants a different value sets it at startup with {@link configureXmlParsing} instead. That
75
+ * accepts a wider range on purpose — probing is a runtime question, while this constant is a shipped default —
76
+ * and it keeps the workload that justifies a change, and the record of it, in the service that measured it.
77
+ */
78
+ export declare const XML_OFFTHREAD_MIN_BYTES: number;
79
+ /**
80
+ * Default ceiling for one off-thread parse. Generous next to a ~1 MB document (~250 ms measured), because
81
+ * firing it also retires the worker — see `TSRQWPoolOptions.taskTimeoutMs`.
82
+ */
83
+ export declare const XML_WORKER_TASK_TIMEOUT_MS = 30000;
84
+ /**
85
+ * Override XML parsing behaviour for THIS process.
86
+ *
87
+ * The defaults above stay the measured-neutral values — 64 KB is the only threshold with evidence behind it
88
+ * (see {@link XML_OFFTHREAD_MIN_BYTES}), so a service that configures nothing gets exactly what was measured.
89
+ * This exists so a service can probe a different value from its OWN startup, without a library edit, a
90
+ * version bump across every consumer, or a hand-patched `node_modules` copy — all three of which were done
91
+ * on 2026-08-16 to test a single number.
92
+ *
93
+ * ⚠ Call this at startup, BEFORE the first large document is parsed. The worker pool is created lazily on
94
+ * first use and reads `workerTaskTimeoutMs` at construction, so a later change to the timeout applies only
95
+ * if the pool is rebuilt. `offThreadMinBytes` is read per call and takes effect immediately.
96
+ *
97
+ * Throws on an out-of-range or non-integer threshold rather than clamping: this value decides which parses
98
+ * hold the event loop, and a silently corrected one would misreport what a measurement actually ran.
99
+ */
100
+ export declare function configureXmlParsing(opts: {
101
+ offThreadMinBytes?: number;
102
+ workerTaskTimeoutMs?: number;
103
+ }): void;
104
+ /** The values actually in force — report these with a measurement, never the defaults. */
105
+ export declare function getXmlParsingConfig(): {
106
+ offThreadMinBytes: number;
107
+ workerTaskTimeoutMs: number;
108
+ };
109
+ /**
110
+ * Old-generation ceiling for the single XML parse worker.
111
+ *
112
+ * Bounds a cost that was previously unbounded in two directions at once: an unbounded number of workers
113
+ * (see the pool construction) and an unbounded heap per worker. V8 does not return old-generation pages
114
+ * to the OS, so a long-lived worker's high-water mark IS the process's RSS for its lifetime.
115
+ *
116
+ * 192 MB is ~25x the parsed graph of the largest document observed (~1 MB of XML), so it is a runaway
117
+ * guard rather than a working limit. Raising it should be justified by `offThreadBytes / offThread`, not
118
+ * by a worker failure — a failure here degrades to inline parsing, it does not drop the document.
119
+ */
120
+ export declare const XML_WORKER_MAX_OLD_GEN_MB = 192;
121
+ /**
122
+ * THE single definition of the feed `isArray` predicate: array-ify every non-attribute nested node.
123
+ *
124
+ * Everything else derives from this — including the worker copies, via
125
+ * {@link FEED_XML_PARSER_WORKER_CONFIG}. Do not restate this logic anywhere.
126
+ */
127
+ export declare const feedXmlIsArray: (_n: any, jPath: any, _l: any, isAttr: any) => boolean;
128
+ /**
129
+ * Data-only fast-xml-parser options (structured-cloneable — no functions).
130
+ *
131
+ * These are the values the atfeed Betradar AMQP worker had been carrying locally, promoted here so
132
+ * every feed parse — REST and AMQP — shares one configuration and atfeed no longer overrides anything.
133
+ *
134
+ * `ignoreDeclaration: true` was the one genuine divergence: the AMQP worker set it, while
135
+ * `TSRequest.xml` and atfeed's own `recovery-request.ts` parser did not. Without it, a document with an
136
+ * `<?xml …?>` prolog yields an extra `?xml` key in the parsed object, so REST and AMQP produced
137
+ * different shapes for the same markup. Adopting the AMQP value drops that key from REST parses too —
138
+ * a deliberate behaviour change that makes the two paths agree. Nothing reads `?xml`; it is prolog
139
+ * metadata, not content.
140
+ */
141
+ export declare const FEED_XML_PARSER_DATA_OPTIONS: {
142
+ readonly ignoreDeclaration: true;
143
+ readonly ignoreAttributes: false;
144
+ readonly attributeNamePrefix: '';
145
+ readonly attributesGroupName: '$';
146
+ readonly textNodeName: '_';
147
+ };
148
+ /**
149
+ * Canonical fast-xml-parser options for provider feed XML (REST **and** AMQP).
150
+ *
151
+ * Use this everywhere a feed document is parsed. Divergence between two parser configurations does not
152
+ * fail loudly — it silently changes the parsed shape, which for odds/settlement payloads means wrong
153
+ * market or outcome data rather than an error.
154
+ */
155
+ export declare function feedXmlParserOptions(): Record<string, unknown>;
156
+ /**
157
+ * Options for a worker that must rebuild the parser itself.
158
+ *
159
+ * A TSRQW callback is stringified into an eval worker, so it closes over nothing, and `isArray` is a
160
+ * function — which structured-clone cannot transfer. Passing the predicate's SOURCE keeps a single
161
+ * definition: the worker reconstructs it with {@link buildFeedXmlParserOptions} rather than restating
162
+ * the logic. Derived via `toString()` so the source can never drift from the function above.
163
+ *
164
+ * Pass this as TSRQW's `workerData` (or merge it into an existing `workerData`).
165
+ */
166
+ export declare const FEED_XML_PARSER_WORKER_CONFIG: {
167
+ readonly ignoreDeclaration: true;
168
+ readonly ignoreAttributes: false;
169
+ readonly attributeNamePrefix: '';
170
+ readonly attributesGroupName: '$';
171
+ readonly textNodeName: '_';
172
+ readonly isArraySource: string;
173
+ };
174
+ /**
175
+ * Rebuild the canonical parser options inside a worker from {@link FEED_XML_PARSER_WORKER_CONFIG}.
176
+ *
177
+ * Safe to call from stringified worker code: it only needs the plain config object. `new Function` is
178
+ * used to revive the predicate — no additional risk in a context that is already an eval worker, and
179
+ * TSRQW's wrapper predefines `__name` for the same reason (bundlers inject it into `toString()` output).
180
+ */
181
+ export declare function buildFeedXmlParserOptions(cfg: {
182
+ isArraySource?: string;
183
+ } & Record<string, unknown>): Record<string, unknown>;
184
+ /**
185
+ * Counters for how XML documents were parsed.
186
+ *
187
+ * `fallback` is the one to watch: a non-zero and growing value means large documents are being parsed
188
+ * on the main thread despite the threshold, so the event-loop protection is not actually in effect.
189
+ */
190
+ export declare function getXmlParseStats(): {
191
+ inline: number;
192
+ offThread: number;
193
+ fallback: number;
194
+ inlineBytes: number;
195
+ inlineMaxBytes: number;
196
+ offThreadBytes: number;
197
+ /** Cumulative wall time of inline parses — see {@link xmlParseStats}. */
198
+ inlineMs: number;
199
+ /** Single worst inline parse. This is the event loop held in one uninterruptible go. */
200
+ inlineMaxMs: number;
201
+ /** Documents handed to the pool. `submitted - offThread - fallback` is the backlog. */
202
+ submitted: number;
203
+ /** Documents currently awaiting a worker reply. Sustained non-zero at idle means a stuck task. */
204
+ inFlight: number;
205
+ };
206
+ export declare function resetXmlParseStatsForTests(): void;
207
+ /** Shut the XML parse pool down (tests, graceful shutdown). Safe to call when none exists. */
208
+ export declare function closeXmlWorkerPool(): void;
209
+ /**
210
+ * Parse an XML document, off the main thread when it is large enough to matter.
211
+ *
212
+ * Falls back to inline parsing when the document is small, when no worker pool can be created, or when
213
+ * the worker fails for any reason — the result must be identical either way, so the only difference a
214
+ * caller can observe is whether the event loop was blocked.
215
+ */
216
+ export declare function parseXmlDocument(body: string): Promise<any>;
217
+ export declare class TSRequest {
218
+ private static responseBody;
219
+ private static isNonXmlBody;
220
+ private static debugTrace;
221
+ static form(url: string, options?: TSRequestHttpOptions, debug?: boolean): Promise<any>;
222
+ static xml(url: string, options?: TSRequestHttpOptions, debug?: boolean): Promise<any>;
223
+ static json(url: string, options?: TSRequestHttpOptions, debug?: boolean): Promise<any>;
224
+ static raw(url: string, options?: TSRequestHttpOptions): Promise<ITSResponse>;
225
+ static error(kind: string, status: number, message: any): Error;
226
+ static url(url: string): {
227
+ href: string;
228
+ protocol: string;
229
+ host: string;
230
+ hostname: string;
231
+ port: string;
232
+ path: string;
233
+ pathname: string;
234
+ search: string;
235
+ hash: string;
236
+ } | {
237
+ href?: undefined;
238
+ protocol?: undefined;
239
+ host?: undefined;
240
+ hostname?: undefined;
241
+ port?: undefined;
242
+ path?: undefined;
243
+ pathname?: undefined;
244
+ search?: undefined;
245
+ hash?: undefined;
246
+ };
247
+ }
248
+ export declare function request(url: string, { body, qs, debug, ...options }?: Record<string, unknown>, kind?: 'raw' | 'xml' | 'json' | 'form'): Promise<any>;