Skip to content

Commit

Permalink
Merge pull request open-source-labs#118 from oslabs-beta/main
Browse files Browse the repository at this point in the history
Merge Quell 8.0
  • Loading branch information
jonahpw authored May 30, 2023
2 parents 6547304 + 9f886b6 commit c3ec13b
Show file tree
Hide file tree
Showing 38 changed files with 671 additions and 153 deletions.
248 changes: 168 additions & 80 deletions quell-client/src/Quellify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,73 @@ import { DocumentNode } from 'graphql';
import { Collection } from 'lokijs';
import { parse } from 'graphql/language/parser';
import determineType from './helpers/determineType';
import { LRUCache } from 'lru-cache';
import Loki from 'lokijs';
import type {
import {
CostParamsType,
IDLokiCacheType,
LokiGetType,
FetchObjType,
JSONObject,
JSONValue,
ClientErrorType
} from './types';


const lokidb: Loki = new Loki('client-cache');
let lokiCache: Collection = lokidb.addCollection('loki-client-cache', {
disableMeta: true
});

/* The IDCache is a psuedo-join table that is a JSON object in memory,
that uses cached queries to return their location ($loki (lokiID)) from LokiCache.
i.e. {{JSONStringifiedQuery: $lokiID}}
const IDCache = {
query1: $loki1,
query2: $loki2,
query3: $loki3
};
/**
* Function to manually removes item from cache
*/
let IDCache: IDLokiCacheType = {};
const invalidateCache = (query: string): void => {
const cachedEntry = lokiCache.findOne({ query });
if (cachedEntry) {
lruCache.delete(query);
lokiCache.remove(cachedEntry);
}
}

/**
* Clears existing cache and ID cache and resets to a new cache.
* Implement LRU caching strategy
*/

// Set the maximum cache size on LRU cache
const MAX_CACHE_SIZE: number = 2;
const lruCache = new LRUCache<string, LokiGetType>({
max: MAX_CACHE_SIZE,
});

// Track the order of accessed queries
const lruCacheOrder: string[] = [];

// Function to update LRU Cache on each query
const updateLRUCache = (query: string, results: JSONObject): void => {
const cacheSize = lruCacheOrder.length;
if (cacheSize >= MAX_CACHE_SIZE) {
const leastRecentlyUsedQuery = lruCacheOrder.shift();
if (leastRecentlyUsedQuery) {
invalidateCache(leastRecentlyUsedQuery);
}
}
lruCacheOrder.push(query);
lruCache.set(query, results as LokiGetType);
};
/**
* Clears entire existing cache and ID cache and resets to a new cache.
*/
const clearCache = (): void => {
lokidb.removeCollection('loki-client-cache');
lokiCache = lokidb.addCollection('loki-client-cache', {
disableMeta: true
});
IDCache = {};
lruCache.clear();
console.log('Client cache has been cleared.');
};



/**
* Quellify replaces the need for front-end developers who are using GraphQL to communicate with their servers
* to write fetch requests. Quell provides caching functionality that a normal fetch request would not provide.
Expand All @@ -48,22 +77,23 @@ const clearCache = (): void => {
* @param {object} costOptions - Any changes to the default cost options for the query or mutation.
*
* default costOptions = {
* maxCost: 5000, // maximum cost allowed before a request is rejected
* mutationCost: 5, // cost of a mutation
* objectCost: 2, // cost of retrieving an object
* scalarCost: 1, // cost of retrieving a scalar
* depthCostFactor: 1.5, // multiplicative cost of each depth level
* depthMax: 10, //depth limit parameter
* ipRate: 3 // requests allowed per second
* }
*
*/

async function Quellify(
endPoint: string,
query: string,
costOptions: CostParamsType
costOptions: CostParamsType,
variables?: Record<string, any>
) {

// Check the LRU cache before performing fetch request
const cachedResults = lruCache.get(query);
if (cachedResults) {
return [cachedResults, true];
}

/**
* Fetch configuration for post requests that is passed to the performFetch function.
*/
Expand All @@ -74,7 +104,6 @@ async function Quellify(
},
body: JSON.stringify({ query, costOptions })
};

/**
* Fetch configuration for delete requests that is passed to the performFetch function.
*/
Expand All @@ -85,7 +114,6 @@ async function Quellify(
},
body: JSON.stringify({ query, costOptions })
};

/**
* Makes requests to the GraphQL endpoint.
* @param {FetchObjType} [fetchConfig] - (optional) Configuration options for the fetch call.
Expand All @@ -97,6 +125,7 @@ async function Quellify(
try {
const data = await fetch(endPoint, fetchConfig);
const response = await data.json();
updateLRUCache(query, response.queryResponse.data);
return response.queryResponse.data;
} catch (error) {
const err: ClientErrorType = {
Expand All @@ -110,6 +139,49 @@ async function Quellify(
throw error;
}
};
// Refetch LRU cache
const refetchLRUCache = async (): Promise<void> => {
try {
const cacheSize = lruCacheOrder.length;
// i < cacheSize - 1 because the last query in the order array is the current query
for (let i = 0; i < cacheSize - 1; i++) {
const query = lruCacheOrder[i];
// Get operation type for query
const oldAST: DocumentNode = parse(query);
const { operationType } = determineType(oldAST);
// If the operation type is not a query, leave it out of the refetch
if (operationType !== 'query') {
continue;
}
// If the operation type is a query, refetch the query from the LRU cache
const cachedResults = lruCache.get(query);
if (cachedResults) {
// Fetch configuration for post requests that is passed to the performFetch function.
const fetchConfig: FetchObjType = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ query, costOptions })
};
const data = await fetch(endPoint, fetchConfig);
const response = await data.json();
updateLRUCache(query, response.queryResponse.data);
}
}
} catch (error) {
const err: ClientErrorType = {
log: `Error when trying to refetch LRU cache: ${error}.`,
status: 400,
message: {
err: 'Error in refetchLRUCache. Check server log for more details.'
}
};
console.log('Error when refetching LRU cache: ', err);
throw error;
}
};


// Create AST based on the input query using the parse method available in the GraphQL library
// (further reading: https://en.wikipedia.org/wiki/Abstract_syntax_tree).
Expand All @@ -129,68 +201,84 @@ async function Quellify(
// The second element in the return array is a boolean that the data was not found in the lokiCache.
return [parsedData, false];
} else if (operationType === 'mutation') {
// TODO: If the operation is a mutation, we are currently clearing the cache because it is stale.
// The goal would be to instead have a normalized cache and update the cache following a mutation.
clearCache();

// Assign mutationType
const mutationType: string = Object.keys(proto)[0];

// Determine whether the mutation is a add, delete, or update mutation.
if (
mutationType.includes('add') ||
mutationType.includes('new') ||
mutationType.includes('create') ||
mutationType.includes('make')
) {
// If the mutation is a create mutation, execute the mutation and return the result.
// Assume create mutations will start with 'add', 'new', 'create', or 'make'.
const parsedData: JSONValue = await performFetch(postFetch);
// The second element in the return array is a boolean that the data was not found in the lokiCache.
return [parsedData, false];
} else if (
mutationType.includes('delete') ||
mutationType.includes('remove')
) {
// If the mutation is a delete mutation, execute the mutation and return the result.
// Assume delete mutations will start with 'delete' or 'remove'.
const parsedData: JSONObject = await performFetch(deleteFetch);
// The second element in the return array is a boolean that the data was not found in the lokiCache.
return [parsedData, false];
} else if (mutationType.includes('update')) {
// If the mutation is an update mutation, execute the mutation and return the result.
// Assume update mutations will start with 'update'.
const parsedData: JSONValue = await performFetch(postFetch);
// The second element in the return array is a boolean that the data was not found in the lokiCache.
return [parsedData, false];
// Check if mutation is an add mutation
if (
mutationType.includes('add') ||
mutationType.includes('new') ||
mutationType.includes('create') ||
mutationType.includes('make')
) {
// Execute a fetch request with the query
const parsedData: JSONObject = await performFetch(postFetch);
if (parsedData) {
const addedEntry = lokiCache.insert(parsedData);
// Refetch each query in the LRU cache to update the cache
refetchLRUCache();
return [addedEntry, false];
}
}

// Check if mutation is an edit mutation
else if(
mutationType.includes('edit') ||
mutationType.includes('update')
){
// Execute a fetch request with the query
const parsedData: JSONObject = await performFetch(postFetch);
if (parsedData) {
// Find the existing entry by its ID
const cachedEntry = lokiCache.findOne({ id: parsedData.id });
if (cachedEntry) {
// Update the existing entry with the new data
Object.assign(cachedEntry, parsedData);
// Update the entry in the Loki cache
lokiCache.update(cachedEntry);
// Update LRU Cache
updateLRUCache(query, cachedEntry);
refetchLRUCache();
return [cachedEntry, false];
}
}
}
// Check if mutation is a delete mutation
else if (
mutationType.includes('delete') ||
mutationType.includes('remove')
) {
// Execute a fetch request with the query
const parsedData: JSONObject = await performFetch(deleteFetch);
if (parsedData) {
// Find the existing entry by its ID
const cachedEntry = lokiCache.findOne({ id: parsedData.id });
if (cachedEntry) {
// Remove the item from cache
lokiCache.remove(cachedEntry);
invalidateCache(query);
// Refetch each query in the LRU cache to update the cache
refetchLRUCache();
return [cachedEntry, false];
} else {
return [null, false];
}
}
}
// Operation type does not meet mutation or unquellable types.
// In other words, it is a Query
} else {
// Otherwise, the operation is a query.
// Check to see if this query has been made already by checking the IDCache for the query.
if (IDCache[query]) {
// If the query has a $loki ID in the IDCache, retrieve and return the results from the lokiCache.

// Grab the $loki ID from the IDCache.
const queryID: number = IDCache[query];

// Grab the results from lokiCache for the $loki ID.
const results: LokiGetType = lokiCache.get(queryID);

// The second element in the return array is a boolean that the data was found in the lokiCache.
return [results, true];
} else {
// If the query has not been made already, execute a fetch request with the query.
const parsedData: JSONObject = await performFetch(postFetch);
// Add the new data to the lokiCache.
if (parsedData) {
const addedEntry = lokiCache.insert(parsedData);
// Add query $loki ID to IDcache at query key
IDCache[query] = addedEntry.$loki;
// The second element in the return array is a boolean that the data was not found in the lokiCache.
return [addedEntry, false];
// If the query has not been made already, execute a fetch request with the query.
const parsedData: JSONObject = await performFetch(postFetch);
// Add the new data to the lokiCache.
if (parsedData) {
const addedEntry = lokiCache.insert(parsedData);
// Add query and results to lruCache
lruCache.set(query, addedEntry);
// The second element in the return array is a boolean that the data was not found in the lokiCache.
return [addedEntry, false];
}
}
}
}

export { Quellify, clearCache as clearLokiCache };
export { Quellify, clearCache as clearLokiCache, lruCache };
Binary file removed quell-extension/dist/assets/Quell_full_size.png
Binary file not shown.
Binary file removed quell-extension/dist/assets/favicon.ico
Binary file not shown.
Binary file modified quell-extension/dist/assets/icon128.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified quell-extension/dist/assets/icon16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added quell-extension/dist/assets/icon32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified quell-extension/dist/assets/icon48.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed quell-extension/dist/assets/quell-logo-horiz.png
Binary file not shown.
2 changes: 1 addition & 1 deletion quell-extension/dist/manifest.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"description":"Developer tool for the Quell JavaScript library: https://quellql.io","version":"0.9","manifest_version":3,"name":"Quell Developer Tool","homepage_url":"https://quellql.io","author":"Chang Cai, Robert Howton, Joshua Jordan","action":{"default_icon":{"16":"./assets/icon16.png","48":"./assets/icon48.png","128":"./assets/icon128.png"},"default_title":"Quell Developer Tool"},"devtools_page":"devtools.html","background":{"service_worker":"background.bundle.js"},"icons":{"16":"./assets/icon16.png","48":"./assets/icon48.png","128":"./assets/icon128.png"}}
{"description":"Developer tool for the Quell JavaScript library: https://quell.dev","version":"2.0","manifest_version":3,"name":"Quell Developer Tool","permissions":["contextMenus"],"homepage_url":"https://quell.dev","author":"Michael Lav, Lenny Yambao, Jonah Weinbaum, Justin Hua, Chang Cai, Robert Howton, Joshua Jordan, Angelo Chengcuenca, Emily Hoang, Keely Timms, Yusuf Bhaiyat","action":{"default_icon":{"16":"./assets/icon16.png","48":"./assets/icon48.png","128":"./assets/icon128.png"},"default_title":"Quell Developer Tool"},"devtools_page":"devtools.html","icons":{"16":"./assets/icon16.png","48":"./assets/icon48.png","128":"./assets/icon128.png"},"contextMenus":{"id":"quellMenu","title":"Quell","contexts":["page"]},"background":{"service_worker":"background.bundle.js","type":"module"}}
2 changes: 1 addition & 1 deletion quell-extension/dist/panel.bundle.js

Large diffs are not rendered by default.

20 changes: 20 additions & 0 deletions quell-extension/dist/panel.bundle.js.LICENSE.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,26 @@ object-assign
@license MIT
*/

/**
* @license React
* use-sync-external-store-shim.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* @license React
* use-sync-external-store-shim/with-selector.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/** @license React v0.20.2
* scheduler.production.min.js
*
Expand Down
Loading

0 comments on commit c3ec13b

Please sign in to comment.