@pisell/pisellos 2.3.166 → 2.3.168

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.
@@ -9,6 +9,7 @@ var _utils = require("../../../modules/Holder/utils");
9
9
  var _Quotation = require("../../../modules/Quotation");
10
10
  var _types = require("./types");
11
11
  var _product = require("../../utils/product");
12
+ var _replication = require("./replication");
12
13
  /**
13
14
  * IndexDB 存储名称
14
15
  */
@@ -69,6 +70,119 @@ class ProductsModule extends _BaseModule.BaseModule {
69
70
 
70
71
  /** 最近一次智能定价评估提取的 schedule 变价时间点(HH:mm),供 Server 订阅定时刷新使用 */
71
72
  lastScheduleTimePoints = [];
73
+ committedProductState;
74
+ productReplicationSourceGeneration = 0;
75
+ productPersistenceFailed = false;
76
+ runtimeSyncMode = 'normal';
77
+ runtimeSyncGeneration = 0;
78
+ requestReplicaRefresh;
79
+ replicaApplyQueue = Promise.resolve();
80
+ productSyncQueue = Promise.resolve();
81
+ replicaPriceDirtyIds = new Map();
82
+
83
+ /** Installed after initial preload; initial cache/network loading stays unchanged. */
84
+ async setRuntimeSyncMode(mode, requestRefresh) {
85
+ this.runtimeSyncGeneration += 1;
86
+ this.runtimeSyncMode = mode;
87
+ this.requestReplicaRefresh = requestRefresh;
88
+ if (mode === 'memoryReplica') {
89
+ if (this.syncTimer) clearTimeout(this.syncTimer);
90
+ this.pendingSyncMessages = [];
91
+ await this.productSyncQueue;
92
+ await this.productIndexDBSaveQueue;
93
+ }
94
+ }
95
+ isMemoryReplica() {
96
+ return this.runtimeSyncMode === 'memoryReplica';
97
+ }
98
+
99
+ /** Read persisted data once on the main screen to establish a committed baseline. */
100
+ async enableProductReplicationSource() {
101
+ const generation = this.productReplicationSourceGeneration;
102
+ await this.runInProductIndexDBSaveQueue(async () => {
103
+ if (generation !== this.productReplicationSourceGeneration) return;
104
+ if (this.committedProductState) return;
105
+ if (!this.dbManager || this.productPersistenceFailed) {
106
+ throw new Error('Product replication requires a successful persistence baseline');
107
+ }
108
+ const products = await this.dbManager.getAll(INDEXDB_STORE_NAME);
109
+ if (generation !== this.productReplicationSourceGeneration) return;
110
+ if (!Array.isArray(products)) throw new Error('Invalid persisted product snapshot');
111
+ this.committedProductState = new _replication.CommittedProductState();
112
+ this.committedProductState.commit({
113
+ kind: 'snapshot',
114
+ upserts: products,
115
+ deletedIds: []
116
+ });
117
+ });
118
+ }
119
+
120
+ /** Release the extra catalog without changing the normal product store or persistence. */
121
+ disableProductReplicationSource() {
122
+ this.productReplicationSourceGeneration += 1;
123
+ this.committedProductState = undefined;
124
+ }
125
+ getCommittedProductSnapshot() {
126
+ return this.committedProductState?.snapshot() ?? null;
127
+ }
128
+ async recordProductCommit(change) {
129
+ this.productPersistenceFailed = false;
130
+ if (!this.committedProductState) return;
131
+ const committed = this.committedProductState.commit(change);
132
+ try {
133
+ await this.core.effects.emit(_types.ProductsHooks.onProductsCommitted, committed);
134
+ } catch (error) {
135
+ // Transport failure is not a persistence failure; peers recover using the snapshot.
136
+ this.logWarning('商品提交通知失败', {
137
+ revision: committed.revision,
138
+ error: String(error)
139
+ });
140
+ }
141
+ }
142
+ async applyReplicatedProducts(change) {
143
+ if (!this.isMemoryReplica()) throw new Error('Product module is not a memory replica');
144
+ const generation = this.runtimeSyncGeneration;
145
+ const apply = async () => {
146
+ if (!this.isMemoryReplica() || generation !== this.runtimeSyncGeneration) return;
147
+ const changedIds = [...new Set([...change.deletedIds, ...change.upserts.map(p => p.id)])];
148
+ if (change.kind === 'snapshot') {
149
+ this.store.list = change.upserts;
150
+ this.syncProductsMap();
151
+ this.clearPriceCache();
152
+ } else {
153
+ const removed = new Set(change.deletedIds);
154
+ const updates = new Map(change.upserts.map(p => [p.id, p]));
155
+ this.store.list = this.store.list.filter(p => !removed.has(p.id)).map(p => {
156
+ const next = updates.get(p.id);
157
+ if (!next) return p;
158
+ updates.delete(p.id);
159
+ return next;
160
+ });
161
+ this.store.list.push(...updates.values());
162
+ for (const id of removed) this.store.map.delete(id);
163
+ for (const product of change.upserts) this.store.map.set(product.id, product);
164
+ for (const [key, cached] of this.productsPriceCache) {
165
+ this.productsPriceCache.set(key, cached.filter(p => !removed.has(p.id)));
166
+ const dirty = this.replicaPriceDirtyIds.get(key) ?? new Set();
167
+ change.upserts.forEach(p => dirty.add(p.id));
168
+ removed.forEach(id => dirty.delete(id));
169
+ this.replicaPriceDirtyIds.set(key, dirty);
170
+ }
171
+ }
172
+ await this.core.effects.emit(_types.ProductsHooks.onProductsChanged, this.store.list);
173
+ if (generation !== this.runtimeSyncGeneration) return;
174
+ await this.core.effects.emit(_types.ProductsHooks.onProductsSyncCompleted, {
175
+ changedIds
176
+ });
177
+ };
178
+ const task = this.replicaApplyQueue.then(apply, apply);
179
+ this.replicaApplyQueue = task.catch(() => undefined);
180
+ return task;
181
+ }
182
+ async refreshFromReplica(trigger) {
183
+ await this.requestReplicaRefresh?.(trigger);
184
+ return this.store.list;
185
+ }
72
186
  constructor(name, version) {
73
187
  super(name, version);
74
188
  }
@@ -409,7 +523,8 @@ class ProductsModule extends _BaseModule.BaseModule {
409
523
  const scopedProductIds = this.normalizeProductIds(options?.productIds);
410
524
  const cacheKey = this.getProductsPriceCacheKey(schedule_date, extraContext, scopedProductIds);
411
525
  const scopedProductIdSet = scopedProductIds ? new Set(scopedProductIds) : undefined;
412
- const changedIds = scopedProductIdSet ? options?.changedIds?.filter(id => scopedProductIdSet.has(id)) : options?.changedIds;
526
+ const pendingIds = [...new Set([...(options?.changedIds ?? []), ...(this.replicaPriceDirtyIds.get(cacheKey) ?? [])])];
527
+ const changedIds = scopedProductIdSet ? pendingIds.filter(id => scopedProductIdSet.has(id)) : pendingIds;
413
528
  if (this.productsPriceCache.has(cacheKey)) {
414
529
  const cachedProducts = this.productsPriceCache.get(cacheKey);
415
530
 
@@ -424,7 +539,7 @@ class ProductsModule extends _BaseModule.BaseModule {
424
539
  const updatedProducts = await this.prepareProductsWithPrice(schedule_date, extraContext, {
425
540
  productIds: changedIds
426
541
  });
427
- if (updatedProducts.length > 0) {
542
+ if (updatedProducts.length > 0 || this.isMemoryReplica()) {
428
543
  // merge:用更新结果替换缓存中对应的商品,新增的追加
429
544
  const updatedMap = new Map(updatedProducts.map(p => [p.id, p]));
430
545
  const mergedCache = [];
@@ -432,7 +547,7 @@ class ProductsModule extends _BaseModule.BaseModule {
432
547
  if (updatedMap.has(p.id)) {
433
548
  mergedCache.push(updatedMap.get(p.id));
434
549
  updatedMap.delete(p.id);
435
- } else {
550
+ } else if (!this.isMemoryReplica() || !changedIds.includes(p.id)) {
436
551
  mergedCache.push(p);
437
552
  }
438
553
  }
@@ -441,6 +556,7 @@ class ProductsModule extends _BaseModule.BaseModule {
441
556
  mergedCache.push(p);
442
557
  }
443
558
  this.productsPriceCache.set(cacheKey, mergedCache);
559
+ this.replicaPriceDirtyIds.delete(cacheKey);
444
560
  this.logInfo('增量更新完成', {
445
561
  cacheKey,
446
562
  changedCount: updatedProducts.length,
@@ -486,6 +602,7 @@ class ProductsModule extends _BaseModule.BaseModule {
486
602
  productIds: scopedProductIds
487
603
  } : undefined);
488
604
  this.productsPriceCache.set(cacheKey, result);
605
+ this.replicaPriceDirtyIds.delete(cacheKey);
489
606
  this.logInfo('商品价格已缓存', {
490
607
  cacheKey,
491
608
  productCount: result.length
@@ -824,6 +941,7 @@ class ProductsModule extends _BaseModule.BaseModule {
824
941
  const keys = Array.from(this.productsPriceCache.keys());
825
942
  const oldestKey = keys[0];
826
943
  this.productsPriceCache.delete(oldestKey);
944
+ this.replicaPriceDirtyIds.delete(oldestKey);
827
945
  }
828
946
  }
829
947
 
@@ -833,6 +951,7 @@ class ProductsModule extends _BaseModule.BaseModule {
833
951
  */
834
952
  clearPriceCache() {
835
953
  this.productsPriceCache.clear();
954
+ this.replicaPriceDirtyIds.clear();
836
955
  this.quotationScheduleCache.clear();
837
956
  this.quotationScheduleCacheLoaded = false;
838
957
  }
@@ -841,6 +960,7 @@ class ProductsModule extends _BaseModule.BaseModule {
841
960
  * 通过 ProductDataSource SSE 加载完整商品列表
842
961
  */
843
962
  async loadProductsByServer(options = {}) {
963
+ if (this.isMemoryReplica()) return this.refreshFromReplica(options.trigger || 'loadProductsByServer');
844
964
  if (!this.productDataSource) {
845
965
  this.logWarning('loadProductsByServer: ProductDataSource 不可用');
846
966
  return [];
@@ -856,6 +976,7 @@ class ProductsModule extends _BaseModule.BaseModule {
856
976
  sse: {}
857
977
  });
858
978
  const list = productList || [];
979
+ if (this.isMemoryReplica()) return this.store.list;
859
980
  (0, _product.perfMark)('loadProductsByServer.SSE', performance.now() - tSSE, {
860
981
  count: list.length
861
982
  });
@@ -950,7 +1071,9 @@ class ProductsModule extends _BaseModule.BaseModule {
950
1071
  * @param params 查询参数
951
1072
  */
952
1073
  async loadProductsByServerHttp(params) {
1074
+ if (this.isMemoryReplica()) return this.refreshFromReplica('loadProductsByServerHttp');
953
1075
  const productList = await this.fetchProductsByHttp(params);
1076
+ if (this.isMemoryReplica()) return this.store.list;
954
1077
  if (productList.length > 0) {
955
1078
  await this.replaceProductsSnapshotInIndexDB(productList, 'loadProductsByServerHttp');
956
1079
  this.lastServerFullFetchAtMs = Date.now();
@@ -1023,7 +1146,11 @@ class ProductsModule extends _BaseModule.BaseModule {
1023
1146
  * 根据 ID 列表删除商品(用于 pubsub 同步删除场景)
1024
1147
  * 同时更新 store.list、store.map、IndexDB 和价格缓存
1025
1148
  */
1026
- async removeProductsByIds(ids) {
1149
+ async removeProductsByIds(ids, options) {
1150
+ if (this.isMemoryReplica() && !options?.skipIndexDB) {
1151
+ await this.refreshFromReplica('removeProducts');
1152
+ return;
1153
+ }
1027
1154
  const idSet = new Set(ids);
1028
1155
  this.logInfo('removeProductsByIds', {
1029
1156
  ids,
@@ -1033,23 +1160,11 @@ class ProductsModule extends _BaseModule.BaseModule {
1033
1160
  for (const id of ids) {
1034
1161
  this.store.map.delete(id);
1035
1162
  }
1036
- if (this.dbManager) {
1037
- try {
1038
- for (const id of ids) {
1039
- await this.dbManager.delete(INDEXDB_STORE_NAME, id);
1040
- }
1041
- } catch (error) {
1042
- const errorMessage = error instanceof Error ? error.message : String(error);
1043
- this.logError('removeProductsByIds: IndexDB 删除失败', {
1044
- ids,
1045
- error: errorMessage
1046
- });
1047
- }
1048
- }
1163
+ if (!options?.skipIndexDB) await this.persistProductDelta([], ids, 'removeProductsByIds');
1049
1164
  for (const [dateKey, cachedProducts] of this.productsPriceCache.entries()) {
1050
1165
  this.productsPriceCache.set(dateKey, cachedProducts.filter(p => !idSet.has(p.id)));
1051
1166
  }
1052
- this.core.effects.emit(_types.ProductsHooks.onProductsChanged, this.store.list);
1167
+ if (!options?.skipEmit) this.core.effects.emit(_types.ProductsHooks.onProductsChanged, this.store.list);
1053
1168
  this.logInfo('removeProductsByIds 完成', {
1054
1169
  remaining: this.store.list.length
1055
1170
  });
@@ -1060,6 +1175,7 @@ class ProductsModule extends _BaseModule.BaseModule {
1060
1175
  * 用于 pubsub 同步 create / update / batch_update 场景
1061
1176
  */
1062
1177
  async refreshProducts() {
1178
+ if (this.isMemoryReplica()) return this.refreshFromReplica('refreshProducts');
1063
1179
  const tTotal = performance.now();
1064
1180
  this.logInfo('refreshProducts 开始');
1065
1181
  const products = await this.loadProductsByServer({
@@ -1162,9 +1278,10 @@ class ProductsModule extends _BaseModule.BaseModule {
1162
1278
  * await this.replaceProductsSnapshotInIndexDB(list, 'loadProductsByServer')
1163
1279
  */
1164
1280
  async replaceProductsSnapshotInIndexDB(products, source) {
1281
+ if (this.isMemoryReplica()) return false;
1165
1282
  if (!this.dbManager) {
1166
1283
  this.logWarning('replaceProductsSnapshotInIndexDB: dbManager 不可用');
1167
- return;
1284
+ return false;
1168
1285
  }
1169
1286
  const enqueuedAt = performance.now();
1170
1287
  let queueWaitMs = 0;
@@ -1193,6 +1310,11 @@ class ProductsModule extends _BaseModule.BaseModule {
1193
1310
  writeDurationMs = +(performance.now() - writeStartedAt).toFixed(2);
1194
1311
  }
1195
1312
  }
1313
+ await this.recordProductCommit({
1314
+ kind: 'snapshot',
1315
+ upserts: products,
1316
+ deletedIds: []
1317
+ });
1196
1318
  const executeDurationMs = +(performance.now() - operationStartedAt).toFixed(2);
1197
1319
  (0, _product.perfMark)('replaceProductsSnapshotInIndexDB', executeDurationMs, {
1198
1320
  count: products.length
@@ -1207,7 +1329,9 @@ class ProductsModule extends _BaseModule.BaseModule {
1207
1329
  totalDurationMs: +(performance.now() - enqueuedAt).toFixed(2)
1208
1330
  });
1209
1331
  });
1332
+ return true;
1210
1333
  } catch (error) {
1334
+ this.productPersistenceFailed = true;
1211
1335
  const errorMessage = error instanceof Error ? error.message : String(error);
1212
1336
  this.logError('全量保存商品到 IndexDB 失败', {
1213
1337
  source,
@@ -1218,52 +1342,51 @@ class ProductsModule extends _BaseModule.BaseModule {
1218
1342
  totalDurationMs: +(performance.now() - enqueuedAt).toFixed(2),
1219
1343
  error: errorMessage
1220
1344
  });
1345
+ return false;
1221
1346
  }
1222
1347
  }
1223
-
1224
- /**
1225
- * 增量 upsert 商品到 IndexDB,不清空表;用于 pubsub 批次合并后的变更商品。
1226
- *
1227
- * @example
1228
- * await this.patchProductsInIndexDB(changedProducts, 'pubsub.processProductSyncMessages', mergeActions)
1229
- */
1230
- async patchProductsInIndexDB(products, source, mergeActions = {}) {
1231
- if (!this.dbManager || products.length === 0) {
1232
- return;
1233
- }
1348
+ async patchProductsInIndexDB(products, source, _mergeActions = {}) {
1349
+ return this.persistProductDelta(products, [], source);
1350
+ }
1351
+ async persistProductDelta(products, deletedIds, source) {
1352
+ if (this.isMemoryReplica() || !this.dbManager) return false;
1234
1353
  const validProducts = products.filter(p => p?.id !== undefined && p?.id !== null);
1235
- if (validProducts.length === 0) {
1236
- return;
1237
- }
1354
+ if (!validProducts.length && !deletedIds.length) return true;
1355
+ const startedAt = performance.now();
1238
1356
  try {
1239
1357
  await this.runInProductIndexDBSaveQueue(async () => {
1240
- this.logInfo('patchProductsInIndexDB 开始', {
1241
- source,
1242
- count: validProducts.length
1243
- });
1244
- const t0 = performance.now();
1245
- if (typeof this.dbManager.bulkUpdate === 'function') {
1246
- await this.dbManager.bulkUpdate(INDEXDB_STORE_NAME, validProducts);
1247
- } else if (typeof this.dbManager.update === 'function') {
1248
- for (const product of validProducts) {
1249
- await this.dbManager.update(INDEXDB_STORE_NAME, product);
1358
+ for (const id of deletedIds) await this.dbManager.delete(INDEXDB_STORE_NAME, id);
1359
+ if (validProducts.length) {
1360
+ if (typeof this.dbManager.bulkUpdate === 'function') {
1361
+ await this.dbManager.bulkUpdate(INDEXDB_STORE_NAME, validProducts);
1362
+ } else if (typeof this.dbManager.update === 'function') {
1363
+ for (const product of validProducts) await this.dbManager.update(INDEXDB_STORE_NAME, product);
1364
+ } else {
1365
+ throw new Error('Product persistence adapter has no upsert method');
1250
1366
  }
1251
1367
  }
1252
- (0, _product.perfMark)('patchProductsInIndexDB', performance.now() - t0, {
1253
- count: validProducts.length
1368
+ await this.recordProductCommit({
1369
+ kind: 'delta',
1370
+ upserts: validProducts,
1371
+ deletedIds
1254
1372
  });
1255
1373
  this.logInfo('patchProductsInIndexDB 完成', {
1256
1374
  source,
1257
- count: validProducts.length
1375
+ productCount: validProducts.length,
1376
+ deletedCount: deletedIds.length,
1377
+ durationMs: Math.round(performance.now() - startedAt)
1258
1378
  });
1259
1379
  });
1380
+ return true;
1260
1381
  } catch (error) {
1261
- const errorMessage = error instanceof Error ? error.message : String(error);
1262
- this.logError('增量保存商品到 IndexDB 失败', {
1382
+ this.productPersistenceFailed = true;
1383
+ this.logError('增量保存商品失败', {
1263
1384
  source,
1264
- error: errorMessage,
1265
- productCount: validProducts.length
1385
+ productCount: validProducts.length,
1386
+ deletedCount: deletedIds.length,
1387
+ error: String(error)
1266
1388
  });
1389
+ return false;
1267
1390
  }
1268
1391
  }
1269
1392
 
@@ -1288,6 +1411,10 @@ class ProductsModule extends _BaseModule.BaseModule {
1288
1411
  * 在模块注册后自动调用
1289
1412
  */
1290
1413
  async preload() {
1414
+ if (this.isMemoryReplica()) {
1415
+ await this.refreshFromReplica('preload');
1416
+ return;
1417
+ }
1291
1418
  const tTotal = performance.now();
1292
1419
  this.logInfo('开始预加载数据');
1293
1420
  try {
@@ -1392,6 +1519,7 @@ class ProductsModule extends _BaseModule.BaseModule {
1392
1519
  return;
1393
1520
  }
1394
1521
  const createHandler = channelKey => message => {
1522
+ if (this.isMemoryReplica()) return;
1395
1523
  const data = message?.data || message;
1396
1524
  if (!data) return;
1397
1525
  this.logInfo('收到同步消息', {
@@ -1450,14 +1578,25 @@ class ProductsModule extends _BaseModule.BaseModule {
1450
1578
  * 处理完成后 emit onProductsSyncCompleted(携带 changedIds),
1451
1579
  * Server 层监听该事件后对变更商品增量执行 prepareProductsWithPrice 并更新缓存
1452
1580
  */
1453
- async processProductSyncMessages() {
1581
+ processProductSyncMessages() {
1582
+ const run = () => this.processProductSyncBatch();
1583
+ const task = this.productSyncQueue.then(run, run);
1584
+ this.productSyncQueue = task.catch(error => {
1585
+ this.logError('商品同步批次失败', {
1586
+ error: String(error)
1587
+ });
1588
+ });
1589
+ return this.productSyncQueue;
1590
+ }
1591
+ async processProductSyncBatch() {
1592
+ if (this.isMemoryReplica()) return;
1454
1593
  const messages = [...this.pendingSyncMessages];
1455
1594
  this.pendingSyncMessages = [];
1456
1595
  if (messages.length === 0) return;
1457
1596
  this.logInfo('processProductSyncMessages: 开始处理', {
1458
1597
  count: messages.length
1459
1598
  });
1460
- const deleteIds = [];
1599
+ const deleteIds = new Set();
1461
1600
  const bodyUpdates = new Map();
1462
1601
  const sseRefreshIds = [];
1463
1602
  const priceRefreshIds = [];
@@ -1470,10 +1609,18 @@ class ProductsModule extends _BaseModule.BaseModule {
1470
1609
  }
1471
1610
  // 1. 删除场景:operation === 'delete' 或 action === 'delete'
1472
1611
  if (msg.operation === 'delete' || msg.action === 'delete') {
1473
- if (msg.ids?.length) deleteIds.push(...msg.ids);else if (msg.id) deleteIds.push(msg.id);
1612
+ const ids = msg.ids?.length ? msg.ids : msg.id ? [msg.id] : [];
1613
+ for (const id of ids) {
1614
+ deleteIds.add(id);
1615
+ bodyUpdates.delete(id);
1616
+ }
1474
1617
  continue;
1475
1618
  }
1476
1619
 
1620
+ // The last explicit product operation wins within the existing sync batch.
1621
+ for (const id of msg.ids || (msg.id ? [msg.id] : [])) deleteIds.delete(id);
1622
+ if (msg.body?.id) deleteIds.delete(msg.body.id);
1623
+
1477
1624
  // 2. change_types 包含 price → 仅收集需要刷新价格的 IDs(不走 SSE 拉取)
1478
1625
  if (msg.change_types?.includes('price')) {
1479
1626
  const ids = msg.ids || (msg.id ? [msg.id] : []);
@@ -1514,18 +1661,20 @@ class ProductsModule extends _BaseModule.BaseModule {
1514
1661
  }
1515
1662
  }
1516
1663
  }
1517
- const uniqueDeleteIds = [...new Set(deleteIds)];
1518
- const uniqueSSEIds = [...new Set(sseRefreshIds)];
1519
- const uniquePriceIds = [...new Set(priceRefreshIds)];
1664
+ const uniqueDeleteIds = [...deleteIds];
1665
+ const uniqueSSEIds = [...new Set(sseRefreshIds)].filter(id => !deleteIds.has(id));
1666
+ const uniquePriceIds = [...new Set(priceRefreshIds)].filter(id => !deleteIds.has(id));
1520
1667
 
1521
1668
  // 1. 处理删除
1522
1669
  if (uniqueDeleteIds.length > 0) {
1523
- await this.removeProductsByIds(uniqueDeleteIds);
1670
+ await this.removeProductsByIds(uniqueDeleteIds, {
1671
+ skipIndexDB: true,
1672
+ skipEmit: true
1673
+ });
1524
1674
  }
1525
1675
 
1526
1676
  /** 本批次待 patch 的商品(按 id 去重,SSE 覆盖 body) */
1527
1677
  const patchProductMap = new Map();
1528
- const patchMergeActions = {};
1529
1678
  let storeMutated = uniqueDeleteIds.length > 0;
1530
1679
 
1531
1680
  // 2. 处理 body 直接覆盖(仅内存,IndexDB 批次末统一 patch)
@@ -1536,7 +1685,6 @@ class ProductsModule extends _BaseModule.BaseModule {
1536
1685
  });
1537
1686
  for (const product of bodyResult.changedProducts) {
1538
1687
  patchProductMap.set(product.id, product);
1539
- patchMergeActions[product.id] = bodyResult.mergeActions[product.id];
1540
1688
  }
1541
1689
  storeMutated = true;
1542
1690
  }
@@ -1552,7 +1700,6 @@ class ProductsModule extends _BaseModule.BaseModule {
1552
1700
  });
1553
1701
  for (const product of mergeResult.changedProducts) {
1554
1702
  patchProductMap.set(product.id, product);
1555
- patchMergeActions[product.id] = mergeResult.mergeActions[product.id];
1556
1703
  }
1557
1704
  storeMutated = true;
1558
1705
  }
@@ -1562,8 +1709,14 @@ class ProductsModule extends _BaseModule.BaseModule {
1562
1709
  });
1563
1710
  }
1564
1711
  const patchProducts = [...patchProductMap.values()];
1565
- if (patchProducts.length > 0) {
1566
- await this.patchProductsInIndexDB(patchProducts, 'pubsub.processProductSyncMessages', patchMergeActions);
1712
+ // Publish one committed delta for the entire batch, including deletions and price-only IDs.
1713
+ const committedUpserts = new Map(patchProducts.map(p => [p.id, p]));
1714
+ for (const id of uniquePriceIds) {
1715
+ const product = this.store.map.get(id);
1716
+ if (product && !uniqueDeleteIds.includes(id)) committedUpserts.set(id, product);
1717
+ }
1718
+ if (committedUpserts.size || uniqueDeleteIds.length) {
1719
+ await this.persistProductDelta([...committedUpserts.values()], uniqueDeleteIds, 'pubsub.batch');
1567
1720
  }
1568
1721
  if (storeMutated) {
1569
1722
  this.core.effects.emit(_types.ProductsHooks.onProductsChanged, this.store.list);
@@ -1762,6 +1915,7 @@ class ProductsModule extends _BaseModule.BaseModule {
1762
1915
  * @returns 刷新后的商品列表
1763
1916
  */
1764
1917
  async silentRefresh(options = {}) {
1918
+ if (this.isMemoryReplica()) return this.refreshFromReplica(options.trigger || 'background');
1765
1919
  const t0 = performance.now();
1766
1920
  const now = Date.now();
1767
1921
  const trigger = options.trigger || 'background';
@@ -1821,6 +1975,9 @@ class ProductsModule extends _BaseModule.BaseModule {
1821
1975
  * 销毁同步资源(取消 pubsub 订阅、清除定时器)
1822
1976
  */
1823
1977
  destroyProductSync() {
1978
+ void this.core.effects.emit(_types.ProductsHooks.onProductSyncDestroyed, null);
1979
+ this.disableProductReplicationSource();
1980
+ this.requestReplicaRefresh = undefined;
1824
1981
  if (this.syncTimer) {
1825
1982
  clearTimeout(this.syncTimer);
1826
1983
  this.syncTimer = undefined;
@@ -1831,6 +1988,11 @@ class ProductsModule extends _BaseModule.BaseModule {
1831
1988
  this.pendingSyncMessages = [];
1832
1989
  this.logInfo('destroyProductSync: 同步资源已销毁');
1833
1990
  }
1991
+ destroy() {
1992
+ this.runtimeSyncGeneration += 1;
1993
+ this.destroyProductSync();
1994
+ super.destroy();
1995
+ }
1834
1996
 
1835
1997
  /**
1836
1998
  * 获取模块的路由定义
@@ -0,0 +1,8 @@
1
+ import type { CommittedProducts, ReplicatedProducts } from './types';
2
+ /** A separate committed view: failed writes must never leak into recovery snapshots. */
3
+ export declare class CommittedProductState {
4
+ private products;
5
+ private revision;
6
+ commit(change: ReplicatedProducts): CommittedProducts;
7
+ snapshot(): CommittedProducts;
8
+ }
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.CommittedProductState = void 0;
7
+ /** A separate committed view: failed writes must never leak into recovery snapshots. */
8
+ class CommittedProductState {
9
+ products = new Map();
10
+ revision = 0;
11
+ commit(change) {
12
+ const copy = structuredClone(change);
13
+ if (copy.kind === 'snapshot') this.products.clear();
14
+ for (const id of copy.deletedIds) this.products.delete(id);
15
+ for (const product of copy.upserts) this.products.set(product.id, product);
16
+ return {
17
+ ...copy,
18
+ revision: ++this.revision
19
+ };
20
+ }
21
+ snapshot() {
22
+ return {
23
+ kind: 'snapshot',
24
+ revision: this.revision,
25
+ upserts: structuredClone([...this.products.values()]),
26
+ deletedIds: []
27
+ };
28
+ }
29
+ }
30
+ exports.CommittedProductState = CommittedProductState;
@@ -32,6 +32,15 @@ export interface ProductSnapshot {
32
32
  cover?: ProductData['cover'];
33
33
  holder_config?: ProductData['holder_config'];
34
34
  }
35
+ /** Raw product data only; each screen applies its own query/pricing context. */
36
+ export interface ReplicatedProducts {
37
+ kind: 'snapshot' | 'delta';
38
+ upserts: ProductData[];
39
+ deletedIds: number[];
40
+ }
41
+ export interface CommittedProducts extends ReplicatedProducts {
42
+ revision: number;
43
+ }
35
44
  /**
36
45
  * 加载报价单商品价格参数
37
46
  */
@@ -96,7 +105,9 @@ export declare enum ProductsHooks {
96
105
  onProductSelected = "products:onProductSelected",
97
106
  onProductsPriceApplied = "products:onProductsPriceApplied",
98
107
  /** pubsub 同步批次处理完成后触发,Server 层监听此事件重新计算并推送 */
99
- onProductsSyncCompleted = "products:onProductsSyncCompleted"
108
+ onProductsSyncCompleted = "products:onProductsSyncCompleted",
109
+ onProductsCommitted = "products:onProductsCommitted",
110
+ onProductSyncDestroyed = "products:onProductSyncDestroyed"
100
111
  }
101
112
  /** pubsub 商品同步消息结构 */
102
113
  export interface ProductSyncMessage {
@@ -13,6 +13,7 @@ exports.ProductsHooks = void 0;
13
13
  /**
14
14
  * 商品轻量查询范围
15
15
  */
16
+ /** Raw product data only; each screen applies its own query/pricing context. */
16
17
  /**
17
18
  * 加载报价单商品价格参数
18
19
  */
@@ -34,6 +35,8 @@ let ProductsHooks = exports.ProductsHooks = /*#__PURE__*/function (ProductsHooks
34
35
  ProductsHooks["onProductSelected"] = "products:onProductSelected";
35
36
  ProductsHooks["onProductsPriceApplied"] = "products:onProductsPriceApplied";
36
37
  ProductsHooks["onProductsSyncCompleted"] = "products:onProductsSyncCompleted";
38
+ ProductsHooks["onProductsCommitted"] = "products:onProductsCommitted";
39
+ ProductsHooks["onProductSyncDestroyed"] = "products:onProductSyncDestroyed";
37
40
  return ProductsHooks;
38
41
  }({});
39
42
  /** pubsub 商品同步消息结构 */
@@ -328,7 +328,7 @@ export declare class BookingByStepImpl extends BaseModule implements Module {
328
328
  date: string;
329
329
  status: string;
330
330
  week: string;
331
- weekNum: 0 | 2 | 1 | 6 | 4 | 5 | 3;
331
+ weekNum: 0 | 5 | 1 | 2 | 4 | 3 | 6;
332
332
  }[]>;
333
333
  submitTimeSlot(timeSlots: TimeSliceItem): void;
334
334
  private getScheduleDataByIds;
@@ -347,7 +347,7 @@ export declare class BookingByStepImpl extends BaseModule implements Module {
347
347
  count: number;
348
348
  left: number;
349
349
  summaryCount: number;
350
- status: "lots_of_space" | "filling_up_fast" | "sold_out";
350
+ status: "sold_out" | "lots_of_space" | "filling_up_fast";
351
351
  }[];
352
352
  /**
353
353
  * 找到多个资源的公共可用时间段