Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backport into release #8750

Merged
merged 4 commits into from
Jul 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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