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

feat: implement a simple LifetimeService utility, improve document reconstruction #8798

Merged
merged 2 commits into from
Aug 26, 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
2 changes: 2 additions & 0 deletions ember-data-types/cache/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface SingleResourceDataDocument<T = StableExistingRecordIdentifier>
links?: Links | PaginationLinks;
meta?: Meta;
data: T | null;
included?: T[];
}

export interface CollectionResourceDataDocument<T = StableExistingRecordIdentifier> {
Expand All @@ -28,6 +29,7 @@ export interface CollectionResourceDataDocument<T = StableExistingRecordIdentifi
links?: Links | PaginationLinks;
meta?: Meta;
data: T[];
included?: T[];
}

export type ResourceDataDocument<T = StableExistingRecordIdentifier> =
Expand Down
51 changes: 40 additions & 11 deletions packages/json-api/src/-private/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,9 @@ export default class JSONAPICache implements Cache {
doc instanceof Error || (typeof doc.content === 'object' && doc.content !== null)
);
if (isErrorDocument(doc)) {
return this._putDocument(doc as StructuredErrorDocument<ResourceErrorDocument>);
return this._putDocument(doc as StructuredErrorDocument<ResourceErrorDocument>, undefined, undefined);
} else if (isMetaDocument(doc)) {
return this._putDocument(doc);
return this._putDocument(doc, undefined, undefined);
}

const jsonApiDoc = doc.content as SingleResourceDocument | CollectionResourceDocument;
Expand All @@ -191,7 +191,7 @@ export default class JSONAPICache implements Cache {

if (included) {
for (i = 0, length = included.length; i < length; i++) {
putOne(this, identifierCache, included[i]);
included[i] = putOne(this, identifierCache, included[i]);
}
}

Expand All @@ -202,11 +202,19 @@ export default class JSONAPICache implements Cache {
for (i = 0; i < length; i++) {
identifiers.push(putOne(this, identifierCache, jsonApiDoc.data[i]));
}
return this._putDocument(doc as StructuredDataDocument<CollectionResourceDocument>, identifiers);
return this._putDocument(
doc as StructuredDataDocument<CollectionResourceDocument>,
identifiers,
included as StableExistingRecordIdentifier[]
);
}

if (jsonApiDoc.data === null) {
return this._putDocument(doc as StructuredDataDocument<SingleResourceDocument>, null);
return this._putDocument(
doc as StructuredDataDocument<SingleResourceDocument>,
null,
included as StableExistingRecordIdentifier[]
);
}

assert(
Expand All @@ -215,22 +223,37 @@ export default class JSONAPICache implements Cache {
);

let identifier = putOne(this, identifierCache, jsonApiDoc.data);
return this._putDocument(doc as StructuredDataDocument<SingleResourceDocument>, identifier);
return this._putDocument(
doc as StructuredDataDocument<SingleResourceDocument>,
identifier,
included as StableExistingRecordIdentifier[]
);
}

_putDocument<T extends ResourceErrorDocument>(doc: StructuredErrorDocument<T>): ResourceErrorDocument;
_putDocument<T extends ResourceMetaDocument>(doc: StructuredDataDocument<T>): ResourceMetaDocument;
_putDocument<T extends ResourceErrorDocument>(
doc: StructuredErrorDocument<T>,
data: undefined,
included: undefined
): ResourceErrorDocument;
_putDocument<T extends ResourceMetaDocument>(
doc: StructuredDataDocument<T>,
data: undefined,
included: undefined
): ResourceMetaDocument;
_putDocument<T extends SingleResourceDocument>(
doc: StructuredDataDocument<T>,
data: StableExistingRecordIdentifier | null
data: StableExistingRecordIdentifier | null,
included: StableExistingRecordIdentifier[] | undefined
): SingleResourceDataDocument;
_putDocument<T extends CollectionResourceDocument>(
doc: StructuredDataDocument<T>,
data: StableExistingRecordIdentifier[]
data: StableExistingRecordIdentifier[],
included: StableExistingRecordIdentifier[] | undefined
): CollectionResourceDataDocument;
_putDocument<T extends ResourceDocument>(
doc: StructuredDocument<T>,
data?: StableExistingRecordIdentifier[] | StableExistingRecordIdentifier | null
data: StableExistingRecordIdentifier[] | StableExistingRecordIdentifier | null | undefined,
included: StableExistingRecordIdentifier[] | undefined
): SingleResourceDataDocument | CollectionResourceDataDocument | ResourceErrorDocument | ResourceMetaDocument {
// @ts-expect-error narrowing within is just horrible in TS :/
const resourceDocument: SingleResourceDataDocument | CollectionResourceDataDocument | ResourceErrorDocument =
Expand All @@ -240,6 +263,12 @@ export default class JSONAPICache implements Cache {
(resourceDocument as SingleResourceDataDocument | CollectionResourceDataDocument).data = data;
}

if (included !== undefined) {
assert(`There should not be included data on an Error document`, !isErrorDocument(doc));
assert(`There should not be included data on a Meta document`, !isMetaDocument(doc));
(resourceDocument as SingleResourceDataDocument | CollectionResourceDataDocument).included = included;
}

const request = doc.request as StoreRequestInfo | undefined;
const identifier = request ? this.__storeWrapper.identifierCache.getOrCreateDocumentIdentifier(request) : null;

Expand Down
99 changes: 99 additions & 0 deletions packages/request-utils/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { assert } from '@ember/debug';

import type Store from '@ember-data/store';
import { StableDocumentIdentifier } from '@ember-data/types/cache/identifier';

/**
* Simple utility function to assist in url building,
* query params, and other common request operations.
Expand Down Expand Up @@ -385,3 +388,99 @@ export function sortQueryParams(params: QueryParamsSource, options?: QueryParams
export function buildQueryParams(params: QueryParamsSource, options?: QueryParamsSerializationOptions): string {
return sortQueryParams(params, options).toString();
}
export interface CacheControlValue {
immutable?: boolean;
'max-age'?: number;
'must-revalidate'?: boolean;
'must-understand'?: boolean;
'no-cache'?: boolean;
'no-store'?: boolean;
'no-transform'?: boolean;
'only-if-cached'?: boolean;
private?: boolean;
'proxy-revalidate'?: boolean;
public?: boolean;
's-maxage'?: number;
'stale-if-error'?: number;
'stale-while-revalidate'?: number;
}

const NUMERIC_KEYS = new Set(['max-age', 's-maxage', 'stale-if-error', 'stale-while-revalidate']);

export function parseCacheControl(header: string): CacheControlValue {
let key = '';
let value = '';
let isParsingKey = true;
let cacheControlValue: CacheControlValue = {};

for (let i = 0; i < header.length; i++) {
let char = header.charAt(i);
if (char === ',') {
assert(`Invalid Cache-Control value, expected a value`, !isParsingKey || !NUMERIC_KEYS.has(key));
assert(
`Invalid Cache-Control value, expected a value after "=" but got ","`,
i === 0 || header.charAt(i - 1) !== '='
);
isParsingKey = true;
cacheControlValue[key] = NUMERIC_KEYS.has(key) ? Number.parseInt(value) : true;
key = '';
value = '';
continue;
} else if (char === '=') {
assert(`Invalid Cache-Control value, expected a value after "="`, i + 1 !== header.length);
isParsingKey = false;
} else if (char === ' ' || char === `\t` || char === `\n`) {
continue;
} else if (isParsingKey) {
key += char;
} else {
value += char;
}

if (i === header.length - 1) {
cacheControlValue[key] = NUMERIC_KEYS.has(key) ? Number.parseInt(value) : true;
}
}

return cacheControlValue;
}

function isStale(headers: Headers, expirationTime: number): boolean {
// const age = headers.get('age');
// const cacheControl = parseCacheControl(headers.get('cache-control') || '');
// const expires = headers.get('expires');
// const lastModified = headers.get('last-modified');
const date = headers.get('date');

if (!date) {
return true;
}

const time = new Date(date).getTime();
const now = Date.now();
const deadline = time + expirationTime;

const result = now > deadline;

return result;
}

export type LifetimesConfig = { apiCacheSoftExpires: number; apiCacheHardExpires: number };

export class LifetimesService {
declare store: Store;
declare config: LifetimesConfig;
constructor(store: Store, config: LifetimesConfig) {
this.store = store;
this.config = config;
}

isHardExpired(identifier: StableDocumentIdentifier): boolean {
const cached = this.store.cache.peekRequest(identifier);
return !cached || !cached.response || isStale(cached.response.headers, this.config.apiCacheHardExpires);
}
isSoftExpired(identifier: StableDocumentIdentifier): boolean {
const cached = this.store.cache.peekRequest(identifier);
return !cached || !cached.response || isStale(cached.response.headers, this.config.apiCacheSoftExpires);
}
}
4 changes: 4 additions & 0 deletions packages/request/src/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ const Fetch = {
const response = await _fetch(context.request.url!, context.request);
context.setResponse(response);

if (!response.headers.has('date')) {
response.headers.set('date', new Date().toUTCString());
}

// if we are an error, we will want to throw
if (!response.ok || response.status >= 400) {
const text = await response.text();
Expand Down