diff --git a/deploy/playbooks/setup.yml b/deploy/playbooks/setup.yml index 9279d5b..4c5e977 100644 --- a/deploy/playbooks/setup.yml +++ b/deploy/playbooks/setup.yml @@ -160,7 +160,7 @@ # process. They'll be able to manage it without `sudo`, including during # normal deploys, and run `pm2 monit` from their shell to see status. become: yes - command: "pm2 startup systemd {{ ansible_user_id }} --hp /home/{{ ansible_user_id }}" + command: "pm2 startup systemd -u {{ ansible_user_id }} --hp /home/{{ ansible_user_id }}" - name: Create pm2 ecosystem file copy: @@ -267,6 +267,62 @@ - libgif-dev - librsvg2-dev + - name: Install Playwright system dependencies + # NOTE: I copied the package list from the source list for + # `npx playwright install-deps`, which I couldn't get running in + # Ansible as root, and besides, I prefer manually managing the + # package list over running an npm script as root! + # TODO: We're using Puppeteer now, should this list change in some way? + become: yes + apt: + update_cache: yes + name: + # Tools + - xvfb + - fonts-noto-color-emoji + - ttf-unifont + - libfontconfig + - libfreetype6 + - xfonts-cyrillic + - xfonts-scalable + - fonts-liberation + - fonts-ipafont-gothic + - fonts-wqy-zenhei + - fonts-tlwg-loma-otf + - ttf-ubuntu-font-family + # Chromium + - fonts-liberation + - libasound2 + - libatk-bridge2.0-0 + - libatk1.0-0 + - libatspi2.0-0 + - libcairo2 + - libcups2 + - libdbus-1-3 + - libdrm2 + - libegl1 + - libgbm1 + - libglib2.0-0 + - libgtk-3-0 + - libnspr4 + - libnss3 + - libpango-1.0-0 + - libx11-6 + - libx11-xcb1 + - libxcb1 + - libxcomposite1 + - libxdamage1 + - libxext6 + - libxfixes3 + - libxrandr2 + - libxshmfence1 + + - name: Enable user namespace cloning for Chromium sandboxing + become: yes + ansible.posix.sysctl: + name: kernel.unprivileged_userns_clone + value: "1" + handlers: - name: Restart nginx become: yes diff --git a/package.json b/package.json index b0dea43..f8cd806 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "easeljs": "^1.0.2", "escape-html": "^1.0.3", "framer-motion": "^4.1.11", + "generic-pool": "^3.8.2", "graphql": "^15.5.0", "honeycomb-beeline": "^2.7.4", "immer": "^9.0.6", @@ -43,7 +44,7 @@ "mysql2": "^2.1.0", "next": "12.0.2", "node-fetch": "^2.6.0", - "playwright-core": "^1.14.0", + "puppeteer": "^11.0.0", "react": "^17.0.1", "react-autosuggest": "^10.0.2", "react-dom": "^17.0.1", @@ -112,7 +113,6 @@ "inquirer": "^7.3.3", "jest-image-snapshot": "^4.3.0", "lint-staged": "^10.5.4", - "playwright": "^1.14.0", "prettier": "^2.0.5", "react-is": "^16.13.1", "ts-node": "^9.1.1", diff --git a/pages/api/allWakaValues.js b/pages/api/allWakaValues.js index 1b4dcc6..5894eeb 100644 --- a/pages/api/allWakaValues.js +++ b/pages/api/allWakaValues.js @@ -8,127 +8,15 @@ const beeline = require("honeycomb-beeline")({ disableInstrumentationOnLoad: true, }); -import fetch from "node-fetch"; - -import connectToDb from "../../src/server/db"; - async function handle(req, res) { - const allNcItemNamesAndIdsPromise = loadAllNcItemNamesAndIds(); - - let itemValuesByIdOrName; - try { - itemValuesByIdOrName = await loadWakaValuesByIdOrName(); - } catch (e) { - console.error(e); - res.setHeader("Content-Type", "text/plain"); - res.status(500).send("Error loading Waka data from Google Sheets API"); - return; - } - - // Restructure the value data to use IDs as keys, instead of names. - const allNcItemNamesAndIds = await allNcItemNamesAndIdsPromise; - const itemValues = {}; - for (const { name, id } of allNcItemNamesAndIds) { - if (id in itemValuesByIdOrName) { - itemValues[id] = itemValuesByIdOrName[id]; - } else if (name in itemValuesByIdOrName) { - itemValues[id] = itemValuesByIdOrName[name]; - } - } - - // Cache for 1 minute, and immediately serve stale data for a day after. - // This should keep it fast and responsive, and stay well within our API key - // limits. (This will cause the client to send more requests than necessary, - // but the CDN cache should generally respond quickly with a small 304 Not - // Modified, unless the data really did change.) - res.setHeader( - "Cache-Control", - "public, max-age=3600, stale-while-revalidate=86400" - ); - return res.send(itemValues); -} - -async function loadAllNcItemNamesAndIds() { - const db = await connectToDb(); - - const [rows] = await db.query(` - SELECT items.id, item_translations.name FROM items - INNER JOIN item_translations ON item_translations.item_id = items.id - WHERE - (items.rarity_index IN (0, 500) OR is_manually_nc = 1) - AND item_translations.locale = "en" - `); - - return rows.map(({ id, name }) => ({ id, name: normalizeItemName(name) })); -} - -/** - * Load all Waka values from the spreadsheet. Returns an object keyed by ID or - * name - that is, if the item ID is provided in the sheet, we use that as the - * key; or if not, we use the name as the key. - */ -async function loadWakaValuesByIdOrName() { - if (!process.env["GOOGLE_API_KEY"]) { - throw new Error(`GOOGLE_API_KEY environment variable must be provided`); - } - - const res = await fetch( - `https://sheets.googleapis.com/v4/spreadsheets/` + - `1DRMrniTSZP0sgZK6OAFFYqpmbT6xY_Ve_i480zghOX0/values/NC%20Values` + - `?fields=values&key=${encodeURIComponent(process.env["GOOGLE_API_KEY"])}` - ); - const json = await res.json(); - - if (!res.ok) { - if (json.error) { - const { code, status, message } = json.error; - throw new Error( - `Google Sheets API returned error ${code} ${status}: ${message}` - ); - } else { - throw new Error( - `Google Sheets API returned unexpected error: ${res.status} ${res.statusText}` - ); - } - } - - // Get the rows from the JSON response - skipping the first-row headers. - const rows = json.values.slice(1); - - // Reformat the rows as a map from item name to value. We offer the item data - // as an object with a single field `value` for extensibility, but we omit - // the spreadsheet columns that we don't use on DTI, like Notes. - // - // NOTE: The Sheets API only returns the first non-empty cells of the row. - // That's why we set `""` as the defaults, in case the value/notes/etc - // aren't provided. - const itemValuesByIdOrName = {}; - for (const [ - itemName, - value = "", - unusedNotes = "", - unusedMarks = "", - itemId = "", - ] of rows) { - const normalizedItemName = normalizeItemName(itemName); - itemValuesByIdOrName[itemId || normalizedItemName] = { value }; - } - - return itemValuesByIdOrName; -} - -function normalizeItemName(name) { - return ( - name - // Remove all spaces, they're a common source of inconsistency - .replace(/\s+/g, "") - // Lower case, because capitalization is another common source - .toLowerCase() - // Remove diacritics: https://stackoverflow.com/a/37511463/107415 - // Waka has some stray ones in item names, not sure why! - .normalize("NFD") - .replace(/[\u0300-\u036f]/g, "") - ); + res.setHeader("Content-Type", "text/plain; charset=utf8"); + res + .status(410) + .send( + "WakaGuide.com is no longer updating its values, so we no longer " + + "serve them from this endpoint. The most recent set of values is " + + "archived here: https://docs.google.com/spreadsheets/d/1DRMrniTSZP0sgZK6OAFFYqpmbT6xY_Ve_i480zghOX0" + ); } async function handleWithBeeline(req, res) { diff --git a/pages/api/assetImage.js b/pages/api/assetImage.js index ab05e94..6c7d4d9 100644 --- a/pages/api/assetImage.js +++ b/pages/api/assetImage.js @@ -22,34 +22,37 @@ const beeline = require("honeycomb-beeline")({ disableInstrumentationOnLoad: true, }); -// To render the image, we load the /internal/assetImage page in the web app, -// a simple page specifically designed for this API endpoint! -const ASSET_IMAGE_PAGE_BASE_URL = process.env.VERCEL_URL - ? `https://${process.env.VERCEL_URL}/internal/assetImage` - : process.env.NODE_ENV === "development" - ? "http://localhost:3000/internal/assetImage" - : "https://impress-2020.openneo.net/internal/assetImage"; +const puppeteer = require("puppeteer"); +const genericPool = require("generic-pool"); -// TODO: We used to share a browser instamce, but we couldn't get it to reload -// correctly after accidental closes, so we're just gonna always load a -// new one now. What are the perf implications of this? Does it slow down -// response time substantially? -async function getBrowser() { - if (process.env["NODE_ENV"] === "production") { - // In production, we use a special chrome-aws-lambda Chromium. - const chromium = require("chrome-aws-lambda"); - const playwright = require("playwright-core"); - return await playwright.chromium.launch({ - args: chromium.args, - executablePath: await chromium.executablePath, - headless: true, - }); - } else { - // In development, we use the standard playwright Chromium. - const playwright = require("playwright"); - return await playwright.chromium.launch({ headless: true }); - } -} +console.info(`Creating new browser instance`); +const browserPromise = puppeteer.launch({ headless: true }); + +// We maintain a small pool of browser pages, to manage memory usage. If all +// the pages are already in use, a request will wait for one of them to become +// available. +// +// NOTE: 4 pages is about where our 1-cpu prod environment maxes out. We might +// want to upgrade to the 2-cpu box as we add more pressure though, and +// then maybe we can afford more pages in the pool? + +const PAGE_POOL = genericPool.createPool( + { + create: async () => { + console.debug(`Creating a browser page`); + const browser = await browserPromise; + return await browser.newPage(); + }, + destroy: (page) => { + console.debug(`Closing a browser page`); + page.close(); + }, + validate: (page) => page.browser().isConnected(), + }, + { min: 4, max: 4, testOnBorrow: true, acquireTimeoutMillis: 15000 } +); +PAGE_POOL.on("factoryCreateError", (error) => console.error(error)); +PAGE_POOL.on("factoryDestroyError", (error) => console.error(error)); async function handle(req, res) { const { libraryUrl, size } = req.query; @@ -73,6 +76,9 @@ async function handle(req, res) { imageBuffer = await loadAndScreenshotImage(libraryUrl, size); } catch (e) { console.error(e); + if (e.name === "TimeoutError") { + return reject(res, `Server under heavy load: ${e.message}`, 503); + } return reject(res, `Could not load image: ${e.message}`, 500); } @@ -86,18 +92,24 @@ async function handle(req, res) { } async function loadAndScreenshotImage(libraryUrl, size) { - const assetImagePageUrl = new URL(ASSET_IMAGE_PAGE_BASE_URL); + // To render the image, we load the /internal/assetImage page in the web app, + // a simple page specifically designed for this API endpoint! + // + // NOTE: If we deploy to a host where localhost:3000 won't work, make this + // configurable with an env var, e.g. process.env.LOCAL_APP_HOST + const assetImagePageUrl = new URL( + "http://localhost:3000/internal/assetImage" + ); assetImagePageUrl.search = new URLSearchParams({ libraryUrl, size, }).toString(); - console.debug("Opening browser page"); - const browser = await getBrowser(); - const page = await browser.newPage(); - console.debug("Page opened, navigating to: " + assetImagePageUrl.toString()); + console.debug("Getting browser page"); + const page = await PAGE_POOL.acquire(); try { + console.debug("Page ready, navigating to: " + assetImagePageUrl.toString()); await page.goto(assetImagePageUrl.toString()); console.debug("Page loaded, awaiting image"); @@ -106,10 +118,20 @@ async function loadAndScreenshotImage(libraryUrl, size) { // present, or raising the error if present. const imageBufferPromise = screenshotImageFromPage(page); const errorMessagePromise = readErrorMessageFromPage(page); - const firstResultFromPage = await Promise.any([ - imageBufferPromise.then((imageBuffer) => ({ imageBuffer })), - errorMessagePromise.then((errorMessage) => ({ errorMessage })), - ]); + let firstResultFromPage; + try { + firstResultFromPage = await Promise.any([ + imageBufferPromise.then((imageBuffer) => ({ imageBuffer })), + errorMessagePromise.then((errorMessage) => ({ errorMessage })), + ]); + } catch (error) { + if (error.errors) { + // If both promises failed, show all error messages. + throw new Error(error.errors.map((e) => e.message).join(", ")); + } else { + throw error; + } + } if (firstResultFromPage.errorMessage) { throw new Error(firstResultFromPage.errorMessage); @@ -122,18 +144,9 @@ async function loadAndScreenshotImage(libraryUrl, size) { ); } } finally { - // Tear down our resources when we're done! If it fails, log the error, but - // don't block the success of the image. - try { - await page.close(); - } catch (e) { - console.warn("Error closing page after image finished", e); - } - try { - await browser.close(); - } catch (e) { - console.warn("Error closing browser after image finished", e); - } + // To avoid memory leaks, we destroy the page when we're done with it. + // The pool will replace it with a fresh one! + PAGE_POOL.destroy(page); } } @@ -173,7 +186,7 @@ function isNeopetsUrl(urlString) { } function reject(res, message, status = 400) { - res.setHeader("Content-Type", "text/plain"); + res.setHeader("Content-Type", "text/plain; charset=utf8"); return res.status(status).send(message); } diff --git a/pages/api/outfitImage.js b/pages/api/outfitImage.js index 4904757..d1386b1 100644 --- a/pages/api/outfitImage.js +++ b/pages/api/outfitImage.js @@ -35,11 +35,12 @@ const beeline = require("honeycomb-beeline")({ sampleRate: 10, }); -import fetch from "node-fetch"; import gql from "graphql-tag"; -import { print as graphqlPrint } from "graphql/language/printer"; +import { ApolloServer } from "apollo-server"; +import { createTestClient } from "apollo-server-testing"; import connectToDb from "../../src/server/db"; +import { config as graphqlConfig } from "../../src/server"; import { renderOutfitImage } from "../../src/server/outfit-images"; import getVisibleLayers, { petAppearanceFragmentForGetVisibleLayers, @@ -143,50 +144,35 @@ async function handle(req, res) { return res.send(image); } -const GRAPHQL_ENDPOINT = process.env.VERCEL_URL - ? `https://${process.env.VERCEL_URL}/api/graphql` - : process.env.NODE_ENV === "development" - ? "http://localhost:3000/api/graphql" - : "https://impress-2020.openneo.net/api/graphql"; - -// NOTE: Unlike in-app views, we only load PNGs here. We expect this to -// generally perform better, and be pretty reliable now that TNT is -// generating canonical PNGs for every layer! -const GRAPHQL_QUERY = gql` - query ApiOutfitImage($outfitId: ID!, $size: LayerImageSize) { - outfit(id: $outfitId) { - petAppearance { - layers { - id - imageUrl(size: $size) - } - ...PetAppearanceForGetVisibleLayers - } - itemAppearances { - layers { - id - imageUrl(size: $size) - } - ...ItemAppearanceForGetVisibleLayers - } - } - } - ${petAppearanceFragmentForGetVisibleLayers} - ${itemAppearanceFragmentForGetVisibleLayers} -`; -const GRAPHQL_QUERY_STRING = graphqlPrint(GRAPHQL_QUERY); +// Check out this scrappy way of making a query against server code ^_^` +const graphqlClient = createTestClient(new ApolloServer(graphqlConfig)); async function loadLayerUrlsForSavedOutfit(outfitId, size) { - const { errors, data } = await fetch(GRAPHQL_ENDPOINT, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - query: GRAPHQL_QUERY_STRING, - variables: { outfitId, size: `SIZE_${size}` }, - }), - }).then((res) => res.json()); + const { errors, data } = await graphqlClient.query({ + query: gql` + query ApiOutfitImage($outfitId: ID!, $size: LayerImageSize) { + outfit(id: $outfitId) { + petAppearance { + layers { + id + imageUrl(size: $size) + } + ...PetAppearanceForGetVisibleLayers + } + itemAppearances { + layers { + id + imageUrl(size: $size) + } + ...ItemAppearanceForGetVisibleLayers + } + } + } + ${petAppearanceFragmentForGetVisibleLayers} + ${itemAppearanceFragmentForGetVisibleLayers} + `, + variables: { outfitId, size: `SIZE_${size}` }, + }); if (errors && errors.length > 0) { throw new Error( @@ -225,7 +211,7 @@ async function loadUpdatedAtForSavedOutfit(outfitId) { } function reject(res, message, status = 400) { - res.setHeader("Content-Type", "text/plain"); + res.setHeader("Content-Type", "text/plain; charset=utf8"); return res.status(status).send(message); } diff --git a/pages/api/outfitPageSSR.js b/pages/api/outfitPageSSR.js deleted file mode 100644 index d34bd74..0000000 --- a/pages/api/outfitPageSSR.js +++ /dev/null @@ -1,151 +0,0 @@ -/** - * /api/outfitPageSSR also serves the initial request for /outfits/:id, to - * add title and meta tags. This primarily for sharing, like on Discord or - * Twitter or Facebook! - * - * The route is configured in vercel.json, at the project root. - * - * To be honest, we probably should have built Impress 2020 on Next.js, and - * then we'd be getting realistic server-side rendering across practically the - * whole app very cheaply. But this is a good hack for what we have! - * - * TODO: We could add the basic outfit page layout and image preview, to use - * SSR to decrease time-to-first-content for the end-user, too… - */ -const beeline = require("honeycomb-beeline")({ - writeKey: process.env["HONEYCOMB_WRITE_KEY"], - dataset: - process.env["NODE_ENV"] === "production" - ? "Dress to Impress (2020)" - : "Dress to Impress (2020, dev)", - serviceName: "impress-2020-gql-server", - disableInstrumentationOnLoad: true, -}); - -import escapeHtml from "escape-html"; -import fetch from "node-fetch"; - -import connectToDb from "../../src/server/db"; -import { normalizeRow } from "../../src/server/util"; - -async function handle(req, res) { - // Load index.html as our initial page content. If this fails, it probably - // means something is misconfigured in a big way; we don't have a great way - // to recover, and we'll just show an error message. - let initialHtml; - try { - initialHtml = await loadIndexPageHtml(); - } catch (e) { - console.error("Error loading index.html:", e); - return reject(res, "Sorry, there was an error loading this outfit page!"); - } - - // Load the given outfit by ID. If this fails, it's possible that it's just a - // problem with the SSR, and the client will be able to handle it better - // anyway, so just show the standard index.html and let the app load - // normally, as if there was no error. (We'll just log it.) - let outfit; - try { - outfit = await loadOutfitData(req.query.id); - } catch (e) { - console.error("Error loading outfit data:", e); - return sendHtml(res, initialHtml, 200); - } - - // Similarly, if the outfit isn't found, we just show index.html - but with a - // 404 and a gentler log message. - if (outfit == null) { - console.info(`Outfit not found: ${req.query.id}`); - return sendHtml(res, initialHtml, 404); - } - - const outfitName = outfit.name || "Untitled outfit"; - - // Okay, now let's rewrite the HTML to include some outfit data! - // - // WARNING!!! - // Be sure to always use `escapeHtml` when inserting user data!! - // WARNING!!! - // - let html = initialHtml; - - // Add the outfit name to the title. - html = html.replace( - /(.*)<\/title>/, - `<title>${escapeHtml(outfitName)} | Dress to Impress` - ); - - // Add sharing meta tags just before the tag. - const updatedAtTimestamp = Math.floor( - new Date(outfit.updatedAt).getTime() / 1000 - ); - const outfitUrl = - `https://impress-2020.openneo.net/outfits` + - `/${encodeURIComponent(outfit.id)}`; - const imageUrl = - `https://impress-outfit-images.openneo.net/outfits` + - `/${encodeURIComponent(outfit.id)}` + - `/v/${encodeURIComponent(updatedAtTimestamp)}` + - `/600.png`; - const metaTags = ` - - - - - - - `; - html = html.replace(/<\/head>/, `${metaTags}`); - - console.info(`Successfully SSR'd outfit ${outfit.id}`); - - return sendHtml(res, html); -} - -async function loadOutfitData(id) { - const db = await connectToDb(); - const [rows] = await db.query(`SELECT * FROM outfits WHERE id = ?;`, [id]); - if (rows.length === 0) { - return null; - } - - return normalizeRow(rows[0]); -} - -let cachedIndexPageHtml = null; -async function loadIndexPageHtml() { - if (cachedIndexPageHtml == null) { - // Request the same built copy of index.html that we're already serving at - // our homepage. - const homepageUrl = process.env.VERCEL_URL - ? `https://${process.env.VERCEL_URL}/` - : process.env.NODE_ENV === "development" - ? "http://localhost:3000/" - : "https://impress-2020.openneo.net/"; - const liveIndexPageHtml = await fetch(homepageUrl).then((res) => - res.text() - ); - cachedIndexPageHtml = liveIndexPageHtml; - } - - return cachedIndexPageHtml; -} - -function reject(res, message, status = 400) { - res.setHeader("Content-Type", "text/plain"); - return res.status(status).send(message); -} - -function sendHtml(res, html, status = 200) { - res.setHeader("Content-Type", "text/html"); - return res.status(status).send(html); -} - -async function handleWithBeeline(req, res) { - beeline.withTrace( - { name: "api/outfitPageSSR", operation_name: "api/outfitPageSSR" }, - () => handle(req, res) - ); -} - -export default handleWithBeeline; diff --git a/src/app/HomePage.js b/src/app/HomePage.js index 2c524cc..42a7e8b 100644 --- a/src/app/HomePage.js +++ b/src/app/HomePage.js @@ -2,6 +2,7 @@ import React from "react"; import { ClassNames } from "@emotion/react"; import gql from "graphql-tag"; import { + Alert, Box, Button, Center, @@ -56,6 +57,23 @@ function HomePage() { return ( + + + + The Neopets Metaverse team is no longer licensed to use this + software. + {" "} + + More information available here. + {" "} + Thanks for understanding! + + + @@ -795,30 +794,6 @@ const buildItemTradesLoader = (db, loaders) => { cacheKeyFn: ({ itemId, isOwned }) => `${itemId}-${isOwned}` } ); -const buildItemWakaValueLoader = () => - new DataLoader(async (itemIds) => { - // This loader calls our /api/allWakaValues endpoint, to take advantage of - // the CDN caching. This helps us respond a bit faster than Google Sheets - // API would, and avoid putting pressure on our Google Sheets API quotas. - // (Some kind of internal memcache or process-level cache would be a more - // idiomatic solution in a monolith server environment!) - const url = process.env.VERCEL_URL - ? `https://${process.env.VERCEL_URL}/api/allWakaValues` - : process.env.NODE_ENV === "production" - ? "https://impress-2020.openneo.net/api/allWakaValues" - : "http://localhost:3000/api/allWakaValues"; - const res = await fetch(url); - if (!res.ok) { - throw new Error( - `Error loading /api/allWakaValues: ${res.status} ${res.statusText}` - ); - } - - const allWakaValues = await res.json(); - - return itemIds.map((itemId) => allWakaValues[itemId]); - }); - const buildPetTypeLoader = (db, loaders) => new DataLoader(async (petTypeIds) => { const qs = petTypeIds.map((_) => "?").join(","); @@ -1470,7 +1445,6 @@ function buildLoaders(db) { db ); loaders.itemTradesLoader = buildItemTradesLoader(db, loaders); - loaders.itemWakaValueLoader = buildItemWakaValueLoader(); loaders.petTypeLoader = buildPetTypeLoader(db, loaders); loaders.petTypeBySpeciesAndColorLoader = buildPetTypeBySpeciesAndColorLoader( db, diff --git a/src/server/types/Item.js b/src/server/types/Item.js index 1722dc9..5656d14 100644 --- a/src/server/types/Item.js +++ b/src/server/types/Item.js @@ -28,14 +28,13 @@ const typeDefs = gql` createdAt: String """ - This item's capsule trade value as text, according to wakaguide.com, as a - human-readable string. Will be null if the value is not known, or if - there's an error connecting to the data source. + Deprecated: This item's capsule trade value as text, according to + wakaguide.com, as a human-readable string. **This now always returns null.** """ wakaValueText: String @cacheControl(maxAge: ${oneHour}) - currentUserOwnsThis: Boolean! @cacheControl(scope: PRIVATE) - currentUserWantsThis: Boolean! @cacheControl(scope: PRIVATE) + currentUserOwnsThis: Boolean! @cacheControl(maxAge: 0, scope: PRIVATE) + currentUserWantsThis: Boolean! @cacheControl(maxAge: 0, scope: PRIVATE) """ How many users are offering/seeking this in their public trade lists. @@ -315,17 +314,9 @@ const resolvers = { const item = await itemLoader.load(id); return item.createdAt && item.createdAt.toISOString(); }, - wakaValueText: async ({ id }, _, { itemWakaValueLoader }) => { - let wakaValue; - try { - wakaValue = await itemWakaValueLoader.load(id); - } catch (e) { - console.error(`Error loading wakaValueText for item ${id}, skipping:`); - console.error(e); - wakaValue = null; - } - - return wakaValue ? wakaValue.value : null; + wakaValueText: () => { + // This feature is deprecated, so now we just always return unknown value. + return null; }, currentUserOwnsThis: async ( @@ -665,8 +656,13 @@ const resolvers = { itemSearchItemsLoader, petTypeBySpeciesAndColorLoader, currentUserId, - } + }, + { cacheControl } ) => { + if (currentUserOwnsOrWants != null) { + cacheControl.setCacheHint({ scope: "PRIVATE" }); + } + let bodyId = null; if (fitsPet) { const petType = await petTypeBySpeciesAndColorLoader.load({ @@ -799,8 +795,12 @@ const resolvers = { numTotalItems: async ( { query, bodyId, itemKind, currentUserOwnsOrWants, zoneIds }, { offset, limit }, - { currentUserId, itemSearchNumTotalItemsLoader } + { currentUserId, itemSearchNumTotalItemsLoader }, + { cacheControl } ) => { + if (currentUserOwnsOrWants != null) { + cacheControl.setCacheHint({ scope: "PRIVATE" }); + } const numTotalItems = await itemSearchNumTotalItemsLoader.load({ query: query.trim(), bodyId, @@ -816,8 +816,12 @@ const resolvers = { items: async ( { query, bodyId, itemKind, currentUserOwnsOrWants, zoneIds }, { offset, limit }, - { currentUserId, itemSearchItemsLoader } + { currentUserId, itemSearchItemsLoader }, + { cacheControl } ) => { + if (currentUserOwnsOrWants != null) { + cacheControl.setCacheHint({ scope: "PRIVATE" }); + } const items = await itemSearchItemsLoader.load({ query: query.trim(), bodyId, diff --git a/src/server/types/User.js b/src/server/types/User.js index ac81383..9d40f1e 100644 --- a/src/server/types/User.js +++ b/src/server/types/User.js @@ -47,7 +47,20 @@ const typeDefs = gql` user(id: ID!): User userByName(name: String!): User userByEmail(email: String!, supportSecret: String!): User - currentUser: User + + """ + The currently logged-in user. + """ + # Don't allow caching of *anything* nested inside currentUser, because we + # want logins/logouts always reset user data properly. + # + # TODO: If we wanted to privately cache a currentUser field, we could + # remove the maxAge condition here, and attach user ID to the GraphQL + # request URL when sending auth headers. That way, changing user + # would send different requests and avoid the old cache hits. (But we + # should leave the scope, to emphasize that the CDN cache shouldn't + # cache it.) + currentUser: User @cacheControl(maxAge: 0, scope: PRIVATE) } `; diff --git a/yarn.lock b/yarn.lock index 5277daf..e9f17e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4564,7 +4564,7 @@ buffer@5.6.0, buffer@^5.2.0: base64-js "^1.0.2" ieee754 "^1.1.4" -buffer@^5.5.0: +buffer@^5.2.1, buffer@^5.5.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== @@ -4961,7 +4961,7 @@ commander@^5.1.0: resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== -commander@^6.1.0, commander@^6.2.0: +commander@^6.2.0: version "6.2.1" resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== @@ -5560,6 +5560,11 @@ detect-node@^2.0.4: resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw== +devtools-protocol@0.0.901419: + version "0.0.901419" + resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.901419.tgz#79b5459c48fe7e1c5563c02bd72f8fec3e0cebcd" + integrity sha512-4INMPwNm9XRpBukhNbF7OB6fNTTCaI8pzy/fXg0xQzAy5h3zL1P8xT3QazgKqBrb/hAYwIBizqDBZ7GtJE74QQ== + dicer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.3.0.tgz#eacd98b3bfbf92e8ab5c2fdb71aaac44bb06b872" @@ -5930,11 +5935,6 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= -escape-string-regexp@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" - integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== - escape-string-regexp@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" @@ -6361,17 +6361,7 @@ extglob@^2.0.4: snapdragon "^0.8.1" to-regex "^3.0.1" -extract-zip@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927" - integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA== - dependencies: - concat-stream "^1.6.2" - debug "^2.6.9" - mkdirp "^0.5.4" - yauzl "^2.10.0" - -extract-zip@^2.0.1: +extract-zip@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== @@ -6382,6 +6372,16 @@ extract-zip@^2.0.1: optionalDependencies: "@types/yauzl" "^2.9.1" +extract-zip@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927" + integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA== + dependencies: + concat-stream "^1.6.2" + debug "^2.6.9" + mkdirp "^0.5.4" + yauzl "^2.10.0" + extsprintf@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" @@ -6771,6 +6771,11 @@ generate-function@^2.3.1: dependencies: is-property "^1.0.2" +generic-pool@^3.8.2: + version "3.8.2" + resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-3.8.2.tgz#aab4f280adb522fdfbdc5e5b64d718d3683f04e9" + integrity sha512-nGToKy6p3PAbYQ7p1UlWl6vSPwfwU6TMSWK7TTu+WUY4ZjyZQGniGGt2oNVvyNSpyZYSB43zMXVLcBm08MTMkg== + gensync@^1.0.0-beta.1: version "1.0.0-beta.1" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" @@ -6979,11 +6984,6 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== -graceful-fs@^4.2.4: - version "4.2.4" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" - integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== - graphql-extensions@^0.11.1: version "0.11.1" resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.11.1.tgz#f543f544a047a7a4dd930123f662dfcc01527416" @@ -7055,9 +7055,9 @@ graphql@^14.5.3: iterall "^1.2.2" graphql@^15.5.0: - version "15.5.0" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.5.0.tgz#39d19494dbe69d1ea719915b578bf920344a69d5" - integrity sha512-OmaM7y0kaK31NKG31q4YbD2beNYa6jBBKtMFT6gLYJljHLJr42IqJ8KX08u3Li/0ifzTU5HjmoOOrwa5BRLeDA== + version "15.7.2" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.7.2.tgz#85ab0eeb83722977151b3feb4d631b5f2ab287ef" + integrity sha512-AnnKk7hFQFmU/2I9YSQf3xw44ctnSFCfp3zE0N6W174gqe9fWG/2rKaKxROK7CcI3XtERpjEKFqts8o319Kf7A== gud@^1.0.0: version "1.0.0" @@ -7290,6 +7290,14 @@ https-browserify@1.0.0: resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= +https-proxy-agent@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" + integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== + dependencies: + agent-base "6" + debug "4" + https-proxy-agent@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz#b8c286433e87602311b01c8ea34413d856a4af81" @@ -7298,14 +7306,6 @@ https-proxy-agent@^3.0.0: agent-base "^4.3.0" debug "^3.1.0" -https-proxy-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" - integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== - dependencies: - agent-base "6" - debug "4" - human-signals@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" @@ -7988,11 +7988,6 @@ jpeg-js@^0.4.0: resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.1.tgz#937a3ae911eb6427f151760f8123f04c8bfe6ef7" integrity sha512-jA55yJiB5tCXEddos8JBbvW+IMrqY0y1tjjx9KNVtA+QPmu7ND5j0zkKopClpUTsaETL135uOM2XfcYG4XRjmw== -jpeg-js@^0.4.2: - version "0.4.3" - resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.3.tgz#6158e09f1983ad773813704be80680550eff977b" - integrity sha512-ru1HWKek8octvUHFHvE5ZzQ1yAsJmIvRdGWvSoKV52XKyuyYA437QWDttXT8eZXDSbuMpHlLzPDZUPd6idIz+Q== - js-base64@^2.5.1: version "2.6.4" resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4" @@ -9007,6 +9002,13 @@ node-fetch@2.6.1: resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== +node-fetch@2.6.5: + version "2.6.5" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.5.tgz#42735537d7f080a7e5f78b6c549b7146be1742fd" + integrity sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ== + dependencies: + whatwg-url "^5.0.0" + node-fetch@^2.1.2, node-fetch@^2.2.0, node-fetch@^2.6.0: version "2.6.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" @@ -9649,6 +9651,13 @@ pixelmatch@^5.1.0: dependencies: pngjs "^4.0.1" +pkg-dir@4.2.0, pkg-dir@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + pkg-dir@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" @@ -9663,58 +9672,11 @@ pkg-dir@^3.0.0: dependencies: find-up "^3.0.0" -pkg-dir@^4.1.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" - integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== - dependencies: - find-up "^4.0.0" - platform@1.3.6: version "1.3.6" resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.6.tgz#48b4ce983164b209c2d45a107adb31f473a6e7a7" integrity sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg== -playwright-core@^1.14.0: - version "1.14.0" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.14.0.tgz#af51da7b201c11eeda780e2db3f05c8bca74c8be" - integrity sha512-n6NdknezSfRgB6LkLwcrbm5orRQZSpbd8LZmlc4YrIXV0VEvJr5tzP3xlHXpiFBfTr3yoFuagldI3T7bD/8H3w== - dependencies: - commander "^6.1.0" - debug "^4.1.1" - extract-zip "^2.0.1" - https-proxy-agent "^5.0.0" - jpeg-js "^0.4.2" - mime "^2.4.6" - pngjs "^5.0.0" - progress "^2.0.3" - proper-lockfile "^4.1.1" - proxy-from-env "^1.1.0" - rimraf "^3.0.2" - stack-utils "^2.0.3" - ws "^7.4.6" - yazl "^2.5.1" - -playwright@^1.14.0: - version "1.14.0" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.14.0.tgz#18301b11f5278a446d36b5cf96f67db36ce2cd20" - integrity sha512-aR5oZ1iVsjQkGfYCjgYAmyMAVu0MQ0i8MgdnfdqDu9EVLfbnpuuFmTv/Rb7/Yjno1kOrDUP9+RyNC+zfG3wozA== - dependencies: - commander "^6.1.0" - debug "^4.1.1" - extract-zip "^2.0.1" - https-proxy-agent "^5.0.0" - jpeg-js "^0.4.2" - mime "^2.4.6" - pngjs "^5.0.0" - progress "^2.0.3" - proper-lockfile "^4.1.1" - proxy-from-env "^1.1.0" - rimraf "^3.0.2" - stack-utils "^2.0.3" - ws "^7.4.6" - yazl "^2.5.1" - please-upgrade-node@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942" @@ -9732,11 +9694,6 @@ pngjs@^4.0.1: resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-4.0.1.tgz#f803869bb2fc1bfe1bf99aa4ec21c108117cfdbe" integrity sha512-rf5+2/ioHeQxR6IxuYNYGFytUyG3lma/WW1nsmjeHlWwtb2aByla6dkVc8pmJ9nplzkTA0q2xx7mMWrOTqT4Gg== -pngjs@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb" - integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw== - popmotion@9.3.5: version "9.3.5" resolved "https://registry.yarnpkg.com/popmotion/-/popmotion-9.3.5.tgz#e821aff3424a021b0f2c93922db31c55cfe64149" @@ -9845,7 +9802,7 @@ process@~0.5.1: resolved "https://registry.yarnpkg.com/process/-/process-0.5.2.tgz#1638d8a8e34c2f440a91db95ab9aeb677fc185cf" integrity sha1-FjjYqONML0QKkduVq5rrZ3/Bhc8= -progress@^2.0.0, progress@^2.0.3: +progress@2.0.3, progress@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== @@ -9864,15 +9821,6 @@ prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.2: object-assign "^4.1.1" react-is "^16.8.1" -proper-lockfile@^4.1.1: - version "4.1.2" - resolved "https://registry.yarnpkg.com/proper-lockfile/-/proper-lockfile-4.1.2.tgz#c8b9de2af6b2f1601067f98e01ac66baa223141f" - integrity sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA== - dependencies: - graceful-fs "^4.2.4" - retry "^0.12.0" - signal-exit "^3.0.2" - proxy-addr@~2.0.5: version "2.0.6" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" @@ -9895,7 +9843,7 @@ proxy-agent@3: proxy-from-env "^1.0.0" socks-proxy-agent "^4.0.1" -proxy-from-env@^1.0.0, proxy-from-env@^1.1.0: +proxy-from-env@1.1.0, proxy-from-env@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== @@ -9940,6 +9888,24 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +puppeteer@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-11.0.0.tgz#0808719c38e15315ecc1b1c28911f1c9054d201f" + integrity sha512-6rPFqN1ABjn4shgOICGDBITTRV09EjXVqhDERBDKwCLz0UyBxeeBH6Ay0vQUJ84VACmlxwzOIzVEJXThcF3aNg== + dependencies: + debug "4.3.2" + devtools-protocol "0.0.901419" + extract-zip "2.0.1" + https-proxy-agent "5.0.0" + node-fetch "2.6.5" + pkg-dir "4.2.0" + progress "2.0.3" + proxy-from-env "1.1.0" + rimraf "3.0.2" + tar-fs "2.1.1" + unbzip2-stream "1.4.3" + ws "8.2.3" + qs@6.7.0: version "6.7.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" @@ -10484,6 +10450,13 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== +rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" @@ -10491,13 +10464,6 @@ rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3: dependencies: glob "^7.1.3" -rimraf@^3.0.0, rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - rimraf@~2.6.2: version "2.6.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" @@ -11019,13 +10985,6 @@ ssim.js@^3.1.1: resolved "https://registry.yarnpkg.com/ssim.js/-/ssim.js-3.5.0.tgz#d7276b9ee99b57a5ff0db34035f02f35197e62df" integrity sha512-Aj6Jl2z6oDmgYFFbQqK7fght19bXdOxY7Tj03nF+03M9gCBAjeIiO8/PlEGMfKDwYpw4q6iBqVq2YuREorGg/g== -stack-utils@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.3.tgz#cd5f030126ff116b78ccb3c027fe302713b61277" - integrity sha512-gL//fkxfWUsIlFL2Tl42Cl6+HFALEaB1FU76I/Fy+oZjRreP7OPMXFlGbxM7NQsI0ZpUfw76sHnv0WNYuTb7Iw== - dependencies: - escape-string-regexp "^2.0.0" - stacktrace-parser@0.1.10: version "0.1.10" resolved "https://registry.yarnpkg.com/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz#29fb0cae4e0d0b85155879402857a1639eb6051a" @@ -11400,7 +11359,7 @@ table@^6.0.9: string-width "^4.2.3" strip-ansi "^6.0.1" -tar-fs@^2.0.0, tar-fs@^2.1.1: +tar-fs@2.1.1, tar-fs@^2.0.0, tar-fs@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== @@ -11582,6 +11541,11 @@ tr46@^1.0.1: dependencies: punycode "^2.1.0" +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= + truncate-utf8-bytes@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz#405923909592d56f78a5818434b0b78489ca5f2b" @@ -11745,6 +11709,14 @@ unbox-primitive@^1.0.1: has-symbols "^1.0.2" which-boxed-primitive "^1.0.2" +unbzip2-stream@1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" + integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg== + dependencies: + buffer "^5.2.1" + through "^2.3.8" + unfetch@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.1.0.tgz#6ec2dd0de887e58a4dee83a050ded80ffc4137db" @@ -12008,6 +11980,11 @@ watchpack@2.1.1: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= + webidl-conversions@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" @@ -12018,6 +11995,14 @@ whatwg-fetch@^3.0.0: resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c" integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA== +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + whatwg-url@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06" @@ -12109,6 +12094,11 @@ write-file-atomic@^2.3.0: imurmurhash "^0.1.4" signal-exit "^3.0.2" +ws@8.2.3: + version "8.2.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba" + integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA== + ws@^5.2.0: version "5.2.2" resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f" @@ -12123,11 +12113,6 @@ ws@^6.0.0: dependencies: async-limiter "~1.0.0" -ws@^7.4.6: - version "7.5.3" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.3.tgz#160835b63c7d97bfab418fc1b8a9fced2ac01a74" - integrity sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg== - ws@~7.4.2: version "7.4.4" resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.4.tgz#383bc9742cb202292c9077ceab6f6047b17f2d59" @@ -12245,13 +12230,6 @@ yauzl@^2.10.0: buffer-crc32 "~0.2.3" fd-slicer "~1.1.0" -yazl@^2.5.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/yazl/-/yazl-2.5.1.tgz#a3d65d3dd659a5b0937850e8609f22fffa2b5c35" - integrity sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw== - dependencies: - buffer-crc32 "~0.2.3" - yeast@0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"