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.
Files changed (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +159 -2
  3. package/README.zh-CN.md +160 -0
  4. package/dist/adapters/alipay.cjs +52 -0
  5. package/dist/adapters/alipay.cjs.map +1 -0
  6. package/dist/adapters/alipay.d.cts +27 -0
  7. package/dist/adapters/alipay.d.ts +27 -0
  8. package/dist/adapters/alipay.js +52 -0
  9. package/dist/adapters/alipay.js.map +1 -0
  10. package/dist/adapters/async-storage.cjs +93 -0
  11. package/dist/adapters/async-storage.cjs.map +1 -0
  12. package/dist/adapters/async-storage.d.cts +48 -0
  13. package/dist/adapters/async-storage.d.ts +48 -0
  14. package/dist/adapters/async-storage.js +93 -0
  15. package/dist/adapters/async-storage.js.map +1 -0
  16. package/dist/adapters/local-storage.cjs +24 -0
  17. package/dist/adapters/local-storage.cjs.map +1 -0
  18. package/dist/adapters/local-storage.d.cts +14 -0
  19. package/dist/adapters/local-storage.d.ts +14 -0
  20. package/dist/adapters/local-storage.js +24 -0
  21. package/dist/adapters/local-storage.js.map +1 -0
  22. package/dist/adapters/memory.cjs +7 -0
  23. package/dist/adapters/memory.cjs.map +1 -0
  24. package/dist/adapters/memory.d.cts +14 -0
  25. package/dist/adapters/memory.d.ts +14 -0
  26. package/dist/adapters/memory.js +7 -0
  27. package/dist/adapters/memory.js.map +1 -0
  28. package/dist/adapters/node-file.cjs +53 -0
  29. package/dist/adapters/node-file.cjs.map +1 -0
  30. package/dist/adapters/node-file.d.cts +15 -0
  31. package/dist/adapters/node-file.d.ts +15 -0
  32. package/dist/adapters/node-file.js +53 -0
  33. package/dist/adapters/node-file.js.map +1 -0
  34. package/dist/adapters/session-storage.cjs +24 -0
  35. package/dist/adapters/session-storage.cjs.map +1 -0
  36. package/dist/adapters/session-storage.d.cts +14 -0
  37. package/dist/adapters/session-storage.d.ts +14 -0
  38. package/dist/adapters/session-storage.js +24 -0
  39. package/dist/adapters/session-storage.js.map +1 -0
  40. package/dist/adapters/weapp.cjs +51 -0
  41. package/dist/adapters/weapp.cjs.map +1 -0
  42. package/dist/adapters/weapp.d.cts +23 -0
  43. package/dist/adapters/weapp.d.ts +23 -0
  44. package/dist/adapters/weapp.js +51 -0
  45. package/dist/adapters/weapp.js.map +1 -0
  46. package/dist/chunk-DABM2TVR.cjs +58 -0
  47. package/dist/chunk-DABM2TVR.cjs.map +1 -0
  48. package/dist/chunk-DM2CAKGU.js +23 -0
  49. package/dist/chunk-DM2CAKGU.js.map +1 -0
  50. package/dist/chunk-GMM2BYRQ.js +58 -0
  51. package/dist/chunk-GMM2BYRQ.js.map +1 -0
  52. package/dist/chunk-K7E4LWUL.cjs +43 -0
  53. package/dist/chunk-K7E4LWUL.cjs.map +1 -0
  54. package/dist/chunk-NBDAXDB6.cjs +23 -0
  55. package/dist/chunk-NBDAXDB6.cjs.map +1 -0
  56. package/dist/chunk-ZNIZLIUO.js +43 -0
  57. package/dist/chunk-ZNIZLIUO.js.map +1 -0
  58. package/dist/global/index.global.js +2 -0
  59. package/dist/global/index.global.js.map +1 -0
  60. package/dist/index.cjs +334 -0
  61. package/dist/index.cjs.map +1 -0
  62. package/dist/index.d.cts +17 -0
  63. package/dist/index.d.ts +17 -0
  64. package/dist/index.js +334 -0
  65. package/dist/index.js.map +1 -0
  66. package/dist/legacy.cjs +45 -0
  67. package/dist/legacy.cjs.map +1 -0
  68. package/dist/legacy.d.cts +40 -0
  69. package/dist/legacy.d.ts +40 -0
  70. package/dist/legacy.js +45 -0
  71. package/dist/legacy.js.map +1 -0
  72. package/dist/types-hDxN5bc3.d.cts +89 -0
  73. package/dist/types-hDxN5bc3.d.ts +89 -0
  74. package/dist/web-GiaS0cnX.d.cts +8 -0
  75. package/dist/web-GiaS0cnX.d.ts +8 -0
  76. package/package.json +132 -4
package/dist/index.js ADDED
@@ -0,0 +1,334 @@
1
+ import {
2
+ RECORD_VERSION,
3
+ decodeRecord,
4
+ encodeRecord,
5
+ pruneRecord
6
+ } from "./chunk-ZNIZLIUO.js";
7
+ import {
8
+ memoryAdapter
9
+ } from "./chunk-DM2CAKGU.js";
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 : memoryAdapter();
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) => decodeRecord(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
+ pruneRecord(record, keepFrom(compiled, now), maxHits);
294
+ storage.set(storageKey(key), encodeRecord(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 ? decodeRecord(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), encodeRecord({ v: RECORD_VERSION, hits, lifetime }));
326
+ };
327
+ return { tryConsume, peek, reset, exportRecord, importRecord };
328
+ }
329
+ export {
330
+ RECORD_VERSION,
331
+ createFatigue,
332
+ memoryAdapter
333
+ };
334
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/calendar.ts","../src/duration.ts","../src/engine.ts"],"sourcesContent":["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"],"mappings":"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","names":["_a"]}
@@ -0,0 +1,45 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true});
2
+
3
+ var _chunkK7E4LWULcjs = require('./chunk-K7E4LWUL.cjs');
4
+
5
+ // src/legacy.ts
6
+ var DAY_MS = 864e5;
7
+ function migrateLegacyRecord(input) {
8
+ if (input === null || input === void 0) return null;
9
+ let raw = input;
10
+ if (typeof input === "string") {
11
+ try {
12
+ raw = JSON.parse(input);
13
+ } catch (e) {
14
+ return null;
15
+ }
16
+ }
17
+ if (raw === null || typeof raw !== "object") return null;
18
+ const candidate = raw;
19
+ const lastHit = parseLegacyDate(candidate.date);
20
+ if (lastHit === null) return null;
21
+ const record = _chunkK7E4LWULcjs.emptyRecord.call(void 0, );
22
+ record.hits = [lastHit];
23
+ record.lifetime = Math.max(0, numberOr(candidate.times, 0));
24
+ const durationDays = numberOr(candidate.duration, 0);
25
+ return { record, cooldownMs: durationDays > 0 ? durationDays * DAY_MS : null };
26
+ }
27
+ function parseLegacyDate(value) {
28
+ if (typeof value !== "string") return null;
29
+ const match = /^(\d{4})[/-](\d{1,2})[/-](\d{1,2})/.exec(value.trim());
30
+ if (match === null) {
31
+ const parsed = Date.parse(value);
32
+ return Number.isNaN(parsed) ? null : parsed;
33
+ }
34
+ const [, year, month, day] = match;
35
+ if (year === void 0 || month === void 0 || day === void 0) return null;
36
+ return new Date(Number(year), Number(month) - 1, Number(day)).getTime();
37
+ }
38
+ function numberOr(value, fallback) {
39
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
40
+ }
41
+
42
+
43
+
44
+ exports.DAY_MS = DAY_MS; exports.migrateLegacyRecord = migrateLegacyRecord;
45
+ //# sourceMappingURL=legacy.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/fatigue-kit/fatigue-kit/dist/legacy.cjs","../src/legacy.ts"],"names":[],"mappings":"AAAA;AACE;AACF,wDAA6B;AAC7B;AACA;ACSO,IAAM,OAAA,EAAS,KAAA;AA4Bf,SAAS,mBAAA,CACf,KAAA,EAC8B;AAC9B,EAAA,GAAA,CAAI,MAAA,IAAU,KAAA,GAAQ,MAAA,IAAU,KAAA,CAAA,EAAW,OAAO,IAAA;AAClD,EAAA,IAAI,IAAA,EAAe,KAAA;AACnB,EAAA,GAAA,CAAI,OAAO,MAAA,IAAU,QAAA,EAAU;AAC9B,IAAA,IAAI;AACH,MAAA,IAAA,EAAM,IAAA,CAAK,KAAA,CAAM,KAAK,CAAA;AAAA,IACvB,EAAA,MAAA,CAAQ,CAAA,EAAA;AACP,MAAA,OAAO,IAAA;AAAA,IACR;AAAA,EACD;AACA,EAAA,GAAA,CAAI,IAAA,IAAQ,KAAA,GAAQ,OAAO,IAAA,IAAQ,QAAA,EAAU,OAAO,IAAA;AACpD,EAAA,MAAM,UAAA,EAAY,GAAA;AAClB,EAAA,MAAM,QAAA,EAAU,eAAA,CAAgB,SAAA,CAAU,IAAI,CAAA;AAC9C,EAAA,GAAA,CAAI,QAAA,IAAY,IAAA,EAAM,OAAO,IAAA;AAC7B,EAAA,MAAM,OAAA,EAAS,2CAAA,CAAY;AAC3B,EAAA,MAAA,CAAO,KAAA,EAAO,CAAC,OAAO,CAAA;AACtB,EAAA,MAAA,CAAO,SAAA,EAAW,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,QAAA,CAAS,SAAA,CAAU,KAAA,EAAO,CAAC,CAAC,CAAA;AAC1D,EAAA,MAAM,aAAA,EAAe,QAAA,CAAS,SAAA,CAAU,QAAA,EAAU,CAAC,CAAA;AACnD,EAAA,OAAO,EAAE,MAAA,EAAQ,UAAA,EAAY,aAAA,EAAe,EAAA,EAAI,aAAA,EAAe,OAAA,EAAS,KAAK,CAAA;AAC9E;AAEA,SAAS,eAAA,CAAgB,KAAA,EAA+B;AACvD,EAAA,GAAA,CAAI,OAAO,MAAA,IAAU,QAAA,EAAU,OAAO,IAAA;AACtC,EAAA,MAAM,MAAA,EAAQ,oCAAA,CAAqC,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,CAAC,CAAA;AACpE,EAAA,GAAA,CAAI,MAAA,IAAU,IAAA,EAAM;AACnB,IAAA,MAAM,OAAA,EAAS,IAAA,CAAK,KAAA,CAAM,KAAK,CAAA;AAC/B,IAAA,OAAO,MAAA,CAAO,KAAA,CAAM,MAAM,EAAA,EAAI,KAAA,EAAO,MAAA;AAAA,EACtC;AACA,EAAA,MAAM,CAAC,EAAE,IAAA,EAAM,KAAA,EAAO,GAAG,EAAA,EAAI,KAAA;AAC7B,EAAA,GAAA,CAAI,KAAA,IAAS,KAAA,EAAA,GAAa,MAAA,IAAU,KAAA,EAAA,GAAa,IAAA,IAAQ,KAAA,CAAA,EAAW,OAAO,IAAA;AAC3E,EAAA,OAAO,IAAI,IAAA,CAAK,MAAA,CAAO,IAAI,CAAA,EAAG,MAAA,CAAO,KAAK,EAAA,EAAI,CAAA,EAAG,MAAA,CAAO,GAAG,CAAC,CAAA,CAAE,OAAA,CAAQ,CAAA;AACvE;AAEA,SAAS,QAAA,CAAS,KAAA,EAAgB,QAAA,EAA0B;AAC3D,EAAA,OAAO,OAAO,MAAA,IAAU,SAAA,GAAY,MAAA,CAAO,QAAA,CAAS,KAAK,EAAA,EAAI,MAAA,EAAQ,QAAA;AACtE;ADtCA;AACE;AACA;AACF,2EAAC","file":"/home/runner/work/fatigue-kit/fatigue-kit/dist/legacy.cjs","sourcesContent":[null,"import { emptyRecord } from './record'\nimport type { FatigueRecord } from './types'\n\n/** Shape of the records written by the legacy internal FatigueManager. */\nexport interface LegacyFatigueRecord {\n\ttimes?: number\n\t/** `'YYYY/MM/DD'` (`'YYYY-MM-DD'` is tolerated too). */\n\tdate?: string\n\t/** Cooldown length in days. */\n\tduration?: number\n\ttodayCount?: number\n}\n\nexport const DAY_MS = 86_400_000\n\nexport interface MigratedLegacyRecord {\n\trecord: FatigueRecord\n\t/**\n\t * Cooldown in ms implied by the legacy `duration` field, or `null`.\n\t * Pass it as `{ cooldown: cooldownMs }` to keep the old behaviour.\n\t */\n\tcooldownMs: number | null\n}\n\n/**\n * Convert a legacy record (raw string or parsed object) into the fatigue-kit\n * format. Returns `null` when no usable `date` field exists.\n *\n * Approximation: legacy records only remember the last activity *day* plus an\n * aggregate `todayCount`, so the migrated record holds a single hit placed at\n * local midnight of that day; `times` maps to `lifetime`.\n *\n * ```ts\n * const migrated = migrateLegacyRecord(await storage.getItem(legacyKey))\n * if (migrated) {\n * fatigue.importRecord(key, migrated.record)\n * const rules = [{ max: 3, window: '7d' }]\n * if (migrated.cooldownMs !== null) rules.push({ cooldown: migrated.cooldownMs })\n * }\n * ```\n */\nexport function migrateLegacyRecord(\n\tinput: string | LegacyFatigueRecord | null | undefined,\n): MigratedLegacyRecord | null {\n\tif (input === null || input === undefined) return null\n\tlet raw: unknown = input\n\tif (typeof input === 'string') {\n\t\ttry {\n\t\t\traw = JSON.parse(input)\n\t\t} catch {\n\t\t\treturn null\n\t\t}\n\t}\n\tif (raw === null || typeof raw !== 'object') return null\n\tconst candidate = raw as LegacyFatigueRecord\n\tconst lastHit = parseLegacyDate(candidate.date)\n\tif (lastHit === null) return null\n\tconst record = emptyRecord()\n\trecord.hits = [lastHit]\n\trecord.lifetime = Math.max(0, numberOr(candidate.times, 0))\n\tconst durationDays = numberOr(candidate.duration, 0)\n\treturn { record, cooldownMs: durationDays > 0 ? durationDays * DAY_MS : null }\n}\n\nfunction parseLegacyDate(value: unknown): number | null {\n\tif (typeof value !== 'string') return null\n\tconst match = /^(\\d{4})[/-](\\d{1,2})[/-](\\d{1,2})/.exec(value.trim())\n\tif (match === null) {\n\t\tconst parsed = Date.parse(value)\n\t\treturn Number.isNaN(parsed) ? null : parsed\n\t}\n\tconst [, year, month, day] = match\n\tif (year === undefined || month === undefined || day === undefined) return null\n\treturn new Date(Number(year), Number(month) - 1, Number(day)).getTime()\n}\n\nfunction numberOr(value: unknown, fallback: number): number {\n\treturn typeof value === 'number' && Number.isFinite(value) ? value : fallback\n}\n"]}
@@ -0,0 +1,40 @@
1
+ import { e as FatigueRecord } from './types-hDxN5bc3.cjs';
2
+
3
+ /** Shape of the records written by the legacy internal FatigueManager. */
4
+ interface LegacyFatigueRecord {
5
+ times?: number;
6
+ /** `'YYYY/MM/DD'` (`'YYYY-MM-DD'` is tolerated too). */
7
+ date?: string;
8
+ /** Cooldown length in days. */
9
+ duration?: number;
10
+ todayCount?: number;
11
+ }
12
+ declare const DAY_MS = 86400000;
13
+ interface MigratedLegacyRecord {
14
+ record: FatigueRecord;
15
+ /**
16
+ * Cooldown in ms implied by the legacy `duration` field, or `null`.
17
+ * Pass it as `{ cooldown: cooldownMs }` to keep the old behaviour.
18
+ */
19
+ cooldownMs: number | null;
20
+ }
21
+ /**
22
+ * Convert a legacy record (raw string or parsed object) into the fatigue-kit
23
+ * format. Returns `null` when no usable `date` field exists.
24
+ *
25
+ * Approximation: legacy records only remember the last activity *day* plus an
26
+ * aggregate `todayCount`, so the migrated record holds a single hit placed at
27
+ * local midnight of that day; `times` maps to `lifetime`.
28
+ *
29
+ * ```ts
30
+ * const migrated = migrateLegacyRecord(await storage.getItem(legacyKey))
31
+ * if (migrated) {
32
+ * fatigue.importRecord(key, migrated.record)
33
+ * const rules = [{ max: 3, window: '7d' }]
34
+ * if (migrated.cooldownMs !== null) rules.push({ cooldown: migrated.cooldownMs })
35
+ * }
36
+ * ```
37
+ */
38
+ declare function migrateLegacyRecord(input: string | LegacyFatigueRecord | null | undefined): MigratedLegacyRecord | null;
39
+
40
+ export { DAY_MS, type LegacyFatigueRecord, type MigratedLegacyRecord, migrateLegacyRecord };
@@ -0,0 +1,40 @@
1
+ import { e as FatigueRecord } from './types-hDxN5bc3.js';
2
+
3
+ /** Shape of the records written by the legacy internal FatigueManager. */
4
+ interface LegacyFatigueRecord {
5
+ times?: number;
6
+ /** `'YYYY/MM/DD'` (`'YYYY-MM-DD'` is tolerated too). */
7
+ date?: string;
8
+ /** Cooldown length in days. */
9
+ duration?: number;
10
+ todayCount?: number;
11
+ }
12
+ declare const DAY_MS = 86400000;
13
+ interface MigratedLegacyRecord {
14
+ record: FatigueRecord;
15
+ /**
16
+ * Cooldown in ms implied by the legacy `duration` field, or `null`.
17
+ * Pass it as `{ cooldown: cooldownMs }` to keep the old behaviour.
18
+ */
19
+ cooldownMs: number | null;
20
+ }
21
+ /**
22
+ * Convert a legacy record (raw string or parsed object) into the fatigue-kit
23
+ * format. Returns `null` when no usable `date` field exists.
24
+ *
25
+ * Approximation: legacy records only remember the last activity *day* plus an
26
+ * aggregate `todayCount`, so the migrated record holds a single hit placed at
27
+ * local midnight of that day; `times` maps to `lifetime`.
28
+ *
29
+ * ```ts
30
+ * const migrated = migrateLegacyRecord(await storage.getItem(legacyKey))
31
+ * if (migrated) {
32
+ * fatigue.importRecord(key, migrated.record)
33
+ * const rules = [{ max: 3, window: '7d' }]
34
+ * if (migrated.cooldownMs !== null) rules.push({ cooldown: migrated.cooldownMs })
35
+ * }
36
+ * ```
37
+ */
38
+ declare function migrateLegacyRecord(input: string | LegacyFatigueRecord | null | undefined): MigratedLegacyRecord | null;
39
+
40
+ export { DAY_MS, type LegacyFatigueRecord, type MigratedLegacyRecord, migrateLegacyRecord };
package/dist/legacy.js ADDED
@@ -0,0 +1,45 @@
1
+ import {
2
+ emptyRecord
3
+ } from "./chunk-ZNIZLIUO.js";
4
+
5
+ // src/legacy.ts
6
+ var DAY_MS = 864e5;
7
+ function migrateLegacyRecord(input) {
8
+ if (input === null || input === void 0) return null;
9
+ let raw = input;
10
+ if (typeof input === "string") {
11
+ try {
12
+ raw = JSON.parse(input);
13
+ } catch (e) {
14
+ return null;
15
+ }
16
+ }
17
+ if (raw === null || typeof raw !== "object") return null;
18
+ const candidate = raw;
19
+ const lastHit = parseLegacyDate(candidate.date);
20
+ if (lastHit === null) return null;
21
+ const record = emptyRecord();
22
+ record.hits = [lastHit];
23
+ record.lifetime = Math.max(0, numberOr(candidate.times, 0));
24
+ const durationDays = numberOr(candidate.duration, 0);
25
+ return { record, cooldownMs: durationDays > 0 ? durationDays * DAY_MS : null };
26
+ }
27
+ function parseLegacyDate(value) {
28
+ if (typeof value !== "string") return null;
29
+ const match = /^(\d{4})[/-](\d{1,2})[/-](\d{1,2})/.exec(value.trim());
30
+ if (match === null) {
31
+ const parsed = Date.parse(value);
32
+ return Number.isNaN(parsed) ? null : parsed;
33
+ }
34
+ const [, year, month, day] = match;
35
+ if (year === void 0 || month === void 0 || day === void 0) return null;
36
+ return new Date(Number(year), Number(month) - 1, Number(day)).getTime();
37
+ }
38
+ function numberOr(value, fallback) {
39
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
40
+ }
41
+ export {
42
+ DAY_MS,
43
+ migrateLegacyRecord
44
+ };
45
+ //# sourceMappingURL=legacy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/legacy.ts"],"sourcesContent":["import { emptyRecord } from './record'\nimport type { FatigueRecord } from './types'\n\n/** Shape of the records written by the legacy internal FatigueManager. */\nexport interface LegacyFatigueRecord {\n\ttimes?: number\n\t/** `'YYYY/MM/DD'` (`'YYYY-MM-DD'` is tolerated too). */\n\tdate?: string\n\t/** Cooldown length in days. */\n\tduration?: number\n\ttodayCount?: number\n}\n\nexport const DAY_MS = 86_400_000\n\nexport interface MigratedLegacyRecord {\n\trecord: FatigueRecord\n\t/**\n\t * Cooldown in ms implied by the legacy `duration` field, or `null`.\n\t * Pass it as `{ cooldown: cooldownMs }` to keep the old behaviour.\n\t */\n\tcooldownMs: number | null\n}\n\n/**\n * Convert a legacy record (raw string or parsed object) into the fatigue-kit\n * format. Returns `null` when no usable `date` field exists.\n *\n * Approximation: legacy records only remember the last activity *day* plus an\n * aggregate `todayCount`, so the migrated record holds a single hit placed at\n * local midnight of that day; `times` maps to `lifetime`.\n *\n * ```ts\n * const migrated = migrateLegacyRecord(await storage.getItem(legacyKey))\n * if (migrated) {\n * fatigue.importRecord(key, migrated.record)\n * const rules = [{ max: 3, window: '7d' }]\n * if (migrated.cooldownMs !== null) rules.push({ cooldown: migrated.cooldownMs })\n * }\n * ```\n */\nexport function migrateLegacyRecord(\n\tinput: string | LegacyFatigueRecord | null | undefined,\n): MigratedLegacyRecord | null {\n\tif (input === null || input === undefined) return null\n\tlet raw: unknown = input\n\tif (typeof input === 'string') {\n\t\ttry {\n\t\t\traw = JSON.parse(input)\n\t\t} catch {\n\t\t\treturn null\n\t\t}\n\t}\n\tif (raw === null || typeof raw !== 'object') return null\n\tconst candidate = raw as LegacyFatigueRecord\n\tconst lastHit = parseLegacyDate(candidate.date)\n\tif (lastHit === null) return null\n\tconst record = emptyRecord()\n\trecord.hits = [lastHit]\n\trecord.lifetime = Math.max(0, numberOr(candidate.times, 0))\n\tconst durationDays = numberOr(candidate.duration, 0)\n\treturn { record, cooldownMs: durationDays > 0 ? durationDays * DAY_MS : null }\n}\n\nfunction parseLegacyDate(value: unknown): number | null {\n\tif (typeof value !== 'string') return null\n\tconst match = /^(\\d{4})[/-](\\d{1,2})[/-](\\d{1,2})/.exec(value.trim())\n\tif (match === null) {\n\t\tconst parsed = Date.parse(value)\n\t\treturn Number.isNaN(parsed) ? null : parsed\n\t}\n\tconst [, year, month, day] = match\n\tif (year === undefined || month === undefined || day === undefined) return null\n\treturn new Date(Number(year), Number(month) - 1, Number(day)).getTime()\n}\n\nfunction numberOr(value: unknown, fallback: number): number {\n\treturn typeof value === 'number' && Number.isFinite(value) ? value : fallback\n}\n"],"mappings":";;;;;AAaO,IAAM,SAAS;AA4Bf,SAAS,oBACf,OAC8B;AAC9B,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,MAAI,MAAe;AACnB,MAAI,OAAO,UAAU,UAAU;AAC9B,QAAI;AACH,YAAM,KAAK,MAAM,KAAK;AAAA,IACvB,SAAQ;AACP,aAAO;AAAA,IACR;AAAA,EACD;AACA,MAAI,QAAQ,QAAQ,OAAO,QAAQ,SAAU,QAAO;AACpD,QAAM,YAAY;AAClB,QAAM,UAAU,gBAAgB,UAAU,IAAI;AAC9C,MAAI,YAAY,KAAM,QAAO;AAC7B,QAAM,SAAS,YAAY;AAC3B,SAAO,OAAO,CAAC,OAAO;AACtB,SAAO,WAAW,KAAK,IAAI,GAAG,SAAS,UAAU,OAAO,CAAC,CAAC;AAC1D,QAAM,eAAe,SAAS,UAAU,UAAU,CAAC;AACnD,SAAO,EAAE,QAAQ,YAAY,eAAe,IAAI,eAAe,SAAS,KAAK;AAC9E;AAEA,SAAS,gBAAgB,OAA+B;AACvD,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,QAAQ,qCAAqC,KAAK,MAAM,KAAK,CAAC;AACpE,MAAI,UAAU,MAAM;AACnB,UAAM,SAAS,KAAK,MAAM,KAAK;AAC/B,WAAO,OAAO,MAAM,MAAM,IAAI,OAAO;AAAA,EACtC;AACA,QAAM,CAAC,EAAE,MAAM,OAAO,GAAG,IAAI;AAC7B,MAAI,SAAS,UAAa,UAAU,UAAa,QAAQ,OAAW,QAAO;AAC3E,SAAO,IAAI,KAAK,OAAO,IAAI,GAAG,OAAO,KAAK,IAAI,GAAG,OAAO,GAAG,CAAC,EAAE,QAAQ;AACvE;AAEA,SAAS,SAAS,OAAgB,UAA0B;AAC3D,SAAO,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,IAAI,QAAQ;AACtE;","names":[]}
@@ -0,0 +1,89 @@
1
+ /** Injectable clock so tests can freeze or accelerate time. */
2
+ interface Clock {
3
+ now(): number;
4
+ }
5
+ /**
6
+ * Duration: a millisecond number, or a string like `'45s'`, `'30m'`, `'12h'`, `'7d'`.
7
+ */
8
+ type Duration = number | string;
9
+ /** At most `max` occurrences within a sliding window. */
10
+ interface SlidingWindowRule {
11
+ max: number;
12
+ window: Duration;
13
+ }
14
+ /** At most `max` occurrences per calendar period, evaluated in device local time. */
15
+ interface CalendarRule {
16
+ max: number;
17
+ every: 'day' | 'week' | 'month' | 'year';
18
+ /** Period boundary offset in local time, `'HH:mm'`. Default `'00:00'`. */
19
+ cutoff?: string;
20
+ /** First weekday: `0` = Sunday … `6` = Saturday. Default `1` (Monday). */
21
+ weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
22
+ }
23
+ /** Two occurrences must be at least `cooldown` apart. */
24
+ interface CooldownRule {
25
+ cooldown: Duration;
26
+ }
27
+ /** At most `max` occurrences, ever. */
28
+ interface LifetimeRule {
29
+ max: number;
30
+ }
31
+ type FatigueRule = SlidingWindowRule | CalendarRule | CooldownRule | LifetimeRule;
32
+ interface ConsumeResult {
33
+ allowed: boolean;
34
+ /**
35
+ * How many more times the key may be consumed under the strictest count-based
36
+ * rule. `Infinity` when no count-based rule applies (e.g. a cooldown-only key).
37
+ */
38
+ remaining: number;
39
+ /** Earliest timestamp (ms) at which the key may be consumed again. Absent while allowed. */
40
+ nextAvailableAt?: number;
41
+ /** The first rule that caused the denial. */
42
+ deniedBy?: FatigueRule;
43
+ }
44
+ /**
45
+ * A synchronous string key-value store. Every built-in adapter implements this,
46
+ * and so can yours — this is the only contract `createFatigue` relies on.
47
+ */
48
+ interface SyncStorage {
49
+ get(key: string): string | null | undefined;
50
+ set(key: string, value: string): void;
51
+ remove(key: string): void;
52
+ /**
53
+ * Optional: list all keys known to this storage.
54
+ * Enables `fatigue.reset()` without a key.
55
+ */
56
+ keys?(): string[];
57
+ }
58
+ /** Serialized fatigue record. `hits` are ascending timestamps in ms. */
59
+ interface FatigueRecord {
60
+ /** Record format version. */
61
+ v: number;
62
+ hits: number[];
63
+ /** Total consumption count, ever. Survives pruning. */
64
+ lifetime: number;
65
+ }
66
+ interface FatigueOptions {
67
+ /** Backing storage. Defaults to an in-memory store. */
68
+ storage?: SyncStorage;
69
+ /** Clock. Defaults to the real system time. */
70
+ clock?: Clock;
71
+ /** Prefix for storage keys. Default `'fatigue-kit'`. */
72
+ namespace?: string;
73
+ /** Maximum hits kept per record; older ones are pruned on write. Default `100`. */
74
+ maxHits?: number;
75
+ }
76
+ interface Fatigue {
77
+ /** Atomically check the rules and, when allowed, consume one occurrence. */
78
+ tryConsume(key: string, rules: FatigueRule | FatigueRule[]): ConsumeResult;
79
+ /** Check the rules without consuming. */
80
+ peek(key: string, rules: FatigueRule | FatigueRule[]): ConsumeResult;
81
+ /** Drop one key, or every key when omitted (requires `storage.keys`). */
82
+ reset(key?: string): void;
83
+ /** Read the raw record of a key. `null` when absent. */
84
+ exportRecord(key: string): FatigueRecord | null;
85
+ /** Overwrite the record of a key (used for migrations and debugging). */
86
+ importRecord(key: string, record: Partial<FatigueRecord>): void;
87
+ }
88
+
89
+ export type { CalendarRule as C, Duration as D, FatigueOptions as F, LifetimeRule as L, SyncStorage as S, Fatigue as a, Clock as b, ConsumeResult as c, CooldownRule as d, FatigueRecord as e, FatigueRule as f, SlidingWindowRule as g };