diff --git a/.changeset/calm-scissors-grow.md b/.changeset/calm-scissors-grow.md new file mode 100644 index 0000000000..b923614edf --- /dev/null +++ b/.changeset/calm-scissors-grow.md @@ -0,0 +1,5 @@ +--- +'houdini': patch +--- + +prevent store information from leaking across requests boundaries diff --git a/.changeset/khaki-weeks-repeat.md b/.changeset/khaki-weeks-repeat.md new file mode 100644 index 0000000000..4e6e28b192 --- /dev/null +++ b/.changeset/khaki-weeks-repeat.md @@ -0,0 +1,5 @@ +--- +'houdini': patch +--- + +updated type definition for config file to allow for missing marshal/unmarshal functions diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e8d96afa04..a3c14d75cd 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,13 +29,7 @@ jobs: node-version: 16.14.2 cache: 'yarn' - - uses: actions/cache@v2 - with: - path: '**/node_modules' - key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} - - name: Install Dependencies - if: steps.yarn-cache.outputs.cache-hit != 'true' run: yarn install env: YARN_ENABLE_IMMUTABLE_INSTALLS: false @@ -48,16 +42,17 @@ jobs: runs-on: ubuntu-latest steps: - - name: Setup Node - uses: actions/setup-node@v1 - with: - node-version: 16.14.2 - - name: Checkout source - uses: actions/checkout@master + uses: actions/checkout@v3 with: ref: ${{ github.ref }} + - name: Setup Node + uses: actions/setup-node@v3 + with: + cache: 'yarn' + node-version: 16.14.2 + - name: Install Dependencies run: yarn install env: @@ -74,18 +69,27 @@ jobs: runs-on: ubuntu-latest steps: + - name: Checkout source + uses: actions/checkout@v3 + with: + ref: ${{ github.ref }} + - name: Setup Node - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: + cache: 'yarn' node-version: 16.14.2 - - name: Checkout source - uses: actions/checkout@master + - name: Cache playwright binaries + uses: actions/cache@v2 + id: playwright-cache with: - ref: ${{ github.ref }} + path: | + ~/.cache/ms-playwright + key: cache-playwright-linux-1.21.0 - - name: Install Dependencies - run: yarn install + - name: Install dependencies + run: yarn install --immutable env: YARN_ENABLE_IMMUTABLE_INSTALLS: false @@ -93,10 +97,50 @@ jobs: run: yarn build - name: Install Playwright + if: steps.playwright-cache.outputs.cache-hit != 'true' run: npx playwright install --with-deps - name: Integration Tests - run: yarn tests:integration + run: yarn workspace integration tests + env: + RECORD_REPLAY_TEST_RUN_ID: ${{ env.GITHUB_SHA }} + + # - name: Upload videos + # uses: replayio/action-upload@v0.4.1 + # if: ${{ always() }} + # with: + # api-key: ${{ secrets.RECORD_REPLAY_API_KEY }} + # filter: ${{ 'function($v) { $v.metadata.test.result = "failed" and $v.status = "onDisk" }' }} + + integration_linter: + name: Integration Linter + runs-on: ubuntu-latest + + steps: + - name: Checkout source + uses: actions/checkout@v3 + with: + ref: ${{ github.ref }} + + - name: Setup Node + uses: actions/setup-node@v3 + with: + cache: 'yarn' + node-version: 16.14.2 + + - name: Install dependencies + run: yarn install + env: + YARN_ENABLE_IMMUTABLE_INSTALLS: false + + - name: Build packages + run: yarn build + + - name: Generate runtime + run: yarn workspace integration generate + + - name: Build kit assets + run: yarn workspace integration build # needs to run after build & houdini generate - name: Integration lint @@ -104,10 +148,3 @@ jobs: - name: Integration check run: yarn workspace integration check - - - name: Upload test results - if: always() - uses: actions/upload-artifact@v2 - with: - name: playwright-report - path: integration/playwright-report diff --git a/integration/.eslintrc.cjs b/integration/.eslintrc.cjs index 2e05cb6515..68776ac223 100644 --- a/integration/.eslintrc.cjs +++ b/integration/.eslintrc.cjs @@ -19,6 +19,7 @@ module.exports = { }, rules: { '@typescript-eslint/ban-ts-comment': 'off', - '@typescript-eslint/ban-types': 'off' + '@typescript-eslint/ban-types': 'off', + '@typescript-eslint/no-empty-function': 'off' } }; diff --git a/integration/package.json b/integration/package.json index b21d66f5d3..466f64e0a2 100644 --- a/integration/package.json +++ b/integration/package.json @@ -13,7 +13,8 @@ "package": "svelte-kit package", "previewWeb": "cross-env TZ=utc vite preview --port 3007", "preview": "npm run generate && concurrently \"yarn previewWeb\" \"yarn api\" -n \"web,api\" -c \"green,magenta\"", - "tests": "playwright test", + "tests": "playwright test ", + "tests:replay": "playwright test -c ./playwright.replay.config.ts --reporter=line,@replayio/playwright/reporter --workers 10", "check": "svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-check --tsconfig ./tsconfig.json --watch", "lint": "prettier --ignore-path .gitignore --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .", @@ -22,6 +23,7 @@ "devDependencies": { "@kitql/vite-plugin-watch-and-run": "^0.4.0", "@playwright/test": "^1.21.0", + "@replayio/playwright": "^0.2.21", "@sveltejs/adapter-auto": "1.0.0-next.55", "@sveltejs/kit": "1.0.0-next.370", "@typescript-eslint/eslint-plugin": "^5.10.1", diff --git a/integration/playwright.config.ts b/integration/playwright.config.ts index 5a6a634997..bbcb5ec48c 100644 --- a/integration/playwright.config.ts +++ b/integration/playwright.config.ts @@ -1,6 +1,7 @@ import type { PlaywrightTestConfig } from '@playwright/test'; const config: PlaywrightTestConfig = { + retries: 5, reporter: process.env.CI ? [['list'], ['html', { open: 'never' }], ['github']] : [['list']], use: { screenshot: 'only-on-failure' diff --git a/integration/playwright.replay.config.ts b/integration/playwright.replay.config.ts new file mode 100644 index 0000000000..884ed3f5cd --- /dev/null +++ b/integration/playwright.replay.config.ts @@ -0,0 +1,10 @@ +import type { PlaywrightTestConfig } from '@playwright/test'; +import { devices as replayDevices } from '@replayio/playwright'; +import defaultConfig from './playwright.config.ts'; + +const config: PlaywrightTestConfig = { + ...defaultConfig, + use: { ...(replayDevices['Replay Chromium'] as any) } +}; + +export default config; diff --git a/integration/src/lib/utils/testsHelper.ts b/integration/src/lib/utils/testsHelper.ts index fb2b57808f..c248963ef3 100644 --- a/integration/src/lib/utils/testsHelper.ts +++ b/integration/src/lib/utils/testsHelper.ts @@ -1,6 +1,6 @@ import { sleep, stry } from '@kitql/helper'; import type { Page, Response } from '@playwright/test'; -import { expect } from '@playwright/test'; +import { expect, test } from '@playwright/test'; import { routes } from './routes.js'; export async function expectNoGraphQLRequest( @@ -41,10 +41,10 @@ export async function expectNoGraphQLRequest( */ export async function expectGraphQLResponse( page: Page, - selector: string | null, + selector?: string | null, action: 'click' | 'hover' = 'click' ) { - const listStr = await expectNGraphQLResponse(page, selector, 1, action); + const listStr = await expectNGraphQLResponse(page, selector || null, 1, action); return listStr[0]; } @@ -59,29 +59,62 @@ export async function expectNGraphQLResponse( n: number, action: 'click' | 'hover' = 'click' ) { - // let nbRequest = 0; + // we are going to wait for n responses or 10seconds (whichever comes first) + + // a promise that we'll resolve when we have all the responses + let resolve: () => void = () => {}; + let resolved = false; + const responsePromise = new Promise((res) => { + resolve = res; + }); + + // keep track of how many responses we've seen let nbResponse = 0; + + // and a stringified version of the response const listStr: string[] = []; - // function fnReq(request: any) { - // // console.log('>>', request.method(), request.url()); - // if (request.url().endsWith(routes.GraphQL)) { - // nbRequest++; - // } - // } + let lock = false; + + let waitTime: number | null = null; + const start = new Date().valueOf(); + // the function to call on each response async function fnRes(response: Response) { - // console.log('<<', response.status(), response.url()); - if (response.url().endsWith(routes.GraphQL)) { - nbResponse++; + // if the response isn't for our API, don't count it + if (!response.url().endsWith(routes.GraphQL)) { + return; + } + if (waitTime === null) { + waitTime = new Date().valueOf() - start; + } + + while (lock) { + await sleep(10); + } + + lock = true; + + // increment the count + nbResponse++; + + // if we're still waiting for a response, add the body to the list + if (nbResponse <= n) { const json = await response.json(); const str = stry(json, 0); listStr.push(str as string); } + + // if we got enough responses, resolve the promise + if (nbResponse == n) { + resolved = true; + resolve(); + } + + lock = false; } // Listen - // page.on('request', fnReq); page.on('response', fnRes); // Trigger the action @@ -93,18 +126,33 @@ export async function expectNGraphQLResponse( } } - // Wait a bit... - await sleep(1111); + // wait for the first of 10 seconds or n responses + await Promise.race([sleep(10000), responsePromise]); // Remove listeners - // page.removeListener('request', fnReq); page.removeListener('response', fnRes); - // Check if numbers are ok - // expect(nbRequest, 'nbRequest').toBe(n); - expect(nbResponse, 'nbResponse').toBe(n); + // if we got this far without resolving the promise, clean it up + if (!resolved) { + resolve(); + } + + // if we have a wait time, then wait + if (waitTime !== null) { + await sleep(waitTime); + + // if we got an extra request, fail + if (nbResponse > n) { + throw new Error('Encountered too many responses'); + } + } + + // if we didn't get enough responses, we need to fail the test + if (!resolved) { + // we failed the test + throw new Error('Timeout waiting for api requests'); + } - // Sort and return! return listStr.sort(); } diff --git a/integration/src/routes/layouts/layout.spec.ts b/integration/src/routes/layouts/layout.spec.ts index b0a5687601..3999d1874e 100644 --- a/integration/src/routes/layouts/layout.spec.ts +++ b/integration/src/routes/layouts/layout.spec.ts @@ -3,9 +3,7 @@ import { routes } from '../../lib/utils/routes.js'; import { clientSideNavigation, expectNGraphQLResponse, - expectNoGraphQLRequest, - expectToBe, - navSelector + expectNoGraphQLRequest } from '../../lib/utils/testsHelper.js'; test.describe('Layout & comp', () => { diff --git a/integration/src/routes/stores/fragment-null.svelte b/integration/src/routes/stores/fragment-null.svelte index 4c9a781dc1..9be92dcc9b 100644 --- a/integration/src/routes/stores/fragment-null.svelte +++ b/integration/src/routes/stores/fragment-null.svelte @@ -5,5 +5,5 @@
- {$data} + {JSON.stringify($data)}
diff --git a/integration/src/routes/stores/pending-load-csf.spec.ts b/integration/src/routes/stores/pending-load-csf.spec.ts new file mode 100644 index 0000000000..8c1ed35766 --- /dev/null +++ b/integration/src/routes/stores/pending-load-csf.spec.ts @@ -0,0 +1,22 @@ +import { test } from '@playwright/test'; +import { routes } from '../../lib/utils/routes.js'; +import { + clientSideNavigation, + expectGraphQLResponse, + expectNoGraphQLRequest, + expectToBe +} from '../../lib/utils/testsHelper.js'; + +test('Simultaneous Pending Load and CSF', async ({ page }) => { + // start off on any page (/stores/network) + await page.goto(routes.Stores_Network); + + // go to a page with both loads (should also have a clickable thing) + await clientSideNavigation(page, routes.Stores_SSR_UserId_2); + + // we should have gotten a response from the navigation + await expectGraphQLResponse(page); + + // make sure we get a response if we click on the button + await expectGraphQLResponse(page, 'button[id="refresh-1"]'); +}); diff --git a/package.json b/package.json index 4c727792ed..b7bd0f94c5 100755 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "types": "./build/cmd/index.d.ts", "scripts": { "tests": "NODE_ENV=test node --experimental-vm-modules node_modules/.bin/jest", - "tests:integration": "yarn workspace integration tests", + "tests:integration": "yarn workspace integration tests --workers 5", "tests:watch": "node --experimental-vm-modules node_modules/.bin/jest --watch", "build": "concurrently \"npm run build:runtime\" \"npm run build:cmd\" \"npm run build:preprocess\" -n \"run,cmd,pre\" -c \"blue.bold,green.bold,yellow.bold\" && npm run build:typeModule", "build:runtime": "concurrently \"npm run build:runtime:esm\" \"npm run build:runtime:cjs\" -n \"esm,cjs\" -c \"green,yellow\"", diff --git a/src/cmd/generators/runtime/adapter.ts b/src/cmd/generators/runtime/adapter.ts index 077b3565f8..261b362c25 100644 --- a/src/cmd/generators/runtime/adapter.ts +++ b/src/cmd/generators/runtime/adapter.ts @@ -37,7 +37,7 @@ export function goTo(location, options) { export const isBrowser = process.browser -export const clientStarted = true; // Not tested in Sapper. +export const clientStarted = true; export const isPrerender = false ` diff --git a/src/cmd/generators/stores/pagination.ts b/src/cmd/generators/stores/pagination.ts index ebbfe7eb4e..c07baf4bd3 100644 --- a/src/cmd/generators/stores/pagination.ts +++ b/src/cmd/generators/stores/pagination.ts @@ -7,7 +7,7 @@ export default function pagination( which: 'fragment' | 'query' ) { // figure out the extra methods and their types when there's pagination - let methods = {} + let methods: string[] = [] let types = '' let typeImports = '' let storeExtras = '{}' @@ -23,11 +23,7 @@ export default function pagination( types = `{ loadNextPage: (context: HoudiniFetchContext, limit?: number) => Promise }` - methods = { - loadNextPage: 'loadNextPage', - fetch: 'refetch', - loading: 'loading', - } + methods = ['loadNextPage', 'fetch', 'loading'] } // cursor pagination else if (paginationMethod === 'cursor') { @@ -44,24 +40,14 @@ import type { PageInfo } from '../runtime/lib/utils'` loadNextPage: (context: HoudiniFetchContext, pageCount?: number, after?: string | number) => Promise ${which === 'query' ? 'pageInfo: Readable' : ''} }` - methods = { - loadNextPage: 'loadNextPage', - pageInfo: 'pageInfo', - fetch: 'refetch', - loading: 'loading', - } + methods = ['loadNextPage', 'fetch', 'loading'] // backwards cursor pagination } else { types = `{ loadPreviousPage: (context: HoudiniFetchContext, pageCount?: number, before?: string) => Promise }` - methods = { - loadPreviousPage: 'loadPreviousPage', - pageInfo: 'pageInfo', - fetch: 'refetch', - loading: 'loading', - } + methods = ['loadPreviousPage', 'fetch', 'loading'] } } diff --git a/src/cmd/generators/stores/query.test.ts b/src/cmd/generators/stores/query.test.ts index bceaaa6365..de155ec1a2 100644 --- a/src/cmd/generators/stores/query.test.ts +++ b/src/cmd/generators/stores/query.test.ts @@ -60,7 +60,7 @@ test('basic store', async function () { config: defaultConfigValues(houdiniConfig), storeName: "GQL_TestQuery", paginated: false, - paginationMethods: {} + paginationMethods: [], }) export const GQL_TestQuery = factory() @@ -107,12 +107,7 @@ test('forward cursor pagination', async function () { config: defaultConfigValues(houdiniConfig), storeName: "GQL_TestQuery", paginated: true, - paginationMethods: { - "loadNextPage": "loadNextPage", - "pageInfo": "pageInfo", - "fetch": "refetch", - "loading": "loading" - } + paginationMethods: ["loadNextPage","fetch","loading"], }) export const GQL_TestQuery = factory() @@ -159,12 +154,7 @@ test('backwards cursor pagination', async function () { config: defaultConfigValues(houdiniConfig), storeName: "GQL_TestQuery", paginated: true, - paginationMethods: { - "loadPreviousPage": "loadPreviousPage", - "pageInfo": "pageInfo", - "fetch": "refetch", - "loading": "loading" - } + paginationMethods: ["loadPreviousPage","fetch","loading"], }) export const GQL_TestQuery = factory() @@ -207,11 +197,7 @@ test('offset pagination', async function () { config: defaultConfigValues(houdiniConfig), storeName: "GQL_TestQuery", paginated: true, - paginationMethods: { - "loadNextPage": "loadNextPage", - "fetch": "refetch", - "loading": "loading" - } + paginationMethods: ["loadNextPage","fetch","loading"], }) export const GQL_TestQuery = factory() diff --git a/src/cmd/generators/stores/query.ts b/src/cmd/generators/stores/query.ts index a1f220e258..754a66960a 100644 --- a/src/cmd/generators/stores/query.ts +++ b/src/cmd/generators/stores/query.ts @@ -24,10 +24,7 @@ const factory = () => queryStore({ config: defaultConfigValues(houdiniConfig), storeName: ${JSON.stringify(storeName)}, paginated: ${JSON.stringify(Boolean(doc.refetch?.paginated))}, - paginationMethods: ${JSON.stringify(paginationExtras.methods, null, 4).replaceAll( - '\n', - '\n ' - )} + paginationMethods: ${JSON.stringify(paginationExtras.methods)}, }) export const ${storeName} = factory() diff --git a/src/runtime/adapter.ts b/src/runtime/adapter.ts index a4e2104eb5..d86500d37b 100644 --- a/src/runtime/adapter.ts +++ b/src/runtime/adapter.ts @@ -2,10 +2,10 @@ // this file just exists for type checking import type { Page } from '@sveltejs/kit' -import type { Readable } from 'svelte/store' +import type { Readable, Writable } from 'svelte/store' // the actual contents of this file gets overwritten by the runtime generator -export function getSession(): Readable { +export function getSession(): Writable { // @ts-ignore return {} } diff --git a/src/runtime/cache/cache.ts b/src/runtime/cache/cache.ts index 3e76fbec5f..db4dd7dfef 100644 --- a/src/runtime/cache/cache.ts +++ b/src/runtime/cache/cache.ts @@ -701,11 +701,10 @@ class CacheInternal { // if the field is a scalar else if (!fields) { // is the type a custom scalar with a specified unmarshal function - if (this.config.scalars?.[type]?.unmarshal) { + const fnUnmarshal = this.config.scalars?.[type]?.unmarshal + if (fnUnmarshal) { // pass the primitive value to the unmarshal function - target[attributeName] = this.config.scalars[type].unmarshal( - value - ) as GraphQLValue + target[attributeName] = fnUnmarshal(value) as GraphQLValue } // the field does not have an unmarshal function else { diff --git a/src/runtime/inline/fragment.ts b/src/runtime/inline/fragment.ts index fef1e0ab38..05520efbd0 100644 --- a/src/runtime/inline/fragment.ts +++ b/src/runtime/inline/fragment.ts @@ -2,7 +2,7 @@ // locals import { Readable } from 'svelte/store' import type { Fragment, GraphQLTagResult } from '../lib/types' -import { wrapPaginationStore, PaginatedDocumentHandlers } from '../lib/pagination' +import { wrapPaginationStore, PaginatedDocumentHandlers, PageInfo } from '../lib/pagination' // function overloads meant to only return a nullable value // if the reference type was nullable @@ -127,16 +127,20 @@ export function paginatedFragment<_Fragment extends Fragment>( document: GraphQLTagResult, initialValue: _Fragment ): { data: Readable<_Fragment['shape']> } & Omit< - PaginatedDocumentHandlers<_Fragment['shape'], {}>, + Omit< + PaginatedDocumentHandlers<_Fragment['shape'], {}>, + 'pageInfo' & { pageInfo: Readable } + >, 'refetch' > + export function paginatedFragment<_Fragment extends Fragment>( document: GraphQLTagResult, initialValue: _Fragment | null ): { data: Readable<_Fragment['shape']> } & Omit< PaginatedDocumentHandlers<_Fragment['shape'], {}>, - 'refetch' -> { + 'pageInfos' | 'refetch' | 'onUnsubscribe' +> & { pageInfo: Readable } { // make sure we got a query document if (document.kind !== 'HoudiniFragment') { throw new Error('paginatedFragment() must be passed a fragment document') diff --git a/src/runtime/inline/query.ts b/src/runtime/inline/query.ts index d299fb2947..56bb425c4d 100644 --- a/src/runtime/inline/query.ts +++ b/src/runtime/inline/query.ts @@ -2,7 +2,7 @@ import { derived, Readable } from 'svelte/store' // locals import { GraphQLTagResult, Operation, QueryResult, CachePolicy } from '../lib/types' -import { wrapPaginationStore, PaginatedDocumentHandlers } from '../lib/pagination' +import { wrapPaginationStore, PaginatedDocumentHandlers, PageInfo } from '../lib/pagination' import { getHoudiniContext } from '../lib/context' export function query<_Query extends Operation>( @@ -59,7 +59,9 @@ type RefetchConfig = { export function paginatedQuery<_Query extends Operation>( document: GraphQLTagResult ): QueryResponse<_Query['result'], _Query['input']> & - PaginatedDocumentHandlers<_Query['result'], _Query['input']> { + Omit, 'pageInfos'> & { + pageInfo: Readable + } { // TODO: fix type checking paginated // @ts-ignore: the query store will only include the methods when it needs to // and the userland type checking happens as part of the query type generation diff --git a/src/runtime/lib/config.ts b/src/runtime/lib/config.ts index fda9008dea..366405dad5 100644 --- a/src/runtime/lib/config.ts +++ b/src/runtime/lib/config.ts @@ -5,9 +5,9 @@ export type ScalarSpec = { // the type to use at runtime type: string // the function to call that serializes the type for the API - marshal: (val: any) => any + marshal?: (val: any) => any // the function to call that turns the API's response into _ClientType - unmarshal: (val: any) => any + unmarshal?: (val: any) => any } type ScalarMap = { [typeName: string]: ScalarSpec } diff --git a/src/runtime/lib/pagination.ts b/src/runtime/lib/pagination.ts index ac56c9265a..cecb67052b 100644 --- a/src/runtime/lib/pagination.ts +++ b/src/runtime/lib/pagination.ts @@ -1,5 +1,5 @@ // externals -import { derived, get, readable, Readable, Writable, writable } from 'svelte/store' +import { derived, get, Readable, Writable, writable } from 'svelte/store' // locals import { deepEquals, FragmentStore, QueryResult, QueryStore, QueryStoreFetchParams } from '..' import cache from '../cache' @@ -7,8 +7,10 @@ import { ConfigFile, keyFieldsForType } from './config' import { getHoudiniContext } from './context' import { executeQuery } from './network' import { GraphQLObject, HoudiniFetchContext, QueryArtifact } from './types' +import { fetchContext, QueryResultMap, sessionQueryStore } from '../stores/query' +import { currentReqID, sessionStore } from './session' -type RefetchFn<_Data = any, _Input = any> = ( +type FetchFn<_Data = any, _Input = any> = ( params?: QueryStoreFetchParams<_Input> ) => Promise> @@ -16,7 +18,7 @@ export function wrapPaginationStore<_Data, _Input>( store: QueryStore<_Data, _Input> | ReturnType['get']> ) { // @ts-ignore - const { loadNextPage, loadPreviousPage, ...rest } = store + const { loadNextPage, loadPreviousPage, paginationStrategy, subscribe, ...rest } = store // grab the current houdini context const context = getHoudiniContext() @@ -31,19 +33,27 @@ export function wrapPaginationStore<_Data, _Input>( result.loadPreviousPage = (...args) => loadPreviousPage(context, ...args) } - return result + if (paginationStrategy === 'cursor') { + // @ts-ignore + result.pageInfo = derived([{ subscribe }], ([$store]) => { + // @ts-ignore + return $store.pageInfo + }) + } + + return { subscribe, ...result } } export function fragmentHandlers<_Data extends GraphQLObject, _Input>({ config, paginationArtifact, - initialValue, - store, + stores, + storeName, }: { + storeName: string config: ConfigFile paginationArtifact: QueryArtifact - initialValue: _Data | null - store: Readable + stores: { [reqID: string]: Readable } }) { const { targetType } = paginationArtifact.refetch || {} const typeConfig = config.types?.[targetType || ''] @@ -53,19 +63,19 @@ export function fragmentHandlers<_Data extends GraphQLObject, _Input>({ ) } - let queryVariables = () => ({} as _Input) + let queryVariables = (reqID: string) => ({} as _Input) // if the query is embedded we have to figure out the correct variables to pass if (paginationArtifact.refetch!.embedded) { // if we have a specific function to use when computing the variables if (typeConfig.resolve?.arguments) { - queryVariables = () => { - const value = get(store) + queryVariables = (reqID: string) => { + const value = get(stores[reqID]) return (typeConfig.resolve!.arguments?.(value) || {}) as _Input } } else { const keys = keyFieldsForType(config, targetType || '') - queryVariables = () => { - const value = get(store) + queryVariables = (reqID: string) => { + const value = get(stores[reqID]) // @ts-ignore return Object.fromEntries(keys.map((key) => [key, value[key]])) as _Input } @@ -73,67 +83,71 @@ export function fragmentHandlers<_Data extends GraphQLObject, _Input>({ } return paginationHandlers<_Data, _Input>({ + storeName, config, - initialValue, - store: store as Readable, + stores, artifact: paginationArtifact, queryVariables, - refetch: async () => { + fetch: async () => { return {} as any }, + getValue: (reqID) => { + return get(stores[reqID]) as _Data + }, }) } export function queryHandlers<_Data extends GraphQLObject, _Input>({ config, artifact, - store, + stores, + fetch, queryVariables, + storeName, }: { config: ConfigFile artifact: QueryArtifact - store: QueryStore - queryVariables: () => _Input + stores: QueryResultMap<_Data, _Input> + fetch: QueryStore<_Data, _Input>['fetch'] + queryVariables: (reqID: string) => _Input | null pageInfo?: Readable + storeName: string }) { // if there's no refetch config for the artifact there's a problem if (!artifact.refetch) { throw new Error('paginatedQuery must be passed a query with @paginate.') } - // create some derived stores from the query meta data - const loading = derived([store], ([$store]) => $store.isFetching) - const data = derived([store], ([$store]) => $store.data) - // return the handlers return paginationHandlers<_Data, _Input>({ - documentLoading: loading, - initialValue: get(store).data || {}, artifact, - store: data, + stores, queryVariables, - refetch: store.fetch, + fetch, config, + storeName, + getValue: (reqID: string) => get(stores[reqID])?.data || ({} as _Data), }) } function paginationHandlers<_Data extends GraphQLObject, _Input>({ - initialValue, artifact, - store, + stores, queryVariables, - documentLoading, - refetch, + fetch, config, + storeName, + getValue, }: { - initialValue: GraphQLObject | null artifact: QueryArtifact - store: Readable - queryVariables: () => _Input + stores: { [reqID: string]: any } + getValue: (reqID: string) => _Data + queryVariables: (reqID: string) => _Input | null documentLoading?: Readable - refetch: RefetchFn<_Data, _Input> + fetch: FetchFn<_Data, _Input> config: ConfigFile - pageInfo?: Readable + pageInfo?: { [reqID: string]: Writable } + storeName: string }): PaginatedHandlers<_Data, _Input> { // start with the defaults and no meaningful page info let loadPreviousPage: PaginatedHandlers<_Data, _Input>['loadPreviousPage'] = async ( @@ -142,28 +156,35 @@ function paginationHandlers<_Data extends GraphQLObject, _Input>({ let loadNextPage: PaginatedHandlers<_Data, _Input>['loadNextPage'] = async ( ...args: Parameters['loadNextPage']> ) => {} - let pageInfo = readable(nullPageInfo()) + let pageInfos: { [reqID: string]: Writable } = {} // loading state let paginationLoadingState = writable(false) - let refetchQuery: RefetchFn<_Data, _Input> + let onUnsubscribe = (reqID: string) => {} + + let fetchQuery: FetchFn<_Data, _Input> + + let paginationStrategy = artifact.refetch?.method + // if the artifact supports cursor based pagination if (artifact.refetch?.method === 'cursor') { // generate the cursor handlers const cursor = cursorHandlers<_Data, _Input>({ - initialValue, artifact, - store, + stores, queryVariables, loading: paginationLoadingState, - refetch, + fetch, config, + storeName, + getValue, }) // always track pageInfo - pageInfo = cursor.pageInfo + pageInfos = cursor.pageInfos // always use the refetch fn - refetchQuery = cursor.refetch + fetchQuery = cursor.fetch + onUnsubscribe = cursor.onUnsubscribe // if we are implementing forward pagination if (artifact.refetch.update === 'append') { @@ -177,61 +198,54 @@ function paginationHandlers<_Data extends GraphQLObject, _Input>({ // the artifact supports offset-based pagination, only loadNextPage is valid else { const offset = offsetPaginationHandler<_Data, _Input>({ - initialValue, artifact, queryVariables, - loading: paginationLoadingState, - refetch, - store, + fetch, + stores, config, + storeName, + loading: paginationLoadingState, + getValue, }) loadNextPage = offset.loadPage - refetchQuery = offset.refetch - } - - // if no loading state was provided just use a store that's always false - if (!documentLoading) { - documentLoading = readable(false, () => {}) + fetchQuery = offset.fetch } // merge the pagination and document loading state - const loading = derived( - [paginationLoadingState, documentLoading], - ($loadingStates) => $loadingStates[0] || $loadingStates[1] - ) + const loading = derived([paginationLoadingState], ($loadingStates) => $loadingStates[0]) - return { loadNextPage, loadPreviousPage, pageInfo, loading, refetch: refetchQuery } + return { + loadNextPage, + loadPreviousPage, + pageInfos, + loading, + fetch: fetchQuery, + onUnsubscribe, + paginationStrategy, + } } function cursorHandlers<_Data extends GraphQLObject, _Input>({ config, - initialValue, artifact, - store, + stores, queryVariables: extraVariables, loading, - refetch, + fetch, + storeName, + getValue, }: { config: ConfigFile - initialValue: GraphQLObject | null artifact: QueryArtifact - store: Readable - queryVariables: () => _Input + stores: { [reqID: string]: any } + getValue: (reqID: string) => _Data + queryVariables: (reqID: string) => _Input | null loading: Writable - refetch: RefetchFn -}): PaginatedHandlers<_Data, _Input> { - // track the current page info in an easy-to-reach store - const initialPageInfo = extractPageInfo(initialValue, artifact.refetch!.path) ?? nullPageInfo() - - const pageInfo = writable(initialPageInfo) - - // hold onto the current value - let value = initialValue - store.subscribe((val) => { - pageInfo.set(extractPageInfo(val, artifact.refetch!.path)) - value = val - }) + fetch: FetchFn + storeName: string +}) { + const pageInfos: { [reqID: string]: Writable } = {} // dry up the page-loading logic const loadPage = async ({ @@ -245,12 +259,17 @@ function cursorHandlers<_Data extends GraphQLObject, _Input>({ functionName: string input: {} }) => { + // figure out the reqID for this session + const reqID = currentReqID(houdiniContext, stores) + // get the pageInfo store + const pageInfo = pageInfoStore(houdiniContext, pageInfos) + // set the loading state to true loading.set(true) // build up the variables to pass to the query const loadVariables: Record = { - ...extraVariables?.(), + ...extraVariables?.(reqID), ...houdiniContext.variables(), ...input, } @@ -303,8 +322,11 @@ function cursorHandlers<_Data extends GraphQLObject, _Input>({ return { loading, loadNextPage: async (houdiniContext: HoudiniFetchContext, pageCount?: number) => { + // figure out the reqID for this session + const reqID = currentReqID(houdiniContext, stores) + // we need to find the connection object holding the current page info - const currentPageInfo = extractPageInfo(value, artifact.refetch!.path) + const currentPageInfo = extractPageInfo(getValue(reqID), artifact.refetch!.path) // if there is no next page, we're done if (!currentPageInfo.hasNextPage) { @@ -328,8 +350,11 @@ function cursorHandlers<_Data extends GraphQLObject, _Input>({ }) }, loadPreviousPage: async (houdiniContext: HoudiniFetchContext, pageCount?: number) => { + // figure out the reqID for this session + const reqID = currentReqID(houdiniContext, stores) + // we need to find the connection object holding the current page info - const currentPageInfo = extractPageInfo(value, artifact.refetch!.path) + const currentPageInfo = extractPageInfo(getValue(reqID), artifact.refetch!.path) // if there is no next page, we're done if (!currentPageInfo.hasPreviousPage) { @@ -352,25 +377,34 @@ function cursorHandlers<_Data extends GraphQLObject, _Input>({ input, }) }, - pageInfo: { subscribe: pageInfo.subscribe }, - async refetch(params?: QueryStoreFetchParams<_Input>): Promise> { + pageInfos, + async fetch(args?: QueryStoreFetchParams<_Input>): Promise> { + // validate and prepare the request context for the current environment (client vs server) + const { context, params } = fetchContext(artifact, storeName, args) + + // get the session stores we will write to + const reqID = currentReqID(context.session, stores) + const pageInfo = sessionStore(context.session, pageInfos, nullPageInfo) + const data = sessionQueryStore<_Data, _Input>(context.session, stores) + const { variables } = params ?? {} // build up the variables to pass to the query const queryVariables: Record = { - ...extraVariables(), + ...extraVariables(reqID), ...variables, } // if the input is different than the query variables then we just do everything like normal - if (variables && !deepEquals(extraVariables(), variables)) { - return refetch(params) + if (variables && !deepEquals(extraVariables(reqID), variables)) { + const result = await fetch(params) + pageInfo.set(extractPageInfo(result, artifact.refetch!.path)) } // we are updating the current set of items, count the number of items that currently exist // and ask for the full data set const count = - countPage(artifact.refetch!.path.concat('edges'), value) || + countPage(artifact.refetch!.path.concat('edges'), get(data).data) || artifact.refetch!.pageSize // if there are more records than the first page, we need fetch to load everything @@ -383,11 +417,14 @@ function cursorHandlers<_Data extends GraphQLObject, _Input>({ loading.set(true) // send the query - const result = await refetch({ + const result = await fetch({ ...params, variables: queryVariables, }) + // keep the page info store up to date + pageInfo.set(extractPageInfo(result.data, artifact.refetch!.path)) + // we're not loading any more loading.set(false) @@ -400,48 +437,58 @@ function cursorHandlers<_Data extends GraphQLObject, _Input>({ source: result.source, } }, + onUnsubscribe(reqID: string) { + if (pageInfos[reqID]) { + delete pageInfos[reqID] + } + }, } } function offsetPaginationHandler<_Data extends GraphQLObject, _Input>({ artifact, queryVariables: extraVariables, - loading, - refetch, - initialValue, - store, + fetch, + stores, + getValue, config, + loading, + storeName, }: { config: ConfigFile artifact: QueryArtifact - queryVariables: () => _Input + queryVariables: (reqID: string) => _Input | null + fetch: FetchFn + stores: { [reqID: string]: any } + getValue: (reqID: string) => _Data loading: Writable - refetch: RefetchFn - initialValue: GraphQLObject | null - store: Readable + storeName: string }): { loadPage: PaginatedHandlers<_Data, _Input>['loadNextPage'] - refetch: PaginatedHandlers<_Data, _Input>['refetch'] + fetch: PaginatedHandlers<_Data, _Input>['fetch'] } { // we need to track the most recent offset for this handler - let currentOffset = - (artifact.refetch?.start as number) || - countPage(artifact.refetch!.path, initialValue) || - artifact.refetch!.pageSize - - // hold onto the current value - let value = initialValue - store.subscribe((val) => { - value = val - }) + let currentOffset = (ctx: HoudiniFetchContext) => { + const store = sessionQueryStore<_Data, _Input>(ctx, stores) + + return ( + (artifact.refetch?.start as number) || + countPage(artifact.refetch!.path, get(store)?.data) || + artifact.refetch!.pageSize + ) + } return { loadPage: async (houdiniContext: HoudiniFetchContext, limit?: number) => { + const offset = currentOffset(houdiniContext) + // figure out the reqID for this session + const reqID = currentReqID(houdiniContext, stores) + // build up the variables to pass to the query const queryVariables: Record = { ...houdiniContext.variables(), - ...extraVariables(), - offset: currentOffset, + ...extraVariables(reqID), + offset, } if (limit) { queryVariables.limit = limit @@ -480,21 +527,28 @@ function offsetPaginationHandler<_Data extends GraphQLObject, _Input>({ // we're not loading any more loading.set(false) }, - async refetch(params?: QueryStoreFetchParams<_Input>): Promise> { + async fetch(args?: QueryStoreFetchParams<_Input>): Promise> { + const { params, context } = fetchContext(artifact, storeName, args) + + const reqID = currentReqID(context.session, stores) + // make sure we created a query store + sessionQueryStore(context.session, stores) + const { variables } = params ?? {} // if the input is different than the query variables then we just do everything like normal - if (variables && !deepEquals(extraVariables(), variables)) { - return refetch(params) + if (variables && !deepEquals(extraVariables(reqID), variables)) { + return fetch(params) } // we are updating the current set of items, count the number of items that currently exist // and ask for the full data set - const count = countPage(artifact.refetch!.path, value) || artifact.refetch!.pageSize + const count = + countPage(artifact.refetch!.path, getValue(reqID)) || artifact.refetch!.pageSize // build up the variables to pass to the query const queryVariables: Record = { - ...extraVariables(), + ...extraVariables(reqID), } // if there are more records than the first page, we need fetch to load everything @@ -506,7 +560,7 @@ function offsetPaginationHandler<_Data extends GraphQLObject, _Input>({ loading.set(true) // send the query - const result = await refetch({ + const result = await fetch({ ...params, variables: queryVariables, }) @@ -514,9 +568,6 @@ function offsetPaginationHandler<_Data extends GraphQLObject, _Input>({ // we're not loading any more loading.set(false) - // we're not loading any more - loading.set(false) - return { data: result.data, variables: queryVariables as _Input, @@ -548,8 +599,10 @@ export type PaginatedHandlers<_Data, _Input> = { before?: string ): Promise loading: Readable - pageInfo: Readable - refetch: RefetchFn<_Data, _Input> + pageInfos: { [reqID: string]: Writable } + fetch: FetchFn<_Data, _Input> + onUnsubscribe: (reqID: string) => void + paginationStrategy?: 'cursor' | 'offset' } function missingPageSizeError(fnName: string) { @@ -591,7 +644,7 @@ export function countPage<_Data extends GraphQLObject>( value: _Data | null ): number { let data = value - if (value === null || data === null) { + if (value === null || data === null || data === undefined) { return 0 } @@ -610,10 +663,18 @@ export function countPage<_Data extends GraphQLObject>( return 0 } - -export const nullPageInfo = () => ({ +const nullPageInfo = (): PageInfo => ({ startCursor: null, endCursor: null, hasNextPage: false, hasPreviousPage: false, }) + +export const pageInfoStore = ( + session: { session: () => App.Session | null } | null | App.Session, + home: { + [key: string]: Writable + } +): Writable => { + return sessionStore(session, home, nullPageInfo) +} diff --git a/src/runtime/lib/session.ts b/src/runtime/lib/session.ts new file mode 100644 index 0000000000..6a58e0846f --- /dev/null +++ b/src/runtime/lib/session.ts @@ -0,0 +1,60 @@ +import type { Writable } from 'svelte/store' +import { writable } from 'svelte/store' +import { isBrowser } from '../adapter' + +export function sessionStore<_State>( + context: Parameters[0], + home: { [key: string]: Writable<_State> }, + initialState: () => _State +): Writable<_State> { + const reqID = currentReqID(context, home) + + // if we dont have an entry for this reqID already, create one + if (!home[reqID]) { + home[reqID] = writable(initialState()) + } + + // there is an entry for the id, return it and the id we computed + return home[reqID] +} + +export function currentReqID( + context: { session: () => App.Session | null } | null | App.Session, + home: { [key: string]: any } +): string { + let session: App.Session | null = null + + if (isBrowser) { + return 'CLIENT' + } + + // if we were given a context, we need to pull the session out + if (context && 'session' in context) { + session = typeof context.session === 'function' ? context.session() : context.session + } else { + session = context + } + + // @ts-ignore + // get the reqID from the session + let { __houdini_session_key: reqID }: { __houdini_session_key: string } = session ?? {} + + // if we already have a reqID, use it + if (reqID) { + return reqID + } + + // make sure that reqID isn't currently being used + while (!reqID || home[reqID]) { + reqID = Math.random().toString() + } + + // save the session + if (session) { + // @ts-ignore + session.__houdini_session_key = reqID + } + + // return the id we computed + return reqID +} diff --git a/src/runtime/stores/fragment.ts b/src/runtime/stores/fragment.ts index d7ac9397f2..9d024459b8 100644 --- a/src/runtime/stores/fragment.ts +++ b/src/runtime/stores/fragment.ts @@ -1,8 +1,17 @@ // externals -import { writable } from 'svelte/store' +import { derived, get, readable, Writable, writable } from 'svelte/store' +import type { Readable } from 'svelte/store' // locals import { ConfigFile, FragmentStore, GraphQLObject, QueryArtifact } from '../lib' -import { fragmentHandlers, PaginatedHandlers } from '../lib/pagination' +import { + extractPageInfo, + fragmentHandlers, + PageInfo, + pageInfoStore, + PaginatedHandlers, +} from '../lib/pagination' +import { currentReqID, sessionStore } from '../lib/session' +import { getSession, isBrowser } from '../adapter' // a fragment store exists in multiple places in a given application so we // can't just return a store directly, the user has to load the version of the @@ -12,39 +21,106 @@ export function fragmentStore<_Data extends GraphQLObject, _Input = {}>({ config, paginatedArtifact, paginationMethods, + storeName, }: { artifact: QueryArtifact config: ConfigFile paginated: QueryArtifact paginatedArtifact?: QueryArtifact - paginationMethods: { [key: string]: keyof PaginatedHandlers<_Data, _Input> } + paginationMethods: (keyof PaginatedHandlers<_Data, _Input>)[] + storeName: string }): FragmentStore<_Data | null> { return { name: artifact.name, get(initialValue: _Data | null) { + const stores: { [reqID: string]: Writable<_Data | null> } = {} + // at the moment a fragment store doesn't really do anything // but we're going to keep it wrapped in a store so we can eventually // optimize the updates - const fragmentStore = writable<_Data | null>(initialValue) + let store: Writable<_Data | null> // build up the methods we want to use - let extraMethods: {} = {} + let extraMethods: Record = {} + let onUnsubscribe = (reqID: string) => {} + let pageInfos: { [key: string]: Writable } = {} if (paginatedArtifact) { const handlers = fragmentHandlers<_Data, {}>({ + storeName, config, paginationArtifact: paginatedArtifact, - initialValue, - store: fragmentStore, + stores, }) extraMethods = Object.fromEntries( - Object.entries(paginationMethods).map(([key, value]) => [key, handlers[value]]) + paginationMethods.map((key) => [key, handlers[key]]) ) + extraMethods.paginationStrategy = handlers.paginationStrategy + + onUnsubscribe = handlers.onUnsubscribe + pageInfos = handlers.pageInfos } + // we need to track the first time we write to a fragment store so we + // can make sure it has data (filled from the initial value argument) + const written = new Set() + return { - subscribe: fragmentStore.subscribe, - update: fragmentStore.set, + subscribe: (...args: Parameters['subscribe']>) => { + const session = get(getSession()) + + // grab the appropriate store for the session + const requestStore = sessionStore(session, stores, () => initialValue) + const reqID = currentReqID(session, stores) + + // if we haven't written anything yet + if (!written.has(reqID)) { + written.add(reqID) + + // update the fragment value + requestStore.set(initialValue) + + // if we have to set up a paginated fragment + if (paginatedArtifact) { + // update the page info + pageInfoStore(session, pageInfos).set( + extractPageInfo(initialValue, paginatedArtifact.refetch!.path) + ) + } + } + + // hold onto the store reference so client's can update + if (isBrowser) { + store = requestStore + } + + // we need to add the page info + const combined = derived< + [typeof requestStore, Readable], + _Data | null + >([requestStore, pageInfos[reqID] || readable(null)], ([$store, $pageInfo]) => { + if ($store === null) { + return null + } + + // combine the state and page info values + const everything: _Data & { pageInfo?: PageInfo } = { ...$store } + if ($pageInfo) { + everything.pageInfo = $pageInfo + } + + return everything + }) + + const unsub = combined.subscribe(...args) + + return () => { + unsub() + onUnsubscribe(reqID) + written.delete(reqID) + } + }, + update: (val: _Data | null) => store?.set(val), ...extraMethods, } }, diff --git a/src/runtime/stores/mutation.ts b/src/runtime/stores/mutation.ts index aa301bad68..ae26c6f561 100644 --- a/src/runtime/stores/mutation.ts +++ b/src/runtime/stores/mutation.ts @@ -1,5 +1,6 @@ // externals -import { Readable, writable } from 'svelte/store' +import { Readable, get } from 'svelte/store' +import type { Writable } from 'svelte/store' // locals import { ConfigFile, @@ -11,6 +12,8 @@ import { import type { SubscriptionSpec, MutationArtifact } from '../lib' import cache from '../cache' import { marshalInputs, marshalSelection, unmarshalSelection } from '../lib/scalars' +import { getSession } from '../adapter' +import { sessionStore } from '../lib/session' export function mutationStore<_Data, _Input>({ config, @@ -19,13 +22,7 @@ export function mutationStore<_Data, _Input>({ config: ConfigFile artifact: MutationArtifact }): MutationStore<_Data, _Input> { - const { subscribe, set, update } = writable>({ - data: null as _Data | null, - errors: null, - isFetching: false, - isOptimisticResponse: false, - variables: null, - }) + const stores: { [reqID: string]: Writable> } = {} const mutate: MutationStore<_Data, _Input>['mutate'] = async ({ variables, @@ -38,7 +35,9 @@ export function mutationStore<_Data, _Input>({ session: () => null, } - update((c) => { + const store = sessionStore(fetchContext, stores, nullMutationStore) + + store.update((c) => { return { ...c, isFetching: true } }) @@ -78,7 +77,7 @@ export function mutationStore<_Data, _Input>({ } // update the store value - set(storeData) + store.set(storeData) } const newVariables = marshalInputs({ @@ -100,7 +99,7 @@ export function mutationStore<_Data, _Input>({ }) if (result.errors && result.errors.length > 0) { - update((s) => ({ + store.update((s) => ({ ...s, errors: result.errors, isFetching: false, @@ -142,12 +141,12 @@ export function mutationStore<_Data, _Input>({ } // update the store value - set(storeData) + store.set(storeData) // return the value to the caller return storeData } catch (error) { - update((s) => ({ + store.update((s) => ({ ...s, errors: error as { message: string }[], isFetching: false, @@ -167,7 +166,21 @@ export function mutationStore<_Data, _Input>({ return { name: artifact.name, - subscribe, + subscribe(...args: Parameters>['subscribe']>) { + // grab the appropriate store for the session + const requestStore = sessionStore(get(getSession()), stores, nullMutationStore) + + // use it's value + return requestStore.subscribe(...args) + }, mutate, } } + +const nullMutationStore = <_Data = any, _Input = any>(): MutationResult<_Data, _Input> => ({ + data: null as _Data | null, + errors: null, + isFetching: false, + isOptimisticResponse: false, + variables: null, +}) diff --git a/src/runtime/stores/query.ts b/src/runtime/stores/query.ts index 8ed84eae50..4124d4b026 100644 --- a/src/runtime/stores/query.ts +++ b/src/runtime/stores/query.ts @@ -1,9 +1,9 @@ // externals -import { derived, get, Readable, Writable, writable } from 'svelte/store' +import { derived, get, readable, Readable, Writable } from 'svelte/store' import type { LoadEvent } from '@sveltejs/kit' // internals import { CachePolicy, DataSource, fetchQuery, GraphQLObject, QueryStore } from '..' -import { clientStarted, isBrowser } from '../adapter' +import { clientStarted, getSession, isBrowser } from '../adapter' import cache from '../cache' import { FetchContext, @@ -12,11 +12,12 @@ import { SubscriptionSpec, deepEquals, } from '../lib' -import type { ConfigFile, QueryArtifact } from '../lib' +import type { ConfigFile, QueryArtifact, HoudiniFetchContext } from '../lib' import { nullHoudiniContext } from '../lib/context' import { PageInfo, PaginatedHandlers, queryHandlers } from '../lib/pagination' import { marshalInputs, unmarshalSelection } from '../lib/scalars' import * as log from '../lib/log' +import { currentReqID, sessionStore } from '../lib/session' // Terms: // - CSF: client side fetch. identified by a lack of loadEvent @@ -37,6 +38,14 @@ import * as log from '../lib/log' // - still need to subscribe to data // +// our query store needs to be able to handle concurrent requests from users with different sessions +// without leaking data. In order to do this, a query store is going to store independent versions for +// every reqID that it encounters. We're going to then use that `reqID` in the session during store subscribe +// in order to get the value that was loaded fetch +export type QueryResultMap<_Data, _Input> = { + [reqID: string]: Writable & { pageInfo?: PageInfo }> +} + export function queryStore<_Data extends GraphQLObject, _Input>({ config, artifact, @@ -48,22 +57,13 @@ export function queryStore<_Data extends GraphQLObject, _Input>({ artifact: QueryArtifact paginated: boolean storeName: string - paginationMethods: { [key: string]: keyof PaginatedHandlers<_Data, _Input> } + paginationMethods: (keyof PaginatedHandlers<_Data, _Input>)[] }): QueryStore<_Data, _Input> { - // only include pageInfo in the store state if the query is paginated - const initialState = (): QueryResult<_Data, _Input> & { pageInfo?: PageInfo } => ({ - data: null, - errors: null, - isFetching: false, - partial: false, - source: null, - variables: null, - }) - // at its core, a query store is a writable store with extra methods - const store = writable(initialState()) - const setFetching = (isFetching: boolean) => store.update((s) => ({ ...s, isFetching })) - const getVariables = () => get(store).variables + const data: QueryResultMap<_Data, _Input> = {} + const setFetching = (reqID: string, isFetching: boolean) => + data[reqID]?.update((s) => ({ ...s, isFetching })) + const getVariables = (reqID: string): _Input | null => get(data[reqID])?.variables || null // the first client-side request after the mocked load() needs to be blocked let blockNextCSF = false @@ -79,10 +79,10 @@ export function queryStore<_Data extends GraphQLObject, _Input>({ // in order to clear the store's value when unmounting, we need to track how many concurrent subscribers // we have. when this number is 0, we need to clear the store - let subscriberCount = 0 + let subscriberCount: { [reqID: string]: number } = {} // a function to update the store's cache subscriptions - function refreshSubscription(newVariables: _Input) { + function refreshSubscription(reqID: string, newVariables: _Input) { // if the variables changed we need to unsubscribe from the old fields and // listen to the new ones if (subscriptionSpec) { @@ -94,7 +94,7 @@ export function queryStore<_Data extends GraphQLObject, _Input>({ rootType: artifact.rootType, selection: artifact.selection, variables: () => newVariables, - set: (data) => store.update((s) => ({ ...s, data })), + set: (newValue) => data[reqID]?.update((s) => ({ ...s, data: newValue })), } // make sure we subscribe to the new values @@ -111,6 +111,10 @@ export function queryStore<_Data extends GraphQLObject, _Input>({ // validate and prepare the request context for the current environment (client vs server) const { context, policy, params } = fetchContext(artifact, storeName, args) + // get the appropriate store for the session + const store = sessionQueryStore(context.session, data) + const reqID = currentReqID(context.session, data) + // identify if this is a CSF or load const isLoadFetch = Boolean('event' in params && params.event) const isComponentFetch = !isLoadFetch @@ -139,7 +143,7 @@ export function queryStore<_Data extends GraphQLObject, _Input>({ // cause the new data to trigger the old subscription after the store has been // update with fetchAndCache if (isComponentFetch && variableChange) { - refreshSubscription(newVariables) + refreshSubscription(reqID, newVariables) store.update((s) => ({ ...s, variables: newVariables })) } @@ -148,9 +152,12 @@ export function queryStore<_Data extends GraphQLObject, _Input>({ // if the variables haven't changed and we dont have an active subscription // then we need to start listening if (!variableChange && subscriptionSpec === null) { - refreshSubscription(newVariables) + refreshSubscription(reqID, newVariables) } + // we've officially blocked a CSF + blockNextCSF = false + return get(store) } @@ -182,13 +189,13 @@ export function queryStore<_Data extends GraphQLObject, _Input>({ context, artifact, variables: newVariables, - store, + store: store, updateStore: true, cached: true, policy: CachePolicy.CacheOnly, setLoadPending: (val) => { loadPending = val - setFetching(val) + setFetching(reqID, val) }, }) } @@ -196,7 +203,7 @@ export function queryStore<_Data extends GraphQLObject, _Input>({ // if we dont have a subscription but we're ending early we need to listen for // changes if (subscriptionSpec === null) { - refreshSubscription(newVariables) + refreshSubscription(reqID, newVariables) } // make sure we return before the fetch happens @@ -214,7 +221,7 @@ export function queryStore<_Data extends GraphQLObject, _Input>({ // we might not want to wait for the fetch to resolve const fakeAwait = clientStarted && isBrowser && !params?.blocking - setFetching(true) + setFetching(reqID, true) // perform the network request const request = fetchAndCache({ @@ -222,12 +229,12 @@ export function queryStore<_Data extends GraphQLObject, _Input>({ context, artifact, variables: newVariables, - store, + store: store, updateStore, cached: policy !== CachePolicy.NetworkOnly, setLoadPending: (val) => { loadPending = val - setFetching(val) + setFetching(reqID, val) }, }) @@ -240,57 +247,71 @@ export function queryStore<_Data extends GraphQLObject, _Input>({ return get(store) } - // we might need to mix multiple store values for the user - const relevantStores: Readable[] = [store] - // add the pagination methods to the store - let extraMethods: {} = {} + let extraMethods: Record = {} + let pageInfos: ReturnType['pageInfos'] = {} + + // a collection of functions to call when cleaning up + let onUnsub = (key: string) => {} + if (paginated) { - const handlers = queryHandlers({ + const handlers = queryHandlers<_Data, _Input>({ + storeName, config, artifact, - store: { - name: artifact.name, - subscribe: store.subscribe, - async fetch(params?: QueryStoreFetchParams<_Input>) { - return (await fetch({ - ...params, - blocking: true, - }))! - }, + stores: data, + async fetch(params) { + return (await fetch({ + ...params, + blocking: true, + }))! }, queryVariables: getVariables, }) - // we only want to add page info if we have to - relevantStores.push(derived([handlers.pageInfo], ([pageInfo]) => ({ pageInfo }))) + extraMethods = Object.fromEntries(paginationMethods.map((key) => [key, handlers[key]])) + extraMethods.paginationStrategy = handlers.paginationStrategy - extraMethods = Object.fromEntries( - Object.entries(paginationMethods).map(([key, value]) => [key, handlers[value]]) - ) + pageInfos = handlers.pageInfos + onUnsub = handlers.onUnsubscribe } - // mix any of the stores we care about - const userFacingStore = derived(relevantStores, (stores) => Object.assign({}, ...stores)) - return { name: artifact.name, subscribe: (...args: Parameters>['subscribe']>) => { - const bubbleUp = userFacingStore.subscribe(...args) + // figure out the correct store to subscribe to + const session = get(getSession()) + const store = sessionQueryStore(session, data) + const reqID = currentReqID(session, data) + + // add the page info store if it exists + const combined = derived( + [store, pageInfos[reqID] || readable(null)], + ([$store, $pageInfo]) => { + const everything = { ...$store } + if ($pageInfo) { + everything.pageInfo = $pageInfo + } + + return everything + } + ) + + const bubbleUp = combined.subscribe(...args) // we have a new subscriber - subscriberCount++ + subscriberCount[reqID] = (subscriberCount[reqID] ?? 0) + 1 // Handle unsubscribe return () => { // we lost a subscriber - subscriberCount-- + subscriberCount[reqID]-- // don't clear the store state on the server (breaks SSR) // or when there is still an active subscriber - if (isBrowser && subscriberCount <= 0) { + if (subscriberCount[reqID] <= 0) { // clean up any cache subscriptions - if (subscriptionSpec) { + if (isBrowser && subscriptionSpec) { cache.unsubscribe(subscriptionSpec, lastVariables || {}) subscriptionSpec = null } @@ -299,7 +320,9 @@ export function queryStore<_Data extends GraphQLObject, _Input>({ lastVariables = null // reset the store value - store.set(initialState()) + delete data[reqID] + // clean up any pagination state + onUnsub(reqID) } // we're done @@ -311,7 +334,7 @@ export function queryStore<_Data extends GraphQLObject, _Input>({ } } -function fetchContext<_Data, _Input>( +export function fetchContext<_Data, _Input>( artifact: QueryArtifact, storeName: string, params?: QueryStoreFetchParams<_Input> @@ -545,3 +568,27 @@ async function fetchAndCache<_Data extends GraphQLObject, _Input>({ return request } + +export const sessionQueryStore = <_Data, _Input>( + session: { session: () => App.Session | null } | null | App.Session, + home: { + [key: string]: Writable> + } +): Writable< + QueryResult<_Data, _Input> & { + pageInfo?: PageInfo + } +> => { + return sessionStore( + session, + home, + (): QueryResult => ({ + data: null, + errors: null, + isFetching: false, + partial: false, + source: null, + variables: null, + }) + ) +} diff --git a/yarn.lock b/yarn.lock index c803f908d7..33eede3da1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2440,6 +2440,50 @@ __metadata: languageName: node linkType: hard +"@replayio/playwright@npm:^0.2.21": + version: 0.2.21 + resolution: "@replayio/playwright@npm:0.2.21" + dependencies: + "@replayio/replay": ^0.9.1 + uuid: ^8.3.2 + peerDependencies: + "@playwright/test": 1.19.x + bin: + replayio-playwright: bin/replayio-playwright.js + checksum: 2cc72282c1ede3a99a4eb44602beef318180ad74e8aa11ccb519c48ceca69d559e52901537b1f396821b66717edbd178d16335c277c16d8a09f71d0fb1d8a98a + languageName: node + linkType: hard + +"@replayio/replay@npm:^0.9.1": + version: 0.9.1 + resolution: "@replayio/replay@npm:0.9.1" + dependencies: + "@replayio/sourcemap-upload": ^1.0.2 + commander: ^7.2.0 + is-uuid: ^1.0.2 + jsonata: ^1.8.6 + superstruct: ^0.15.4 + text-table: ^0.2.0 + ws: ^7.5.0 + bin: + replay: bin/replay.js + checksum: 794f2eb498ed69478e763b6a89b3113495e4700c2fa3bfb29ab6a06b90ca5e82d47fec20dc4522b3c1c6fd2cf346dbe1fa26f6a4b45a58715ec67fbdf7648f14 + languageName: node + linkType: hard + +"@replayio/sourcemap-upload@npm:^1.0.2": + version: 1.0.2 + resolution: "@replayio/sourcemap-upload@npm:1.0.2" + dependencies: + commander: ^7.2.0 + debug: ^4.3.1 + glob: ^7.1.6 + node-fetch: ^2.6.1 + string.prototype.matchall: ^4.0.5 + checksum: b82009a88f34699862ddb5132dfad1a47d24173f4a2431d0edef838281f9a9d36700ffdcaf547f15649d96bdbc61bb2f961b7f667aa90bc3df6203e1e4ccd135 + languageName: node + linkType: hard + "@rollup/plugin-commonjs@npm:^22.0.0": version: 22.0.0 resolution: "@rollup/plugin-commonjs@npm:22.0.0" @@ -3854,7 +3898,7 @@ __metadata: languageName: node linkType: hard -"call-bind@npm:^1.0.0": +"call-bind@npm:^1.0.0, call-bind@npm:^1.0.2": version: 1.0.2 resolution: "call-bind@npm:1.0.2" dependencies: @@ -4264,6 +4308,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^7.2.0": + version: 7.2.0 + resolution: "commander@npm:7.2.0" + checksum: 53501cbeee61d5157546c0bef0fedb6cdfc763a882136284bed9a07225f09a14b82d2a84e7637edfd1a679fb35ed9502fd58ef1d091e6287f60d790147f68ddc + languageName: node + linkType: hard + "common-ancestor-path@npm:^1.0.1": version: 1.0.1 resolution: "common-ancestor-path@npm:1.0.1" @@ -4619,6 +4670,16 @@ __metadata: languageName: node linkType: hard +"define-properties@npm:^1.1.4": + version: 1.1.4 + resolution: "define-properties@npm:1.1.4" + dependencies: + has-property-descriptors: ^1.0.0 + object-keys: ^1.1.1 + checksum: ce0aef3f9eb193562b5cfb79b2d2c86b6a109dfc9fdcb5f45d680631a1a908c06824ddcdb72b7573b54e26ace07f0a23420aaba0d5c627b34d2c1de8ef527e2b + languageName: node + linkType: hard + "define-property@npm:^0.2.5": version: 0.2.5 resolution: "define-property@npm:0.2.5" @@ -4849,6 +4910,48 @@ __metadata: languageName: node linkType: hard +"es-abstract@npm:^1.19.0, es-abstract@npm:^1.19.1, es-abstract@npm:^1.19.5": + version: 1.20.1 + resolution: "es-abstract@npm:1.20.1" + dependencies: + call-bind: ^1.0.2 + es-to-primitive: ^1.2.1 + function-bind: ^1.1.1 + function.prototype.name: ^1.1.5 + get-intrinsic: ^1.1.1 + get-symbol-description: ^1.0.0 + has: ^1.0.3 + has-property-descriptors: ^1.0.0 + has-symbols: ^1.0.3 + internal-slot: ^1.0.3 + is-callable: ^1.2.4 + is-negative-zero: ^2.0.2 + is-regex: ^1.1.4 + is-shared-array-buffer: ^1.0.2 + is-string: ^1.0.7 + is-weakref: ^1.0.2 + object-inspect: ^1.12.0 + object-keys: ^1.1.1 + object.assign: ^4.1.2 + regexp.prototype.flags: ^1.4.3 + string.prototype.trimend: ^1.0.5 + string.prototype.trimstart: ^1.0.5 + unbox-primitive: ^1.0.2 + checksum: 28da27ae0ed9c76df7ee8ef5c278df79dcfdb554415faf7068bb7c58f8ba8e2a16bfb59e586844be6429ab4c302ca7748979d48442224cb1140b051866d74b7f + languageName: node + linkType: hard + +"es-to-primitive@npm:^1.2.1": + version: 1.2.1 + resolution: "es-to-primitive@npm:1.2.1" + dependencies: + is-callable: ^1.1.4 + is-date-object: ^1.0.1 + is-symbol: ^1.0.2 + checksum: 4ead6671a2c1402619bdd77f3503991232ca15e17e46222b0a41a5d81aebc8740a77822f5b3c965008e631153e9ef0580540007744521e72de8e33599fca2eed + languageName: node + linkType: hard + "es6-promise@npm:^3.1.2": version: 3.3.1 resolution: "es6-promise@npm:3.3.1" @@ -5973,6 +6076,18 @@ __metadata: languageName: node linkType: hard +"function.prototype.name@npm:^1.1.5": + version: 1.1.5 + resolution: "function.prototype.name@npm:1.1.5" + dependencies: + call-bind: ^1.0.2 + define-properties: ^1.1.3 + es-abstract: ^1.19.0 + functions-have-names: ^1.2.2 + checksum: acd21d733a9b649c2c442f067567743214af5fa248dbeee69d8278ce7df3329ea5abac572be9f7470b4ec1cd4d8f1040e3c5caccf98ebf2bf861a0deab735c27 + languageName: node + linkType: hard + "functional-red-black-tree@npm:^1.0.1": version: 1.0.1 resolution: "functional-red-black-tree@npm:1.0.1" @@ -5980,6 +6095,13 @@ __metadata: languageName: node linkType: hard +"functions-have-names@npm:^1.2.2": + version: 1.2.3 + resolution: "functions-have-names@npm:1.2.3" + checksum: c3f1f5ba20f4e962efb71344ce0a40722163e85bee2101ce25f88214e78182d2d2476aa85ef37950c579eb6cf6ee811c17b3101bb84004bb75655f3e33f3fdb5 + languageName: node + linkType: hard + "gauge@npm:^3.0.0": version: 3.0.2 resolution: "gauge@npm:3.0.2" @@ -6054,6 +6176,17 @@ __metadata: languageName: node linkType: hard +"get-intrinsic@npm:^1.1.0, get-intrinsic@npm:^1.1.1": + version: 1.1.2 + resolution: "get-intrinsic@npm:1.1.2" + dependencies: + function-bind: ^1.1.1 + has: ^1.0.3 + has-symbols: ^1.0.3 + checksum: 252f45491f2ba88ebf5b38018020c7cc3279de54b1d67ffb70c0cdf1dfa8ab31cd56467b5d117a8b4275b7a4dde91f86766b163a17a850f036528a7b2faafb2b + languageName: node + linkType: hard + "get-package-type@npm:^0.1.0": version: 0.1.0 resolution: "get-package-type@npm:0.1.0" @@ -6079,6 +6212,16 @@ __metadata: languageName: node linkType: hard +"get-symbol-description@npm:^1.0.0": + version: 1.0.0 + resolution: "get-symbol-description@npm:1.0.0" + dependencies: + call-bind: ^1.0.2 + get-intrinsic: ^1.1.1 + checksum: 9ceff8fe968f9270a37a1f73bf3f1f7bda69ca80f4f80850670e0e7b9444ff99323f7ac52f96567f8b5f5fbe7ac717a0d81d3407c7313e82810c6199446a5247 + languageName: node + linkType: hard + "get-value@npm:^2.0.3, get-value@npm:^2.0.6": version: 2.0.6 resolution: "get-value@npm:2.0.6" @@ -6311,6 +6454,13 @@ __metadata: languageName: node linkType: hard +"has-bigints@npm:^1.0.1, has-bigints@npm:^1.0.2": + version: 1.0.2 + resolution: "has-bigints@npm:1.0.2" + checksum: 390e31e7be7e5c6fe68b81babb73dfc35d413604d7ee5f56da101417027a4b4ce6a27e46eff97ad040c835b5d228676eae99a9b5c3bc0e23c8e81a49241ff45b + languageName: node + linkType: hard + "has-flag@npm:^3.0.0": version: 3.0.0 resolution: "has-flag@npm:3.0.0" @@ -6325,6 +6475,15 @@ __metadata: languageName: node linkType: hard +"has-property-descriptors@npm:^1.0.0": + version: 1.0.0 + resolution: "has-property-descriptors@npm:1.0.0" + dependencies: + get-intrinsic: ^1.1.1 + checksum: a6d3f0a266d0294d972e354782e872e2fe1b6495b321e6ef678c9b7a06a40408a6891817350c62e752adced73a94ac903c54734fee05bf65b1905ee1368194bb + languageName: node + linkType: hard + "has-symbols@npm:^1.0.1": version: 1.0.1 resolution: "has-symbols@npm:1.0.1" @@ -6332,6 +6491,22 @@ __metadata: languageName: node linkType: hard +"has-symbols@npm:^1.0.2, has-symbols@npm:^1.0.3": + version: 1.0.3 + resolution: "has-symbols@npm:1.0.3" + checksum: a054c40c631c0d5741a8285010a0777ea0c068f99ed43e5d6eb12972da223f8af553a455132fdb0801bdcfa0e0f443c0c03a68d8555aa529b3144b446c3f2410 + languageName: node + linkType: hard + +"has-tostringtag@npm:^1.0.0": + version: 1.0.0 + resolution: "has-tostringtag@npm:1.0.0" + dependencies: + has-symbols: ^1.0.2 + checksum: cc12eb28cb6ae22369ebaad3a8ab0799ed61270991be88f208d508076a1e99abe4198c965935ce85ea90b60c94ddda73693b0920b58e7ead048b4a391b502c1c + languageName: node + linkType: hard + "has-unicode@npm:^2.0.0, has-unicode@npm:^2.0.1": version: 2.0.1 resolution: "has-unicode@npm:2.0.1" @@ -6734,6 +6909,7 @@ __metadata: "@graphql-yoga/node": ^2.8.0 "@kitql/vite-plugin-watch-and-run": ^0.4.0 "@playwright/test": ^1.21.0 + "@replayio/playwright": ^0.2.21 "@sveltejs/adapter-auto": 1.0.0-next.55 "@sveltejs/kit": 1.0.0-next.370 "@typescript-eslint/eslint-plugin": ^5.10.1 @@ -6759,6 +6935,17 @@ __metadata: languageName: unknown linkType: soft +"internal-slot@npm:^1.0.3": + version: 1.0.3 + resolution: "internal-slot@npm:1.0.3" + dependencies: + get-intrinsic: ^1.1.0 + has: ^1.0.3 + side-channel: ^1.0.4 + checksum: 1944f92e981e47aebc98a88ff0db579fd90543d937806104d0b96557b10c1f170c51fb777b97740a8b6ddeec585fca8c39ae99fd08a8e058dfc8ab70937238bf + languageName: node + linkType: hard + "ip-regex@npm:^2.1.0": version: 2.1.0 resolution: "ip-regex@npm:2.1.0" @@ -6805,6 +6992,15 @@ __metadata: languageName: node linkType: hard +"is-bigint@npm:^1.0.1": + version: 1.0.4 + resolution: "is-bigint@npm:1.0.4" + dependencies: + has-bigints: ^1.0.1 + checksum: c56edfe09b1154f8668e53ebe8252b6f185ee852a50f9b41e8d921cb2bed425652049fbe438723f6cb48a63ca1aa051e948e7e401e093477c99c84eba244f666 + languageName: node + linkType: hard + "is-binary-path@npm:~2.1.0": version: 2.1.0 resolution: "is-binary-path@npm:2.1.0" @@ -6814,6 +7010,16 @@ __metadata: languageName: node linkType: hard +"is-boolean-object@npm:^1.1.0": + version: 1.1.2 + resolution: "is-boolean-object@npm:1.1.2" + dependencies: + call-bind: ^1.0.2 + has-tostringtag: ^1.0.0 + checksum: c03b23dbaacadc18940defb12c1c0e3aaece7553ef58b162a0f6bba0c2a7e1551b59f365b91e00d2dbac0522392d576ef322628cb1d036a0fe51eb466db67222 + languageName: node + linkType: hard + "is-buffer@npm:^1.1.5": version: 1.1.6 resolution: "is-buffer@npm:1.1.6" @@ -6821,6 +7027,13 @@ __metadata: languageName: node linkType: hard +"is-callable@npm:^1.1.4, is-callable@npm:^1.2.4": + version: 1.2.4 + resolution: "is-callable@npm:1.2.4" + checksum: 1a28d57dc435797dae04b173b65d6d1e77d4f16276e9eff973f994eadcfdc30a017e6a597f092752a083c1103cceb56c91e3dadc6692fedb9898dfaba701575f + languageName: node + linkType: hard + "is-ci@npm:^2.0.0": version: 2.0.0 resolution: "is-ci@npm:2.0.0" @@ -6906,6 +7119,15 @@ __metadata: languageName: node linkType: hard +"is-date-object@npm:^1.0.1": + version: 1.0.5 + resolution: "is-date-object@npm:1.0.5" + dependencies: + has-tostringtag: ^1.0.0 + checksum: baa9077cdf15eb7b58c79398604ca57379b2fc4cf9aa7a9b9e295278648f628c9b201400c01c5e0f7afae56507d741185730307cbe7cad3b9f90a77e5ee342fc + languageName: node + linkType: hard + "is-descriptor@npm:^0.1.0": version: 0.1.6 resolution: "is-descriptor@npm:0.1.6" @@ -7029,6 +7251,22 @@ __metadata: languageName: node linkType: hard +"is-negative-zero@npm:^2.0.2": + version: 2.0.2 + resolution: "is-negative-zero@npm:2.0.2" + checksum: f3232194c47a549da60c3d509c9a09be442507616b69454716692e37ae9f37c4dea264fb208ad0c9f3efd15a796a46b79df07c7e53c6227c32170608b809149a + languageName: node + linkType: hard + +"is-number-object@npm:^1.0.4": + version: 1.0.7 + resolution: "is-number-object@npm:1.0.7" + dependencies: + has-tostringtag: ^1.0.0 + checksum: d1e8d01bb0a7134c74649c4e62da0c6118a0bfc6771ea3c560914d52a627873e6920dd0fd0ebc0e12ad2ff4687eac4c308f7e80320b973b2c8a2c8f97a7524f7 + languageName: node + linkType: hard + "is-number@npm:^3.0.0": version: 3.0.0 resolution: "is-number@npm:3.0.0" @@ -7077,6 +7315,25 @@ __metadata: languageName: node linkType: hard +"is-regex@npm:^1.1.4": + version: 1.1.4 + resolution: "is-regex@npm:1.1.4" + dependencies: + call-bind: ^1.0.2 + has-tostringtag: ^1.0.0 + checksum: 362399b33535bc8f386d96c45c9feb04cf7f8b41c182f54174c1a45c9abbbe5e31290bbad09a458583ff6bf3b2048672cdb1881b13289569a7c548370856a652 + languageName: node + linkType: hard + +"is-shared-array-buffer@npm:^1.0.2": + version: 1.0.2 + resolution: "is-shared-array-buffer@npm:1.0.2" + dependencies: + call-bind: ^1.0.2 + checksum: 9508929cf14fdc1afc9d61d723c6e8d34f5e117f0bffda4d97e7a5d88c3a8681f633a74f8e3ad1fe92d5113f9b921dc5ca44356492079612f9a247efbce7032a + languageName: node + linkType: hard + "is-stream@npm:^1.1.0": version: 1.1.0 resolution: "is-stream@npm:1.1.0" @@ -7091,6 +7348,15 @@ __metadata: languageName: node linkType: hard +"is-string@npm:^1.0.5, is-string@npm:^1.0.7": + version: 1.0.7 + resolution: "is-string@npm:1.0.7" + dependencies: + has-tostringtag: ^1.0.0 + checksum: 323b3d04622f78d45077cf89aab783b2f49d24dc641aa89b5ad1a72114cfeff2585efc8c12ef42466dff32bde93d839ad321b26884cf75e5a7892a938b089989 + languageName: node + linkType: hard + "is-subdir@npm:^1.1.1": version: 1.2.0 resolution: "is-subdir@npm:1.2.0" @@ -7100,6 +7366,15 @@ __metadata: languageName: node linkType: hard +"is-symbol@npm:^1.0.2, is-symbol@npm:^1.0.3": + version: 1.0.4 + resolution: "is-symbol@npm:1.0.4" + dependencies: + has-symbols: ^1.0.2 + checksum: 92805812ef590738d9de49d677cd17dfd486794773fb6fa0032d16452af46e9b91bb43ffe82c983570f015b37136f4b53b28b8523bfb10b0ece7a66c31a54510 + languageName: node + linkType: hard + "is-typedarray@npm:^1.0.0, is-typedarray@npm:~1.0.0": version: 1.0.0 resolution: "is-typedarray@npm:1.0.0" @@ -7114,6 +7389,22 @@ __metadata: languageName: node linkType: hard +"is-uuid@npm:^1.0.2": + version: 1.0.2 + resolution: "is-uuid@npm:1.0.2" + checksum: 53eb3544e4416ee2f170e204a319bf686768087d2de1f061f7261decd6b8e2cc16c83687fb081d3aac398c657c8e461ad43e3f664d3155b866db4016697624a3 + languageName: node + linkType: hard + +"is-weakref@npm:^1.0.2": + version: 1.0.2 + resolution: "is-weakref@npm:1.0.2" + dependencies: + call-bind: ^1.0.2 + checksum: 95bd9a57cdcb58c63b1c401c60a474b0f45b94719c30f548c891860f051bc2231575c290a6b420c6bc6e7ed99459d424c652bd5bf9a1d5259505dc35b4bf83de + languageName: node + linkType: hard + "is-windows@npm:^1.0.0, is-windows@npm:^1.0.2": version: 1.0.2 resolution: "is-windows@npm:1.0.2" @@ -7812,6 +8103,13 @@ __metadata: languageName: node linkType: hard +"jsonata@npm:^1.8.6": + version: 1.8.6 + resolution: "jsonata@npm:1.8.6" + checksum: 748e052aa1d848ba263918603a5e7e872c26fbd63fbb3330dd28cb9fa9aed0478730732fe03149fddcb1b1b4963f5eb359f06da6e17ef84734f2067b1841b995 + languageName: node + linkType: hard + "jsonfile@npm:^4.0.0": version: 4.0.0 resolution: "jsonfile@npm:4.0.0" @@ -9148,6 +9446,13 @@ __metadata: languageName: node linkType: hard +"object-inspect@npm:^1.12.0, object-inspect@npm:^1.9.0": + version: 1.12.2 + resolution: "object-inspect@npm:1.12.2" + checksum: a534fc1b8534284ed71f25ce3a496013b7ea030f3d1b77118f6b7b1713829262be9e6243acbcb3ef8c626e2b64186112cb7f6db74e37b2789b9c789ca23048b2 + languageName: node + linkType: hard + "object-keys@npm:^1.0.12, object-keys@npm:^1.1.1": version: 1.1.1 resolution: "object-keys@npm:1.1.1" @@ -9164,7 +9469,7 @@ __metadata: languageName: node linkType: hard -"object.assign@npm:^4.1.0": +"object.assign@npm:^4.1.0, object.assign@npm:^4.1.2": version: 4.1.2 resolution: "object.assign@npm:4.1.2" dependencies: @@ -10011,6 +10316,17 @@ __metadata: languageName: node linkType: hard +"regexp.prototype.flags@npm:^1.4.1, regexp.prototype.flags@npm:^1.4.3": + version: 1.4.3 + resolution: "regexp.prototype.flags@npm:1.4.3" + dependencies: + call-bind: ^1.0.2 + define-properties: ^1.1.3 + functions-have-names: ^1.2.2 + checksum: 51228bae732592adb3ededd5e15426be25f289e9c4ef15212f4da73f4ec3919b6140806374b8894036a86020d054a8d2657d3fee6bb9b4d35d8939c20030b7a6 + languageName: node + linkType: hard + "regexparam@npm:^2.0.0": version: 2.0.0 resolution: "regexparam@npm:2.0.0" @@ -10655,6 +10971,17 @@ resolve@^1.19.0: languageName: node linkType: hard +"side-channel@npm:^1.0.4": + version: 1.0.4 + resolution: "side-channel@npm:1.0.4" + dependencies: + call-bind: ^1.0.0 + get-intrinsic: ^1.0.2 + object-inspect: ^1.9.0 + checksum: 351e41b947079c10bd0858364f32bb3a7379514c399edb64ab3dce683933483fc63fb5e4efe0a15a2e8a7e3c436b6a91736ddb8d8c6591b0460a24bb4a1ee245 + languageName: node + linkType: hard + "signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.2": version: 3.0.3 resolution: "signal-exit@npm:3.0.3" @@ -11042,6 +11369,44 @@ resolve@^1.19.0: languageName: node linkType: hard +"string.prototype.matchall@npm:^4.0.5": + version: 4.0.7 + resolution: "string.prototype.matchall@npm:4.0.7" + dependencies: + call-bind: ^1.0.2 + define-properties: ^1.1.3 + es-abstract: ^1.19.1 + get-intrinsic: ^1.1.1 + has-symbols: ^1.0.3 + internal-slot: ^1.0.3 + regexp.prototype.flags: ^1.4.1 + side-channel: ^1.0.4 + checksum: fc09f3ccbfb325de0472bcc87a6be0598a7499e0b4a31db5789676155b15754a4cc4bb83924f15fc9ed48934dac7366ee52c8b9bd160bed6fd072c93b489e75c + languageName: node + linkType: hard + +"string.prototype.trimend@npm:^1.0.5": + version: 1.0.5 + resolution: "string.prototype.trimend@npm:1.0.5" + dependencies: + call-bind: ^1.0.2 + define-properties: ^1.1.4 + es-abstract: ^1.19.5 + checksum: d44f543833112f57224e79182debadc9f4f3bf9d48a0414d6f0cbd2a86f2b3e8c0ca1f95c3f8e5b32ae83e91554d79d932fc746b411895f03f93d89ed3dfb6bc + languageName: node + linkType: hard + +"string.prototype.trimstart@npm:^1.0.5": + version: 1.0.5 + resolution: "string.prototype.trimstart@npm:1.0.5" + dependencies: + call-bind: ^1.0.2 + define-properties: ^1.1.4 + es-abstract: ^1.19.5 + checksum: a4857c5399ad709d159a77371eeaa8f9cc284469a0b5e1bfe405de16f1fd4166a8ea6f4180e55032f348d1b679b1599fd4301fbc7a8b72bdb3e795e43f7b1048 + languageName: node + linkType: hard + "string_decoder@npm:^1.1.1": version: 1.3.0 resolution: "string_decoder@npm:1.3.0" @@ -11147,6 +11512,13 @@ resolve@^1.19.0: languageName: node linkType: hard +"superstruct@npm:^0.15.4": + version: 0.15.5 + resolution: "superstruct@npm:0.15.5" + checksum: 6d1f5249fee789424b7178fa0a1ffb2ace629c5480c39505885bd8c0046a4ff8b267569a3442fa53b8c560a7ba6599cf3f8af94225aebeb2cf6023f7dd911050 + languageName: node + linkType: hard + "supports-color@npm:^5.3.0": version: 5.5.0 resolution: "supports-color@npm:5.5.0" @@ -11768,6 +12140,18 @@ resolve@^1.19.0: languageName: node linkType: hard +"unbox-primitive@npm:^1.0.2": + version: 1.0.2 + resolution: "unbox-primitive@npm:1.0.2" + dependencies: + call-bind: ^1.0.2 + has-bigints: ^1.0.2 + has-symbols: ^1.0.3 + which-boxed-primitive: ^1.0.2 + checksum: b7a1cf5862b5e4b5deb091672ffa579aa274f648410009c81cca63fed3b62b610c4f3b773f912ce545bb4e31edc3138975b5bc777fc6e4817dca51affb6380e9 + languageName: node + linkType: hard + "undici@npm:^5.1.0": version: 5.3.0 resolution: "undici@npm:5.3.0" @@ -11912,7 +12296,7 @@ resolve@^1.19.0: languageName: node linkType: hard -"uuid@npm:^8.3.0": +"uuid@npm:^8.3.0, uuid@npm:^8.3.2": version: 8.3.2 resolution: "uuid@npm:8.3.2" bin: @@ -12120,6 +12504,19 @@ resolve@^1.19.0: languageName: node linkType: hard +"which-boxed-primitive@npm:^1.0.2": + version: 1.0.2 + resolution: "which-boxed-primitive@npm:1.0.2" + dependencies: + is-bigint: ^1.0.1 + is-boolean-object: ^1.1.0 + is-number-object: ^1.0.4 + is-string: ^1.0.5 + is-symbol: ^1.0.3 + checksum: 53ce774c7379071729533922adcca47220228405e1895f26673bbd71bdf7fb09bee38c1d6399395927c6289476b5ae0629863427fd151491b71c4b6cb04f3a5e + languageName: node + linkType: hard + "which-module@npm:^2.0.0": version: 2.0.0 resolution: "which-module@npm:2.0.0" @@ -12260,6 +12657,21 @@ resolve@^1.19.0: languageName: node linkType: hard +"ws@npm:^7.5.0": + version: 7.5.9 + resolution: "ws@npm:7.5.9" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: c3c100a181b731f40b7f2fddf004aa023f79d64f489706a28bc23ff88e87f6a64b3c6651fbec3a84a53960b75159574d7a7385709847a62ddb7ad6af76f49138 + languageName: node + linkType: hard + "xml-name-validator@npm:^3.0.0": version: 3.0.0 resolution: "xml-name-validator@npm:3.0.0"