@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.
- package/dist/modules/Order/index.d.ts +1 -1
- package/dist/server/index.js +18 -16
- package/dist/server/modules/products/index.d.ts +28 -7
- package/dist/server/modules/products/index.js +1295 -769
- package/dist/server/modules/products/replication.d.ts +8 -0
- package/dist/server/modules/products/replication.js +69 -0
- package/dist/server/modules/products/types.d.ts +12 -1
- package/dist/server/modules/products/types.js +4 -0
- package/dist/solution/BookingByStep/index.d.ts +2 -2
- package/dist/solution/ScanOrder/index.js +36 -28
- package/dist/solution/ScanOrder/types.d.ts +3 -0
- package/lib/model/strategy/adapter/promotion/index.js +46 -1
- package/lib/modules/Order/index.d.ts +1 -1
- package/lib/server/index.js +2 -2
- package/lib/server/modules/products/index.d.ts +28 -7
- package/lib/server/modules/products/index.js +224 -62
- package/lib/server/modules/products/replication.d.ts +8 -0
- package/lib/server/modules/products/replication.js +30 -0
- package/lib/server/modules/products/types.d.ts +12 -1
- package/lib/server/modules/products/types.js +3 -0
- package/lib/solution/BookingByStep/index.d.ts +2 -2
- package/lib/solution/ScanOrder/index.js +31 -18
- package/lib/solution/ScanOrder/types.d.ts +3 -0
- package/package.json +1 -1
|
@@ -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
|
|
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.
|
|
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
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
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
|
|
1236
|
-
|
|
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.
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
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
|
-
|
|
1253
|
-
|
|
1368
|
+
await this.recordProductCommit({
|
|
1369
|
+
kind: 'delta',
|
|
1370
|
+
upserts: validProducts,
|
|
1371
|
+
deletedIds
|
|
1254
1372
|
});
|
|
1255
1373
|
this.logInfo('patchProductsInIndexDB 完成', {
|
|
1256
1374
|
source,
|
|
1257
|
-
|
|
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
|
-
|
|
1262
|
-
this.logError('
|
|
1382
|
+
this.productPersistenceFailed = true;
|
|
1383
|
+
this.logError('增量保存商品失败', {
|
|
1263
1384
|
source,
|
|
1264
|
-
|
|
1265
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = [...
|
|
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
|
-
|
|
1566
|
-
|
|
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 |
|
|
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: "
|
|
350
|
+
status: "sold_out" | "lots_of_space" | "filling_up_fast";
|
|
351
351
|
}[];
|
|
352
352
|
/**
|
|
353
353
|
* 找到多个资源的公共可用时间段
|