@@ -16,8 +16,12 @@ class MultiLevelCache {
1616 * @param {Object } [options.policy]
1717 * @param {'both'|'local-first-async-remote' } [options.policy.writePolicy='both']
1818 * @param {boolean } [options.policy.backfillLocalOnRemoteHit=true]
19+ * @param {number } [options.policy.backfillLocalTTL=0] - 回填 L1 时使用的兜底 TTL(毫秒);
20+ * 当 remote 支持 getWithTTL 时优先使用 L2 剩余 TTL,否则降级到此值;
21+ * 0 = 不设 TTL(永不过期,向后兼容)。建议生产环境配置合理值(如 60000)
1922 * @param {number } [options.remoteTimeoutMs=50] - 远端单次操作超时
2023 * @param {(msg:object)=>void } [options.publish] - 可选:失效广播发布器
24+ * @since 1.1.9 backfillLocalTTL 支持
2125 */
2226 constructor ( options = { } ) {
2327 const { local, remote, policy = { } , remoteTimeoutMs = 50 , publish } = options ;
@@ -29,6 +33,8 @@ class MultiLevelCache {
2933 this . policy = {
3034 writePolicy : policy . writePolicy || 'both' ,
3135 backfillLocalOnRemoteHit : policy . backfillLocalOnRemoteHit !== false ,
36+ // 🔧 v1.1.9 修复:新增回填 TTL 兜底配置,0 = 不设 TTL(向后兼容)
37+ backfillLocalTTL : typeof policy . backfillLocalTTL === 'number' ? policy . backfillLocalTTL : 0 ,
3238 } ;
3339 this . remoteTimeoutMs = Number ( remoteTimeoutMs ) || 50 ;
3440 this . publish = typeof publish === 'function' ? publish : null ;
@@ -74,13 +80,30 @@ class MultiLevelCache {
7480 const v = await this . local . get ( key ) ;
7581 if ( v !== undefined ) return v ;
7682 try {
77- const r = await this . _withTimeout ( this . remote . get ( key ) ) ;
83+ let r ;
84+ // 🔧 v1.1.9 修复:backfillTTL 优先取 L2 剩余 TTL(方案A),降级到 backfillLocalTTL(方案B)
85+ let backfillTTL = this . policy . backfillLocalTTL ;
86+
87+ if ( this . remote && typeof this . remote . getWithTTL === 'function' ) {
88+ // 方案A:remote 支持 getWithTTL,单次 RTT 同时获取值与剩余 TTL
89+ const meta = await this . _withTimeout ( this . remote . getWithTTL ( key ) ) ;
90+ if ( meta !== undefined ) {
91+ r = meta . value ;
92+ if ( meta . remainingTTL > 0 ) backfillTTL = meta . remainingTTL ;
93+ }
94+ } else {
95+ // 方案B:remote 不支持 getWithTTL,使用普通 get + backfillLocalTTL 兜底
96+ r = await this . _withTimeout ( this . remote . get ( key ) ) ;
97+ }
98+
7899 if ( r !== undefined && this . policy . backfillLocalOnRemoteHit ) {
79- // 无剩余 TTL 信息,采用一个保守的回填策略:原 TTL 由上层 set 决定;这里回填一个短 TTL(例如 50ms)不合适
80- // 因为我们无法得知原始 TTL,这里直接不设置 TTL(等同于本地不持久)。但为了可用性,设置 50% 的常用短 TTL可配置。
81- // 为保持简单,我们不传 TTL(由本地实现解释,等价于无过期)。
82- // 若调用方强依赖 TTL 严格一致性,应在远端适配器中扩展 getWithTTL。
83- try { await this . local . set ( key , r ) ; } catch ( _ ) { }
100+ // 🔧 v1.1.9 补充:backfillTTL=0 时跳过 null 回填,防止无配置用户触发永久驻留 Bug
101+ // - backfillTTL>0:回填所有值(含 null),TTL 保护下能正常过期
102+ // - backfillTTL=0 + null:跳过——无 TTL 保护的 null 永久驻留 = Bug 本身
103+ // - backfillTTL=0 + 非 null:回填(保留原有永久缓存行为,向后兼容)
104+ if ( backfillTTL > 0 || r !== null ) {
105+ try { await this . local . set ( key , r , backfillTTL ) ; } catch ( _ ) { }
106+ }
84107 }
85108 return r ;
86109 } catch ( _ ) {
@@ -128,8 +151,16 @@ class MultiLevelCache {
128151 try {
129152 const remoteRes = await this . _withTimeout ( this . remote . getMany ( misses ) ) ;
130153 if ( remoteRes && typeof remoteRes === 'object' ) {
131- // 回填本地(异步)
132- if ( this . policy . backfillLocalOnRemoteHit ) this . local . setMany ( remoteRes ) . catch ( ( ) => { } ) ;
154+ // 🔧 v1.1.9 修复:传入 backfillLocalTTL,防止无 TTL 永久回填(含 null 值)
155+ // 🔧 v1.1.9 补充:backfillLocalTTL=0 时过滤 null,防止无配置用户触发永久驻留 Bug
156+ if ( this . policy . backfillLocalOnRemoteHit ) {
157+ const backfillData = this . policy . backfillLocalTTL > 0
158+ ? remoteRes
159+ : Object . fromEntries ( Object . entries ( remoteRes ) . filter ( ( [ , v ] ) => v !== null ) ) ;
160+ if ( Object . keys ( backfillData ) . length > 0 ) {
161+ this . local . setMany ( backfillData , this . policy . backfillLocalTTL ) . catch ( ( ) => { } ) ;
162+ }
163+ }
133164 for ( const k of misses ) { if ( remoteRes [ k ] !== undefined ) out [ k ] = remoteRes [ k ] ; }
134165 }
135166 } catch ( _ ) { /* 降级 */ }
0 commit comments