fatigue-kit 0.0.0-stage → 0.2.1
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 +28 -0
- package/dist/adapters/alipay.d.ts +28 -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 +49 -0
- package/dist/adapters/async-storage.d.ts +49 -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 +15 -0
- package/dist/adapters/memory.d.ts +15 -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 +16 -0
- package/dist/adapters/node-file.d.ts +16 -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 +24 -0
- package/dist/adapters/weapp.d.ts +24 -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 +41 -0
- package/dist/legacy.d.ts +41 -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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026-present fatigue-kit contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,160 @@
|
|
|
1
|
-
#
|
|
1
|
+
# fatigue-kit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
English | [简体中文](./README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
> Zero-dependency frequency capping for Web, mini-programs, React Native and Node.js.
|
|
6
|
+
|
|
7
|
+
`fatigue-kit` answers one question: **should this popup / toast / guide / coupon be shown now?**
|
|
8
|
+
Compose simple rules, back them by any storage, and get an atomic, testable answer — synchronously — in every JS runtime.
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- **Composable rules** — calendar day / week / month (custom cutoff), sliding window, cooldown, lifetime cap. Stack any number of rules; the strictest wins.
|
|
13
|
+
- **Atomic `tryConsume`** — check and count in one synchronous step, so double triggers can never double-fire.
|
|
14
|
+
- **Every environment, on demand** — zero-dependency core with storage adapters as subpath exports: `localStorage`, `sessionStorage`, React Native `AsyncStorage`, WeChat / Alipay mini-programs, Node.js file. Bundlers ship only what you import.
|
|
15
|
+
- **Testable** — inject the clock, inject the storage, inspect and seed records.
|
|
16
|
+
- **Small & safe** — ~3 kB gzip core, versioned self-pruning record format, corrupt storage degrades instead of throwing.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npm install fatigue-kit
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick start (Web)
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { createFatigue } from 'fatigue-kit'
|
|
28
|
+
import { localStorageAdapter } from 'fatigue-kit/adapters/local-storage'
|
|
29
|
+
|
|
30
|
+
const fatigue = createFatigue({ storage: localStorageAdapter() })
|
|
31
|
+
|
|
32
|
+
const result = fatigue.tryConsume('coupon-popup', [
|
|
33
|
+
{ max: 1, every: 'day' }, // once per calendar day
|
|
34
|
+
{ max: 3, window: '7d' }, // at most 3 times in any rolling 7 days
|
|
35
|
+
{ cooldown: '4h' }, // at least 4 hours between two showings
|
|
36
|
+
])
|
|
37
|
+
|
|
38
|
+
if (result.allowed) {
|
|
39
|
+
showCouponPopup()
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Environments
|
|
44
|
+
|
|
45
|
+
| Environment | Adapter | Import |
|
|
46
|
+
| --- | --- | --- |
|
|
47
|
+
| Web / H5 (persistent) | `localStorageAdapter()` | `fatigue-kit/adapters/local-storage` |
|
|
48
|
+
| Web / H5 (per tab visit) | `sessionStorageAdapter()` | `fatigue-kit/adapters/session-storage` |
|
|
49
|
+
| React Native | `asyncStorageAdapter(AsyncStorage)` | `fatigue-kit/adapters/async-storage` |
|
|
50
|
+
| WeChat mini-program | `weappAdapter()` | `fatigue-kit/adapters/weapp` |
|
|
51
|
+
| Alipay mini-program | `alipayAdapter()` | `fatigue-kit/adapters/alipay` |
|
|
52
|
+
| Node.js (persistent) | `fileAdapter('./state.json')` | `fatigue-kit/adapters/node-file` |
|
|
53
|
+
| Tests / SSR / default | `memoryAdapter()` (built in) | — |
|
|
54
|
+
|
|
55
|
+
Anything with three synchronous functions is a valid adapter:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import type { SyncStorage } from 'fatigue-kit'
|
|
59
|
+
const custom: SyncStorage = { get(key) { … }, set(key, value) { … }, remove(key) { … } }
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### React Native
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import AsyncStorage from '@react-native-async-storage/async-storage'
|
|
66
|
+
import { AppState } from 'react-native'
|
|
67
|
+
import { createFatigue } from 'fatigue-kit'
|
|
68
|
+
import { asyncStorageAdapter } from 'fatigue-kit/adapters/async-storage'
|
|
69
|
+
|
|
70
|
+
const handle = asyncStorageAdapter(AsyncStorage)
|
|
71
|
+
await handle.ready // hydrate the cache once
|
|
72
|
+
|
|
73
|
+
const fatigue = createFatigue({ storage: handle.storage })
|
|
74
|
+
|
|
75
|
+
AppState.addEventListener('change', (state) => {
|
|
76
|
+
if (state === 'background') handle.flush() // don't lose the last writes
|
|
77
|
+
})
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Mini-programs
|
|
81
|
+
|
|
82
|
+
Enable **构建 npm** in the devtools, then `require('fatigue-kit/adapters/weapp')` — CommonJS subpaths are shipped for exactly this. No `DOM` assumptions anywhere in the core.
|
|
83
|
+
|
|
84
|
+
## API
|
|
85
|
+
|
|
86
|
+
### `createFatigue(options?)`
|
|
87
|
+
|
|
88
|
+
| Option | Default | Description |
|
|
89
|
+
| --- | --- | --- |
|
|
90
|
+
| `storage` | `memoryAdapter()` | Any `SyncStorage` |
|
|
91
|
+
| `clock` | system time | `{ now(): number }`, inject in tests |
|
|
92
|
+
| `namespace` | `'fatigue-kit'` | Prefix for storage keys |
|
|
93
|
+
| `maxHits` | `100` | Hits kept per record; older ones are pruned |
|
|
94
|
+
|
|
95
|
+
### Methods
|
|
96
|
+
|
|
97
|
+
- `tryConsume(key, rules)` — atomically check and consume. Returns `{ allowed, remaining, nextAvailableAt?, deniedBy? }`. `remaining` describes the state **after** the call: `remaining === 0` means "that was the last allowed consumption". `peek` reports the current state.
|
|
98
|
+
- `peek(key, rules)` — same result, without consuming.
|
|
99
|
+
- `reset(key?)` — forget one key, or all keys (needs `storage.keys()`).
|
|
100
|
+
- `exportRecord(key)` / `importRecord(key, record)` — inspect and seed records (migrations, debugging).
|
|
101
|
+
|
|
102
|
+
### Rules
|
|
103
|
+
|
|
104
|
+
| Rule | Meaning |
|
|
105
|
+
| --- | --- |
|
|
106
|
+
| `{ max: 1, every: 'day' }` | Once per calendar day (local time) |
|
|
107
|
+
| `{ max: 1, every: 'day', cutoff: '04:00' }` | Calendar day that starts at 04:00 |
|
|
108
|
+
| `{ max: 3, window: '7d' }` | At most 3 in any rolling 7 days |
|
|
109
|
+
| `{ cooldown: '4h' }` | At least 4 hours between two occurrences |
|
|
110
|
+
| `{ max: 5 }` | At most 5 times, ever |
|
|
111
|
+
| `every: 'week'` / `'month'` / `'year'`, `weekStartsOn` | Calendar periods |
|
|
112
|
+
|
|
113
|
+
Durations accept `'45s' / '30m' / '12h' / '7d'` or milliseconds. Calendar math uses local date components, so DST transitions never shift a boundary.
|
|
114
|
+
|
|
115
|
+
## Migrating legacy records
|
|
116
|
+
|
|
117
|
+
Coming from a legacy `{ times, date, duration, todayCount }` store?
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import { migrateLegacyRecord } from 'fatigue-kit/legacy'
|
|
121
|
+
|
|
122
|
+
const migrated = migrateLegacyRecord(rawLegacyValue)
|
|
123
|
+
if (migrated) {
|
|
124
|
+
fatigue.importRecord(key, migrated.record)
|
|
125
|
+
// Keep the old "cooldown after last show" behaviour:
|
|
126
|
+
const rules = [{ max: 1, window: '7d' }]
|
|
127
|
+
if (migrated.cooldownMs !== null) rules.push({ cooldown: migrated.cooldownMs })
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Testing your rules
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { createFatigue } from 'fatigue-kit'
|
|
135
|
+
const clock = { current: 0, now: () => clock.current }
|
|
136
|
+
const fatigue = createFatigue({ storage: memoryAdapter(), clock })
|
|
137
|
+
fatigue.tryConsume('popup', [{ max: 1, every: 'day' }])
|
|
138
|
+
clock.current = Number.MAX_SAFE_INTEGER // travel to the end of time
|
|
139
|
+
expect(fatigue.peek('popup', [{ max: 1, every: 'day' }]).allowed).toBe(true)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Known limitations (v1)
|
|
143
|
+
|
|
144
|
+
- Reads are atomic within one JS context; **cross-tab / cross-process** races are last-write-wins (a `BroadcastChannel`-based mode is on the roadmap).
|
|
145
|
+
- Calendar rules use device local time; IANA timezone support is planned.
|
|
146
|
+
- The RN adapter needs a `flush()` call on app-background to guarantee durability.
|
|
147
|
+
|
|
148
|
+
## Roadmap
|
|
149
|
+
|
|
150
|
+
- `@fatigue-kit/react` — `useFatigue()` hook bindings
|
|
151
|
+
- Group budgets — "all marketing popups together, max 3 per day", with priorities
|
|
152
|
+
- Cross-tab consistency, IANA timezone support, debug inspector
|
|
153
|
+
|
|
154
|
+
## Contributing
|
|
155
|
+
|
|
156
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md). Design decisions and the full migration plan (中文): [docs/design.zh-CN.md](./docs/design.zh-CN.md).
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
[MIT](./LICENSE)
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# fatigue-kit
|
|
2
|
+
|
|
3
|
+
[English](./README.md) | 简体中文
|
|
4
|
+
|
|
5
|
+
> 零依赖的前端频控(疲劳度控制)库,覆盖 Web、小程序、React Native 和 Node.js。
|
|
6
|
+
|
|
7
|
+
`fatigue-kit` 只回答一个问题:**这个弹窗 / toast / 引导 / 优惠,现在能不能出?**
|
|
8
|
+
用可组合的规则 + 可替换的存储,在所有 JS 运行时里给出同步、原子的答案。
|
|
9
|
+
|
|
10
|
+
## 特性
|
|
11
|
+
|
|
12
|
+
- **规则可组合** —— 自然日 / 周 / 月(可设日界线)、滑动窗口、冷却、终身上限。任意叠加,以最严为准。
|
|
13
|
+
- **原子 `tryConsume`** —— 检查与计数在同一步同步完成,连续触发不会双弹。
|
|
14
|
+
- **全环境、按需加载** —— 零依赖核心,存储适配器按子路径引入:`localStorage`、`sessionStorage`、React Native `AsyncStorage`、微信 / 支付宝小程序、Node.js 文件。bundler 只打包你 import 的部分。
|
|
15
|
+
- **可测试** —— 时钟可注入、存储可注入,记录可检查、可灌入。
|
|
16
|
+
- **小而稳** —— 核心 gzip 约 3 kB;记录格式带版本号并自动修剪;存储损坏时优雅降级而不是抛错。
|
|
17
|
+
|
|
18
|
+
## 安装
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npm install fatigue-kit
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 快速开始(Web)
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { createFatigue } from 'fatigue-kit'
|
|
28
|
+
import { localStorageAdapter } from 'fatigue-kit/adapters/local-storage'
|
|
29
|
+
|
|
30
|
+
const fatigue = createFatigue({ storage: localStorageAdapter() })
|
|
31
|
+
|
|
32
|
+
const result = fatigue.tryConsume('coupon-popup', [
|
|
33
|
+
{ max: 1, every: 'day' }, // 每个自然日最多 1 次
|
|
34
|
+
{ max: 3, window: '7d' }, // 任意滚动 7 天内最多 3 次
|
|
35
|
+
{ cooldown: '4h' }, // 两次之间至少间隔 4 小时
|
|
36
|
+
])
|
|
37
|
+
|
|
38
|
+
if (result.allowed) {
|
|
39
|
+
showCouponPopup()
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 环境支持
|
|
44
|
+
|
|
45
|
+
| 环境 | 适配器 | 引入路径 |
|
|
46
|
+
| --- | --- | --- |
|
|
47
|
+
| Web / H5(持久) | `localStorageAdapter()` | `fatigue-kit/adapters/local-storage` |
|
|
48
|
+
| Web / H5(每次访问) | `sessionStorageAdapter()` | `fatigue-kit/adapters/session-storage` |
|
|
49
|
+
| React Native | `asyncStorageAdapter(AsyncStorage)` | `fatigue-kit/adapters/async-storage` |
|
|
50
|
+
| 微信小程序 | `weappAdapter()` | `fatigue-kit/adapters/weapp` |
|
|
51
|
+
| 支付宝小程序 | `alipayAdapter()` | `fatigue-kit/adapters/alipay` |
|
|
52
|
+
| Node.js(持久) | `fileAdapter('./state.json')` | `fatigue-kit/adapters/node-file` |
|
|
53
|
+
| 测试 / SSR / 默认 | `memoryAdapter()`(内置) | — |
|
|
54
|
+
|
|
55
|
+
只要实现三个同步函数,就是合法的适配器:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import type { SyncStorage } from 'fatigue-kit'
|
|
59
|
+
const custom: SyncStorage = { get(key) { … }, set(key, value) { … }, remove(key) { … } }
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### React Native
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import AsyncStorage from '@react-native-async-storage/async-storage'
|
|
66
|
+
import { AppState } from 'react-native'
|
|
67
|
+
import { createFatigue } from 'fatigue-kit'
|
|
68
|
+
import { asyncStorageAdapter } from 'fatigue-kit/adapters/async-storage'
|
|
69
|
+
|
|
70
|
+
const handle = asyncStorageAdapter(AsyncStorage)
|
|
71
|
+
await handle.ready // 一次性预载缓存
|
|
72
|
+
|
|
73
|
+
const fatigue = createFatigue({ storage: handle.storage })
|
|
74
|
+
|
|
75
|
+
AppState.addEventListener('change', (state) => {
|
|
76
|
+
if (state === 'background') handle.flush() // 别丢掉最后的写入
|
|
77
|
+
})
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 小程序
|
|
81
|
+
|
|
82
|
+
在开发者工具里开启 **构建 npm**,然后 `require('fatigue-kit/adapters/weapp')` —— CommonJS 子路径就是为这个场景发的。核心代码不做任何 `DOM` 假设。
|
|
83
|
+
|
|
84
|
+
## API
|
|
85
|
+
|
|
86
|
+
### `createFatigue(options?)`
|
|
87
|
+
|
|
88
|
+
| 选项 | 默认值 | 说明 |
|
|
89
|
+
| --- | --- | --- |
|
|
90
|
+
| `storage` | `memoryAdapter()` | 任意 `SyncStorage` 实现 |
|
|
91
|
+
| `clock` | 系统时间 | `{ now(): number }`,测试时注入 |
|
|
92
|
+
| `namespace` | `'fatigue-kit'` | 存储 key 前缀 |
|
|
93
|
+
| `maxHits` | `100` | 每条记录保留的最大命中数,更旧的会被修剪 |
|
|
94
|
+
|
|
95
|
+
### 方法
|
|
96
|
+
|
|
97
|
+
- `tryConsume(key, rules)` —— 原子地检查并消费。返回 `{ allowed, remaining, nextAvailableAt?, deniedBy? }`。`remaining` 描述**本次调用之后**的状态:`remaining === 0` 表示"刚才是最后一次";`peek` 返回当前状态。
|
|
98
|
+
- `peek(key, rules)` —— 结果相同,但不消费。
|
|
99
|
+
- `reset(key?)` —— 清除一个 key,或不传参数清除全部(需要 `storage.keys()`)。
|
|
100
|
+
- `exportRecord(key)` / `importRecord(key, record)` —— 读取与灌入记录(迁移、调试用)。
|
|
101
|
+
|
|
102
|
+
### 规则
|
|
103
|
+
|
|
104
|
+
| 规则 | 含义 |
|
|
105
|
+
| --- | --- |
|
|
106
|
+
| `{ max: 1, every: 'day' }` | 每个自然日(本地时间)最多 1 次 |
|
|
107
|
+
| `{ max: 1, every: 'day', cutoff: '04:00' }` | 从凌晨 4 点起算的"业务日" |
|
|
108
|
+
| `{ max: 3, window: '7d' }` | 任意滚动 7 天内最多 3 次 |
|
|
109
|
+
| `{ cooldown: '4h' }` | 两次之间至少间隔 4 小时 |
|
|
110
|
+
| `{ max: 5 }` | 终身最多 5 次 |
|
|
111
|
+
| `every: 'week'` / `'month'` / `'year'`、`weekStartsOn` | 日历周期 |
|
|
112
|
+
|
|
113
|
+
时长支持 `'45s' / '30m' / '12h' / '7d'` 或毫秒数。日历运算全部基于本地日期分量,夏令时切换不会移动边界。
|
|
114
|
+
|
|
115
|
+
## 迁移旧数据
|
|
116
|
+
|
|
117
|
+
从旧版 `{ times, date, duration, todayCount }` 存储迁移?
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import { migrateLegacyRecord } from 'fatigue-kit/legacy'
|
|
121
|
+
|
|
122
|
+
const migrated = migrateLegacyRecord(rawLegacyValue)
|
|
123
|
+
if (migrated) {
|
|
124
|
+
fatigue.importRecord(key, migrated.record)
|
|
125
|
+
// 保持旧的"最后一次展示后冷却 N 天"行为:
|
|
126
|
+
const rules = [{ max: 1, window: '7d' }]
|
|
127
|
+
if (migrated.cooldownMs !== null) rules.push({ cooldown: migrated.cooldownMs })
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## 测试你的规则
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { createFatigue } from 'fatigue-kit'
|
|
135
|
+
const clock = { current: 0, now: () => clock.current }
|
|
136
|
+
const fatigue = createFatigue({ storage: memoryAdapter(), clock })
|
|
137
|
+
fatigue.tryConsume('popup', [{ max: 1, every: 'day' }])
|
|
138
|
+
clock.current = Number.MAX_SAFE_INTEGER // 时间旅行到世界末日
|
|
139
|
+
expect(fatigue.peek('popup', [{ max: 1, every: 'day' }]).allowed).toBe(true)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## 已知限制(v1)
|
|
143
|
+
|
|
144
|
+
- 读取在单个 JS 上下文内原子;**跨 tab / 跨进程**是 last-write-wins(基于 `BroadcastChannel` 的方案在路线图上)。
|
|
145
|
+
- 日历规则跟随设备本地时间;IANA 时区支持在计划中。
|
|
146
|
+
- RN 适配器需要在退后台时调用一次 `flush()` 才能保证写入落盘。
|
|
147
|
+
|
|
148
|
+
## 路线图
|
|
149
|
+
|
|
150
|
+
- `@fatigue-kit/react` —— `useFatigue()` Hook 绑定
|
|
151
|
+
- 分组预算 —— "所有营销弹窗合计每天最多 3 个",带优先级仲裁
|
|
152
|
+
- 跨 tab 一致性、IANA 时区支持、调试面板
|
|
153
|
+
|
|
154
|
+
## 参与贡献
|
|
155
|
+
|
|
156
|
+
见 [CONTRIBUTING.md](./CONTRIBUTING.md)。完整设计决策与迁移计划见 [docs/design.zh-CN.md](./docs/design.zh-CN.md)。
|
|
157
|
+
|
|
158
|
+
## 许可证
|
|
159
|
+
|
|
160
|
+
[MIT](./LICENSE)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true});// src/adapters/alipay.ts
|
|
2
|
+
function alipayAdapter(options = {}) {
|
|
3
|
+
var _a, _b;
|
|
4
|
+
const client = (_a = options.client) != null ? _a : typeof my !== "undefined" && my !== null ? my : void 0;
|
|
5
|
+
if (client === void 0) {
|
|
6
|
+
throw new Error(
|
|
7
|
+
"[fatigue-kit] alipayAdapter: no `my` global found. Pass `{ client }` explicitly when not running inside Alipay."
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
const onError = (_b = options.onError) != null ? _b : ((error) => {
|
|
11
|
+
console.warn("[fatigue-kit] alipay storage operation failed:", error);
|
|
12
|
+
});
|
|
13
|
+
return {
|
|
14
|
+
get(key) {
|
|
15
|
+
try {
|
|
16
|
+
const response = client.getStorageSync({ key });
|
|
17
|
+
const data = response == null ? void 0 : response.data;
|
|
18
|
+
return typeof data === "string" && data.length > 0 ? data : null;
|
|
19
|
+
} catch (error) {
|
|
20
|
+
onError(error);
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
set(key, value) {
|
|
25
|
+
try {
|
|
26
|
+
client.setStorageSync({ key, data: value });
|
|
27
|
+
} catch (error) {
|
|
28
|
+
onError(error);
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
remove(key) {
|
|
32
|
+
try {
|
|
33
|
+
client.removeStorageSync({ key });
|
|
34
|
+
} catch (error) {
|
|
35
|
+
onError(error);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
keys() {
|
|
39
|
+
var _a2, _b2;
|
|
40
|
+
try {
|
|
41
|
+
return (_b2 = (_a2 = client.getStorageInfoSync) == null ? void 0 : _a2.call(client).keys) != null ? _b2 : [];
|
|
42
|
+
} catch (error) {
|
|
43
|
+
onError(error);
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
exports.alipayAdapter = alipayAdapter;
|
|
52
|
+
//# sourceMappingURL=alipay.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/fatigue-kit/fatigue-kit/dist/adapters/alipay.cjs","../../src/adapters/alipay.ts"],"names":["_a","_b"],"mappings":"AAAA;ACmBO,SAAS,aAAA,CAAc,QAAA,EAAgC,CAAC,CAAA,EAAgB;AAnB/E,EAAA,IAAA,EAAA,EAAA,EAAA;AAoBC,EAAA,MAAM,OAAA,EAAA,CAAS,GAAA,EAAA,OAAA,CAAQ,MAAA,EAAA,GAAR,KAAA,EAAA,GAAA,EAAmB,OAAO,GAAA,IAAO,YAAA,GAAe,GAAA,IAAO,KAAA,EAAO,GAAA,EAAK,KAAA,CAAA;AAClF,EAAA,GAAA,CAAI,OAAA,IAAW,KAAA,CAAA,EAAW;AACzB,IAAA,MAAM,IAAI,KAAA;AAAA,MACT;AAAA,IACD,CAAA;AAAA,EACD;AACA,EAAA,MAAM,QAAA,EAAA,CACL,GAAA,EAAA,OAAA,CAAQ,OAAA,EAAA,GAAR,KAAA,EAAA,GAAA,EAAA,CACC,CAAC,KAAA,EAAA,GAAmB;AACpB,IAAA,OAAA,CAAQ,IAAA,CAAK,gDAAA,EAAkD,KAAK,CAAA;AAAA,EACrE,CAAA,CAAA;AACD,EAAA,OAAO;AAAA,IACN,GAAA,CAAI,GAAA,EAAK;AACR,MAAA,IAAI;AACH,QAAA,MAAM,SAAA,EAAW,MAAA,CAAO,cAAA,CAAe,EAAE,IAAI,CAAC,CAAA;AAC9C,QAAA,MAAM,KAAA,EAAO,SAAA,GAAA,KAAA,EAAA,KAAA,EAAA,EAAA,QAAA,CAAU,IAAA;AACvB,QAAA,OAAO,OAAO,KAAA,IAAS,SAAA,GAAY,IAAA,CAAK,OAAA,EAAS,EAAA,EAAI,KAAA,EAAO,IAAA;AAAA,MAC7D,EAAA,MAAA,CAAS,KAAA,EAAO;AACf,QAAA,OAAA,CAAQ,KAAK,CAAA;AACb,QAAA,OAAO,IAAA;AAAA,MACR;AAAA,IACD,CAAA;AAAA,IACA,GAAA,CAAI,GAAA,EAAK,KAAA,EAAO;AACf,MAAA,IAAI;AACH,QAAA,MAAA,CAAO,cAAA,CAAe,EAAE,GAAA,EAAK,IAAA,EAAM,MAAM,CAAC,CAAA;AAAA,MAC3C,EAAA,MAAA,CAAS,KAAA,EAAO;AACf,QAAA,OAAA,CAAQ,KAAK,CAAA;AAAA,MACd;AAAA,IACD,CAAA;AAAA,IACA,MAAA,CAAO,GAAA,EAAK;AACX,MAAA,IAAI;AACH,QAAA,MAAA,CAAO,iBAAA,CAAkB,EAAE,IAAI,CAAC,CAAA;AAAA,MACjC,EAAA,MAAA,CAAS,KAAA,EAAO;AACf,QAAA,OAAA,CAAQ,KAAK,CAAA;AAAA,MACd;AAAA,IACD,CAAA;AAAA,IACA,IAAA,CAAA,EAAO;AAxDT,MAAA,IAAAA,GAAAA,EAAAC,GAAAA;AAyDG,MAAA,IAAI;AACH,QAAA,OAAA,CAAOA,IAAAA,EAAAA,CAAAD,IAAAA,EAAA,MAAA,CAAO,kBAAA,EAAA,GAAP,KAAA,EAAA,KAAA,EAAA,EAAAA,GAAAA,CAAA,IAAA,CAAA,MAAA,CAAA,CAA8B,IAAA,EAAA,GAA9B,KAAA,EAAAC,IAAAA,EAAsC,CAAC,CAAA;AAAA,MAC/C,EAAA,MAAA,CAAS,KAAA,EAAO;AACf,QAAA,OAAA,CAAQ,KAAK,CAAA;AACb,QAAA,OAAO,CAAC,CAAA;AAAA,MACT;AAAA,IACD;AAAA,EACD,CAAA;AACD;ADjBA;AACE;AACF,sCAAC","file":"/home/runner/work/fatigue-kit/fatigue-kit/dist/adapters/alipay.cjs","sourcesContent":[null,"import type { SyncStorage } from '../types'\n\n/** Alipay mini-program storage API (object arguments, values wrapped in `{ data }`). */\nexport interface AlipayStorageLike {\n\tgetStorageSync(options: { key: string }): unknown\n\tsetStorageSync(options: { key: string; data: string }): void\n\tremoveStorageSync(options: { key: string }): void\n\tgetStorageInfoSync?(): { keys?: string[] }\n}\n\nexport interface AlipayAdapterOptions {\n\t/** Defaults to the `my` global; inject explicitly in tests or abstractions. */\n\tclient?: AlipayStorageLike\n\tonError?: (error: unknown) => void\n}\n\ndeclare const my: AlipayStorageLike | undefined\n\n/** Alipay mini-program adapter backed by `my.getStorageSync` & friends. */\nexport function alipayAdapter(options: AlipayAdapterOptions = {}): SyncStorage {\n\tconst client = options.client ?? (typeof my !== 'undefined' && my !== null ? my : undefined)\n\tif (client === undefined) {\n\t\tthrow new Error(\n\t\t\t'[fatigue-kit] alipayAdapter: no `my` global found. Pass `{ client }` explicitly when not running inside Alipay.',\n\t\t)\n\t}\n\tconst onError =\n\t\toptions.onError ??\n\t\t((error: unknown) => {\n\t\t\tconsole.warn('[fatigue-kit] alipay storage operation failed:', error)\n\t\t})\n\treturn {\n\t\tget(key) {\n\t\t\ttry {\n\t\t\t\tconst response = client.getStorageSync({ key }) as { data?: unknown } | null | undefined\n\t\t\t\tconst data = response?.data\n\t\t\t\treturn typeof data === 'string' && data.length > 0 ? data : null\n\t\t\t} catch (error) {\n\t\t\t\tonError(error)\n\t\t\t\treturn null\n\t\t\t}\n\t\t},\n\t\tset(key, value) {\n\t\t\ttry {\n\t\t\t\tclient.setStorageSync({ key, data: value })\n\t\t\t} catch (error) {\n\t\t\t\tonError(error)\n\t\t\t}\n\t\t},\n\t\tremove(key) {\n\t\t\ttry {\n\t\t\t\tclient.removeStorageSync({ key })\n\t\t\t} catch (error) {\n\t\t\t\tonError(error)\n\t\t\t}\n\t\t},\n\t\tkeys() {\n\t\t\ttry {\n\t\t\t\treturn client.getStorageInfoSync?.().keys ?? []\n\t\t\t} catch (error) {\n\t\t\t\tonError(error)\n\t\t\t\treturn []\n\t\t\t}\n\t\t},\n\t}\n}\n"]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { SyncStorage } from '../index.cjs';
|
|
2
|
+
|
|
3
|
+
/** Alipay mini-program storage API (object arguments, values wrapped in `{ data }`). */
|
|
4
|
+
interface AlipayStorageLike {
|
|
5
|
+
getStorageSync(options: {
|
|
6
|
+
key: string;
|
|
7
|
+
}): unknown;
|
|
8
|
+
setStorageSync(options: {
|
|
9
|
+
key: string;
|
|
10
|
+
data: string;
|
|
11
|
+
}): void;
|
|
12
|
+
removeStorageSync(options: {
|
|
13
|
+
key: string;
|
|
14
|
+
}): void;
|
|
15
|
+
getStorageInfoSync?(): {
|
|
16
|
+
keys?: string[];
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
interface AlipayAdapterOptions {
|
|
20
|
+
/** Defaults to the `my` global; inject explicitly in tests or abstractions. */
|
|
21
|
+
client?: AlipayStorageLike;
|
|
22
|
+
onError?: (error: unknown) => void;
|
|
23
|
+
}
|
|
24
|
+
/** Alipay mini-program adapter backed by `my.getStorageSync` & friends. */
|
|
25
|
+
declare function alipayAdapter(options?: AlipayAdapterOptions): SyncStorage;
|
|
26
|
+
|
|
27
|
+
export { alipayAdapter };
|
|
28
|
+
export type { AlipayAdapterOptions, AlipayStorageLike };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { SyncStorage } from '../index.js';
|
|
2
|
+
|
|
3
|
+
/** Alipay mini-program storage API (object arguments, values wrapped in `{ data }`). */
|
|
4
|
+
interface AlipayStorageLike {
|
|
5
|
+
getStorageSync(options: {
|
|
6
|
+
key: string;
|
|
7
|
+
}): unknown;
|
|
8
|
+
setStorageSync(options: {
|
|
9
|
+
key: string;
|
|
10
|
+
data: string;
|
|
11
|
+
}): void;
|
|
12
|
+
removeStorageSync(options: {
|
|
13
|
+
key: string;
|
|
14
|
+
}): void;
|
|
15
|
+
getStorageInfoSync?(): {
|
|
16
|
+
keys?: string[];
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
interface AlipayAdapterOptions {
|
|
20
|
+
/** Defaults to the `my` global; inject explicitly in tests or abstractions. */
|
|
21
|
+
client?: AlipayStorageLike;
|
|
22
|
+
onError?: (error: unknown) => void;
|
|
23
|
+
}
|
|
24
|
+
/** Alipay mini-program adapter backed by `my.getStorageSync` & friends. */
|
|
25
|
+
declare function alipayAdapter(options?: AlipayAdapterOptions): SyncStorage;
|
|
26
|
+
|
|
27
|
+
export { alipayAdapter };
|
|
28
|
+
export type { AlipayAdapterOptions, AlipayStorageLike };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// src/adapters/alipay.ts
|
|
2
|
+
function alipayAdapter(options = {}) {
|
|
3
|
+
var _a, _b;
|
|
4
|
+
const client = (_a = options.client) != null ? _a : typeof my !== "undefined" && my !== null ? my : void 0;
|
|
5
|
+
if (client === void 0) {
|
|
6
|
+
throw new Error(
|
|
7
|
+
"[fatigue-kit] alipayAdapter: no `my` global found. Pass `{ client }` explicitly when not running inside Alipay."
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
const onError = (_b = options.onError) != null ? _b : ((error) => {
|
|
11
|
+
console.warn("[fatigue-kit] alipay storage operation failed:", error);
|
|
12
|
+
});
|
|
13
|
+
return {
|
|
14
|
+
get(key) {
|
|
15
|
+
try {
|
|
16
|
+
const response = client.getStorageSync({ key });
|
|
17
|
+
const data = response == null ? void 0 : response.data;
|
|
18
|
+
return typeof data === "string" && data.length > 0 ? data : null;
|
|
19
|
+
} catch (error) {
|
|
20
|
+
onError(error);
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
set(key, value) {
|
|
25
|
+
try {
|
|
26
|
+
client.setStorageSync({ key, data: value });
|
|
27
|
+
} catch (error) {
|
|
28
|
+
onError(error);
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
remove(key) {
|
|
32
|
+
try {
|
|
33
|
+
client.removeStorageSync({ key });
|
|
34
|
+
} catch (error) {
|
|
35
|
+
onError(error);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
keys() {
|
|
39
|
+
var _a2, _b2;
|
|
40
|
+
try {
|
|
41
|
+
return (_b2 = (_a2 = client.getStorageInfoSync) == null ? void 0 : _a2.call(client).keys) != null ? _b2 : [];
|
|
42
|
+
} catch (error) {
|
|
43
|
+
onError(error);
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export {
|
|
50
|
+
alipayAdapter
|
|
51
|
+
};
|
|
52
|
+
//# sourceMappingURL=alipay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/alipay.ts"],"sourcesContent":["import type { SyncStorage } from '../types'\n\n/** Alipay mini-program storage API (object arguments, values wrapped in `{ data }`). */\nexport interface AlipayStorageLike {\n\tgetStorageSync(options: { key: string }): unknown\n\tsetStorageSync(options: { key: string; data: string }): void\n\tremoveStorageSync(options: { key: string }): void\n\tgetStorageInfoSync?(): { keys?: string[] }\n}\n\nexport interface AlipayAdapterOptions {\n\t/** Defaults to the `my` global; inject explicitly in tests or abstractions. */\n\tclient?: AlipayStorageLike\n\tonError?: (error: unknown) => void\n}\n\ndeclare const my: AlipayStorageLike | undefined\n\n/** Alipay mini-program adapter backed by `my.getStorageSync` & friends. */\nexport function alipayAdapter(options: AlipayAdapterOptions = {}): SyncStorage {\n\tconst client = options.client ?? (typeof my !== 'undefined' && my !== null ? my : undefined)\n\tif (client === undefined) {\n\t\tthrow new Error(\n\t\t\t'[fatigue-kit] alipayAdapter: no `my` global found. Pass `{ client }` explicitly when not running inside Alipay.',\n\t\t)\n\t}\n\tconst onError =\n\t\toptions.onError ??\n\t\t((error: unknown) => {\n\t\t\tconsole.warn('[fatigue-kit] alipay storage operation failed:', error)\n\t\t})\n\treturn {\n\t\tget(key) {\n\t\t\ttry {\n\t\t\t\tconst response = client.getStorageSync({ key }) as { data?: unknown } | null | undefined\n\t\t\t\tconst data = response?.data\n\t\t\t\treturn typeof data === 'string' && data.length > 0 ? data : null\n\t\t\t} catch (error) {\n\t\t\t\tonError(error)\n\t\t\t\treturn null\n\t\t\t}\n\t\t},\n\t\tset(key, value) {\n\t\t\ttry {\n\t\t\t\tclient.setStorageSync({ key, data: value })\n\t\t\t} catch (error) {\n\t\t\t\tonError(error)\n\t\t\t}\n\t\t},\n\t\tremove(key) {\n\t\t\ttry {\n\t\t\t\tclient.removeStorageSync({ key })\n\t\t\t} catch (error) {\n\t\t\t\tonError(error)\n\t\t\t}\n\t\t},\n\t\tkeys() {\n\t\t\ttry {\n\t\t\t\treturn client.getStorageInfoSync?.().keys ?? []\n\t\t\t} catch (error) {\n\t\t\t\tonError(error)\n\t\t\t\treturn []\n\t\t\t}\n\t\t},\n\t}\n}\n"],"mappings":";AAmBO,SAAS,cAAc,UAAgC,CAAC,GAAgB;AAnB/E;AAoBC,QAAM,UAAS,aAAQ,WAAR,YAAmB,OAAO,OAAO,eAAe,OAAO,OAAO,KAAK;AAClF,MAAI,WAAW,QAAW;AACzB,UAAM,IAAI;AAAA,MACT;AAAA,IACD;AAAA,EACD;AACA,QAAM,WACL,aAAQ,YAAR,aACC,CAAC,UAAmB;AACpB,YAAQ,KAAK,kDAAkD,KAAK;AAAA,EACrE;AACD,SAAO;AAAA,IACN,IAAI,KAAK;AACR,UAAI;AACH,cAAM,WAAW,OAAO,eAAe,EAAE,IAAI,CAAC;AAC9C,cAAM,OAAO,qCAAU;AACvB,eAAO,OAAO,SAAS,YAAY,KAAK,SAAS,IAAI,OAAO;AAAA,MAC7D,SAAS,OAAO;AACf,gBAAQ,KAAK;AACb,eAAO;AAAA,MACR;AAAA,IACD;AAAA,IACA,IAAI,KAAK,OAAO;AACf,UAAI;AACH,eAAO,eAAe,EAAE,KAAK,MAAM,MAAM,CAAC;AAAA,MAC3C,SAAS,OAAO;AACf,gBAAQ,KAAK;AAAA,MACd;AAAA,IACD;AAAA,IACA,OAAO,KAAK;AACX,UAAI;AACH,eAAO,kBAAkB,EAAE,IAAI,CAAC;AAAA,MACjC,SAAS,OAAO;AACf,gBAAQ,KAAK;AAAA,MACd;AAAA,IACD;AAAA,IACA,OAAO;AAxDT,UAAAA,KAAAC;AAyDG,UAAI;AACH,gBAAOA,OAAAD,MAAA,OAAO,uBAAP,gBAAAA,IAAA,aAA8B,SAA9B,OAAAC,MAAsC,CAAC;AAAA,MAC/C,SAAS,OAAO;AACf,gBAAQ,KAAK;AACb,eAAO,CAAC;AAAA,MACT;AAAA,IACD;AAAA,EACD;AACD;","names":["_a","_b"]}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true});// src/adapters/async-storage.ts
|
|
2
|
+
var DEFAULT_NAMESPACE = "fatigue-kit";
|
|
3
|
+
var DEFAULT_FLUSH_DELAY = 50;
|
|
4
|
+
function asyncStorageAdapter(client, options = {}) {
|
|
5
|
+
var _a, _b, _c;
|
|
6
|
+
const namespace = (_a = options.namespace) != null ? _a : DEFAULT_NAMESPACE;
|
|
7
|
+
const flushDelay = (_b = options.flushDelay) != null ? _b : DEFAULT_FLUSH_DELAY;
|
|
8
|
+
const onError = (_c = options.onError) != null ? _c : ((error) => {
|
|
9
|
+
console.warn("[fatigue-kit] async storage operation failed:", error);
|
|
10
|
+
});
|
|
11
|
+
const indexKey = `${namespace}:__index__`;
|
|
12
|
+
const cache = /* @__PURE__ */ new Map();
|
|
13
|
+
const pending = /* @__PURE__ */ new Map();
|
|
14
|
+
const index = /* @__PURE__ */ new Set();
|
|
15
|
+
let indexDirty = false;
|
|
16
|
+
let timer = null;
|
|
17
|
+
const safe = async (operation) => {
|
|
18
|
+
try {
|
|
19
|
+
return await operation();
|
|
20
|
+
} catch (error) {
|
|
21
|
+
onError(error);
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const ready = (async () => {
|
|
26
|
+
const rawIndex = await safe(() => client.getItem(indexKey));
|
|
27
|
+
if (rawIndex !== null) {
|
|
28
|
+
try {
|
|
29
|
+
for (const key of JSON.parse(rawIndex)) index.add(key);
|
|
30
|
+
} catch (e) {
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
await Promise.all(
|
|
34
|
+
[...index].map(async (key) => {
|
|
35
|
+
cache.set(key, await safe(() => client.getItem(key)));
|
|
36
|
+
})
|
|
37
|
+
);
|
|
38
|
+
})();
|
|
39
|
+
async function flush() {
|
|
40
|
+
if (timer !== null) {
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
timer = null;
|
|
43
|
+
}
|
|
44
|
+
const writes = [...pending];
|
|
45
|
+
pending.clear();
|
|
46
|
+
await Promise.all(
|
|
47
|
+
writes.map(
|
|
48
|
+
([key, value]) => value === null ? safe(() => client.removeItem(key)) : safe(() => client.setItem(key, value))
|
|
49
|
+
)
|
|
50
|
+
);
|
|
51
|
+
if (indexDirty) {
|
|
52
|
+
indexDirty = false;
|
|
53
|
+
await safe(() => client.setItem(indexKey, JSON.stringify([...index])));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function schedule() {
|
|
57
|
+
if (timer !== null) return;
|
|
58
|
+
timer = setTimeout(() => {
|
|
59
|
+
timer = null;
|
|
60
|
+
void flush();
|
|
61
|
+
}, flushDelay);
|
|
62
|
+
}
|
|
63
|
+
const storage = {
|
|
64
|
+
get: (key) => {
|
|
65
|
+
var _a2;
|
|
66
|
+
return (_a2 = cache.get(key)) != null ? _a2 : null;
|
|
67
|
+
},
|
|
68
|
+
set: (key, value) => {
|
|
69
|
+
cache.set(key, value);
|
|
70
|
+
pending.set(key, value);
|
|
71
|
+
if (!index.has(key)) {
|
|
72
|
+
index.add(key);
|
|
73
|
+
indexDirty = true;
|
|
74
|
+
}
|
|
75
|
+
schedule();
|
|
76
|
+
},
|
|
77
|
+
remove: (key) => {
|
|
78
|
+
cache.delete(key);
|
|
79
|
+
pending.set(key, null);
|
|
80
|
+
if (index.has(key)) {
|
|
81
|
+
index.delete(key);
|
|
82
|
+
indexDirty = true;
|
|
83
|
+
}
|
|
84
|
+
schedule();
|
|
85
|
+
},
|
|
86
|
+
keys: () => [...index]
|
|
87
|
+
};
|
|
88
|
+
return { storage, ready, flush };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
exports.asyncStorageAdapter = asyncStorageAdapter;
|
|
93
|
+
//# sourceMappingURL=async-storage.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/fatigue-kit/fatigue-kit/dist/adapters/async-storage.cjs","../../src/adapters/async-storage.ts"],"names":["_a"],"mappings":"AAAA;AC6BA,IAAM,kBAAA,EAAoB,aAAA;AAC1B,IAAM,oBAAA,EAAsB,EAAA;AAqBrB,SAAS,mBAAA,CACf,MAAA,EACA,QAAA,EAAsC,CAAC,CAAA,EACjB;AAtDvB,EAAA,IAAA,EAAA,EAAA,EAAA,EAAA,EAAA;AAuDC,EAAA,MAAM,UAAA,EAAA,CAAY,GAAA,EAAA,OAAA,CAAQ,SAAA,EAAA,GAAR,KAAA,EAAA,GAAA,EAAqB,iBAAA;AACvC,EAAA,MAAM,WAAA,EAAA,CAAa,GAAA,EAAA,OAAA,CAAQ,UAAA,EAAA,GAAR,KAAA,EAAA,GAAA,EAAsB,mBAAA;AACzC,EAAA,MAAM,QAAA,EAAA,CACL,GAAA,EAAA,OAAA,CAAQ,OAAA,EAAA,GAAR,KAAA,EAAA,GAAA,EAAA,CACC,CAAC,KAAA,EAAA,GAAmB;AACpB,IAAA,OAAA,CAAQ,IAAA,CAAK,+CAAA,EAAiD,KAAK,CAAA;AAAA,EACpE,CAAA,CAAA;AACD,EAAA,MAAM,SAAA,EAAW,CAAA,EAAA;AACH,EAAA;AACE,EAAA;AACF,EAAA;AACG,EAAA;AACiC,EAAA;AAErC,EAAA;AACR,IAAA;AACU,MAAA;AACE,IAAA;AACF,MAAA;AACN,MAAA;AACR,IAAA;AACD,EAAA;AAEe,EAAA;AACR,IAAA;AACF,IAAA;AACC,MAAA;AACQ,QAAA;AACJ,MAAA;AAER,MAAA;AACD,IAAA;AACc,IAAA;AACF,MAAA;AACA,QAAA;AACV,MAAA;AACF,IAAA;AACE,EAAA;AAEY,EAAA;AACA,IAAA;AACA,MAAA;AACL,MAAA;AACT,IAAA;AACgB,IAAA;AACF,IAAA;AACA,IAAA;AACN,MAAA;AAAgB,QAAA;AAIvB,MAAA;AACD,IAAA;AACgB,IAAA;AACF,MAAA;AACF,MAAA;AACZ,IAAA;AACD,EAAA;AAES,EAAA;AACM,IAAA;AACN,IAAA;AACC,MAAA;AACG,MAAA;AACC,IAAA;AACd,EAAA;AAE6B,EAAA;AACjB,IAAA;AA3HbA,MAAAA;AA2HgB,MAAA;AAAkB,IAAA;AACrB,IAAA;AACA,MAAA;AACE,MAAA;AACD,MAAA;AACA,QAAA;AACV,QAAA;AACD,MAAA;AACS,MAAA;AACV,IAAA;AACS,IAAA;AACK,MAAA;AACD,MAAA;AACE,MAAA;AACP,QAAA;AACN,QAAA;AACD,MAAA;AACS,MAAA;AACV,IAAA;AACgB,IAAA;AACjB,EAAA;AAEkB,EAAA;AACnB;ADzDoB;AACA;AACA","file":"/home/runner/work/fatigue-kit/fatigue-kit/dist/adapters/async-storage.cjs","sourcesContent":[null,"import type { SyncStorage } from '../types'\n\n/**\n * Minimal interface shared by `@react-native-async-storage/async-storage` and\n * most RN-compatible storage libraries. Pass your storage instance directly.\n */\nexport interface AsyncStorageLike {\n\tgetItem(key: string): Promise<string | null>\n\tsetItem(key: string, value: string): Promise<void>\n\tremoveItem(key: string): Promise<void>\n}\n\nexport interface AsyncStorageAdapter {\n\t/** Synchronous view over the async backend. Only use it once `ready` resolved. */\n\tstorage: SyncStorage\n\t/** Resolves once the in-memory cache has been hydrated from the backend. */\n\tready: Promise<void>\n\t/** Persist pending writes now. Also runs automatically, debounced. */\n\tflush(): Promise<void>\n}\n\nexport interface AsyncStorageAdapterOptions {\n\t/** Must match the `namespace` of the fatigue instance. Default `'fatigue-kit'`. */\n\tnamespace?: string\n\t/** Debounce window for background flushes, ms. Default `50`. */\n\tflushDelay?: number\n\tonError?: (error: unknown) => void\n}\n\nconst DEFAULT_NAMESPACE = 'fatigue-kit'\nconst DEFAULT_FLUSH_DELAY = 50\n\n/**\n * React Native (and any async backend) adapter.\n *\n * The core engine is synchronous for atomicity, so this adapter hydrates all\n * known keys once up front (`ready`), serves reads from memory, and writes back\n * in debounced batches. An index key keeps track of which keys belong to\n * fatigue-kit. Call `flush()` from an `AppState` \"background\" listener so the\n * last writes survive an app kill.\n *\n * ```ts\n * import AsyncStorage from '@react-native-async-storage/async-storage'\n * const handle = asyncStorageAdapter(AsyncStorage)\n * await handle.ready\n * const fatigue = createFatigue({ storage: handle.storage })\n * AppState.addEventListener('change', (state) => {\n * if (state === 'background') handle.flush()\n * })\n * ```\n */\nexport function asyncStorageAdapter(\n\tclient: AsyncStorageLike,\n\toptions: AsyncStorageAdapterOptions = {},\n): AsyncStorageAdapter {\n\tconst namespace = options.namespace ?? DEFAULT_NAMESPACE\n\tconst flushDelay = options.flushDelay ?? DEFAULT_FLUSH_DELAY\n\tconst onError =\n\t\toptions.onError ??\n\t\t((error: unknown) => {\n\t\t\tconsole.warn('[fatigue-kit] async storage operation failed:', error)\n\t\t})\n\tconst indexKey = `${namespace}:__index__`\n\tconst cache = new Map<string, string | null>()\n\tconst pending = new Map<string, string | null>()\n\tconst index = new Set<string>()\n\tlet indexDirty = false\n\tlet timer: ReturnType<typeof setTimeout> | null = null\n\n\tconst safe = async <T>(operation: () => Promise<T>): Promise<T | null> => {\n\t\ttry {\n\t\t\treturn await operation()\n\t\t} catch (error) {\n\t\t\tonError(error)\n\t\t\treturn null\n\t\t}\n\t}\n\n\tconst ready = (async () => {\n\t\tconst rawIndex = await safe(() => client.getItem(indexKey))\n\t\tif (rawIndex !== null) {\n\t\t\ttry {\n\t\t\t\tfor (const key of JSON.parse(rawIndex) as string[]) index.add(key)\n\t\t\t} catch {\n\t\t\t\t// Corrupt index: start empty; keys re-register on their next write.\n\t\t\t}\n\t\t}\n\t\tawait Promise.all(\n\t\t\t[...index].map(async (key) => {\n\t\t\t\tcache.set(key, await safe(() => client.getItem(key)))\n\t\t\t}),\n\t\t)\n\t})()\n\n\tasync function flush(): Promise<void> {\n\t\tif (timer !== null) {\n\t\t\tclearTimeout(timer)\n\t\t\ttimer = null\n\t\t}\n\t\tconst writes = [...pending]\n\t\tpending.clear()\n\t\tawait Promise.all(\n\t\t\twrites.map(([key, value]) =>\n\t\t\t\tvalue === null\n\t\t\t\t\t? safe(() => client.removeItem(key))\n\t\t\t\t\t: safe(() => client.setItem(key, value)),\n\t\t\t),\n\t\t)\n\t\tif (indexDirty) {\n\t\t\tindexDirty = false\n\t\t\tawait safe(() => client.setItem(indexKey, JSON.stringify([...index])))\n\t\t}\n\t}\n\n\tfunction schedule(): void {\n\t\tif (timer !== null) return\n\t\ttimer = setTimeout(() => {\n\t\t\ttimer = null\n\t\t\tvoid flush()\n\t\t}, flushDelay)\n\t}\n\n\tconst storage: SyncStorage = {\n\t\tget: (key) => cache.get(key) ?? null,\n\t\tset: (key, value) => {\n\t\t\tcache.set(key, value)\n\t\t\tpending.set(key, value)\n\t\t\tif (!index.has(key)) {\n\t\t\t\tindex.add(key)\n\t\t\t\tindexDirty = true\n\t\t\t}\n\t\t\tschedule()\n\t\t},\n\t\tremove: (key) => {\n\t\t\tcache.delete(key)\n\t\t\tpending.set(key, null)\n\t\t\tif (index.has(key)) {\n\t\t\t\tindex.delete(key)\n\t\t\t\tindexDirty = true\n\t\t\t}\n\t\t\tschedule()\n\t\t},\n\t\tkeys: () => [...index],\n\t}\n\n\treturn { storage, ready, flush }\n}\n"]}
|