@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
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { d as defineGate, a as withSignalStore, n as notifyInternalSignal, p as peekInternalSignal, 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 } from "./-leaked-BHLmJyaw.js";
|
|
2
|
-
import { EnableHydration, STRUCTURED } from './types/request.js';
|
|
3
1
|
import { D as Destroy, C as Context, S as SOURCE, a as Checkout, b as Commit } from "./-private-sql1_mdx.js";
|
|
4
|
-
import {
|
|
5
|
-
import { setTransient, peekTransient, getOrSetGlobal } from './types/-private.js';
|
|
6
|
-
import './types/
|
|
7
|
-
import {
|
|
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 } from "./-leaked-DgjQ5X55.js";
|
|
3
|
+
import { setTransient, peekTransient, getOrSetGlobal, peekUniversalTransient, setUniversalTransient } from './types/-private.js';
|
|
4
|
+
import { EnableHydration, SkipCache, STRUCTURED } from './types/request.js';
|
|
5
|
+
import { setIsMaybeMirage } from './types/runtime.js';
|
|
6
|
+
import { w as withBrand, c as cloneResponseProperties, I as IS_CACHE_HANDLER, e as executeNextHandler, u as upgradePromise } from "./request-eb7Zm-oS.js";
|
|
7
|
+
import { b as getRequestResult, a as setPromiseResult, c as clearRequestResult } from "./promise-cache-DUblkX-U.js";
|
|
8
|
+
import { DefaultCachePolicy } from './store.js';
|
|
8
9
|
import { CACHE_OWNER } from './types/identifier.js';
|
|
10
|
+
import { isResourceSchema } from './types/schema/fields.js';
|
|
9
11
|
import { RecordStore, Type } from './types/symbols.js';
|
|
10
12
|
function urlFromLink(link) {
|
|
11
13
|
if (typeof link === 'string') return link;
|
|
@@ -1211,6 +1213,212 @@ function unloadRecord(instances, identifier) {
|
|
|
1211
1213
|
}
|
|
1212
1214
|
instances.store._requestCache._clearEntries(identifier);
|
|
1213
1215
|
}
|
|
1216
|
+
function isCacheOperationValue(value) {
|
|
1217
|
+
return value === 'added' || value === 'state' || value === 'updated' || value === 'removed' || value === 'invalidated';
|
|
1218
|
+
}
|
|
1219
|
+
function _unsubscribe(token, cache) {
|
|
1220
|
+
const cacheKey = token.for;
|
|
1221
|
+
if (cacheKey) {
|
|
1222
|
+
const callbacks = cache.get(cacheKey);
|
|
1223
|
+
if (!callbacks) {
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
const index = callbacks.indexOf(token);
|
|
1227
|
+
if (index === -1) {
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
callbacks.splice(index, 1);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
/**
|
|
1235
|
+
* The NotificationManager provides the ability to subscribe to
|
|
1236
|
+
* changes to Cache state.
|
|
1237
|
+
*
|
|
1238
|
+
* This Feature is what allows WarpDrive to create subscriptions that
|
|
1239
|
+
* work with any framework or change-notification system.
|
|
1240
|
+
*
|
|
1241
|
+
* @hideconstructor
|
|
1242
|
+
* @public
|
|
1243
|
+
*/
|
|
1244
|
+
class NotificationManager {
|
|
1245
|
+
/** @internal */
|
|
1246
|
+
|
|
1247
|
+
/** @internal */
|
|
1248
|
+
|
|
1249
|
+
/** @internal */
|
|
1250
|
+
|
|
1251
|
+
/** @internal */
|
|
1252
|
+
|
|
1253
|
+
/** @internal */
|
|
1254
|
+
|
|
1255
|
+
/** @internal */
|
|
1256
|
+
|
|
1257
|
+
constructor(store) {
|
|
1258
|
+
this.store = store;
|
|
1259
|
+
this.isDestroyed = false;
|
|
1260
|
+
this._buffered = new Map();
|
|
1261
|
+
this._hasFlush = false;
|
|
1262
|
+
this._cache = new Map();
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
/**
|
|
1266
|
+
* Subscribe to changes for a given ResourceKey, RequestKey, or addition/removal of any resource
|
|
1267
|
+
* or document.
|
|
1268
|
+
*
|
|
1269
|
+
* ```ts
|
|
1270
|
+
* export type CacheOperation = 'added' | 'removed' | 'updated' | 'state';
|
|
1271
|
+
*
|
|
1272
|
+
* export interface NotificationCallback {
|
|
1273
|
+
* (cacheKey: ResourceKey, notificationType: 'attributes' | 'relationships', key?: string): void;
|
|
1274
|
+
* (cacheKey: ResourceKey, notificationType: 'errors' | 'meta' | 'identity' | 'state'): void;
|
|
1275
|
+
* (cacheKey: ResourceKey, notificationType: NotificationType, key?: string): void;
|
|
1276
|
+
* }
|
|
1277
|
+
* export interface ResourceOperationCallback {
|
|
1278
|
+
* // resource updates
|
|
1279
|
+
* (cacheKey: ResourceKey, notificationType: CacheOperation): void;
|
|
1280
|
+
* }
|
|
1281
|
+
* export interface DocumentOperationCallback {
|
|
1282
|
+
* // document updates
|
|
1283
|
+
* (cacheKey: RequestKey, notificationType: CacheOperation): void;
|
|
1284
|
+
* }
|
|
1285
|
+
* ```
|
|
1286
|
+
*
|
|
1287
|
+
* @public
|
|
1288
|
+
* @return an opaque token to be used with unsubscribe
|
|
1289
|
+
*/
|
|
1290
|
+
|
|
1291
|
+
subscribe(cacheKey, callback) {
|
|
1292
|
+
let callbacks = this._cache.get(cacheKey);
|
|
1293
|
+
// we use the callback as the cancellation token
|
|
1294
|
+
//@ts-expect-error
|
|
1295
|
+
callback.for = cacheKey;
|
|
1296
|
+
if (!callbacks) {
|
|
1297
|
+
callbacks = [];
|
|
1298
|
+
this._cache.set(cacheKey, callbacks);
|
|
1299
|
+
}
|
|
1300
|
+
callbacks.push(callback);
|
|
1301
|
+
return callback;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
/**
|
|
1305
|
+
* remove a previous subscription
|
|
1306
|
+
*
|
|
1307
|
+
* @public
|
|
1308
|
+
*/
|
|
1309
|
+
unsubscribe(token) {
|
|
1310
|
+
if (!this.isDestroyed) {
|
|
1311
|
+
_unsubscribe(token, this._cache);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/**
|
|
1316
|
+
* Custom Caches and Application Code should not call this method directly.
|
|
1317
|
+
*
|
|
1318
|
+
* @private
|
|
1319
|
+
*/
|
|
1320
|
+
|
|
1321
|
+
notify(cacheKey, value, key) {
|
|
1322
|
+
if (this.isDestroyed) {
|
|
1323
|
+
return false;
|
|
1324
|
+
}
|
|
1325
|
+
if (!isResourceKey(cacheKey) && !isRequestKey(cacheKey)) {
|
|
1326
|
+
return false;
|
|
1327
|
+
}
|
|
1328
|
+
const _hasSubscribers = hasSubscribers(this._cache, cacheKey, value);
|
|
1329
|
+
if (_hasSubscribers) {
|
|
1330
|
+
let buffer = this._buffered.get(cacheKey);
|
|
1331
|
+
if (!buffer) {
|
|
1332
|
+
buffer = [];
|
|
1333
|
+
this._buffered.set(cacheKey, buffer);
|
|
1334
|
+
}
|
|
1335
|
+
buffer.push([value, key || null]);
|
|
1336
|
+
if (!this._scheduleNotify()) ;
|
|
1337
|
+
}
|
|
1338
|
+
return _hasSubscribers;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
/** @internal */
|
|
1342
|
+
_onNextFlush(cb) {
|
|
1343
|
+
this._onFlushCB = cb;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
/** @internal */
|
|
1347
|
+
_scheduleNotify() {
|
|
1348
|
+
const asyncFlush = this.store._enableAsyncFlush;
|
|
1349
|
+
if (this._hasFlush) {
|
|
1350
|
+
if (asyncFlush !== false && !willSyncFlushWatchers()) {
|
|
1351
|
+
return false;
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
if (asyncFlush && !willSyncFlushWatchers()) {
|
|
1355
|
+
this._hasFlush = true;
|
|
1356
|
+
return false;
|
|
1357
|
+
}
|
|
1358
|
+
this._flush();
|
|
1359
|
+
return true;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
/** @internal */
|
|
1363
|
+
_flush() {
|
|
1364
|
+
const buffered = this._buffered;
|
|
1365
|
+
if (buffered.size) {
|
|
1366
|
+
this._buffered = new Map();
|
|
1367
|
+
for (const [cacheKey, states] of buffered) {
|
|
1368
|
+
for (let i = 0; i < states.length; i++) {
|
|
1369
|
+
// @ts-expect-error
|
|
1370
|
+
_flushNotification(this._cache, cacheKey, states[i][0], states[i][1]);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
this._hasFlush = false;
|
|
1375
|
+
this._onFlushCB?.();
|
|
1376
|
+
this._onFlushCB = undefined;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
/** @internal */
|
|
1380
|
+
destroy() {
|
|
1381
|
+
this.isDestroyed = true;
|
|
1382
|
+
this._cache.clear();
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
/**
|
|
1387
|
+
* This type exists for internal use only for
|
|
1388
|
+
* where intimate contracts still exist either for
|
|
1389
|
+
* the Test Suite or for Legacy code.
|
|
1390
|
+
*
|
|
1391
|
+
* @private
|
|
1392
|
+
*/
|
|
1393
|
+
|
|
1394
|
+
function _flushNotification(cache, cacheKey, value, key) {
|
|
1395
|
+
// TODO for documents this will need to switch based on Identifier kind
|
|
1396
|
+
if (isCacheOperationValue(value)) {
|
|
1397
|
+
const callbackMap = cache.get(isRequestKey(cacheKey) ? 'document' : 'resource');
|
|
1398
|
+
if (callbackMap) {
|
|
1399
|
+
callbackMap.forEach(cb => {
|
|
1400
|
+
cb(cacheKey, value);
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
const callbacks = cache.get(cacheKey);
|
|
1405
|
+
if (!callbacks || !callbacks.length) {
|
|
1406
|
+
return false;
|
|
1407
|
+
}
|
|
1408
|
+
callbacks.forEach(cb => {
|
|
1409
|
+
// @ts-expect-error overload doesn't narrow within body
|
|
1410
|
+
cb(cacheKey, value, key);
|
|
1411
|
+
});
|
|
1412
|
+
return true;
|
|
1413
|
+
}
|
|
1414
|
+
function hasSubscribers(cache, cacheKey, value) {
|
|
1415
|
+
const hasSubscriber = Boolean(cache.get(cacheKey)?.length);
|
|
1416
|
+
if (hasSubscriber || !isCacheOperationValue(value)) {
|
|
1417
|
+
return hasSubscriber;
|
|
1418
|
+
}
|
|
1419
|
+
const callbackMap = cache.get(isRequestKey(cacheKey) ? 'document' : 'resource');
|
|
1420
|
+
return Boolean(callbackMap?.length);
|
|
1421
|
+
}
|
|
1214
1422
|
function update() {
|
|
1215
1423
|
if (this.isUpdating) {
|
|
1216
1424
|
return this._updatingPromise;
|
|
@@ -1578,7 +1786,7 @@ const ArrayHandler = {
|
|
|
1578
1786
|
return false;
|
|
1579
1787
|
}
|
|
1580
1788
|
const original = target[index];
|
|
1581
|
-
const newIdentifier = extractIdentifierFromRecord$
|
|
1789
|
+
const newIdentifier = extractIdentifierFromRecord$2(value);
|
|
1582
1790
|
// We generate "transactions" whenever a setter method on the array
|
|
1583
1791
|
// is called and might bulk update multiple array cells. Fundamentally,
|
|
1584
1792
|
// all array operations decompose into individual cell replacements.
|
|
@@ -1649,7 +1857,7 @@ function createReactiveResourceArray(options) {
|
|
|
1649
1857
|
// Ensure instanceof works correctly
|
|
1650
1858
|
// Object.setPrototypeOf(IdentifierArray.prototype, Array.prototype);
|
|
1651
1859
|
|
|
1652
|
-
function extractIdentifierFromRecord$
|
|
1860
|
+
function extractIdentifierFromRecord$2(record) {
|
|
1653
1861
|
if (!record) {
|
|
1654
1862
|
return null;
|
|
1655
1863
|
}
|
|
@@ -2447,93 +2655,1169 @@ function isLegacyLiveArray(array) {
|
|
|
2447
2655
|
const context = array[Context];
|
|
2448
2656
|
return context.features !== null && context.features.DEPRECATED_CLASS_NAME === 'LiveArray';
|
|
2449
2657
|
}
|
|
2658
|
+
const Touching = getOrSetGlobal('Touching', Symbol('touching'));
|
|
2659
|
+
const RequestPromise = getOrSetGlobal('RequestPromise', Symbol('promise'));
|
|
2660
|
+
const EMPTY_ARR = [];
|
|
2661
|
+
function hasRecordIdentifier(op) {
|
|
2662
|
+
return 'recordIdentifier' in op;
|
|
2663
|
+
}
|
|
2450
2664
|
|
|
2451
2665
|
/**
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
*/
|
|
2458
|
-
class
|
|
2666
|
+
* The RequestStateService is used to track the state of requests
|
|
2667
|
+
* for fetching or updating known resource identifies that are inflight.
|
|
2668
|
+
*
|
|
2669
|
+
* @hideconstructor
|
|
2670
|
+
* @public
|
|
2671
|
+
*/
|
|
2672
|
+
class RequestStateService {
|
|
2459
2673
|
/** @internal */
|
|
2460
|
-
|
|
2461
|
-
// unsubscribe token given to us by the notification manager
|
|
2674
|
+
_pending = new Map();
|
|
2462
2675
|
/** @internal */
|
|
2463
|
-
|
|
2676
|
+
_done = new Map();
|
|
2464
2677
|
/** @internal */
|
|
2465
|
-
|
|
2678
|
+
_subscriptions = new Map();
|
|
2679
|
+
/** @internal */
|
|
2680
|
+
_toFlush = [];
|
|
2681
|
+
/** @internal */
|
|
2682
|
+
_store;
|
|
2683
|
+
constructor(store) {
|
|
2684
|
+
this._store = store;
|
|
2685
|
+
}
|
|
2686
|
+
/** @internal */
|
|
2687
|
+
_clearEntries(identifier) {
|
|
2688
|
+
this._done.delete(identifier);
|
|
2689
|
+
}
|
|
2690
|
+
/** @internal */
|
|
2691
|
+
_enqueue(promise, queryRequest) {
|
|
2692
|
+
const query = queryRequest.data[0];
|
|
2693
|
+
if (hasRecordIdentifier(query)) {
|
|
2694
|
+
const identifier = query.recordIdentifier;
|
|
2695
|
+
const type = query.op === 'saveRecord' ? 'mutation' : 'query';
|
|
2696
|
+
if (!this._pending.has(identifier)) {
|
|
2697
|
+
this._pending.set(identifier, []);
|
|
2698
|
+
}
|
|
2699
|
+
const request = {
|
|
2700
|
+
state: 'pending',
|
|
2701
|
+
request: queryRequest,
|
|
2702
|
+
type
|
|
2703
|
+
};
|
|
2704
|
+
request[Touching] = [query.recordIdentifier];
|
|
2705
|
+
request[RequestPromise] = promise;
|
|
2706
|
+
this._pending.get(identifier).push(request);
|
|
2707
|
+
this._triggerSubscriptions(request);
|
|
2708
|
+
return promise.then(result => {
|
|
2709
|
+
this._dequeue(identifier, request);
|
|
2710
|
+
const finalizedRequest = {
|
|
2711
|
+
state: 'fulfilled',
|
|
2712
|
+
request: queryRequest,
|
|
2713
|
+
type,
|
|
2714
|
+
response: {
|
|
2715
|
+
data: result
|
|
2716
|
+
}
|
|
2717
|
+
};
|
|
2718
|
+
finalizedRequest[Touching] = request[Touching];
|
|
2719
|
+
this._addDone(finalizedRequest);
|
|
2720
|
+
this._triggerSubscriptions(finalizedRequest);
|
|
2721
|
+
return result;
|
|
2722
|
+
}, error => {
|
|
2723
|
+
this._dequeue(identifier, request);
|
|
2724
|
+
const finalizedRequest = {
|
|
2725
|
+
state: 'rejected',
|
|
2726
|
+
request: queryRequest,
|
|
2727
|
+
type,
|
|
2728
|
+
response: {
|
|
2729
|
+
data: error
|
|
2730
|
+
}
|
|
2731
|
+
};
|
|
2732
|
+
finalizedRequest[Touching] = request[Touching];
|
|
2733
|
+
this._addDone(finalizedRequest);
|
|
2734
|
+
this._triggerSubscriptions(finalizedRequest);
|
|
2735
|
+
throw error;
|
|
2736
|
+
});
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
/** @internal */
|
|
2741
|
+
_triggerSubscriptions(req) {
|
|
2742
|
+
if (req.state === 'pending') {
|
|
2743
|
+
this._flushRequest(req);
|
|
2744
|
+
return;
|
|
2745
|
+
}
|
|
2746
|
+
this._toFlush.push(req);
|
|
2747
|
+
if (this._toFlush.length === 1) {
|
|
2748
|
+
this._store.notifications._onNextFlush(() => {
|
|
2749
|
+
this._flush();
|
|
2750
|
+
});
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2466
2754
|
/** @internal */
|
|
2755
|
+
_flush() {
|
|
2756
|
+
this._toFlush.forEach(req => {
|
|
2757
|
+
this._flushRequest(req);
|
|
2758
|
+
});
|
|
2759
|
+
this._toFlush = [];
|
|
2760
|
+
}
|
|
2467
2761
|
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
if (
|
|
2473
|
-
|
|
2762
|
+
/** @internal */
|
|
2763
|
+
_flushRequest(req) {
|
|
2764
|
+
req[Touching].forEach(identifier => {
|
|
2765
|
+
const subscriptions = this._subscriptions.get(identifier);
|
|
2766
|
+
if (subscriptions) {
|
|
2767
|
+
subscriptions.forEach(callback => callback(req));
|
|
2474
2768
|
}
|
|
2475
2769
|
});
|
|
2476
2770
|
}
|
|
2477
2771
|
|
|
2478
2772
|
/** @internal */
|
|
2479
|
-
|
|
2480
|
-
this.
|
|
2773
|
+
_dequeue(identifier, request) {
|
|
2774
|
+
const pending = this._pending.get(identifier);
|
|
2775
|
+
this._pending.set(identifier, pending.filter(req => req !== request));
|
|
2481
2776
|
}
|
|
2482
|
-
|
|
2483
|
-
|
|
2777
|
+
|
|
2778
|
+
/** @internal */
|
|
2779
|
+
_addDone(request) {
|
|
2780
|
+
request[Touching].forEach(identifier => {
|
|
2781
|
+
// TODO add support for multiple
|
|
2782
|
+
const requestDataOp = request.request.data[0].op;
|
|
2783
|
+
let requests = this._done.get(identifier);
|
|
2784
|
+
if (requests) {
|
|
2785
|
+
requests = requests.filter(req => {
|
|
2786
|
+
// TODO add support for multiple
|
|
2787
|
+
let data;
|
|
2788
|
+
if (Array.isArray(req.request.data)) {
|
|
2789
|
+
data = req.request.data[0];
|
|
2790
|
+
} else {
|
|
2791
|
+
data = req.request.data;
|
|
2792
|
+
}
|
|
2793
|
+
return data.op !== requestDataOp;
|
|
2794
|
+
});
|
|
2795
|
+
}
|
|
2796
|
+
requests = requests || [];
|
|
2797
|
+
requests.push(request);
|
|
2798
|
+
this._done.set(identifier, requests);
|
|
2799
|
+
});
|
|
2484
2800
|
}
|
|
2485
2801
|
|
|
2486
2802
|
/**
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2803
|
+
* Subscribe to requests for a given resource identity.
|
|
2804
|
+
*
|
|
2805
|
+
* The callback will receive the current state of the request.
|
|
2806
|
+
*
|
|
2807
|
+
* ```ts
|
|
2808
|
+
* interface RequestState {
|
|
2809
|
+
* state: 'pending' | 'fulfilled' | 'rejected';
|
|
2810
|
+
* type: 'query' | 'mutation';
|
|
2811
|
+
* request: Request;
|
|
2812
|
+
* response?: { data: unknown };
|
|
2813
|
+
* }
|
|
2814
|
+
* ```
|
|
2815
|
+
*
|
|
2816
|
+
* Note: It should be considered dangerous to use this API for more than simple
|
|
2817
|
+
* state derivation or debugging. The `request` and `response` properties are poorly
|
|
2818
|
+
* spec'd and may change unexpectedly when shifting what Handlers are in use or how
|
|
2819
|
+
* requests are issued from the Store.
|
|
2820
|
+
*
|
|
2821
|
+
* We expect to revisit this API in the near future as we continue to refine the
|
|
2822
|
+
* RequestManager ergonomics, as a simpler but more powerful direct integration
|
|
2823
|
+
* with the RequestManager for these purposes is likely to be a better long-term
|
|
2824
|
+
* design.
|
|
2825
|
+
*
|
|
2826
|
+
* @public
|
|
2827
|
+
* @param {ResourceKey} identifier
|
|
2828
|
+
* @param {(state: RequestCacheRequestState) => void} callback
|
|
2829
|
+
*/
|
|
2830
|
+
subscribeForRecord(identifier, callback) {
|
|
2831
|
+
let subscriptions = this._subscriptions.get(identifier);
|
|
2832
|
+
if (!subscriptions) {
|
|
2833
|
+
subscriptions = [];
|
|
2834
|
+
this._subscriptions.set(identifier, subscriptions);
|
|
2835
|
+
}
|
|
2836
|
+
subscriptions.push(callback);
|
|
2502
2837
|
}
|
|
2503
2838
|
|
|
2504
2839
|
/**
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
@public
|
|
2514
|
-
@return The identifier of the record.
|
|
2515
|
-
*/
|
|
2516
|
-
identifier() {
|
|
2517
|
-
return this.___identifier;
|
|
2840
|
+
* Retrieve all active requests for a given resource identity.
|
|
2841
|
+
*
|
|
2842
|
+
* @public
|
|
2843
|
+
* @param {ResourceKey} identifier
|
|
2844
|
+
* @return {RequestCacheRequestState[]} an array of request states for any pending requests for the given identifier
|
|
2845
|
+
*/
|
|
2846
|
+
getPendingRequestsForRecord(identifier) {
|
|
2847
|
+
return this._pending.get(identifier) || EMPTY_ARR;
|
|
2518
2848
|
}
|
|
2519
2849
|
|
|
2520
2850
|
/**
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
return
|
|
2851
|
+
* Retrieve the last completed request for a given resource identity.
|
|
2852
|
+
*
|
|
2853
|
+
* @public
|
|
2854
|
+
* @param {ResourceKey} identifier
|
|
2855
|
+
* @return {RequestCacheRequestState | null} the state of the most recent request for the given identifier
|
|
2856
|
+
*/
|
|
2857
|
+
getLastRequestForRecord(identifier) {
|
|
2858
|
+
const requests = this._done.get(identifier);
|
|
2859
|
+
if (requests) {
|
|
2860
|
+
return requests[requests.length - 1];
|
|
2861
|
+
}
|
|
2862
|
+
return null;
|
|
2533
2863
|
}
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
/**
|
|
2867
|
+
* This type exists for internal use only for
|
|
2868
|
+
* where intimate contracts still exist either for
|
|
2869
|
+
* the Test Suite or for Legacy code.
|
|
2870
|
+
*
|
|
2871
|
+
* @private
|
|
2872
|
+
*/
|
|
2873
|
+
|
|
2874
|
+
// this import location is deprecated but breaks in 4.8 and older
|
|
2875
|
+
|
|
2876
|
+
// `AwaitedKeys` is needed here to resolve any promise types like `PromiseBelongsTo`.
|
|
2877
|
+
|
|
2878
|
+
/**
|
|
2879
|
+
* Currently only records that extend object can be created via
|
|
2880
|
+
* store.createRecord. This is a limitation of the current API,
|
|
2881
|
+
* but can be worked around by creating a new identifier, running
|
|
2882
|
+
* the cache.clientDidCreate method, and then peeking the record
|
|
2883
|
+
* for the identifier.
|
|
2884
|
+
*
|
|
2885
|
+
* To assign primary key to a record during creation, only `id` will
|
|
2886
|
+
* work correctly for `store.createRecord`, other primary key may be
|
|
2887
|
+
* handled by updating the record after creation or using the flow
|
|
2888
|
+
* described above.
|
|
2889
|
+
*
|
|
2890
|
+
* TODO: These are limitations we want to (and can) address. If you
|
|
2891
|
+
* have need of lifting these limitations, please open an issue.
|
|
2892
|
+
*
|
|
2893
|
+
*/
|
|
2894
|
+
|
|
2895
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2896
|
+
|
|
2897
|
+
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
|
2898
|
+
const EmptyClass = class {
|
|
2899
|
+
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
|
2900
|
+
constructor(args) {}
|
|
2901
|
+
};
|
|
2902
|
+
const _BaseClass = EmptyClass;
|
|
2903
|
+
const BaseClass = _BaseClass.default ? _BaseClass.default : _BaseClass;
|
|
2904
|
+
/**
|
|
2905
|
+
* ```ts
|
|
2906
|
+
* import { Store } from '@warp-drive/core';
|
|
2907
|
+
* ```
|
|
2908
|
+
*
|
|
2909
|
+
* The `Store` is the central piece of the ***Warp*Drive** experience. It connects
|
|
2910
|
+
* requests for data with schemas, caching and reactivity.
|
|
2911
|
+
*
|
|
2912
|
+
* While it's easy to use ***just*** ***Warp*Drive**'s request management, most projects will find they
|
|
2913
|
+
* require far more than basic fetch management. For this reason it's often best to start with a `Store`
|
|
2914
|
+
* even when you aren't sure yet.
|
|
2915
|
+
*
|
|
2916
|
+
* Most projects will only have a single `Store`, though using multiple distinct stores
|
|
2917
|
+
* is possible.
|
|
2918
|
+
*
|
|
2919
|
+
* @public
|
|
2920
|
+
* @hideconstructor
|
|
2921
|
+
*/
|
|
2922
|
+
class Store extends BaseClass {
|
|
2923
|
+
/** @internal */
|
|
2534
2924
|
|
|
2535
2925
|
/**
|
|
2536
|
-
|
|
2926
|
+
* Provides access to the {@link NotificationManager} associated
|
|
2927
|
+
* with this Store instance.
|
|
2928
|
+
*
|
|
2929
|
+
* The NotificationManager can be used to subscribe to
|
|
2930
|
+
* changes to the cache.
|
|
2931
|
+
*
|
|
2932
|
+
* @public
|
|
2933
|
+
*/
|
|
2934
|
+
|
|
2935
|
+
/**
|
|
2936
|
+
* Provides access to the SchemaService instance
|
|
2937
|
+
* for this Store instance.
|
|
2938
|
+
*
|
|
2939
|
+
* The SchemaService can be used to query for
|
|
2940
|
+
* information about the schema of a resource.
|
|
2941
|
+
*
|
|
2942
|
+
* @public
|
|
2943
|
+
*/
|
|
2944
|
+
get schema() {
|
|
2945
|
+
if (!this._schema) {
|
|
2946
|
+
this._schema = this.createSchemaService();
|
|
2947
|
+
}
|
|
2948
|
+
return this._schema;
|
|
2949
|
+
}
|
|
2950
|
+
/** @internal */
|
|
2951
|
+
|
|
2952
|
+
/**
|
|
2953
|
+
* Provides access to the CacheKeyManager
|
|
2954
|
+
* for this store.
|
|
2955
|
+
*
|
|
2956
|
+
* The CacheKeyManager can be used to generate or
|
|
2957
|
+
* retrieve a stable unique CacheKey for any resource
|
|
2958
|
+
* or request.
|
|
2959
|
+
*
|
|
2960
|
+
* @public
|
|
2961
|
+
*/
|
|
2962
|
+
|
|
2963
|
+
/**
|
|
2964
|
+
* Provides access to the {@link RequestManager} instance associated
|
|
2965
|
+
* with this Store instance.
|
|
2966
|
+
*
|
|
2967
|
+
* See also:
|
|
2968
|
+
* - {@link Fetch}
|
|
2969
|
+
* - {@link CacheHandlerInterface | CacheHandler (Interface)}
|
|
2970
|
+
* - {@link CacheHandler | CacheHandler (Class)}
|
|
2971
|
+
*
|
|
2972
|
+
* ```ts
|
|
2973
|
+
* import { CacheHandler, Fetch, RequestManager, Store } from '@warp-drive/core';
|
|
2974
|
+
*
|
|
2975
|
+
* class AppStore extends Store {
|
|
2976
|
+
* requestManager = new RequestManager()
|
|
2977
|
+
* .use([Fetch])
|
|
2978
|
+
* .useCache(CacheHandler);
|
|
2979
|
+
* }
|
|
2980
|
+
* ```
|
|
2981
|
+
*
|
|
2982
|
+
* @public
|
|
2983
|
+
*/
|
|
2984
|
+
|
|
2985
|
+
/**
|
|
2986
|
+
* A Property which an App may set to provide a CachePolicy
|
|
2987
|
+
* to control when a cached request becomes stale.
|
|
2988
|
+
*
|
|
2989
|
+
* Note, when defined, these methods will only be invoked if a
|
|
2990
|
+
* cache key exists for the request, either because the request
|
|
2991
|
+
* contains `cacheOptions.key` or because the {@link CacheKeyManager}
|
|
2992
|
+
* was able to generate a key for the request using the configured
|
|
2993
|
+
* {@link setIdentifierGenerationMethod | generation method}.
|
|
2994
|
+
*
|
|
2995
|
+
* `isSoftExpired` will only be invoked if `isHardExpired` returns `false`.
|
|
2996
|
+
*
|
|
2997
|
+
* ```ts
|
|
2998
|
+
* store.lifetimes = {
|
|
2999
|
+
* // make the request and ignore the current cache state
|
|
3000
|
+
* isHardExpired(key: RequestKey): boolean {
|
|
3001
|
+
* return false;
|
|
3002
|
+
* }
|
|
3003
|
+
*
|
|
3004
|
+
* // make the request in the background if true, return cache state
|
|
3005
|
+
* isSoftExpired(key: RequestKey): boolean {
|
|
3006
|
+
* return false;
|
|
3007
|
+
* }
|
|
3008
|
+
* }
|
|
3009
|
+
* ```
|
|
3010
|
+
*
|
|
3011
|
+
* @public
|
|
3012
|
+
*/
|
|
3013
|
+
|
|
3014
|
+
// Private
|
|
3015
|
+
/** @internal */
|
|
3016
|
+
|
|
3017
|
+
/** @internal */
|
|
3018
|
+
|
|
3019
|
+
/** @internal */
|
|
3020
|
+
|
|
3021
|
+
/** @internal */
|
|
3022
|
+
|
|
3023
|
+
/** @internal */
|
|
3024
|
+
|
|
3025
|
+
/**
|
|
3026
|
+
* Async flush buffers notifications until flushed
|
|
3027
|
+
* by finalization of a future configured by store.request
|
|
3028
|
+
*
|
|
3029
|
+
* This is useful for ensuring that notifications are delivered
|
|
3030
|
+
* prior to the promise resolving but without risk of promise
|
|
3031
|
+
* interleaving.
|
|
3032
|
+
*
|
|
3033
|
+
* @internal
|
|
3034
|
+
*/
|
|
3035
|
+
|
|
3036
|
+
/**
|
|
3037
|
+
* Available in DEBUG Only
|
|
3038
|
+
* @internal
|
|
3039
|
+
*/
|
|
3040
|
+
|
|
3041
|
+
/** @internal */
|
|
3042
|
+
|
|
3043
|
+
/** @internal */
|
|
3044
|
+
|
|
3045
|
+
/** @private */
|
|
3046
|
+
get isDestroying() {
|
|
3047
|
+
return this._isDestroying;
|
|
3048
|
+
}
|
|
3049
|
+
/** @internal */
|
|
3050
|
+
set isDestroying(value) {
|
|
3051
|
+
this._isDestroying = value;
|
|
3052
|
+
}
|
|
3053
|
+
/** @private */
|
|
3054
|
+
get isDestroyed() {
|
|
3055
|
+
return this._isDestroyed;
|
|
3056
|
+
}
|
|
3057
|
+
/** @internal */
|
|
3058
|
+
set isDestroyed(value) {
|
|
3059
|
+
this._isDestroyed = value;
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3062
|
+
/** @deprecated use {@link Store.cacheKeyManager} */
|
|
3063
|
+
get identifierCache() {
|
|
3064
|
+
return this.cacheKeyManager;
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
/**
|
|
3068
|
+
@private
|
|
3069
|
+
*/
|
|
3070
|
+
constructor(createArgs) {
|
|
3071
|
+
super(createArgs);
|
|
3072
|
+
Object.assign(this, createArgs);
|
|
3073
|
+
this.cacheKeyManager = new CacheKeyManager();
|
|
3074
|
+
this.notifications = new NotificationManager(this);
|
|
3075
|
+
|
|
3076
|
+
// private but maybe useful to be here, somewhat intimate
|
|
3077
|
+
this.recordArrayManager = new RecordArrayManager({
|
|
3078
|
+
store: this
|
|
3079
|
+
});
|
|
3080
|
+
|
|
3081
|
+
// private
|
|
3082
|
+
this._requestCache = new RequestStateService(this);
|
|
3083
|
+
this._instanceCache = new InstanceCache(this);
|
|
3084
|
+
this.isDestroying = false;
|
|
3085
|
+
this.isDestroyed = false;
|
|
3086
|
+
}
|
|
3087
|
+
|
|
3088
|
+
/** @internal */
|
|
3089
|
+
_run(cb) {
|
|
3090
|
+
const _cbs = this._cbs = {};
|
|
3091
|
+
{
|
|
3092
|
+
cb();
|
|
3093
|
+
if (_cbs.coalesce) {
|
|
3094
|
+
_cbs.coalesce();
|
|
3095
|
+
}
|
|
3096
|
+
if (_cbs.sync) {
|
|
3097
|
+
_cbs.sync();
|
|
3098
|
+
}
|
|
3099
|
+
if (_cbs.notify) {
|
|
3100
|
+
_cbs.notify();
|
|
3101
|
+
}
|
|
3102
|
+
this._cbs = null;
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
/**
|
|
3107
|
+
* Executes the callback, ensurng that any work that calls
|
|
3108
|
+
* store._schedule is executed after in the right order.
|
|
3109
|
+
*
|
|
3110
|
+
* When queues already exist, scheduled callbacks will
|
|
3111
|
+
* join the existing queue.
|
|
3112
|
+
*
|
|
3113
|
+
* @internal
|
|
3114
|
+
*/
|
|
3115
|
+
_join(cb) {
|
|
3116
|
+
if (this._cbs) {
|
|
3117
|
+
cb();
|
|
3118
|
+
} else {
|
|
3119
|
+
this._run(cb);
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
|
|
3123
|
+
/** @internal */
|
|
3124
|
+
_schedule(name, cb) {
|
|
3125
|
+
this._cbs[name] = cb;
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
/**
|
|
3129
|
+
* Retrieve the RequestStateService instance
|
|
3130
|
+
* associated with this Store.
|
|
3131
|
+
*
|
|
3132
|
+
* This can be used to query the status of requests
|
|
3133
|
+
* that have been initiated for a given identifier.
|
|
3134
|
+
*
|
|
3135
|
+
* @return {RequestStateService}
|
|
3136
|
+
* @public
|
|
3137
|
+
*/
|
|
3138
|
+
getRequestStateService() {
|
|
3139
|
+
return this._requestCache;
|
|
3140
|
+
}
|
|
3141
|
+
|
|
3142
|
+
/** @internal */
|
|
3143
|
+
_getAllPending() {}
|
|
3144
|
+
|
|
3145
|
+
/**
|
|
3146
|
+
* ::: tip 💡 For a more complete overview see the [Request Guide](/guides/2-requests/1-overview)
|
|
3147
|
+
* :::
|
|
3148
|
+
*
|
|
3149
|
+
* Issue a request via the configured {@link RequestManager},
|
|
3150
|
+
* inserting the response into the {@link Store.cache | cache} and handing
|
|
3151
|
+
* back a {@link Future} which resolves to a {@link ReactiveDocument | ReactiveDocument}
|
|
3152
|
+
*
|
|
3153
|
+
* #### Request Cache Keys
|
|
3154
|
+
*
|
|
3155
|
+
* Only {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods/GET | GET} requests with a url or requests with an explicit
|
|
3156
|
+
* {@link CacheOptions.key | cache key} will have the request result
|
|
3157
|
+
* and document cached.
|
|
3158
|
+
*
|
|
3159
|
+
* The cache key used is {@link RequestInfo.cacheOptions.key | RequestInfo.cacheOptions.key}
|
|
3160
|
+
* if present, falling back to {@link RequestInfo.url}.
|
|
3161
|
+
*
|
|
3162
|
+
* Params are not serialized as part of the cache-key, so
|
|
3163
|
+
* either ensure they are already in the url or utilize
|
|
3164
|
+
* `requestConfig.cacheOptions.key`. For queries issued
|
|
3165
|
+
* via the {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods/POST | POST} method `requestConfig.cacheOptions.key`
|
|
3166
|
+
* MUST be supplied for the document to be cached.
|
|
3167
|
+
*
|
|
3168
|
+
* #### Requesting Without a Cache Key
|
|
3169
|
+
*
|
|
3170
|
+
* Resource data within the request is always updated in the cache,
|
|
3171
|
+
* regardless of whether a cache key is present for the request.
|
|
3172
|
+
*
|
|
3173
|
+
* #### Fulfilling From Cache
|
|
3174
|
+
*
|
|
3175
|
+
* When a cache-key is determined, the request may fulfill
|
|
3176
|
+
* from cache provided the cache is not stale.
|
|
3177
|
+
*
|
|
3178
|
+
* Cache staleness is determined by the configured {@link CachePolicy}
|
|
3179
|
+
* with priority given to the {@link CacheOptions.reload} and
|
|
3180
|
+
* {@link CacheOptions.backgroundReload} on the request if present.
|
|
3181
|
+
*
|
|
3182
|
+
* If the cache data has soft expired or the request asks for a background
|
|
3183
|
+
* reload, the request will fulfill from cache if possible and
|
|
3184
|
+
* make a non-blocking request in the background to update the cache.
|
|
3185
|
+
*
|
|
3186
|
+
* If the cache data has hard expired or the request asks for a reload,
|
|
3187
|
+
* the request will not fulfill from cache and will make a blocking
|
|
3188
|
+
* request to update the cache.
|
|
3189
|
+
*
|
|
3190
|
+
* #### The Response
|
|
3191
|
+
*
|
|
3192
|
+
* The primary difference between {@link RequestManager.request} and `store.request`
|
|
3193
|
+
* is that `store.request` will convert the response into a {@link ReactiveDocument}
|
|
3194
|
+
* containing {@link Store.instantiateRecord | ReactiveResources}.
|
|
3195
|
+
*
|
|
3196
|
+
* @public
|
|
3197
|
+
*/
|
|
3198
|
+
request(requestConfig) {
|
|
3199
|
+
// we lazily set the cache handler when we issue the first request
|
|
3200
|
+
// because constructor doesn't allow for this to run after
|
|
3201
|
+
// the user has had the chance to set the prop.
|
|
3202
|
+
const opts = {
|
|
3203
|
+
store: this,
|
|
3204
|
+
[EnableHydration]: requestConfig[EnableHydration] ?? true
|
|
3205
|
+
};
|
|
3206
|
+
if (requestConfig.records) {
|
|
3207
|
+
const cacheKeyManager = this.cacheKeyManager;
|
|
3208
|
+
opts.records = requestConfig.records.map(r => cacheKeyManager.getOrCreateRecordIdentifier(r));
|
|
3209
|
+
}
|
|
3210
|
+
const request = Object.assign({}, requestConfig, opts);
|
|
3211
|
+
const future = this.requestManager.request(request);
|
|
3212
|
+
future.onFinalize(() => {
|
|
3213
|
+
// skip flush for legacy belongsTo
|
|
3214
|
+
if (requestConfig.op === 'findBelongsTo' && !requestConfig.url) {
|
|
3215
|
+
return;
|
|
3216
|
+
}
|
|
3217
|
+
this.notifications._flush();
|
|
3218
|
+
});
|
|
3219
|
+
return future;
|
|
3220
|
+
}
|
|
3221
|
+
|
|
3222
|
+
/**
|
|
3223
|
+
Creates a new record in the current store.
|
|
3224
|
+
> [!CAUTION]
|
|
3225
|
+
> This should not be used to mock records or to create
|
|
3226
|
+
> a record representing data that could be fetched from
|
|
3227
|
+
> the API.
|
|
3228
|
+
The properties passed to this method are set on
|
|
3229
|
+
the newly created record.
|
|
3230
|
+
For instance: to create a new `post`:
|
|
3231
|
+
```js
|
|
3232
|
+
store.createRecord('post', {
|
|
3233
|
+
title: 'WarpDrive is Stellar!'
|
|
3234
|
+
});
|
|
3235
|
+
```
|
|
3236
|
+
Relationships can be set during create. For instance,
|
|
3237
|
+
to create a new `post` that has an existing user as
|
|
3238
|
+
it's author:
|
|
3239
|
+
```js
|
|
3240
|
+
const user = store.peekRecord('user', '1');
|
|
3241
|
+
store.createRecord('post', {
|
|
3242
|
+
title: 'WarpDrive is Stellar!',
|
|
3243
|
+
user: user
|
|
3244
|
+
});
|
|
3245
|
+
```
|
|
3246
|
+
### lid handling
|
|
3247
|
+
All new records are assigned an `lid` that can be used to handle
|
|
3248
|
+
transactional saves of multiple records, or to link the data to
|
|
3249
|
+
other data in scenarios involving eventual-consistency or remote
|
|
3250
|
+
syncing.
|
|
3251
|
+
```ts
|
|
3252
|
+
const post = store.createRecord('post', {
|
|
3253
|
+
title: 'WarpDrive is Stellar!'
|
|
3254
|
+
});
|
|
3255
|
+
const { lid } = recordIdentifierFor(post);
|
|
3256
|
+
```
|
|
3257
|
+
The `lid` defaults to a uuidv4 string.
|
|
3258
|
+
In order to support receiving knowledge about unpersisted creates
|
|
3259
|
+
from other sources (say a different tab in the same web-browser),
|
|
3260
|
+
createRecord allows for the `lid` to be provided as part of an
|
|
3261
|
+
optional third argument. **If this lid already exists in the store
|
|
3262
|
+
an error will be thrown.**
|
|
3263
|
+
```ts
|
|
3264
|
+
const post = store.createRecord(
|
|
3265
|
+
'post',
|
|
3266
|
+
{ title: 'WarpDrive is Stellar!' },
|
|
3267
|
+
{ lid: '4d47bb88-931f-496e-986d-c4888cef7373' }
|
|
3268
|
+
);
|
|
3269
|
+
```
|
|
3270
|
+
@public
|
|
3271
|
+
@param type the name of the resource
|
|
3272
|
+
@param inputProperties a hash of properties to set on the
|
|
3273
|
+
newly created record.
|
|
3274
|
+
@return a record in the "isNew" state
|
|
3275
|
+
*/
|
|
3276
|
+
|
|
3277
|
+
createRecord(type, inputProperties, context) {
|
|
3278
|
+
// This is wrapped in a `run.join` so that in test environments users do not need to manually wrap
|
|
3279
|
+
// calls to `createRecord`. The run loop usage here is because we batch the joining and updating
|
|
3280
|
+
// of record-arrays via ember's run loop, not our own.
|
|
3281
|
+
//
|
|
3282
|
+
// to remove this, we would need to move to a new `async` API.
|
|
3283
|
+
let record;
|
|
3284
|
+
this._join(() => {
|
|
3285
|
+
const normalizedModelName = normalizeModelName(type);
|
|
3286
|
+
const properties = {
|
|
3287
|
+
...inputProperties
|
|
3288
|
+
};
|
|
3289
|
+
|
|
3290
|
+
// If the passed properties do not include a primary key,
|
|
3291
|
+
// give the adapter an opportunity to generate one. Typically,
|
|
3292
|
+
// client-side ID generators will use something like uuid.js
|
|
3293
|
+
// to avoid conflicts.
|
|
3294
|
+
let id = null;
|
|
3295
|
+
if (properties.id === null || properties.id === undefined) {
|
|
3296
|
+
const adapter = this.adapterFor?.(normalizedModelName, true);
|
|
3297
|
+
if (adapter && adapter.generateIdForRecord) {
|
|
3298
|
+
id = properties.id = coerceId(adapter.generateIdForRecord(this, normalizedModelName, properties));
|
|
3299
|
+
} else {
|
|
3300
|
+
id = properties.id = null;
|
|
3301
|
+
}
|
|
3302
|
+
} else {
|
|
3303
|
+
id = properties.id = coerceId(properties.id);
|
|
3304
|
+
}
|
|
3305
|
+
const resource = {
|
|
3306
|
+
type: normalizedModelName,
|
|
3307
|
+
id
|
|
3308
|
+
};
|
|
3309
|
+
if (resource.id) {
|
|
3310
|
+
this.cacheKeyManager.peekResourceKey(resource);
|
|
3311
|
+
}
|
|
3312
|
+
if (context?.lid) {
|
|
3313
|
+
this.cacheKeyManager.peekResourceKey({
|
|
3314
|
+
lid: context?.lid
|
|
3315
|
+
});
|
|
3316
|
+
resource.lid = context.lid;
|
|
3317
|
+
}
|
|
3318
|
+
const identifier = this.cacheKeyManager.createIdentifierForNewRecord(resource);
|
|
3319
|
+
const cache = this.cache;
|
|
3320
|
+
const createOptions = normalizeProperties(this, identifier, properties);
|
|
3321
|
+
const resultProps = cache.clientDidCreate(identifier, createOptions);
|
|
3322
|
+
record = getNewRecord(this._instanceCache, identifier, resultProps);
|
|
3323
|
+
});
|
|
3324
|
+
return record;
|
|
3325
|
+
}
|
|
3326
|
+
|
|
3327
|
+
/**
|
|
3328
|
+
For symmetry, a record can be deleted via the store.
|
|
3329
|
+
Example
|
|
3330
|
+
```javascript
|
|
3331
|
+
let post = store.createRecord('post', {
|
|
3332
|
+
title: 'Ember is awesome!'
|
|
3333
|
+
});
|
|
3334
|
+
store.deleteRecord(post);
|
|
3335
|
+
```
|
|
3336
|
+
@public
|
|
3337
|
+
@param {unknown} record
|
|
3338
|
+
*/
|
|
3339
|
+
deleteRecord(record) {
|
|
3340
|
+
const identifier = peekResourceKey(record);
|
|
3341
|
+
const cache = this.cache;
|
|
3342
|
+
this._join(() => {
|
|
3343
|
+
cache.setIsDeleted(identifier, true);
|
|
3344
|
+
if (cache.isNew(identifier)) {
|
|
3345
|
+
this._instanceCache.unloadRecord(identifier);
|
|
3346
|
+
}
|
|
3347
|
+
});
|
|
3348
|
+
}
|
|
3349
|
+
|
|
3350
|
+
/**
|
|
3351
|
+
For symmetry, a record can be unloaded via the store.
|
|
3352
|
+
This will cause the record to be destroyed and freed up for garbage collection.
|
|
3353
|
+
Example
|
|
3354
|
+
```javascript
|
|
3355
|
+
const { content: { data: post } } = await store.request(findRecord({ type: 'post', id: '1' }));
|
|
3356
|
+
store.unloadRecord(post);
|
|
3357
|
+
```
|
|
3358
|
+
@public
|
|
3359
|
+
@param {Model} record
|
|
3360
|
+
*/
|
|
3361
|
+
unloadRecord(record) {
|
|
3362
|
+
const identifier = peekResourceKey(record);
|
|
3363
|
+
if (identifier) {
|
|
3364
|
+
this._instanceCache.unloadRecord(identifier);
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
|
|
3368
|
+
/**
|
|
3369
|
+
Get a record by a given type and ID without triggering a fetch.
|
|
3370
|
+
This method will synchronously return the record if it is available in the store,
|
|
3371
|
+
otherwise it will return `null`. A record is available if it has been fetched earlier, or
|
|
3372
|
+
pushed manually into the store.
|
|
3373
|
+
**Example 1**
|
|
3374
|
+
```ts
|
|
3375
|
+
const post = store.peekRecord('post', '1');
|
|
3376
|
+
post.id; // '1'
|
|
3377
|
+
```
|
|
3378
|
+
`peekRecord` can be called with a single identifier argument instead of the combination
|
|
3379
|
+
of `type` (modelName) and `id` as separate arguments. You may recognize this combo as
|
|
3380
|
+
the typical pairing from [JSON:API](https://jsonapi.org/format/#document-resource-object-identification)
|
|
3381
|
+
**Example 2**
|
|
3382
|
+
```ts
|
|
3383
|
+
const post = store.peekRecord({ type: 'post', id: '1' });
|
|
3384
|
+
post.id; // '1'
|
|
3385
|
+
```
|
|
3386
|
+
If you have previously received an lid from an Identifier for this record, you can lookup the record again using
|
|
3387
|
+
just the lid.
|
|
3388
|
+
**Example 3**
|
|
3389
|
+
```js
|
|
3390
|
+
let post = store.peekRecord({ lid });
|
|
3391
|
+
post.id; // '1'
|
|
3392
|
+
```
|
|
3393
|
+
@since 1.13.0
|
|
3394
|
+
@public
|
|
3395
|
+
@param type - either a string representing the modelName or a ResourceIdentifier object containing both the type (a string) and the id (a string) for the record or an lid (a string) of an existing record
|
|
3396
|
+
@param id - optional only if the first param is a ResourceIdentifier, else the string id of the record to be retrieved.
|
|
3397
|
+
*/
|
|
3398
|
+
|
|
3399
|
+
peekRecord(identifier, id) {
|
|
3400
|
+
if (arguments.length === 1 && isMaybeIdentifier(identifier)) {
|
|
3401
|
+
const stableIdentifier = this.cacheKeyManager.peekResourceKey(identifier);
|
|
3402
|
+
const isLoaded = stableIdentifier && this._instanceCache.recordIsLoaded(stableIdentifier);
|
|
3403
|
+
// TODO come up with a better mechanism for determining if we have data and could peek.
|
|
3404
|
+
// this is basically an "are we not empty" query.
|
|
3405
|
+
return isLoaded ? this._instanceCache.getRecord(stableIdentifier) : null;
|
|
3406
|
+
}
|
|
3407
|
+
const type = normalizeModelName(identifier);
|
|
3408
|
+
const normalizedId = ensureStringId(id);
|
|
3409
|
+
const resource = {
|
|
3410
|
+
type,
|
|
3411
|
+
id: normalizedId
|
|
3412
|
+
};
|
|
3413
|
+
const stableIdentifier = this.cacheKeyManager.peekResourceKey(resource);
|
|
3414
|
+
const isLoaded = stableIdentifier && this._instanceCache.recordIsLoaded(stableIdentifier);
|
|
3415
|
+
return isLoaded ? this._instanceCache.getRecord(stableIdentifier) : null;
|
|
3416
|
+
}
|
|
3417
|
+
|
|
3418
|
+
/**
|
|
3419
|
+
This method returns the {@link LegacyLiveArray} that contains all of the
|
|
3420
|
+
known records for a given type in the store. Each ResourceType has only
|
|
3421
|
+
one LiveArray instance, so multiple calls to `peekAll` with the same type
|
|
3422
|
+
will always return the same instance.
|
|
3423
|
+
Note that because it's a LiveArray, the result will contain any
|
|
3424
|
+
locally created records of the type, however, it will not make a
|
|
3425
|
+
request to the backend to retrieve additional records.
|
|
3426
|
+
Example
|
|
3427
|
+
```ts
|
|
3428
|
+
const allPosts = store.peekAll('post');
|
|
3429
|
+
```
|
|
3430
|
+
@since 1.13.0
|
|
3431
|
+
@public
|
|
3432
|
+
@param type the name of the resource
|
|
3433
|
+
*/
|
|
3434
|
+
|
|
3435
|
+
peekAll(type) {
|
|
3436
|
+
return this.recordArrayManager.liveArrayFor(normalizeModelName(type));
|
|
3437
|
+
}
|
|
3438
|
+
|
|
3439
|
+
/**
|
|
3440
|
+
This method unloads all records in the store.
|
|
3441
|
+
It schedules unloading to happen during the next run loop.
|
|
3442
|
+
Optionally you can pass a type which unload all records for a given type.
|
|
3443
|
+
```javascript
|
|
3444
|
+
store.unloadAll();
|
|
3445
|
+
store.unloadAll('post');
|
|
3446
|
+
```
|
|
3447
|
+
@param {String} type the name of the resource
|
|
3448
|
+
@public
|
|
3449
|
+
*/
|
|
3450
|
+
|
|
3451
|
+
unloadAll(type) {
|
|
3452
|
+
this._join(() => {
|
|
3453
|
+
this._enableAsyncFlush = true;
|
|
3454
|
+
if (type === undefined) {
|
|
3455
|
+
this.recordArrayManager.pause();
|
|
3456
|
+
// destroy the graph before unloadAll
|
|
3457
|
+
// since then we avoid churning relationships
|
|
3458
|
+
// during unload
|
|
3459
|
+
this._graph?.identifiers.clear();
|
|
3460
|
+
this.recordArrayManager.clear();
|
|
3461
|
+
this._instanceCache.clear();
|
|
3462
|
+
} else {
|
|
3463
|
+
this._instanceCache.clear(normalizeModelName(type));
|
|
3464
|
+
}
|
|
3465
|
+
this._enableAsyncFlush = null;
|
|
3466
|
+
this.notifications._flush();
|
|
3467
|
+
if (type === undefined) {
|
|
3468
|
+
this.recordArrayManager.resume();
|
|
3469
|
+
}
|
|
3470
|
+
});
|
|
3471
|
+
}
|
|
3472
|
+
|
|
3473
|
+
/**
|
|
3474
|
+
Push some data for a given type into the store.
|
|
3475
|
+
This method expects normalized [JSON API](http://jsonapi.org/) document. This means you have to follow [JSON API specification](http://jsonapi.org/format/) with few minor adjustments:
|
|
3476
|
+
- record's `type` should always be in singular, dasherized form
|
|
3477
|
+
- members (properties) should be camelCased
|
|
3478
|
+
[Your primary data should be wrapped inside `data` property](http://jsonapi.org/format/#document-top-level):
|
|
3479
|
+
```js
|
|
3480
|
+
store.push({
|
|
3481
|
+
data: {
|
|
3482
|
+
// primary data for single record of type `Person`
|
|
3483
|
+
id: '1',
|
|
3484
|
+
type: 'person',
|
|
3485
|
+
attributes: {
|
|
3486
|
+
firstName: 'Daniel',
|
|
3487
|
+
lastName: 'Kmak'
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
});
|
|
3491
|
+
```
|
|
3492
|
+
[Demo.](http://ember-twiddle.com/fb99f18cd3b4d3e2a4c7)
|
|
3493
|
+
`data` property can also hold an array (of records):
|
|
3494
|
+
```js
|
|
3495
|
+
store.push({
|
|
3496
|
+
data: [
|
|
3497
|
+
// an array of records
|
|
3498
|
+
{
|
|
3499
|
+
id: '1',
|
|
3500
|
+
type: 'person',
|
|
3501
|
+
attributes: {
|
|
3502
|
+
firstName: 'Daniel',
|
|
3503
|
+
lastName: 'Kmak'
|
|
3504
|
+
}
|
|
3505
|
+
},
|
|
3506
|
+
{
|
|
3507
|
+
id: '2',
|
|
3508
|
+
type: 'person',
|
|
3509
|
+
attributes: {
|
|
3510
|
+
firstName: 'Tom',
|
|
3511
|
+
lastName: 'Dale'
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
]
|
|
3515
|
+
});
|
|
3516
|
+
```
|
|
3517
|
+
[Demo.](http://ember-twiddle.com/69cdbeaa3702159dc355)
|
|
3518
|
+
There are some typical properties for `JSONAPI` payload:
|
|
3519
|
+
* `id` - mandatory, unique record's key
|
|
3520
|
+
* `type` - mandatory string which matches `model`'s dasherized name in singular form
|
|
3521
|
+
* `attributes` - object which holds data for record attributes - `attr`'s declared in model
|
|
3522
|
+
* `relationships` - object which must contain any of the following properties under each relationships' respective key (example path is `relationships.achievements.data`):
|
|
3523
|
+
- [`links`](http://jsonapi.org/format/#document-links)
|
|
3524
|
+
- [`data`](http://jsonapi.org/format/#document-resource-object-linkage) - place for primary data
|
|
3525
|
+
- [`meta`](http://jsonapi.org/format/#document-meta) - object which contains meta-information about relationship
|
|
3526
|
+
For this model:
|
|
3527
|
+
```js [app/models/person.js]
|
|
3528
|
+
import Model, { attr, hasMany } from '@warp-drive/legacy/model';
|
|
3529
|
+
export default class PersonRoute extends Route {
|
|
3530
|
+
@attr('string') firstName;
|
|
3531
|
+
@attr('string') lastName;
|
|
3532
|
+
@hasMany('person') children;
|
|
3533
|
+
}
|
|
3534
|
+
```
|
|
3535
|
+
To represent the children as IDs:
|
|
3536
|
+
```js
|
|
3537
|
+
{
|
|
3538
|
+
data: {
|
|
3539
|
+
id: '1',
|
|
3540
|
+
type: 'person',
|
|
3541
|
+
attributes: {
|
|
3542
|
+
firstName: 'Tom',
|
|
3543
|
+
lastName: 'Dale'
|
|
3544
|
+
},
|
|
3545
|
+
relationships: {
|
|
3546
|
+
children: {
|
|
3547
|
+
data: [
|
|
3548
|
+
{
|
|
3549
|
+
id: '2',
|
|
3550
|
+
type: 'person'
|
|
3551
|
+
},
|
|
3552
|
+
{
|
|
3553
|
+
id: '3',
|
|
3554
|
+
type: 'person'
|
|
3555
|
+
},
|
|
3556
|
+
{
|
|
3557
|
+
id: '4',
|
|
3558
|
+
type: 'person'
|
|
3559
|
+
}
|
|
3560
|
+
]
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3564
|
+
}
|
|
3565
|
+
```
|
|
3566
|
+
[Demo.](http://ember-twiddle.com/343e1735e034091f5bde)
|
|
3567
|
+
To represent the children relationship as a URL:
|
|
3568
|
+
```js
|
|
3569
|
+
{
|
|
3570
|
+
data: {
|
|
3571
|
+
id: '1',
|
|
3572
|
+
type: 'person',
|
|
3573
|
+
attributes: {
|
|
3574
|
+
firstName: 'Tom',
|
|
3575
|
+
lastName: 'Dale'
|
|
3576
|
+
},
|
|
3577
|
+
relationships: {
|
|
3578
|
+
children: {
|
|
3579
|
+
links: {
|
|
3580
|
+
related: '/people/1/children'
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3586
|
+
```
|
|
3587
|
+
If you're streaming data, or implementing response handling, make sure
|
|
3588
|
+
that you have converted the incoming data into this form.
|
|
3589
|
+
This method can be used both to push in brand new
|
|
3590
|
+
records, as well as to update existing records.
|
|
3591
|
+
See also {@link Cache.patch}
|
|
3592
|
+
@public
|
|
3593
|
+
@param data
|
|
3594
|
+
@return the primary record(s) that created or updated.
|
|
3595
|
+
*/
|
|
3596
|
+
|
|
3597
|
+
push(data) {
|
|
3598
|
+
const pushed = this._push(data, false);
|
|
3599
|
+
if (Array.isArray(pushed)) {
|
|
3600
|
+
return pushed.map(identifier => this._instanceCache.getRecord(identifier));
|
|
3601
|
+
}
|
|
3602
|
+
if (pushed === null) {
|
|
3603
|
+
return null;
|
|
3604
|
+
}
|
|
3605
|
+
return this._instanceCache.getRecord(pushed);
|
|
3606
|
+
}
|
|
3607
|
+
|
|
3608
|
+
/**
|
|
3609
|
+
Push some data in the form of a json-api document into the store,
|
|
3610
|
+
without creating materialized records.
|
|
3611
|
+
@private
|
|
3612
|
+
@return identifiers for the primary records that had data loaded
|
|
3613
|
+
*/
|
|
3614
|
+
_push(jsonApiDoc, asyncFlush) {
|
|
3615
|
+
if (asyncFlush) {
|
|
3616
|
+
this._enableAsyncFlush = true;
|
|
3617
|
+
}
|
|
3618
|
+
let ret;
|
|
3619
|
+
this._join(() => {
|
|
3620
|
+
ret = this.cache.put({
|
|
3621
|
+
content: jsonApiDoc
|
|
3622
|
+
});
|
|
3623
|
+
});
|
|
3624
|
+
this._enableAsyncFlush = null;
|
|
3625
|
+
return 'data' in ret ? ret.data : null;
|
|
3626
|
+
}
|
|
3627
|
+
|
|
3628
|
+
/**
|
|
3629
|
+
* Returns the cache instance associated to this Store, instantiates the Cache
|
|
3630
|
+
* if necessary via `Store.createCache`
|
|
3631
|
+
*
|
|
3632
|
+
* @public
|
|
3633
|
+
*/
|
|
3634
|
+
get cache() {
|
|
3635
|
+
let {
|
|
3636
|
+
cache
|
|
3637
|
+
} = this._instanceCache;
|
|
3638
|
+
if (!cache) {
|
|
3639
|
+
cache = this._instanceCache.cache = this.createCache(this._instanceCache._storeWrapper);
|
|
3640
|
+
}
|
|
3641
|
+
return cache;
|
|
3642
|
+
}
|
|
3643
|
+
|
|
3644
|
+
/** @private */
|
|
3645
|
+
destroy() {
|
|
3646
|
+
if (this.isDestroyed) {
|
|
3647
|
+
// @ember/test-helpers will call destroy multiple times
|
|
3648
|
+
return;
|
|
3649
|
+
}
|
|
3650
|
+
this.isDestroying = true;
|
|
3651
|
+
this._graph?.destroy();
|
|
3652
|
+
this._graph = undefined;
|
|
3653
|
+
this.notifications.destroy();
|
|
3654
|
+
this.recordArrayManager.destroy();
|
|
3655
|
+
this.cacheKeyManager.destroy();
|
|
3656
|
+
this._instanceCache.clear();
|
|
3657
|
+
this.isDestroyed = true;
|
|
3658
|
+
}
|
|
3659
|
+
|
|
3660
|
+
/**
|
|
3661
|
+
* This method
|
|
3662
|
+
*
|
|
3663
|
+
* @private
|
|
3664
|
+
*/
|
|
3665
|
+
static create(args) {
|
|
3666
|
+
return new this(args);
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
|
|
3670
|
+
/**
|
|
3671
|
+
* This type exists for internal use only for
|
|
3672
|
+
* where intimate contracts still exist either for
|
|
3673
|
+
* the Test Suite or for Legacy code.
|
|
3674
|
+
*
|
|
3675
|
+
* @private
|
|
3676
|
+
*/
|
|
3677
|
+
|
|
3678
|
+
/**
|
|
3679
|
+
* Upgrade the type for Store to PrivateStore, which will also
|
|
3680
|
+
* upgrade any associated types to their private equivalents.
|
|
3681
|
+
*
|
|
3682
|
+
* @private
|
|
3683
|
+
*/
|
|
3684
|
+
function assertPrivateStore(store) {}
|
|
3685
|
+
/**
|
|
3686
|
+
* Upgrade the type for Store to PrivateStore, which will also
|
|
3687
|
+
* upgrade any associated types to their private equivalents.
|
|
3688
|
+
*
|
|
3689
|
+
* @private
|
|
3690
|
+
*/
|
|
3691
|
+
function isPrivateStore(store) {
|
|
3692
|
+
return store;
|
|
3693
|
+
}
|
|
3694
|
+
function isMaybeIdentifier(maybeIdentifier) {
|
|
3695
|
+
return Boolean(maybeIdentifier !== null && typeof maybeIdentifier === 'object' && ('id' in maybeIdentifier && 'type' in maybeIdentifier && maybeIdentifier.id && maybeIdentifier.type || maybeIdentifier.lid));
|
|
3696
|
+
}
|
|
3697
|
+
function normalizeProperties(store, identifier, properties) {
|
|
3698
|
+
// assert here
|
|
3699
|
+
if (properties !== undefined) {
|
|
3700
|
+
const {
|
|
3701
|
+
type
|
|
3702
|
+
} = identifier;
|
|
3703
|
+
|
|
3704
|
+
// convert relationship Records to RecordDatas before passing to RecordData
|
|
3705
|
+
const defs = store.schema.fields({
|
|
3706
|
+
type
|
|
3707
|
+
});
|
|
3708
|
+
if (defs.size) {
|
|
3709
|
+
const keys = Object.keys(properties);
|
|
3710
|
+
for (let i = 0; i < keys.length; i++) {
|
|
3711
|
+
const prop = keys[i];
|
|
3712
|
+
const field = defs.get(prop);
|
|
3713
|
+
if (!field) continue;
|
|
3714
|
+
if (field.kind === 'hasMany') {
|
|
3715
|
+
properties[prop] = extractIdentifiersFromRecords$1(properties[prop]);
|
|
3716
|
+
} else if (field.kind === 'belongsTo') {
|
|
3717
|
+
properties[prop] = extractIdentifierFromRecord$1(properties[prop]);
|
|
3718
|
+
}
|
|
3719
|
+
}
|
|
3720
|
+
}
|
|
3721
|
+
}
|
|
3722
|
+
return properties;
|
|
3723
|
+
}
|
|
3724
|
+
function extractIdentifiersFromRecords$1(records) {
|
|
3725
|
+
return records.map(record => extractIdentifierFromRecord$1(record));
|
|
3726
|
+
}
|
|
3727
|
+
function extractIdentifierFromRecord$1(recordOrPromiseRecord) {
|
|
3728
|
+
if (!recordOrPromiseRecord) {
|
|
3729
|
+
return null;
|
|
3730
|
+
}
|
|
3731
|
+
const extract = recordIdentifierFor;
|
|
3732
|
+
return extract(recordOrPromiseRecord);
|
|
3733
|
+
}
|
|
3734
|
+
|
|
3735
|
+
/**
|
|
3736
|
+
A `RecordReference` is a low-level API that allows users and
|
|
3737
|
+
addon authors to perform meta-operations on a record.
|
|
3738
|
+
|
|
3739
|
+
@hideconstructor
|
|
3740
|
+
@public
|
|
3741
|
+
*/
|
|
3742
|
+
class RecordReference {
|
|
3743
|
+
/** @internal */
|
|
3744
|
+
|
|
3745
|
+
// unsubscribe token given to us by the notification manager
|
|
3746
|
+
/** @internal */
|
|
3747
|
+
___token;
|
|
3748
|
+
/** @internal */
|
|
3749
|
+
___identifier;
|
|
3750
|
+
/** @internal */
|
|
3751
|
+
|
|
3752
|
+
constructor(store, identifier) {
|
|
3753
|
+
this.store = store;
|
|
3754
|
+
this.___identifier = identifier;
|
|
3755
|
+
this.___token = store.notifications.subscribe(identifier, (_, bucket, notifiedKey) => {
|
|
3756
|
+
if (bucket === 'identity' || bucket === 'attributes' && notifiedKey === 'id') {
|
|
3757
|
+
this._ref++;
|
|
3758
|
+
}
|
|
3759
|
+
});
|
|
3760
|
+
}
|
|
3761
|
+
|
|
3762
|
+
/** @internal */
|
|
3763
|
+
destroy() {
|
|
3764
|
+
this.store.notifications.unsubscribe(this.___token);
|
|
3765
|
+
}
|
|
3766
|
+
get type() {
|
|
3767
|
+
return this.identifier().type;
|
|
3768
|
+
}
|
|
3769
|
+
|
|
3770
|
+
/**
|
|
3771
|
+
The `id` of the record that this reference refers to.
|
|
3772
|
+
Together, the `type` and `id` properties form a composite key for
|
|
3773
|
+
the identity map.
|
|
3774
|
+
Example
|
|
3775
|
+
```javascript
|
|
3776
|
+
let userRef = store.getReference('user', 1);
|
|
3777
|
+
userRef.id(); // '1'
|
|
3778
|
+
```
|
|
3779
|
+
@public
|
|
3780
|
+
@return The id of the record.
|
|
3781
|
+
*/
|
|
3782
|
+
id() {
|
|
3783
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
3784
|
+
this._ref; // consume the tracked prop
|
|
3785
|
+
return this.___identifier.id;
|
|
3786
|
+
}
|
|
3787
|
+
|
|
3788
|
+
/**
|
|
3789
|
+
The `identifier` of the record that this reference refers to.
|
|
3790
|
+
Together, the `type` and `id` properties form a composite key for
|
|
3791
|
+
the identity map.
|
|
3792
|
+
Example
|
|
3793
|
+
```javascript
|
|
3794
|
+
let userRef = store.getReference('user', 1);
|
|
3795
|
+
userRef.identifier(); // '1'
|
|
3796
|
+
```
|
|
3797
|
+
@public
|
|
3798
|
+
@return The identifier of the record.
|
|
3799
|
+
*/
|
|
3800
|
+
identifier() {
|
|
3801
|
+
return this.___identifier;
|
|
3802
|
+
}
|
|
3803
|
+
|
|
3804
|
+
/**
|
|
3805
|
+
How the reference will be looked up when it is loaded. Currently
|
|
3806
|
+
this always returns `identity` to signify that a record will be
|
|
3807
|
+
loaded by its `type` and `id`.
|
|
3808
|
+
Example
|
|
3809
|
+
```javascript
|
|
3810
|
+
const userRef = store.getReference('user', 1);
|
|
3811
|
+
userRef.remoteType(); // 'identity'
|
|
3812
|
+
```
|
|
3813
|
+
@public
|
|
3814
|
+
*/
|
|
3815
|
+
remoteType() {
|
|
3816
|
+
return 'identity';
|
|
3817
|
+
}
|
|
3818
|
+
|
|
3819
|
+
/**
|
|
3820
|
+
This API allows you to provide a reference with new data. The
|
|
2537
3821
|
simplest usage of this API is similar to `store.push`: you provide a
|
|
2538
3822
|
normalized hash of data and the object represented by the reference
|
|
2539
3823
|
will update.
|
|
@@ -2627,7 +3911,338 @@ class RecordReference {
|
|
|
2627
3911
|
}
|
|
2628
3912
|
}
|
|
2629
3913
|
}
|
|
2630
|
-
defineSignal(RecordReference.prototype, '_ref');
|
|
3914
|
+
defineSignal(RecordReference.prototype, '_ref');
|
|
3915
|
+
const MUTATION_OPS$1 = new Set(['createRecord', 'updateRecord', 'deleteRecord']);
|
|
3916
|
+
function calcShouldFetch(store, request, hasCachedValue, identifier) {
|
|
3917
|
+
const {
|
|
3918
|
+
cacheOptions
|
|
3919
|
+
} = request;
|
|
3920
|
+
return request.op && MUTATION_OPS$1.has(request.op) || cacheOptions?.reload || !hasCachedValue || (store.lifetimes && identifier ? store.lifetimes.isHardExpired(identifier, store) : false);
|
|
3921
|
+
}
|
|
3922
|
+
function calcShouldBackgroundFetch(store, request, willFetch, identifier) {
|
|
3923
|
+
const {
|
|
3924
|
+
cacheOptions
|
|
3925
|
+
} = request;
|
|
3926
|
+
return cacheOptions?.backgroundReload || (store.lifetimes && identifier ? store.lifetimes.isSoftExpired(identifier, store) : false);
|
|
3927
|
+
}
|
|
3928
|
+
function isMutation(request) {
|
|
3929
|
+
return Boolean(request.op && MUTATION_OPS$1.has(request.op));
|
|
3930
|
+
}
|
|
3931
|
+
function isCacheAffecting(document) {
|
|
3932
|
+
if (!isMutation(document.request)) {
|
|
3933
|
+
return true;
|
|
3934
|
+
}
|
|
3935
|
+
// a mutation combined with a 204 has no cache impact when no known records were involved
|
|
3936
|
+
// a createRecord with a 201 with an empty response and no known records should similarly
|
|
3937
|
+
// have no cache impact
|
|
3938
|
+
|
|
3939
|
+
if (document.request.op === 'createRecord' && document.response?.status === 201) {
|
|
3940
|
+
return document.content ? Object.keys(document.content).length > 0 : false;
|
|
3941
|
+
}
|
|
3942
|
+
return document.response?.status !== 204;
|
|
3943
|
+
}
|
|
3944
|
+
function isAggregateError(error) {
|
|
3945
|
+
return error instanceof AggregateError || error.name === 'AggregateError' && Array.isArray(error.errors);
|
|
3946
|
+
}
|
|
3947
|
+
// TODO @runspired, consider if we should deep freeze errors (potentially only in debug) vs cloning them
|
|
3948
|
+
function cloneError(error) {
|
|
3949
|
+
const isAggregate = isAggregateError(error);
|
|
3950
|
+
const cloned = isAggregate ? new AggregateError(structuredClone(error.errors), error.message) : new Error(error.message);
|
|
3951
|
+
cloned.stack = error.stack;
|
|
3952
|
+
cloned.error = error.error;
|
|
3953
|
+
|
|
3954
|
+
// copy over enumerable properties
|
|
3955
|
+
Object.assign(cloned, error);
|
|
3956
|
+
return cloned;
|
|
3957
|
+
}
|
|
3958
|
+
function getPriority(identifier, deduped, priority) {
|
|
3959
|
+
if (identifier) {
|
|
3960
|
+
const existing = deduped.get(identifier);
|
|
3961
|
+
if (existing) {
|
|
3962
|
+
return existing.priority;
|
|
3963
|
+
}
|
|
3964
|
+
}
|
|
3965
|
+
return priority;
|
|
3966
|
+
}
|
|
3967
|
+
|
|
3968
|
+
/**
|
|
3969
|
+
* A CacheHandler that adds support for using an WarpDrive Cache with a RequestManager.
|
|
3970
|
+
*
|
|
3971
|
+
* This handler will only run when a request has supplied a `store` instance. Requests
|
|
3972
|
+
* issued by the store via `store.request()` will automatically have the `store` instance
|
|
3973
|
+
* attached to the request.
|
|
3974
|
+
*
|
|
3975
|
+
* ```ts
|
|
3976
|
+
* requestManager.request({
|
|
3977
|
+
* store: store,
|
|
3978
|
+
* url: '/api/posts',
|
|
3979
|
+
* method: 'GET'
|
|
3980
|
+
* });
|
|
3981
|
+
* ```
|
|
3982
|
+
*
|
|
3983
|
+
* When this handler elects to handle a request, it will return the raw `StructuredDocument`
|
|
3984
|
+
* unless the request has `[EnableHydration]` set to `true`. In this case, the handler will
|
|
3985
|
+
* return a `Document` instance that will automatically update the UI when the cache is updated
|
|
3986
|
+
* in the future and will hydrate any identifiers in the StructuredDocument into Record instances.
|
|
3987
|
+
*
|
|
3988
|
+
* When issuing a request via the store, [EnableHydration] is automatically set to `true`. This
|
|
3989
|
+
* means that if desired you can issue requests that utilize the cache without needing to also
|
|
3990
|
+
* utilize Record instances if desired.
|
|
3991
|
+
*
|
|
3992
|
+
* Said differently, you could elect to issue all requests via a RequestManager, without ever using
|
|
3993
|
+
* the store directly, by setting [EnableHydration] to `true` and providing a store instance. Not
|
|
3994
|
+
* necessarily the most useful thing, but the decoupled nature of the RequestManager and incremental-feature
|
|
3995
|
+
* approach of WarpDrive allows for this flexibility.
|
|
3996
|
+
*
|
|
3997
|
+
* ```ts
|
|
3998
|
+
* import { EnableHydration } from '@warp-drive/core/types/request';
|
|
3999
|
+
*
|
|
4000
|
+
* requestManager.request({
|
|
4001
|
+
* store: store,
|
|
4002
|
+
* url: '/api/posts',
|
|
4003
|
+
* method: 'GET',
|
|
4004
|
+
* [EnableHydration]: true
|
|
4005
|
+
* });
|
|
4006
|
+
*
|
|
4007
|
+
*/
|
|
4008
|
+
const CacheHandler = {
|
|
4009
|
+
request(context, next) {
|
|
4010
|
+
// if we have no cache or no cache-key skip cache handling
|
|
4011
|
+
if (!context.request.store || context.request.cacheOptions?.[SkipCache]) {
|
|
4012
|
+
return next(context.request);
|
|
4013
|
+
}
|
|
4014
|
+
const {
|
|
4015
|
+
store
|
|
4016
|
+
} = context.request;
|
|
4017
|
+
const identifier = store.cacheKeyManager.getOrCreateDocumentIdentifier(context.request);
|
|
4018
|
+
if (identifier) {
|
|
4019
|
+
context.setIdentifier(identifier);
|
|
4020
|
+
}
|
|
4021
|
+
|
|
4022
|
+
// used to dedupe existing requests that match
|
|
4023
|
+
const DEDUPE = store.requestManager._deduped;
|
|
4024
|
+
const activeRequest = identifier && DEDUPE.get(identifier);
|
|
4025
|
+
const peeked = identifier ? store.cache.peekRequest(identifier) : null;
|
|
4026
|
+
|
|
4027
|
+
// determine if we should skip cache
|
|
4028
|
+
if (calcShouldFetch(store, context.request, !!peeked, identifier)) {
|
|
4029
|
+
if (activeRequest) {
|
|
4030
|
+
activeRequest.priority = {
|
|
4031
|
+
blocking: true
|
|
4032
|
+
};
|
|
4033
|
+
return activeRequest.promise;
|
|
4034
|
+
}
|
|
4035
|
+
let promise = fetchContentAndHydrate(next, context, identifier, {
|
|
4036
|
+
blocking: true
|
|
4037
|
+
});
|
|
4038
|
+
if (identifier) {
|
|
4039
|
+
promise = promise.finally(() => {
|
|
4040
|
+
DEDUPE.delete(identifier);
|
|
4041
|
+
store.notifications.notify(identifier, 'state', null);
|
|
4042
|
+
});
|
|
4043
|
+
DEDUPE.set(identifier, {
|
|
4044
|
+
priority: {
|
|
4045
|
+
blocking: true
|
|
4046
|
+
},
|
|
4047
|
+
promise
|
|
4048
|
+
});
|
|
4049
|
+
queueMicrotask(() => {
|
|
4050
|
+
store.notifications.notify(identifier, 'state', null);
|
|
4051
|
+
});
|
|
4052
|
+
}
|
|
4053
|
+
store.requestManager._pending.set(context.id, promise);
|
|
4054
|
+
return promise;
|
|
4055
|
+
}
|
|
4056
|
+
|
|
4057
|
+
// if we have not skipped cache, determine if we should update behind the scenes
|
|
4058
|
+
if (calcShouldBackgroundFetch(store, context.request, false, identifier)) {
|
|
4059
|
+
let promise = activeRequest?.promise || fetchContentAndHydrate(next, context, identifier, {
|
|
4060
|
+
blocking: false
|
|
4061
|
+
});
|
|
4062
|
+
if (identifier && !activeRequest) {
|
|
4063
|
+
promise = promise.finally(() => {
|
|
4064
|
+
DEDUPE.delete(identifier);
|
|
4065
|
+
store.notifications.notify(identifier, 'state', null);
|
|
4066
|
+
});
|
|
4067
|
+
DEDUPE.set(identifier, {
|
|
4068
|
+
priority: {
|
|
4069
|
+
blocking: false
|
|
4070
|
+
},
|
|
4071
|
+
promise
|
|
4072
|
+
});
|
|
4073
|
+
queueMicrotask(() => {
|
|
4074
|
+
store.notifications.notify(identifier, 'state', null);
|
|
4075
|
+
});
|
|
4076
|
+
}
|
|
4077
|
+
store.requestManager._pending.set(context.id, promise);
|
|
4078
|
+
}
|
|
4079
|
+
const shouldHydrate = context.request[EnableHydration] || false;
|
|
4080
|
+
context.setResponse(peeked.response);
|
|
4081
|
+
if ('error' in peeked) {
|
|
4082
|
+
const content = shouldHydrate ? maybeUpdateUiObjects(store, context.request, {
|
|
4083
|
+
shouldHydrate,
|
|
4084
|
+
identifier
|
|
4085
|
+
}, peeked.content) : peeked.content;
|
|
4086
|
+
const newError = cloneError(peeked);
|
|
4087
|
+
newError.content = content;
|
|
4088
|
+
throw newError;
|
|
4089
|
+
}
|
|
4090
|
+
const result = shouldHydrate ? maybeUpdateUiObjects(store, context.request, {
|
|
4091
|
+
shouldHydrate,
|
|
4092
|
+
identifier
|
|
4093
|
+
}, peeked.content) : peeked.content;
|
|
4094
|
+
return result;
|
|
4095
|
+
}
|
|
4096
|
+
};
|
|
4097
|
+
function maybeUpdateUiObjects(store, request, options, document) {
|
|
4098
|
+
const {
|
|
4099
|
+
identifier
|
|
4100
|
+
} = options;
|
|
4101
|
+
if (!document || !options.shouldHydrate) {
|
|
4102
|
+
return document ?? null;
|
|
4103
|
+
}
|
|
4104
|
+
if (identifier) {
|
|
4105
|
+
return store._instanceCache.getDocument(identifier);
|
|
4106
|
+
}
|
|
4107
|
+
|
|
4108
|
+
// if we don't have an identifier, we give the document
|
|
4109
|
+
// its own local cache
|
|
4110
|
+
return createReactiveDocument(store, null, {
|
|
4111
|
+
request,
|
|
4112
|
+
document
|
|
4113
|
+
});
|
|
4114
|
+
}
|
|
4115
|
+
function updateCacheForSuccess(store, request, options, document) {
|
|
4116
|
+
let response = null;
|
|
4117
|
+
if (isMutation(request)) {
|
|
4118
|
+
if (Array.isArray(request.records)) {
|
|
4119
|
+
response = store.cache.didCommit(request.records, document);
|
|
4120
|
+
} else if (request.data?.record) {
|
|
4121
|
+
// legacy fallback, the data option should no longer be used for this
|
|
4122
|
+
response = store.cache.didCommit(request.data.record, document);
|
|
4123
|
+
|
|
4124
|
+
// a mutation combined with a 204 has no cache impact when no known records were involved
|
|
4125
|
+
// a createRecord with a 201 with an empty response and no known records should similarly
|
|
4126
|
+
// have no cache impact
|
|
4127
|
+
} else if (isCacheAffecting(document)) {
|
|
4128
|
+
response = store.cache.put(document);
|
|
4129
|
+
}
|
|
4130
|
+
} else {
|
|
4131
|
+
response = store.cache.put(document);
|
|
4132
|
+
}
|
|
4133
|
+
return maybeUpdateUiObjects(store, request, options, response);
|
|
4134
|
+
}
|
|
4135
|
+
function handleFetchSuccess(store, context, options, document) {
|
|
4136
|
+
const {
|
|
4137
|
+
request
|
|
4138
|
+
} = context;
|
|
4139
|
+
store.requestManager._pending.delete(context.id);
|
|
4140
|
+
store._enableAsyncFlush = true;
|
|
4141
|
+
let response;
|
|
4142
|
+
store._join(() => {
|
|
4143
|
+
response = updateCacheForSuccess(store, request, options, document);
|
|
4144
|
+
});
|
|
4145
|
+
store._enableAsyncFlush = null;
|
|
4146
|
+
if (store.lifetimes?.didRequest) {
|
|
4147
|
+
store.lifetimes.didRequest(context.request, document.response, options.identifier, store);
|
|
4148
|
+
}
|
|
4149
|
+
const finalPriority = getPriority(options.identifier, store.requestManager._deduped, options.priority);
|
|
4150
|
+
if (finalPriority.blocking) {
|
|
4151
|
+
return response;
|
|
4152
|
+
} else {
|
|
4153
|
+
store.notifications._flush();
|
|
4154
|
+
}
|
|
4155
|
+
}
|
|
4156
|
+
function updateCacheForError(store, context, options, error) {
|
|
4157
|
+
let response;
|
|
4158
|
+
if (isMutation(context.request)) {
|
|
4159
|
+
// TODO similar to didCommit we should spec this to be similar to cache.put for handling full response
|
|
4160
|
+
// currently we let the response remain undefiend.
|
|
4161
|
+
const errors = error && error.content && typeof error.content === 'object' && 'errors' in error.content && Array.isArray(error.content.errors) ? error.content.errors : undefined;
|
|
4162
|
+
if (Array.isArray(context.request.records)) {
|
|
4163
|
+
store.cache.commitWasRejected(context.request.records, errors);
|
|
4164
|
+
} else if (context.request.data?.record) {
|
|
4165
|
+
// legacy fallback, the data option should no longer be used for this
|
|
4166
|
+
store.cache.commitWasRejected(context.request.data.record, errors);
|
|
4167
|
+
} else {
|
|
4168
|
+
store.cache.put(error);
|
|
4169
|
+
}
|
|
4170
|
+
} else {
|
|
4171
|
+
response = store.cache.put(error);
|
|
4172
|
+
return maybeUpdateUiObjects(store, context.request, options, response);
|
|
4173
|
+
}
|
|
4174
|
+
}
|
|
4175
|
+
function handleFetchError(store, context, options, error) {
|
|
4176
|
+
store.requestManager._pending.delete(context.id);
|
|
4177
|
+
if (context.request.signal?.aborted) {
|
|
4178
|
+
throw error;
|
|
4179
|
+
}
|
|
4180
|
+
store._enableAsyncFlush = true;
|
|
4181
|
+
let response;
|
|
4182
|
+
store._join(() => {
|
|
4183
|
+
response = updateCacheForError(store, context, options, error);
|
|
4184
|
+
});
|
|
4185
|
+
store._enableAsyncFlush = null;
|
|
4186
|
+
if (options.identifier && store.lifetimes?.didRequest) {
|
|
4187
|
+
store.lifetimes.didRequest(context.request, error.response, options.identifier, store);
|
|
4188
|
+
}
|
|
4189
|
+
if (isMutation(context.request)) {
|
|
4190
|
+
throw error;
|
|
4191
|
+
}
|
|
4192
|
+
const finalPriority = getPriority(options.identifier, store.requestManager._deduped, options.priority);
|
|
4193
|
+
if (finalPriority.blocking) {
|
|
4194
|
+
const newError = cloneError(error);
|
|
4195
|
+
newError.content = response;
|
|
4196
|
+
throw newError;
|
|
4197
|
+
} else {
|
|
4198
|
+
store.notifications._flush();
|
|
4199
|
+
}
|
|
4200
|
+
}
|
|
4201
|
+
function fetchContentAndHydrate(next, context, identifier, priority) {
|
|
4202
|
+
const {
|
|
4203
|
+
store
|
|
4204
|
+
} = context.request;
|
|
4205
|
+
const shouldHydrate = context.request[EnableHydration] || false;
|
|
4206
|
+
const options = {
|
|
4207
|
+
shouldHydrate,
|
|
4208
|
+
identifier,
|
|
4209
|
+
priority
|
|
4210
|
+
};
|
|
4211
|
+
let isMut = false;
|
|
4212
|
+
if (isMutation(context.request)) {
|
|
4213
|
+
isMut = true;
|
|
4214
|
+
if (Array.isArray(context.request.records)) {
|
|
4215
|
+
context.request.records.forEach(record => {
|
|
4216
|
+
store.cache.willCommit(record, context);
|
|
4217
|
+
});
|
|
4218
|
+
} else if (context.request.data?.record) {
|
|
4219
|
+
// legacy fallback, the data option should no longer be used for this
|
|
4220
|
+
store.cache.willCommit(context.request.data.record, context);
|
|
4221
|
+
}
|
|
4222
|
+
}
|
|
4223
|
+
if (store.lifetimes?.willRequest) {
|
|
4224
|
+
store.lifetimes.willRequest(context.request, identifier, store);
|
|
4225
|
+
}
|
|
4226
|
+
const promise = next(context.request).then(document => {
|
|
4227
|
+
return handleFetchSuccess(store, context, options, document);
|
|
4228
|
+
}, error => {
|
|
4229
|
+
return handleFetchError(store, context, options, error);
|
|
4230
|
+
});
|
|
4231
|
+
if (!isMut) {
|
|
4232
|
+
return promise;
|
|
4233
|
+
}
|
|
4234
|
+
|
|
4235
|
+
// for mutations we need to enqueue the promise with the requestStateService
|
|
4236
|
+
// TODO should we enque a request per record in records?
|
|
4237
|
+
const record = context.request.data?.record || context.request.records?.[0];
|
|
4238
|
+
return store._requestCache._enqueue(promise, {
|
|
4239
|
+
data: [{
|
|
4240
|
+
op: 'saveRecord',
|
|
4241
|
+
recordIdentifier: record,
|
|
4242
|
+
options: undefined
|
|
4243
|
+
}]
|
|
4244
|
+
});
|
|
4245
|
+
}
|
|
2631
4246
|
|
|
2632
4247
|
/**
|
|
2633
4248
|
A `ManyArray` is a `MutableArray` that represents the contents of a has-many
|
|
@@ -3385,14 +5000,41 @@ function getBelongsToField(context) {
|
|
|
3385
5000
|
}
|
|
3386
5001
|
function setBelongsToField(context) {
|
|
3387
5002
|
const {
|
|
5003
|
+
field,
|
|
5004
|
+
resourceKey,
|
|
3388
5005
|
store
|
|
3389
5006
|
} = context;
|
|
3390
5007
|
const {
|
|
3391
|
-
schema
|
|
5008
|
+
schema,
|
|
5009
|
+
cache
|
|
3392
5010
|
} = store;
|
|
5011
|
+
|
|
5012
|
+
/**
|
|
5013
|
+
* We currently only allow editing sync belongsTo relationships
|
|
5014
|
+
* in PolarisMode
|
|
5015
|
+
*/
|
|
5016
|
+
const isValidPolarisConfig = field.options.linksMode && !field.options.async;
|
|
5017
|
+
if (!context.legacy && isValidPolarisConfig) {
|
|
5018
|
+
const name = context.path.at(-1);
|
|
5019
|
+
cache.mutate({
|
|
5020
|
+
op: 'replaceRelatedRecord',
|
|
5021
|
+
record: resourceKey,
|
|
5022
|
+
field: name,
|
|
5023
|
+
value: getResourceKey(context.value)
|
|
5024
|
+
},
|
|
5025
|
+
// @ts-expect-error
|
|
5026
|
+
true);
|
|
5027
|
+
return true;
|
|
5028
|
+
}
|
|
3393
5029
|
schema._kind('@legacy', 'belongsTo').set(store, context.record, context.resourceKey, context.field, context.value);
|
|
3394
5030
|
return true;
|
|
3395
5031
|
}
|
|
5032
|
+
function getResourceKey(record) {
|
|
5033
|
+
if (!record) {
|
|
5034
|
+
return null;
|
|
5035
|
+
}
|
|
5036
|
+
return recordIdentifierFor(record);
|
|
5037
|
+
}
|
|
3396
5038
|
function getCollectionField(context) {
|
|
3397
5039
|
entangleSignal(context.signals, context.record, context.path.at(-1), null);
|
|
3398
5040
|
}
|
|
@@ -4024,6 +5666,17 @@ function setSchemaObjectField(context) {
|
|
|
4024
5666
|
// }
|
|
4025
5667
|
return true;
|
|
4026
5668
|
}
|
|
5669
|
+
|
|
5670
|
+
/**
|
|
5671
|
+
* A ReactiveResource context object passed to
|
|
5672
|
+
* the get/set functions of a field kind implementation.
|
|
5673
|
+
*
|
|
5674
|
+
* This provides all the necessary information for
|
|
5675
|
+
* the field kind to perform its operations.
|
|
5676
|
+
*
|
|
5677
|
+
* @internal
|
|
5678
|
+
*/
|
|
5679
|
+
|
|
4027
5680
|
const DefaultMode = {
|
|
4028
5681
|
'@hash': {
|
|
4029
5682
|
get: getHashField,
|
|
@@ -4294,6 +5947,9 @@ class ReactiveResource {
|
|
|
4294
5947
|
if (prop === Destroy || prop === Checkout) {
|
|
4295
5948
|
return true;
|
|
4296
5949
|
}
|
|
5950
|
+
if (prop === identityField?.name) {
|
|
5951
|
+
return true;
|
|
5952
|
+
}
|
|
4297
5953
|
return fields.has(prop);
|
|
4298
5954
|
},
|
|
4299
5955
|
getOwnPropertyDescriptor(target, prop) {
|
|
@@ -4692,6 +6348,29 @@ function checkout(resource) {
|
|
|
4692
6348
|
function commit(record) {
|
|
4693
6349
|
return record[Commit]();
|
|
4694
6350
|
}
|
|
6351
|
+
function instantiateRecord(store, identifier, createArgs) {
|
|
6352
|
+
const schema = store.schema;
|
|
6353
|
+
const resourceSchema = schema.resource(identifier);
|
|
6354
|
+
const legacy = resourceSchema?.legacy ?? false;
|
|
6355
|
+
const editable = legacy;
|
|
6356
|
+
const record = new ReactiveResource({
|
|
6357
|
+
store,
|
|
6358
|
+
resourceKey: identifier,
|
|
6359
|
+
modeName: legacy ? 'legacy' : 'polaris',
|
|
6360
|
+
legacy: legacy,
|
|
6361
|
+
editable: editable,
|
|
6362
|
+
path: null,
|
|
6363
|
+
field: null,
|
|
6364
|
+
value: null
|
|
6365
|
+
});
|
|
6366
|
+
if (createArgs && editable) {
|
|
6367
|
+
Object.assign(record, createArgs);
|
|
6368
|
+
}
|
|
6369
|
+
return record;
|
|
6370
|
+
}
|
|
6371
|
+
function teardownRecord(record) {
|
|
6372
|
+
record[Destroy]();
|
|
6373
|
+
}
|
|
4695
6374
|
const Support = getOrSetGlobal('Support', new WeakMap());
|
|
4696
6375
|
const ConstructorField = {
|
|
4697
6376
|
type: '@constructor',
|
|
@@ -4706,6 +6385,14 @@ const TypeField = {
|
|
|
4706
6385
|
key: 'type'
|
|
4707
6386
|
}
|
|
4708
6387
|
};
|
|
6388
|
+
const IdentifierField = {
|
|
6389
|
+
type: '@identity',
|
|
6390
|
+
name: '$key',
|
|
6391
|
+
kind: 'derived',
|
|
6392
|
+
options: {
|
|
6393
|
+
key: '^'
|
|
6394
|
+
}
|
|
6395
|
+
};
|
|
4709
6396
|
const DefaultIdentityField = {
|
|
4710
6397
|
name: 'id',
|
|
4711
6398
|
kind: '@id'
|
|
@@ -4874,6 +6561,7 @@ function withDefaults(schema) {
|
|
|
4874
6561
|
// we add TypeField to the beginning so that it will
|
|
4875
6562
|
// appear right next to the identity field
|
|
4876
6563
|
schema.fields.unshift(TypeField);
|
|
6564
|
+
schema.fields.unshift(IdentifierField);
|
|
4877
6565
|
schema.fields.push(ConstructorField);
|
|
4878
6566
|
return schema;
|
|
4879
6567
|
}
|
|
@@ -4958,6 +6646,18 @@ class SchemaService {
|
|
|
4958
6646
|
|
|
4959
6647
|
/** @internal */
|
|
4960
6648
|
|
|
6649
|
+
/**
|
|
6650
|
+
* Tracks, per abstract type, the relationship fields that concrete
|
|
6651
|
+
* implementers have contributed via `options.as`, along with the type
|
|
6652
|
+
* that contributed each one (for assertion messages). This is
|
|
6653
|
+
* independent of whatever schema (synthesized or user-registered)
|
|
6654
|
+
* currently occupies `_schemas` for that type, so that these fields
|
|
6655
|
+
* survive regardless of the order in which the abstract type's
|
|
6656
|
+
* implementers and its own (optional) concrete schema are registered.
|
|
6657
|
+
*
|
|
6658
|
+
* @internal
|
|
6659
|
+
*/
|
|
6660
|
+
|
|
4961
6661
|
/** @internal */
|
|
4962
6662
|
|
|
4963
6663
|
/** @internal */
|
|
@@ -4969,6 +6669,7 @@ class SchemaService {
|
|
|
4969
6669
|
this._derivations = new Map();
|
|
4970
6670
|
this._traits = new Map();
|
|
4971
6671
|
this._modes = new Map();
|
|
6672
|
+
this._abstractImplementerFields = new Map();
|
|
4972
6673
|
this._extensions = {
|
|
4973
6674
|
object: new Map(),
|
|
4974
6675
|
array: new Map()
|
|
@@ -5014,12 +6715,33 @@ class SchemaService {
|
|
|
5014
6715
|
const fields = new Map();
|
|
5015
6716
|
const relationships = {};
|
|
5016
6717
|
const attributes = {};
|
|
6718
|
+
const abstractImplementations = [];
|
|
5017
6719
|
for (const field of schema.fields) {
|
|
5018
6720
|
fields.set(field.name, field);
|
|
5019
6721
|
if (field.kind === 'attribute') {
|
|
5020
6722
|
attributes[field.name] = field;
|
|
5021
6723
|
} else if (field.kind === 'belongsTo' || field.kind === 'hasMany') {
|
|
5022
6724
|
relationships[field.name] = field;
|
|
6725
|
+
if (field.options?.as) {
|
|
6726
|
+
abstractImplementations.push(field);
|
|
6727
|
+
}
|
|
6728
|
+
}
|
|
6729
|
+
}
|
|
6730
|
+
|
|
6731
|
+
// This type may already be known as an abstract polymorphic type,
|
|
6732
|
+
// implemented by other concrete types via `options.as`, from before it
|
|
6733
|
+
// ever had a schema of its own (see `_registerAbstractTypeImplementation`).
|
|
6734
|
+
// Carry those previously-contributed fields forward so that registering
|
|
6735
|
+
// a "real" schema for the type - whenever that happens to occur - never
|
|
6736
|
+
// erases the relationships its implementers depend on.
|
|
6737
|
+
const implementerFields = this._abstractImplementerFields.get(schema.type);
|
|
6738
|
+
if (implementerFields) {
|
|
6739
|
+
for (const [name, contribution] of implementerFields) {
|
|
6740
|
+
const ownField = fields.get(name);
|
|
6741
|
+
if (!ownField) {
|
|
6742
|
+
fields.set(name, contribution.field);
|
|
6743
|
+
relationships[name] = contribution.field;
|
|
6744
|
+
}
|
|
5023
6745
|
}
|
|
5024
6746
|
}
|
|
5025
6747
|
const cacheFields = null;
|
|
@@ -5038,6 +6760,92 @@ class SchemaService {
|
|
|
5038
6760
|
internalSchema.cacheFields = getCacheFields(internalSchema);
|
|
5039
6761
|
}
|
|
5040
6762
|
this._schemas.set(schema.type, internalSchema);
|
|
6763
|
+
|
|
6764
|
+
// A relationship field's `as` option marks it as a valid concrete
|
|
6765
|
+
// implementer of an abstract polymorphic type (e.g. `as: 'commentable'`).
|
|
6766
|
+
// That abstract type may never be given its own schema by the user - it
|
|
6767
|
+
// may exist only to be implemented by concrete types like this one - or
|
|
6768
|
+
// it may already have (or later receive) a schema of its own, e.g. if it
|
|
6769
|
+
// turns out to also be a real, directly-resolvable resource. Either way,
|
|
6770
|
+
// ensure it has a schema with this field present, so that it behaves
|
|
6771
|
+
// like any other registered resource (`hasResource`, `fields`, etc.)
|
|
6772
|
+
// instead of requiring special-casing wherever abstract relationship
|
|
6773
|
+
// types are resolved.
|
|
6774
|
+
for (const field of abstractImplementations) {
|
|
6775
|
+
this._registerAbstractTypeImplementation(field, schema.type);
|
|
6776
|
+
}
|
|
6777
|
+
}
|
|
6778
|
+
|
|
6779
|
+
/** @internal */
|
|
6780
|
+
_registerAbstractTypeImplementation(field, implementer) {
|
|
6781
|
+
const abstractType = field.options.as;
|
|
6782
|
+
let implementerFields = this._abstractImplementerFields.get(abstractType);
|
|
6783
|
+
if (!implementerFields) {
|
|
6784
|
+
implementerFields = new Map();
|
|
6785
|
+
this._abstractImplementerFields.set(abstractType, implementerFields);
|
|
6786
|
+
}
|
|
6787
|
+
|
|
6788
|
+
// Unlike the original approach this replaced, `as` is *not* stripped here:
|
|
6789
|
+
// the field as synthesized onto the abstract type's own schema keeps
|
|
6790
|
+
// `options.as === abstractType`, redundant/self-referential as that is.
|
|
6791
|
+
// This keeps every contributor - implementers and the abstract type's
|
|
6792
|
+
// own schema alike - declaring the same thing, which is what
|
|
6793
|
+
// `assertConsistentAbstractFieldShape` checks, and it is also what lets
|
|
6794
|
+
// `assertPolymorphicType` (which reads a field's declared `as` off
|
|
6795
|
+
// whatever `schema.fields()` serves for its type) correctly permit the
|
|
6796
|
+
// abstract type itself to be pushed directly into this relationship.
|
|
6797
|
+
const abstractField = {
|
|
6798
|
+
...field
|
|
6799
|
+
};
|
|
6800
|
+
const contribution = {
|
|
6801
|
+
field: abstractField,
|
|
6802
|
+
source: implementer
|
|
6803
|
+
};
|
|
6804
|
+
|
|
6805
|
+
// all concrete implementations of an abstract type are required to
|
|
6806
|
+
// share the same shape for the field that implements it, so the first
|
|
6807
|
+
// one registered is as good a canonical source as any - but a later,
|
|
6808
|
+
// differently-shaped one is almost certainly a mistake rather than an
|
|
6809
|
+
// intentional override, so we catch it rather than silently ignoring it.
|
|
6810
|
+
const existingImplementer = implementerFields.get(field.name);
|
|
6811
|
+
if (existingImplementer) {
|
|
6812
|
+
return;
|
|
6813
|
+
}
|
|
6814
|
+
implementerFields.set(field.name, contribution);
|
|
6815
|
+
let abstractSchema = this._schemas.get(abstractType);
|
|
6816
|
+
if (!abstractSchema) {
|
|
6817
|
+
abstractSchema = {
|
|
6818
|
+
original: {
|
|
6819
|
+
legacy: true,
|
|
6820
|
+
identity: {
|
|
6821
|
+
kind: '@id',
|
|
6822
|
+
name: 'id'
|
|
6823
|
+
},
|
|
6824
|
+
type: abstractType,
|
|
6825
|
+
fields: []
|
|
6826
|
+
},
|
|
6827
|
+
finalized: true,
|
|
6828
|
+
fields: new Map(),
|
|
6829
|
+
cacheFields: new Map(),
|
|
6830
|
+
relationships: {},
|
|
6831
|
+
attributes: {},
|
|
6832
|
+
traits: new Set()
|
|
6833
|
+
};
|
|
6834
|
+
this._schemas.set(abstractType, abstractSchema);
|
|
6835
|
+
}
|
|
6836
|
+
const existingAbstractField = abstractSchema.fields.get(field.name);
|
|
6837
|
+
if (existingAbstractField) {
|
|
6838
|
+
return;
|
|
6839
|
+
}
|
|
6840
|
+
abstractSchema.fields.set(field.name, abstractField);
|
|
6841
|
+
abstractSchema.relationships[field.name] = abstractField;
|
|
6842
|
+
|
|
6843
|
+
// If the schema is mid-finalization (traits pending), finalizeResource
|
|
6844
|
+
// will recompute cacheFields from the current `fields` map anyway; avoid
|
|
6845
|
+
// doing so prematurely from a partially-resolved set of fields.
|
|
6846
|
+
if (abstractSchema.finalized) {
|
|
6847
|
+
abstractSchema.cacheFields = getCacheFields(abstractSchema);
|
|
6848
|
+
}
|
|
5041
6849
|
}
|
|
5042
6850
|
|
|
5043
6851
|
/**
|
|
@@ -5234,4 +7042,506 @@ function mergeMap(base, toApply) {
|
|
|
5234
7042
|
base.set(key, value);
|
|
5235
7043
|
}
|
|
5236
7044
|
}
|
|
5237
|
-
|
|
7045
|
+
|
|
7046
|
+
// Lazily close over fetch to avoid breaking Mirage
|
|
7047
|
+
const _fetch = typeof fetch !== 'undefined' ? (...args) => fetch(...args) : typeof FastBoot !== 'undefined' ? (...args) => FastBoot.require('node-fetch')(...args) : () => {
|
|
7048
|
+
throw new Error('No Fetch Implementation Found');
|
|
7049
|
+
};
|
|
7050
|
+
|
|
7051
|
+
// clones a response in a way that should still
|
|
7052
|
+
// allow it to stream
|
|
7053
|
+
function cloneResponse(response, overrides) {
|
|
7054
|
+
const props = cloneResponseProperties(response);
|
|
7055
|
+
return new Response(response.body, Object.assign(props, overrides));
|
|
7056
|
+
}
|
|
7057
|
+
function withDateHeader(response) {
|
|
7058
|
+
const headers = new Headers(response.headers);
|
|
7059
|
+
headers.set('date', new Date().toUTCString());
|
|
7060
|
+
return cloneResponse(response, {
|
|
7061
|
+
headers
|
|
7062
|
+
});
|
|
7063
|
+
}
|
|
7064
|
+
const MUTATION_OPS = new Set(['updateRecord', 'createRecord', 'deleteRecord']);
|
|
7065
|
+
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']]);
|
|
7066
|
+
|
|
7067
|
+
/**
|
|
7068
|
+
* ```ts
|
|
7069
|
+
* import { Fetch } from '@warp-drive/core';
|
|
7070
|
+
* ```
|
|
7071
|
+
*
|
|
7072
|
+
* A basic Fetch Handler which converts a request into a
|
|
7073
|
+
* `fetch` call presuming the response to be `json`.
|
|
7074
|
+
*
|
|
7075
|
+
* ```ts
|
|
7076
|
+
* import { RequestManager, Fetch } from '@warp-drive/core';
|
|
7077
|
+
*
|
|
7078
|
+
* const manager = new RequestManager()
|
|
7079
|
+
* .use([Fetch]);
|
|
7080
|
+
* ```
|
|
7081
|
+
*
|
|
7082
|
+
* @public
|
|
7083
|
+
*/
|
|
7084
|
+
const Fetch = {
|
|
7085
|
+
async request(context) {
|
|
7086
|
+
let response;
|
|
7087
|
+
try {
|
|
7088
|
+
({});
|
|
7089
|
+
response = await _fetch(context.request.url, context.request);
|
|
7090
|
+
} catch (e) {
|
|
7091
|
+
if (e instanceof DOMException && e.name === 'AbortError') {
|
|
7092
|
+
e.statusText = 'Aborted';
|
|
7093
|
+
e.status = 20;
|
|
7094
|
+
e.isRequestError = true;
|
|
7095
|
+
} else {
|
|
7096
|
+
e.statusText = 'Unknown Network Error';
|
|
7097
|
+
e.status = 0;
|
|
7098
|
+
if (!(e instanceof DOMException)) {
|
|
7099
|
+
e.code = 0;
|
|
7100
|
+
}
|
|
7101
|
+
e.isRequestError = true;
|
|
7102
|
+
}
|
|
7103
|
+
throw e;
|
|
7104
|
+
}
|
|
7105
|
+
const isError = !response.ok || response.status >= 400;
|
|
7106
|
+
const op = context.request.op;
|
|
7107
|
+
const isMutationOp = Boolean(op && MUTATION_OPS.has(op));
|
|
7108
|
+
if (!isError && !isMutationOp && response.status !== 204 && !response.headers.has('date')) {
|
|
7109
|
+
{
|
|
7110
|
+
response = withDateHeader(response);
|
|
7111
|
+
}
|
|
7112
|
+
}
|
|
7113
|
+
context.setResponse(response);
|
|
7114
|
+
if (response.status === 204 || context.request.method === 'HEAD') {
|
|
7115
|
+
return null;
|
|
7116
|
+
}
|
|
7117
|
+
let text = '';
|
|
7118
|
+
// if we are in a mirage context, we cannot support streaming
|
|
7119
|
+
{
|
|
7120
|
+
const reader = response.body.getReader();
|
|
7121
|
+
const decoder = new TextDecoder();
|
|
7122
|
+
let isStreaming = context.hasRequestedStream;
|
|
7123
|
+
let stream = isStreaming ? new TransformStream() : null;
|
|
7124
|
+
let writer = stream?.writable.getWriter();
|
|
7125
|
+
if (isStreaming) {
|
|
7126
|
+
// Listen for the abort event on the AbortSignal
|
|
7127
|
+
context.request.signal?.addEventListener('abort', () => {
|
|
7128
|
+
if (!isStreaming) {
|
|
7129
|
+
return;
|
|
7130
|
+
}
|
|
7131
|
+
void stream.writable.abort('Request Aborted');
|
|
7132
|
+
void stream.readable.cancel('Request Aborted');
|
|
7133
|
+
});
|
|
7134
|
+
context.setStream(stream.readable);
|
|
7135
|
+
}
|
|
7136
|
+
while (true) {
|
|
7137
|
+
// we manually read the stream instead of using `response.json()`
|
|
7138
|
+
// or `response.text()` because if we need to stream the body
|
|
7139
|
+
// we need to be able to pass the stream along efficiently.
|
|
7140
|
+
const {
|
|
7141
|
+
done,
|
|
7142
|
+
value
|
|
7143
|
+
} = await reader.read();
|
|
7144
|
+
if (done) {
|
|
7145
|
+
if (isStreaming) {
|
|
7146
|
+
isStreaming = false;
|
|
7147
|
+
await writer.ready;
|
|
7148
|
+
await writer.close();
|
|
7149
|
+
}
|
|
7150
|
+
break;
|
|
7151
|
+
}
|
|
7152
|
+
text += decoder.decode(value, {
|
|
7153
|
+
stream: true
|
|
7154
|
+
});
|
|
7155
|
+
|
|
7156
|
+
// if we are streaming, we want to pass the stream along
|
|
7157
|
+
if (isStreaming) {
|
|
7158
|
+
await writer.ready;
|
|
7159
|
+
await writer.write(value);
|
|
7160
|
+
} else if (context.hasRequestedStream) {
|
|
7161
|
+
const encode = new TextEncoder();
|
|
7162
|
+
isStreaming = true;
|
|
7163
|
+
stream = new TransformStream();
|
|
7164
|
+
// Listen for the abort event on the AbortSignal
|
|
7165
|
+
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
|
7166
|
+
context.request.signal?.addEventListener('abort', () => {
|
|
7167
|
+
if (!isStreaming) {
|
|
7168
|
+
return;
|
|
7169
|
+
}
|
|
7170
|
+
void stream.writable.abort('Request Aborted');
|
|
7171
|
+
void stream.readable.cancel('Request Aborted');
|
|
7172
|
+
});
|
|
7173
|
+
context.setStream(stream.readable);
|
|
7174
|
+
writer = stream.writable.getWriter();
|
|
7175
|
+
await writer.ready;
|
|
7176
|
+
await writer.write(encode.encode(text));
|
|
7177
|
+
await writer.ready;
|
|
7178
|
+
await writer.write(value);
|
|
7179
|
+
}
|
|
7180
|
+
}
|
|
7181
|
+
if (isStreaming) {
|
|
7182
|
+
isStreaming = false;
|
|
7183
|
+
await writer.ready;
|
|
7184
|
+
await writer.close();
|
|
7185
|
+
}
|
|
7186
|
+
}
|
|
7187
|
+
// if we are an error, we will want to throw
|
|
7188
|
+
if (isError) {
|
|
7189
|
+
let errorPayload;
|
|
7190
|
+
try {
|
|
7191
|
+
errorPayload = JSON.parse(text);
|
|
7192
|
+
} catch {
|
|
7193
|
+
// void;
|
|
7194
|
+
}
|
|
7195
|
+
// attempt errors discovery
|
|
7196
|
+
const errors = Array.isArray(errorPayload) ? errorPayload : isDict(errorPayload) && Array.isArray(errorPayload.errors) ? errorPayload.errors : null;
|
|
7197
|
+
const statusText = response.statusText || ERROR_STATUS_CODE_FOR.get(response.status) || 'Unknown Request Error';
|
|
7198
|
+
const msg = `[${response.status} ${statusText}] ${context.request.method ?? 'GET'} (${response.type}) - ${response.url}`;
|
|
7199
|
+
const error = errors ? new AggregateError(errors, msg) : new Error(msg);
|
|
7200
|
+
error.status = response.status;
|
|
7201
|
+
error.statusText = statusText;
|
|
7202
|
+
error.isRequestError = true;
|
|
7203
|
+
error.code = error.status;
|
|
7204
|
+
error.name = error.statusText.replaceAll(' ', '') + 'Error';
|
|
7205
|
+
error.content = errorPayload;
|
|
7206
|
+
throw error;
|
|
7207
|
+
} else {
|
|
7208
|
+
return JSON.parse(text);
|
|
7209
|
+
}
|
|
7210
|
+
}
|
|
7211
|
+
};
|
|
7212
|
+
function isDict(v) {
|
|
7213
|
+
return v !== null && typeof v === 'object';
|
|
7214
|
+
}
|
|
7215
|
+
|
|
7216
|
+
/**
|
|
7217
|
+
* ## Import
|
|
7218
|
+
*
|
|
7219
|
+
* ```js
|
|
7220
|
+
* import { RequestManager } from '@warp-drive/core';
|
|
7221
|
+
* ```
|
|
7222
|
+
*
|
|
7223
|
+
* For complete usage guide see the [RequestManager Documentation](/guides/).
|
|
7224
|
+
*
|
|
7225
|
+
* ## How It Works
|
|
7226
|
+
*
|
|
7227
|
+
* ```ts
|
|
7228
|
+
* interface RequestManager {
|
|
7229
|
+
* request<T>(req: RequestInfo): Future<T>;
|
|
7230
|
+
* }
|
|
7231
|
+
* ```
|
|
7232
|
+
*
|
|
7233
|
+
* A RequestManager provides a request/response flow in which configured
|
|
7234
|
+
* handlers are successively given the opportunity to handle, modify, or
|
|
7235
|
+
* pass-along a request.
|
|
7236
|
+
*
|
|
7237
|
+
* <img src="/images/handlers-all-labeled.gif" alt="RequestManager Flow Animation" width="100%" />
|
|
7238
|
+
*
|
|
7239
|
+
* For example:
|
|
7240
|
+
*
|
|
7241
|
+
* ::: code-group
|
|
7242
|
+
*
|
|
7243
|
+
* ```ts [Setup.ts]
|
|
7244
|
+
* import { RequestManager, Fetch } from '@warp-drive/core';
|
|
7245
|
+
* import { AutoCompress } from '@warp-drive/utilities/handlers';
|
|
7246
|
+
* import Auth from 'ember-simple-auth/handler';
|
|
7247
|
+
*
|
|
7248
|
+
* // ... create manager
|
|
7249
|
+
* const manager = new RequestManager()
|
|
7250
|
+
* .use([Auth, new AutoCompress(), Fetch]); // [!code focus]
|
|
7251
|
+
* ```
|
|
7252
|
+
*
|
|
7253
|
+
* ```ts [Usage.ts]
|
|
7254
|
+
* import Config from './config';
|
|
7255
|
+
*
|
|
7256
|
+
* const { apiUrl } = Config;
|
|
7257
|
+
*
|
|
7258
|
+
* // ... execute a request
|
|
7259
|
+
* const response = await manager.request({
|
|
7260
|
+
* url: `${apiUrl}/users`
|
|
7261
|
+
* });
|
|
7262
|
+
* ```
|
|
7263
|
+
*
|
|
7264
|
+
* :::
|
|
7265
|
+
*
|
|
7266
|
+
* ### Futures
|
|
7267
|
+
*
|
|
7268
|
+
* The return value of `manager.request` is a `Future`, which allows
|
|
7269
|
+
* access to limited information about the request while it is still
|
|
7270
|
+
* pending and fulfills with the final state when the request completes.
|
|
7271
|
+
*
|
|
7272
|
+
* A `Future` is cancellable via `abort`.
|
|
7273
|
+
*
|
|
7274
|
+
* Handlers may optionally expose a `ReadableStream` to the `Future` for
|
|
7275
|
+
* streaming data; however, when doing so the future should not resolve
|
|
7276
|
+
* until the response stream is fully read.
|
|
7277
|
+
*
|
|
7278
|
+
* ```ts
|
|
7279
|
+
* interface Future<T> extends Promise<StructuredDocument<T>> {
|
|
7280
|
+
* abort(): void;
|
|
7281
|
+
*
|
|
7282
|
+
* async getStream(): ReadableStream | null;
|
|
7283
|
+
* }
|
|
7284
|
+
* ```
|
|
7285
|
+
*
|
|
7286
|
+
* ### StructuredDocuments
|
|
7287
|
+
*
|
|
7288
|
+
* A Future resolves with a `StructuredDataDocument` or rejects with a `StructuredErrorDocument`.
|
|
7289
|
+
*
|
|
7290
|
+
* ```ts
|
|
7291
|
+
* interface StructuredDataDocument<T> {
|
|
7292
|
+
* request: ImmutableRequestInfo;
|
|
7293
|
+
* response: ImmutableResponseInfo;
|
|
7294
|
+
* content: T;
|
|
7295
|
+
* }
|
|
7296
|
+
* interface StructuredErrorDocument extends Error {
|
|
7297
|
+
* request: ImmutableRequestInfo;
|
|
7298
|
+
* response: ImmutableResponseInfo;
|
|
7299
|
+
* error: string | object;
|
|
7300
|
+
* }
|
|
7301
|
+
* type StructuredDocument<T> = StructuredDataDocument<T> | StructuredErrorDocument;
|
|
7302
|
+
* ```
|
|
7303
|
+
*
|
|
7304
|
+
* @public
|
|
7305
|
+
*/
|
|
7306
|
+
class RequestManager {
|
|
7307
|
+
/** @internal */
|
|
7308
|
+
|
|
7309
|
+
/** @internal */
|
|
7310
|
+
|
|
7311
|
+
/**
|
|
7312
|
+
* A map of pending requests from request.id to their
|
|
7313
|
+
* associated CacheHandler promise.
|
|
7314
|
+
*
|
|
7315
|
+
* This queue is managed by the CacheHandler
|
|
7316
|
+
*
|
|
7317
|
+
* @internal
|
|
7318
|
+
*/
|
|
7319
|
+
|
|
7320
|
+
/** @internal */
|
|
7321
|
+
|
|
7322
|
+
constructor(options) {
|
|
7323
|
+
Object.assign(this, options);
|
|
7324
|
+
this._pending = new Map();
|
|
7325
|
+
this._deduped = new Map();
|
|
7326
|
+
this._handlers = [];
|
|
7327
|
+
}
|
|
7328
|
+
|
|
7329
|
+
/**
|
|
7330
|
+
* Register a handler to use for primary cache intercept.
|
|
7331
|
+
*
|
|
7332
|
+
* Only one such handler may exist. If using the same
|
|
7333
|
+
* RequestManager as the Store instance the Store
|
|
7334
|
+
* registers itself as a Cache handler.
|
|
7335
|
+
*
|
|
7336
|
+
* @public
|
|
7337
|
+
*/
|
|
7338
|
+
useCache(cacheHandler) {
|
|
7339
|
+
cacheHandler[IS_CACHE_HANDLER] = true;
|
|
7340
|
+
this._handlers.unshift(cacheHandler);
|
|
7341
|
+
return this;
|
|
7342
|
+
}
|
|
7343
|
+
|
|
7344
|
+
/**
|
|
7345
|
+
* Register handler(s) to use when a request is issued.
|
|
7346
|
+
*
|
|
7347
|
+
* Handlers will be invoked in the order they are registered.
|
|
7348
|
+
* Each Handler is given the opportunity to handle the request,
|
|
7349
|
+
* curry the request, or pass along a modified request.
|
|
7350
|
+
*
|
|
7351
|
+
* @public
|
|
7352
|
+
*/
|
|
7353
|
+
use(newHandlers) {
|
|
7354
|
+
const handlers = this._handlers;
|
|
7355
|
+
handlers.push(...newHandlers);
|
|
7356
|
+
return this;
|
|
7357
|
+
}
|
|
7358
|
+
|
|
7359
|
+
/**
|
|
7360
|
+
* Issue a Request.
|
|
7361
|
+
*
|
|
7362
|
+
* Returns a Future that fulfills with a StructuredDocument
|
|
7363
|
+
*
|
|
7364
|
+
* @public
|
|
7365
|
+
*/
|
|
7366
|
+
request(request) {
|
|
7367
|
+
const handlers = this._handlers;
|
|
7368
|
+
const controller = request.controller || new AbortController();
|
|
7369
|
+
if (request.controller) {
|
|
7370
|
+
delete request.controller;
|
|
7371
|
+
}
|
|
7372
|
+
const requestId = peekUniversalTransient('REQ_ID') ?? 0;
|
|
7373
|
+
setUniversalTransient('REQ_ID', requestId + 1);
|
|
7374
|
+
const context = {
|
|
7375
|
+
controller,
|
|
7376
|
+
response: null,
|
|
7377
|
+
stream: null,
|
|
7378
|
+
hasRequestedStream: false,
|
|
7379
|
+
id: requestId,
|
|
7380
|
+
identifier: null,
|
|
7381
|
+
requester: request[EnableHydration] && request.store ? request.store : this
|
|
7382
|
+
};
|
|
7383
|
+
const promise = executeNextHandler(handlers, request, 0, context);
|
|
7384
|
+
|
|
7385
|
+
// the cache handler will set the result of the request synchronously
|
|
7386
|
+
// if it is able to fulfill the request from the cache
|
|
7387
|
+
const cacheResult = getRequestResult(requestId);
|
|
7388
|
+
// const promise1 = store.request(myRequest);
|
|
7389
|
+
// const promise2 = store.request(myRequest);
|
|
7390
|
+
// promise1 === promise2; // false
|
|
7391
|
+
// either we need to make promise1 === promise2, or we need to make sure that
|
|
7392
|
+
// we need to have a way to key from request to result
|
|
7393
|
+
// such that we can lookup the result here and return it if it exists
|
|
7394
|
+
const finalPromise = upgradePromise(promise.then(result => {
|
|
7395
|
+
setPromiseResult(finalPromise, {
|
|
7396
|
+
isError: false,
|
|
7397
|
+
result
|
|
7398
|
+
});
|
|
7399
|
+
clearRequestResult(requestId);
|
|
7400
|
+
return result;
|
|
7401
|
+
}, error => {
|
|
7402
|
+
setPromiseResult(finalPromise, {
|
|
7403
|
+
isError: true,
|
|
7404
|
+
result: error
|
|
7405
|
+
});
|
|
7406
|
+
clearRequestResult(requestId);
|
|
7407
|
+
throw error;
|
|
7408
|
+
}), promise);
|
|
7409
|
+
if (cacheResult) {
|
|
7410
|
+
setPromiseResult(finalPromise, cacheResult);
|
|
7411
|
+
}
|
|
7412
|
+
return finalPromise;
|
|
7413
|
+
}
|
|
7414
|
+
|
|
7415
|
+
/**
|
|
7416
|
+
* This method exists so that the RequestManager can be created
|
|
7417
|
+
* can be created by container/factory systems that expect to
|
|
7418
|
+
* call a static `create` method to instantiate the class.
|
|
7419
|
+
*
|
|
7420
|
+
* Using `new RequestManager()` directly is preferred.
|
|
7421
|
+
*
|
|
7422
|
+
* @private
|
|
7423
|
+
*/
|
|
7424
|
+
static create(options) {
|
|
7425
|
+
return new this(options);
|
|
7426
|
+
}
|
|
7427
|
+
}
|
|
7428
|
+
|
|
7429
|
+
/**
|
|
7430
|
+
* This type exists for internal use only for
|
|
7431
|
+
* where intimate contracts still exist either for
|
|
7432
|
+
* the Test Suite or for Legacy code.
|
|
7433
|
+
*
|
|
7434
|
+
* @private
|
|
7435
|
+
*/
|
|
7436
|
+
|
|
7437
|
+
/**
|
|
7438
|
+
* @module
|
|
7439
|
+
* @mergeModuleWith <project>
|
|
7440
|
+
*/
|
|
7441
|
+
|
|
7442
|
+
// Mirage (or another Pretender-based fetch mock) may be used outside of a
|
|
7443
|
+
// `TESTING` build (e.g. `ember serve` in dev mode), so this override is
|
|
7444
|
+
// exposed unconditionally rather than gated to the `TESTING` env.
|
|
7445
|
+
// @ts-expect-error adding to globalThis
|
|
7446
|
+
globalThis.setWarpDriveIsMaybeMirage = setIsMaybeMirage;
|
|
7447
|
+
|
|
7448
|
+
/**
|
|
7449
|
+
* Options for setting up a Store instance with `useRecommendedStore`.
|
|
7450
|
+
*/
|
|
7451
|
+
|
|
7452
|
+
/**
|
|
7453
|
+
* Creates a configured Store class with recommended defaults
|
|
7454
|
+
* for schema handling, reactivity, caching, and request management.
|
|
7455
|
+
*
|
|
7456
|
+
* ```ts
|
|
7457
|
+
* import { useRecommendedStore } from '@warp-drive/core';
|
|
7458
|
+
* import { JSONAPICache } from '@warp-drive/json-api';
|
|
7459
|
+
*
|
|
7460
|
+
* export const Store = useRecommendedStore({
|
|
7461
|
+
* cache: JSONAPICache,
|
|
7462
|
+
* schemas: [],
|
|
7463
|
+
* });
|
|
7464
|
+
* ```
|
|
7465
|
+
*/
|
|
7466
|
+
function useRecommendedStore(options, StoreKlass = Store) {
|
|
7467
|
+
return class AppStore extends StoreKlass {
|
|
7468
|
+
constructor(createArgs) {
|
|
7469
|
+
super(createArgs);
|
|
7470
|
+
// installed via defineProperty (rather than a class field/accessor) so that
|
|
7471
|
+
// this lazy override of the inherited `requestManager` field does not
|
|
7472
|
+
// conflict with the documented pattern of assigning it directly on
|
|
7473
|
+
// consumer-authored Store subclasses. The setter preserves the ability
|
|
7474
|
+
// to replace `requestManager` outright after construction.
|
|
7475
|
+
let requestManager;
|
|
7476
|
+
Object.defineProperty(this, 'requestManager', {
|
|
7477
|
+
configurable: true,
|
|
7478
|
+
enumerable: true,
|
|
7479
|
+
get: () => {
|
|
7480
|
+
if (!requestManager) {
|
|
7481
|
+
const handlers = typeof options.handlers === 'function' ? options.handlers(this) : options.handlers ?? [];
|
|
7482
|
+
requestManager = new RequestManager().use([...handlers, Fetch]).useCache(CacheHandler);
|
|
7483
|
+
}
|
|
7484
|
+
return requestManager;
|
|
7485
|
+
},
|
|
7486
|
+
set: value => {
|
|
7487
|
+
requestManager = value;
|
|
7488
|
+
}
|
|
7489
|
+
});
|
|
7490
|
+
}
|
|
7491
|
+
lifetimes = options.policy ?? new DefaultCachePolicy({
|
|
7492
|
+
apiCacheHardExpires: 15 * 60 * 1000,
|
|
7493
|
+
// 15 minutes
|
|
7494
|
+
apiCacheSoftExpires: 1 * 30 * 1000,
|
|
7495
|
+
// 30 seconds
|
|
7496
|
+
constraints: {
|
|
7497
|
+
headers: {
|
|
7498
|
+
'X-WarpDrive-Expires': true,
|
|
7499
|
+
'Cache-Control': true,
|
|
7500
|
+
Expires: true
|
|
7501
|
+
}
|
|
7502
|
+
}
|
|
7503
|
+
});
|
|
7504
|
+
createSchemaService() {
|
|
7505
|
+
const schema = new SchemaService();
|
|
7506
|
+
registerDerivations(schema);
|
|
7507
|
+
if (options.schemas) schema.registerResources(options.schemas);
|
|
7508
|
+
if (options.traits) {
|
|
7509
|
+
for (const trait of options.traits) {
|
|
7510
|
+
schema.registerTrait(trait);
|
|
7511
|
+
}
|
|
7512
|
+
}
|
|
7513
|
+
if (options.derivations) {
|
|
7514
|
+
for (const derivation of options.derivations) {
|
|
7515
|
+
schema.registerDerivation(derivation);
|
|
7516
|
+
}
|
|
7517
|
+
}
|
|
7518
|
+
if (options.transformations) {
|
|
7519
|
+
for (const transformation of options.transformations) {
|
|
7520
|
+
schema.registerTransformation(transformation);
|
|
7521
|
+
}
|
|
7522
|
+
}
|
|
7523
|
+
if (options.hashFns) {
|
|
7524
|
+
for (const hashFn of options.hashFns) {
|
|
7525
|
+
schema.registerHashFn(hashFn);
|
|
7526
|
+
}
|
|
7527
|
+
}
|
|
7528
|
+
if (options.CAUTION_MEGA_DANGER_ZONE_extensions) {
|
|
7529
|
+
for (const extension of options.CAUTION_MEGA_DANGER_ZONE_extensions) {
|
|
7530
|
+
schema.CAUTION_MEGA_DANGER_ZONE_registerExtension(extension);
|
|
7531
|
+
}
|
|
7532
|
+
}
|
|
7533
|
+
return schema;
|
|
7534
|
+
}
|
|
7535
|
+
createCache(capabilities) {
|
|
7536
|
+
// eslint-disable-next-line new-cap
|
|
7537
|
+
return new options.cache(capabilities);
|
|
7538
|
+
}
|
|
7539
|
+
instantiateRecord(key, createArgs) {
|
|
7540
|
+
return instantiateRecord(this, key, createArgs);
|
|
7541
|
+
}
|
|
7542
|
+
teardownRecord(record) {
|
|
7543
|
+
return teardownRecord(record);
|
|
7544
|
+
}
|
|
7545
|
+
};
|
|
7546
|
+
}
|
|
7547
|
+
export { setIdentifierForgetMethod as A, setIdentifierResetMethod as B, CacheHandler as C, setKeyInfoForResource as D, Fetch as F, RecordArrayManager as R, Store as S, _clearCaches as _, isRequestKey as a, assertPrivateStore as b, coerceId as c, isPrivateStore as d, ensureStringId as e, fastPush as f, assertPrivateCapabilities as g, setRecordIdentifier as h, isResourceKey as i, StoreMap as j, createLegacyManyArray as k, checkout as l, instantiateRecord as m, normalizeModelName as n, SchemaService as o, fromIdentity as p, registerDerivations as q, recordIdentifierFor as r, storeFor as s, teardownRecord as t, commit as u, RequestManager as v, withDefaults as w, useRecommendedStore as x, setIdentifierGenerationMethod as y, setIdentifierUpdateMethod as z };
|