feat: 🎸 Update how we store tokens in indexed-db (#2223)
Added logic to make the list tokens stored in indexed DB more granular. I get a hash of the query parameters that will get sent to the controller and use that as the id for the token. This also means extra logic for when a token is invalid as we will need to delete all tokens of the same resource type.
This commit is contained in:
1 parent
6c59baa413
commit
5d32d987bc
13 files changed
+121
-33
No files matched your search
@@ -283,7 +283,7 @@ export default class ApplicationAdapter extends RESTAdapter.extend(
|
||||
const baseError = {
|
||||
isInvalid: true,
|
||||
status,
|
||||
code: payload.code,
|
||||
code: payload.kind,
|
||||
detail: payload.message,
|
||||
source: { pointer: '/data' },
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { inject as 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
|
||||
@@ -34,9 +35,10 @@ export default class IndexedDbHandler {
|
||||
}
|
||||
|
||||
let { page, pageSize, query: queryObj, ...remainingQuery } = query;
|
||||
const tokenKey = `${type}-${hashCode(remainingQuery)}`;
|
||||
|
||||
if (!peekIndexedDB) {
|
||||
const listToken = await indexedDb.token.get(type);
|
||||
const listToken = await indexedDb.token.get(tokenKey);
|
||||
const adapter = store.adapterFor(type);
|
||||
const schema = store.modelFor(type);
|
||||
|
||||
@@ -44,35 +46,41 @@ export default class IndexedDbHandler {
|
||||
try {
|
||||
payload = await adapter.query(store, schema, {
|
||||
...remainingQuery,
|
||||
scope_id: 'global',
|
||||
recursive: true,
|
||||
list_token: listToken?.token,
|
||||
});
|
||||
} catch (e) {
|
||||
// If we get an invalid list token, we'll delete the token and
|
||||
// clear the DB and try again without a token.
|
||||
if (
|
||||
e?.errors[0].status === 400 &&
|
||||
e?.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);
|
||||
|
||||
payload = await adapter.query(store, schema, {
|
||||
...remainingQuery,
|
||||
});
|
||||
} catch (e) {
|
||||
// If we get any error that's not a rate limiting error,
|
||||
// assume the token is not good anymore. We'll delete the token and
|
||||
// clear the DB and try again without a token.
|
||||
// TODO: Probably should check for 400 and bad token error specifically instead?
|
||||
if (e?.errors[0].status !== 429) {
|
||||
await indexedDb[type].clear();
|
||||
await indexedDb.token.delete(type);
|
||||
|
||||
payload = await adapter.query(store, schema, {
|
||||
...remainingQuery,
|
||||
scope_id: 'global',
|
||||
recursive: true,
|
||||
list_token: null,
|
||||
});
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Store the token we just got back from the payload
|
||||
// Store the token we just got back from the payload if it exists
|
||||
if (payload.list_token) {
|
||||
await indexedDb.token.put({
|
||||
id: type,
|
||||
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) {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Takes an object and gets the JSON stringified version of it to be converted
|
||||
* to a hash code. Taken from https://stackoverflow.com/a/8076436
|
||||
* @param obj
|
||||
*/
|
||||
export const hashCode = (obj) => {
|
||||
if (!obj) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Use a replacer function to sort the keys of the object so we get the same hashcode
|
||||
// regardless of the order of the keys.
|
||||
const jsonString = JSON.stringify(obj, Object.keys(obj).sort());
|
||||
|
||||
let hash = 0;
|
||||
for (let i = 0; i < jsonString.length; i++) {
|
||||
let code = jsonString.charCodeAt(i);
|
||||
hash = (hash << 5) - hash + code;
|
||||
hash = hash & hash; // Convert to 32bit integer
|
||||
}
|
||||
|
||||
// Make sure the hash is positive since we're using it as an ID
|
||||
return Math.abs(hash);
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import { hashCode } from 'api/utils/hash-code';
|
||||
import { module, test } from 'qunit';
|
||||
|
||||
module('Unit | Utility | hash-code', function () {
|
||||
test('it hashes', function (assert) {
|
||||
const result = hashCode({ a: 1, b: 2 });
|
||||
assert.ok(result);
|
||||
});
|
||||
|
||||
test('it hashes to same result regardless of object order', function (assert) {
|
||||
const first = hashCode({ a: 1, b: 2, c: 3 });
|
||||
const second = hashCode({ b: 2, c: 3, a: 1 });
|
||||
assert.strictEqual(first, second);
|
||||
});
|
||||
|
||||
test('it hashes to different result if objects are different', function (assert) {
|
||||
const first = hashCode({ a: 1, b: 2, c: 3 });
|
||||
const second = hashCode({ a: '1', b: '2', c: '3' });
|
||||
assert.notStrictEqual(first, second);
|
||||
});
|
||||
|
||||
test('it hashes an empty object', function (assert) {
|
||||
const result = hashCode({});
|
||||
assert.ok(result);
|
||||
});
|
||||
|
||||
test('it hashes null to 0', function (assert) {
|
||||
const result = hashCode(null);
|
||||
assert.strictEqual(result, 0);
|
||||
});
|
||||
|
||||
test('it hashes undefined to 0', function (assert) {
|
||||
const result = hashCode(undefined);
|
||||
assert.strictEqual(result, 0);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user