235 lines
6.6 KiB
JavaScript
235 lines
6.6 KiB
JavaScript
/**
|
|
* Copyright (c) HashiCorp, Inc.
|
|
* SPDX-License-Identifier: BUSL-1.1
|
|
*/
|
|
|
|
import { modelIndexes } from 'api/services/indexed-db';
|
|
import { service } from '@ember/service';
|
|
import { getOwner, setOwner } from '@ember/application';
|
|
import { queryIndexedDb } from '../utils/indexed-db-query';
|
|
import { paginateResults } from '../utils/paginate-results';
|
|
import { hashCode } from '../utils/hash-code';
|
|
|
|
/**
|
|
* Handler to sit in front of the API request layer
|
|
* so we can handle any caching with indexedDb first
|
|
*/
|
|
export default class IndexedDbHandler {
|
|
@service indexedDb;
|
|
|
|
batchLimit = 10_000;
|
|
|
|
constructor(context) {
|
|
setOwner(this, getOwner(context));
|
|
}
|
|
|
|
async request(context, next) {
|
|
const { batchLimit } = this;
|
|
|
|
switch (context.request.op) {
|
|
case 'query': {
|
|
const { store, data } = context.request;
|
|
// TODO: Remove storeToken option as this is a temporary fix for auth-methods.
|
|
const {
|
|
type,
|
|
query,
|
|
options: {
|
|
pushToStore = true,
|
|
peekIndexedDB = false,
|
|
storeToken = true,
|
|
} = {},
|
|
} = data;
|
|
const supportedModels = Object.keys(modelIndexes);
|
|
const { db: indexedDb } = this.indexedDb ?? {};
|
|
|
|
// Go through normal flow if we don't yet support the model
|
|
// or if we don't have an indexedDb instance
|
|
if (!supportedModels.includes(type) || !indexedDb) {
|
|
return next(context.request);
|
|
}
|
|
|
|
let { page, pageSize, query: queryObj, ...remainingQuery } = query;
|
|
|
|
const adapter = store.adapterFor(type);
|
|
const schema = store.modelFor(type);
|
|
const serializer = store.serializerFor(type);
|
|
|
|
if (!peekIndexedDB) {
|
|
const tokenKey = `${type}-${hashCode(remainingQuery)}`;
|
|
let payload;
|
|
let listToken;
|
|
let writeToIndexedDbPromise;
|
|
|
|
if (storeToken) {
|
|
const tokenObj = await indexedDb.token.get(tokenKey);
|
|
listToken = tokenObj?.token;
|
|
}
|
|
|
|
do {
|
|
try {
|
|
payload = await adapter.query(store, schema, {
|
|
...remainingQuery,
|
|
list_token: listToken,
|
|
batchLimit: batchLimit,
|
|
});
|
|
|
|
// await the previous writeToIndexedDbPromise before writing to indexedDb again
|
|
if (writeToIndexedDbPromise) {
|
|
await writeToIndexedDbPromise;
|
|
}
|
|
} catch (err) {
|
|
payload = await this.retryQueryFailure({
|
|
err,
|
|
indexedDb,
|
|
type,
|
|
adapter,
|
|
store,
|
|
schema,
|
|
remainingQuery,
|
|
});
|
|
}
|
|
listToken = payload.list_token;
|
|
|
|
// Don't await immediately and let the next query run while we write to indexedDb
|
|
writeToIndexedDbPromise = this.writeToIndexedDb({
|
|
payload,
|
|
storeToken,
|
|
indexedDb,
|
|
tokenKey,
|
|
type,
|
|
serializer,
|
|
store,
|
|
schema,
|
|
});
|
|
} while (payload.response_type === 'delta');
|
|
|
|
await writeToIndexedDbPromise;
|
|
}
|
|
|
|
const indexedDbResults = await queryIndexedDb(
|
|
indexedDb,
|
|
type,
|
|
queryObj,
|
|
);
|
|
|
|
const dbRecords = paginateResults(indexedDbResults, page, pageSize).map(
|
|
(item) =>
|
|
this.indexedDb.normalizeData({
|
|
data: item,
|
|
cleanData: false,
|
|
schema,
|
|
serializer,
|
|
}),
|
|
);
|
|
|
|
// Return the raw data if we don't push to the store.
|
|
let records = dbRecords.map((record) => ({
|
|
...record.attributes,
|
|
id: record.id,
|
|
}));
|
|
|
|
if (pushToStore) {
|
|
records = store.push({ data: dbRecords });
|
|
}
|
|
|
|
// Set a meta property on the array to store the total items of the results.
|
|
// This isn't conventional but is better than returning an ArrayProxy
|
|
// or EmberArray since the ember store query method asserts it has to be an array
|
|
// so we can't just return an object.
|
|
records.meta = { totalItems: indexedDbResults.length };
|
|
|
|
return records;
|
|
}
|
|
default:
|
|
return next(context.request);
|
|
}
|
|
}
|
|
|
|
async retryQueryFailure({
|
|
err,
|
|
indexedDb,
|
|
type,
|
|
adapter,
|
|
store,
|
|
schema,
|
|
remainingQuery,
|
|
}) {
|
|
// If we get an invalid list token, we'll delete the token and
|
|
// clear the DB and try again without a token.
|
|
if (
|
|
err?.errors[0].status === 400 &&
|
|
err?.errors[0].code === 'invalid list token'
|
|
) {
|
|
await indexedDb[type].clear();
|
|
// Delete all tokens of the same resource type so we don't keep clearing the DB.
|
|
// Even if other tokens may still be valid, we might have cleared data that they
|
|
// depended on so we should clear all tokens of the same type when clearing the DB.
|
|
const tokenKeys = await indexedDb.token
|
|
.where(':id')
|
|
.startsWith(type)
|
|
.primaryKeys();
|
|
|
|
await indexedDb.token.bulkDelete(tokenKeys);
|
|
|
|
return await adapter.query(store, schema, {
|
|
...remainingQuery,
|
|
});
|
|
} else {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async writeToIndexedDb({
|
|
payload,
|
|
storeToken,
|
|
indexedDb,
|
|
tokenKey,
|
|
type,
|
|
serializer,
|
|
store,
|
|
schema,
|
|
}) {
|
|
// Store the token we just got back from the payload if it exists
|
|
if (payload.list_token && storeToken) {
|
|
await indexedDb.token.put({
|
|
id: tokenKey,
|
|
token: payload.list_token,
|
|
});
|
|
}
|
|
|
|
// Remove any records from the DB if the API indicates they've been deleted
|
|
if (payload.removed_ids?.length > 0) {
|
|
await indexedDb[type].bulkDelete(payload.removed_ids);
|
|
}
|
|
|
|
// This is a temporary fix of clearing the DB (specifically for auth-methods)
|
|
// since we are not storing the token we do not get back a list of removed_ids
|
|
// from the API call and we do not want deleted items from showing in list view.
|
|
if (!storeToken) {
|
|
await indexedDb[type].clear();
|
|
}
|
|
|
|
const normalizedPayload = serializer.normalizeResponse(
|
|
store,
|
|
schema,
|
|
payload,
|
|
null,
|
|
'query',
|
|
);
|
|
|
|
const { data: payloadData } = normalizedPayload;
|
|
|
|
// Store the new data we just got back from the API refresh
|
|
const items = payloadData.map((datum) =>
|
|
this.indexedDb.normalizeData({
|
|
data: datum,
|
|
cleanData: true,
|
|
schema,
|
|
serializer,
|
|
}),
|
|
);
|
|
|
|
await indexedDb[type].bulkPut(items);
|
|
}
|
|
}
|