@ifc-lite/mutations 1.27.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -1
- package/dist/apply-mutations.d.ts +47 -0
- package/dist/apply-mutations.d.ts.map +1 -0
- package/dist/apply-mutations.js +182 -0
- package/dist/apply-mutations.js.map +1 -0
- package/dist/base-qset-lookup.d.ts +14 -0
- package/dist/base-qset-lookup.d.ts.map +1 -0
- package/dist/base-qset-lookup.js +14 -0
- package/dist/base-qset-lookup.js.map +1 -0
- package/dist/csv-connector.d.ts +7 -6
- package/dist/csv-connector.d.ts.map +1 -1
- package/dist/csv-connector.js +17 -32
- package/dist/csv-connector.js.map +1 -1
- package/dist/csv-parse-value.d.ts +16 -0
- package/dist/csv-parse-value.d.ts.map +1 -0
- package/dist/csv-parse-value.js +65 -0
- package/dist/csv-parse-value.js.map +1 -0
- package/dist/mutable-property-view.d.ts +41 -3
- package/dist/mutable-property-view.d.ts.map +1 -1
- package/dist/mutable-property-view.js +110 -274
- package/dist/mutable-property-view.js.map +1 -1
- package/dist/nonfinite-json.d.ts +8 -0
- package/dist/nonfinite-json.d.ts.map +1 -0
- package/dist/nonfinite-json.js +57 -0
- package/dist/nonfinite-json.js.map +1 -0
- package/dist/same-name-set-claims.d.ts +54 -0
- package/dist/same-name-set-claims.d.ts.map +1 -0
- package/dist/same-name-set-claims.js +74 -0
- package/dist/same-name-set-claims.js.map +1 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +4 -4
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
2
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
import { findQuantityInBaseSets } from './base-qset-lookup.js';
|
|
5
|
+
import { computeSetClaims, mutatedMembersForInstance } from './same-name-set-claims.js';
|
|
6
|
+
import { encodeNonFiniteNumbers, decodeNonFiniteNumbers } from './nonfinite-json.js';
|
|
4
7
|
import { PropertyValueType, QuantityType } from '@ifc-lite/data';
|
|
5
8
|
import { propertyKey, quantityKey, attributeKey, generateMutationId } from './types.js';
|
|
6
9
|
import { collectEffectiveChanges } from './effective-changes.js';
|
|
10
|
+
import { applyMutationsBatch } from './apply-mutations.js';
|
|
7
11
|
export class MutablePropertyView {
|
|
8
12
|
baseTable;
|
|
9
13
|
onDemandExtractor = null;
|
|
@@ -245,58 +249,31 @@ export class MutablePropertyView {
|
|
|
245
249
|
const seenPsets = new Set();
|
|
246
250
|
// First, add properties from base (on-demand or table) with mutations applied
|
|
247
251
|
const basePsets = this.getBasePropertiesForEntity(entityId);
|
|
252
|
+
// Two base psets can share a name (type pset + occurrence pset); a
|
|
253
|
+
// mutation key has no per-instance identity, so figure out up front
|
|
254
|
+
// which instance an edit -- or a brand-new property -- is claimed by:
|
|
255
|
+
// decides both an EXISTING property's SET/DELETE and where a brand-new
|
|
256
|
+
// property lands. Same mechanism `getQuantitiesForEntity` below uses,
|
|
257
|
+
// via `same-name-set-claims.ts`.
|
|
258
|
+
const claims = computeSetClaims(basePsets, pset => pset.properties);
|
|
248
259
|
for (const pset of basePsets) {
|
|
249
260
|
// Skip deleted property sets
|
|
250
261
|
if (this.deletedPsets.has(`${entityId}:${pset.name}`)) {
|
|
251
262
|
continue;
|
|
252
263
|
}
|
|
253
264
|
seenPsets.add(pset.name);
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
type: mutation.valueType ?? prop.type,
|
|
267
|
-
value: mutation.value ?? null,
|
|
268
|
-
unit: mutation.unit ?? prop.unit,
|
|
269
|
-
dataType: prop.dataType,
|
|
270
|
-
});
|
|
271
|
-
}
|
|
272
|
-
else {
|
|
273
|
-
mutatedProperties.push(prop);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
// Check for new properties added to this pset. Iterate the per-entity
|
|
277
|
-
// key set so this stays O(M_entity) instead of scanning every mutation
|
|
278
|
-
// in the model.
|
|
279
|
-
const entityPropKeys = this.propertyKeysByEntity.get(entityId);
|
|
280
|
-
if (entityPropKeys) {
|
|
281
|
-
const psetPrefix = `${entityId}:${pset.name}:`;
|
|
282
|
-
for (const key of entityPropKeys) {
|
|
283
|
-
if (!key.startsWith(psetPrefix))
|
|
284
|
-
continue;
|
|
285
|
-
const mutation = this.propertyMutations.get(key);
|
|
286
|
-
if (!mutation || mutation.operation !== 'SET')
|
|
287
|
-
continue;
|
|
288
|
-
const propName = key.slice(psetPrefix.length);
|
|
289
|
-
// Only add if not already in the list
|
|
290
|
-
if (!mutatedProperties.some(p => p.name === propName)) {
|
|
291
|
-
mutatedProperties.push({
|
|
292
|
-
name: propName,
|
|
293
|
-
type: mutation.valueType ?? PropertyValueType.String,
|
|
294
|
-
value: mutation.value ?? null,
|
|
295
|
-
unit: mutation.unit,
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
265
|
+
const mutatedProperties = mutatedMembersForInstance(entityId, pset, pset.properties, claims, this.propertyMutations, this.propertyKeysByEntity.get(entityId), propertyKey, (prop, mutation) => ({
|
|
266
|
+
name: prop.name,
|
|
267
|
+
type: mutation.valueType ?? prop.type,
|
|
268
|
+
value: mutation.value ?? null,
|
|
269
|
+
unit: mutation.unit ?? prop.unit,
|
|
270
|
+
dataType: prop.dataType,
|
|
271
|
+
}), prop => prop, (name, mutation) => ({
|
|
272
|
+
name,
|
|
273
|
+
type: mutation.valueType ?? PropertyValueType.String,
|
|
274
|
+
value: mutation.value ?? null,
|
|
275
|
+
unit: mutation.unit,
|
|
276
|
+
}));
|
|
300
277
|
if (mutatedProperties.length > 0) {
|
|
301
278
|
result.push({
|
|
302
279
|
name: pset.name,
|
|
@@ -336,14 +313,17 @@ export class MutablePropertyView {
|
|
|
336
313
|
return prop.value;
|
|
337
314
|
}
|
|
338
315
|
}
|
|
339
|
-
// Fall back to on-demand extraction or base table
|
|
316
|
+
// Fall back to on-demand extraction or base table. Scan every same-named
|
|
317
|
+
// pset (an entity can carry two, e.g. type + occurrence), not just the
|
|
318
|
+
// first -- see findQuantityInBaseSets's doc for why this doesn't import
|
|
319
|
+
// @ifc-lite/query's version.
|
|
340
320
|
const basePsets = this.getBasePropertiesForEntity(entityId);
|
|
341
|
-
const pset
|
|
342
|
-
|
|
321
|
+
for (const pset of basePsets) {
|
|
322
|
+
if (pset.name !== psetName)
|
|
323
|
+
continue;
|
|
343
324
|
const prop = pset.properties.find(p => p.name === propName);
|
|
344
|
-
if (prop)
|
|
325
|
+
if (prop)
|
|
345
326
|
return prop.value;
|
|
346
|
-
}
|
|
347
327
|
}
|
|
348
328
|
return null;
|
|
349
329
|
}
|
|
@@ -351,8 +331,13 @@ export class MutablePropertyView {
|
|
|
351
331
|
* Set a property value
|
|
352
332
|
* If the property set doesn't exist, creates it automatically
|
|
353
333
|
* @param skipHistory - If true, don't add to mutation history (used for undo/redo)
|
|
334
|
+
* @param dataType - IFC measure dataType this value was scaled against at
|
|
335
|
+
* write time (e.g. an IDS correction, #3929/#3943), stored on the
|
|
336
|
+
* `PropertyMutation` for a read-side overlay that needs to convert it
|
|
337
|
+
* between unit frames. New, additive, optional — every existing caller
|
|
338
|
+
* is unaffected.
|
|
354
339
|
*/
|
|
355
|
-
setProperty(entityId, psetName, propName, value, valueType = PropertyValueType.String, unit, skipHistory = false) {
|
|
340
|
+
setProperty(entityId, psetName, propName, value, valueType = PropertyValueType.String, unit, skipHistory = false, dataType) {
|
|
356
341
|
const key = propertyKey(entityId, psetName, propName);
|
|
357
342
|
// Get old value for undo
|
|
358
343
|
const oldValue = this.getPropertyValue(entityId, psetName, propName);
|
|
@@ -437,6 +422,7 @@ export class MutablePropertyView {
|
|
|
437
422
|
value,
|
|
438
423
|
valueType,
|
|
439
424
|
unit,
|
|
425
|
+
dataType,
|
|
440
426
|
});
|
|
441
427
|
const mutation = {
|
|
442
428
|
id: generateMutationId(),
|
|
@@ -591,17 +577,20 @@ export class MutablePropertyView {
|
|
|
591
577
|
this.deletePropertyMutation(entityId, key);
|
|
592
578
|
}
|
|
593
579
|
}
|
|
594
|
-
// A DELETE marker in `deletedPsets` only earns its keep when it is
|
|
595
|
-
//
|
|
596
|
-
//
|
|
597
|
-
// base-existence check): a purely in-session pset (added via
|
|
580
|
+
// A DELETE marker in `deletedPsets` only earns its keep when it is masking
|
|
581
|
+
// a pset that genuinely exists in the base data — same argument as
|
|
582
|
+
// `deleteProperty` one level down. A purely in-session pset (added via
|
|
598
583
|
// `createPropertySet`, never in the base file) has nothing to mask, so
|
|
599
|
-
// dropping
|
|
600
|
-
//
|
|
601
|
-
//
|
|
584
|
+
// dropping it above already nets to nothing: recording a deletion here
|
|
585
|
+
// told the export review a pset would go when the net change was zero.
|
|
586
|
+
// EVERY same-named pset: `deletedPsets` masks by name so the panel hides
|
|
587
|
+
// both, but the DELETE markers the exporter reads are per PROPERTY —
|
|
588
|
+
// covering only the first left `getForEntity` and `getPropertyValue`
|
|
589
|
+
// disagreeing on whether the second still exists.
|
|
602
590
|
const existingPsets = this.getBasePropertiesForEntity(entityId);
|
|
603
|
-
const pset
|
|
604
|
-
|
|
591
|
+
for (const pset of existingPsets) {
|
|
592
|
+
if (pset.name !== psetName)
|
|
593
|
+
continue;
|
|
605
594
|
this.deletedPsets.add(`${entityId}:${psetName}`);
|
|
606
595
|
for (const prop of pset.properties) {
|
|
607
596
|
const key = propertyKey(entityId, psetName, prop.name);
|
|
@@ -642,50 +631,25 @@ export class MutablePropertyView {
|
|
|
642
631
|
const result = [];
|
|
643
632
|
const seenQsets = new Set();
|
|
644
633
|
const baseQsets = this.getBaseQuantitiesForEntity(entityId);
|
|
634
|
+
// Same name-only key, and the same claiming rule, as the property path
|
|
635
|
+
// above (`same-name-set-claims.ts`): an edit or a brand-new quantity
|
|
636
|
+
// lands on exactly one same-named qset instance.
|
|
637
|
+
const claims = computeSetClaims(baseQsets, qset => qset.quantities);
|
|
645
638
|
for (const qset of baseQsets) {
|
|
646
639
|
if (this.deletedQsets.has(`${entityId}:${qset.name}`))
|
|
647
640
|
continue;
|
|
648
641
|
seenQsets.add(qset.name);
|
|
649
|
-
const mutatedQuantities =
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
unit: mutation.unit ?? q.unit,
|
|
661
|
-
});
|
|
662
|
-
}
|
|
663
|
-
else {
|
|
664
|
-
mutatedQuantities.push(q);
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
// Check for new quantities added to this qset (per-entity index — see
|
|
668
|
-
// the property-mutations site above for rationale).
|
|
669
|
-
const entityQtyKeys = this.quantityKeysByEntity.get(entityId);
|
|
670
|
-
if (entityQtyKeys) {
|
|
671
|
-
const qsetPrefix = `${entityId}:${qset.name}:`;
|
|
672
|
-
for (const key of entityQtyKeys) {
|
|
673
|
-
if (!key.startsWith(qsetPrefix))
|
|
674
|
-
continue;
|
|
675
|
-
const mutation = this.quantityMutations.get(key);
|
|
676
|
-
if (!mutation || mutation.operation !== 'SET')
|
|
677
|
-
continue;
|
|
678
|
-
const quantName = key.slice(qsetPrefix.length);
|
|
679
|
-
if (!mutatedQuantities.some(q => q.name === quantName)) {
|
|
680
|
-
mutatedQuantities.push({
|
|
681
|
-
name: quantName,
|
|
682
|
-
type: mutation.quantityType ?? QuantityType.Count,
|
|
683
|
-
value: mutation.value ?? 0,
|
|
684
|
-
unit: mutation.unit,
|
|
685
|
-
});
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
}
|
|
642
|
+
const mutatedQuantities = mutatedMembersForInstance(entityId, qset, qset.quantities, claims, this.quantityMutations, this.quantityKeysByEntity.get(entityId), quantityKey, (q, mutation) => ({
|
|
643
|
+
name: q.name,
|
|
644
|
+
type: mutation.quantityType ?? q.type,
|
|
645
|
+
value: mutation.value ?? q.value,
|
|
646
|
+
unit: mutation.unit ?? q.unit,
|
|
647
|
+
}), q => q, (name, mutation) => ({
|
|
648
|
+
name,
|
|
649
|
+
type: mutation.quantityType ?? QuantityType.Count,
|
|
650
|
+
value: mutation.value ?? 0,
|
|
651
|
+
unit: mutation.unit,
|
|
652
|
+
}));
|
|
689
653
|
if (mutatedQuantities.length > 0) {
|
|
690
654
|
result.push({ name: qset.name, quantities: mutatedQuantities });
|
|
691
655
|
}
|
|
@@ -789,9 +753,7 @@ export class MutablePropertyView {
|
|
|
789
753
|
isUpdate = true;
|
|
790
754
|
}
|
|
791
755
|
else {
|
|
792
|
-
const baseQuantity = baseQsets
|
|
793
|
-
.find(q => q.name === qsetName)
|
|
794
|
-
?.quantities.find(q => q.name === quantName);
|
|
756
|
+
const baseQuantity = findQuantityInBaseSets(baseQsets, qsetName, quantName);
|
|
795
757
|
oldValue = baseQuantity ? baseQuantity.value : null;
|
|
796
758
|
isUpdate = baseQuantity !== undefined;
|
|
797
759
|
}
|
|
@@ -847,12 +809,11 @@ export class MutablePropertyView {
|
|
|
847
809
|
this.deleteQuantityMutation(entityId, quantityKey(entityId, qsetName, quantity.name));
|
|
848
810
|
}
|
|
849
811
|
}
|
|
850
|
-
//
|
|
851
|
-
//
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
if (baseQset) {
|
|
812
|
+
// Only masks a qset that genuinely exists in the base file, and covers
|
|
813
|
+
// EVERY same-named one - both arguments as in `deletePropertySet` above.
|
|
814
|
+
for (const baseQset of this.getBaseQuantitiesForEntity(entityId)) {
|
|
815
|
+
if (baseQset.name !== qsetName)
|
|
816
|
+
continue;
|
|
856
817
|
this.deletedQsets.add(`${entityId}:${qsetName}`);
|
|
857
818
|
for (const quantity of baseQset.quantities) {
|
|
858
819
|
this.setQuantityMutation(entityId, quantityKey(entityId, qsetName, quantity.name), { operation: 'DELETE' });
|
|
@@ -1476,6 +1437,38 @@ export class MutablePropertyView {
|
|
|
1476
1437
|
getMutationsForEntity(entityId) {
|
|
1477
1438
|
return this.mutationHistory.filter(m => m.entityId === entityId);
|
|
1478
1439
|
}
|
|
1440
|
+
/**
|
|
1441
|
+
* The live overlay's CURRENT property mutation for one entity's specific
|
|
1442
|
+
* pset+prop, or `undefined` when that exact key carries no override right
|
|
1443
|
+
* now.
|
|
1444
|
+
*
|
|
1445
|
+
* Reads `propertyMutations` directly (the same map `getPropertyValue` and
|
|
1446
|
+
* `hasChanges` consult) — never `mutationHistory` (see `getMutationsForEntity`
|
|
1447
|
+
* above), which is append-only and does not shrink on undo. Undo re-applies
|
|
1448
|
+
* the inverse mutation with `skipHistory=true` (`mutationSlice.ts`, "to
|
|
1449
|
+
* avoid polluting mutation history"): that inverse call still writes
|
|
1450
|
+
* through `setProperty`/`deleteProperty`, so `propertyMutations` — and thus
|
|
1451
|
+
* this method — reflects the reverted (or, after redo, re-applied) value
|
|
1452
|
+
* immediately, while `getMutationsForEntity` keeps returning the stale
|
|
1453
|
+
* pre-undo entry.
|
|
1454
|
+
*
|
|
1455
|
+
* Unlike `getPropertyValue` (which collapses "no override", "override
|
|
1456
|
+
* value is null", and "override is a DELETE marker" all down to a bare
|
|
1457
|
+
* `null`), this returns the raw `PropertyMutation` so a caller projecting
|
|
1458
|
+
* the overlay onto an EXTERNAL base it doesn't otherwise share with this
|
|
1459
|
+
* view (e.g. the IDS bridge's `PropertyOverlayResolver`, #3929) can tell
|
|
1460
|
+
* "nothing to apply here" apart from "apply a DELETE" apart from "apply a
|
|
1461
|
+
* SET to null". Unlike `getEffectiveChanges()`, this does not require the
|
|
1462
|
+
* view's own `getBasePropertiesForEntity` to already know the pset —
|
|
1463
|
+
* `setProperty` always writes `propertyMutations` regardless of whether the
|
|
1464
|
+
* pset is new-in-session or pre-existing (see its "Always store in
|
|
1465
|
+
* propertyMutations for tracking" comment), so this stays correct for a
|
|
1466
|
+
* view with no base wired at all (a `MutablePropertyView` overlay used
|
|
1467
|
+
* purely as a delta against someone else's separate base).
|
|
1468
|
+
*/
|
|
1469
|
+
getPropertyMutation(entityId, psetName, propName) {
|
|
1470
|
+
return this.propertyMutations.get(propertyKey(entityId, psetName, propName));
|
|
1471
|
+
}
|
|
1479
1472
|
/**
|
|
1480
1473
|
* Check if an entity currently carries an overlay change.
|
|
1481
1474
|
*
|
|
@@ -1663,170 +1656,13 @@ export class MutablePropertyView {
|
|
|
1663
1656
|
this.mutationHistory = [];
|
|
1664
1657
|
}
|
|
1665
1658
|
/**
|
|
1666
|
-
* Apply a batch of mutations (e.g., from imported change set)
|
|
1659
|
+
* Apply a batch of mutations (e.g., from imported change set). The
|
|
1660
|
+
* dispatcher itself lives in `applyMutationsBatch` (./apply-mutations.js)
|
|
1661
|
+
* — this method just supplies the two bits of private state it needs
|
|
1662
|
+
* without exposing them publicly.
|
|
1667
1663
|
*/
|
|
1668
1664
|
applyMutations(mutations) {
|
|
1669
|
-
|
|
1670
|
-
// payload via restoreNewEntity). Track the ids we've skipped so a
|
|
1671
|
-
// matching DELETE_ENTITY in the same batch doesn't tombstone an
|
|
1672
|
-
// entity that never made it into this view — that stale tombstone
|
|
1673
|
-
// would later suppress a freshly-allocated overlay entity reusing
|
|
1674
|
-
// the same expressId.
|
|
1675
|
-
// Pass 1: collect every CREATE_ENTITY id up front, over the whole
|
|
1676
|
-
// array, before applying anything. CREATE_ENTITY is unconditionally
|
|
1677
|
-
// skipped below (every id it's called for lands here) — but a caller
|
|
1678
|
-
// supplying an arbitrary (e.g. imported/merged) Mutation[] may not have
|
|
1679
|
-
// its CREATE_ENTITY appear before the mutations that depend on it. A
|
|
1680
|
-
// single incremental forward pass would only "see" a create once the
|
|
1681
|
-
// loop reaches it, so a dependent mutation earlier in the array would
|
|
1682
|
-
// replay before its own entity's creation was known to be skipped —
|
|
1683
|
-
// reproducing the orphaned-pset bug via ordering instead of via the
|
|
1684
|
-
// original bug shape. Doing the full collection first makes the result
|
|
1685
|
-
// order-independent.
|
|
1686
|
-
const skippedCreateIds = new Set();
|
|
1687
|
-
for (const mutation of mutations) {
|
|
1688
|
-
if (mutation.type === 'CREATE_ENTITY') {
|
|
1689
|
-
skippedCreateIds.add(mutation.entityId);
|
|
1690
|
-
}
|
|
1691
|
-
}
|
|
1692
|
-
// Pass 2: apply mutations against the now-complete skip set.
|
|
1693
|
-
for (const mutation of mutations) {
|
|
1694
|
-
// Any mutation recorded against an entity whose own CREATE_ENTITY was
|
|
1695
|
-
// skipped above would otherwise replay into an orphan — a pset (or
|
|
1696
|
-
// attribute/quantity/type edit) keyed to an expressId that exists in
|
|
1697
|
-
// neither the source buffer nor `newEntities`. Refuse those too, so
|
|
1698
|
-
// the round trip is lossy (entity + its edits both dropped) rather
|
|
1699
|
-
// than corrupting (edits surviving without their entity). This keys
|
|
1700
|
-
// off `skippedCreateIds`, not "id absent from newEntities", so a
|
|
1701
|
-
// mutation against a normal, pre-existing source-buffer entity is
|
|
1702
|
-
// never affected — only ids that had their own CREATE_ENTITY skipped
|
|
1703
|
-
// in this same batch land here.
|
|
1704
|
-
// The `newEntities` check makes the condition "the create was skipped
|
|
1705
|
-
// AND nothing else supplied the entity". A caller following the
|
|
1706
|
-
// documented recovery flow calls `restoreNewEntity()` first and
|
|
1707
|
-
// *then* replays the history; the id is live by the time we get here,
|
|
1708
|
-
// so there is no orphan to guard against and dropping its edits would
|
|
1709
|
-
// silently lose data on the exact path the console.warn recommends.
|
|
1710
|
-
if (mutation.type !== 'CREATE_ENTITY' &&
|
|
1711
|
-
skippedCreateIds.has(mutation.entityId) &&
|
|
1712
|
-
!this.newEntities.has(mutation.entityId)) {
|
|
1713
|
-
continue;
|
|
1714
|
-
}
|
|
1715
|
-
switch (mutation.type) {
|
|
1716
|
-
case 'CREATE_PROPERTY':
|
|
1717
|
-
case 'UPDATE_PROPERTY':
|
|
1718
|
-
if (mutation.psetName && mutation.propName && mutation.newValue !== undefined) {
|
|
1719
|
-
this.setProperty(mutation.entityId, mutation.psetName, mutation.propName, mutation.newValue, mutation.valueType);
|
|
1720
|
-
}
|
|
1721
|
-
break;
|
|
1722
|
-
case 'DELETE_PROPERTY':
|
|
1723
|
-
if (mutation.psetName && mutation.propName) {
|
|
1724
|
-
this.deleteProperty(mutation.entityId, mutation.psetName, mutation.propName);
|
|
1725
|
-
}
|
|
1726
|
-
break;
|
|
1727
|
-
case 'DELETE_PROPERTY_SET':
|
|
1728
|
-
if (mutation.psetName) {
|
|
1729
|
-
this.deletePropertySet(mutation.entityId, mutation.psetName);
|
|
1730
|
-
}
|
|
1731
|
-
break;
|
|
1732
|
-
case 'DELETE_QUANTITY_SET':
|
|
1733
|
-
if (mutation.psetName) {
|
|
1734
|
-
this.deleteQuantitySet(mutation.entityId, mutation.psetName);
|
|
1735
|
-
// The marker is recorded even when this view cannot SEE the base
|
|
1736
|
-
// set, unlike the live path. `deleteQuantitySet` only masks a set
|
|
1737
|
-
// the quantity extractor reports, and that extractor is opt-in
|
|
1738
|
-
// (null by default, and several in-tree callers wire the property
|
|
1739
|
-
// one beside it and not it). A replayed deletion is a decision the
|
|
1740
|
-
// origin session already made, so dropping it here would let a
|
|
1741
|
-
// later export regenerate a set the user removed. An inert marker
|
|
1742
|
-
// on a set that does not exist costs a row in the change list;
|
|
1743
|
-
// losing the deletion costs the user's edit.
|
|
1744
|
-
this.deletedQsets.add(`${mutation.entityId}:${mutation.psetName}`);
|
|
1745
|
-
}
|
|
1746
|
-
break;
|
|
1747
|
-
case 'CREATE_QUANTITY':
|
|
1748
|
-
case 'UPDATE_QUANTITY':
|
|
1749
|
-
if (mutation.psetName && mutation.propName && mutation.newValue !== undefined) {
|
|
1750
|
-
this.setQuantity(mutation.entityId, mutation.psetName, mutation.propName, Number(mutation.newValue), mutation.quantityType ?? QuantityType.Count, mutation.unit);
|
|
1751
|
-
}
|
|
1752
|
-
else if (mutation.type === 'CREATE_QUANTITY' &&
|
|
1753
|
-
mutation.psetName &&
|
|
1754
|
-
Array.isArray(mutation.newValue)) {
|
|
1755
|
-
// `createQuantitySet()` (whole-qset creation, e.g.
|
|
1756
|
-
// `StoreEditor.addQuantitySet`) records ONE CREATE_QUANTITY mutation
|
|
1757
|
-
// for the whole set — no `propName`, `newValue` is the full
|
|
1758
|
-
// quantities array — unlike `setQuantity()`'s per-quantity
|
|
1759
|
-
// CREATE_QUANTITY, which always carries both. Mirrors the
|
|
1760
|
-
// CREATE_PROPERTY_SET handling below. Without this branch the
|
|
1761
|
-
// `psetName && propName` check above is false and the record
|
|
1762
|
-
// matched this `case` with nothing done — never falling through to
|
|
1763
|
-
// the "unhandled mutation type" warning either — so a freshly
|
|
1764
|
-
// created quantity set silently vanished on
|
|
1765
|
-
// exportMutations()/importMutations() round trip.
|
|
1766
|
-
this.createQuantitySet(mutation.entityId, mutation.psetName, mutation.newValue);
|
|
1767
|
-
}
|
|
1768
|
-
break;
|
|
1769
|
-
case 'UPDATE_POSITIONAL_ATTRIBUTE': {
|
|
1770
|
-
// attributeName is `@<index>` for positional mutations.
|
|
1771
|
-
const attr = mutation.attributeName ?? '';
|
|
1772
|
-
if (!attr.startsWith('@'))
|
|
1773
|
-
break;
|
|
1774
|
-
const index = Number(attr.slice(1));
|
|
1775
|
-
if (!Number.isInteger(index) || index < 0)
|
|
1776
|
-
break;
|
|
1777
|
-
if (mutation.newValue === undefined)
|
|
1778
|
-
break;
|
|
1779
|
-
this.setPositionalAttribute(mutation.entityId, index, mutation.newValue);
|
|
1780
|
-
break;
|
|
1781
|
-
}
|
|
1782
|
-
case 'UPDATE_ENTITY_TYPE': {
|
|
1783
|
-
const newType = mutation.entityType ?? (typeof mutation.newValue === 'string' ? mutation.newValue : undefined);
|
|
1784
|
-
if (!newType)
|
|
1785
|
-
break;
|
|
1786
|
-
this.setEntityType(mutation.entityId, newType, mutation.predefinedType ?? null, mutation.oldValue == null ? undefined : String(mutation.oldValue));
|
|
1787
|
-
break;
|
|
1788
|
-
}
|
|
1789
|
-
case 'UPDATE_ATTRIBUTE':
|
|
1790
|
-
if (mutation.attributeName && mutation.newValue !== undefined && mutation.newValue !== null) {
|
|
1791
|
-
this.setAttribute(mutation.entityId, mutation.attributeName, String(mutation.newValue), mutation.oldValue == null ? undefined : String(mutation.oldValue));
|
|
1792
|
-
}
|
|
1793
|
-
break;
|
|
1794
|
-
case 'CREATE_PROPERTY_SET':
|
|
1795
|
-
if (mutation.psetName && Array.isArray(mutation.newValue)) {
|
|
1796
|
-
// newValue is the original properties array (see createPropertySet,
|
|
1797
|
-
// where newValue = properties: Array<{ name; value; type?; unit? }>).
|
|
1798
|
-
this.createPropertySet(mutation.entityId, mutation.psetName, mutation.newValue);
|
|
1799
|
-
}
|
|
1800
|
-
break;
|
|
1801
|
-
case 'CREATE_ENTITY': {
|
|
1802
|
-
// Replay creates rely on the importer providing the entity body
|
|
1803
|
-
// via `restoreNewEntity` separately. The history record alone
|
|
1804
|
-
// doesn't carry the type+attributes payload — applying a bare
|
|
1805
|
-
// CREATE_ENTITY would lose the entity. We log and skip rather
|
|
1806
|
-
// than silently dropping it, so callers see they need to
|
|
1807
|
-
// restore the payload through the dedicated path. Unless the
|
|
1808
|
-
// caller already restored it, every other mutation recorded
|
|
1809
|
-
// against this id in this batch is dropped too (see the guard
|
|
1810
|
-
// above this switch) — otherwise the entity is gone but its edits
|
|
1811
|
-
// survive as an orphan. (skippedCreateIds was already fully
|
|
1812
|
-
// populated in pass 1, above.)
|
|
1813
|
-
// eslint-disable-next-line no-console
|
|
1814
|
-
console.warn(`applyMutations: CREATE_ENTITY for #${mutation.entityId} requires a NewEntity payload — ` +
|
|
1815
|
-
`restore via restoreNewEntity(). Skipping the record; dependent mutations recorded against ` +
|
|
1816
|
-
`#${mutation.entityId} are dropped too unless the entity was restored before this call.`);
|
|
1817
|
-
break;
|
|
1818
|
-
}
|
|
1819
|
-
case 'DELETE_ENTITY':
|
|
1820
|
-
this.deleteEntity(mutation.entityId);
|
|
1821
|
-
break;
|
|
1822
|
-
default:
|
|
1823
|
-
// Surface unhandled mutation types instead of silently dropping
|
|
1824
|
-
// them, so future gaps in this switch are visible.
|
|
1825
|
-
// eslint-disable-next-line no-console
|
|
1826
|
-
console.warn(`applyMutations: unhandled mutation type '${mutation.type}' for #${mutation.entityId} — skipped`);
|
|
1827
|
-
break;
|
|
1828
|
-
}
|
|
1829
|
-
}
|
|
1665
|
+
applyMutationsBatch(this, mutations, (entityId) => this.newEntities.has(entityId), (entityId, qsetName) => this.deletedQsets.add(`${entityId}:${qsetName}`));
|
|
1830
1666
|
}
|
|
1831
1667
|
/**
|
|
1832
1668
|
* Export mutations as JSON. Includes every record in `mutationHistory`,
|
|
@@ -1838,7 +1674,7 @@ export class MutablePropertyView {
|
|
|
1838
1674
|
modelId: this.modelId,
|
|
1839
1675
|
mutations: this.mutationHistory,
|
|
1840
1676
|
exportedAt: Date.now(),
|
|
1841
|
-
},
|
|
1677
|
+
}, encodeNonFiniteNumbers, 2);
|
|
1842
1678
|
}
|
|
1843
1679
|
/**
|
|
1844
1680
|
* Import mutations from JSON produced by `exportMutations`.
|
|
@@ -1860,7 +1696,7 @@ export class MutablePropertyView {
|
|
|
1860
1696
|
* fires.
|
|
1861
1697
|
*/
|
|
1862
1698
|
importMutations(json) {
|
|
1863
|
-
const data = JSON.parse(json);
|
|
1699
|
+
const data = JSON.parse(json, decodeNonFiniteNumbers);
|
|
1864
1700
|
if (data.mutations && Array.isArray(data.mutations)) {
|
|
1865
1701
|
this.applyMutations(data.mutations);
|
|
1866
1702
|
}
|