monsqlize 1.3.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/CHANGELOG.md +56 -60
  2. package/LICENSE +201 -21
  3. package/README.md +537 -1828
  4. package/changelogs/README.md +160 -0
  5. package/changelogs/v2.0.0.md +222 -0
  6. package/dist/cjs/index.cjs +10600 -0
  7. package/dist/cjs/mongodb/common/transaction-aware.cjs +10 -0
  8. package/dist/cjs/transaction/CacheLockManager.cjs +100 -0
  9. package/dist/cjs/transaction/Transaction.cjs +158 -0
  10. package/dist/cjs/transaction/TransactionManager.cjs +298 -0
  11. package/dist/esm/index.mjs +10650 -0
  12. package/dist/types/base.d.ts +81 -0
  13. package/dist/types/collection.d.ts +1031 -0
  14. package/dist/types/expression.d.ts +115 -0
  15. package/dist/types/index.d.ts +23 -0
  16. package/dist/types/lock.d.ts +74 -0
  17. package/dist/types/model.d.ts +526 -0
  18. package/dist/types/mongodb.d.ts +49 -0
  19. package/dist/types/monsqlize.d.ts +491 -0
  20. package/dist/types/pool.d.ts +84 -0
  21. package/dist/types/runtime.d.ts +362 -0
  22. package/dist/types/saga.d.ts +143 -0
  23. package/dist/types/slow-query-log.d.ts +126 -0
  24. package/dist/types/sync.d.ts +103 -0
  25. package/dist/types/transaction.d.ts +132 -0
  26. package/package.json +67 -69
  27. package/index.d.ts +0 -206
  28. package/index.mjs +0 -52
  29. package/lib/cache-invalidation.js +0 -279
  30. package/lib/cache.js +0 -530
  31. package/lib/common/cursor.js +0 -59
  32. package/lib/common/docs-urls.js +0 -73
  33. package/lib/common/index-options.js +0 -223
  34. package/lib/common/log.js +0 -61
  35. package/lib/common/namespace.js +0 -22
  36. package/lib/common/normalize.js +0 -34
  37. package/lib/common/page-result.js +0 -43
  38. package/lib/common/runner.js +0 -57
  39. package/lib/common/server-features.js +0 -232
  40. package/lib/common/shape-builders.js +0 -27
  41. package/lib/common/validation.js +0 -113
  42. package/lib/connect.js +0 -99
  43. package/lib/constants.js +0 -55
  44. package/lib/count-queue.js +0 -188
  45. package/lib/distributed-cache-invalidator.js +0 -260
  46. package/lib/errors.js +0 -168
  47. package/lib/expression/cache/ExpressionCache.js +0 -114
  48. package/lib/expression/compiler/ExpressionCompiler.js +0 -1090
  49. package/lib/expression/compiler/ExpressionCompilerExtensions.js +0 -531
  50. package/lib/expression/detector.js +0 -84
  51. package/lib/expression/factory.js +0 -29
  52. package/lib/expression/index.js +0 -19
  53. package/lib/function-cache.js +0 -533
  54. package/lib/index.js +0 -1251
  55. package/lib/infrastructure/ConnectionPoolManager.js +0 -464
  56. package/lib/infrastructure/HealthChecker.js +0 -281
  57. package/lib/infrastructure/PoolConfig.js +0 -199
  58. package/lib/infrastructure/PoolSelector.js +0 -225
  59. package/lib/infrastructure/PoolStats.js +0 -244
  60. package/lib/infrastructure/ssh-tunnel-ssh2.js +0 -212
  61. package/lib/infrastructure/ssh-tunnel.js +0 -41
  62. package/lib/infrastructure/uri-parser.js +0 -36
  63. package/lib/lock/Lock.js +0 -67
  64. package/lib/lock/errors.js +0 -28
  65. package/lib/lock/index.js +0 -13
  66. package/lib/logger.js +0 -225
  67. package/lib/model/examples/test.js +0 -311
  68. package/lib/model/features/defaults.js +0 -161
  69. package/lib/model/features/populate.js +0 -568
  70. package/lib/model/features/relations.js +0 -120
  71. package/lib/model/features/soft-delete.js +0 -349
  72. package/lib/model/features/version.js +0 -157
  73. package/lib/model/features/virtuals.js +0 -219
  74. package/lib/model/index.js +0 -1265
  75. package/lib/mongodb/common/accessor-helpers.js +0 -59
  76. package/lib/mongodb/common/agg-pipeline.js +0 -36
  77. package/lib/mongodb/common/aggregation-validator.js +0 -127
  78. package/lib/mongodb/common/iid.js +0 -28
  79. package/lib/mongodb/common/lexicographic-expr.js +0 -53
  80. package/lib/mongodb/common/shape.js +0 -32
  81. package/lib/mongodb/common/sort.js +0 -39
  82. package/lib/mongodb/common/transaction-aware.js +0 -25
  83. package/lib/mongodb/connect.js +0 -234
  84. package/lib/mongodb/index.js +0 -639
  85. package/lib/mongodb/management/admin-ops.js +0 -200
  86. package/lib/mongodb/management/bookmark-ops.js +0 -167
  87. package/lib/mongodb/management/cache-ops.js +0 -50
  88. package/lib/mongodb/management/collection-ops.js +0 -387
  89. package/lib/mongodb/management/database-ops.js +0 -202
  90. package/lib/mongodb/management/index-ops.js +0 -475
  91. package/lib/mongodb/management/index.js +0 -17
  92. package/lib/mongodb/management/namespace.js +0 -31
  93. package/lib/mongodb/management/validation-ops.js +0 -268
  94. package/lib/mongodb/queries/aggregate.js +0 -172
  95. package/lib/mongodb/queries/chain.js +0 -631
  96. package/lib/mongodb/queries/count.js +0 -99
  97. package/lib/mongodb/queries/distinct.js +0 -78
  98. package/lib/mongodb/queries/find-and-count.js +0 -193
  99. package/lib/mongodb/queries/find-by-ids.js +0 -236
  100. package/lib/mongodb/queries/find-one-by-id.js +0 -171
  101. package/lib/mongodb/queries/find-one.js +0 -71
  102. package/lib/mongodb/queries/find-page.js +0 -618
  103. package/lib/mongodb/queries/find.js +0 -171
  104. package/lib/mongodb/queries/index.js +0 -51
  105. package/lib/mongodb/queries/watch.js +0 -538
  106. package/lib/mongodb/writes/common/batch-retry.js +0 -65
  107. package/lib/mongodb/writes/delete-batch.js +0 -323
  108. package/lib/mongodb/writes/delete-many.js +0 -181
  109. package/lib/mongodb/writes/delete-one.js +0 -173
  110. package/lib/mongodb/writes/find-one-and-delete.js +0 -203
  111. package/lib/mongodb/writes/find-one-and-replace.js +0 -239
  112. package/lib/mongodb/writes/find-one-and-update.js +0 -240
  113. package/lib/mongodb/writes/increment-one.js +0 -259
  114. package/lib/mongodb/writes/index.js +0 -46
  115. package/lib/mongodb/writes/insert-batch.js +0 -508
  116. package/lib/mongodb/writes/insert-many.js +0 -223
  117. package/lib/mongodb/writes/insert-one.js +0 -169
  118. package/lib/mongodb/writes/replace-one.js +0 -226
  119. package/lib/mongodb/writes/result-handler.js +0 -237
  120. package/lib/mongodb/writes/update-batch.js +0 -416
  121. package/lib/mongodb/writes/update-many.js +0 -275
  122. package/lib/mongodb/writes/update-one.js +0 -273
  123. package/lib/mongodb/writes/upsert-one.js +0 -203
  124. package/lib/multi-level-cache.js +0 -244
  125. package/lib/operators.js +0 -330
  126. package/lib/redis-cache-adapter.js +0 -267
  127. package/lib/saga/SagaContext.js +0 -67
  128. package/lib/saga/SagaDefinition.js +0 -32
  129. package/lib/saga/SagaExecutor.js +0 -201
  130. package/lib/saga/SagaOrchestrator.js +0 -186
  131. package/lib/saga/index.js +0 -11
  132. package/lib/slow-query-log/base-storage.js +0 -70
  133. package/lib/slow-query-log/batch-queue.js +0 -97
  134. package/lib/slow-query-log/config-manager.js +0 -196
  135. package/lib/slow-query-log/index.js +0 -238
  136. package/lib/slow-query-log/mongodb-storage.js +0 -324
  137. package/lib/slow-query-log/query-hash.js +0 -39
  138. package/lib/sync/ChangeStreamSyncManager.js +0 -405
  139. package/lib/sync/ResumeTokenStore.js +0 -192
  140. package/lib/sync/SyncConfig.js +0 -127
  141. package/lib/sync/SyncTarget.js +0 -240
  142. package/lib/sync/index.js +0 -20
  143. package/lib/transaction/CacheLockManager.js +0 -162
  144. package/lib/transaction/DistributedCacheLockManager.js +0 -475
  145. package/lib/transaction/Transaction.js +0 -315
  146. package/lib/transaction/TransactionManager.js +0 -267
  147. package/lib/transaction/index.js +0 -11
  148. package/lib/utils/objectid-converter.js +0 -632
  149. package/types/README.md +0 -122
  150. package/types/base.ts +0 -94
  151. package/types/batch.ts +0 -187
  152. package/types/cache.ts +0 -71
  153. package/types/chain.ts +0 -254
  154. package/types/collection.ts +0 -357
  155. package/types/expression.ts +0 -109
  156. package/types/function-cache.d.ts +0 -135
  157. package/types/lock.ts +0 -95
  158. package/types/model/definition.ts +0 -152
  159. package/types/model/index.ts +0 -10
  160. package/types/model/instance.ts +0 -121
  161. package/types/model/relations.ts +0 -121
  162. package/types/model/virtuals.ts +0 -32
  163. package/types/monsqlize.ts +0 -245
  164. package/types/options.ts +0 -192
  165. package/types/pagination.ts +0 -154
  166. package/types/pool.ts +0 -125
  167. package/types/query.ts +0 -71
  168. package/types/saga.ts +0 -125
  169. package/types/stream.ts +0 -64
  170. package/types/sync.ts +0 -79
  171. package/types/transaction.ts +0 -79
  172. package/types/write.ts +0 -77
@@ -1,203 +0,0 @@
1
- /**
2
- * upsertOne 写操作模块
3
- * @description 便利方法:存在则更新,不存在则插入
4
- */
5
-
6
- const { createError, ErrorCodes } = require('../../errors');
7
- const { isInTransaction, getTransactionFromSession } = require('../common/transaction-aware');
8
- const { convertObjectIdStrings } = require('../../utils/objectid-converter');
9
-
10
- /**
11
- * 创建 upsertOne 操作
12
- * @param {Object} context - 上下文对象
13
- * @returns {Function} upsertOne 方法
14
- */
15
- function createUpsertOneOps(context) {
16
- const {
17
- collection,
18
- defaults,
19
- instanceId,
20
- effectiveDbName,
21
- logger,
22
- emit,
23
- mongoSlowLogShaper,
24
- cache,
25
- type
26
- } = context;
27
-
28
- /**
29
- * upsert 单个文档(存在则更新,不存在则插入)
30
- * @param {Object} filter - 查询条件
31
- * @param {Object} update - 更新内容(直接设置字段,自动包装为 $set)
32
- * @param {Object} [options={}] - 操作选项
33
- * @param {number} [options.maxTimeMS] - 操作超时(毫秒)
34
- * @param {string} [options.comment] - 查询注释
35
- * @returns {Promise<Object>} 操作结果
36
- *
37
- * @example
38
- * // 基础用法
39
- * const result = await collection('users').upsertOne(
40
- * { userId: 'user123' },
41
- * { name: 'Alice', email: 'alice@example.com' }
42
- * );
43
- *
44
- * @example
45
- * // 带选项
46
- * const result = await collection('config').upsertOne(
47
- * { key: 'theme' },
48
- * { value: 'dark', updatedAt: new Date() },
49
- * { maxTimeMS: 5000, comment: 'sync-config' }
50
- * );
51
- */
52
- const upsertOne = async function upsertOne(filter, update, options = {}) {
53
- const startTime = Date.now();
54
-
55
- // 1. 参数验证
56
- if (!filter || typeof filter !== 'object' || Array.isArray(filter)) {
57
- throw createError(
58
- ErrorCodes.INVALID_ARGUMENT,
59
- 'filter 必须是非空对象',
60
- [{ field: 'filter', type: 'type', message: 'filter 必须是对象', received: typeof filter }]
61
- );
62
- }
63
-
64
- if (!update || typeof update !== 'object' || Array.isArray(update)) {
65
- throw createError(
66
- ErrorCodes.INVALID_ARGUMENT,
67
- 'update 必须是非空对象',
68
- [{ field: 'update', type: 'type', message: 'update 必须是对象', received: typeof update }]
69
- );
70
- }
71
-
72
- // ✅ v1.3.0: 自动转换 ObjectId 字符串
73
- const convertedFilter = convertObjectIdStrings(filter, 'filter', 0, new WeakSet(), {
74
- logger: context.logger,
75
- excludeFields: context.autoConvertConfig?.excludeFields,
76
- customFieldPatterns: context.autoConvertConfig?.customFieldPatterns,
77
- maxDepth: context.autoConvertConfig?.maxDepth
78
- });
79
-
80
- const convertedUpdate = convertObjectIdStrings(update, 'document', 0, new WeakSet(), {
81
- logger: context.logger,
82
- excludeFields: context.autoConvertConfig?.excludeFields,
83
- customFieldPatterns: context.autoConvertConfig?.customFieldPatterns,
84
- maxDepth: context.autoConvertConfig?.maxDepth
85
- });
86
-
87
- // 2. 检查 update 是否包含更新操作符
88
- const hasOperator = Object.keys(convertedUpdate).some(key => key.startsWith('$'));
89
-
90
- // 如果没有操作符,自动包装为 $set
91
- const updateDoc = hasOperator ? convertedUpdate : { $set: convertedUpdate };
92
-
93
- // 3. 构建选项
94
- const maxTimeMS = options.maxTimeMS !== undefined ? options.maxTimeMS : defaults.maxTimeMS;
95
- const comment = options.comment;
96
-
97
- const driverOpts = { upsert: true, maxTimeMS };
98
- if (comment) driverOpts.comment = comment;
99
-
100
- // 4. 执行 updateOne 操作
101
- let result;
102
- try {
103
- result = await collection.updateOne(convertedFilter, updateDoc, driverOpts);
104
- } catch (error) {
105
- // 统一错误处理
106
- if (error.code === 11000) {
107
- throw createError(
108
- ErrorCodes.DUPLICATE_KEY,
109
- '批量插入失败:违反唯一性约束',
110
- [{ field: '_id', type: 'unique', message: error.message }],
111
- error
112
- );
113
- }
114
- throw error;
115
- }
116
-
117
- // 5. 自动失效缓存
118
- const upsertedCount = result.matchedCount === 0 ? 1 : 0;
119
-
120
- if (cache && (result.modifiedCount > 0 || upsertedCount > 0)) {
121
- try {
122
- // 🆕 v1.1.6: 解析精准失效配置
123
- const instanceAuto = context.cacheAutoInvalidate || false;
124
- const queryAuto = options.autoInvalidate;
125
- const shouldAuto = queryAuto !== undefined ? queryAuto : instanceAuto;
126
-
127
- if (shouldAuto) {
128
- // 精准失效:upsertOne 使用 filter 来匹配缓存
129
- const CacheInvalidationEngine = require('../../cache-invalidation');
130
-
131
- const deleted = await CacheInvalidationEngine.invalidatePrecise(cache, {
132
- instanceId,
133
- type,
134
- db: effectiveDbName,
135
- collection: collection.collectionName,
136
- document: convertedFilter, // 使用 filter 来匹配
137
- operation: 'upsertOne'
138
- });
139
-
140
- if (deleted > 0) {
141
- logger?.debug?.(`[upsertOne] 精准失效缓存: ${collection.collectionName}, 删除 ${deleted} 个缓存键`);
142
- }
143
- } else {
144
- logger?.debug?.(`[upsertOne] 跳过自动失效(autoInvalidate=false)`);
145
- }
146
- } catch (cacheError) {
147
- logger?.warn?.('[upsertOne] 缓存失效失败', { error: cacheError.message });
148
- }
149
- }
150
-
151
- // 6. 慢查询日志
152
- const duration = Date.now() - startTime;
153
- const slowQueryMs = defaults?.slowQueryMs || 1000;
154
-
155
- if (duration >= slowQueryMs) {
156
- try {
157
- const meta = {
158
- operation: 'upsertOne',
159
- durationMs: duration,
160
- iid: instanceId,
161
- type,
162
- db: effectiveDbName,
163
- collection: collection.collectionName,
164
- matchedCount: result.matchedCount,
165
- modifiedCount: result.modifiedCount,
166
- upsertedId: result.upsertedId,
167
- filter: mongoSlowLogShaper?.sanitize ? mongoSlowLogShaper.sanitize(filter) : filter,
168
- update: mongoSlowLogShaper?.sanitize ? mongoSlowLogShaper.sanitize(updateDoc) : updateDoc,
169
- comment
170
- };
171
- logger?.warn?.('🐌 Slow query: upsertOne', meta);
172
- emit?.('slow-query', meta);
173
- } catch (_) {
174
- // 忽略日志错误
175
- }
176
- }
177
-
178
- // 7. 日志记录
179
- logger?.debug?.('[upsertOne] 操作完成', {
180
- ns: `${effectiveDbName}.${collection.collectionName}`,
181
- duration,
182
- matchedCount: result.matchedCount,
183
- modifiedCount: result.modifiedCount,
184
- upsertedId: result.upsertedId === null ? undefined : result.upsertedId,
185
- upsertedCount
186
- });
187
-
188
- // 8. 返回结果
189
- return {
190
- acknowledged: result.acknowledged,
191
- matchedCount: result.matchedCount,
192
- modifiedCount: result.modifiedCount,
193
- upsertedId: result.upsertedId === null ? undefined : result.upsertedId,
194
- upsertedCount
195
- };
196
- };
197
-
198
- return { upsertOne };
199
- }
200
-
201
- module.exports = { createUpsertOneOps };
202
-
203
-
@@ -1,244 +0,0 @@
1
- /**
2
- * MultiLevelCache: 组合本地与远端缓存,实现 CacheLike 接口
3
- * 设计要点:
4
- * - 读:本地优先;本地未命中查远端,命中则异步回填本地;远端失败需优雅降级
5
- * - 写:默认本地+远端双写;可配置本地优先、远端异步
6
- * - 删:本地删除;远端删除尽力而为;delPattern 默认仅本地执行并返回删除数(重型场景交由远端广播/索引)
7
- * - keys/clear:仅作用于本地层(避免误伤远端)
8
- * - TTL 单位毫秒,与上层保持一致
9
- */
10
-
11
- class MultiLevelCache {
12
- /**
13
- * @param {Object} options
14
- * @param {Object} options.local - 本地 CacheLike
15
- * @param {Object} options.remote - 远端 CacheLike(可选)
16
- * @param {Object} [options.policy]
17
- * @param {'both'|'local-first-async-remote'} [options.policy.writePolicy='both']
18
- * @param {boolean} [options.policy.backfillLocalOnRemoteHit=true]
19
- * @param {number} [options.policy.backfillLocalTTL=0] - 回填 L1 时使用的兜底 TTL(毫秒);
20
- * 当 remote 支持 getWithTTL 时优先使用 L2 剩余 TTL,否则降级到此值;
21
- * 0 = 不设 TTL(永不过期,向后兼容)。建议生产环境配置合理值(如 60000)
22
- * @param {number} [options.remoteTimeoutMs=50] - 远端单次操作超时
23
- * @param {(msg:object)=>void} [options.publish] - 可选:失效广播发布器
24
- * @since 1.1.9 backfillLocalTTL 支持
25
- */
26
- constructor(options = {}) {
27
- const { local, remote, policy = {}, remoteTimeoutMs = 50, publish } = options;
28
- if (!local || typeof local.get !== 'function') {
29
- throw new Error('MultiLevelCache requires a valid local cache');
30
- }
31
- this.local = local;
32
- this.remote = remote;
33
- this.policy = {
34
- writePolicy: policy.writePolicy || 'both',
35
- backfillLocalOnRemoteHit: policy.backfillLocalOnRemoteHit !== false,
36
- // 🔧 v1.1.9 修复:新增回填 TTL 兜底配置,0 = 不设 TTL(向后兼容)
37
- backfillLocalTTL: typeof policy.backfillLocalTTL === 'number' ? policy.backfillLocalTTL : 0,
38
- };
39
- this.remoteTimeoutMs = Number(remoteTimeoutMs) || 50;
40
- this.publish = typeof publish === 'function' ? publish : null;
41
- }
42
-
43
- /**
44
- * 动态设置 publish 回调(用于分布式缓存失效)
45
- * @param {Function} publishFn - 发布函数,接收 { type, pattern, ts } 对象
46
- */
47
- setPublish(publishFn) {
48
- if (typeof publishFn === 'function') {
49
- this.publish = publishFn;
50
- }
51
- }
52
-
53
- /**
54
- * 设置锁管理器(透传到本地缓存)
55
- * @param {Object} lockManager
56
- */
57
- setLockManager(lockManager) {
58
- if (this.local && typeof this.local.setLockManager === 'function') {
59
- this.local.setLockManager(lockManager);
60
- }
61
- }
62
-
63
- // 工具:为远端操作加超时与降级
64
- async _withTimeout(promise) {
65
- if (!this.remote) return Promise.reject(new Error('NO_REMOTE'));
66
- let timer;
67
- const t = new Promise((_, reject) => {
68
- timer = setTimeout(() => reject(new Error('REMOTE_TIMEOUT')), this.remoteTimeoutMs);
69
- if (timer && typeof timer.unref === 'function') try { timer.unref(); } catch(_) {}
70
- });
71
- try {
72
- const val = await Promise.race([promise, t]);
73
- return val;
74
- } finally {
75
- clearTimeout(timer);
76
- }
77
- }
78
-
79
- async get(key) {
80
- const v = await this.local.get(key);
81
- if (v !== undefined) return v;
82
- try {
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
-
99
- if (r !== undefined && this.policy.backfillLocalOnRemoteHit) {
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
- }
107
- }
108
- return r;
109
- } catch(_) {
110
- return undefined; // 远端异常降级
111
- }
112
- }
113
-
114
- async set(key, val, ttl = 0) {
115
- await this.local.set(key, val, ttl);
116
- if (!this.remote) return;
117
- if (this.policy.writePolicy === 'both') {
118
- try { await this._withTimeout(this.remote.set(key, val, ttl)); } catch(_) {}
119
- } else {
120
- // local-first-async-remote
121
- this._withTimeout(this.remote.set(key, val, ttl)).catch(() => {});
122
- }
123
- }
124
-
125
- async del(key) {
126
- let a = false, b = false;
127
- try { a = await this.local.del(key); } catch(_) {}
128
- if (this.remote) {
129
- try { b = await this._withTimeout(this.remote.del(key)).then(Boolean).catch(() => false); } catch(_) {}
130
- }
131
- return !!(a || b);
132
- }
133
-
134
- async exists(key) {
135
- const a = await this.local.exists(key);
136
- if (a) return true;
137
- if (!this.remote) return false;
138
- try { return !!(await this._withTimeout(this.remote.exists(key))); } catch(_) { return false; }
139
- }
140
-
141
- async getMany(keys) {
142
- const out = {};
143
- // 先批量从本地取
144
- const localRes = await this.local.getMany(keys);
145
- const misses = [];
146
- for (const k of keys) {
147
- const v = localRes[k];
148
- if (v !== undefined) out[k] = v; else misses.push(k);
149
- }
150
- if (!misses.length || !this.remote) return out;
151
- try {
152
- const remoteRes = await this._withTimeout(this.remote.getMany(misses));
153
- if (remoteRes && typeof remoteRes === 'object') {
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
- }
164
- for (const k of misses) { if (remoteRes[k] !== undefined) out[k] = remoteRes[k]; }
165
- }
166
- } catch(_) { /* 降级 */ }
167
- return out;
168
- }
169
-
170
- async setMany(obj, ttl = 0) {
171
- await this.local.setMany(obj, ttl);
172
- if (!this.remote) return true;
173
- if (this.policy.writePolicy === 'both') {
174
- try { await this._withTimeout(this.remote.setMany(obj, ttl)); } catch(_) {}
175
- } else {
176
- this._withTimeout(this.remote.setMany(obj, ttl)).catch(() => {});
177
- }
178
- return true;
179
- }
180
-
181
- async delMany(keys) {
182
- let n = 0;
183
- try { n += await this.local.delMany(keys); } catch(_) {}
184
- if (this.remote) {
185
- try { await this._withTimeout(this.remote.delMany(keys)); } catch(_) {}
186
- }
187
- return n;
188
- }
189
-
190
- async delPattern(pattern) {
191
- let deleted = 0;
192
-
193
- // 删除本地缓存
194
- try {
195
- deleted = await this.local.delPattern(pattern);
196
- } catch(err) {
197
- // 忽略本地删除错误
198
- }
199
-
200
- // 删除远端缓存(如果存在)
201
- if (this.remote && typeof this.remote.delPattern === 'function') {
202
- try {
203
- await this._withTimeout(this.remote.delPattern(pattern));
204
- } catch(err) {
205
- // 远端删除失败,降级处理
206
- }
207
- }
208
-
209
- // 向集群广播(可选)
210
- try {
211
- if (this.publish) {
212
- this.publish({ type: 'invalidate', pattern, ts: Date.now() });
213
- }
214
- } catch(err) {
215
- // 忽略广播错误
216
- }
217
-
218
- return deleted;
219
- }
220
-
221
- clear() {
222
- // 仅清理本地
223
- try { this.local.clear(); } catch(_) {}
224
- }
225
-
226
- keys(pattern = '*') {
227
- try { return this.local.keys(pattern); } catch(_) { return []; }
228
- }
229
-
230
- getStats() {
231
- const local = this.local.getStats ? this.local.getStats() : null;
232
- const remote = this.remote && this.remote.getStats ? this.remote.getStats() : null;
233
- const hits = (local?.hits || 0) + (remote?.hits || 0);
234
- const misses = (local?.misses || 0) + (remote?.misses || 0);
235
- return {
236
- local,
237
- remote,
238
- hitRateApprox: hits / (hits + misses) || 0,
239
- };
240
- }
241
- }
242
-
243
- module.exports = MultiLevelCache;
244
-