@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.
- package/declarations/graph/-private/-state.d.ts +1 -0
- package/declarations/index.d.ts +10 -1
- package/declarations/reactive/-private/default-mode.d.ts +22 -9
- package/declarations/reactive/-private/kind/belongs-to-field.d.ts +3 -0
- package/declarations/types/runtime.d.ts +16 -0
- package/declarations/types/schema/fields.d.ts +40 -12
- package/dist/graph/-private.js +67 -52
- package/dist/{index-CFwnjldF.js → index-LI2ckwf9.js} +332 -10
- package/dist/index.js +1 -1
- package/dist/reactive.js +1 -1
- package/dist/store/-private.js +1 -1
- package/dist/types/-private.js +1 -1
- package/dist/types/runtime.js +20 -1
- package/dist/unpkg/dev/graph/-private.js +67 -52
- package/dist/unpkg/dev/{index-BZRBBa34.js → index-DU_HLpg-.js} +332 -10
- package/dist/unpkg/dev/index.js +1 -1
- package/dist/unpkg/dev/reactive.js +1 -1
- package/dist/unpkg/dev/store/-private.js +2 -2
- package/dist/unpkg/dev/types/-private.js +1 -1
- package/dist/unpkg/dev/types/runtime.js +20 -1
- package/dist/unpkg/dev-deprecated/graph/-private.js +67 -52
- package/dist/unpkg/dev-deprecated/{index-Bu52tQ9I.js → index-BYFUHVPd.js} +332 -10
- package/dist/unpkg/dev-deprecated/index.js +1 -1
- package/dist/unpkg/dev-deprecated/reactive.js +1 -1
- package/dist/unpkg/dev-deprecated/store/-private.js +1 -1
- package/dist/unpkg/dev-deprecated/types/-private.js +1 -1
- package/dist/unpkg/dev-deprecated/types/runtime.js +20 -1
- package/dist/unpkg/prod/{-leaked-BHLmJyaw.js → -leaked-DgjQ5X55.js} +2 -2
- package/dist/unpkg/prod/configure.js +1 -1
- package/dist/unpkg/prod/graph/-private.js +17 -25
- package/dist/unpkg/prod/{schema-BdC3DirM.js → index-C-AhVafS.js} +2383 -73
- package/dist/unpkg/prod/index.js +5 -483
- package/dist/unpkg/prod/{promise-cache-DIT8Ypjq.js → promise-cache-DUblkX-U.js} +1 -1
- package/dist/unpkg/prod/reactive.js +3 -29
- package/dist/unpkg/prod/{request-BrJSCG6r.js → request-eb7Zm-oS.js} +1 -1
- package/dist/unpkg/prod/request.js +2 -2
- package/dist/unpkg/prod/signals/-leaked.js +1 -1
- package/dist/unpkg/prod/store/-private.js +2 -3
- package/dist/unpkg/prod/types/-private.js +1 -1
- package/dist/unpkg/prod/types/runtime.js +20 -1
- package/dist/unpkg/prod-deprecated/{-leaked-D_5Yrs5c.js → -leaked-BNVttukY.js} +1 -1
- package/dist/unpkg/prod-deprecated/configure.js +1 -1
- package/dist/unpkg/prod-deprecated/graph/-private.js +17 -25
- package/dist/unpkg/prod-deprecated/{schema-h0nuUTCj.js → index-CplZl2hv.js} +1035 -7
- package/dist/unpkg/prod-deprecated/index.js +5 -483
- package/dist/unpkg/prod-deprecated/{promise-cache-DIT8Ypjq.js → promise-cache-DUblkX-U.js} +1 -1
- package/dist/unpkg/prod-deprecated/reactive.js +2 -3
- package/dist/unpkg/prod-deprecated/{request-BrJSCG6r.js → request-eb7Zm-oS.js} +1 -1
- package/dist/unpkg/prod-deprecated/request.js +2 -2
- package/dist/unpkg/prod-deprecated/signals/-leaked.js +1 -1
- package/dist/unpkg/prod-deprecated/store/-private.js +1 -2
- package/dist/unpkg/prod-deprecated/types/-private.js +1 -1
- package/dist/unpkg/prod-deprecated/types/runtime.js +20 -1
- package/package.json +3 -3
- package/dist/unpkg/prod/handler-BJogFbfS.js +0 -1619
- package/dist/unpkg/prod-deprecated/handler-kGR8zguj.js +0 -334
- 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
|
-
|
|
59
|
-
|
|
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('
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2286
|
-
|
|
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 = () =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|