@h-ai/cache 0.1.0-alpha.66 → 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 CHANGED
@@ -4,56 +4,64 @@
4
4
 
5
5
  [Framework home](../../README.en.md) · Related modules: [@h-ai/core](../core/README.en.md)
6
6
 
7
+ ## Introduction
8
+
7
9
  A unified cache module. The `cache` object provides KV / Hash / List / Set / ZSet / distributed lock operations with Memory and Redis backends.
8
10
 
9
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.
10
12
 
11
- ## Use cases
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
+
27
+ ## Core features
12
28
 
13
29
  - In-memory caching for development and tests
14
30
  - Redis caching in production (standalone, URL, Cluster, Sentinel)
15
31
  - Set caches for IAM sessions and permissions
16
32
  - Distributed locks for mutual exclusion across nodes
17
33
 
18
- ## Quick start
19
-
20
- ```ts
21
- import { cache, HaiCacheError } from '@h-ai/cache'
22
-
23
- const initResult = await cache.init({ type: 'memory' })
24
- if (!initResult.success) {
25
- throw new Error(initResult.error.message)
26
- }
27
-
28
- await cache.kv.set('user:1', { name: '张三' }, { ex: 3600 })
29
- const user = await cache.kv.get<{ name: string }>('user:1')
34
+ ## Getting started
30
35
 
31
- await cache.hash.hset('profile:1', { age: 18, city: 'Hangzhou' })
32
- await cache.list.lpush('queue:jobs', 'job-1', 'job-2')
33
- await cache.set_.sadd('role:admin:perms', 'user.read', 'user.write')
34
- await cache.zset.zadd('rank', { member: 'u1', score: 100 })
36
+ ### Quick start
35
37
 
36
- // 分布式锁
37
- const acquired = await cache.lock.acquire('my-lock', { ttl: 30, owner: 'node-1' })
38
- if (acquired.success && acquired.data) {
39
- try {
40
- // 执行受保护的操作
41
- }
42
- finally {
43
- await cache.lock.release('my-lock', 'node-1')
44
- }
38
+ ```ts
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 })
45
54
  }
46
-
47
- if (!user.success && user.error.code === HaiCacheError.NOT_INITIALIZED.code) {
48
- // 请先调用 cache.init()
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
- await cache.close()
52
60
  ```
53
61
 
54
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.
55
63
 
56
- ## Initialization options
64
+ ### Initialization options
57
65
 
58
66
  ```ts
59
67
  // Memory(开发/测试)
@@ -93,7 +101,11 @@ await cache.init({
93
101
  })
94
102
  ```
95
103
 
96
- ## API overview
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
+
108
+ ### API overview
97
109
 
98
110
  - `cache.kv`: string/object key-value storage and TTL (`set/get/mget/mset/expire/ttl/...`).
99
111
  - `cache.hash`: hash structures (`hset/hget/hgetall/...`).
@@ -101,21 +113,30 @@ await cache.init({
101
113
  - `cache.set_`: sets (`sadd/smembers/sismember/...`).
102
114
  - `cache.zset`: sorted sets (`zadd/zrange/zrevrange/...`).
103
115
  - `cache.lock`: distributed locks (`acquire/release/isLocked/extend`).
104
- - `CacheProvider`: a public provider contract for advanced integrations and custom backends.
105
116
 
106
- ## Distributed locks
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`.
126
+
127
+ ### Distributed locks
107
128
 
108
129
  Distributed mutual exclusion based on SET NX EX, suitable for deployments across multiple nodes.
109
130
 
110
131
  ```ts
111
132
  // 获取锁(TTL 30 秒,owner 用于标识持有者)
112
- const acquired = await cache.lock.acquire('my-lock', { ttl: 30, owner: 'node-1' })
133
+ const acquired = await cache.lock.acquire('my-lock', { ttl: 30 })
113
134
  if (acquired.success && acquired.data) {
114
135
  try {
115
136
  // 执行受保护操作
116
137
  }
117
138
  finally {
118
- await cache.lock.release('my-lock', 'node-1')
139
+ await cache.lock.release('my-lock', acquired.data)
119
140
  }
120
141
  }
121
142
 
@@ -123,14 +144,17 @@ if (acquired.success && acquired.data) {
123
144
  const locked = await cache.lock.isLocked('my-lock')
124
145
 
125
146
  // 续期锁
126
- await cache.lock.extend('my-lock', 60, 'node-1')
147
+ if (acquired.success && acquired.data)
148
+ await cache.lock.extend('my-lock', 60, acquired.data)
127
149
  ```
128
150
 
129
151
  - **Memory backend**: Map-based storage with TTL expiration.
130
152
  - **Redis backend**: atomic locking with `SET NX EX`; Lua scripts verify the owner for safe release and renewal.
131
- - `owner` prevents accidental release of another owner's lock; a stable node identifier is recommended for multi-node deployments.
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.
132
156
 
133
- ## Error codes
157
+ ### Error codes
134
158
 
135
159
  All operations return `HaiResult<T>`. Common error codes:
136
160
 
@@ -146,7 +170,11 @@ All operations return `HaiResult<T>`. Common error codes:
146
170
  | `HaiCacheError.UNSUPPORTED_TYPE` | `hai:cache:011` | Unsupported cache type |
147
171
  | `HaiCacheError.CONFIG_ERROR` | `hai:cache:012` | Invalid configuration |
148
172
 
149
- ## Tests
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
+
177
+ ## Development and testing
150
178
 
151
179
  ```bash
152
180
  pnpm --filter @h-ai/cache test
@@ -154,6 +182,8 @@ pnpm --filter @h-ai/cache test
154
182
 
155
183
  > Redis tests require Docker.
156
184
 
185
+ Public contracts and internal structures include typechecked examples; see the [JSDoc conventions (Chinese)](../../.agents/instructions/module-conventions.md#jsdoc-与示例验证).
186
+
157
187
  ## License
158
188
 
159
189
  Apache-2.0
package/README.ja.md CHANGED
@@ -4,56 +4,64 @@
4
4
 
5
5
  [フレームワークのホーム](../../README.ja.md) · 関連モジュール:[@h-ai/core](../core/README.ja.md)
6
6
 
7
+ ## 概要
8
+
7
9
  統一されたキャッシュモジュールです。`cache` オブジェクトで KV / Hash / List / Set / ZSet / 分散ロックを操作し、メモリーと Redis のバックエンドに対応します。
8
10
 
9
11
  メモリー KV は有効期限に達した時点で無効になります。`incr/decr/incrBy/decrBy` は安全な整数値または整数文字列のみを受け付け、不正な増分やオーバーフローでは元の値を保持して `OPERATION_FAILED` を返します。Hash の `__proto__` / `constructor` などは通常のフィールドとして保持され、戻り値のオブジェクトのプロトタイプを変更しません。
10
12
 
11
- ## 用途
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
+
27
+ ## 主な機能
12
28
 
13
29
  - 開発・テスト用のメモリーキャッシュ
14
30
  - 本番環境の Redis キャッシュ(単一ノード、URL、Cluster、Sentinel)
15
31
  - IAM セッション・権限用の集合キャッシュ
16
32
  - 複数ノード間の排他制御に使う分散ロック
17
33
 
18
- ## クイックスタート
19
-
20
- ```ts
21
- import { cache, HaiCacheError } from '@h-ai/cache'
22
-
23
- const initResult = await cache.init({ type: 'memory' })
24
- if (!initResult.success) {
25
- throw new Error(initResult.error.message)
26
- }
27
-
28
- await cache.kv.set('user:1', { name: '张三' }, { ex: 3600 })
29
- const user = await cache.kv.get<{ name: string }>('user:1')
34
+ ## 使い始める
30
35
 
31
- await cache.hash.hset('profile:1', { age: 18, city: 'Hangzhou' })
32
- await cache.list.lpush('queue:jobs', 'job-1', 'job-2')
33
- await cache.set_.sadd('role:admin:perms', 'user.read', 'user.write')
34
- await cache.zset.zadd('rank', { member: 'u1', score: 100 })
36
+ ### クイックスタート
35
37
 
36
- // 分布式锁
37
- const acquired = await cache.lock.acquire('my-lock', { ttl: 30, owner: 'node-1' })
38
- if (acquired.success && acquired.data) {
39
- try {
40
- // 执行受保护的操作
41
- }
42
- finally {
43
- await cache.lock.release('my-lock', 'node-1')
44
- }
38
+ ```ts
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 })
45
54
  }
46
-
47
- if (!user.success && user.error.code === HaiCacheError.NOT_INITIALIZED.code) {
48
- // 请先调用 cache.init()
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
- await cache.close()
52
60
  ```
53
61
 
54
62
  `cache.config` は**機密情報をマスクした設定スナップショット**を返します。Redis の `password` / `url` などの認証情報は自動処理されるため、元の秘密情報を書き戻す用途には使わないでください。
55
63
 
56
- ## 初期化設定
64
+ ### 初期化設定
57
65
 
58
66
  ```ts
59
67
  // Memory(开发/测试)
@@ -93,7 +101,11 @@ await cache.init({
93
101
  })
94
102
  ```
95
103
 
96
- ## API 一覧
104
+ `cache.init(config)` は組み込み memory/redis を選択します。Provider は内部契約であり、独自バックエンドの登録 API はありません。
105
+
106
+ `password` は単独・Cluster・Sentinel のデータノードに使用され、発見・リダイレクト後の接続にも適用します。`url` 指定時は URL 内の認証情報を使用し、独立した password フィールドでは上書きしません。
107
+
108
+ ### API 一覧
97
109
 
98
110
  - `cache.kv`:文字列・オブジェクトのキーと値、および TTL(`set/get/mget/mset/expire/ttl/...`)。
99
111
  - `cache.hash`:Hash 構造(`hset/hget/hgetall/...`)。
@@ -101,21 +113,30 @@ await cache.init({
101
113
  - `cache.set_`:集合(`sadd/smembers/sismember/...`)。
102
114
  - `cache.zset`:ソート済み集合(`zadd/zrange/zrevrange/...`)。
103
115
  - `cache.lock`:分散ロック(`acquire/release/isLocked/extend`)。
104
- - `CacheProvider`:高度な統合や独自バックエンド用の公開 Provider 契約。
105
116
 
106
- ## 分散ロック
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` を返します。
126
+
127
+ ### 分散ロック
107
128
 
108
129
  SET NX EX を利用する分散排他ロックで、複数ノードのデプロイに適しています。
109
130
 
110
131
  ```ts
111
132
  // 获取锁(TTL 30 秒,owner 用于标识持有者)
112
- const acquired = await cache.lock.acquire('my-lock', { ttl: 30, owner: 'node-1' })
133
+ const acquired = await cache.lock.acquire('my-lock', { ttl: 30 })
113
134
  if (acquired.success && acquired.data) {
114
135
  try {
115
136
  // 执行受保护操作
116
137
  }
117
138
  finally {
118
- await cache.lock.release('my-lock', 'node-1')
139
+ await cache.lock.release('my-lock', acquired.data)
119
140
  }
120
141
  }
121
142
 
@@ -123,14 +144,17 @@ if (acquired.success && acquired.data) {
123
144
  const locked = await cache.lock.isLocked('my-lock')
124
145
 
125
146
  // 续期锁
126
- await cache.lock.extend('my-lock', 60, 'node-1')
147
+ if (acquired.success && acquired.data)
148
+ await cache.lock.extend('my-lock', 60, acquired.data)
127
149
  ```
128
150
 
129
151
  - **Memory バックエンド**:Map と TTL による有効期限管理。
130
152
  - **Redis バックエンド**:`SET NX EX` でアトミックにロックを取得。Lua スクリプトで owner を検証して安全に解除・延長します。
131
- - `owner` は他の所有者のロックの誤解除を防ぎます。複数ノードのデプロイでは安定したノード識別子の指定を推奨します。
153
+ - `acquire` は取得ごとの一意なトークン(競合時は `null`)を返します。解除・延長にはこのトークンが必須です。ノード ID やトークンなしの操作は使用できません。
154
+
155
+ owner はリース延長・解除の所有権であり、リソースの fencing token ではありません。停止した処理は期限切れ後も再開できるため、最終書込には保存先の CAS・世代条件・トランザクション制約が必要です。延長や処理後の失敗返却だけでは旧書込を防げません。
132
156
 
133
- ## エラーコード
157
+ ### エラーコード
134
158
 
135
159
  すべての操作は `HaiResult<T>` を返します。主なエラーコード:
136
160
 
@@ -146,7 +170,11 @@ await cache.lock.extend('my-lock', 60, 'node-1')
146
170
  | `HaiCacheError.UNSUPPORTED_TYPE` | `hai:cache:011` | 未対応のキャッシュ種別 |
147
171
  | `HaiCacheError.CONFIG_ERROR` | `hai:cache:012` | 設定エラー |
148
172
 
149
- ## テスト
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
+
177
+ ## 開発とテスト
150
178
 
151
179
  ```bash
152
180
  pnpm --filter @h-ai/cache test
@@ -154,6 +182,8 @@ pnpm --filter @h-ai/cache test
154
182
 
155
183
  > Redis 関連のテストには Docker が必要です。
156
184
 
157
- ## License
185
+ 公開契約と内部構造には型検査できる例を用意しています。[JSDoc 規約(中国語)](../../.agents/instructions/module-conventions.md#jsdoc-与示例验证) を参照してください。
186
+
187
+ ## ライセンス
158
188
 
159
189
  Apache-2.0
package/README.md CHANGED
@@ -4,56 +4,64 @@
4
4
 
5
5
  [框架首页](../../README.md) · 相关模块:[@h-ai/core](../core/README.md)
6
6
 
7
+ ## 简介
8
+
7
9
  统一缓存模块,通过 `cache` 对象提供 KV / Hash / List / Set / ZSet / 分布式锁 操作,支持内存与 Redis 两种后端。
8
10
 
9
11
  内存 KV 在到期时间点即视为失效;`incr/decr/incrBy/decrBy` 只接受安全整数数值或整数字符串,非法增量和溢出返回 `OPERATION_FAILED` 并保留原值。Hash 的 `__proto__` / `constructor` 等名称按普通字段保留,不修改返回对象原型。
10
12
 
11
- ## 适用场景
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
+
27
+ ## 核心功能清单
12
28
 
13
29
  - 开发/测试场景下的内存缓存
14
30
  - 生产环境 Redis 缓存(单机、URL、集群、哨兵)
15
31
  - IAM 会话与权限相关的集合缓存
16
32
  - 分布式锁(多节点部署互斥控制)
17
33
 
18
- ## 快速开始
34
+ ## 使用入门
19
35
 
20
- ```ts
21
- import { cache, HaiCacheError } from '@h-ai/cache'
36
+ ### 快速开始
22
37
 
23
- const initResult = await cache.init({ type: 'memory' })
24
- if (!initResult.success) {
25
- throw new Error(initResult.error.message)
26
- }
27
-
28
- await cache.kv.set('user:1', { name: '张三' }, { ex: 3600 })
29
- const user = await cache.kv.get<{ name: string }>('user:1')
30
-
31
- await cache.hash.hset('profile:1', { age: 18, city: 'Hangzhou' })
32
- await cache.list.lpush('queue:jobs', 'job-1', 'job-2')
33
- await cache.set_.sadd('role:admin:perms', 'user.read', 'user.write')
34
- await cache.zset.zadd('rank', { member: 'u1', score: 100 })
35
-
36
- // 分布式锁
37
- const acquired = await cache.lock.acquire('my-lock', { ttl: 30, owner: 'node-1' })
38
- if (acquired.success && acquired.data) {
39
- try {
40
- // 执行受保护的操作
41
- }
42
- finally {
43
- await cache.lock.release('my-lock', 'node-1')
44
- }
38
+ ```ts
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 })
45
54
  }
46
-
47
- if (!user.success && user.error.code === HaiCacheError.NOT_INITIALIZED.code) {
48
- // 请先调用 cache.init()
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
- await cache.close()
52
60
  ```
53
61
 
54
62
  `cache.config` 返回的是**脱敏后的配置快照**:如后续接入 Redis `password` / `url` 等凭证字段,会自动做安全处理,不应用作回写原始密钥。
55
63
 
56
- ## 初始化配置
64
+ ### 初始化配置
57
65
 
58
66
  ```ts
59
67
  // Memory(开发/测试)
@@ -93,7 +101,11 @@ await cache.init({
93
101
  })
94
102
  ```
95
103
 
96
- ## API 概览
104
+ 内置后端由 `cache.init(config)` 选择 memory/redis;Provider 实现是内部契约,不提供自定义后端注册入口。
105
+
106
+ `password` 用于单机、Cluster 与 Sentinel 数据节点;Cluster 发现和重定向后的节点连接也携带密码。配置 `url` 时采用 URL 自身的认证信息,独立 password 字段不覆盖它。
107
+
108
+ ### API 概览
97
109
 
98
110
  - `cache.kv`: 字符串/对象键值与 TTL(`set/get/mget/mset/expire/ttl/...`)
99
111
  - `cache.hash`: 哈希结构(`hset/hget/hgetall/...`)
@@ -101,21 +113,41 @@ await cache.init({
101
113
  - `cache.set_`: 集合结构(`sadd/smembers/sismember/...`)
102
114
  - `cache.zset`: 有序集合(`zadd/zrange/zrevrange/...`)
103
115
  - `cache.lock`: 分布式锁(`acquire/release/isLocked/extend`)
104
- - `CacheProvider`: 公开的 Provider 契约,供高级集成或自定义后端实现复用
105
116
 
106
- ## 分布式锁
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
+ ```
137
+
138
+ ### 分布式锁
107
139
 
108
140
  基于 SET NX EX 模式实现的分布式互斥锁,适用于多节点部署场景。
109
141
 
110
142
  ```ts
111
143
  // 获取锁(TTL 30 秒,owner 用于标识持有者)
112
- const acquired = await cache.lock.acquire('my-lock', { ttl: 30, owner: 'node-1' })
144
+ const acquired = await cache.lock.acquire('my-lock', { ttl: 30 })
113
145
  if (acquired.success && acquired.data) {
114
146
  try {
115
147
  // 执行受保护操作
116
148
  }
117
149
  finally {
118
- await cache.lock.release('my-lock', 'node-1')
150
+ await cache.lock.release('my-lock', acquired.data)
119
151
  }
120
152
  }
121
153
 
@@ -123,14 +155,17 @@ if (acquired.success && acquired.data) {
123
155
  const locked = await cache.lock.isLocked('my-lock')
124
156
 
125
157
  // 续期锁
126
- await cache.lock.extend('my-lock', 60, 'node-1')
158
+ if (acquired.success && acquired.data)
159
+ await cache.lock.extend('my-lock', 60, acquired.data)
127
160
  ```
128
161
 
129
162
  - **Memory 后端**:基于 Map + TTL 过期机制
130
163
  - **Redis 后端**:使用 `SET NX EX` 原子获锁,Lua 脚本实现 owner 验证的安全释放/续期
131
- - `owner` 参数用于防止误释放他人锁,多节点部署时建议设置稳定的节点标识
164
+ - `acquire` 返回本次获锁的唯一凭据,锁占用时为 `null`;释放/续租必须传回该凭据,禁止使用节点 ID 或无凭据强制操作。
132
165
 
133
- ## 错误码
166
+ owner 只保护锁的续期与释放,不是资源 fencing token。进程暂停或租约到期后旧任务仍能继续;最终写入必须在资源存储中使用 CAS、代际条件或事务约束。续租成功或最后返回失败都不能代替提交条件。
167
+
168
+ ### 错误码
134
169
 
135
170
  所有操作返回 `HaiResult<T>`,常用错误码如下:
136
171
 
@@ -146,7 +181,11 @@ await cache.lock.extend('my-lock', 60, 'node-1')
146
181
  | `HaiCacheError.UNSUPPORTED_TYPE` | `hai:cache:011` | 不支持的缓存类型 |
147
182
  | `HaiCacheError.CONFIG_ERROR` | `hai:cache:012` | 配置错误 |
148
183
 
149
- ## 测试
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
+
188
+ ## 开发测试
150
189
 
151
190
  ```bash
152
191
  pnpm --filter @h-ai/cache test
@@ -154,6 +193,8 @@ pnpm --filter @h-ai/cache test
154
193
 
155
194
  > Redis 相关测试需要 Docker 环境。
156
195
 
157
- ## License
196
+ 公开契约与内部结构均提供可类型检查的示例;见[模块 JSDoc 约定](../../.agents/instructions/module-conventions.md#jsdoc-与示例验证)。
197
+
198
+ ## 许可证
158
199
 
159
200
  Apache-2.0