Use latest ~owls NC trade values API
They're moving away from the bulk endpoint to individual item data lookups, so we're updating to match!
This commit is contained in:
parent
e6176b6c16
commit
4c9dbf91fb
6 changed files with 161 additions and 33 deletions
|
@ -81,7 +81,8 @@
|
||||||
"build-cached-data": "ts-node --compiler=typescript-cached-transpile --transpile-only -r dotenv/config scripts/build-cached-data.js",
|
"build-cached-data": "ts-node --compiler=typescript-cached-transpile --transpile-only -r dotenv/config scripts/build-cached-data.js",
|
||||||
"cache-asset-manifests": "ts-node --compiler=typescript-cached-transpile --transpile-only -r dotenv/config scripts/cache-asset-manifests.js",
|
"cache-asset-manifests": "ts-node --compiler=typescript-cached-transpile --transpile-only -r dotenv/config scripts/cache-asset-manifests.js",
|
||||||
"delete-user": "ts-node --compiler=typescript-cached-transpile --transpile-only -r dotenv/config scripts/delete-user.js",
|
"delete-user": "ts-node --compiler=typescript-cached-transpile --transpile-only -r dotenv/config scripts/delete-user.js",
|
||||||
"export-users-to-auth0": "ts-node --compiler=typescript-cached-transpile --transpile-only -r dotenv/config scripts/export-users-to-auth0.js"
|
"export-users-to-auth0": "ts-node --compiler=typescript-cached-transpile --transpile-only -r dotenv/config scripts/export-users-to-auth0.js",
|
||||||
|
"validate-owls-data": "ts-node --compiler=typescript-cached-transpile --transpile-only -r dotenv/config scripts/validate-owls-data.js"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
|
|
|
@ -1,3 +1,10 @@
|
||||||
|
/**
|
||||||
|
* /api/allNCTradeValues returns all the NC trade values OWLS has!
|
||||||
|
*
|
||||||
|
* NOTE: We no longer use API endpoint as a caching layer for individual item
|
||||||
|
* data requests. See `nc-trade-values.js` for the new system we use!
|
||||||
|
* This endpoint is therefore deprecated and might vanish.
|
||||||
|
*/
|
||||||
const beeline = require("honeycomb-beeline")({
|
const beeline = require("honeycomb-beeline")({
|
||||||
writeKey: process.env["HONEYCOMB_WRITE_KEY"],
|
writeKey: process.env["HONEYCOMB_WRITE_KEY"],
|
||||||
dataset:
|
dataset:
|
||||||
|
|
72
scripts/validate-owls-data.js
Normal file
72
scripts/validate-owls-data.js
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
/**
|
||||||
|
* This script compares the data we get from the OWLS bulk endpoint to the data
|
||||||
|
* we get by loading the data with the new individual endpoint! This will help
|
||||||
|
* us check for bugs as we switch over!
|
||||||
|
*/
|
||||||
|
const fetch = require("node-fetch");
|
||||||
|
const connectToDb = require("../src/server/db");
|
||||||
|
const buildLoaders = require("../src/server/loaders");
|
||||||
|
const { getOWLSTradeValue } = require("../src/server/nc-trade-values");
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const db = await connectToDb();
|
||||||
|
const { itemTranslationLoader } = buildLoaders(db);
|
||||||
|
|
||||||
|
// Load the bulk data. We're gonna loop through it all!
|
||||||
|
const bulkEndpointRes = await fetch(
|
||||||
|
`http://localhost:3000/api/allNCTradeValues`
|
||||||
|
);
|
||||||
|
const bulkEndpointData = await bulkEndpointRes.json();
|
||||||
|
|
||||||
|
// Load the item names, bc our bulk data is keyed by item ID.
|
||||||
|
const itemIds = Object.keys(bulkEndpointData);
|
||||||
|
const itemTranslations = await itemTranslationLoader.loadMany(itemIds);
|
||||||
|
|
||||||
|
// Load the OWLs data! I don't do any of it in parallel, because I'm worried
|
||||||
|
// about upsetting the server, y'know?
|
||||||
|
let numChecked = 0;
|
||||||
|
let numSuccesses = 0;
|
||||||
|
let numFailures = 0;
|
||||||
|
for (const { name, itemId } of itemTranslations) {
|
||||||
|
if (numChecked % 100 === 0) {
|
||||||
|
console.info(`Checked ${numChecked} items`);
|
||||||
|
}
|
||||||
|
numChecked++;
|
||||||
|
const expectedValueText = bulkEndpointData[itemId].valueText;
|
||||||
|
let actualValue;
|
||||||
|
try {
|
||||||
|
actualValue = await getOWLSTradeValue(name);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[${itemId} / ${name}]: Error loading data:\n`, error);
|
||||||
|
numFailures++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (actualValue == null) {
|
||||||
|
console.error(`[${itemId} / ${name}]: No value found.`);
|
||||||
|
numFailures++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const actualValueText = actualValue.valueText;
|
||||||
|
if (expectedValueText !== actualValueText) {
|
||||||
|
console.error(
|
||||||
|
`[${itemId}]: Value did not match. ` +
|
||||||
|
`Expected: ${JSON.stringify(expectedValueText)}. ` +
|
||||||
|
`Actual: ${JSON.stringify(actualValueText)}`
|
||||||
|
);
|
||||||
|
numFailures++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
numSuccesses++;
|
||||||
|
}
|
||||||
|
console.info(
|
||||||
|
`Checked all ${numChecked} items. ` +
|
||||||
|
`${numSuccesses} successes, ${numFailures} failures.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
})
|
||||||
|
.then(() => process.exit());
|
|
@ -1,5 +1,4 @@
|
||||||
import DataLoader from "dataloader";
|
import DataLoader from "dataloader";
|
||||||
import fetch from "node-fetch";
|
|
||||||
import { normalizeRow } from "./util";
|
import { normalizeRow } from "./util";
|
||||||
|
|
||||||
const buildClosetListLoader = (db) =>
|
const buildClosetListLoader = (db) =>
|
||||||
|
@ -826,31 +825,6 @@ const buildItemTradesLoader = (db, loaders) =>
|
||||||
{ cacheKeyFn: ({ itemId, isOwned }) => `${itemId}-${isOwned}` }
|
{ cacheKeyFn: ({ itemId, isOwned }) => `${itemId}-${isOwned}` }
|
||||||
);
|
);
|
||||||
|
|
||||||
const buildItemNCTradeValueLoader = () =>
|
|
||||||
new DataLoader(async (itemIds) => {
|
|
||||||
// This loader calls our /api/allNCTradeValues endpoint, to take advantage
|
|
||||||
// of the CDN caching. This helps us respond a bit faster than calling the
|
|
||||||
// API directly would, and avoids putting network pressure or caching
|
|
||||||
// complexity on our ~owls friends! (It would also be pretty reasonable to
|
|
||||||
// do this as a process-level cache or something instead, but I'm reusing
|
|
||||||
// Waka code from when we were on a more distributed system where that
|
|
||||||
// wouldn't have worked out, and I don't think the effort to refactor this
|
|
||||||
// just for the potential perf win is worthy!)
|
|
||||||
const url = process.env.NODE_ENV === "production"
|
|
||||||
? "https://impress-2020.openneo.net/api/allNCTradeValues"
|
|
||||||
: "http://localhost:3000/api/allNCTradeValues";
|
|
||||||
const res = await fetch(url);
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error(
|
|
||||||
`Error loading /api/allNCTradeValues: ${res.status} ${res.statusText}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const allNCTradeValues = await res.json();
|
|
||||||
|
|
||||||
return itemIds.map((itemId) => allNCTradeValues[itemId]);
|
|
||||||
});
|
|
||||||
|
|
||||||
const buildPetTypeLoader = (db, loaders) =>
|
const buildPetTypeLoader = (db, loaders) =>
|
||||||
new DataLoader(async (petTypeIds) => {
|
new DataLoader(async (petTypeIds) => {
|
||||||
const qs = petTypeIds.map((_) => "?").join(",");
|
const qs = petTypeIds.map((_) => "?").join(",");
|
||||||
|
@ -1521,7 +1495,6 @@ function buildLoaders(db) {
|
||||||
db
|
db
|
||||||
);
|
);
|
||||||
loaders.itemTradesLoader = buildItemTradesLoader(db, loaders);
|
loaders.itemTradesLoader = buildItemTradesLoader(db, loaders);
|
||||||
loaders.itemNCTradeValueLoader = buildItemNCTradeValueLoader();
|
|
||||||
loaders.petTypeLoader = buildPetTypeLoader(db, loaders);
|
loaders.petTypeLoader = buildPetTypeLoader(db, loaders);
|
||||||
loaders.petTypeBySpeciesAndColorLoader = buildPetTypeBySpeciesAndColorLoader(
|
loaders.petTypeBySpeciesAndColorLoader = buildPetTypeBySpeciesAndColorLoader(
|
||||||
db,
|
db,
|
||||||
|
|
48
src/server/nc-trade-values.js
Normal file
48
src/server/nc-trade-values.js
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import LRU from "lru-cache";
|
||||||
|
import fetch from "node-fetch";
|
||||||
|
|
||||||
|
// NOTE: I didn't validate any of these cache settings very carefully, just
|
||||||
|
// that the cache works basically at all. I figure if it's misconfigured
|
||||||
|
// in a way that causes stale data or memory issues, we'll discover that
|
||||||
|
// when it becomes a problem!
|
||||||
|
const owlsTradeValueCache = new LRU({
|
||||||
|
// Cache up to 500 entries (they're small!), for 15 minutes each. (The 15min
|
||||||
|
// cap should keep the cache much smaller than that in practice I think!)
|
||||||
|
max: 500,
|
||||||
|
ttl: 1000 * 60 * 15,
|
||||||
|
|
||||||
|
// We also enforce a ~5MB total limit, just to make sure some kind of issue
|
||||||
|
// in the API communication won't cause huge memory leaks. (Size in memory is
|
||||||
|
// approximated as the length of the key string and the length of the value
|
||||||
|
// object in JSON. Not exactly accurate, but very close!)
|
||||||
|
maxSize: 5_000_000,
|
||||||
|
sizeCalculation: (value, key) => JSON.stringify(value).length + key.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function getOWLSTradeValue(itemName) {
|
||||||
|
const cachedValue = owlsTradeValueCache.get(itemName);
|
||||||
|
if (cachedValue != null) {
|
||||||
|
console.debug("[getOWLSTradeValue] Serving cached value", cachedValue);
|
||||||
|
return cachedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newValue = await loadOWLSTradeValueFromAPI(itemName);
|
||||||
|
owlsTradeValueCache.set(itemName, newValue);
|
||||||
|
return newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadOWLSTradeValueFromAPI(itemName) {
|
||||||
|
const res = await fetch(
|
||||||
|
`https://neo-owls.net/itemdata/${encodeURIComponent(itemName)}`
|
||||||
|
);
|
||||||
|
if (!res.ok) {
|
||||||
|
// TODO: Differentiate between 500 and 404. (Right now, when the item isn't
|
||||||
|
// found, it returns a 500, so it's hard to say.)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const data = await res.json();
|
||||||
|
return {
|
||||||
|
valueText: data.owls_value,
|
||||||
|
lastUpdated: data.last_updated,
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
import { gql } from "apollo-server";
|
import { gql } from "apollo-server";
|
||||||
|
import { getOWLSTradeValue } from "../nc-trade-values";
|
||||||
import {
|
import {
|
||||||
getRestrictedZoneIds,
|
getRestrictedZoneIds,
|
||||||
normalizeRow,
|
normalizeRow,
|
||||||
|
@ -332,9 +333,7 @@ const resolvers = {
|
||||||
},
|
},
|
||||||
isNc: async ({ id }, _, { itemLoader }) => {
|
isNc: async ({ id }, _, { itemLoader }) => {
|
||||||
const item = await itemLoader.load(id);
|
const item = await itemLoader.load(id);
|
||||||
return (
|
return isNC(item);
|
||||||
item.rarityIndex === 500 || item.rarityIndex === 0 || item.isManuallyNc
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
isPb: async ({ id }, _, { itemTranslationLoader }) => {
|
isPb: async ({ id }, _, { itemTranslationLoader }) => {
|
||||||
const translation = await itemTranslationLoader.load(id);
|
const translation = await itemTranslationLoader.load(id);
|
||||||
|
@ -356,10 +355,31 @@ const resolvers = {
|
||||||
// This feature is deprecated, so now we just always return unknown value.
|
// This feature is deprecated, so now we just always return unknown value.
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
ncTradeValueText: async ({ id }, _, { itemNCTradeValueLoader }) => {
|
ncTradeValueText: async (
|
||||||
|
{ id },
|
||||||
|
_,
|
||||||
|
{ itemLoader, itemTranslationLoader }
|
||||||
|
) => {
|
||||||
|
// Skip this lookup for non-NC items, as a perf optimization.
|
||||||
|
const item = await itemLoader.load(id);
|
||||||
|
if (!isNC(item)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the item name, which is how we look things up in ~owls.
|
||||||
|
const itemTranslation = await itemTranslationLoader.load(id);
|
||||||
|
let itemName = itemTranslation.name;
|
||||||
|
|
||||||
|
// HACK: The name "Butterfly Dress" is used for two different items.
|
||||||
|
// Here's what ~owls does to distinguish!
|
||||||
|
if (id === "76073") {
|
||||||
|
itemName = "Butterfly Dress (from Faerie Festival event)";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the NC trade value from ~owls, if any.
|
||||||
let ncTradeValue;
|
let ncTradeValue;
|
||||||
try {
|
try {
|
||||||
ncTradeValue = await itemNCTradeValueLoader.load(id);
|
ncTradeValue = await getOWLSTradeValue(itemName);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(
|
console.error(
|
||||||
`Error loading ncTradeValueText for item ${id}, skipping:`
|
`Error loading ncTradeValueText for item ${id}, skipping:`
|
||||||
|
@ -368,6 +388,7 @@ const resolvers = {
|
||||||
ncTradeValue = null;
|
ncTradeValue = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If there was a value, get the text. If not, return null.
|
||||||
return ncTradeValue ? ncTradeValue.valueText : null;
|
return ncTradeValue ? ncTradeValue.valueText : null;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1225,4 +1246,10 @@ async function loadClosetListOrDefaultList(listId, closetListLoader) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isNC(item) {
|
||||||
|
return (
|
||||||
|
item.rarityIndex === 500 || item.rarityIndex === 0 || item.isManuallyNc
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = { typeDefs, resolvers };
|
module.exports = { typeDefs, resolvers };
|
||||||
|
|
Loading…
Reference in a new issue