@configura/web-api 3.0.0-alpha.1 → 3.0.0-alpha.3

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 (31) hide show
  1. package/dist/CatalogueAPI.d.ts +68 -1
  2. package/dist/CatalogueAPI.js +184 -219
  3. package/dist/CfgMeasure.js +1 -2
  4. package/dist/CfgProduct.d.ts +22 -3
  5. package/dist/CfgProduct.js +172 -162
  6. package/dist/io/CfgIOManager.js +2 -11
  7. package/dist/io/CfgIOProdConfConnector.js +14 -25
  8. package/dist/io/CfgObservableStateManager.js +1 -1
  9. package/dist/productConfiguration/CfgFeature.d.ts +5 -0
  10. package/dist/productConfiguration/CfgFeature.js +68 -53
  11. package/dist/productConfiguration/CfgOption.d.ts +8 -0
  12. package/dist/productConfiguration/CfgOption.js +68 -73
  13. package/dist/productConfiguration/CfgProductConfiguration.js +22 -34
  14. package/dist/productConfiguration/productParamsGenerator.js +43 -57
  15. package/dist/productLoader.js +2 -13
  16. package/dist/syncGroups/SyncGroupsHandler.js +60 -83
  17. package/dist/syncGroups/SyncGroupsPathHelper.js +5 -5
  18. package/dist/syncGroups/SyncGroupsState.js +4 -5
  19. package/dist/syncGroups/SyncGroupsTransaction.d.ts +19 -1
  20. package/dist/syncGroups/SyncGroupsTransaction.js +352 -303
  21. package/dist/tasks/TaskHandler.js +53 -64
  22. package/dist/tests/testData/dummyProductForTest.js +4 -1
  23. package/dist/tests/testData/testDataAdditionalProductInAdditionalProductInProductForTest.js +18 -21
  24. package/dist/tests/testData/testDataCachedGetProduct.js +20 -20
  25. package/dist/tests/testData/testDataCachedPostValidate.js +6 -15
  26. package/dist/tests/testData/testDataProductAggregatedPrice.js +21 -21
  27. package/dist/tests/testData/testDataUpcharge.js +31 -22
  28. package/dist/utilitiesCatalogueData.js +21 -9
  29. package/dist/utilitiesCataloguePermission.js +5 -2
  30. package/dist/utilitiesConfiguration.js +21 -23
  31. package/package.json +3 -3
@@ -1,12 +1,3 @@
1
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
- return new (P || (P = Promise))(function (resolve, reject) {
4
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
- step((generator = generator.apply(thisArg, _arguments || [])).next());
8
- });
9
- };
10
1
  import { assert } from "@configura/web-utilities";
11
2
  import { CfgProductBubbleMode } from "../CfgProduct.js";
12
3
  import { SelectionType, } from "../productConfiguration/CfgFeature.js";
@@ -57,11 +48,9 @@ export class SyncGroupsTransaction {
57
48
  this.target = target;
58
49
  this.initial = initial;
59
50
  }
60
- static make(syncState, updateMode, product, productLoader, assumeNoStartState) {
61
- return __awaiter(this, void 0, void 0, function* () {
62
- const t = new this(syncState, updateMode, productLoader, product, yield product.clone(), assumeNoStartState ? undefined : yield product.clone());
63
- return t;
64
- });
51
+ static async make(syncState, updateMode, product, productLoader, assumeNoStartState) {
52
+ const t = new this(syncState, updateMode, productLoader, product, await product.clone(), assumeNoStartState ? undefined : await product.clone());
53
+ return t;
65
54
  }
66
55
  /************************************************************************
67
56
  * Public API (intentionally limited)
@@ -79,59 +68,55 @@ export class SyncGroupsTransaction {
79
68
  * but as this method will hand over to applyProduct if it updates the SyncState it can cause
80
69
  * the Features to change. And we need to pass that information back to know when to stop.
81
70
  */
82
- applyRootProduct() {
83
- return __awaiter(this, void 0, void 0, function* () {
84
- const productWithInitial = { target: this.target, initial: this.initial };
85
- if (!(yield this.applyProduct(productWithInitial))) {
86
- // All done!
87
- return false;
88
- }
89
- return yield this.updateRootProduct(undefined);
90
- });
71
+ async applyRootProduct() {
72
+ const productWithInitial = { target: this.target, initial: this.initial };
73
+ if (!(await this.applyProduct(productWithInitial))) {
74
+ // All done!
75
+ return false;
76
+ }
77
+ return await this.updateRootProduct(undefined);
91
78
  }
92
79
  /**
93
80
  * This is the entry point for an active (often user) selection of an option.
94
81
  */
95
- selectOption(optionPath, on) {
96
- return __awaiter(this, void 0, void 0, function* () {
97
- const option = SyncGroupsPathHelper.getOptionFromPath(optionPath, this.target);
98
- const feature = option.parent;
99
- let change = false;
100
- let productToValidate = undefined;
101
- // At this point in handling sync groups we go back to normal selecting.
102
- //
103
- // This is because a normal selection can bring features into scope which should push to or
104
- // (if the SyncGroup is defined) pull from the Sync State.
105
- //
106
- // We use BubbleSelected (without validation) as we can delay the validation until after we
107
- // have applied the SyncGroup (if any).
108
- if (yield feature.selectOption(option, on, ProductConfigurationBubbleMode.BubbleSelected)) {
109
- productToValidate = feature.parentProduct;
110
- change = true;
111
- switch (feature.selectionType) {
112
- case SelectionType.SelectOne:
113
- this.affectedSelectOneFeatures.set(feature, AffectedLevel.Set);
114
- break;
115
- case SelectionType.SelectMany:
116
- this.affectedSelectManyOptions.add(option);
117
- break;
118
- default:
119
- throw new Error("Should not happen");
120
- }
121
- }
82
+ async selectOption(optionPath, on) {
83
+ const option = SyncGroupsPathHelper.getOptionFromPath(optionPath, this.target);
84
+ const feature = option.parent;
85
+ let change = false;
86
+ let productToValidate = undefined;
87
+ // At this point in handling sync groups we go back to normal selecting.
88
+ //
89
+ // This is because a normal selection can bring features into scope which should push to or
90
+ // (if the SyncGroup is defined) pull from the Sync State.
91
+ //
92
+ // We use BubbleSelected (without validation) as we can delay the validation until after we
93
+ // have applied the SyncGroup (if any).
94
+ if (await feature.selectOption(option, on, ProductConfigurationBubbleMode.BubbleSelected)) {
95
+ productToValidate = feature.parentProduct;
96
+ change = true;
122
97
  switch (feature.selectionType) {
123
98
  case SelectionType.SelectOne:
124
- this.applySelectOneFeature(feature, true, false);
99
+ this.affectedSelectOneFeatures.set(feature, AffectedLevel.Set);
125
100
  break;
126
101
  case SelectionType.SelectMany:
127
- this.applySelectManyOption(option, true);
102
+ this.affectedSelectManyOptions.add(option);
128
103
  break;
104
+ default:
105
+ throw new Error("Should not happen");
129
106
  }
130
- if (yield this.updateRootProduct(productToValidate)) {
131
- change = true;
132
- }
133
- return change;
134
- });
107
+ }
108
+ switch (feature.selectionType) {
109
+ case SelectionType.SelectOne:
110
+ this.applySelectOneFeature(feature, true, false);
111
+ break;
112
+ case SelectionType.SelectMany:
113
+ this.applySelectManyOption(option, true);
114
+ break;
115
+ }
116
+ if (await this.updateRootProduct(productToValidate)) {
117
+ change = true;
118
+ }
119
+ return change;
135
120
  }
136
121
  /**
137
122
  * Overwrites the original Product and SyncGroupState (supplied when creating the Transaction)
@@ -139,12 +124,10 @@ export class SyncGroupsTransaction {
139
124
  *
140
125
  * @throws error if the transaction has already been closed.
141
126
  */
142
- commit() {
143
- return __awaiter(this, void 0, void 0, function* () {
144
- assert(this.isClosed === false, "Trying to commit a closed Transaction");
145
- this.originalSyncState.copyFrom(this.syncState);
146
- yield this.originalProduct.copyFrom(this.target, false, this.productLoader);
147
- });
127
+ async commit() {
128
+ assert(this.isClosed === false, "Trying to commit a closed Transaction");
129
+ this.originalSyncState.copyFrom(this.syncState);
130
+ await this.originalProduct.copyFrom(this.target, false, this.productLoader);
148
131
  }
149
132
  /************************************************************************
150
133
  * Updating things with the Transaction's SyncState
@@ -153,81 +136,76 @@ export class SyncGroupsTransaction {
153
136
  * Apply current sync groups on those who wants to listen until there is no more to settle.
154
137
  * @returns true if at least one Feature changed selected Option
155
138
  */
156
- updateRootProduct(productToValidate) {
157
- return __awaiter(this, void 0, void 0, function* () {
158
- const productsToValidate = new Set();
159
- if (productToValidate !== undefined) {
160
- productsToValidate.add(productToValidate);
161
- }
162
- const productWithInitial = { target: this.target, initial: this.initial };
163
- yield this.updateProduct(productWithInitial, productsToValidate);
164
- if (productsToValidate.size === 0) {
165
- // All have been settled, continue to pullPhase
166
- return yield this.applyRootProduct();
167
- }
168
- if (this.isClosed) {
169
- // We could exit in more places when the transaction has been aborted, but as
170
- // revalidate is really the only thing that could be expensive time consuming we only
171
- // check here.
172
- return false;
173
- }
174
- const promises = [];
175
- for (const product of productsToValidate) {
176
- promises.push(product._revalidate(CfgProductBubbleMode.ToRoot, this.productLoader, true));
177
- }
178
- const revalidationResults = yield Promise.all(promises);
179
- if (revalidationResults.every((r) => !r)) {
180
- this.close();
181
- return false;
182
- }
183
- // Apply over again, to settle deeper down. Our theory is that the front of "settled" will
184
- // move deeper and deeper into the tree.
185
- yield this.updateRootProduct(undefined);
186
- // We had productsToValidate, and so there must have been a change.
187
- return true;
188
- });
139
+ async updateRootProduct(productToValidate) {
140
+ const productsToValidate = new Set();
141
+ if (productToValidate !== undefined) {
142
+ productsToValidate.add(productToValidate);
143
+ }
144
+ const productWithInitial = { target: this.target, initial: this.initial };
145
+ await this.updateProduct(productWithInitial, productsToValidate);
146
+ if (productsToValidate.size === 0) {
147
+ // All have been settled, continue to pullPhase
148
+ return await this.applyRootProduct();
149
+ }
150
+ if (this.isClosed) {
151
+ // We could exit in more places when the transaction has been aborted, but as
152
+ // revalidate is really the only thing that could be expensive time consuming we only
153
+ // check here.
154
+ return false;
155
+ }
156
+ let anyChanges = false;
157
+ for (const product of productsToValidate) {
158
+ anyChanges =
159
+ (await product._revalidate(CfgProductBubbleMode.ToRoot, this.productLoader, true)) || anyChanges;
160
+ const diff = product.takeDiff();
161
+ if (diff)
162
+ this.applyFromDiff(diff);
163
+ }
164
+ if (!anyChanges) {
165
+ this.close();
166
+ return false;
167
+ }
168
+ // Apply over again, to settle deeper down. Our theory is that the front of "settled" will
169
+ // move deeper and deeper into the tree.
170
+ await this.updateRootProduct(undefined);
171
+ // We had productsToValidate, and so there must have been a change.
172
+ return true;
189
173
  }
190
174
  /**
191
175
  * Applies the SyncState to the Product and it's AdditionalProducts (sub-products).
192
176
  * @param productsToValidate To this all products that will need validation are added.
193
177
  */
194
- updateProduct(product, productsToValidate) {
195
- return __awaiter(this, void 0, void 0, function* () {
196
- const promises = [];
197
- promises.push(this.updateFeatures(getFeaturesFromProduct(product), productsToValidate));
198
- for (const additionalProduct of getAdditionalProducts(product)) {
199
- promises.push(this.updateProduct(additionalProduct, productsToValidate));
200
- }
201
- yield Promise.all(promises);
202
- });
178
+ async updateProduct(product, productsToValidate) {
179
+ const promises = [];
180
+ promises.push(this.updateFeatures(getFeaturesFromProduct(product), productsToValidate));
181
+ for (const additionalProduct of getAdditionalProducts(product)) {
182
+ promises.push(this.updateProduct(additionalProduct, productsToValidate));
183
+ }
184
+ await Promise.all(promises);
203
185
  }
204
186
  /**
205
187
  * Applies the SyncState to an array of Features.
206
188
  * @param productsToValidate To this all products that will need validation are added.
207
189
  */
208
- updateFeatures(features, productsToValidate) {
209
- return __awaiter(this, void 0, void 0, function* () {
210
- yield Promise.all(features.map((feature) => __awaiter(this, void 0, void 0, function* () {
211
- switch (yield this.updateFeature(feature, productsToValidate)) {
212
- case "stop":
213
- return;
214
- case "recurseDown":
215
- yield this.updateOptions(getSelectedOptions(feature), productsToValidate);
216
- return;
217
- }
218
- })));
219
- });
190
+ async updateFeatures(features, productsToValidate) {
191
+ await Promise.all(features.map(async (feature) => {
192
+ switch (await this.updateFeature(feature, productsToValidate)) {
193
+ case "stop":
194
+ return;
195
+ case "recurseDown":
196
+ await this.updateOptions(getSelectedOptions(feature), productsToValidate);
197
+ return;
198
+ }
199
+ }));
220
200
  }
221
201
  /**
222
202
  * Applies the SyncState to an array of Options.
223
203
  * @param productsToValidate To this all products that will need validation are added.
224
204
  */
225
- updateOptions(options, productsToValidate) {
226
- return __awaiter(this, void 0, void 0, function* () {
227
- yield Promise.all(options.map((option) => {
228
- return this.updateFeatures(getFeaturesFromOption(option), productsToValidate);
229
- }));
230
- });
205
+ async updateOptions(options, productsToValidate) {
206
+ await Promise.all(options.map((option) => {
207
+ return this.updateFeatures(getFeaturesFromOption(option), productsToValidate);
208
+ }));
231
209
  }
232
210
  /**
233
211
  * Applies the SyncState to a Feature
@@ -235,30 +213,28 @@ export class SyncGroupsTransaction {
235
213
  * @returns Whether we shall stop recursing down (because we are in a state which we
236
214
  * expect to be resolved later), we shall continue recursing down.
237
215
  */
238
- updateFeature(featureWithInitial, productsToValidate) {
239
- return __awaiter(this, void 0, void 0, function* () {
240
- const feature = featureWithInitial.target;
241
- const syncCode = feature.getSyncCode("pull");
242
- if (syncCode === undefined) {
243
- // Just continue down, features with no sync groups are always settled
216
+ async updateFeature(featureWithInitial, productsToValidate) {
217
+ const feature = featureWithInitial.target;
218
+ const syncCode = feature.getSyncCode("pull");
219
+ if (syncCode === undefined) {
220
+ // Just continue down, features with no sync groups are always settled
221
+ return "recurseDown";
222
+ }
223
+ if (syncCode === false) {
224
+ // Here we only handle pull. Initializing those missing in SyncMap
225
+ // we do later. We wait until we have settled as much as we can with what
226
+ // we have now, to increase chances of the values being written to the
227
+ // SyncState being the right ones.
228
+ return "stop";
229
+ }
230
+ switch (feature.selectionType) {
231
+ case SelectionType.Group:
244
232
  return "recurseDown";
245
- }
246
- if (syncCode === false) {
247
- // Here we only handle pull. Initializing those missing in SyncMap
248
- // we do later. We wait until we have settled as much as we can with what
249
- // we have now, to increase chances of the values being written to the
250
- // SyncState being the right ones.
251
- return "stop";
252
- }
253
- switch (feature.selectionType) {
254
- case SelectionType.Group:
255
- return "recurseDown";
256
- case SelectionType.SelectOne:
257
- return yield this.updateSelectOneFeature(syncCode, featureWithInitial, productsToValidate);
258
- case SelectionType.SelectMany:
259
- return yield this.updateSelectManyFeature(syncCode, featureWithInitial, productsToValidate);
260
- }
261
- });
233
+ case SelectionType.SelectOne:
234
+ return await this.updateSelectOneFeature(syncCode, featureWithInitial, productsToValidate);
235
+ case SelectionType.SelectMany:
236
+ return await this.updateSelectManyFeature(syncCode, featureWithInitial, productsToValidate);
237
+ }
262
238
  }
263
239
  /**
264
240
  * Decides if the SyncState can be applied to the SelectOne Feature, and then changes
@@ -268,48 +244,46 @@ export class SyncGroupsTransaction {
268
244
  * @returns Whether we shall stop recursing down (because we are in a state which we
269
245
  * expect to be resolved later), we shall continue recursing down.
270
246
  */
271
- updateSelectOneFeature(syncCode, featureWithInitial, productsToValidate) {
272
- return __awaiter(this, void 0, void 0, function* () {
273
- const feature = featureWithInitial.target;
274
- const featureDidJustComeIntoScope = featureWithInitial.initial === undefined;
275
- const hasBeenAffectedLevel = this.affectedSelectOneFeatures.get(feature);
276
- const syncGroupHasBeenUpdated = this.affectedSelectOneSyncGroups.has(syncCode);
277
- if (hasBeenAffectedLevel === AffectedLevel.Set ||
278
- (!syncGroupHasBeenUpdated && hasBeenAffectedLevel === AffectedLevel.Initialized)) {
279
- // A feature can change value for two reasons:
280
- // 1. The feature did just come into scope and loads from the sync group to set its "defaults"
281
- // 2. The sync group has updated with a new value
282
- // The rule is that in one sync group transaction the feature value is allowed to be updated
283
- // once or twice. Once for initialization and once for new sync group value. But they have to
284
- // happen in the right order.
285
- return "recurseDown";
286
- }
287
- if (!featureDidJustComeIntoScope && !syncGroupHasBeenUpdated) {
288
- return "recurseDown";
289
- }
290
- const currentSyncGroupValue = this.syncState.getForSelectOne(syncCode);
291
- if (currentSyncGroupValue === undefined) {
292
- // This branch will have to be settled later. Don't go further down here.
293
- return "stop";
294
- }
295
- const selectedOption = feature.selectedOptions[0];
296
- if (selectedOption !== undefined && selectedOption.code === currentSyncGroupValue) {
297
- // Settled, continue
298
- return "recurseDown";
299
- }
300
- const optionToSelect = feature.options.find((o) => o.code === currentSyncGroupValue);
301
- if (optionToSelect === undefined) {
302
- // No option which can be selected (keep current value and recurse down)
303
- return "recurseDown";
304
- }
305
- // Update the value. Validations are collected so that we do not send more than necessary.
306
- // Do not recurse further as we will change the state and so what is selected now won't be
307
- // selected then.
308
- yield feature.selectOption(optionToSelect._internal, true, ProductConfigurationBubbleMode.ToRoot);
309
- this.affectedSelectOneFeatures.set(feature, syncGroupHasBeenUpdated ? AffectedLevel.Set : AffectedLevel.Initialized);
310
- productsToValidate.add(feature.parentProduct);
247
+ async updateSelectOneFeature(syncCode, featureWithInitial, productsToValidate) {
248
+ const feature = featureWithInitial.target;
249
+ const featureDidJustComeIntoScope = featureWithInitial.initial === undefined;
250
+ const hasBeenAffectedLevel = this.affectedSelectOneFeatures.get(feature);
251
+ const syncGroupHasBeenUpdated = this.affectedSelectOneSyncGroups.has(syncCode);
252
+ if (hasBeenAffectedLevel === AffectedLevel.Set ||
253
+ (!syncGroupHasBeenUpdated && hasBeenAffectedLevel === AffectedLevel.Initialized)) {
254
+ // A feature can change value for two reasons:
255
+ // 1. The feature did just come into scope and loads from the sync group to set its "defaults"
256
+ // 2. The sync group has updated with a new value
257
+ // The rule is that in one sync group transaction the feature value is allowed to be updated
258
+ // once or twice. Once for initialization and once for new sync group value. But they have to
259
+ // happen in the right order.
260
+ return "recurseDown";
261
+ }
262
+ if (!featureDidJustComeIntoScope && !syncGroupHasBeenUpdated) {
263
+ return "recurseDown";
264
+ }
265
+ const currentSyncGroupValue = this.syncState.getForSelectOne(syncCode);
266
+ if (currentSyncGroupValue === undefined) {
267
+ // This branch will have to be settled later. Don't go further down here.
311
268
  return "stop";
312
- });
269
+ }
270
+ const selectedOption = feature.selectedOptions[0];
271
+ if (selectedOption !== undefined && selectedOption.code === currentSyncGroupValue) {
272
+ // Settled, continue
273
+ return "recurseDown";
274
+ }
275
+ const optionToSelect = feature.options.find((o) => o.code === currentSyncGroupValue);
276
+ if (optionToSelect === undefined) {
277
+ // No option which can be selected (keep current value and recurse down)
278
+ return "recurseDown";
279
+ }
280
+ // Update the value. Validations are collected so that we do not send more than necessary.
281
+ // Do not recurse further as we will change the state and so what is selected now won't be
282
+ // selected then.
283
+ await feature.selectOption(optionToSelect._internal, true, ProductConfigurationBubbleMode.ToRoot);
284
+ this.affectedSelectOneFeatures.set(feature, syncGroupHasBeenUpdated ? AffectedLevel.Set : AffectedLevel.Initialized);
285
+ productsToValidate.add(feature.parentProduct);
286
+ return "stop";
313
287
  }
314
288
  /**
315
289
  * Decides if the SyncState can be applied to Options in the SelectMany Feature, and
@@ -318,22 +292,20 @@ export class SyncGroupsTransaction {
318
292
  * @param productsToValidate To this all products that will need validation are added
319
293
  * @returns Always "stop" as recursion is handled internally. Return for consistency.
320
294
  */
321
- updateSelectManyFeature(syncCode, feature, productsToValidate) {
322
- return __awaiter(this, void 0, void 0, function* () {
323
- const optionsToContinueDown = [];
324
- for (const option of getOptions(feature)) {
325
- switch (yield this.updateSelectManyOption(syncCode, option, productsToValidate)) {
326
- case "stop":
327
- continue;
328
- case "recurseDown":
329
- optionsToContinueDown.push(option);
330
- continue;
331
- }
295
+ async updateSelectManyFeature(syncCode, feature, productsToValidate) {
296
+ const optionsToContinueDown = [];
297
+ for (const option of getOptions(feature)) {
298
+ switch (await this.updateSelectManyOption(syncCode, option, productsToValidate)) {
299
+ case "stop":
300
+ continue;
301
+ case "recurseDown":
302
+ optionsToContinueDown.push(option);
303
+ continue;
332
304
  }
333
- yield this.updateOptions(optionsToContinueDown, productsToValidate);
334
- // stop as the method above handles the recursion down
335
- return "stop";
336
- });
305
+ }
306
+ await this.updateOptions(optionsToContinueDown, productsToValidate);
307
+ // stop as the method above handles the recursion down
308
+ return "stop";
337
309
  }
338
310
  /**
339
311
  * Decides if the SyncState can be applied to the SelectMany Option, and then changes
@@ -343,108 +315,100 @@ export class SyncGroupsTransaction {
343
315
  * @returns Whether we shall stop recursing down (because we are in a state which we
344
316
  * expect to be resolved later), we shall continue recursing down.
345
317
  */
346
- updateSelectManyOption(syncCode, optionWithInitial, productsToValidate) {
347
- return __awaiter(this, void 0, void 0, function* () {
348
- const option = optionWithInitial.target;
349
- const featureDidJustComeIntoScope = optionWithInitial.initial === undefined;
350
- const optionSelected = option.selected;
351
- const recurseOrStopIfNoChange = optionSelected ? "recurseDown" : "stop";
352
- if (this.affectedSelectManyOptions.has(option)) {
353
- // This option has already changed selection once for this transaction. We expect
354
- // this to happen very rarely, as the algorithm should settle the selection tree
355
- // further and further out. Nevertheless, this safeguard is needed to avoid infinite
356
- // looping if for example the server would return the same data over and over.
357
- return recurseOrStopIfNoChange;
358
- }
359
- if (!featureDidJustComeIntoScope &&
360
- !this.hasSyncGroupAffectedForSelectMany(syncCode, option)) {
361
- return "recurseDown";
362
- }
363
- const syncGroupValueForOption = this.syncState.getForSelectMany(syncCode, option.code);
364
- if (syncGroupValueForOption === undefined) {
365
- // The sync group has no opinion on this. If it is selected, just continue down.
366
- return recurseOrStopIfNoChange;
367
- }
368
- if (syncGroupValueForOption === optionSelected) {
369
- // We are in sync for this option
370
- return recurseOrStopIfNoChange;
371
- }
372
- const feature = option.parent;
373
- // Update the value. Validations are collected so that we do not
374
- // send more than necessary. Do not recurse further as we will change
375
- // the state and so what is selected now won't be selected then.
376
- yield feature.selectOption(option, syncGroupValueForOption, ProductConfigurationBubbleMode.ToRoot);
377
- this.affectedSelectManyOptions.add(option);
378
- productsToValidate.add(feature.parentProduct);
379
- return "stop";
380
- });
318
+ async updateSelectManyOption(syncCode, optionWithInitial, productsToValidate) {
319
+ const option = optionWithInitial.target;
320
+ const featureDidJustComeIntoScope = optionWithInitial.initial === undefined;
321
+ const optionSelected = option.selected;
322
+ const recurseOrStopIfNoChange = optionSelected ? "recurseDown" : "stop";
323
+ if (this.affectedSelectManyOptions.has(option)) {
324
+ // This option has already changed selection once for this transaction. We expect
325
+ // this to happen very rarely, as the algorithm should settle the selection tree
326
+ // further and further out. Nevertheless, this safeguard is needed to avoid infinite
327
+ // looping if for example the server would return the same data over and over.
328
+ return recurseOrStopIfNoChange;
329
+ }
330
+ if (!featureDidJustComeIntoScope &&
331
+ !this.hasSyncGroupAffectedForSelectMany(syncCode, option)) {
332
+ return "recurseDown";
333
+ }
334
+ const syncGroupValueForOption = this.syncState.getForSelectMany(syncCode, option.code);
335
+ if (syncGroupValueForOption === undefined) {
336
+ // The sync group has no opinion on this. If it is selected, just continue down.
337
+ return recurseOrStopIfNoChange;
338
+ }
339
+ if (syncGroupValueForOption === optionSelected) {
340
+ // We are in sync for this option
341
+ return recurseOrStopIfNoChange;
342
+ }
343
+ const feature = option.parent;
344
+ // Update the value. Validations are collected so that we do not
345
+ // send more than necessary. Do not recurse further as we will change
346
+ // the state and so what is selected now won't be selected then.
347
+ await feature.selectOption(option, syncGroupValueForOption, ProductConfigurationBubbleMode.ToRoot);
348
+ this.affectedSelectManyOptions.add(option);
349
+ productsToValidate.add(feature.parentProduct);
350
+ return "stop";
381
351
  }
382
352
  /************************************************************************
383
353
  * Applying things to the Transaction's SyncState
384
354
  ************************************************************************/
385
- applyProduct(product) {
386
- return __awaiter(this, void 0, void 0, function* () {
387
- let change = false;
388
- if (yield this.applyFeatures(getFeaturesFromProduct(product))) {
355
+ async applyProduct(product) {
356
+ let change = false;
357
+ if (await this.applyFeatures(getFeaturesFromProduct(product))) {
358
+ if (this.updateMode === SyncGroupsApplyMode.Strict) {
359
+ return true;
360
+ }
361
+ change = true;
362
+ }
363
+ for (const additionalProduct of getAdditionalProducts(product)) {
364
+ if (await this.applyProduct(additionalProduct)) {
389
365
  if (this.updateMode === SyncGroupsApplyMode.Strict) {
390
366
  return true;
391
367
  }
392
368
  change = true;
393
369
  }
394
- for (const additionalProduct of getAdditionalProducts(product)) {
395
- if (yield this.applyProduct(additionalProduct)) {
396
- if (this.updateMode === SyncGroupsApplyMode.Strict) {
397
- return true;
370
+ }
371
+ return change;
372
+ }
373
+ async applyFeatures(featureWithInitials) {
374
+ let change = false;
375
+ for (const featureWithInitial of featureWithInitials) {
376
+ const feature = featureWithInitial.target;
377
+ switch (feature.selectionType) {
378
+ case SelectionType.SelectOne:
379
+ if (this.applySelectOneFeature(feature, false, featureWithInitial.initial === undefined)) {
380
+ change = true;
398
381
  }
399
- change = true;
400
- }
382
+ break;
383
+ case SelectionType.SelectMany:
384
+ if (this.applySelectManyFeature(featureWithInitial)) {
385
+ change = true;
386
+ }
387
+ break;
401
388
  }
402
- return change;
403
- });
404
- }
405
- applyFeatures(featureWithInitials) {
406
- return __awaiter(this, void 0, void 0, function* () {
407
- let change = false;
408
- for (const featureWithInitial of featureWithInitials) {
409
- const feature = featureWithInitial.target;
410
- switch (feature.selectionType) {
411
- case SelectionType.SelectOne:
412
- if (this.applySelectOneFeature(feature, false, featureWithInitial.initial === undefined)) {
413
- change = true;
414
- }
415
- break;
416
- case SelectionType.SelectMany:
417
- if (this.applySelectManyFeature(featureWithInitial)) {
418
- change = true;
419
- }
420
- break;
421
- }
422
- if (change && this.updateMode === SyncGroupsApplyMode.Strict) {
389
+ if (change && this.updateMode === SyncGroupsApplyMode.Strict) {
390
+ return true;
391
+ }
392
+ if (await this.applyOptions(getSelectedOptions(featureWithInitial))) {
393
+ if (this.updateMode === SyncGroupsApplyMode.Strict) {
423
394
  return true;
424
395
  }
425
- if (yield this.applyOptions(getSelectedOptions(featureWithInitial))) {
426
- if (this.updateMode === SyncGroupsApplyMode.Strict) {
427
- return true;
428
- }
429
- change = true;
430
- }
396
+ change = true;
431
397
  }
432
- return change;
433
- });
398
+ }
399
+ return change;
434
400
  }
435
- applyOptions(options) {
436
- return __awaiter(this, void 0, void 0, function* () {
437
- let change = false;
438
- for (const option of options) {
439
- if (yield this.applyFeatures(getFeaturesFromOption(option))) {
440
- if (this.updateMode === SyncGroupsApplyMode.Strict) {
441
- return true;
442
- }
443
- change = true;
401
+ async applyOptions(options) {
402
+ let change = false;
403
+ for (const option of options) {
404
+ if (await this.applyFeatures(getFeaturesFromOption(option))) {
405
+ if (this.updateMode === SyncGroupsApplyMode.Strict) {
406
+ return true;
444
407
  }
408
+ change = true;
445
409
  }
446
- return change;
447
- });
410
+ }
411
+ return change;
448
412
  }
449
413
  applySelectOneFeature(feature, userInitiated, featureDidJustComeIntoScope) {
450
414
  const selectionType = feature.selectionType;
@@ -525,6 +489,44 @@ export class SyncGroupsTransaction {
525
489
  this.syncState.setForSelectMany(syncCode, option.code, optionSelected);
526
490
  return true;
527
491
  }
492
+ /**
493
+ * Applies the given diff to the transaction's sync state.
494
+ */
495
+ applyFromDiff(diffs) {
496
+ for (const diff of diffs) {
497
+ this.applyNewStateFor(diff);
498
+ }
499
+ }
500
+ /**
501
+ * Applies the new state for the given option to the transaction's sync state.
502
+ */
503
+ applyNewStateFor(option) {
504
+ const feature = option.parent;
505
+ const syncCode = feature.getSyncCode("push");
506
+ if (syncCode === undefined || syncCode === false)
507
+ return false;
508
+ switch (feature.selectionType) {
509
+ case SelectionType.SelectOne: {
510
+ if (this.affectedSelectOneSyncGroups.has(syncCode)) {
511
+ return false;
512
+ }
513
+ this.affectedSelectOneSyncGroups.add(syncCode);
514
+ this.syncState.setForSelectOne(syncCode, option.code);
515
+ return true;
516
+ }
517
+ case SelectionType.SelectMany: {
518
+ if (this.hasSyncGroupAffectedForSelectMany(syncCode, option)) {
519
+ return false;
520
+ }
521
+ this.addSyncGroupAffectedForSelectMany(syncCode, option);
522
+ this.syncState.setForSelectMany(syncCode, option.code, option.selected);
523
+ return true;
524
+ }
525
+ case SelectionType.Group:
526
+ console.warn("No state to update for group: ", option.code);
527
+ return false;
528
+ }
529
+ }
528
530
  addSyncGroupAffectedForSelectMany(syncCode, option) {
529
531
  let forSyncCode = this.affectedSelectManySyncGroupsAndOptions.get(syncCode);
530
532
  if (forSyncCode === undefined) {
@@ -534,9 +536,60 @@ export class SyncGroupsTransaction {
534
536
  forSyncCode.add(option.code);
535
537
  }
536
538
  hasSyncGroupAffectedForSelectMany(syncCode, option) {
537
- var _a;
538
- return ((_a = this.affectedSelectManySyncGroupsAndOptions.get(syncCode)) === null || _a === void 0 ? void 0 : _a.has(option.code)) === true;
539
+ return this.affectedSelectManySyncGroupsAndOptions.get(syncCode)?.has(option.code) === true;
540
+ }
541
+ }
542
+ /************************************************************************
543
+ * Product diffing
544
+ ************************************************************************/
545
+ /**
546
+ * Finds all options that have changed in the new product compared to the original product.
547
+ *
548
+ * This is used to determine which options need to be updated in the sync state.
549
+ */
550
+ export function generateSelectionDiff(originalProduct, newProduct) {
551
+ const diff = generateFeaturesDiff(getFeaturesFromProduct({ initial: originalProduct, target: newProduct }));
552
+ return diff.length ? diff : null;
553
+ }
554
+ function generateFeaturesDiff(features) {
555
+ const changed = [];
556
+ for (const feature of features) {
557
+ switch (feature.target.selectionType) {
558
+ case SelectionType.SelectOne: {
559
+ const option = generateDiffForSelectOne(feature);
560
+ if (option)
561
+ changed.push(option);
562
+ break;
563
+ }
564
+ case SelectionType.SelectMany:
565
+ changed.push(...generateDiffForSelectMany(feature));
566
+ break;
567
+ }
568
+ for (const option of getSelectedOptions(feature)) {
569
+ changed.push(...generateFeaturesDiff(getFeaturesFromOption(option)));
570
+ }
571
+ }
572
+ return changed;
573
+ }
574
+ function generateDiffForSelectOne({ initial, target, }) {
575
+ if (!target.getSyncCode("push"))
576
+ return null;
577
+ const selected = target.selectedOptions[0];
578
+ if (selected && initial?.selectedOptions?.[0]?.code !== selected.code) {
579
+ return selected._internal;
580
+ }
581
+ return null;
582
+ }
583
+ function generateDiffForSelectMany(feature) {
584
+ const changed = [];
585
+ if (!feature.target.getSyncCode("push"))
586
+ return changed;
587
+ for (const { initial, target } of getOptions(feature)) {
588
+ if (initial?.selected !== target.selected) {
589
+ changed.push(target);
590
+ }
539
591
  }
592
+ return changed;
540
593
  }
541
594
  function getAdditionalProducts(product) {
542
595
  const initial = product.initial;
@@ -544,46 +597,42 @@ function getAdditionalProducts(product) {
544
597
  .filter((p) => p.selected)
545
598
  .map((childTarget) => {
546
599
  const refKey = childTarget.refKey;
547
- const childInitial = initial === null || initial === void 0 ? void 0 : initial.additionalProducts.find((p) => refKey === p.refKey);
600
+ const childInitial = initial?.additionalProducts.find((p) => refKey === p.refKey);
548
601
  return {
549
602
  target: childTarget._internal,
550
- initial: childInitial === null || childInitial === void 0 ? void 0 : childInitial._internal,
603
+ initial: childInitial?._internal,
551
604
  };
552
605
  });
553
606
  }
554
607
  function pairOptions(targets, initials) {
555
608
  return targets.map((childTarget) => {
556
609
  const key = childTarget.key;
557
- const childInitial = initials === null || initials === void 0 ? void 0 : initials.find((o) => key === o.key);
610
+ const childInitial = initials?.find((o) => key === o.key);
558
611
  return {
559
612
  target: childTarget._internal,
560
- initial: childInitial === null || childInitial === void 0 ? void 0 : childInitial._internal,
613
+ initial: childInitial?._internal,
561
614
  };
562
615
  });
563
616
  }
564
617
  function getSelectedOptions(feature) {
565
- var _a;
566
- return pairOptions(feature.target.selectedOptions, (_a = feature.initial) === null || _a === void 0 ? void 0 : _a.selectedOptions);
618
+ return pairOptions(feature.target.selectedOptions, feature.initial?.selectedOptions);
567
619
  }
568
620
  function getOptions(feature) {
569
- var _a;
570
- return pairOptions(feature.target.options, (_a = feature.initial) === null || _a === void 0 ? void 0 : _a.options);
621
+ return pairOptions(feature.target.options, feature.initial?.options);
571
622
  }
572
623
  function pairFeatures(targets, initials) {
573
624
  return targets.map((childTarget) => {
574
625
  const code = childTarget.code;
575
- const childInitial = initials === null || initials === void 0 ? void 0 : initials.find((f) => code === f.code);
626
+ const childInitial = initials?.find((f) => code === f.code);
576
627
  return {
577
628
  target: childTarget._internal,
578
- initial: childInitial === null || childInitial === void 0 ? void 0 : childInitial._internal,
629
+ initial: childInitial?._internal,
579
630
  };
580
631
  });
581
632
  }
582
633
  function getFeaturesFromProduct(product) {
583
- var _a;
584
- return pairFeatures(product.target.configuration.features, (_a = product.initial) === null || _a === void 0 ? void 0 : _a.configuration.features);
634
+ return pairFeatures(product.target.configuration.features, product.initial?.configuration.features);
585
635
  }
586
636
  function getFeaturesFromOption(option) {
587
- var _a;
588
- return pairFeatures(option.target.features, (_a = option.initial) === null || _a === void 0 ? void 0 : _a.getFeaturesOutsideSync());
637
+ return pairFeatures(option.target.features, option.initial?.getFeaturesOutsideSync());
589
638
  }