Skip to content

Commit

Permalink
Backport into release (#8750)
Browse files Browse the repository at this point in the history
* fix: @ember-data/debug should declare its peer-dependency on @ember-data/store (#8703)

* fix: de-dupe coalescing when includes or adapterOptions is present but still use findRecord (#8704)

* fix: make implicit relationship teardown following delete of related record safe (#8705)

* fix: catch errors during didCommit in DEBUG (#8708)

---------

Co-authored-by: Chris Thoburn <runspired@users.noreply.github.com>
  • Loading branch information
jrjohnson and runspired authored Jul 28, 2023
1 parent 628fa59 commit 662ce74
Show file tree
Hide file tree
Showing 14 changed files with 2,049 additions and 115 deletions.
3 changes: 2 additions & 1 deletion packages/debug/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"directories": {},
"scripts": {},
"peerDependencies": {
"@ember/string": "^3.1.1"
"@ember/string": "^3.1.1",
"@ember-data/store": "workspace:4.12.2"
},
"dependenciesMeta": {
"@ember-data/private-build-infra": {
Expand Down
17 changes: 17 additions & 0 deletions packages/graph/src/-private/graph/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,16 +199,33 @@ export class Graph {
isReleasable(identifier: StableRecordIdentifier): boolean {
const relationships = this.identifiers.get(identifier);
if (!relationships) {
if (LOG_GRAPH) {
// eslint-disable-next-line no-console
console.log(`graph: RELEASABLE ${String(identifier)}`);
}
return true;
}
const keys = Object.keys(relationships);
for (let i = 0; i < keys.length; i++) {
const relationship: RelationshipEdge = relationships[keys[i]];
// account for previously unloaded relationships
// typically from a prior deletion of a record that pointed to this one implicitly
if (relationship === undefined) {
continue;
}
assert(`Expected a relationship`, relationship);
if (relationship.definition.inverseIsAsync) {
if (LOG_GRAPH) {
// eslint-disable-next-line no-console
console.log(`graph: <<NOT>> RELEASABLE ${String(identifier)}`);
}
return false;
}
}
if (LOG_GRAPH) {
// eslint-disable-next-line no-console
console.log(`graph: RELEASABLE ${String(identifier)}`);
}
return true;
}

Expand Down
143 changes: 103 additions & 40 deletions packages/legacy-compat/src/legacy-network-handler/fetch-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ interface PendingFetchItem {
resolver: Deferred<any>;
options: FindOptions;
trace?: unknown;
promise: Promise<StableRecordIdentifier>;
promise: Promise<StableExistingRecordIdentifier>;
}

interface PendingSaveItem {
Expand All @@ -60,7 +60,7 @@ export default class FetchManager {
declare isDestroyed: boolean;
declare requestCache: RequestStateService;
// fetches pending in the runloop, waiting to be coalesced
declare _pendingFetch: Map<string, PendingFetchItem[]>;
declare _pendingFetch: Map<string, Map<StableExistingRecordIdentifier, PendingFetchItem[]>>;
declare _store: Store;

constructor(store: Store) {
Expand Down Expand Up @@ -114,7 +114,7 @@ export default class FetchManager {
identifier: StableExistingRecordIdentifier,
options: FindOptions,
request: StoreRequestInfo
): Promise<StableRecordIdentifier> {
): Promise<StableExistingRecordIdentifier> {
let query: FindRecordQuery = {
op: 'findRecord',
recordIdentifier: identifier,
Expand Down Expand Up @@ -193,13 +193,21 @@ export default class FetchManager {
});
}

let fetches = this._pendingFetch;
let fetchesByType = this._pendingFetch;
let fetchesById = fetchesByType.get(modelName);

if (!fetches.has(modelName)) {
fetches.set(modelName, []);
if (!fetchesById) {
fetchesById = new Map();
fetchesByType.set(modelName, fetchesById);
}

(fetches.get(modelName) as PendingFetchItem[]).push(pendingFetchItem);
let requestsForIdentifier = fetchesById.get(identifier);
if (!requestsForIdentifier) {
requestsForIdentifier = [];
fetchesById.set(identifier, requestsForIdentifier);
}

requestsForIdentifier.push(pendingFetchItem);

if (TESTING) {
if (!request.disableTestWaiter) {
Expand All @@ -214,14 +222,12 @@ export default class FetchManager {
return promise;
}

getPendingFetch(identifier: StableRecordIdentifier, options: FindOptions) {
let pendingFetches = this._pendingFetch.get(identifier.type);
getPendingFetch(identifier: StableExistingRecordIdentifier, options: FindOptions) {
let pendingFetches = this._pendingFetch.get(identifier.type)?.get(identifier);

// We already have a pending fetch for this
if (pendingFetches) {
let matchingPendingFetch = pendingFetches.find(
(fetch) => fetch.identifier === identifier && isSameRequest(options, fetch.options)
);
let matchingPendingFetch = pendingFetches.find((fetch) => isSameRequest(options, fetch.options));
if (matchingPendingFetch) {
return matchingPendingFetch.promise;
}
Expand All @@ -239,15 +245,15 @@ export default class FetchManager {
}

fetchDataIfNeededForIdentifier(
identifier: StableRecordIdentifier,
identifier: StableExistingRecordIdentifier,
options: FindOptions = {},
request: StoreRequestInfo
): Promise<StableRecordIdentifier> {
): Promise<StableExistingRecordIdentifier> {
// pre-loading will change the isEmpty value
const isEmpty = _isEmpty(this._store._instanceCache, identifier);
const isLoading = _isLoading(this._store._instanceCache, identifier);

let promise: Promise<StableRecordIdentifier>;
let promise: Promise<StableExistingRecordIdentifier>;
if (isEmpty) {
assertIdentifierHasId(identifier);

Expand Down Expand Up @@ -296,12 +302,47 @@ function _isLoading(cache: InstanceCache, identifier: StableRecordIdentifier): b
);
}

function includesSatisfies(current: undefined | string | string[], existing: undefined | string | string[]): boolean {
// if we have no includes we are good
if (!current?.length) {
return true;
}

// if we are here we have includes,
// and if existing has no includes then we will need a new request
if (!existing?.length) {
return false;
}

const arrCurrent = (Array.isArray(current) ? current : current.split(',')).sort();
const arrExisting = (Array.isArray(existing) ? existing : existing.split(',')).sort();

// includes are identical
if (arrCurrent.join(',') === arrExisting.join(',')) {
return true;
}

// if all of current includes are in existing includes then we are good
// so if we find one that is not in existing then we need a new request
for (let i = 0; i < arrCurrent.length; i++) {
if (!arrExisting.includes(arrCurrent[i])) {
return false;
}
}

return true;
}

function optionsSatisfies(current: object | undefined, existing: object | undefined): boolean {
return !current || current === existing || Object.keys(current).length === 0;
}

// this function helps resolve whether we have a pending request that we should use instead
function isSameRequest(options: FindOptions = {}, existingOptions: FindOptions = {}) {
let includedMatches = !options.include || options.include === existingOptions.include;
let adapterOptionsMatches = options.adapterOptions === existingOptions.adapterOptions;

return includedMatches && adapterOptionsMatches;
return (
optionsSatisfies(options.adapterOptions, existingOptions.adapterOptions) &&
includesSatisfies(options.include, existingOptions.include)
);
}

function _findMany(
Expand Down Expand Up @@ -499,35 +540,57 @@ function _processCoalescedGroup(
}
}

function _flushPendingFetchForType(store: Store, pendingFetchItems: PendingFetchItem[], modelName: string) {
function _flushPendingFetchForType(
store: Store,
pendingFetchMap: Map<StableExistingRecordIdentifier, PendingFetchItem[]>,
modelName: string
) {
let adapter = store.adapterFor(modelName);
let shouldCoalesce = !!adapter.findMany && adapter.coalesceFindRequests;
let totalItems = pendingFetchItems.length;

if (shouldCoalesce) {
let snapshots = new Array<Snapshot>(totalItems);
let fetchMap = new Map<Snapshot, PendingFetchItem>();
for (let i = 0; i < totalItems; i++) {
let fetchItem = pendingFetchItems[i];
snapshots[i] = store._fetchManager.createSnapshot(fetchItem.identifier, fetchItem.options);
fetchMap.set(snapshots[i], fetchItem);
}
const pendingFetchItems: PendingFetchItem[] = [];
pendingFetchMap.forEach((requestsForIdentifier, identifier) => {
if (requestsForIdentifier.length > 1) {
return;
}

let groups: Snapshot[][];
if (adapter.groupRecordsForFindMany) {
groups = adapter.groupRecordsForFindMany(store, snapshots);
} else {
groups = [snapshots];
}
// remove this entry from the map so it's not processed again
pendingFetchMap.delete(identifier);
pendingFetchItems.push(requestsForIdentifier[0]);
});

for (let i = 0, l = groups.length; i < l; i++) {
_processCoalescedGroup(store, fetchMap, groups[i], adapter, modelName);
}
} else {
for (let i = 0; i < totalItems; i++) {
void _fetchRecord(store, adapter, pendingFetchItems[i]);
let totalItems = pendingFetchItems.length;

if (totalItems > 1) {
let snapshots = new Array<Snapshot>(totalItems);
let fetchMap = new Map<Snapshot, PendingFetchItem>();
for (let i = 0; i < totalItems; i++) {
let fetchItem = pendingFetchItems[i];
snapshots[i] = store._fetchManager.createSnapshot(fetchItem.identifier, fetchItem.options);
fetchMap.set(snapshots[i], fetchItem);
}

let groups: Snapshot[][];
if (adapter.groupRecordsForFindMany) {
groups = adapter.groupRecordsForFindMany(store, snapshots);
} else {
groups = [snapshots];
}

for (let i = 0, l = groups.length; i < l; i++) {
_processCoalescedGroup(store, fetchMap, groups[i], adapter, modelName);
}
} else if (totalItems === 1) {
void _fetchRecord(store, adapter, pendingFetchItems[0]);
}
}

pendingFetchMap.forEach((pendingFetchItems) => {
pendingFetchItems.forEach((pendingFetchItem) => {
void _fetchRecord(store, adapter, pendingFetchItem);
});
});
}

function _flushPendingSave(store: Store, pending: PendingSaveItem) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ function findBelongsTo<T>(context: StoreRequestContext): Promise<T> {
const identifier = identifiers?.[0];

// short circuit if we are already loading
let pendingRequest = identifier && store._fetchManager.getPendingFetch(identifier, options);
let pendingRequest =
identifier && store._fetchManager.getPendingFetch(identifier as StableExistingRecordIdentifier, options);
if (pendingRequest) {
return pendingRequest as Promise<T>;
}
Expand Down
4 changes: 4 additions & 0 deletions packages/model/src/-private/many-array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,10 @@ export default class RelatedCollection extends RecordArray {

return record;
}

destroy() {
super.destroy(false);
}
}
RelatedCollection.prototype.isAsync = false;
RelatedCollection.prototype.isPolymorphic = false;
Expand Down
Loading

0 comments on commit 662ce74

Please sign in to comment.