@warp-drive/core 5.9.0-alpha.17 → 5.9.0-alpha.19

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 (57) hide show
  1. package/declarations/graph/-private/-state.d.ts +1 -0
  2. package/declarations/index.d.ts +10 -1
  3. package/declarations/reactive/-private/default-mode.d.ts +22 -9
  4. package/declarations/reactive/-private/kind/belongs-to-field.d.ts +3 -0
  5. package/declarations/types/runtime.d.ts +16 -0
  6. package/declarations/types/schema/fields.d.ts +40 -12
  7. package/dist/graph/-private.js +67 -52
  8. package/dist/{index-CFwnjldF.js → index-LI2ckwf9.js} +332 -10
  9. package/dist/index.js +1 -1
  10. package/dist/reactive.js +1 -1
  11. package/dist/store/-private.js +1 -1
  12. package/dist/types/-private.js +1 -1
  13. package/dist/types/runtime.js +20 -1
  14. package/dist/unpkg/dev/graph/-private.js +67 -52
  15. package/dist/unpkg/dev/{index-BZRBBa34.js → index-DU_HLpg-.js} +332 -10
  16. package/dist/unpkg/dev/index.js +1 -1
  17. package/dist/unpkg/dev/reactive.js +1 -1
  18. package/dist/unpkg/dev/store/-private.js +2 -2
  19. package/dist/unpkg/dev/types/-private.js +1 -1
  20. package/dist/unpkg/dev/types/runtime.js +20 -1
  21. package/dist/unpkg/dev-deprecated/graph/-private.js +67 -52
  22. package/dist/unpkg/dev-deprecated/{index-Bu52tQ9I.js → index-BYFUHVPd.js} +332 -10
  23. package/dist/unpkg/dev-deprecated/index.js +1 -1
  24. package/dist/unpkg/dev-deprecated/reactive.js +1 -1
  25. package/dist/unpkg/dev-deprecated/store/-private.js +1 -1
  26. package/dist/unpkg/dev-deprecated/types/-private.js +1 -1
  27. package/dist/unpkg/dev-deprecated/types/runtime.js +20 -1
  28. package/dist/unpkg/prod/{-leaked-BHLmJyaw.js → -leaked-DgjQ5X55.js} +2 -2
  29. package/dist/unpkg/prod/configure.js +1 -1
  30. package/dist/unpkg/prod/graph/-private.js +17 -25
  31. package/dist/unpkg/prod/{schema-BdC3DirM.js → index-C-AhVafS.js} +2383 -73
  32. package/dist/unpkg/prod/index.js +5 -483
  33. package/dist/unpkg/prod/{promise-cache-DIT8Ypjq.js → promise-cache-DUblkX-U.js} +1 -1
  34. package/dist/unpkg/prod/reactive.js +3 -29
  35. package/dist/unpkg/prod/{request-BrJSCG6r.js → request-eb7Zm-oS.js} +1 -1
  36. package/dist/unpkg/prod/request.js +2 -2
  37. package/dist/unpkg/prod/signals/-leaked.js +1 -1
  38. package/dist/unpkg/prod/store/-private.js +2 -3
  39. package/dist/unpkg/prod/types/-private.js +1 -1
  40. package/dist/unpkg/prod/types/runtime.js +20 -1
  41. package/dist/unpkg/prod-deprecated/{-leaked-D_5Yrs5c.js → -leaked-BNVttukY.js} +1 -1
  42. package/dist/unpkg/prod-deprecated/configure.js +1 -1
  43. package/dist/unpkg/prod-deprecated/graph/-private.js +17 -25
  44. package/dist/unpkg/prod-deprecated/{schema-h0nuUTCj.js → index-CplZl2hv.js} +1035 -7
  45. package/dist/unpkg/prod-deprecated/index.js +5 -483
  46. package/dist/unpkg/prod-deprecated/{promise-cache-DIT8Ypjq.js → promise-cache-DUblkX-U.js} +1 -1
  47. package/dist/unpkg/prod-deprecated/reactive.js +2 -3
  48. package/dist/unpkg/prod-deprecated/{request-BrJSCG6r.js → request-eb7Zm-oS.js} +1 -1
  49. package/dist/unpkg/prod-deprecated/request.js +2 -2
  50. package/dist/unpkg/prod-deprecated/signals/-leaked.js +1 -1
  51. package/dist/unpkg/prod-deprecated/store/-private.js +1 -2
  52. package/dist/unpkg/prod-deprecated/types/-private.js +1 -1
  53. package/dist/unpkg/prod-deprecated/types/runtime.js +20 -1
  54. package/package.json +3 -3
  55. package/dist/unpkg/prod/handler-BJogFbfS.js +0 -1619
  56. package/dist/unpkg/prod-deprecated/handler-kGR8zguj.js +0 -334
  57. package/dist/unpkg/prod-deprecated/hooks-wJdveHPi.js +0 -26
@@ -55,9 +55,14 @@ function assertValidRelationshipPayload(graph, op) {
55
55
  kind
56
56
  } = definition;
57
57
  if (payload.links) {
58
- console.warn(`You pushed a record of type '${type}' with a relationship '${field}' configured as 'async: false'. You've included a link but no primary data, this may be an error in your payload. WarpDrive will treat this relationship as known-to-be-empty.`, isAsync || payload.data === null || !!payload.data || state.hasReceivedData, {
59
- id: 'ds.store.push-link-for-sync-relationship'
60
- });
58
+ // once JSON_API_CACHE_VALIDATION_ERRORS is active, this warning is instead
59
+ // the responsibility of the new document validator (see validateResourceRelationships
60
+ // in warp-drive-packages/json-api/src/-private/validator/1.1/7.2_resource-objects.ts)
61
+ {
62
+ console.warn(`You pushed a record of type '${type}' with a relationship '${field}' configured as 'async: false'. You've included a link but no primary data, this may be an error in your payload. WarpDrive will treat this relationship as known-to-be-empty.`, isAsync || payload.data === null || !!payload.data || state.hasReceivedData, {
63
+ id: 'ds.store.push-link-for-sync-relationship'
64
+ });
65
+ }
61
66
  } else if (payload.data) {
62
67
  if (kind === 'belongsTo') {
63
68
  (test => {
@@ -748,7 +753,7 @@ let assertInheritedSchema;
748
753
  errors.set('type', ` <---- should be '${definition.inverseType}'`);
749
754
  }
750
755
  if (definition.inverseKind !== meta.kind) {
751
- errors.set('type', ` <---- should be '${definition.inverseKind}'`);
756
+ errors.set('kind', ` <---- should be '${definition.inverseKind}'`);
752
757
  }
753
758
  if (definition.inverseIsAsync !== meta.options.async) {
754
759
  errors.set('async', ` <---- should be ${definition.inverseIsAsync}`);
@@ -759,7 +764,12 @@ let assertInheritedSchema;
759
764
  if (definition.key !== meta.options.inverse) {
760
765
  errors.set('inverse', ` <---- should be '${definition.key}'`);
761
766
  }
762
- if (definition.type !== meta.options.as) {
767
+ // `as` is only meaningful when `definition` itself is polymorphic (i.e. it
768
+ // points at an abstract type that `meta` is expected to implement). When
769
+ // `definition` isn't polymorphic, there's no abstract type for `meta` to
770
+ // declare `as` against, so `meta.options.as` is correctly absent - that's
771
+ // not an error to report.
772
+ if (definition.isPolymorphic && definition.type !== meta.options.as) {
763
773
  errors.set('as', ` <---- should be '${definition.type}'`);
764
774
  }
765
775
  return errors;
@@ -798,18 +808,6 @@ let assertInheritedSchema;
798
808
 
799
809
  `;
800
810
  }
801
- function metaFrom(definition) {
802
- return {
803
- name: definition.key,
804
- type: definition.type,
805
- kind: definition.kind,
806
- options: {
807
- async: definition.isAsync,
808
- polymorphic: definition.isPolymorphic,
809
- inverse: definition.inverseKey
810
- }
811
- };
812
- }
813
811
  function inverseMetaFrom(definition) {
814
812
  return {
815
813
  name: definition.inverseKey,
@@ -852,15 +850,18 @@ let assertInheritedSchema;
852
850
  });
853
851
  }
854
852
  assertInheritedSchema = function assertInheritedSchema(definition, type) {
855
- const meta1 = metaFrom(definition);
853
+ // `definition` is the only source of truth we have here - there is no
854
+ // second, independently-declared schema available to diff it against.
855
+ // That means the only thing we can actually verify is internal: does
856
+ // `definition`'s own resolved inverse agree that this relationship is
857
+ // polymorphic? Every other field of `meta2` below (name/type/kind/async/
858
+ // inverse) is derived directly from `definition` itself, so comparing
859
+ // them back to `definition` can never disagree - only the polymorphic
860
+ // flag is independently meaningful, because `definitionWithPolymorphic`
861
+ // asserts what it *should* be rather than mirroring what it already is.
856
862
  const meta2 = inverseMetaFrom(definition);
857
- const errors1 = validateSchema(inverseDefinition(definition), meta1);
858
863
  const errors2 = validateSchema(definitionWithPolymorphic(definition), meta2);
859
- if (errors2.size === 0 && errors1.size > 0) {
860
- throw new Error(`The schema for the relationship '${type}.${definition.key}' is not configured to satisfy '${definition.inverseType}' and thus cannot utilize the '${definition.inverseType}.${definition.key}' relationship to connect with '${definition.type}.${definition.inverseKey}'\n\nIf using this relationship in a polymorphic manner is desired, the relationships schema definition for '${type}' should include:${printSchema(meta1, errors1)}`);
861
- } else if (errors1.size > 0) {
862
- throw new Error(`The schema for the relationship '${type}.${definition.key}' is not configured to satisfy '${definition.inverseType}' and thus cannot utilize the '${definition.inverseType}.${definition.key}' relationship to connect with '${definition.type}.${definition.inverseKey}'\n\nIf using this relationship in a polymorphic manner is desired, the relationships schema definition for '${type}' should include:${printSchema(meta1, errors1)} and the relationships schema definition for '${definition.type}' should include:${printSchema(meta2, errors2)}`);
863
- } else if (errors2.size > 0) {
864
+ if (errors2.size > 0) {
864
865
  throw new Error(`The schema for the relationship '${type}.${definition.key}' satisfies '${definition.inverseType}' but cannot utilize the '${definition.inverseType}.${definition.key}' relationship to connect with '${definition.type}.${definition.inverseKey}' because that relationship is not polymorphic.\n\nIf using this relationship in a polymorphic manner is desired, the relationships schema definition for '${definition.type}' should include:${printSchema(meta2, errors2)}`);
865
866
  }
866
867
  };
@@ -1355,8 +1356,10 @@ function replaceRelatedRecord(graph, op, isRemote = false) {
1355
1356
  if (op.value === existingState) {
1356
1357
  // if we were empty before but now know we are empty this needs to be true
1357
1358
  state.hasReceivedData = true;
1359
+
1358
1360
  // if this is a remote update we still sync
1359
1361
  if (isRemote) {
1362
+ state.hasReceivedRemoteData = true;
1360
1363
  const {
1361
1364
  localState
1362
1365
  } = relationship;
@@ -1379,6 +1382,15 @@ function replaceRelatedRecord(graph, op, isRemote = false) {
1379
1382
  // update value to the new value
1380
1383
  relationship[prop] = op.value;
1381
1384
  state.hasReceivedData = true;
1385
+ /**
1386
+ * Local mutations never give us additional information about
1387
+ * whether the relationship has received *remote* data. This is
1388
+ * what `getRemoteRelationship` relies on to avoid reflecting
1389
+ * local-only edits.
1390
+ */
1391
+ if (isRemote) {
1392
+ state.hasReceivedRemoteData = true;
1393
+ }
1382
1394
  state.isEmpty = op.value === null;
1383
1395
  state.isStale = false;
1384
1396
  state.hasFailedLoadAttempt = false;
@@ -1890,8 +1902,30 @@ function rollbackRelationship(graph, key, field, relationship) {
1890
1902
  }
1891
1903
  }
1892
1904
  function createState() {
1905
+ {
1906
+ {
1907
+ if (getGlobalConfig().WarpDrive.debug.LOG_GRAPH || globalThis.getWarpDriveRuntimeConfig().debug.LOG_GRAPH) {
1908
+ let hasReceivedData = false;
1909
+ return {
1910
+ get hasReceivedData() {
1911
+ return hasReceivedData;
1912
+ },
1913
+ set hasReceivedData(value) {
1914
+ hasReceivedData = value;
1915
+ },
1916
+ hasReceivedRemoteData: false,
1917
+ isEmpty: true,
1918
+ isStale: false,
1919
+ hasFailedLoadAttempt: false,
1920
+ shouldForceReload: false,
1921
+ hasDematerializedInverse: false
1922
+ };
1923
+ }
1924
+ }
1925
+ }
1893
1926
  return {
1894
1927
  hasReceivedData: false,
1928
+ hasReceivedRemoteData: false,
1895
1929
  isEmpty: true,
1896
1930
  isStale: false,
1897
1931
  hasFailedLoadAttempt: false,
@@ -1996,13 +2030,10 @@ function legacyGetResourceRelationshipData(source, getRemoteState) {
1996
2030
  source.accessed = true;
1997
2031
  let data;
1998
2032
  const payload = {};
1999
- if (getRemoteState && source.remoteState) {
2000
- data = source.remoteState;
2001
- } else if (!getRemoteState && source.localState) {
2002
- data = source.localState;
2003
- }
2004
- if ((getRemoteState && source.remoteState === null || source.localState === null) && source.state.hasReceivedData) {
2005
- data = null;
2033
+ if (getRemoteState && (source.remoteState || source.state.hasReceivedRemoteData)) {
2034
+ data = source.remoteState ?? null;
2035
+ } else if (!getRemoteState && (source.localState || source.state.hasReceivedData)) {
2036
+ data = source.localState ?? null;
2006
2037
  }
2007
2038
  if (source.links) {
2008
2039
  payload.links = source.links;
@@ -2216,7 +2247,6 @@ function updateRelationshipOperation(graph, op) {
2216
2247
  })(isHasMany(relationship) || isBelongsTo(relationship));
2217
2248
  const {
2218
2249
  definition,
2219
- state,
2220
2250
  identifier
2221
2251
  } = relationship;
2222
2252
  const {
@@ -2256,23 +2286,6 @@ function updateRelationshipOperation(graph, op) {
2256
2286
  value: payload.data ? graph.store.cacheKeyManager.upgradeIdentifier(payload.data) : null
2257
2287
  }, true);
2258
2288
  }
2259
- } else if (definition.isAsync === false && !state.hasReceivedData) {
2260
- hasRelationshipDataProperty = true;
2261
- if (isCollection) {
2262
- graph.update({
2263
- op: 'replaceRelatedRecords',
2264
- record: identifier,
2265
- field: op.field,
2266
- value: []
2267
- }, true);
2268
- } else {
2269
- graph.update({
2270
- op: 'replaceRelatedRecord',
2271
- record: identifier,
2272
- field: op.field,
2273
- value: null
2274
- }, true);
2275
- }
2276
2289
  }
2277
2290
  if (payload.links) {
2278
2291
  const originalLinks = relationship.links;
@@ -2282,9 +2295,10 @@ function updateRelationshipOperation(graph, op) {
2282
2295
  const currentLink = originalLinks && originalLinks.related ? _normalizeLink(originalLinks.related) : null;
2283
2296
  const currentLinkHref = currentLink ? currentLink.href : null;
2284
2297
  if (relatedLink && relatedLink.href && relatedLink.href !== currentLinkHref) {
2285
- console.warn(`You pushed a record of type '${identifier.type}' with a relationship '${definition.key}' configured as 'async: false'. You've included a link but no primary data, this may be an error in your payload. WarpDrive will treat this relationship as known-to-be-empty.`, definition.isAsync || state.hasReceivedData, {
2286
- id: 'ds.store.push-link-for-sync-relationship'
2287
- });
2298
+ // Note: the equivalent warning for this case is issued eagerly (and with a more
2299
+ // complete condition, since it also accounts for `payload.data` being present) by
2300
+ // `assertValidRelationshipPayload` in `-utils.ts`, which always runs immediately
2301
+ // before this operation in DEBUG builds. Warning here too would double-warn.
2288
2302
  (test => {
2289
2303
  if (!test) {
2290
2304
  throw new Error(`You have pushed a record of type '${identifier.type}' with '${definition.key}' as a link, but the value of that link is not a string.`);
@@ -2315,6 +2329,7 @@ function updateRelationshipOperation(graph, op) {
2315
2329
  // we don't need to notify here as the update op we pushed in above will notify once
2316
2330
  // membership is in the correct state.
2317
2331
  relationship.state.hasReceivedData = true;
2332
+ relationship.state.hasReceivedRemoteData = true;
2318
2333
  relationship.state.isStale = false;
2319
2334
  relationship.state.hasDematerializedInverse = false;
2320
2335
  relationship.state.isEmpty = relationshipIsEmpty;
@@ -2,13 +2,13 @@ import { D as Destroy, C as Context, S as SOURCE, a as Checkout, b as Commit } f
2
2
  import { d as defineGate, w as withSignalStore, n as notifyInternalSignal, p as peekInternalSignal, a as willSyncFlushWatchers, g as getOrCreateInternalSignal, c as consumeInternalSignal, b as createInternalSignal, A as ARRAY_SIGNAL, S as Signals, e as createSignalDescriptor, f as defineSignal, h as entangleSignal, i as entangleInitiallyStaleSignal, O as OBJECT_SIGNAL, j as createInternalMemo, o as waitFor } from "./-leaked-BohmerbH.js";
3
3
  import { EnableHydration, SkipCache, STRUCTURED } from './types/request.js';
4
4
  import { isResourceSchema } from './types/schema/fields.js';
5
+ import { setLogging, getRuntimeConfig, setIsMaybeMirage } from './types/runtime.js';
5
6
  import { a as cloneResponseProperties, I as IS_CACHE_HANDLER, b as assertValidRequest, e as executeNextHandler, d as getRequestResult, u as upgradePromise, s as setPromiseResult, f as clearRequestResult } from "./future-DUuPqyLj.js";
6
7
  import { getOrSetGlobal, setTransient, peekTransient, peekUniversalTransient, setUniversalTransient } from './types/-private.js';
7
8
  import { DefaultCachePolicy } from './store.js';
8
9
  import { withBrand } from './request.js';
9
10
  import { CACHE_OWNER, DEBUG_STALE_CACHE_OWNER, DEBUG_KEY_TYPE, DEBUG_CLIENT_ORIGINATED } from './types/identifier.js';
10
11
  import { g as getGlobalConfig } from "./runtime-DGG4CvlW.js";
11
- import { setLogging, getRuntimeConfig } from './types/runtime.js';
12
12
  import { RecordStore, Type } from './types/symbols.js';
13
13
  function urlFromLink(link) {
14
14
  if (typeof link === 'string') return link;
@@ -6660,11 +6660,32 @@ function getBelongsToField(context) {
6660
6660
  }
6661
6661
  function setBelongsToField(context) {
6662
6662
  const {
6663
+ field,
6664
+ resourceKey,
6663
6665
  store
6664
6666
  } = context;
6665
6667
  const {
6666
- schema
6668
+ schema,
6669
+ cache
6667
6670
  } = store;
6671
+
6672
+ /**
6673
+ * We currently only allow editing sync belongsTo relationships
6674
+ * in PolarisMode
6675
+ */
6676
+ const isValidPolarisConfig = field.options.linksMode && !field.options.async;
6677
+ if (!context.legacy && isValidPolarisConfig) {
6678
+ const name = context.path.at(-1);
6679
+ cache.mutate({
6680
+ op: 'replaceRelatedRecord',
6681
+ record: resourceKey,
6682
+ field: name,
6683
+ value: getResourceKey(context.value)
6684
+ },
6685
+ // @ts-expect-error
6686
+ true);
6687
+ return true;
6688
+ }
6668
6689
  (test => {
6669
6690
  if (!test) {
6670
6691
  throw new Error(`Can only mutate belongsTo fields when the resource is in legacy mode`);
@@ -6673,6 +6694,12 @@ function setBelongsToField(context) {
6673
6694
  schema._kind('@legacy', 'belongsTo').set(store, context.record, context.resourceKey, context.field, context.value);
6674
6695
  return true;
6675
6696
  }
6697
+ function getResourceKey(record) {
6698
+ if (!record) {
6699
+ return null;
6700
+ }
6701
+ return recordIdentifierFor(record);
6702
+ }
6676
6703
  function getCollectionField(context) {
6677
6704
  entangleSignal(context.signals, context.record, context.path.at(-1), null);
6678
6705
  (test => {
@@ -7405,6 +7432,17 @@ function setSchemaObjectField(context) {
7405
7432
  // }
7406
7433
  return true;
7407
7434
  }
7435
+
7436
+ /**
7437
+ * A ReactiveResource context object passed to
7438
+ * the get/set functions of a field kind implementation.
7439
+ *
7440
+ * This provides all the necessary information for
7441
+ * the field kind to perform its operations.
7442
+ *
7443
+ * @internal
7444
+ */
7445
+
7408
7446
  const DefaultMode = {
7409
7447
  '@hash': {
7410
7448
  get: getHashField,
@@ -7700,6 +7738,9 @@ class ReactiveResource {
7700
7738
  if (prop === Destroy || prop === Checkout) {
7701
7739
  return true;
7702
7740
  }
7741
+ if (prop === identityField?.name) {
7742
+ return true;
7743
+ }
7703
7744
  return fields.has(prop);
7704
7745
  },
7705
7746
  getOwnPropertyDescriptor(target, prop) {
@@ -8198,6 +8239,14 @@ const TypeField = {
8198
8239
  key: 'type'
8199
8240
  }
8200
8241
  };
8242
+ const IdentifierField = {
8243
+ type: '@identity',
8244
+ name: '$key',
8245
+ kind: 'derived',
8246
+ options: {
8247
+ key: '^'
8248
+ }
8249
+ };
8201
8250
  const DefaultIdentityField = {
8202
8251
  name: 'id',
8203
8252
  kind: '@id'
@@ -8386,6 +8435,7 @@ function withDefaults(schema) {
8386
8435
  // we add TypeField to the beginning so that it will
8387
8436
  // appear right next to the identity field
8388
8437
  schema.fields.unshift(TypeField);
8438
+ schema.fields.unshift(IdentifierField);
8389
8439
  schema.fields.push(ConstructorField);
8390
8440
  return schema;
8391
8441
  }
@@ -8441,6 +8491,90 @@ function registerDerivations(schema) {
8441
8491
  schema.registerDerivation(fromIdentity);
8442
8492
  schema.registerDerivation(_constructor);
8443
8493
  }
8494
+
8495
+ /**
8496
+ * A relationship field capable of declaring `options.as`, marking it as a
8497
+ * concrete implementation of an abstract polymorphic type.
8498
+ */
8499
+
8500
+ /**
8501
+ * A relationship field contributed to an abstract type's schema, along with
8502
+ * the name of the type that contributed it (either a concrete implementer,
8503
+ * via `options.as`, or the abstract type's own schema), for use in assertion
8504
+ * messages when two contributions disagree on shape.
8505
+ */
8506
+
8507
+ /**
8508
+ * Reduces a field to a plain object with a fixed key order, covering every
8509
+ * option any relationship-implementer field can declare, so two fields can
8510
+ * be compared for exact equality via `JSON.stringify` regardless of the key
8511
+ * order the schema author happened to write them in.
8512
+ *
8513
+ * `options.as` is included and is *not* special-cased: every contributor to
8514
+ * a given field name on an abstract type - including the abstract type's
8515
+ * own schema, if it has one - is expected to declare `as` equal to that
8516
+ * abstract type's own name. This is redundant/self-referential for the
8517
+ * abstract type's own schema, but required for consistency: relationship
8518
+ * resolution elsewhere (e.g. `inverse` name-matching) does not itself
8519
+ * validate `as`, so a field with a missing or incorrect `as` can otherwise
8520
+ * be silently pulled into a relationship it never declared it implements.
8521
+ */
8522
+ function comparableAbstractFieldShape(field) {
8523
+ const {
8524
+ kind,
8525
+ name,
8526
+ type,
8527
+ sourceKey,
8528
+ options
8529
+ } = field;
8530
+ return {
8531
+ kind,
8532
+ name,
8533
+ type,
8534
+ sourceKey,
8535
+ options: {
8536
+ as: options?.as,
8537
+ async: options?.async,
8538
+ inverse: options?.inverse,
8539
+ polymorphic: options?.polymorphic,
8540
+ linksMode: options?.linksMode,
8541
+ resetOnRemoteUpdate: options?.resetOnRemoteUpdate,
8542
+ arrayExtensions: options?.arrayExtensions
8543
+ }
8544
+ };
8545
+ }
8546
+
8547
+ /**
8548
+ * Asserts that two contributions to the same field name on an abstract
8549
+ * polymorphic type - whether from two different concrete implementers, or
8550
+ * from an implementer and the abstract type's own schema - declare an
8551
+ * identical shape: `kind`, `type`, and every option, including `as` (which
8552
+ * every contributor, the abstract type's own schema included, must declare
8553
+ * equal to `abstractType`). A mismatch here (e.g. one implementer's field
8554
+ * being a `hasMany` while another's is a `belongsTo`, the two disagreeing on
8555
+ * `inverse`/`async`/`polymorphic`, or one omitting or misdeclaring `as`)
8556
+ * cannot be resolved by picking one side arbitrarily, since that would
8557
+ * silently leave whichever side lost the mismatch pointing at a relationship
8558
+ * whose true shape differs from what its own schema declared.
8559
+ */
8560
+ function assertConsistentAbstractFieldShape(abstractType, fieldName, existing, incoming) {
8561
+ // This function exists solely to support the `assert()` call below - there
8562
+ // is no other work here with an effect in production. Since `assert()`'s
8563
+ // own babel-macro stripping only removes the call expression itself, not
8564
+ // the (comparatively expensive, involving `JSON.stringify`) computation of
8565
+ // its arguments, guard the whole body so none of it survives in production
8566
+ // builds, matching how `assertPolymorphicType`/`assertInheritedSchema` and
8567
+ // their call sites are gated in `assert-polymorphic-type.ts`.
8568
+ {
8569
+ const existingShape = comparableAbstractFieldShape(existing.field);
8570
+ const incomingShape = comparableAbstractFieldShape(incoming.field);
8571
+ (test => {
8572
+ if (!test) {
8573
+ throw new Error(`Expected the relationship '${fieldName}' on the abstract polymorphic type '${abstractType}' to be declared identically everywhere it is implemented (including 'options.as', which every contributor - the abstract type's own schema included - must declare equal to '${abstractType}'), but '${existing.source}' declares it as:\n\n${JSON.stringify(existingShape, null, 2)}\n\nwhile '${incoming.source}' declares it as:\n\n${JSON.stringify(incomingShape, null, 2)}\n\nAll concrete implementers of an abstract type - and the abstract type's own schema, if it has one - must declare an identical shape for any relationship field they share.`);
8574
+ }
8575
+ })(JSON.stringify(existingShape) === JSON.stringify(incomingShape));
8576
+ }
8577
+ }
8444
8578
  /**
8445
8579
  * Wraps a derivation in a new function with Derivation signature but that looks
8446
8580
  * up the value in the cache before recomputing.
@@ -8480,6 +8614,18 @@ class SchemaService {
8480
8614
 
8481
8615
  /** @internal */
8482
8616
 
8617
+ /**
8618
+ * Tracks, per abstract type, the relationship fields that concrete
8619
+ * implementers have contributed via `options.as`, along with the type
8620
+ * that contributed each one (for assertion messages). This is
8621
+ * independent of whatever schema (synthesized or user-registered)
8622
+ * currently occupies `_schemas` for that type, so that these fields
8623
+ * survive regardless of the order in which the abstract type's
8624
+ * implementers and its own (optional) concrete schema are registered.
8625
+ *
8626
+ * @internal
8627
+ */
8628
+
8483
8629
  /** @internal */
8484
8630
 
8485
8631
  /** @internal */
@@ -8491,6 +8637,7 @@ class SchemaService {
8491
8637
  this._derivations = new Map();
8492
8638
  this._traits = new Map();
8493
8639
  this._modes = new Map();
8640
+ this._abstractImplementerFields = new Map();
8494
8641
  this._extensions = {
8495
8642
  object: new Map(),
8496
8643
  array: new Map()
@@ -8586,6 +8733,7 @@ class SchemaService {
8586
8733
  const fields = new Map();
8587
8734
  const relationships = {};
8588
8735
  const attributes = {};
8736
+ const abstractImplementations = [];
8589
8737
  for (const field of schema.fields) {
8590
8738
  (test => {
8591
8739
  if (!test) {
@@ -8599,6 +8747,31 @@ class SchemaService {
8599
8747
  attributes[field.name] = field;
8600
8748
  } else if (field.kind === 'belongsTo' || field.kind === 'hasMany') {
8601
8749
  relationships[field.name] = field;
8750
+ if (field.options?.as) {
8751
+ abstractImplementations.push(field);
8752
+ }
8753
+ }
8754
+ }
8755
+
8756
+ // This type may already be known as an abstract polymorphic type,
8757
+ // implemented by other concrete types via `options.as`, from before it
8758
+ // ever had a schema of its own (see `_registerAbstractTypeImplementation`).
8759
+ // Carry those previously-contributed fields forward so that registering
8760
+ // a "real" schema for the type - whenever that happens to occur - never
8761
+ // erases the relationships its implementers depend on.
8762
+ const implementerFields = this._abstractImplementerFields.get(schema.type);
8763
+ if (implementerFields) {
8764
+ for (const [name, contribution] of implementerFields) {
8765
+ const ownField = fields.get(name);
8766
+ if (!ownField) {
8767
+ fields.set(name, contribution.field);
8768
+ relationships[name] = contribution.field;
8769
+ } else {
8770
+ assertConsistentAbstractFieldShape(schema.type, name, contribution, {
8771
+ field: ownField,
8772
+ source: schema.type
8773
+ });
8774
+ }
8602
8775
  }
8603
8776
  }
8604
8777
  const cacheFields = null;
@@ -8617,6 +8790,101 @@ class SchemaService {
8617
8790
  internalSchema.cacheFields = getCacheFields(internalSchema);
8618
8791
  }
8619
8792
  this._schemas.set(schema.type, internalSchema);
8793
+
8794
+ // A relationship field's `as` option marks it as a valid concrete
8795
+ // implementer of an abstract polymorphic type (e.g. `as: 'commentable'`).
8796
+ // That abstract type may never be given its own schema by the user - it
8797
+ // may exist only to be implemented by concrete types like this one - or
8798
+ // it may already have (or later receive) a schema of its own, e.g. if it
8799
+ // turns out to also be a real, directly-resolvable resource. Either way,
8800
+ // ensure it has a schema with this field present, so that it behaves
8801
+ // like any other registered resource (`hasResource`, `fields`, etc.)
8802
+ // instead of requiring special-casing wherever abstract relationship
8803
+ // types are resolved.
8804
+ for (const field of abstractImplementations) {
8805
+ this._registerAbstractTypeImplementation(field, schema.type);
8806
+ }
8807
+ }
8808
+
8809
+ /** @internal */
8810
+ _registerAbstractTypeImplementation(field, implementer) {
8811
+ const abstractType = field.options.as;
8812
+ let implementerFields = this._abstractImplementerFields.get(abstractType);
8813
+ if (!implementerFields) {
8814
+ implementerFields = new Map();
8815
+ this._abstractImplementerFields.set(abstractType, implementerFields);
8816
+ }
8817
+
8818
+ // Unlike the original approach this replaced, `as` is *not* stripped here:
8819
+ // the field as synthesized onto the abstract type's own schema keeps
8820
+ // `options.as === abstractType`, redundant/self-referential as that is.
8821
+ // This keeps every contributor - implementers and the abstract type's
8822
+ // own schema alike - declaring the same thing, which is what
8823
+ // `assertConsistentAbstractFieldShape` checks, and it is also what lets
8824
+ // `assertPolymorphicType` (which reads a field's declared `as` off
8825
+ // whatever `schema.fields()` serves for its type) correctly permit the
8826
+ // abstract type itself to be pushed directly into this relationship.
8827
+ const abstractField = {
8828
+ ...field
8829
+ };
8830
+ const contribution = {
8831
+ field: abstractField,
8832
+ source: implementer
8833
+ };
8834
+
8835
+ // all concrete implementations of an abstract type are required to
8836
+ // share the same shape for the field that implements it, so the first
8837
+ // one registered is as good a canonical source as any - but a later,
8838
+ // differently-shaped one is almost certainly a mistake rather than an
8839
+ // intentional override, so we catch it rather than silently ignoring it.
8840
+ const existingImplementer = implementerFields.get(field.name);
8841
+ if (existingImplementer) {
8842
+ {
8843
+ assertConsistentAbstractFieldShape(abstractType, field.name, existingImplementer, contribution);
8844
+ }
8845
+ return;
8846
+ }
8847
+ implementerFields.set(field.name, contribution);
8848
+ let abstractSchema = this._schemas.get(abstractType);
8849
+ if (!abstractSchema) {
8850
+ abstractSchema = {
8851
+ original: {
8852
+ legacy: true,
8853
+ identity: {
8854
+ kind: '@id',
8855
+ name: 'id'
8856
+ },
8857
+ type: abstractType,
8858
+ fields: []
8859
+ },
8860
+ finalized: true,
8861
+ fields: new Map(),
8862
+ cacheFields: new Map(),
8863
+ relationships: {},
8864
+ attributes: {},
8865
+ traits: new Set()
8866
+ };
8867
+ this._schemas.set(abstractType, abstractSchema);
8868
+ }
8869
+ const existingAbstractField = abstractSchema.fields.get(field.name);
8870
+ if (existingAbstractField) {
8871
+ {
8872
+ assertConsistentAbstractFieldShape(abstractType, field.name, {
8873
+ field: existingAbstractField,
8874
+ source: abstractType
8875
+ }, contribution);
8876
+ }
8877
+ return;
8878
+ }
8879
+ abstractSchema.fields.set(field.name, abstractField);
8880
+ abstractSchema.relationships[field.name] = abstractField;
8881
+
8882
+ // If the schema is mid-finalization (traits pending), finalizeResource
8883
+ // will recompute cacheFields from the current `fields` map anyway; avoid
8884
+ // doing so prematurely from a partially-resolved set of fields.
8885
+ if (abstractSchema.finalized) {
8886
+ abstractSchema.cacheFields = getCacheFields(abstractSchema);
8887
+ }
8620
8888
  }
8621
8889
 
8622
8890
  /**
@@ -8875,9 +9143,33 @@ function cloneResponse(response, overrides) {
8875
9143
  const props = cloneResponseProperties(response);
8876
9144
  return new Response(response.body, Object.assign(props, overrides));
8877
9145
  }
9146
+ function withDateHeader(response) {
9147
+ const headers = new Headers(response.headers);
9148
+ headers.set('date', new Date().toUTCString());
9149
+ return cloneResponse(response, {
9150
+ headers
9151
+ });
9152
+ }
9153
+
9154
+ // Detects Mirage (or another Pretender-based fetch mock), whose `Response`
9155
+ // may (unlike a spec-compliant `fetch()`, whose Response headers are always
9156
+ // immutable) allow header mutation and may not support streaming reads.
9157
+ //
9158
+ // This is inherently a heuristic - many legitimate tools (APM/monitoring
9159
+ // agents, browser extensions, polyfills) also patch `window.fetch`, and can
9160
+ // produce false positives. `setWarpDriveIsMaybeMirage(value)` lets an app
9161
+ // declare the answer explicitly (`true` or `false`), overriding the
9162
+ // heuristic in either direction; leave it unset to keep the default
9163
+ // detection. See https://github.com/warp-drive-data/warp-drive/issues/10535
8878
9164
  let IS_MAYBE_MIRAGE = () => false;
8879
9165
  {
8880
- IS_MAYBE_MIRAGE = () => Boolean(typeof window !== 'undefined' && (window.server?.pretender || window.fetch.toString().replace(/\s+/g, '') !== 'function fetch() { [native code] }'.replace(/\s+/g, '')));
9166
+ IS_MAYBE_MIRAGE = () => {
9167
+ const override = getRuntimeConfig().mirage;
9168
+ if (override !== undefined) {
9169
+ return override;
9170
+ }
9171
+ return Boolean(typeof window !== 'undefined' && (window.server?.pretender || window.fetch.toString().replace(/\s+/g, '') !== 'function fetch() { [native code] }'.replace(/\s+/g, '')));
9172
+ };
8881
9173
  }
8882
9174
  const MUTATION_OPS = new Set(['updateRecord', 'createRecord', 'deleteRecord']);
8883
9175
  const ERROR_STATUS_CODE_FOR = new Map([[400, 'Bad Request'], [401, 'Unauthorized'], [402, 'Payment Required'], [403, 'Forbidden'], [404, 'Not Found'], [405, 'Method Not Allowed'], [406, 'Not Acceptable'], [407, 'Proxy Authentication Required'], [408, 'Request Timeout'], [409, 'Conflict'], [410, 'Gone'], [411, 'Length Required'], [412, 'Precondition Failed'], [413, 'Payload Too Large'], [414, 'URI Too Long'], [415, 'Unsupported Media Type'], [416, 'Range Not Satisfiable'], [417, 'Expectation Failed'], [419, 'Page Expired'], [420, 'Enhance Your Calm'], [421, 'Misdirected Request'], [422, 'Unprocessable Entity'], [423, 'Locked'], [424, 'Failed Dependency'], [425, 'Too Early'], [426, 'Upgrade Required'], [428, 'Precondition Required'], [429, 'Too Many Requests'], [430, 'Request Header Fields Too Large'], [431, 'Request Header Fields Too Large'], [450, 'Blocked By Windows Parental Controls'], [451, 'Unavailable For Legal Reasons'], [500, 'Internal Server Error'], [501, 'Not Implemented'], [502, 'Bad Gateway'], [503, 'Service Unavailable'], [504, 'Gateway Timeout'], [505, 'HTTP Version Not Supported'], [506, 'Variant Also Negotiates'], [507, 'Insufficient Storage'], [508, 'Loop Detected'], [509, 'Bandwidth Limit Exceeded'], [510, 'Not Extended'], [511, 'Network Authentication Required']]);
@@ -8929,13 +9221,15 @@ const Fetch = {
8929
9221
  const isMutationOp = Boolean(op && MUTATION_OPS.has(op));
8930
9222
  if (!isError && !isMutationOp && response.status !== 204 && !response.headers.has('date')) {
8931
9223
  if (IS_MAYBE_MIRAGE()) {
8932
- response.headers.set('date', new Date().toUTCString());
9224
+ try {
9225
+ response.headers.set('date', new Date().toUTCString());
9226
+ } catch {
9227
+ // IS_MAYBE_MIRAGE() was a false positive, or the mock's headers
9228
+ // are (correctly) immutable - fall back to cloning.
9229
+ response = withDateHeader(response);
9230
+ }
8933
9231
  } else {
8934
- const headers = new Headers(response.headers);
8935
- headers.set('date', new Date().toUTCString());
8936
- response = cloneResponse(response, {
8937
- headers
8938
- });
9232
+ response = withDateHeader(response);
8939
9233
  }
8940
9234
  }
8941
9235
  context.setResponse(response);
@@ -9330,6 +9624,12 @@ class RequestManager {
9330
9624
  globalThis.getWarpDriveRuntimeConfig = getRuntimeConfig;
9331
9625
  }
9332
9626
 
9627
+ // Mirage (or another Pretender-based fetch mock) may be used outside of a
9628
+ // `TESTING` build (e.g. `ember serve` in dev mode), so this override is
9629
+ // exposed unconditionally rather than gated to the `TESTING` env.
9630
+ // @ts-expect-error adding to globalThis
9631
+ globalThis.setWarpDriveIsMaybeMirage = setIsMaybeMirage;
9632
+
9333
9633
  /**
9334
9634
  * Options for setting up a Store instance with `useRecommendedStore`.
9335
9635
  */
@@ -9350,7 +9650,29 @@ class RequestManager {
9350
9650
  */
9351
9651
  function useRecommendedStore(options, StoreKlass = Store) {
9352
9652
  return class AppStore extends StoreKlass {
9353
- requestManager = new RequestManager().use([...(options.handlers ?? []), Fetch]).useCache(CacheHandler);
9653
+ constructor(createArgs) {
9654
+ super(createArgs);
9655
+ // installed via defineProperty (rather than a class field/accessor) so that
9656
+ // this lazy override of the inherited `requestManager` field does not
9657
+ // conflict with the documented pattern of assigning it directly on
9658
+ // consumer-authored Store subclasses. The setter preserves the ability
9659
+ // to replace `requestManager` outright after construction.
9660
+ let requestManager;
9661
+ Object.defineProperty(this, 'requestManager', {
9662
+ configurable: true,
9663
+ enumerable: true,
9664
+ get: () => {
9665
+ if (!requestManager) {
9666
+ const handlers = typeof options.handlers === 'function' ? options.handlers(this) : options.handlers ?? [];
9667
+ requestManager = new RequestManager().use([...handlers, Fetch]).useCache(CacheHandler);
9668
+ }
9669
+ return requestManager;
9670
+ },
9671
+ set: value => {
9672
+ requestManager = value;
9673
+ }
9674
+ });
9675
+ }
9354
9676
  lifetimes = options.policy ?? new DefaultCachePolicy({
9355
9677
  apiCacheHardExpires: 15 * 60 * 1000,
9356
9678
  // 15 minutes