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.
- package/LICENSE +1 -0
- package/README.md +8 -0
- package/db/TSJournal.d.ts +108 -0
- package/db/TSJournal.js +229 -0
- package/db/TSMongo.d.ts +103 -0
- package/db/TSMongo.js +516 -0
- package/db/TSRQW.d.ts +625 -0
- package/db/TSRQW.js +1204 -0
- package/db/TSRedis.d.ts +530 -0
- package/db/TSRedis.js +1368 -0
- package/db/TSRedisTB.d.ts +80 -0
- package/db/TSRedisTB.js +178 -0
- package/package.json +85 -0
- package/ussd/TSUssdMenu.d.ts +139 -0
- package/ussd/TSUssdMenu.js +368 -0
- package/ussd/TSUssdScreen.d.ts +58 -0
- package/ussd/TSUssdScreen.js +218 -0
- package/ussd/index.d.ts +3 -0
- package/ussd/index.js +19 -0
- package/ussd/providers/AfricasTalking.d.ts +3 -0
- package/ussd/providers/AfricasTalking.js +17 -0
- package/ussd/providers/AirtelDRC.d.ts +9 -0
- package/ussd/providers/AirtelDRC.js +31 -0
- package/ussd/providers/OrangeDRC.d.ts +5 -0
- package/ussd/providers/OrangeDRC.js +213 -0
- package/ussd/providers/VodacomDRC.d.ts +9 -0
- package/ussd/providers/VodacomDRC.js +48 -0
- package/ussd/providers/_.d.ts +55 -0
- package/ussd/providers/_.js +83 -0
- package/ussd/providers/index.d.ts +13 -0
- package/ussd/providers/index.js +56 -0
- package/utils/TSFifo.d.ts +109 -0
- package/utils/TSFifo.js +145 -0
- package/utils/TSFile.d.ts +36 -0
- package/utils/TSFile.js +244 -0
- package/utils/TSHash.d.ts +19 -0
- package/utils/TSHash.js +71 -0
- package/utils/TSRequest.d.ts +248 -0
- package/utils/TSRequest.js +689 -0
- package/utils/TSStub.d.ts +159 -0
- package/utils/TSStub.js +296 -0
- package/utils/abort.d.ts +18 -0
- package/utils/abort.js +97 -0
- package/utils/mime.json +11358 -0
- package/utils/object-keys.d.ts +39 -0
- package/utils/object-keys.js +52 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TSRedisTB — a cross-process token-bucket rate budget shared by every process/replica that draws from the SAME Redis,
|
|
3
|
+
* so a per-credential / per-operator cap is honoured fleet-wide instead of statically divided (N replicas each
|
|
4
|
+
* believing they own the whole allowance). A topology-aware primitive built on {@link TSRedis} — like {@link TSRQW},
|
|
5
|
+
* it does not care whether the fleet is cluster workers, `docker scale N`, or k8s pods: they all draw from one bucket,
|
|
6
|
+
* so the sum of grants across ANY number of processes can never exceed the cap. No process needs to know the live
|
|
7
|
+
* replica count.
|
|
8
|
+
*
|
|
9
|
+
* `take(want)` is ONE atomic Lua round trip: refill by elapsed time, grant `min(floor(tokens), want)`, persist the
|
|
10
|
+
* remainder. Atomicity is the whole guarantee — two processes asking for the last token at the same instant cannot
|
|
11
|
+
* both be granted it. The key is hash-tagged (`{ratelimit:<scope>}`) so the script stays single-slot on a cluster.
|
|
12
|
+
*
|
|
13
|
+
* Optional / fail-open by design: pass `null` as the client (or `perSecond <= 0`) and every `take` grants in full —
|
|
14
|
+
* an unwired or absent shared cap must never block the caller. That makes it safe to reach for in a host that may or
|
|
15
|
+
* may not have Redis: single-process / no-Redis deployments simply pace themselves elsewhere.
|
|
16
|
+
*/
|
|
17
|
+
import { type TSRedisClient } from './TSRedis';
|
|
18
|
+
export interface TokenGrant {
|
|
19
|
+
/** Tokens actually handed out, 0..want. */
|
|
20
|
+
granted: number;
|
|
21
|
+
/** When another token is expected, in ms (0 when some were granted). */
|
|
22
|
+
retryAfterMs: number;
|
|
23
|
+
}
|
|
24
|
+
export interface TSRedisTBOptions {
|
|
25
|
+
/** Budget identity — the shared cap (e.g. the operator id). Every process using the same `scope` draws one budget. */
|
|
26
|
+
scope: string;
|
|
27
|
+
/** Sustained refill rate (the operator cap). */
|
|
28
|
+
perSecond: number;
|
|
29
|
+
/** Bucket depth (max burst). Default: `perSecond`. */
|
|
30
|
+
burst?: number;
|
|
31
|
+
/** Time source — injectable for determinism; defaults to the system clock. */
|
|
32
|
+
now?: () => number;
|
|
33
|
+
}
|
|
34
|
+
export declare class TSRedisTB {
|
|
35
|
+
#private;
|
|
36
|
+
constructor(client: TSRedisClient | null, opts: TSRedisTBOptions);
|
|
37
|
+
/** The configured sustained rate (the shared cap), for reporting. */
|
|
38
|
+
get perSecond(): number;
|
|
39
|
+
/** The configured bucket depth (max burst). */
|
|
40
|
+
get burst(): number;
|
|
41
|
+
/** Atomically claim up to `want` tokens from the shared budget. Fail-open (grant all) when unwired (no Redis) or
|
|
42
|
+
* unlimited (`perSecond <= 0`) — an absent shared cap must never block the caller. */
|
|
43
|
+
take(want: number): Promise<TokenGrant>;
|
|
44
|
+
}
|
|
45
|
+
/** Default tokens to claim per shared-bucket round trip: ~100ms of tokens, capped by burst, floored at 1. */
|
|
46
|
+
export declare function reservationBatch(perSecond: number, burst: number): number;
|
|
47
|
+
/** Result of {@link TSRedisTBReservation.acquire}. `retryAfterMs` is a wait hint when not admitted. */
|
|
48
|
+
export interface Admission {
|
|
49
|
+
admitted: boolean;
|
|
50
|
+
retryAfterMs: number;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A LOCAL token reservation over a shared {@link TSRedisTB} — the reusable "amortise the round trip" layer both the
|
|
54
|
+
* durable queue ({@link TSRQW}'s rate limit) and per-driver consumers build on, so the batching + coalescing logic
|
|
55
|
+
* lives in ONE place.
|
|
56
|
+
*
|
|
57
|
+
* `acquire()` spends a locally-reserved token with NO round trip; when empty it claims `batch` tokens from the
|
|
58
|
+
* shared bucket in ONE atomic round trip and serves them locally. Concurrent acquirers COALESCE onto the same
|
|
59
|
+
* in-flight claim (a burst of N costs one round trip, not N). The shared bucket is still the ceiling — a batch is
|
|
60
|
+
* claimed atomically, so N processes never exceed the cap; unspent reserved tokens on a crash are simply lost
|
|
61
|
+
* (safe under-admission). Fail-open is inherited from the bucket (`perSecond <= 0` / no Redis ⇒ every claim grants,
|
|
62
|
+
* so a reservation is never a blocker when the cap is unset). Construct ONE per consumer (per driver / per queue).
|
|
63
|
+
*/
|
|
64
|
+
export declare class TSRedisTBReservation {
|
|
65
|
+
#private;
|
|
66
|
+
constructor(bucket: TSRedisTB, batch?: number);
|
|
67
|
+
/** Admit one unit against the shared cap, amortising the round trip across the batch. */
|
|
68
|
+
acquire(): Promise<Admission>;
|
|
69
|
+
}
|
|
70
|
+
export interface TokenBucketCheck {
|
|
71
|
+
name: string;
|
|
72
|
+
ok: boolean;
|
|
73
|
+
detail?: string;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Conformance suite for any `TSRedisTB`-shaped budget (the counterpart of `checkJournalContract`). `make` returns a
|
|
77
|
+
* bucket for a FRESH, unique scope on each call so the checks do not collide — back it with a live Redis. Proves the
|
|
78
|
+
* properties that make the budget a real shared cap: burst grant, drain, refill, partial, and fail-open.
|
|
79
|
+
*/
|
|
80
|
+
export declare function checkTokenBucketContract(make: (opts: TSRedisTBOptions) => TSRedisTB, scopePrefix?: string): Promise<TokenBucketCheck[]>;
|
package/db/TSRedisTB.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TSRedisTBReservation = exports.TSRedisTB = void 0;
|
|
4
|
+
exports.reservationBatch = reservationBatch;
|
|
5
|
+
exports.checkTokenBucketContract = checkTokenBucketContract;
|
|
6
|
+
/**
|
|
7
|
+
* TSRedisTB — a cross-process token-bucket rate budget shared by every process/replica that draws from the SAME Redis,
|
|
8
|
+
* so a per-credential / per-operator cap is honoured fleet-wide instead of statically divided (N replicas each
|
|
9
|
+
* believing they own the whole allowance). A topology-aware primitive built on {@link TSRedis} — like {@link TSRQW},
|
|
10
|
+
* it does not care whether the fleet is cluster workers, `docker scale N`, or k8s pods: they all draw from one bucket,
|
|
11
|
+
* so the sum of grants across ANY number of processes can never exceed the cap. No process needs to know the live
|
|
12
|
+
* replica count.
|
|
13
|
+
*
|
|
14
|
+
* `take(want)` is ONE atomic Lua round trip: refill by elapsed time, grant `min(floor(tokens), want)`, persist the
|
|
15
|
+
* remainder. Atomicity is the whole guarantee — two processes asking for the last token at the same instant cannot
|
|
16
|
+
* both be granted it. The key is hash-tagged (`{ratelimit:<scope>}`) so the script stays single-slot on a cluster.
|
|
17
|
+
*
|
|
18
|
+
* Optional / fail-open by design: pass `null` as the client (or `perSecond <= 0`) and every `take` grants in full —
|
|
19
|
+
* an unwired or absent shared cap must never block the caller. That makes it safe to reach for in a host that may or
|
|
20
|
+
* may not have Redis: single-process / no-Redis deployments simply pace themselves elsewhere.
|
|
21
|
+
*/
|
|
22
|
+
const TSRedis_1 = require("./TSRedis");
|
|
23
|
+
// Atomic take: refill from elapsed, grant min(floor(tokens), want), store the remainder + a 60s idle expiry.
|
|
24
|
+
// Returns { granted, retryAfterMs }. Fractional tokens live in the hash (float string); the reply is integers.
|
|
25
|
+
const TAKE_LUA = `
|
|
26
|
+
local want = tonumber(ARGV[1])
|
|
27
|
+
local perSecond = tonumber(ARGV[2])
|
|
28
|
+
local burst = tonumber(ARGV[3])
|
|
29
|
+
local now = tonumber(ARGV[4])
|
|
30
|
+
local tokens = tonumber(redis.call('HGET', KEYS[1], 'tokens'))
|
|
31
|
+
local at = tonumber(redis.call('HGET', KEYS[1], 'at'))
|
|
32
|
+
if tokens == nil then tokens = burst; at = now end
|
|
33
|
+
tokens = math.min(burst, tokens + (now - at) * perSecond / 1000)
|
|
34
|
+
local granted = math.min(math.floor(tokens), want)
|
|
35
|
+
tokens = tokens - granted
|
|
36
|
+
redis.call('HSET', KEYS[1], 'tokens', tokens, 'at', now)
|
|
37
|
+
redis.call('PEXPIRE', KEYS[1], 60000)
|
|
38
|
+
local retry = 0
|
|
39
|
+
if granted == 0 and perSecond > 0 then
|
|
40
|
+
retry = math.ceil((1 - tokens) / perSecond * 1000)
|
|
41
|
+
if retry < 1 then retry = 1 end
|
|
42
|
+
end
|
|
43
|
+
return { granted, retry }`;
|
|
44
|
+
class TSRedisTB {
|
|
45
|
+
#client;
|
|
46
|
+
#key;
|
|
47
|
+
#perSecond;
|
|
48
|
+
#burst;
|
|
49
|
+
#now;
|
|
50
|
+
constructor(client, opts) {
|
|
51
|
+
this.#client = client;
|
|
52
|
+
this.#key = `{ratelimit:${opts.scope}}:tb`;
|
|
53
|
+
this.#perSecond = opts.perSecond;
|
|
54
|
+
this.#burst = Math.max(1, opts.burst ?? opts.perSecond);
|
|
55
|
+
this.#now = opts.now ?? (() => Date.now());
|
|
56
|
+
}
|
|
57
|
+
/** The configured sustained rate (the shared cap), for reporting. */
|
|
58
|
+
get perSecond() {
|
|
59
|
+
return this.#perSecond;
|
|
60
|
+
}
|
|
61
|
+
/** The configured bucket depth (max burst). */
|
|
62
|
+
get burst() {
|
|
63
|
+
return this.#burst;
|
|
64
|
+
}
|
|
65
|
+
/** Atomically claim up to `want` tokens from the shared budget. Fail-open (grant all) when unwired (no Redis) or
|
|
66
|
+
* unlimited (`perSecond <= 0`) — an absent shared cap must never block the caller. */
|
|
67
|
+
async take(want) {
|
|
68
|
+
if (this.#client === null || this.#perSecond <= 0)
|
|
69
|
+
return { granted: want, retryAfterMs: 0 };
|
|
70
|
+
const res = (await (0, TSRedis_1.evalTaggedScript)(this.#client, {
|
|
71
|
+
keys: [this.#key],
|
|
72
|
+
argv: [String(want), String(this.#perSecond), String(this.#burst), String(this.#now())],
|
|
73
|
+
source: TAKE_LUA
|
|
74
|
+
}));
|
|
75
|
+
const granted = Number(res?.[0] ?? 0);
|
|
76
|
+
const retryAfterMs = Number(res?.[1] ?? 0);
|
|
77
|
+
return { granted, retryAfterMs };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
exports.TSRedisTB = TSRedisTB;
|
|
81
|
+
/** Default tokens to claim per shared-bucket round trip: ~100ms of tokens, capped by burst, floored at 1. */
|
|
82
|
+
function reservationBatch(perSecond, burst) {
|
|
83
|
+
return Math.max(1, Math.min(burst, Math.ceil(Math.max(1, perSecond) / 10)));
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* A LOCAL token reservation over a shared {@link TSRedisTB} — the reusable "amortise the round trip" layer both the
|
|
87
|
+
* durable queue ({@link TSRQW}'s rate limit) and per-driver consumers build on, so the batching + coalescing logic
|
|
88
|
+
* lives in ONE place.
|
|
89
|
+
*
|
|
90
|
+
* `acquire()` spends a locally-reserved token with NO round trip; when empty it claims `batch` tokens from the
|
|
91
|
+
* shared bucket in ONE atomic round trip and serves them locally. Concurrent acquirers COALESCE onto the same
|
|
92
|
+
* in-flight claim (a burst of N costs one round trip, not N). The shared bucket is still the ceiling — a batch is
|
|
93
|
+
* claimed atomically, so N processes never exceed the cap; unspent reserved tokens on a crash are simply lost
|
|
94
|
+
* (safe under-admission). Fail-open is inherited from the bucket (`perSecond <= 0` / no Redis ⇒ every claim grants,
|
|
95
|
+
* so a reservation is never a blocker when the cap is unset). Construct ONE per consumer (per driver / per queue).
|
|
96
|
+
*/
|
|
97
|
+
class TSRedisTBReservation {
|
|
98
|
+
#bucket;
|
|
99
|
+
#batch;
|
|
100
|
+
#reserved = 0;
|
|
101
|
+
#refill = null;
|
|
102
|
+
#retryMs = 0;
|
|
103
|
+
constructor(bucket, batch) {
|
|
104
|
+
this.#bucket = bucket;
|
|
105
|
+
this.#batch = Math.max(1, batch ?? reservationBatch(bucket.perSecond, bucket.burst));
|
|
106
|
+
}
|
|
107
|
+
/** Admit one unit against the shared cap, amortising the round trip across the batch. */
|
|
108
|
+
async acquire() {
|
|
109
|
+
if (this.#reserved >= 1) {
|
|
110
|
+
this.#reserved -= 1;
|
|
111
|
+
return { admitted: true, retryAfterMs: 0 };
|
|
112
|
+
}
|
|
113
|
+
// Coalesce concurrent claims into ONE round trip: every acquirer awaits the SAME in-flight take.
|
|
114
|
+
this.#refill ??= this.#bucket.take(this.#batch).then((g) => { this.#reserved += g.granted; this.#retryMs = g.retryAfterMs; this.#refill = null; });
|
|
115
|
+
await this.#refill;
|
|
116
|
+
if (this.#reserved >= 1) {
|
|
117
|
+
this.#reserved -= 1;
|
|
118
|
+
return { admitted: true, retryAfterMs: 0 };
|
|
119
|
+
}
|
|
120
|
+
return { admitted: false, retryAfterMs: this.#retryMs };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
exports.TSRedisTBReservation = TSRedisTBReservation;
|
|
124
|
+
/**
|
|
125
|
+
* Conformance suite for any `TSRedisTB`-shaped budget (the counterpart of `checkJournalContract`). `make` returns a
|
|
126
|
+
* bucket for a FRESH, unique scope on each call so the checks do not collide — back it with a live Redis. Proves the
|
|
127
|
+
* properties that make the budget a real shared cap: burst grant, drain, refill, partial, and fail-open.
|
|
128
|
+
*/
|
|
129
|
+
async function checkTokenBucketContract(make, scopePrefix = 'contract') {
|
|
130
|
+
const checks = [];
|
|
131
|
+
const push = (name, ok, detail) => {
|
|
132
|
+
checks.push(detail === undefined ? { name, ok } : { name, ok, detail });
|
|
133
|
+
};
|
|
134
|
+
let seq = 0;
|
|
135
|
+
const freshScope = () => `${scopePrefix}:${seq++}`;
|
|
136
|
+
// A fixed clock so refill is deterministic — the bucket reads `now()` on every take.
|
|
137
|
+
let clock = 0;
|
|
138
|
+
const now = () => clock;
|
|
139
|
+
// 1. A fresh bucket grants up to `burst` immediately.
|
|
140
|
+
{
|
|
141
|
+
clock = 1_000_000;
|
|
142
|
+
const b = make({ scope: freshScope(), perSecond: 10, burst: 5, now });
|
|
143
|
+
const g = await b.take(5);
|
|
144
|
+
push('grants up to burst on a fresh bucket', g.granted === 5, `granted=${g.granted}`);
|
|
145
|
+
}
|
|
146
|
+
// 2. Draining past the burst grants nothing more within the same instant, and reports a positive retry.
|
|
147
|
+
{
|
|
148
|
+
clock = 2_000_000;
|
|
149
|
+
const b = make({ scope: freshScope(), perSecond: 10, burst: 5, now });
|
|
150
|
+
await b.take(5);
|
|
151
|
+
const g = await b.take(1);
|
|
152
|
+
push('never grants past the burst when drained', g.granted === 0, `granted=${g.granted}`);
|
|
153
|
+
push('reports a positive retryAfterMs when empty', g.retryAfterMs > 0, `retryAfterMs=${g.retryAfterMs}`);
|
|
154
|
+
}
|
|
155
|
+
// 3. Refills by elapsed time: after 1s at 10/s, ~10 tokens are available again (capped at burst).
|
|
156
|
+
{
|
|
157
|
+
clock = 3_000_000;
|
|
158
|
+
const b = make({ scope: freshScope(), perSecond: 10, burst: 20, now });
|
|
159
|
+
await b.take(20); // drain
|
|
160
|
+
clock += 1_000; // one second later
|
|
161
|
+
const g = await b.take(20);
|
|
162
|
+
push('refills by elapsed time', g.granted >= 9 && g.granted <= 11, `granted=${g.granted}`);
|
|
163
|
+
}
|
|
164
|
+
// 4. A partial grant hands out only what is available, not the full ask.
|
|
165
|
+
{
|
|
166
|
+
clock = 4_000_000;
|
|
167
|
+
const b = make({ scope: freshScope(), perSecond: 10, burst: 3, now });
|
|
168
|
+
const g = await b.take(10);
|
|
169
|
+
push('grants only what is available on a partial ask', g.granted === 3, `granted=${g.granted}`);
|
|
170
|
+
}
|
|
171
|
+
// 5. Fail-open when unlimited (perSecond <= 0): grants the full ask regardless of Redis.
|
|
172
|
+
{
|
|
173
|
+
const b = make({ scope: freshScope(), perSecond: 0, now });
|
|
174
|
+
const g = await b.take(1000);
|
|
175
|
+
push('fails open when unlimited (perSecond<=0)', g.granted === 1000, `granted=${g.granted}`);
|
|
176
|
+
}
|
|
177
|
+
return checks;
|
|
178
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ts-server-lib",
|
|
3
|
+
"version": "0.0.48",
|
|
4
|
+
"scripts": {
|
|
5
|
+
"prepublishOnly": "npm run build",
|
|
6
|
+
"postpublish": "npm run clean:artifacts",
|
|
7
|
+
"build": "tsc --declaration -p .",
|
|
8
|
+
"watch": "tsc -w --declaration -p .",
|
|
9
|
+
"clean:artifacts": "node -e \"const fs=require('fs'),path=require('path');const root=process.cwd();for(const rel of ['db','ussd','ussd/providers','utils','ws']){const dir=path.join(root,rel);let n;try{n=fs.readdirSync(dir)}catch{continue}for(const name of n){if(!name.endsWith('.d.ts')&&path.extname(name)!=='.js')continue;const fp=path.join(dir,name);try{if(fs.statSync(fp).isFile())fs.unlinkSync(fp)}catch{}}}\"",
|
|
10
|
+
"clean:deps": "node -e \"for(const p of ['coverage','node_modules','package-lock.json'])try{require('fs').rmSync(p,{recursive:true,force:true})}catch{}\"",
|
|
11
|
+
"clean": "npm run clean:artifacts && npm run clean:deps",
|
|
12
|
+
"lint": "oxlint --fix",
|
|
13
|
+
"test": "npm run build && vitest run",
|
|
14
|
+
"test:watch": "vitest",
|
|
15
|
+
"test:coverage": "vitest run --coverage"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/onalbi/ts-server-lib"
|
|
20
|
+
},
|
|
21
|
+
"author": {
|
|
22
|
+
"name": "Albion Liçi",
|
|
23
|
+
"email": "lici.albion@gmail.com"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"typescript",
|
|
27
|
+
"server"
|
|
28
|
+
],
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/onalbi/ts-server-lib/issues"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"db/**/*.{js,d.ts}",
|
|
35
|
+
"utils/**/*.{js,d.ts}",
|
|
36
|
+
"ussd/**/*.{js,d.ts}",
|
|
37
|
+
"utils/mime.json"
|
|
38
|
+
],
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"cron": "^4.4.0",
|
|
41
|
+
"fast-xml-parser": "^5.8.0",
|
|
42
|
+
"i18n": "^0.15.3",
|
|
43
|
+
"mongodb": "^7.2.0",
|
|
44
|
+
"redis": "^6.0.0",
|
|
45
|
+
"ts-common-lib": "latest"
|
|
46
|
+
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=24.0.0"
|
|
49
|
+
},
|
|
50
|
+
"vitest": {
|
|
51
|
+
"globals": true,
|
|
52
|
+
"environment": "node",
|
|
53
|
+
"include": [
|
|
54
|
+
"test/**/*.spec.ts"
|
|
55
|
+
],
|
|
56
|
+
"exclude": [
|
|
57
|
+
"**/*.spec.js",
|
|
58
|
+
"**/node_modules/**"
|
|
59
|
+
],
|
|
60
|
+
"resolve": {
|
|
61
|
+
"extensions": [
|
|
62
|
+
".ts",
|
|
63
|
+
".tsx",
|
|
64
|
+
".mts",
|
|
65
|
+
".cts",
|
|
66
|
+
".js",
|
|
67
|
+
".mjs",
|
|
68
|
+
".jsx",
|
|
69
|
+
".json"
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
"coverage": {
|
|
73
|
+
"provider": "v8",
|
|
74
|
+
"include": [
|
|
75
|
+
"db/**/*.ts",
|
|
76
|
+
"ussd/**/*.ts",
|
|
77
|
+
"utils/**/*.ts"
|
|
78
|
+
],
|
|
79
|
+
"reporter": [
|
|
80
|
+
"text",
|
|
81
|
+
"lcov"
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* USSD Menu with state
|
|
3
|
+
*
|
|
4
|
+
* - General rules
|
|
5
|
+
* - Routing
|
|
6
|
+
* - ussd text in form 1*2*7
|
|
7
|
+
* - * bring back to start menu
|
|
8
|
+
*
|
|
9
|
+
* - Development Section
|
|
10
|
+
* - To publish local ussd receiver to remote through ssh
|
|
11
|
+
* # ssh -R 9093:localhost:9090 root@185.41.154.247 -N
|
|
12
|
+
*
|
|
13
|
+
*/
|
|
14
|
+
import { EventEmitter } from 'events';
|
|
15
|
+
export declare class TSUssdState {
|
|
16
|
+
menu: TSUssdMenu;
|
|
17
|
+
name: string | null;
|
|
18
|
+
run: ((state: TSUssdState) => void) | null;
|
|
19
|
+
defaultNext: string | null;
|
|
20
|
+
val: string | null;
|
|
21
|
+
next: Record<string, unknown> | null;
|
|
22
|
+
constructor(menu: TSUssdMenu);
|
|
23
|
+
}
|
|
24
|
+
export declare class TSUssdMenu extends EventEmitter {
|
|
25
|
+
map: Record<string, unknown>;
|
|
26
|
+
static START_STATE: string;
|
|
27
|
+
static config: any;
|
|
28
|
+
session: any;
|
|
29
|
+
args: any;
|
|
30
|
+
states: any;
|
|
31
|
+
result: string;
|
|
32
|
+
onResult: any;
|
|
33
|
+
current: any;
|
|
34
|
+
val: string;
|
|
35
|
+
static forOwn(items: unknown[] | Record<string, unknown>, cb: (v: unknown, key?: number | string) => void): void;
|
|
36
|
+
static mapArgs(args?: Record<string, unknown>, map?: Record<string, unknown>): Record<string, unknown>;
|
|
37
|
+
static cleanRoute(route: string): string;
|
|
38
|
+
constructor(args: Record<string, unknown> | undefined, map: Record<string, unknown>);
|
|
39
|
+
/**
|
|
40
|
+
* A callback after result is set
|
|
41
|
+
*/
|
|
42
|
+
callOnResult(): void;
|
|
43
|
+
/**
|
|
44
|
+
* This method send message to the client and take the session opened
|
|
45
|
+
*
|
|
46
|
+
* @param text string Message to sent
|
|
47
|
+
*/
|
|
48
|
+
con(text: string): string;
|
|
49
|
+
/**
|
|
50
|
+
* This method send message to the client and close the session
|
|
51
|
+
*
|
|
52
|
+
* @param text string Message to sent
|
|
53
|
+
*/
|
|
54
|
+
end(text: string): string;
|
|
55
|
+
/**
|
|
56
|
+
* This method format the message and if debug calcualte the length
|
|
57
|
+
*
|
|
58
|
+
* @param text string Message to sent
|
|
59
|
+
*/
|
|
60
|
+
print(text: string): string;
|
|
61
|
+
testLinkRule(rule: string | RegExp | unknown, val: string): boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Find state based on route — async/await, no external dependencies.
|
|
64
|
+
*
|
|
65
|
+
* next values may be: a string, a callback-based function (cb) => void,
|
|
66
|
+
* a function returning a string synchronously, or a function returning a Promise.
|
|
67
|
+
*/
|
|
68
|
+
resolveRoute(route: string): Promise<TSUssdState | undefined>;
|
|
69
|
+
runState(state: TSUssdState): boolean | undefined;
|
|
70
|
+
go(stateName: string): void;
|
|
71
|
+
goStart(): void;
|
|
72
|
+
/**
|
|
73
|
+
* Configure custom session handler cross-compatible between callbacks and promises
|
|
74
|
+
*
|
|
75
|
+
* @example Memory configuration example with callback
|
|
76
|
+
*
|
|
77
|
+
* let sessions = {};
|
|
78
|
+
* let menu = new UssdMenu();
|
|
79
|
+
* menu.sessionConfig({
|
|
80
|
+
* start: (sessionId, callback) {
|
|
81
|
+
* // initialize current session if it doesn't exist this is called by menu.run()
|
|
82
|
+
* if(!(sessionId in sessions)) sessions[sessionId] = {};
|
|
83
|
+
* callback();
|
|
84
|
+
* },
|
|
85
|
+
* end: (sessionId, callback) {
|
|
86
|
+
* // clear current session this is called by menu.end()
|
|
87
|
+
* delete sessions[sessionId];
|
|
88
|
+
* callback();
|
|
89
|
+
* },
|
|
90
|
+
* set: (sessionId, key, value, callback) {
|
|
91
|
+
* // store key-value pair in current session
|
|
92
|
+
* sessions[sessionId][key] = value;
|
|
93
|
+
* callback();
|
|
94
|
+
* },
|
|
95
|
+
* get: (sessionId, key, callback) {
|
|
96
|
+
* // retrieve value by key in current session
|
|
97
|
+
* callback(null, sessions[sessionId][key]);
|
|
98
|
+
* },
|
|
99
|
+
* del: (sessionId, key, callback) {
|
|
100
|
+
* delete sessions[sessionId][key];
|
|
101
|
+
* callback();
|
|
102
|
+
* }
|
|
103
|
+
* });
|
|
104
|
+
*
|
|
105
|
+
* @param {Object} c object with implementation for get, set, start and end methods
|
|
106
|
+
* @param {String} a attribute of args
|
|
107
|
+
*/
|
|
108
|
+
sessionConfig(c: Record<string, (...args: unknown[]) => unknown>, a?: string): void;
|
|
109
|
+
/**
|
|
110
|
+
* Create a state on the ussd chain
|
|
111
|
+
*
|
|
112
|
+
* @param string name name of the state
|
|
113
|
+
* @param object options
|
|
114
|
+
* @param object options.next object mapping of route val to state names
|
|
115
|
+
* @param string options.defaultNext name of state to run when the given route from this state can't be resolved
|
|
116
|
+
* @param function options.run the method to run when this state is resolved
|
|
117
|
+
* @return TSUssdMenu the same instance of Ussd
|
|
118
|
+
*/
|
|
119
|
+
state(name: string, options: {
|
|
120
|
+
next?: Record<string, unknown>;
|
|
121
|
+
defaultNext?: string;
|
|
122
|
+
run?: (state: TSUssdState) => void;
|
|
123
|
+
}): this;
|
|
124
|
+
/**
|
|
125
|
+
* Create the start state of the ussd chain
|
|
126
|
+
*/
|
|
127
|
+
startState(options: {
|
|
128
|
+
next?: Record<string, unknown>;
|
|
129
|
+
defaultNext?: string;
|
|
130
|
+
run?: (state: TSUssdState) => void;
|
|
131
|
+
}): this;
|
|
132
|
+
/**
|
|
133
|
+
* Run the ussd menu
|
|
134
|
+
*
|
|
135
|
+
* @param function onResult a callback to handle on result
|
|
136
|
+
* @param TSUssdArgs options request args from the gateway api
|
|
137
|
+
*/
|
|
138
|
+
run(onResult: (result: string) => void, map: Record<string, unknown>): void;
|
|
139
|
+
}
|