@mearie/core 0.5.0 → 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.
@@ -518,35 +538,49 @@ const makeFieldKeyFromArgs = (field, args) => {
518
538
 
519
539
  //#endregion
520
540
  //#region src/cache/normalize.ts
541
+ const resolveTypename = (selections, data) => {
542
+ for (const s of selections) if (s.kind === "Field" && s.name === "__typename") return data[s.alias ?? "__typename"];
543
+ return data.__typename;
544
+ };
521
545
  const normalize = (schemaMeta, selections, storage, data, variables, accessor) => {
522
- const normalizeField = (storageKey, selections, value) => {
546
+ const resolveEntityKey = (typename, data) => {
547
+ if (!typename) return null;
548
+ const entityMeta = schemaMeta.entities[typename];
549
+ if (!entityMeta) return null;
550
+ const keys = entityMeta.keyFields.map((field) => data[field]);
551
+ if (keys.every((k) => k !== void 0 && k !== null)) return makeEntityKey(typename, keys);
552
+ return null;
553
+ };
554
+ const normalizeField = (storageKey, selections, value, parentType) => {
523
555
  if (isNullish(value)) return value;
524
- 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));
525
557
  const data = value;
526
- const typename = data.__typename;
527
- let entityMeta = schemaMeta.entities[typename];
528
- if (entityMeta) {
529
- const keys = entityMeta.keyFields.map((field) => data[field]);
530
- if (keys.every((k) => k !== void 0 && k !== null)) storageKey = makeEntityKey(typename, keys);
531
- else entityMeta = void 0;
532
- }
558
+ const typename = resolveTypename(selections, data) ?? (parentType && schemaMeta.entities[parentType] ? parentType : void 0);
559
+ const entityKey = resolveEntityKey(typename, data);
560
+ if (entityKey) storageKey = entityKey;
533
561
  const fields = {};
534
562
  for (const selection of selections) if (selection.kind === "Field") {
535
563
  const fieldKey = makeFieldKey(selection, variables);
536
- 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;
566
+ if (storageKey !== null && selection.selections && typeof fieldValue === "object" && fieldValue !== null && !Array.isArray(fieldValue)) {
567
+ const fieldTypename = resolveTypename(selection.selections, fieldValue) ?? (selection.type && schemaMeta.entities[selection.type] ? selection.type : void 0);
568
+ if (fieldTypename && schemaMeta.entities[fieldTypename] && !resolveEntityKey(fieldTypename, fieldValue) && isEntityLink(storage[storageKey]?.[fieldKey])) continue;
569
+ }
537
570
  const oldValue = storageKey === null ? void 0 : storage[storageKey]?.[fieldKey];
538
571
  if (storageKey !== null && (!selection.selections || isNullish(oldValue) || isNullish(fieldValue))) accessor?.(storageKey, fieldKey, oldValue, fieldValue);
539
- fields[fieldKey] = selection.selections ? normalizeField(null, selection.selections, fieldValue) : fieldValue;
572
+ fields[fieldKey] = selection.selections ? normalizeField(null, selection.selections, fieldValue, selection.type) : fieldValue;
540
573
  if (storageKey !== null && selection.selections && !isNullish(oldValue) && !isNullish(fieldValue) && !isEntityLink(fields[fieldKey]) && !isEqual(oldValue, fields[fieldKey])) accessor?.(storageKey, fieldKey, oldValue, fields[fieldKey]);
541
574
  } else if (selection.kind === "FragmentSpread" || selection.kind === "InlineFragment" && selection.on === typename) {
542
575
  const inner = normalizeField(storageKey, selection.selections, value);
543
576
  if (!isEntityLink(inner)) mergeFields(fields, inner);
544
577
  }
545
- if (entityMeta && storageKey !== null) {
546
- const existing = storage[storageKey];
578
+ markNormalized(fields);
579
+ if (entityKey) {
580
+ const existing = storage[entityKey];
547
581
  if (existing) mergeFields(existing, fields);
548
- else storage[storageKey] = fields;
549
- return { [EntityLinkKey]: storageKey };
582
+ else storage[entityKey] = fields;
583
+ return { [EntityLinkKey]: entityKey };
550
584
  }
551
585
  return fields;
552
586
  };
@@ -591,7 +625,7 @@ const denormalize = (selections, storage, value, variables, accessor) => {
591
625
  }
592
626
  const name = selection.alias ?? selection.name;
593
627
  const value = selection.selections ? denormalizeField(null, selection.selections, fieldValue) : fieldValue;
594
- if (name in fields) mergeFields(fields, { [name]: value });
628
+ if (name in fields) mergeFields(fields, { [name]: value }, true);
595
629
  else fields[name] = value;
596
630
  } else if (selection.kind === "FragmentSpread") if (storageKey !== null && storageKey !== RootFieldKey) {
597
631
  fields[FragmentRefKey] = storageKey;
@@ -607,8 +641,8 @@ const denormalize = (selections, storage, value, variables, accessor) => {
607
641
  };
608
642
  }
609
643
  if (accessor) denormalize(selection.selections, storage, { [EntityLinkKey]: storageKey }, variables, accessor);
610
- } else mergeFields(fields, denormalizeField(storageKey, selection.selections, value));
611
- 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);
612
646
  return fields;
613
647
  };
614
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.
@@ -517,35 +537,49 @@ const makeFieldKeyFromArgs = (field, args) => {
517
537
 
518
538
  //#endregion
519
539
  //#region src/cache/normalize.ts
540
+ const resolveTypename = (selections, data) => {
541
+ for (const s of selections) if (s.kind === "Field" && s.name === "__typename") return data[s.alias ?? "__typename"];
542
+ return data.__typename;
543
+ };
520
544
  const normalize = (schemaMeta, selections, storage, data, variables, accessor) => {
521
- const normalizeField = (storageKey, selections, value) => {
545
+ const resolveEntityKey = (typename, data) => {
546
+ if (!typename) return null;
547
+ const entityMeta = schemaMeta.entities[typename];
548
+ if (!entityMeta) return null;
549
+ const keys = entityMeta.keyFields.map((field) => data[field]);
550
+ if (keys.every((k) => k !== void 0 && k !== null)) return makeEntityKey(typename, keys);
551
+ return null;
552
+ };
553
+ const normalizeField = (storageKey, selections, value, parentType) => {
522
554
  if (isNullish(value)) return value;
523
- 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));
524
556
  const data = value;
525
- const typename = data.__typename;
526
- let entityMeta = schemaMeta.entities[typename];
527
- if (entityMeta) {
528
- const keys = entityMeta.keyFields.map((field) => data[field]);
529
- if (keys.every((k) => k !== void 0 && k !== null)) storageKey = makeEntityKey(typename, keys);
530
- else entityMeta = void 0;
531
- }
557
+ const typename = resolveTypename(selections, data) ?? (parentType && schemaMeta.entities[parentType] ? parentType : void 0);
558
+ const entityKey = resolveEntityKey(typename, data);
559
+ if (entityKey) storageKey = entityKey;
532
560
  const fields = {};
533
561
  for (const selection of selections) if (selection.kind === "Field") {
534
562
  const fieldKey = makeFieldKey(selection, variables);
535
- 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;
565
+ if (storageKey !== null && selection.selections && typeof fieldValue === "object" && fieldValue !== null && !Array.isArray(fieldValue)) {
566
+ const fieldTypename = resolveTypename(selection.selections, fieldValue) ?? (selection.type && schemaMeta.entities[selection.type] ? selection.type : void 0);
567
+ if (fieldTypename && schemaMeta.entities[fieldTypename] && !resolveEntityKey(fieldTypename, fieldValue) && isEntityLink(storage[storageKey]?.[fieldKey])) continue;
568
+ }
536
569
  const oldValue = storageKey === null ? void 0 : storage[storageKey]?.[fieldKey];
537
570
  if (storageKey !== null && (!selection.selections || isNullish(oldValue) || isNullish(fieldValue))) accessor?.(storageKey, fieldKey, oldValue, fieldValue);
538
- fields[fieldKey] = selection.selections ? normalizeField(null, selection.selections, fieldValue) : fieldValue;
571
+ fields[fieldKey] = selection.selections ? normalizeField(null, selection.selections, fieldValue, selection.type) : fieldValue;
539
572
  if (storageKey !== null && selection.selections && !isNullish(oldValue) && !isNullish(fieldValue) && !isEntityLink(fields[fieldKey]) && !isEqual(oldValue, fields[fieldKey])) accessor?.(storageKey, fieldKey, oldValue, fields[fieldKey]);
540
573
  } else if (selection.kind === "FragmentSpread" || selection.kind === "InlineFragment" && selection.on === typename) {
541
574
  const inner = normalizeField(storageKey, selection.selections, value);
542
575
  if (!isEntityLink(inner)) mergeFields(fields, inner);
543
576
  }
544
- if (entityMeta && storageKey !== null) {
545
- const existing = storage[storageKey];
577
+ markNormalized(fields);
578
+ if (entityKey) {
579
+ const existing = storage[entityKey];
546
580
  if (existing) mergeFields(existing, fields);
547
- else storage[storageKey] = fields;
548
- return { [EntityLinkKey]: storageKey };
581
+ else storage[entityKey] = fields;
582
+ return { [EntityLinkKey]: entityKey };
549
583
  }
550
584
  return fields;
551
585
  };
@@ -590,7 +624,7 @@ const denormalize = (selections, storage, value, variables, accessor) => {
590
624
  }
591
625
  const name = selection.alias ?? selection.name;
592
626
  const value = selection.selections ? denormalizeField(null, selection.selections, fieldValue) : fieldValue;
593
- if (name in fields) mergeFields(fields, { [name]: value });
627
+ if (name in fields) mergeFields(fields, { [name]: value }, true);
594
628
  else fields[name] = value;
595
629
  } else if (selection.kind === "FragmentSpread") if (storageKey !== null && storageKey !== RootFieldKey) {
596
630
  fields[FragmentRefKey] = storageKey;
@@ -606,8 +640,8 @@ const denormalize = (selections, storage, value, variables, accessor) => {
606
640
  };
607
641
  }
608
642
  if (accessor) denormalize(selection.selections, storage, { [EntityLinkKey]: storageKey }, variables, accessor);
609
- } else mergeFields(fields, denormalizeField(storageKey, selection.selections, value));
610
- 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);
611
645
  return fields;
612
646
  };
613
647
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mearie/core",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Type-safe, zero-overhead GraphQL client",
5
5
  "keywords": [
6
6
  "graphql",