@bymax-one/nest-ai-tokens 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,98 @@
1
+ import { Redis } from 'ioredis';
2
+
3
+ /**
4
+ * @fileoverview The optional live cross-replica budget counter port (spec §15.1,
5
+ * §10.8). The fast path for budget enforcement; the DB conditional consume is the
6
+ * fallback. Values are serialized as int64 decimal strings. Implemented by the
7
+ * official Redis adapter (`./redis`).
8
+ * @layer server
9
+ */
10
+ /** The live budget counter port. */
11
+ interface IBudgetCounterStore {
12
+ /**
13
+ * Atomically increment `key` by `amount` iff the result stays `<= limit`,
14
+ * setting the TTL. Returns `false` when the increment would exceed the limit.
15
+ */
16
+ incrIfBelow(key: string, amount: bigint, limit: bigint, ttlSeconds: number): Promise<boolean>;
17
+ /** Decrement `key` by `amount` (release/reverse). */
18
+ decr(key: string, amount: bigint): Promise<void>;
19
+ /** Reset `key` (window rotation). */
20
+ reset(key: string): Promise<void>;
21
+ }
22
+
23
+ /**
24
+ * @fileoverview `RedisBudgetCounterStore` — the optional live cross-replica budget
25
+ * counter (`./redis` subpath, spec §10.8, §15.1). It makes budget enforcement cheap
26
+ * under high concurrency: `incrIfBelow` is a SINGLE atomic Lua script (increment,
27
+ * then undo and reject when the result would exceed the limit), so two racing
28
+ * replicas can never both pass a nearly-full budget. The DB conditional consume
29
+ * stays authoritative — this counter is the fast path, not the source of truth.
30
+ * Values are int64 (Redis native) written/compared as decimal strings; keys carry
31
+ * the caller's optional prefix. `ioredis` is an OPTIONAL peer: the constructor
32
+ * accepts a live `Redis` instance, or a connection URL that is lazily connected via
33
+ * a DYNAMIC import so the base library never statically depends on the client.
34
+ * Connection URLs (which may embed credentials) are never logged or placed on an
35
+ * exception.
36
+ * @layer redis
37
+ */
38
+
39
+ /** Construction options for {@link RedisBudgetCounterStore}. */
40
+ interface RedisBudgetCounterStoreOptions {
41
+ /** Prepended to every counter key (namespacing a shared Redis). */
42
+ keyPrefix?: string;
43
+ }
44
+ /**
45
+ * Signals that a counter operand or result falls outside the IEEE-754 safe-integer
46
+ * range (`±(2^53 − 1)`), where the Lua/Redis double arithmetic the fast path relies
47
+ * on would silently lose precision. It is thrown so `BudgetService` treats the counter
48
+ * as unavailable and falls back to the authoritative DB conditional consume, which
49
+ * compares int64 nano-USD exactly.
50
+ */
51
+ declare class CounterValueOutOfRangeError extends Error {
52
+ readonly isCounterValueOutOfRange = true;
53
+ constructor();
54
+ }
55
+ /** The official Redis adapter for the live budget counter port. */
56
+ declare class RedisBudgetCounterStore implements IBudgetCounterStore {
57
+ private readonly keyPrefix;
58
+ private readonly source;
59
+ private connecting;
60
+ /**
61
+ * @param redis A live `ioredis` client, or a connection URL to lazily connect.
62
+ * @param options Optional key prefix.
63
+ */
64
+ constructor(redis: Redis | string, options?: RedisBudgetCounterStoreOptions);
65
+ /**
66
+ * Atomically increment `key` by `amount` iff the result stays `<= limit`, setting
67
+ * the TTL. Returns `false` when the increment would exceed the limit.
68
+ *
69
+ * @param key The dimension counter key.
70
+ * @param amount The amount to add (int64 nano-USD or token/count units).
71
+ * @param limit The dimension limit.
72
+ * @param ttlSeconds The key TTL (window length + grace).
73
+ * @returns `true` when incremented within the limit, `false` when it would exceed it.
74
+ * @throws {CounterValueOutOfRangeError} when an operand or the resulting counter exceeds the safe-integer range.
75
+ */
76
+ incrIfBelow(key: string, amount: bigint, limit: bigint, ttlSeconds: number): Promise<boolean>;
77
+ /**
78
+ * Decrement `key` by `amount`, flooring the stored value at zero (release/reverse).
79
+ *
80
+ * @param key The dimension counter key.
81
+ * @param amount The amount to subtract.
82
+ */
83
+ decr(key: string, amount: bigint): Promise<void>;
84
+ /**
85
+ * Reset (delete) `key` — used on window rotation.
86
+ *
87
+ * @param key The dimension counter key.
88
+ */
89
+ reset(key: string): Promise<void>;
90
+ /** Prepend the configured key prefix. */
91
+ private prefixed;
92
+ /** Resolve the live client, lazily connecting a URL via a dynamic `ioredis` import. */
93
+ private resolve;
94
+ /** Dynamically import `ioredis` and connect the given URL. */
95
+ private connect;
96
+ }
97
+
98
+ export { CounterValueOutOfRangeError, RedisBudgetCounterStore, type RedisBudgetCounterStoreOptions };
@@ -0,0 +1,107 @@
1
+ // src/redis/index.ts
2
+ var SAFE_INTEGER_MAX = "9007199254740991";
3
+ var INCR_IF_BELOW_LUA = `
4
+ local amount = tonumber(ARGV[1])
5
+ local limit = tonumber(ARGV[2])
6
+ if amount > ${SAFE_INTEGER_MAX} or amount < -${SAFE_INTEGER_MAX} or limit > ${SAFE_INTEGER_MAX} or limit < -${SAFE_INTEGER_MAX} then
7
+ return -1
8
+ end
9
+ local newValue = redis.call('INCRBY', KEYS[1], ARGV[1])
10
+ if newValue > ${SAFE_INTEGER_MAX} or newValue < -${SAFE_INTEGER_MAX} then
11
+ redis.call('DECRBY', KEYS[1], ARGV[1])
12
+ return -1
13
+ end
14
+ if newValue > limit then
15
+ redis.call('DECRBY', KEYS[1], ARGV[1])
16
+ return 0
17
+ end
18
+ redis.call('PEXPIRE', KEYS[1], ARGV[3])
19
+ return 1
20
+ `;
21
+ var DECR_FLOOR_LUA = `
22
+ local newValue = redis.call('DECRBY', KEYS[1], ARGV[1])
23
+ if newValue < 0 then
24
+ redis.call('SET', KEYS[1], '0')
25
+ end
26
+ return 1
27
+ `;
28
+ var CounterValueOutOfRangeError = class extends Error {
29
+ isCounterValueOutOfRange = true;
30
+ constructor() {
31
+ super("counter operand or result exceeds the safe-integer range");
32
+ this.name = "CounterValueOutOfRangeError";
33
+ }
34
+ };
35
+ var RedisBudgetCounterStore = class {
36
+ keyPrefix;
37
+ source;
38
+ connecting = null;
39
+ /**
40
+ * @param redis A live `ioredis` client, or a connection URL to lazily connect.
41
+ * @param options Optional key prefix.
42
+ */
43
+ constructor(redis, options = {}) {
44
+ this.keyPrefix = options.keyPrefix ?? "";
45
+ this.source = redis;
46
+ }
47
+ /**
48
+ * Atomically increment `key` by `amount` iff the result stays `<= limit`, setting
49
+ * the TTL. Returns `false` when the increment would exceed the limit.
50
+ *
51
+ * @param key The dimension counter key.
52
+ * @param amount The amount to add (int64 nano-USD or token/count units).
53
+ * @param limit The dimension limit.
54
+ * @param ttlSeconds The key TTL (window length + grace).
55
+ * @returns `true` when incremented within the limit, `false` when it would exceed it.
56
+ * @throws {CounterValueOutOfRangeError} when an operand or the resulting counter exceeds the safe-integer range.
57
+ */
58
+ async incrIfBelow(key, amount, limit, ttlSeconds) {
59
+ const client = await this.resolve();
60
+ const result = await client.eval(
61
+ INCR_IF_BELOW_LUA,
62
+ 1,
63
+ this.prefixed(key),
64
+ amount.toString(),
65
+ limit.toString(),
66
+ Math.ceil(ttlSeconds * 1e3).toString()
67
+ );
68
+ if (result === -1) throw new CounterValueOutOfRangeError();
69
+ return result === 1;
70
+ }
71
+ /**
72
+ * Decrement `key` by `amount`, flooring the stored value at zero (release/reverse).
73
+ *
74
+ * @param key The dimension counter key.
75
+ * @param amount The amount to subtract.
76
+ */
77
+ async decr(key, amount) {
78
+ const client = await this.resolve();
79
+ await client.eval(DECR_FLOOR_LUA, 1, this.prefixed(key), amount.toString());
80
+ }
81
+ /**
82
+ * Reset (delete) `key` — used on window rotation.
83
+ *
84
+ * @param key The dimension counter key.
85
+ */
86
+ async reset(key) {
87
+ const client = await this.resolve();
88
+ await client.del(this.prefixed(key));
89
+ }
90
+ /** Prepend the configured key prefix. */
91
+ prefixed(key) {
92
+ return `${this.keyPrefix}${key}`;
93
+ }
94
+ /** Resolve the live client, lazily connecting a URL via a dynamic `ioredis` import. */
95
+ resolve() {
96
+ if (typeof this.source !== "string") return Promise.resolve(this.source);
97
+ this.connecting ??= this.connect(this.source);
98
+ return this.connecting;
99
+ }
100
+ /** Dynamically import `ioredis` and connect the given URL. */
101
+ async connect(url) {
102
+ const { default: RedisClient } = await import('ioredis');
103
+ return new RedisClient(url);
104
+ }
105
+ };
106
+
107
+ export { CounterValueOutOfRangeError, RedisBudgetCounterStore };