@h-ai/cache 0.1.0-alpha.67 → 0.1.0-alpha.68
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/README.en.md +60 -34
- package/README.ja.md +60 -34
- package/README.md +71 -34
- package/dist/index.d.ts +291 -93
- package/dist/index.js +423 -133
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.en.md
CHANGED
|
@@ -10,6 +10,20 @@ A unified cache module. The `cache` object provides KV / Hash / List / Set / ZSe
|
|
|
10
10
|
|
|
11
11
|
Memory KV entries expire at their expiration time. `incr/decr/incrBy/decrBy` accept only safe integers or integer strings; invalid increments and overflow return `OPERATION_FAILED` without changing the original value. Hash names such as `__proto__` / `constructor` remain ordinary fields and do not change the returned object's prototype.
|
|
12
12
|
|
|
13
|
+
Connections become available only after a successful health check. Failed initialization disposes its candidate; `error.ext.cleanupPending === true` identifies cleanup that still needs retry. `cache.close()` returns `Promise<HaiResult<void>>`, waits for initialization, and coalesces concurrent close calls. Once cleanup starts, `isInitialized` is false and configuration/operations are unavailable. A failed cleanup retains the resource: the next `close()` or `init()` retries disposal before allocating a replacement. Always inspect the close result.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
const closed = await cache.close()
|
|
17
|
+
if (!closed.success) {
|
|
18
|
+
// Retry once after addressing the cleanup failure; report another failure.
|
|
19
|
+
const retried = await cache.close()
|
|
20
|
+
if (!retried.success)
|
|
21
|
+
throw new Error(retried.error.message)
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Redis falls back to disconnect when QUIT fails, and reports success only after releasing the client.
|
|
26
|
+
|
|
13
27
|
## Core features
|
|
14
28
|
|
|
15
29
|
- In-memory caching for development and tests
|
|
@@ -22,37 +36,27 @@ Memory KV entries expire at their expiration time. `incr/decr/incrBy/decrBy` acc
|
|
|
22
36
|
### Quick start
|
|
23
37
|
|
|
24
38
|
```ts
|
|
25
|
-
import { cache
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
39
|
+
import { cache } from '@h-ai/cache'
|
|
40
|
+
import { core } from '@h-ai/core'
|
|
41
|
+
|
|
42
|
+
const initialized = await cache.init({ type: 'redis', host: 'localhost' })
|
|
43
|
+
if (!initialized.success)
|
|
44
|
+
throw new Error(initialized.error.message)
|
|
45
|
+
try {
|
|
46
|
+
const written = await cache.kv.set('user:1', { name: 'Alice' }, { ex: 3600 })
|
|
47
|
+
if (!written.success)
|
|
48
|
+
throw new Error(written.error.message)
|
|
49
|
+
const user = await cache.kv.get<{ name: string }>('user:1')
|
|
50
|
+
if (!user.success)
|
|
51
|
+
throw new Error(user.error.message)
|
|
52
|
+
if (user.data)
|
|
53
|
+
core.logger.info('Cached user', { name: user.data.name })
|
|
30
54
|
}
|
|
31
|
-
|
|
32
|
-
await cache.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
await cache.hash.hset('profile:1', { age: 18, city: 'Hangzhou' })
|
|
36
|
-
await cache.list.lpush('queue:jobs', 'job-1', 'job-2')
|
|
37
|
-
await cache.set_.sadd('role:admin:perms', 'user.read', 'user.write')
|
|
38
|
-
await cache.zset.zadd('rank', { member: 'u1', score: 100 })
|
|
39
|
-
|
|
40
|
-
// 分布式锁
|
|
41
|
-
const acquired = await cache.lock.acquire('my-lock', { ttl: 30, owner: 'node-1' })
|
|
42
|
-
if (acquired.success && acquired.data) {
|
|
43
|
-
try {
|
|
44
|
-
// 执行受保护的操作
|
|
45
|
-
}
|
|
46
|
-
finally {
|
|
47
|
-
await cache.lock.release('my-lock', 'node-1')
|
|
48
|
-
}
|
|
55
|
+
finally {
|
|
56
|
+
const closed = await cache.close()
|
|
57
|
+
if (!closed.success)
|
|
58
|
+
core.logger.error('Cache cleanup pending', { code: closed.error.code })
|
|
49
59
|
}
|
|
50
|
-
|
|
51
|
-
if (!user.success && user.error.code === HaiCacheError.NOT_INITIALIZED.code) {
|
|
52
|
-
// 请先调用 cache.init()
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
await cache.close()
|
|
56
60
|
```
|
|
57
61
|
|
|
58
62
|
`cache.config` returns a **redacted configuration snapshot**. Redis credential fields such as `password` / `url` are sanitized automatically; do not use the snapshot to write back original secrets.
|
|
@@ -97,6 +101,10 @@ await cache.init({
|
|
|
97
101
|
})
|
|
98
102
|
```
|
|
99
103
|
|
|
104
|
+
`cache.init(config)` selects the built-in memory/redis backend. Provider implementations are internal; there is no custom backend registration API.
|
|
105
|
+
|
|
106
|
+
`password` authenticates standalone, Cluster, and Sentinel data nodes, including discovered/redirected Cluster connections. In `url` mode the credentials in the URL take precedence; the separate password field does not override them.
|
|
107
|
+
|
|
100
108
|
### API overview
|
|
101
109
|
|
|
102
110
|
- `cache.kv`: string/object key-value storage and TTL (`set/get/mget/mset/expire/ttl/...`).
|
|
@@ -105,7 +113,16 @@ await cache.init({
|
|
|
105
113
|
- `cache.set_`: sets (`sadd/smembers/sismember/...`).
|
|
106
114
|
- `cache.zset`: sorted sets (`zadd/zrange/zrevrange/...`).
|
|
107
115
|
- `cache.lock`: distributed locks (`acquire/release/isLocked/extend`).
|
|
108
|
-
|
|
116
|
+
|
|
117
|
+
`kv.set` returns `HaiResult<boolean>`: `data=true` means the write applied. An NX/XX miss returns `success=true,data=false` without changing the value or TTL. Check both fields before issuing tickets, sessions or key registrations. `ex/px/exat/pxat` use seconds, milliseconds, Unix seconds and Unix milliseconds; choose one positive safe integer, excluding `keepTtl=true`. NX and XX are mutually exclusive. Zero, negative, fractional, non-finite or overflowing values return `INVALID_SET_OPTIONS` (`cache:016`) without creating a permanent key. A positive absolute deadline in the past expires immediately. Omitting expiry removes an existing TTL; `keepTtl` retains it and leaves new keys permanent. Both providers store JSON snapshots.
|
|
118
|
+
|
|
119
|
+
compareAndSwap atomically compares one JSON value and replaces/deletes it, returning boolean applied. Undefined expected means absent; undefined value deletes; null is a JSON value. px is a positive integer and excludes keepTtl. Omitting expiry persists the replacement; a comparison miss preserves TTL. Use it for shared versioned commits.
|
|
120
|
+
|
|
121
|
+
### Cluster multi-key operations and scanning
|
|
122
|
+
|
|
123
|
+
`mget/del/exists` support multiple slots through bounded independent key operations. MGET preserves input order; there is no cross-slot atomicity or snapshot. A failed DEL may already have deleted some keys; retry is idempotent. `mset`, set algebra and multi-key blocking pops require a shared slot; group keys with a common `{hash-tag}` or receive `CROSS_SLOT`. MSET never falls back to partial writes.
|
|
124
|
+
|
|
125
|
+
Start `scan` with string `'0'` and pass the returned opaque cursor unchanged. Another application node with the same configuration and topology can continue it. Cluster scans every master. A topology, match or keyPrefix change returns `SCAN_TOPOLOGY_CHANGED`; restart from `'0'`. Returned keys omit keyPrefix, whose glob characters are literal. `keys` follows the same rule and deduplicates. SCAN has no snapshot and may repeat keys; count is a positive integer hint, not an output limit. Invalid cursors/count return `INVALID_SCAN`.
|
|
109
126
|
|
|
110
127
|
### Distributed locks
|
|
111
128
|
|
|
@@ -113,13 +130,13 @@ Distributed mutual exclusion based on SET NX EX, suitable for deployments across
|
|
|
113
130
|
|
|
114
131
|
```ts
|
|
115
132
|
// 获取锁(TTL 30 秒,owner 用于标识持有者)
|
|
116
|
-
const acquired = await cache.lock.acquire('my-lock', { ttl: 30
|
|
133
|
+
const acquired = await cache.lock.acquire('my-lock', { ttl: 30 })
|
|
117
134
|
if (acquired.success && acquired.data) {
|
|
118
135
|
try {
|
|
119
136
|
// 执行受保护操作
|
|
120
137
|
}
|
|
121
138
|
finally {
|
|
122
|
-
await cache.lock.release('my-lock',
|
|
139
|
+
await cache.lock.release('my-lock', acquired.data)
|
|
123
140
|
}
|
|
124
141
|
}
|
|
125
142
|
|
|
@@ -127,12 +144,15 @@ if (acquired.success && acquired.data) {
|
|
|
127
144
|
const locked = await cache.lock.isLocked('my-lock')
|
|
128
145
|
|
|
129
146
|
// 续期锁
|
|
130
|
-
|
|
147
|
+
if (acquired.success && acquired.data)
|
|
148
|
+
await cache.lock.extend('my-lock', 60, acquired.data)
|
|
131
149
|
```
|
|
132
150
|
|
|
133
151
|
- **Memory backend**: Map-based storage with TTL expiration.
|
|
134
152
|
- **Redis backend**: atomic locking with `SET NX EX`; Lua scripts verify the owner for safe release and renewal.
|
|
135
|
-
- `
|
|
153
|
+
- `acquire` returns a unique acquisition token, or `null` while occupied. Release/renewal require this token; node IDs and ownerless operations are not supported.
|
|
154
|
+
|
|
155
|
+
The owner token only protects lease renewal and release; it is not a resource fencing token. A paused task can resume after expiry. Final writes require CAS, generation predicates or transactional constraints in the resource store; renewal or returning a failure afterwards cannot prevent a stale commit.
|
|
136
156
|
|
|
137
157
|
### Error codes
|
|
138
158
|
|
|
@@ -150,6 +170,10 @@ All operations return `HaiResult<T>`. Common error codes:
|
|
|
150
170
|
| `HaiCacheError.UNSUPPORTED_TYPE` | `hai:cache:011` | Unsupported cache type |
|
|
151
171
|
| `HaiCacheError.CONFIG_ERROR` | `hai:cache:012` | Invalid configuration |
|
|
152
172
|
|
|
173
|
+
## Production shared state
|
|
174
|
+
|
|
175
|
+
Production requires Redis (including Cluster); memory is for development/explicit tests. Redis errors fail rather than falling back locally. Initialization checks apply when NODE_ENV or HAI_CORE_ENV is production; only explicit HAI_E2E=1/HAI_E2E_MOCK=1 tests are exempt. [State, concurrency and recovery (Chinese)](../../.agents/instructions/distributed-state.md)。
|
|
176
|
+
|
|
153
177
|
## Development and testing
|
|
154
178
|
|
|
155
179
|
```bash
|
|
@@ -158,6 +182,8 @@ pnpm --filter @h-ai/cache test
|
|
|
158
182
|
|
|
159
183
|
> Redis tests require Docker.
|
|
160
184
|
|
|
185
|
+
Public contracts and internal structures include typechecked examples; see the [JSDoc conventions (Chinese)](../../.agents/instructions/module-conventions.md#jsdoc-与示例验证).
|
|
186
|
+
|
|
161
187
|
## License
|
|
162
188
|
|
|
163
189
|
Apache-2.0
|
package/README.ja.md
CHANGED
|
@@ -10,6 +10,20 @@
|
|
|
10
10
|
|
|
11
11
|
メモリー KV は有効期限に達した時点で無効になります。`incr/decr/incrBy/decrBy` は安全な整数値または整数文字列のみを受け付け、不正な増分やオーバーフローでは元の値を保持して `OPERATION_FAILED` を返します。Hash の `__proto__` / `constructor` などは通常のフィールドとして保持され、戻り値のオブジェクトのプロトタイプを変更しません。
|
|
12
12
|
|
|
13
|
+
接続はヘルスチェック成功後に公開します。初期化失敗時は候補リソースを解放し、解放にも失敗した場合は初期化エラーの `error.ext.cleanupPending` が `true` になります。`cache.close()` は `Promise<HaiResult<void>>` を返し、初期化を待機して並行する終了処理をまとめます。解放開始後は `isInitialized` が `false` となり、設定と操作は利用できません。解放に失敗したリソースは保持され、次の `close()` または `init()` で再試行します。解放が成功するまで代替接続は作成しません。終了結果を必ず確認してください。
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
const closed = await cache.close()
|
|
17
|
+
if (!closed.success) {
|
|
18
|
+
// 障害への対処後に一度再試行し、再失敗はアプリへ報告します。
|
|
19
|
+
const retried = await cache.close()
|
|
20
|
+
if (!retried.success)
|
|
21
|
+
throw new Error(retried.error.message)
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Redis の QUIT が失敗した場合は disconnect で切断し、解放が完了した場合のみ成功を返します。
|
|
26
|
+
|
|
13
27
|
## 主な機能
|
|
14
28
|
|
|
15
29
|
- 開発・テスト用のメモリーキャッシュ
|
|
@@ -22,37 +36,27 @@
|
|
|
22
36
|
### クイックスタート
|
|
23
37
|
|
|
24
38
|
```ts
|
|
25
|
-
import { cache
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
39
|
+
import { cache } from '@h-ai/cache'
|
|
40
|
+
import { core } from '@h-ai/core'
|
|
41
|
+
|
|
42
|
+
const initialized = await cache.init({ type: 'redis', host: 'localhost' })
|
|
43
|
+
if (!initialized.success)
|
|
44
|
+
throw new Error(initialized.error.message)
|
|
45
|
+
try {
|
|
46
|
+
const written = await cache.kv.set('user:1', { name: 'Alice' }, { ex: 3600 })
|
|
47
|
+
if (!written.success)
|
|
48
|
+
throw new Error(written.error.message)
|
|
49
|
+
const user = await cache.kv.get<{ name: string }>('user:1')
|
|
50
|
+
if (!user.success)
|
|
51
|
+
throw new Error(user.error.message)
|
|
52
|
+
if (user.data)
|
|
53
|
+
core.logger.info('Cached user', { name: user.data.name })
|
|
30
54
|
}
|
|
31
|
-
|
|
32
|
-
await cache.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
await cache.hash.hset('profile:1', { age: 18, city: 'Hangzhou' })
|
|
36
|
-
await cache.list.lpush('queue:jobs', 'job-1', 'job-2')
|
|
37
|
-
await cache.set_.sadd('role:admin:perms', 'user.read', 'user.write')
|
|
38
|
-
await cache.zset.zadd('rank', { member: 'u1', score: 100 })
|
|
39
|
-
|
|
40
|
-
// 分布式锁
|
|
41
|
-
const acquired = await cache.lock.acquire('my-lock', { ttl: 30, owner: 'node-1' })
|
|
42
|
-
if (acquired.success && acquired.data) {
|
|
43
|
-
try {
|
|
44
|
-
// 执行受保护的操作
|
|
45
|
-
}
|
|
46
|
-
finally {
|
|
47
|
-
await cache.lock.release('my-lock', 'node-1')
|
|
48
|
-
}
|
|
55
|
+
finally {
|
|
56
|
+
const closed = await cache.close()
|
|
57
|
+
if (!closed.success)
|
|
58
|
+
core.logger.error('Cache cleanup pending', { code: closed.error.code })
|
|
49
59
|
}
|
|
50
|
-
|
|
51
|
-
if (!user.success && user.error.code === HaiCacheError.NOT_INITIALIZED.code) {
|
|
52
|
-
// 请先调用 cache.init()
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
await cache.close()
|
|
56
60
|
```
|
|
57
61
|
|
|
58
62
|
`cache.config` は**機密情報をマスクした設定スナップショット**を返します。Redis の `password` / `url` などの認証情報は自動処理されるため、元の秘密情報を書き戻す用途には使わないでください。
|
|
@@ -97,6 +101,10 @@ await cache.init({
|
|
|
97
101
|
})
|
|
98
102
|
```
|
|
99
103
|
|
|
104
|
+
`cache.init(config)` は組み込み memory/redis を選択します。Provider は内部契約であり、独自バックエンドの登録 API はありません。
|
|
105
|
+
|
|
106
|
+
`password` は単独・Cluster・Sentinel のデータノードに使用され、発見・リダイレクト後の接続にも適用します。`url` 指定時は URL 内の認証情報を使用し、独立した password フィールドでは上書きしません。
|
|
107
|
+
|
|
100
108
|
### API 一覧
|
|
101
109
|
|
|
102
110
|
- `cache.kv`:文字列・オブジェクトのキーと値、および TTL(`set/get/mget/mset/expire/ttl/...`)。
|
|
@@ -105,7 +113,16 @@ await cache.init({
|
|
|
105
113
|
- `cache.set_`:集合(`sadd/smembers/sismember/...`)。
|
|
106
114
|
- `cache.zset`:ソート済み集合(`zadd/zrange/zrevrange/...`)。
|
|
107
115
|
- `cache.lock`:分散ロック(`acquire/release/isLocked/extend`)。
|
|
108
|
-
|
|
116
|
+
|
|
117
|
+
`kv.set` は `HaiResult<boolean>` を返し、`data=true` は書き込みの適用を表します。NX/XX が不成立なら `success=true,data=false` となり、値と TTL を変更しません。チケット、セッション、公鍵登録の発行前に両方を確認してください。`ex/px/exat/pxat` の単位は秒、ミリ秒、Unix 秒、Unix ミリ秒です。正の安全な整数を一つだけ指定し、`keepTtl=true` と併用できません。NX と XX も排他です。ゼロ、負数、小数、非有限値、オーバーフローは `INVALID_SET_OPTIONS`(`cache:016`)となり、永続キーを作成しません。過去の正の絶対期限は即時失効します。期限を省略すると既存 TTL は削除され、`keepTtl` は既存 TTL を維持します。新規キーは期限なしです。両 provider は JSON スナップショットを保存します。
|
|
118
|
+
|
|
119
|
+
compareAndSwap は単一キーの JSON 値を原子的に比較・置換/削除し、適用有無を返します。expected の undefined は不存在、value の undefined は削除、null は JSON 値です。px は正の整数で keepTtl と排他です。期限省略時は永続保存し、不一致なら TTL を変更しません。
|
|
120
|
+
|
|
121
|
+
### Cluster の複数キー操作と走査
|
|
122
|
+
|
|
123
|
+
`mget/del/exists` は slot ごとの独立操作を有界並行実行し、MGET は入力順序を維持します。slot 間の原子性やスナップショットは提供しません。DEL の失敗時は一部削除済みの場合があり、再試行は冪等です。`mset`、集合演算、複数キーの blocking pop は同一 slot が必要です。共通の `{hash-tag}` を使わない場合は `CROSS_SLOT` を返し、MSET の部分書き込みは行いません。
|
|
124
|
+
|
|
125
|
+
`scan` は文字列 `'0'` で開始し、不透明な返却カーソルをそのまま渡します。同じ設定とトポロジーなら別のアプリケーションノードで継続できます。Cluster の全 master を走査し、トポロジー・match・keyPrefix が変わると `SCAN_TOPOLOGY_CHANGED` を返すため `'0'` から再開します。返却キーから keyPrefix を除去し、prefix の glob 文字はリテラルとして扱います。`keys` も同じ規則で重複を除去します。SCAN はスナップショットではなく重複を許容し、count は正の整数の目安です。無効なカーソル/count は `INVALID_SCAN` を返します。
|
|
109
126
|
|
|
110
127
|
### 分散ロック
|
|
111
128
|
|
|
@@ -113,13 +130,13 @@ SET NX EX を利用する分散排他ロックで、複数ノードのデプロ
|
|
|
113
130
|
|
|
114
131
|
```ts
|
|
115
132
|
// 获取锁(TTL 30 秒,owner 用于标识持有者)
|
|
116
|
-
const acquired = await cache.lock.acquire('my-lock', { ttl: 30
|
|
133
|
+
const acquired = await cache.lock.acquire('my-lock', { ttl: 30 })
|
|
117
134
|
if (acquired.success && acquired.data) {
|
|
118
135
|
try {
|
|
119
136
|
// 执行受保护操作
|
|
120
137
|
}
|
|
121
138
|
finally {
|
|
122
|
-
await cache.lock.release('my-lock',
|
|
139
|
+
await cache.lock.release('my-lock', acquired.data)
|
|
123
140
|
}
|
|
124
141
|
}
|
|
125
142
|
|
|
@@ -127,12 +144,15 @@ if (acquired.success && acquired.data) {
|
|
|
127
144
|
const locked = await cache.lock.isLocked('my-lock')
|
|
128
145
|
|
|
129
146
|
// 续期锁
|
|
130
|
-
|
|
147
|
+
if (acquired.success && acquired.data)
|
|
148
|
+
await cache.lock.extend('my-lock', 60, acquired.data)
|
|
131
149
|
```
|
|
132
150
|
|
|
133
151
|
- **Memory バックエンド**:Map と TTL による有効期限管理。
|
|
134
152
|
- **Redis バックエンド**:`SET NX EX` でアトミックにロックを取得。Lua スクリプトで owner を検証して安全に解除・延長します。
|
|
135
|
-
- `
|
|
153
|
+
- `acquire` は取得ごとの一意なトークン(競合時は `null`)を返します。解除・延長にはこのトークンが必須です。ノード ID やトークンなしの操作は使用できません。
|
|
154
|
+
|
|
155
|
+
owner はリース延長・解除の所有権であり、リソースの fencing token ではありません。停止した処理は期限切れ後も再開できるため、最終書込には保存先の CAS・世代条件・トランザクション制約が必要です。延長や処理後の失敗返却だけでは旧書込を防げません。
|
|
136
156
|
|
|
137
157
|
### エラーコード
|
|
138
158
|
|
|
@@ -150,6 +170,10 @@ await cache.lock.extend('my-lock', 60, 'node-1')
|
|
|
150
170
|
| `HaiCacheError.UNSUPPORTED_TYPE` | `hai:cache:011` | 未対応のキャッシュ種別 |
|
|
151
171
|
| `HaiCacheError.CONFIG_ERROR` | `hai:cache:012` | 設定エラー |
|
|
152
172
|
|
|
173
|
+
## 本番の共有状態
|
|
174
|
+
|
|
175
|
+
本番は Redis(Cluster を含む)が必須です。memory は開発・明示的テスト用であり、Redis 障害時もローカルへフォールバックしません。 NODE_ENV または HAI_CORE_ENV が production の場合、初期化成功前に検証します。明示的な HAI_E2E=1/HAI_E2E_MOCK=1 テストのみ除外します。 [状態・並行制御・復旧範囲(中国語)](../../.agents/instructions/distributed-state.md)。
|
|
176
|
+
|
|
153
177
|
## 開発とテスト
|
|
154
178
|
|
|
155
179
|
```bash
|
|
@@ -158,6 +182,8 @@ pnpm --filter @h-ai/cache test
|
|
|
158
182
|
|
|
159
183
|
> Redis 関連のテストには Docker が必要です。
|
|
160
184
|
|
|
185
|
+
公開契約と内部構造には型検査できる例を用意しています。[JSDoc 規約(中国語)](../../.agents/instructions/module-conventions.md#jsdoc-与示例验证) を参照してください。
|
|
186
|
+
|
|
161
187
|
## ライセンス
|
|
162
188
|
|
|
163
189
|
Apache-2.0
|
package/README.md
CHANGED
|
@@ -10,6 +10,20 @@
|
|
|
10
10
|
|
|
11
11
|
内存 KV 在到期时间点即视为失效;`incr/decr/incrBy/decrBy` 只接受安全整数数值或整数字符串,非法增量和溢出返回 `OPERATION_FAILED` 并保留原值。Hash 的 `__proto__` / `constructor` 等名称按普通字段保留,不修改返回对象原型。
|
|
12
12
|
|
|
13
|
+
初始化只在健康检查成功后发布连接;失败时释放候选资源。若清理仍失败,初始化错误的 `error.ext.cleanupPending` 为 `true`。`cache.close()` 返回 `Promise<HaiResult<void>>`,等待正在进行的初始化,并合并并发关闭。开始清理后 `isInitialized` 为 `false`,配置与业务操作不再可用;清理失败仍保留资源,后续 `close()` 或 `init()` 先重试释放,成功前不会创建替代连接。调用方必须检查关闭结果。
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
const closed = await cache.close()
|
|
17
|
+
if (!closed.success) {
|
|
18
|
+
// 处理清理故障后有限重试;再次失败应向应用报告。
|
|
19
|
+
const retried = await cache.close()
|
|
20
|
+
if (!retried.success)
|
|
21
|
+
throw new Error(retried.error.message)
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Redis 的 QUIT 失败后会强制 disconnect;只有实际释放成功才报告关闭成功。
|
|
26
|
+
|
|
13
27
|
## 核心功能清单
|
|
14
28
|
|
|
15
29
|
- 开发/测试场景下的内存缓存
|
|
@@ -22,37 +36,27 @@
|
|
|
22
36
|
### 快速开始
|
|
23
37
|
|
|
24
38
|
```ts
|
|
25
|
-
import { cache
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
await cache.kv.set('user:1', { name: '
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
await cache.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
// 分布式锁
|
|
41
|
-
const acquired = await cache.lock.acquire('my-lock', { ttl: 30, owner: 'node-1' })
|
|
42
|
-
if (acquired.success && acquired.data) {
|
|
43
|
-
try {
|
|
44
|
-
// 执行受保护的操作
|
|
45
|
-
}
|
|
46
|
-
finally {
|
|
47
|
-
await cache.lock.release('my-lock', 'node-1')
|
|
48
|
-
}
|
|
39
|
+
import { cache } from '@h-ai/cache'
|
|
40
|
+
import { core } from '@h-ai/core'
|
|
41
|
+
|
|
42
|
+
const initialized = await cache.init({ type: 'redis', host: 'localhost' })
|
|
43
|
+
if (!initialized.success)
|
|
44
|
+
throw new Error(initialized.error.message)
|
|
45
|
+
try {
|
|
46
|
+
const written = await cache.kv.set('user:1', { name: 'Alice' }, { ex: 3600 })
|
|
47
|
+
if (!written.success)
|
|
48
|
+
throw new Error(written.error.message)
|
|
49
|
+
const user = await cache.kv.get<{ name: string }>('user:1')
|
|
50
|
+
if (!user.success)
|
|
51
|
+
throw new Error(user.error.message)
|
|
52
|
+
if (user.data)
|
|
53
|
+
core.logger.info('Cached user', { name: user.data.name })
|
|
49
54
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
finally {
|
|
56
|
+
const closed = await cache.close()
|
|
57
|
+
if (!closed.success)
|
|
58
|
+
core.logger.error('Cache cleanup pending', { code: closed.error.code })
|
|
53
59
|
}
|
|
54
|
-
|
|
55
|
-
await cache.close()
|
|
56
60
|
```
|
|
57
61
|
|
|
58
62
|
`cache.config` 返回的是**脱敏后的配置快照**:如后续接入 Redis `password` / `url` 等凭证字段,会自动做安全处理,不应用作回写原始密钥。
|
|
@@ -97,6 +101,10 @@ await cache.init({
|
|
|
97
101
|
})
|
|
98
102
|
```
|
|
99
103
|
|
|
104
|
+
内置后端由 `cache.init(config)` 选择 memory/redis;Provider 实现是内部契约,不提供自定义后端注册入口。
|
|
105
|
+
|
|
106
|
+
`password` 用于单机、Cluster 与 Sentinel 数据节点;Cluster 发现和重定向后的节点连接也携带密码。配置 `url` 时采用 URL 自身的认证信息,独立 password 字段不覆盖它。
|
|
107
|
+
|
|
100
108
|
### API 概览
|
|
101
109
|
|
|
102
110
|
- `cache.kv`: 字符串/对象键值与 TTL(`set/get/mget/mset/expire/ttl/...`)
|
|
@@ -105,7 +113,27 @@ await cache.init({
|
|
|
105
113
|
- `cache.set_`: 集合结构(`sadd/smembers/sismember/...`)
|
|
106
114
|
- `cache.zset`: 有序集合(`zadd/zrange/zrevrange/...`)
|
|
107
115
|
- `cache.lock`: 分布式锁(`acquire/release/isLocked/extend`)
|
|
108
|
-
|
|
116
|
+
|
|
117
|
+
`kv.set` 返回 `HaiResult<boolean>`:`data=true` 表示实际写入,NX/XX 未命中返回 `success=true,data=false` 且不改变原值或 TTL。签发票据、会话或登记公钥前必须检查这两个字段。`ex/px/exat/pxat` 分别使用秒、毫秒、Unix 秒、Unix 毫秒,须为正安全整数且只能指定一种,与 `keepTtl=true` 互斥;`nx/xx` 也互斥。零、负数、小数、非有限值或溢出返回 `INVALID_SET_OPTIONS`(`cache:016`),不会写成永久键。过去的正绝对时间表示立即过期。省略期限会移除原 TTL;`keepTtl` 保留已有 TTL,新键仍永久保存。memory 和 Redis 都保存 JSON 快照。
|
|
118
|
+
|
|
119
|
+
compareAndSwap 对一个 key 原子比较 JSON 值并替换/删除,返回 boolean applied;expected=undefined 表示不存在,value=undefined 表示删除,null 是实际 JSON 值。px 必须为正整数且与 keepTtl 互斥,省略过期策略会持久保存;比较失败不改 TTL。可用于跨节点版本提交,不能把进程锁当作提交条件。
|
|
120
|
+
|
|
121
|
+
### Cluster 多键与扫描
|
|
122
|
+
|
|
123
|
+
`mget/del/exists` 支持跨 slot,按独立 key 有界执行,mget 保留输入顺序;不提供跨 slot 的原子性或快照。DEL 失败可能已有部分键被删除,重试幂等。`mset`、集合交/并/差和多键阻塞弹出要求同 slot;用共同的 `{hash-tag}` 分组,否则返回 `CROSS_SLOT`,MSET 不进行部分写入。
|
|
124
|
+
|
|
125
|
+
`scan` 首次传字符串 `'0'`,返回不透明字符串游标,可在配置和拓扑一致的不同应用节点继续。Cluster 遍历全部主节点;拓扑、match 或 keyPrefix 变化返回 `SCAN_TOPOLOGY_CHANGED`,调用方从 `'0'` 重新遍历。返回 key 已去除 keyPrefix,前缀中的 glob 字符按字面量匹配;`keys` 也遵循此规则并去重。SCAN 无快照,允许重复项,count 是正整数提示值,不保证返回条数。无效游标/count 返回 `INVALID_SCAN`。
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
let cursor = '0'
|
|
129
|
+
do {
|
|
130
|
+
const page = await cache.kv.scan(cursor, { match: 'users:*', count: 100 })
|
|
131
|
+
if (!page.success)
|
|
132
|
+
throw new Error(page.error.message)
|
|
133
|
+
cursor = page.data[0]
|
|
134
|
+
await consume(page.data[1])
|
|
135
|
+
} while (cursor !== '0')
|
|
136
|
+
```
|
|
109
137
|
|
|
110
138
|
### 分布式锁
|
|
111
139
|
|
|
@@ -113,13 +141,13 @@ await cache.init({
|
|
|
113
141
|
|
|
114
142
|
```ts
|
|
115
143
|
// 获取锁(TTL 30 秒,owner 用于标识持有者)
|
|
116
|
-
const acquired = await cache.lock.acquire('my-lock', { ttl: 30
|
|
144
|
+
const acquired = await cache.lock.acquire('my-lock', { ttl: 30 })
|
|
117
145
|
if (acquired.success && acquired.data) {
|
|
118
146
|
try {
|
|
119
147
|
// 执行受保护操作
|
|
120
148
|
}
|
|
121
149
|
finally {
|
|
122
|
-
await cache.lock.release('my-lock',
|
|
150
|
+
await cache.lock.release('my-lock', acquired.data)
|
|
123
151
|
}
|
|
124
152
|
}
|
|
125
153
|
|
|
@@ -127,12 +155,15 @@ if (acquired.success && acquired.data) {
|
|
|
127
155
|
const locked = await cache.lock.isLocked('my-lock')
|
|
128
156
|
|
|
129
157
|
// 续期锁
|
|
130
|
-
|
|
158
|
+
if (acquired.success && acquired.data)
|
|
159
|
+
await cache.lock.extend('my-lock', 60, acquired.data)
|
|
131
160
|
```
|
|
132
161
|
|
|
133
162
|
- **Memory 后端**:基于 Map + TTL 过期机制
|
|
134
163
|
- **Redis 后端**:使用 `SET NX EX` 原子获锁,Lua 脚本实现 owner 验证的安全释放/续期
|
|
135
|
-
- `
|
|
164
|
+
- `acquire` 返回本次获锁的唯一凭据,锁占用时为 `null`;释放/续租必须传回该凭据,禁止使用节点 ID 或无凭据强制操作。
|
|
165
|
+
|
|
166
|
+
owner 只保护锁的续期与释放,不是资源 fencing token。进程暂停或租约到期后旧任务仍能继续;最终写入必须在资源存储中使用 CAS、代际条件或事务约束。续租成功或最后返回失败都不能代替提交条件。
|
|
136
167
|
|
|
137
168
|
### 错误码
|
|
138
169
|
|
|
@@ -150,6 +181,10 @@ await cache.lock.extend('my-lock', 60, 'node-1')
|
|
|
150
181
|
| `HaiCacheError.UNSUPPORTED_TYPE` | `hai:cache:011` | 不支持的缓存类型 |
|
|
151
182
|
| `HaiCacheError.CONFIG_ERROR` | `hai:cache:012` | 配置错误 |
|
|
152
183
|
|
|
184
|
+
## 生产共享状态
|
|
185
|
+
|
|
186
|
+
生产仅使用 Redis(含 Cluster);memory 只用于开发/明确测试。Redis 故障返回失败,不回退本机缓存。 NODE_ENV 或 HAI_CORE_ENV 为 production 时在初始化发布成功前校验;仅 HAI_E2E=1/HAI_E2E_MOCK=1 明确测试可豁免。 [状态、并发和恢复边界](../../.agents/instructions/distributed-state.md)。
|
|
187
|
+
|
|
153
188
|
## 开发测试
|
|
154
189
|
|
|
155
190
|
```bash
|
|
@@ -158,6 +193,8 @@ pnpm --filter @h-ai/cache test
|
|
|
158
193
|
|
|
159
194
|
> Redis 相关测试需要 Docker 环境。
|
|
160
195
|
|
|
196
|
+
公开契约与内部结构均提供可类型检查的示例;见[模块 JSDoc 约定](../../.agents/instructions/module-conventions.md#jsdoc-与示例验证)。
|
|
197
|
+
|
|
161
198
|
## 许可证
|
|
162
199
|
|
|
163
200
|
Apache-2.0
|