@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.
@@ -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
- // Apply property mutations
255
- const mutatedProperties = [];
256
- for (const prop of pset.properties) {
257
- const key = propertyKey(entityId, pset.name, prop.name);
258
- const mutation = this.propertyMutations.get(key);
259
- if (mutation) {
260
- if (mutation.operation === 'DELETE') {
261
- continue; // Skip deleted properties
262
- }
263
- // Apply SET mutation
264
- mutatedProperties.push({
265
- name: prop.name,
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 = basePsets.find(p => p.name === psetName);
342
- if (pset) {
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
- // masking a pset that genuinely exists in the base data — same argument
596
- // as `deleteProperty` one level down (see the comment above its own
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 the pset above already nets to nothing and there is no
600
- // deletion to report. Recording it as deleted here told the export
601
- // review a pset would be removed when the net change was zero.
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 = existingPsets.find(p => p.name === psetName);
604
- if (pset) {
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
- for (const q of qset.quantities) {
651
- const key = quantityKey(entityId, qset.name, q.name);
652
- const mutation = this.quantityMutations.get(key);
653
- if (mutation) {
654
- if (mutation.operation === 'DELETE')
655
- continue;
656
- mutatedQuantities.push({
657
- name: q.name,
658
- type: mutation.quantityType ?? q.type,
659
- value: mutation.value ?? q.value,
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
- // A DELETE marker only earns its keep against a qset that genuinely exists
851
- // in the base file - same argument as `deletePropertySet`'s. A purely
852
- // in-session qset has nothing to mask, and recording one would tell the
853
- // export review a set is being removed when the net change is zero.
854
- const baseQset = this.getBaseQuantitiesForEntity(entityId).find(q => q.name === qsetName);
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
- // CREATE_ENTITY records are skipped (callers must restore the
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
- }, null, 2);
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
  }