fatigue-kit 0.0.0-stage → 0.2.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.
- package/LICENSE +21 -0
- package/README.md +159 -2
- package/README.zh-CN.md +160 -0
- package/dist/adapters/alipay.cjs +52 -0
- package/dist/adapters/alipay.cjs.map +1 -0
- package/dist/adapters/alipay.d.cts +27 -0
- package/dist/adapters/alipay.d.ts +27 -0
- package/dist/adapters/alipay.js +52 -0
- package/dist/adapters/alipay.js.map +1 -0
- package/dist/adapters/async-storage.cjs +93 -0
- package/dist/adapters/async-storage.cjs.map +1 -0
- package/dist/adapters/async-storage.d.cts +48 -0
- package/dist/adapters/async-storage.d.ts +48 -0
- package/dist/adapters/async-storage.js +93 -0
- package/dist/adapters/async-storage.js.map +1 -0
- package/dist/adapters/local-storage.cjs +24 -0
- package/dist/adapters/local-storage.cjs.map +1 -0
- package/dist/adapters/local-storage.d.cts +14 -0
- package/dist/adapters/local-storage.d.ts +14 -0
- package/dist/adapters/local-storage.js +24 -0
- package/dist/adapters/local-storage.js.map +1 -0
- package/dist/adapters/memory.cjs +7 -0
- package/dist/adapters/memory.cjs.map +1 -0
- package/dist/adapters/memory.d.cts +14 -0
- package/dist/adapters/memory.d.ts +14 -0
- package/dist/adapters/memory.js +7 -0
- package/dist/adapters/memory.js.map +1 -0
- package/dist/adapters/node-file.cjs +53 -0
- package/dist/adapters/node-file.cjs.map +1 -0
- package/dist/adapters/node-file.d.cts +15 -0
- package/dist/adapters/node-file.d.ts +15 -0
- package/dist/adapters/node-file.js +53 -0
- package/dist/adapters/node-file.js.map +1 -0
- package/dist/adapters/session-storage.cjs +24 -0
- package/dist/adapters/session-storage.cjs.map +1 -0
- package/dist/adapters/session-storage.d.cts +14 -0
- package/dist/adapters/session-storage.d.ts +14 -0
- package/dist/adapters/session-storage.js +24 -0
- package/dist/adapters/session-storage.js.map +1 -0
- package/dist/adapters/weapp.cjs +51 -0
- package/dist/adapters/weapp.cjs.map +1 -0
- package/dist/adapters/weapp.d.cts +23 -0
- package/dist/adapters/weapp.d.ts +23 -0
- package/dist/adapters/weapp.js +51 -0
- package/dist/adapters/weapp.js.map +1 -0
- package/dist/chunk-DABM2TVR.cjs +58 -0
- package/dist/chunk-DABM2TVR.cjs.map +1 -0
- package/dist/chunk-DM2CAKGU.js +23 -0
- package/dist/chunk-DM2CAKGU.js.map +1 -0
- package/dist/chunk-GMM2BYRQ.js +58 -0
- package/dist/chunk-GMM2BYRQ.js.map +1 -0
- package/dist/chunk-K7E4LWUL.cjs +43 -0
- package/dist/chunk-K7E4LWUL.cjs.map +1 -0
- package/dist/chunk-NBDAXDB6.cjs +23 -0
- package/dist/chunk-NBDAXDB6.cjs.map +1 -0
- package/dist/chunk-ZNIZLIUO.js +43 -0
- package/dist/chunk-ZNIZLIUO.js.map +1 -0
- package/dist/global/index.global.js +2 -0
- package/dist/global/index.global.js.map +1 -0
- package/dist/index.cjs +334 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +17 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +334 -0
- package/dist/index.js.map +1 -0
- package/dist/legacy.cjs +45 -0
- package/dist/legacy.cjs.map +1 -0
- package/dist/legacy.d.cts +40 -0
- package/dist/legacy.d.ts +40 -0
- package/dist/legacy.js +45 -0
- package/dist/legacy.js.map +1 -0
- package/dist/types-hDxN5bc3.d.cts +89 -0
- package/dist/types-hDxN5bc3.d.ts +89 -0
- package/dist/web-GiaS0cnX.d.cts +8 -0
- package/dist/web-GiaS0cnX.d.ts +8 -0
- package/package.json +132 -4
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true});
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
var _chunkK7E4LWULcjs = require('./chunk-K7E4LWUL.cjs');
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
var _chunkNBDAXDB6cjs = require('./chunk-NBDAXDB6.cjs');
|
|
10
|
+
|
|
11
|
+
// src/calendar.ts
|
|
12
|
+
function parseCutoff(cutoff) {
|
|
13
|
+
if (cutoff === void 0) return 0;
|
|
14
|
+
const match = /^(\d{1,2}):(\d{2})$/.exec(cutoff.trim());
|
|
15
|
+
if (match === null || match[1] === void 0 || match[2] === void 0) {
|
|
16
|
+
throw new TypeError(`[fatigue-kit] invalid cutoff: "${cutoff}". Expected 'HH:mm'.`);
|
|
17
|
+
}
|
|
18
|
+
const hours = Number(match[1]);
|
|
19
|
+
const minutes = Number(match[2]);
|
|
20
|
+
if (hours > 23 || minutes > 59) {
|
|
21
|
+
throw new TypeError(
|
|
22
|
+
`[fatigue-kit] invalid cutoff: "${cutoff}". Hours must be 0-23, minutes 0-59.`
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
return hours * 60 + minutes;
|
|
26
|
+
}
|
|
27
|
+
function localMidnight(ts) {
|
|
28
|
+
const date = new Date(ts);
|
|
29
|
+
date.setHours(0, 0, 0, 0);
|
|
30
|
+
return date;
|
|
31
|
+
}
|
|
32
|
+
function naturalPeriodStart(ts, spec) {
|
|
33
|
+
switch (spec.every) {
|
|
34
|
+
case "day":
|
|
35
|
+
return localMidnight(ts).getTime();
|
|
36
|
+
case "week": {
|
|
37
|
+
const date = localMidnight(ts);
|
|
38
|
+
const offset = (date.getDay() - spec.weekStartsOn + 7) % 7;
|
|
39
|
+
date.setDate(date.getDate() - offset);
|
|
40
|
+
return date.getTime();
|
|
41
|
+
}
|
|
42
|
+
case "month": {
|
|
43
|
+
const date = new Date(ts);
|
|
44
|
+
return new Date(date.getFullYear(), date.getMonth(), 1).getTime();
|
|
45
|
+
}
|
|
46
|
+
case "year": {
|
|
47
|
+
const date = new Date(ts);
|
|
48
|
+
return new Date(date.getFullYear(), 0, 1).getTime();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function previousNaturalStart(naturalStart, spec) {
|
|
53
|
+
switch (spec.every) {
|
|
54
|
+
case "day": {
|
|
55
|
+
const date = new Date(naturalStart);
|
|
56
|
+
date.setDate(date.getDate() - 1);
|
|
57
|
+
return date.getTime();
|
|
58
|
+
}
|
|
59
|
+
case "week": {
|
|
60
|
+
const date = new Date(naturalStart);
|
|
61
|
+
date.setDate(date.getDate() - 7);
|
|
62
|
+
return date.getTime();
|
|
63
|
+
}
|
|
64
|
+
case "month": {
|
|
65
|
+
const date = new Date(naturalStart);
|
|
66
|
+
return new Date(date.getFullYear(), date.getMonth() - 1, 1).getTime();
|
|
67
|
+
}
|
|
68
|
+
case "year": {
|
|
69
|
+
const date = new Date(naturalStart);
|
|
70
|
+
return new Date(date.getFullYear() - 1, 0, 1).getTime();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function wallClockBoundary(midnight, cutoffMinutes) {
|
|
75
|
+
const date = new Date(midnight);
|
|
76
|
+
return new Date(
|
|
77
|
+
date.getFullYear(),
|
|
78
|
+
date.getMonth(),
|
|
79
|
+
date.getDate(),
|
|
80
|
+
Math.floor(cutoffMinutes / 60),
|
|
81
|
+
cutoffMinutes % 60
|
|
82
|
+
).getTime();
|
|
83
|
+
}
|
|
84
|
+
function calendarPeriodStart(ts, spec) {
|
|
85
|
+
const natural = naturalPeriodStart(ts, spec);
|
|
86
|
+
const boundary = wallClockBoundary(natural, spec.cutoffMinutes);
|
|
87
|
+
if (boundary <= ts) return boundary;
|
|
88
|
+
return wallClockBoundary(previousNaturalStart(natural, spec), spec.cutoffMinutes);
|
|
89
|
+
}
|
|
90
|
+
var JUMP_MS = {
|
|
91
|
+
day: 12 * 36e5,
|
|
92
|
+
week: 8 * 864e5,
|
|
93
|
+
month: 8 * 864e5,
|
|
94
|
+
year: 32 * 864e5
|
|
95
|
+
};
|
|
96
|
+
function calendarNextPeriodStart(ts, spec) {
|
|
97
|
+
const jump = JUMP_MS[spec.every];
|
|
98
|
+
let cursor = ts;
|
|
99
|
+
for (let i = 0; i < 50; i++) {
|
|
100
|
+
cursor += jump;
|
|
101
|
+
const start = calendarPeriodStart(cursor, spec);
|
|
102
|
+
if (start > ts) return start;
|
|
103
|
+
}
|
|
104
|
+
throw new Error("[fatigue-kit] failed to compute the next calendar period");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/duration.ts
|
|
108
|
+
var UNITS = {
|
|
109
|
+
s: 1e3,
|
|
110
|
+
m: 6e4,
|
|
111
|
+
h: 36e5,
|
|
112
|
+
d: 864e5
|
|
113
|
+
};
|
|
114
|
+
function parseDuration(duration) {
|
|
115
|
+
if (typeof duration === "number") {
|
|
116
|
+
if (!Number.isFinite(duration) || duration <= 0) {
|
|
117
|
+
throw new TypeError(`[fatigue-kit] invalid duration: ${duration}`);
|
|
118
|
+
}
|
|
119
|
+
return duration;
|
|
120
|
+
}
|
|
121
|
+
const match = /^(\d+(?:\.\d+)?)\s*([smhd])$/.exec(duration.trim());
|
|
122
|
+
if (match === null || match[1] === void 0 || match[2] === void 0) {
|
|
123
|
+
throw new TypeError(
|
|
124
|
+
`[fatigue-kit] invalid duration: "${duration}". Expected a positive number of ms, or '45s' / '30m' / '12h' / '7d'.`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
const unitMs = UNITS[match[2]];
|
|
128
|
+
if (unitMs === void 0) {
|
|
129
|
+
throw new TypeError(`[fatigue-kit] invalid duration: "${duration}"`);
|
|
130
|
+
}
|
|
131
|
+
const ms = Number(match[1]) * unitMs;
|
|
132
|
+
if (!(ms > 0)) {
|
|
133
|
+
throw new TypeError(`[fatigue-kit] invalid duration: "${duration}"`);
|
|
134
|
+
}
|
|
135
|
+
return ms;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/engine.ts
|
|
139
|
+
var DEFAULT_NAMESPACE = "fatigue-kit";
|
|
140
|
+
var DEFAULT_MAX_HITS = 100;
|
|
141
|
+
function assertNonEmpty(value, label) {
|
|
142
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
143
|
+
throw new TypeError(`[fatigue-kit] ${label} must be a non-empty string`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function requireMax(max) {
|
|
147
|
+
if (!Number.isInteger(max) || max < 1) {
|
|
148
|
+
throw new TypeError(`[fatigue-kit] rule "max" must be a positive integer, got ${max}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function compileRules(input) {
|
|
152
|
+
const list = Array.isArray(input) ? input : [input];
|
|
153
|
+
if (list.length === 0) {
|
|
154
|
+
throw new TypeError("[fatigue-kit] at least one rule is required");
|
|
155
|
+
}
|
|
156
|
+
return list.map((rule) => {
|
|
157
|
+
var _a;
|
|
158
|
+
if (rule === null || typeof rule !== "object") {
|
|
159
|
+
throw new TypeError(`[fatigue-kit] unknown rule: ${JSON.stringify(rule)}`);
|
|
160
|
+
}
|
|
161
|
+
if ("window" in rule) {
|
|
162
|
+
requireMax(rule.max);
|
|
163
|
+
return { kind: "sliding", rule, max: rule.max, windowMs: parseDuration(rule.window) };
|
|
164
|
+
}
|
|
165
|
+
if ("every" in rule) {
|
|
166
|
+
requireMax(rule.max);
|
|
167
|
+
if (rule.every !== "day" && rule.every !== "week" && rule.every !== "month" && rule.every !== "year") {
|
|
168
|
+
throw new TypeError(`[fatigue-kit] invalid "every": ${JSON.stringify(rule.every)}`);
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
kind: "calendar",
|
|
172
|
+
rule,
|
|
173
|
+
max: rule.max,
|
|
174
|
+
spec: {
|
|
175
|
+
every: rule.every,
|
|
176
|
+
cutoffMinutes: parseCutoff(rule.cutoff),
|
|
177
|
+
weekStartsOn: (_a = rule.weekStartsOn) != null ? _a : 1
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
if ("cooldown" in rule) {
|
|
182
|
+
return { kind: "cooldown", rule, cooldownMs: parseDuration(rule.cooldown) };
|
|
183
|
+
}
|
|
184
|
+
if ("max" in rule) {
|
|
185
|
+
requireMax(rule.max);
|
|
186
|
+
return { kind: "lifetime", rule, max: rule.max };
|
|
187
|
+
}
|
|
188
|
+
throw new TypeError(`[fatigue-kit] unknown rule: ${JSON.stringify(rule)}`);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
function outcomeFor(compiled, record, now) {
|
|
192
|
+
switch (compiled.kind) {
|
|
193
|
+
case "sliding": {
|
|
194
|
+
const from = now - compiled.windowMs;
|
|
195
|
+
let count = 0;
|
|
196
|
+
let oldest = Number.POSITIVE_INFINITY;
|
|
197
|
+
for (const hit of record.hits) {
|
|
198
|
+
if (hit > from) {
|
|
199
|
+
count++;
|
|
200
|
+
if (hit < oldest) oldest = hit;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const remaining = compiled.max - count;
|
|
204
|
+
return {
|
|
205
|
+
remaining,
|
|
206
|
+
nextAvailableAt: remaining <= 0 && oldest !== Number.POSITIVE_INFINITY ? oldest + compiled.windowMs : void 0
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
case "calendar": {
|
|
210
|
+
const start = calendarPeriodStart(now, compiled.spec);
|
|
211
|
+
let count = 0;
|
|
212
|
+
for (const hit of record.hits) {
|
|
213
|
+
if (hit >= start) count++;
|
|
214
|
+
}
|
|
215
|
+
const remaining = compiled.max - count;
|
|
216
|
+
return {
|
|
217
|
+
remaining,
|
|
218
|
+
nextAvailableAt: remaining <= 0 ? calendarNextPeriodStart(now, compiled.spec) : void 0
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
case "cooldown": {
|
|
222
|
+
const lastHit = record.hits.length > 0 ? record.hits[record.hits.length - 1] : void 0;
|
|
223
|
+
if (lastHit === void 0) return { remaining: Number.POSITIVE_INFINITY };
|
|
224
|
+
const readyAt = lastHit + compiled.cooldownMs;
|
|
225
|
+
return {
|
|
226
|
+
remaining: Number.POSITIVE_INFINITY,
|
|
227
|
+
nextAvailableAt: readyAt > now ? readyAt : void 0
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
case "lifetime": {
|
|
231
|
+
return { remaining: compiled.max - record.lifetime };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function evaluate(record, compiled, now) {
|
|
236
|
+
let remaining = Number.POSITIVE_INFINITY;
|
|
237
|
+
let nextAvailableAt;
|
|
238
|
+
let deniedBy;
|
|
239
|
+
for (const rule of compiled) {
|
|
240
|
+
const outcome = outcomeFor(rule, record, now);
|
|
241
|
+
if (Number.isFinite(outcome.remaining)) {
|
|
242
|
+
remaining = Math.min(remaining, outcome.remaining);
|
|
243
|
+
}
|
|
244
|
+
if (outcome.nextAvailableAt !== void 0 && (nextAvailableAt === void 0 || outcome.nextAvailableAt > nextAvailableAt)) {
|
|
245
|
+
nextAvailableAt = outcome.nextAvailableAt;
|
|
246
|
+
}
|
|
247
|
+
const ruleAllows = outcome.remaining > 0 && (outcome.nextAvailableAt === void 0 || outcome.nextAvailableAt <= now);
|
|
248
|
+
if (!ruleAllows) deniedBy != null ? deniedBy : deniedBy = rule.rule;
|
|
249
|
+
}
|
|
250
|
+
const result = {
|
|
251
|
+
allowed: deniedBy === void 0,
|
|
252
|
+
remaining: Math.max(0, remaining)
|
|
253
|
+
};
|
|
254
|
+
if (nextAvailableAt !== void 0) result.nextAvailableAt = nextAvailableAt;
|
|
255
|
+
if (deniedBy !== void 0) result.deniedBy = deniedBy;
|
|
256
|
+
return result;
|
|
257
|
+
}
|
|
258
|
+
function keepFrom(compiled, now) {
|
|
259
|
+
let keep = now;
|
|
260
|
+
for (const rule of compiled) {
|
|
261
|
+
if (rule.kind === "sliding") keep = Math.min(keep, now - rule.windowMs);
|
|
262
|
+
else if (rule.kind === "calendar") keep = Math.min(keep, calendarPeriodStart(now, rule.spec));
|
|
263
|
+
else if (rule.kind === "cooldown") keep = Math.min(keep, now - rule.cooldownMs);
|
|
264
|
+
}
|
|
265
|
+
return keep;
|
|
266
|
+
}
|
|
267
|
+
function createFatigue(options = {}) {
|
|
268
|
+
var _a, _b, _c, _d;
|
|
269
|
+
const storage = (_a = options.storage) != null ? _a : _chunkNBDAXDB6cjs.memoryAdapter.call(void 0, );
|
|
270
|
+
const clock = (_b = options.clock) != null ? _b : { now: () => Date.now() };
|
|
271
|
+
const namespace = (_c = options.namespace) != null ? _c : DEFAULT_NAMESPACE;
|
|
272
|
+
const maxHits = (_d = options.maxHits) != null ? _d : DEFAULT_MAX_HITS;
|
|
273
|
+
assertNonEmpty(namespace, "namespace");
|
|
274
|
+
if (!Number.isInteger(maxHits) || maxHits < 1) {
|
|
275
|
+
throw new TypeError(`[fatigue-kit] maxHits must be a positive integer, got ${maxHits}`);
|
|
276
|
+
}
|
|
277
|
+
const storageKey = (key) => {
|
|
278
|
+
assertNonEmpty(key, "key");
|
|
279
|
+
return `${namespace}:${key}`;
|
|
280
|
+
};
|
|
281
|
+
const read = (key) => _chunkK7E4LWULcjs.decodeRecord.call(void 0, storage.get(storageKey(key)));
|
|
282
|
+
const tryConsume = (key, rules) => {
|
|
283
|
+
const compiled = compileRules(rules);
|
|
284
|
+
const now = clock.now();
|
|
285
|
+
const record = read(key);
|
|
286
|
+
const result = evaluate(record, compiled, now);
|
|
287
|
+
if (result.allowed) {
|
|
288
|
+
if (Number.isFinite(result.remaining)) {
|
|
289
|
+
result.remaining = Math.max(0, result.remaining - 1);
|
|
290
|
+
}
|
|
291
|
+
record.hits.push(now);
|
|
292
|
+
record.lifetime += 1;
|
|
293
|
+
_chunkK7E4LWULcjs.pruneRecord.call(void 0, record, keepFrom(compiled, now), maxHits);
|
|
294
|
+
storage.set(storageKey(key), _chunkK7E4LWULcjs.encodeRecord.call(void 0, record));
|
|
295
|
+
}
|
|
296
|
+
return result;
|
|
297
|
+
};
|
|
298
|
+
const peek = (key, rules) => {
|
|
299
|
+
const compiled = compileRules(rules);
|
|
300
|
+
return evaluate(read(key), compiled, clock.now());
|
|
301
|
+
};
|
|
302
|
+
const reset = (key) => {
|
|
303
|
+
if (key !== void 0) {
|
|
304
|
+
storage.remove(storageKey(key));
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (typeof storage.keys !== "function") {
|
|
308
|
+
throw new Error(
|
|
309
|
+
"[fatigue-kit] reset() without a key requires a storage adapter implementing keys()"
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
const prefix = `${namespace}:`;
|
|
313
|
+
for (const storedKey of storage.keys()) {
|
|
314
|
+
if (storedKey.startsWith(prefix)) storage.remove(storedKey);
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
const exportRecord = (key) => {
|
|
318
|
+
const raw = storage.get(storageKey(key));
|
|
319
|
+
return raw ? _chunkK7E4LWULcjs.decodeRecord.call(void 0, raw) : null;
|
|
320
|
+
};
|
|
321
|
+
const importRecord = (key, record) => {
|
|
322
|
+
var _a2;
|
|
323
|
+
const hits = ((_a2 = record.hits) != null ? _a2 : []).filter((hit) => typeof hit === "number" && Number.isFinite(hit)).sort((a, b) => a - b);
|
|
324
|
+
const lifetime = typeof record.lifetime === "number" && Number.isFinite(record.lifetime) && record.lifetime >= 0 ? record.lifetime : hits.length;
|
|
325
|
+
storage.set(storageKey(key), _chunkK7E4LWULcjs.encodeRecord.call(void 0, { v: _chunkK7E4LWULcjs.RECORD_VERSION, hits, lifetime }));
|
|
326
|
+
};
|
|
327
|
+
return { tryConsume, peek, reset, exportRecord, importRecord };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
exports.RECORD_VERSION = _chunkK7E4LWULcjs.RECORD_VERSION; exports.createFatigue = createFatigue; exports.memoryAdapter = _chunkNBDAXDB6cjs.memoryAdapter;
|
|
334
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/fatigue-kit/fatigue-kit/dist/index.cjs","../src/calendar.ts","../src/duration.ts","../src/engine.ts"],"names":["_a"],"mappings":"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","file":"/home/runner/work/fatigue-kit/fatigue-kit/dist/index.cjs","sourcesContent":[null,"export type CalendarEvery = 'day' | 'week' | 'month' | 'year'\n\nexport interface CalendarSpec {\n\tevery: CalendarEvery\n\tcutoffMinutes: number\n\tweekStartsOn: number\n}\n\n/** Parse a `'HH:mm'` cutoff into minutes after local midnight. Default (no cutoff) is `0`. */\nexport function parseCutoff(cutoff: string | undefined): number {\n\tif (cutoff === undefined) return 0\n\tconst match = /^(\\d{1,2}):(\\d{2})$/.exec(cutoff.trim())\n\tif (match === null || match[1] === undefined || match[2] === undefined) {\n\t\tthrow new TypeError(`[fatigue-kit] invalid cutoff: \"${cutoff}\". Expected 'HH:mm'.`)\n\t}\n\tconst hours = Number(match[1])\n\tconst minutes = Number(match[2])\n\tif (hours > 23 || minutes > 59) {\n\t\tthrow new TypeError(\n\t\t\t`[fatigue-kit] invalid cutoff: \"${cutoff}\". Hours must be 0-23, minutes 0-59.`,\n\t\t)\n\t}\n\treturn hours * 60 + minutes\n}\n\nfunction localMidnight(ts: number): Date {\n\tconst date = new Date(ts)\n\tdate.setHours(0, 0, 0, 0)\n\treturn date\n}\n\n/** Midnight-based start of the period containing `ts`, before applying the cutoff. */\nfunction naturalPeriodStart(ts: number, spec: CalendarSpec): number {\n\tswitch (spec.every) {\n\t\tcase 'day':\n\t\t\treturn localMidnight(ts).getTime()\n\t\tcase 'week': {\n\t\t\tconst date = localMidnight(ts)\n\t\t\tconst offset = (date.getDay() - spec.weekStartsOn + 7) % 7\n\t\t\tdate.setDate(date.getDate() - offset)\n\t\t\treturn date.getTime()\n\t\t}\n\t\tcase 'month': {\n\t\t\tconst date = new Date(ts)\n\t\t\treturn new Date(date.getFullYear(), date.getMonth(), 1).getTime()\n\t\t}\n\t\tcase 'year': {\n\t\t\tconst date = new Date(ts)\n\t\t\treturn new Date(date.getFullYear(), 0, 1).getTime()\n\t\t}\n\t}\n}\n\nfunction previousNaturalStart(naturalStart: number, spec: CalendarSpec): number {\n\tswitch (spec.every) {\n\t\tcase 'day': {\n\t\t\tconst date = new Date(naturalStart)\n\t\t\tdate.setDate(date.getDate() - 1)\n\t\t\treturn date.getTime()\n\t\t}\n\t\tcase 'week': {\n\t\t\tconst date = new Date(naturalStart)\n\t\t\tdate.setDate(date.getDate() - 7)\n\t\t\treturn date.getTime()\n\t\t}\n\t\tcase 'month': {\n\t\t\tconst date = new Date(naturalStart)\n\t\t\treturn new Date(date.getFullYear(), date.getMonth() - 1, 1).getTime()\n\t\t}\n\t\tcase 'year': {\n\t\t\tconst date = new Date(naturalStart)\n\t\t\treturn new Date(date.getFullYear() - 1, 0, 1).getTime()\n\t\t}\n\t}\n}\n\n/**\n * Local wall-clock time of `cutoffMinutes` past midnight, built from date\n * components. On DST transition days \"midnight epoch + fixed minutes\" would\n * land an hour off wall-clock time (e.g. after spring-forward a 04:00 cutoff\n * computed by addition would actually fire at 05:00), so the boundary must be\n * constructed, never added.\n */\nfunction wallClockBoundary(midnight: number, cutoffMinutes: number): number {\n\tconst date = new Date(midnight)\n\treturn new Date(\n\t\tdate.getFullYear(),\n\t\tdate.getMonth(),\n\t\tdate.getDate(),\n\t\tMath.floor(cutoffMinutes / 60),\n\t\tcutoffMinutes % 60,\n\t).getTime()\n}\n\n/**\n * Start of the calendar period containing `ts`, cutoff applied.\n * All arithmetic works on local date components (never `+ 24h`), so DST\n * transitions cannot shift a boundary.\n */\nexport function calendarPeriodStart(ts: number, spec: CalendarSpec): number {\n\tconst natural = naturalPeriodStart(ts, spec)\n\tconst boundary = wallClockBoundary(natural, spec.cutoffMinutes)\n\tif (boundary <= ts) return boundary\n\treturn wallClockBoundary(previousNaturalStart(natural, spec), spec.cutoffMinutes)\n}\n\n// Per-step cursors used to find the *next* boundary. Each jump is guaranteed to\n// be shorter than one period (so no period can be skipped) and long enough to\n// make progress; the loop simply walks forward until the boundary passes `ts`.\nconst JUMP_MS: Record<CalendarEvery, number> = {\n\tday: 12 * 3_600_000,\n\tweek: 8 * 86_400_000,\n\tmonth: 8 * 86_400_000,\n\tyear: 32 * 86_400_000,\n}\n\n/** Start of the calendar period following the one containing `ts`. Strictly greater than `ts`. */\nexport function calendarNextPeriodStart(ts: number, spec: CalendarSpec): number {\n\tconst jump = JUMP_MS[spec.every]\n\tlet cursor = ts\n\tfor (let i = 0; i < 50; i++) {\n\t\tcursor += jump\n\t\tconst start = calendarPeriodStart(cursor, spec)\n\t\tif (start > ts) return start\n\t}\n\tthrow new Error('[fatigue-kit] failed to compute the next calendar period')\n}\n","import type { Duration } from './types'\n\nconst UNITS: Record<string, number> = {\n\ts: 1_000,\n\tm: 60_000,\n\th: 3_600_000,\n\td: 86_400_000,\n}\n\n/** Parse a {@link Duration} into milliseconds. Throws `TypeError` on invalid input. */\nexport function parseDuration(duration: Duration): number {\n\tif (typeof duration === 'number') {\n\t\tif (!Number.isFinite(duration) || duration <= 0) {\n\t\t\tthrow new TypeError(`[fatigue-kit] invalid duration: ${duration}`)\n\t\t}\n\t\treturn duration\n\t}\n\tconst match = /^(\\d+(?:\\.\\d+)?)\\s*([smhd])$/.exec(duration.trim())\n\tif (match === null || match[1] === undefined || match[2] === undefined) {\n\t\tthrow new TypeError(\n\t\t\t`[fatigue-kit] invalid duration: \"${duration}\". Expected a positive number of ms, or '45s' / '30m' / '12h' / '7d'.`,\n\t\t)\n\t}\n\tconst unitMs = UNITS[match[2]]\n\tif (unitMs === undefined) {\n\t\tthrow new TypeError(`[fatigue-kit] invalid duration: \"${duration}\"`)\n\t}\n\tconst ms = Number(match[1]) * unitMs\n\tif (!(ms > 0)) {\n\t\tthrow new TypeError(`[fatigue-kit] invalid duration: \"${duration}\"`)\n\t}\n\treturn ms\n}\n","import { memoryAdapter } from './adapters/memory'\nimport {\n\ttype CalendarSpec,\n\tcalendarNextPeriodStart,\n\tcalendarPeriodStart,\n\tparseCutoff,\n} from './calendar'\nimport { parseDuration } from './duration'\nimport { decodeRecord, encodeRecord, pruneRecord, RECORD_VERSION } from './record'\nimport type {\n\tCalendarRule,\n\tClock,\n\tConsumeResult,\n\tCooldownRule,\n\tFatigue,\n\tFatigueOptions,\n\tFatigueRecord,\n\tFatigueRule,\n\tLifetimeRule,\n\tSlidingWindowRule,\n\tSyncStorage,\n} from './types'\n\nconst DEFAULT_NAMESPACE = 'fatigue-kit'\nconst DEFAULT_MAX_HITS = 100\n\n// Rules are compiled once per call so duration strings are parsed a single time.\ntype CompiledRule =\n\t| { kind: 'sliding'; rule: SlidingWindowRule; max: number; windowMs: number }\n\t| { kind: 'calendar'; rule: CalendarRule; max: number; spec: CalendarSpec }\n\t| { kind: 'cooldown'; rule: CooldownRule; cooldownMs: number }\n\t| { kind: 'lifetime'; rule: LifetimeRule; max: number }\n\ninterface RuleOutcome {\n\tremaining: number\n\tnextAvailableAt?: number\n}\n\nfunction assertNonEmpty(value: string, label: string): void {\n\tif (typeof value !== 'string' || value.length === 0) {\n\t\tthrow new TypeError(`[fatigue-kit] ${label} must be a non-empty string`)\n\t}\n}\n\nfunction requireMax(max: number): void {\n\tif (!Number.isInteger(max) || max < 1) {\n\t\tthrow new TypeError(`[fatigue-kit] rule \"max\" must be a positive integer, got ${max}`)\n\t}\n}\n\nfunction compileRules(input: FatigueRule | FatigueRule[]): CompiledRule[] {\n\tconst list = Array.isArray(input) ? input : [input]\n\tif (list.length === 0) {\n\t\tthrow new TypeError('[fatigue-kit] at least one rule is required')\n\t}\n\treturn list.map((rule): CompiledRule => {\n\t\tif (rule === null || typeof rule !== 'object') {\n\t\t\tthrow new TypeError(`[fatigue-kit] unknown rule: ${JSON.stringify(rule)}`)\n\t\t}\n\t\tif ('window' in rule) {\n\t\t\trequireMax(rule.max)\n\t\t\treturn { kind: 'sliding', rule, max: rule.max, windowMs: parseDuration(rule.window) }\n\t\t}\n\t\tif ('every' in rule) {\n\t\t\trequireMax(rule.max)\n\t\t\tif (\n\t\t\t\trule.every !== 'day' &&\n\t\t\t\trule.every !== 'week' &&\n\t\t\t\trule.every !== 'month' &&\n\t\t\t\trule.every !== 'year'\n\t\t\t) {\n\t\t\t\tthrow new TypeError(`[fatigue-kit] invalid \"every\": ${JSON.stringify(rule.every)}`)\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tkind: 'calendar',\n\t\t\t\trule,\n\t\t\t\tmax: rule.max,\n\t\t\t\tspec: {\n\t\t\t\t\tevery: rule.every,\n\t\t\t\t\tcutoffMinutes: parseCutoff(rule.cutoff),\n\t\t\t\t\tweekStartsOn: rule.weekStartsOn ?? 1,\n\t\t\t\t},\n\t\t\t}\n\t\t}\n\t\tif ('cooldown' in rule) {\n\t\t\treturn { kind: 'cooldown', rule, cooldownMs: parseDuration(rule.cooldown) }\n\t\t}\n\t\tif ('max' in rule) {\n\t\t\trequireMax(rule.max)\n\t\t\treturn { kind: 'lifetime', rule, max: rule.max }\n\t\t}\n\t\tthrow new TypeError(`[fatigue-kit] unknown rule: ${JSON.stringify(rule)}`)\n\t})\n}\n\nfunction outcomeFor(compiled: CompiledRule, record: FatigueRecord, now: number): RuleOutcome {\n\tswitch (compiled.kind) {\n\t\tcase 'sliding': {\n\t\t\tconst from = now - compiled.windowMs\n\t\t\tlet count = 0\n\t\t\tlet oldest = Number.POSITIVE_INFINITY\n\t\t\tfor (const hit of record.hits) {\n\t\t\t\tif (hit > from) {\n\t\t\t\t\tcount++\n\t\t\t\t\tif (hit < oldest) oldest = hit\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst remaining = compiled.max - count\n\t\t\treturn {\n\t\t\t\tremaining,\n\t\t\t\tnextAvailableAt:\n\t\t\t\t\tremaining <= 0 && oldest !== Number.POSITIVE_INFINITY\n\t\t\t\t\t\t? oldest + compiled.windowMs\n\t\t\t\t\t\t: undefined,\n\t\t\t}\n\t\t}\n\t\tcase 'calendar': {\n\t\t\tconst start = calendarPeriodStart(now, compiled.spec)\n\t\t\tlet count = 0\n\t\t\tfor (const hit of record.hits) {\n\t\t\t\tif (hit >= start) count++\n\t\t\t}\n\t\t\tconst remaining = compiled.max - count\n\t\t\treturn {\n\t\t\t\tremaining,\n\t\t\t\tnextAvailableAt: remaining <= 0 ? calendarNextPeriodStart(now, compiled.spec) : undefined,\n\t\t\t}\n\t\t}\n\t\tcase 'cooldown': {\n\t\t\tconst lastHit = record.hits.length > 0 ? record.hits[record.hits.length - 1] : undefined\n\t\t\tif (lastHit === undefined) return { remaining: Number.POSITIVE_INFINITY }\n\t\t\tconst readyAt = lastHit + compiled.cooldownMs\n\t\t\treturn {\n\t\t\t\tremaining: Number.POSITIVE_INFINITY,\n\t\t\t\tnextAvailableAt: readyAt > now ? readyAt : undefined,\n\t\t\t}\n\t\t}\n\t\tcase 'lifetime': {\n\t\t\treturn { remaining: compiled.max - record.lifetime }\n\t\t}\n\t}\n}\n\nfunction evaluate(record: FatigueRecord, compiled: CompiledRule[], now: number): ConsumeResult {\n\tlet remaining = Number.POSITIVE_INFINITY\n\tlet nextAvailableAt: number | undefined\n\tlet deniedBy: FatigueRule | undefined\n\tfor (const rule of compiled) {\n\t\tconst outcome = outcomeFor(rule, record, now)\n\t\tif (Number.isFinite(outcome.remaining)) {\n\t\t\tremaining = Math.min(remaining, outcome.remaining)\n\t\t}\n\t\tif (\n\t\t\toutcome.nextAvailableAt !== undefined &&\n\t\t\t(nextAvailableAt === undefined || outcome.nextAvailableAt > nextAvailableAt)\n\t\t) {\n\t\t\tnextAvailableAt = outcome.nextAvailableAt\n\t\t}\n\t\tconst ruleAllows =\n\t\t\toutcome.remaining > 0 &&\n\t\t\t(outcome.nextAvailableAt === undefined || outcome.nextAvailableAt <= now)\n\t\tif (!ruleAllows) deniedBy ??= rule.rule\n\t}\n\tconst result: ConsumeResult = {\n\t\tallowed: deniedBy === undefined,\n\t\tremaining: Math.max(0, remaining),\n\t}\n\tif (nextAvailableAt !== undefined) result.nextAvailableAt = nextAvailableAt\n\tif (deniedBy !== undefined) result.deniedBy = deniedBy\n\treturn result\n}\n\n/**\n * Oldest timestamp that still has to be kept to evaluate the rules. Used to\n * prune the hit list on write: anything older than this is dead weight, and the\n * `lifetime` counter keeps lifetime rules working regardless.\n */\nfunction keepFrom(compiled: CompiledRule[], now: number): number {\n\tlet keep = now\n\tfor (const rule of compiled) {\n\t\tif (rule.kind === 'sliding') keep = Math.min(keep, now - rule.windowMs)\n\t\telse if (rule.kind === 'calendar') keep = Math.min(keep, calendarPeriodStart(now, rule.spec))\n\t\telse if (rule.kind === 'cooldown') keep = Math.min(keep, now - rule.cooldownMs)\n\t}\n\treturn keep\n}\n\n/**\n * Create a fatigue (frequency capping) instance.\n *\n * ```ts\n * const fatigue = createFatigue({ storage: localStorageAdapter() })\n * const { allowed } = fatigue.tryConsume('popup', [{ max: 1, every: 'day' }])\n * ```\n */\nexport function createFatigue(options: FatigueOptions = {}): Fatigue {\n\tconst storage: SyncStorage = options.storage ?? memoryAdapter()\n\tconst clock: Clock = options.clock ?? { now: () => Date.now() }\n\tconst namespace = options.namespace ?? DEFAULT_NAMESPACE\n\tconst maxHits = options.maxHits ?? DEFAULT_MAX_HITS\n\tassertNonEmpty(namespace, 'namespace')\n\tif (!Number.isInteger(maxHits) || maxHits < 1) {\n\t\tthrow new TypeError(`[fatigue-kit] maxHits must be a positive integer, got ${maxHits}`)\n\t}\n\n\tconst storageKey = (key: string): string => {\n\t\tassertNonEmpty(key, 'key')\n\t\treturn `${namespace}:${key}`\n\t}\n\n\tconst read = (key: string): FatigueRecord => decodeRecord(storage.get(storageKey(key)))\n\n\tconst tryConsume = (key: string, rules: FatigueRule | FatigueRule[]): ConsumeResult => {\n\t\tconst compiled = compileRules(rules)\n\t\tconst now = clock.now()\n\t\tconst record = read(key)\n\t\tconst result = evaluate(record, compiled, now)\n\t\tif (result.allowed) {\n\t\t\t// `remaining` describes the state *after* the call, so `remaining === 0`\n\t\t\t// means \"this was the last allowed consumption\".\n\t\t\tif (Number.isFinite(result.remaining)) {\n\t\t\t\tresult.remaining = Math.max(0, result.remaining - 1)\n\t\t\t}\n\t\t\trecord.hits.push(now)\n\t\t\trecord.lifetime += 1\n\t\t\tpruneRecord(record, keepFrom(compiled, now), maxHits)\n\t\t\tstorage.set(storageKey(key), encodeRecord(record))\n\t\t}\n\t\treturn result\n\t}\n\n\tconst peek = (key: string, rules: FatigueRule | FatigueRule[]): ConsumeResult => {\n\t\tconst compiled = compileRules(rules)\n\t\treturn evaluate(read(key), compiled, clock.now())\n\t}\n\n\tconst reset = (key?: string): void => {\n\t\tif (key !== undefined) {\n\t\t\tstorage.remove(storageKey(key))\n\t\t\treturn\n\t\t}\n\t\tif (typeof storage.keys !== 'function') {\n\t\t\tthrow new Error(\n\t\t\t\t'[fatigue-kit] reset() without a key requires a storage adapter implementing keys()',\n\t\t\t)\n\t\t}\n\t\tconst prefix = `${namespace}:`\n\t\tfor (const storedKey of storage.keys()) {\n\t\t\tif (storedKey.startsWith(prefix)) storage.remove(storedKey)\n\t\t}\n\t}\n\n\tconst exportRecord = (key: string): FatigueRecord | null => {\n\t\tconst raw = storage.get(storageKey(key))\n\t\treturn raw ? decodeRecord(raw) : null\n\t}\n\n\tconst importRecord = (key: string, record: Partial<FatigueRecord>): void => {\n\t\tconst hits = (record.hits ?? [])\n\t\t\t.filter((hit): hit is number => typeof hit === 'number' && Number.isFinite(hit))\n\t\t\t.sort((a, b) => a - b)\n\t\tconst lifetime =\n\t\t\ttypeof record.lifetime === 'number' &&\n\t\t\tNumber.isFinite(record.lifetime) &&\n\t\t\trecord.lifetime >= 0\n\t\t\t\t? record.lifetime\n\t\t\t\t: hits.length\n\t\tstorage.set(storageKey(key), encodeRecord({ v: RECORD_VERSION, hits, lifetime }))\n\t}\n\n\treturn { tryConsume, peek, reset, exportRecord, importRecord }\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { memoryAdapter } from './adapters/memory.cjs';
|
|
2
|
+
import { F as FatigueOptions, a as Fatigue } from './types-hDxN5bc3.cjs';
|
|
3
|
+
export { C as CalendarRule, b as Clock, c as ConsumeResult, d as CooldownRule, D as Duration, e as FatigueRecord, f as FatigueRule, L as LifetimeRule, g as SlidingWindowRule, S as SyncStorage } from './types-hDxN5bc3.cjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create a fatigue (frequency capping) instance.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* const fatigue = createFatigue({ storage: localStorageAdapter() })
|
|
10
|
+
* const { allowed } = fatigue.tryConsume('popup', [{ max: 1, every: 'day' }])
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
declare function createFatigue(options?: FatigueOptions): Fatigue;
|
|
14
|
+
|
|
15
|
+
declare const RECORD_VERSION = 1;
|
|
16
|
+
|
|
17
|
+
export { Fatigue, FatigueOptions, RECORD_VERSION, createFatigue };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { memoryAdapter } from './adapters/memory.js';
|
|
2
|
+
import { F as FatigueOptions, a as Fatigue } from './types-hDxN5bc3.js';
|
|
3
|
+
export { C as CalendarRule, b as Clock, c as ConsumeResult, d as CooldownRule, D as Duration, e as FatigueRecord, f as FatigueRule, L as LifetimeRule, g as SlidingWindowRule, S as SyncStorage } from './types-hDxN5bc3.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create a fatigue (frequency capping) instance.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* const fatigue = createFatigue({ storage: localStorageAdapter() })
|
|
10
|
+
* const { allowed } = fatigue.tryConsume('popup', [{ max: 1, every: 'day' }])
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
declare function createFatigue(options?: FatigueOptions): Fatigue;
|
|
14
|
+
|
|
15
|
+
declare const RECORD_VERSION = 1;
|
|
16
|
+
|
|
17
|
+
export { Fatigue, FatigueOptions, RECORD_VERSION, createFatigue };
|