@mearie/core 0.5.1 → 0.5.2

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/index.cjs CHANGED
@@ -482,28 +482,48 @@ const replaceEqualDeep = (prev, next) => {
482
482
  }
483
483
  return allSame ? prev : result;
484
484
  };
485
+ const NormalizedKey = Symbol("mearie.normalized");
485
486
  /**
486
- * Deeply merges two values. Objects are recursively merged, arrays are element-wise merged,
487
- * entity links and primitives use last-write-wins.
487
+ * Marks a record as a normalized cache object so that {@link mergeFields}
488
+ * can distinguish it from opaque scalar values (e.g. JSON scalars).
489
+ * Only normalized records are deep-merged; unmarked objects are treated as
490
+ * atomic values and replaced entirely on write.
488
491
  * @internal
489
492
  */
490
- const mergeFieldValue = (existing, incoming) => {
493
+ const markNormalized = (obj) => {
494
+ Object.defineProperty(obj, NormalizedKey, { value: true });
495
+ };
496
+ const isNormalizedRecord = (value) => {
497
+ return typeof value === "object" && value !== null && NormalizedKey in value;
498
+ };
499
+ /**
500
+ * Deeply merges two values. When {@link deep} is false (default), only
501
+ * {@link markNormalized normalized} cache objects are recursively merged;
502
+ * unmarked plain objects (e.g. JSON scalars) are atomically replaced.
503
+ * When {@link deep} is true, all objects are recursively merged unconditionally.
504
+ * Arrays are element-wise merged, entity links and primitives use last-write-wins.
505
+ * @internal
506
+ */
507
+ const mergeFieldValue = (existing, incoming, deep) => {
491
508
  if (isNullish(existing) || isNullish(incoming)) return incoming;
492
509
  if (typeof existing !== "object" || typeof incoming !== "object") return incoming;
493
510
  if (isEntityLink(existing) || isEntityLink(incoming)) return incoming;
494
- if (Array.isArray(existing) && Array.isArray(incoming)) return incoming.map((item, i) => i < existing.length ? mergeFieldValue(existing[i], item) : item);
511
+ if (Array.isArray(existing) && Array.isArray(incoming)) return incoming.map((item, i) => i < existing.length ? mergeFieldValue(existing[i], item, deep) : item);
495
512
  if (Array.isArray(existing) || Array.isArray(incoming)) return incoming;
496
- mergeFields(existing, incoming);
513
+ if (!deep && !isNormalizedRecord(incoming)) return incoming;
514
+ mergeFields(existing, incoming, deep);
497
515
  return existing;
498
516
  };
499
517
  /**
500
- * Deeply merges source fields into target. Objects are recursively merged,
501
- * arrays are element-wise merged, entity links and primitives use last-write-wins.
518
+ * Deeply merges source fields into target.
519
+ * When {@link deep} is false (default), only {@link markNormalized normalized}
520
+ * objects are recursively merged; unmarked objects are atomically replaced.
521
+ * When {@link deep} is true, all objects are recursively merged unconditionally.
502
522
  * @internal
503
523
  */
504
- const mergeFields = (target, source) => {
524
+ const mergeFields = (target, source, deep) => {
505
525
  if (isNullish(source) || typeof source !== "object" || Array.isArray(source)) return;
506
- for (const key of Object.keys(source)) target[key] = mergeFieldValue(target[key], source[key]);
526
+ for (const key of Object.keys(source)) target[key] = mergeFieldValue(target[key], source[key], deep ?? false);
507
527
  };
508
528
  /**
509
529
  * Creates a FieldKey from a raw field name and optional arguments.
@@ -531,29 +551,31 @@ const normalize = (schemaMeta, selections, storage, data, variables, accessor) =
531
551
  if (keys.every((k) => k !== void 0 && k !== null)) return makeEntityKey(typename, keys);
532
552
  return null;
533
553
  };
534
- const normalizeField = (storageKey, selections, value) => {
554
+ const normalizeField = (storageKey, selections, value, parentType) => {
535
555
  if (isNullish(value)) return value;
536
- if (Array.isArray(value)) return value.map((item) => normalizeField(storageKey, selections, item));
556
+ if (Array.isArray(value)) return value.map((item) => normalizeField(storageKey, selections, item, parentType));
537
557
  const data = value;
538
- const typename = resolveTypename(selections, data);
558
+ const typename = resolveTypename(selections, data) ?? (parentType && schemaMeta.entities[parentType] ? parentType : void 0);
539
559
  const entityKey = resolveEntityKey(typename, data);
540
560
  if (entityKey) storageKey = entityKey;
541
561
  const fields = {};
542
562
  for (const selection of selections) if (selection.kind === "Field") {
543
563
  const fieldKey = makeFieldKey(selection, variables);
544
- const fieldValue = data[selection.alias ?? selection.name];
564
+ let fieldValue = data[selection.alias ?? selection.name];
565
+ if (selection.name === "__typename" && fieldValue === void 0 && typename) fieldValue = typename;
545
566
  if (storageKey !== null && selection.selections && typeof fieldValue === "object" && fieldValue !== null && !Array.isArray(fieldValue)) {
546
- const fieldTypename = resolveTypename(selection.selections, fieldValue);
567
+ const fieldTypename = resolveTypename(selection.selections, fieldValue) ?? (selection.type && schemaMeta.entities[selection.type] ? selection.type : void 0);
547
568
  if (fieldTypename && schemaMeta.entities[fieldTypename] && !resolveEntityKey(fieldTypename, fieldValue) && isEntityLink(storage[storageKey]?.[fieldKey])) continue;
548
569
  }
549
570
  const oldValue = storageKey === null ? void 0 : storage[storageKey]?.[fieldKey];
550
571
  if (storageKey !== null && (!selection.selections || isNullish(oldValue) || isNullish(fieldValue))) accessor?.(storageKey, fieldKey, oldValue, fieldValue);
551
- fields[fieldKey] = selection.selections ? normalizeField(null, selection.selections, fieldValue) : fieldValue;
572
+ fields[fieldKey] = selection.selections ? normalizeField(null, selection.selections, fieldValue, selection.type) : fieldValue;
552
573
  if (storageKey !== null && selection.selections && !isNullish(oldValue) && !isNullish(fieldValue) && !isEntityLink(fields[fieldKey]) && !isEqual(oldValue, fields[fieldKey])) accessor?.(storageKey, fieldKey, oldValue, fields[fieldKey]);
553
574
  } else if (selection.kind === "FragmentSpread" || selection.kind === "InlineFragment" && selection.on === typename) {
554
575
  const inner = normalizeField(storageKey, selection.selections, value);
555
576
  if (!isEntityLink(inner)) mergeFields(fields, inner);
556
577
  }
578
+ markNormalized(fields);
557
579
  if (entityKey) {
558
580
  const existing = storage[entityKey];
559
581
  if (existing) mergeFields(existing, fields);
@@ -603,7 +625,7 @@ const denormalize = (selections, storage, value, variables, accessor) => {
603
625
  }
604
626
  const name = selection.alias ?? selection.name;
605
627
  const value = selection.selections ? denormalizeField(null, selection.selections, fieldValue) : fieldValue;
606
- if (name in fields) mergeFields(fields, { [name]: value });
628
+ if (name in fields) mergeFields(fields, { [name]: value }, true);
607
629
  else fields[name] = value;
608
630
  } else if (selection.kind === "FragmentSpread") if (storageKey !== null && storageKey !== RootFieldKey) {
609
631
  fields[FragmentRefKey] = storageKey;
@@ -619,8 +641,8 @@ const denormalize = (selections, storage, value, variables, accessor) => {
619
641
  };
620
642
  }
621
643
  if (accessor) denormalize(selection.selections, storage, { [EntityLinkKey]: storageKey }, variables, accessor);
622
- } else mergeFields(fields, denormalizeField(storageKey, selection.selections, value));
623
- else if (selection.kind === "InlineFragment" && selection.on === data[typenameFieldKey]) mergeFields(fields, denormalizeField(storageKey, selection.selections, value));
644
+ } else mergeFields(fields, denormalizeField(storageKey, selection.selections, value), true);
645
+ else if (selection.kind === "InlineFragment" && selection.on === data[typenameFieldKey]) mergeFields(fields, denormalizeField(storageKey, selection.selections, value), true);
624
646
  return fields;
625
647
  };
626
648
  return {
package/dist/index.mjs CHANGED
@@ -481,28 +481,48 @@ const replaceEqualDeep = (prev, next) => {
481
481
  }
482
482
  return allSame ? prev : result;
483
483
  };
484
+ const NormalizedKey = Symbol("mearie.normalized");
484
485
  /**
485
- * Deeply merges two values. Objects are recursively merged, arrays are element-wise merged,
486
- * entity links and primitives use last-write-wins.
486
+ * Marks a record as a normalized cache object so that {@link mergeFields}
487
+ * can distinguish it from opaque scalar values (e.g. JSON scalars).
488
+ * Only normalized records are deep-merged; unmarked objects are treated as
489
+ * atomic values and replaced entirely on write.
487
490
  * @internal
488
491
  */
489
- const mergeFieldValue = (existing, incoming) => {
492
+ const markNormalized = (obj) => {
493
+ Object.defineProperty(obj, NormalizedKey, { value: true });
494
+ };
495
+ const isNormalizedRecord = (value) => {
496
+ return typeof value === "object" && value !== null && NormalizedKey in value;
497
+ };
498
+ /**
499
+ * Deeply merges two values. When {@link deep} is false (default), only
500
+ * {@link markNormalized normalized} cache objects are recursively merged;
501
+ * unmarked plain objects (e.g. JSON scalars) are atomically replaced.
502
+ * When {@link deep} is true, all objects are recursively merged unconditionally.
503
+ * Arrays are element-wise merged, entity links and primitives use last-write-wins.
504
+ * @internal
505
+ */
506
+ const mergeFieldValue = (existing, incoming, deep) => {
490
507
  if (isNullish(existing) || isNullish(incoming)) return incoming;
491
508
  if (typeof existing !== "object" || typeof incoming !== "object") return incoming;
492
509
  if (isEntityLink(existing) || isEntityLink(incoming)) return incoming;
493
- if (Array.isArray(existing) && Array.isArray(incoming)) return incoming.map((item, i) => i < existing.length ? mergeFieldValue(existing[i], item) : item);
510
+ if (Array.isArray(existing) && Array.isArray(incoming)) return incoming.map((item, i) => i < existing.length ? mergeFieldValue(existing[i], item, deep) : item);
494
511
  if (Array.isArray(existing) || Array.isArray(incoming)) return incoming;
495
- mergeFields(existing, incoming);
512
+ if (!deep && !isNormalizedRecord(incoming)) return incoming;
513
+ mergeFields(existing, incoming, deep);
496
514
  return existing;
497
515
  };
498
516
  /**
499
- * Deeply merges source fields into target. Objects are recursively merged,
500
- * arrays are element-wise merged, entity links and primitives use last-write-wins.
517
+ * Deeply merges source fields into target.
518
+ * When {@link deep} is false (default), only {@link markNormalized normalized}
519
+ * objects are recursively merged; unmarked objects are atomically replaced.
520
+ * When {@link deep} is true, all objects are recursively merged unconditionally.
501
521
  * @internal
502
522
  */
503
- const mergeFields = (target, source) => {
523
+ const mergeFields = (target, source, deep) => {
504
524
  if (isNullish(source) || typeof source !== "object" || Array.isArray(source)) return;
505
- for (const key of Object.keys(source)) target[key] = mergeFieldValue(target[key], source[key]);
525
+ for (const key of Object.keys(source)) target[key] = mergeFieldValue(target[key], source[key], deep ?? false);
506
526
  };
507
527
  /**
508
528
  * Creates a FieldKey from a raw field name and optional arguments.
@@ -530,29 +550,31 @@ const normalize = (schemaMeta, selections, storage, data, variables, accessor) =
530
550
  if (keys.every((k) => k !== void 0 && k !== null)) return makeEntityKey(typename, keys);
531
551
  return null;
532
552
  };
533
- const normalizeField = (storageKey, selections, value) => {
553
+ const normalizeField = (storageKey, selections, value, parentType) => {
534
554
  if (isNullish(value)) return value;
535
- if (Array.isArray(value)) return value.map((item) => normalizeField(storageKey, selections, item));
555
+ if (Array.isArray(value)) return value.map((item) => normalizeField(storageKey, selections, item, parentType));
536
556
  const data = value;
537
- const typename = resolveTypename(selections, data);
557
+ const typename = resolveTypename(selections, data) ?? (parentType && schemaMeta.entities[parentType] ? parentType : void 0);
538
558
  const entityKey = resolveEntityKey(typename, data);
539
559
  if (entityKey) storageKey = entityKey;
540
560
  const fields = {};
541
561
  for (const selection of selections) if (selection.kind === "Field") {
542
562
  const fieldKey = makeFieldKey(selection, variables);
543
- const fieldValue = data[selection.alias ?? selection.name];
563
+ let fieldValue = data[selection.alias ?? selection.name];
564
+ if (selection.name === "__typename" && fieldValue === void 0 && typename) fieldValue = typename;
544
565
  if (storageKey !== null && selection.selections && typeof fieldValue === "object" && fieldValue !== null && !Array.isArray(fieldValue)) {
545
- const fieldTypename = resolveTypename(selection.selections, fieldValue);
566
+ const fieldTypename = resolveTypename(selection.selections, fieldValue) ?? (selection.type && schemaMeta.entities[selection.type] ? selection.type : void 0);
546
567
  if (fieldTypename && schemaMeta.entities[fieldTypename] && !resolveEntityKey(fieldTypename, fieldValue) && isEntityLink(storage[storageKey]?.[fieldKey])) continue;
547
568
  }
548
569
  const oldValue = storageKey === null ? void 0 : storage[storageKey]?.[fieldKey];
549
570
  if (storageKey !== null && (!selection.selections || isNullish(oldValue) || isNullish(fieldValue))) accessor?.(storageKey, fieldKey, oldValue, fieldValue);
550
- fields[fieldKey] = selection.selections ? normalizeField(null, selection.selections, fieldValue) : fieldValue;
571
+ fields[fieldKey] = selection.selections ? normalizeField(null, selection.selections, fieldValue, selection.type) : fieldValue;
551
572
  if (storageKey !== null && selection.selections && !isNullish(oldValue) && !isNullish(fieldValue) && !isEntityLink(fields[fieldKey]) && !isEqual(oldValue, fields[fieldKey])) accessor?.(storageKey, fieldKey, oldValue, fields[fieldKey]);
552
573
  } else if (selection.kind === "FragmentSpread" || selection.kind === "InlineFragment" && selection.on === typename) {
553
574
  const inner = normalizeField(storageKey, selection.selections, value);
554
575
  if (!isEntityLink(inner)) mergeFields(fields, inner);
555
576
  }
577
+ markNormalized(fields);
556
578
  if (entityKey) {
557
579
  const existing = storage[entityKey];
558
580
  if (existing) mergeFields(existing, fields);
@@ -602,7 +624,7 @@ const denormalize = (selections, storage, value, variables, accessor) => {
602
624
  }
603
625
  const name = selection.alias ?? selection.name;
604
626
  const value = selection.selections ? denormalizeField(null, selection.selections, fieldValue) : fieldValue;
605
- if (name in fields) mergeFields(fields, { [name]: value });
627
+ if (name in fields) mergeFields(fields, { [name]: value }, true);
606
628
  else fields[name] = value;
607
629
  } else if (selection.kind === "FragmentSpread") if (storageKey !== null && storageKey !== RootFieldKey) {
608
630
  fields[FragmentRefKey] = storageKey;
@@ -618,8 +640,8 @@ const denormalize = (selections, storage, value, variables, accessor) => {
618
640
  };
619
641
  }
620
642
  if (accessor) denormalize(selection.selections, storage, { [EntityLinkKey]: storageKey }, variables, accessor);
621
- } else mergeFields(fields, denormalizeField(storageKey, selection.selections, value));
622
- else if (selection.kind === "InlineFragment" && selection.on === data[typenameFieldKey]) mergeFields(fields, denormalizeField(storageKey, selection.selections, value));
643
+ } else mergeFields(fields, denormalizeField(storageKey, selection.selections, value), true);
644
+ else if (selection.kind === "InlineFragment" && selection.on === data[typenameFieldKey]) mergeFields(fields, denormalizeField(storageKey, selection.selections, value), true);
623
645
  return fields;
624
646
  };
625
647
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mearie/core",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "Type-safe, zero-overhead GraphQL client",
5
5
  "keywords": [
6
6
  "graphql",