Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/large-socks-slide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@gqty/logger': patch
'@gqty/react': patch
'@gqty/solid': patch
'gqty': patch
---

chore(deps): upgrade @react-hookz/web for object equality
5 changes: 5 additions & 0 deletions .changeset/red-cougars-jam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'gqty': minor
---

Add isFetching to proxy states
22 changes: 11 additions & 11 deletions examples/gnt/app/000-noverby-infinite-fetch/gqty/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { createReactClient } from '@gqty/react';
import { NhostClient } from '@nhost/nextjs';
import { createClient as createNhostClient } from '@nhost/nhost-js';
import type { QueryFetcher } from 'gqty';
import { Cache, createClient } from 'gqty';
import { createClient as createSubscriptionsClient } from 'graphql-ws';
import type { GeneratedSchema } from './schema.generated';
import { generatedSchema, scalarsEnumsHash } from './schema.generated';

const nhost = new NhostClient({
const nhost = createNhostClient({
subdomain: process.env.NEXT_PUBLIC_NHOST_SUBDOMAIN,
region: process.env.NEXT_PUBLIC_NHOST_REGION,
});
Expand All @@ -17,15 +17,15 @@ const getHeaders = (): Record<string, string> =>
'Content-Type': 'application/json',
'x-hasura-admin-secret': process.env.HASURA_GRAPHQL_ADMIN_SECRET,
}
: nhost.auth.isAuthenticated()
? {
'Content-Type': 'application/json',
authorization: `Bearer ${nhost.auth.getAccessToken()}`,
}
: {
'Content-Type': 'application/json',
'x-hasura-role': 'public',
};
: nhost.getUserSession()
? {
'Content-Type': 'application/json',
authorization: `Bearer ${nhost.getUserSession()?.accessToken}`,
}
: {
'Content-Type': 'application/json',
'x-hasura-role': 'public',
};

const url = `https://${process.env.NEXT_PUBLIC_NHOST_SUBDOMAIN}.hasura.${process.env.NEXT_PUBLIC_NHOST_REGION}.nhost.run/v1/graphql`;

Expand Down
2 changes: 1 addition & 1 deletion examples/gnt/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"dependencies": {
"@gqty/logger": "workspace:^",
"@gqty/react": "workspace:^",
"@nhost/nextjs": "^2.2.9",
"@nhost/nhost-js": "^4.2.0",
"gqty": "workspace:^",
"graphql": "^16.11.0",
"graphql-ws": "^5.16.2",
Expand Down
4 changes: 2 additions & 2 deletions examples/vite-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@
"dependencies": {
"@gqty/cli": "workspace:^",
"@gqty/react": "workspace:^",
"@react-hookz/web": "^24.0.4",
"@react-hookz/web": "^25.2.0",
"gqty": "workspace:^",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@types/node": "^22.16.5",
"@types/node": "^25.0.0",
"@types/react": "^18.3.23",
"@types/react-dom": "^18.3.7",
"@vitejs/plugin-react": "^4.7.0",
Expand Down
4 changes: 2 additions & 2 deletions packages/gqty/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
"p-defer": "^3.0.0"
},
"devDependencies": {
"@types/node": "^22.16.5",
"@types/node": "^25.0.0",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.38.0",
"@typescript-eslint/parser": "^8.38.0",
Expand Down Expand Up @@ -120,7 +120,7 @@
}
},
"engines": {
"node": "^12.20.0 || >=14.13.0"
"node": ">=16 <=25"
},
"publishConfig": {
"directory": "dist"
Expand Down
3 changes: 2 additions & 1 deletion packages/gqty/src/Accessor/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ export type Meta = {
context: SchemaContext;
cache: CacheDataContainer<CacheNode>;
selection: Selection;
/** Object type definition from the geneated schema. */
/** Object type definition from the generated schema. */
type: Type;
isFetching: boolean;
};

/** Pun-intended, a universe of metadata. */
Expand Down
32 changes: 27 additions & 5 deletions packages/gqty/src/Accessor/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
type Type,
} from '../Schema';
import type { Selection } from '../Selection';
import { isFetchingCacheKey } from '../Selection';
import { isPlainObject } from '../Utils';
import type { Meta } from './meta';
import { $meta } from './meta';
Expand Down Expand Up @@ -179,7 +180,9 @@ export const createUnionAccessor = ({
cache,
possibleTypes,
selection,
}: Omit<Meta, 'type'> & { possibleTypes: readonly string[] }) => {
}: Omit<Meta, 'isFetching' | 'type'> & {
possibleTypes: readonly string[];
}) => {
return new Proxy(
Object.fromEntries(possibleTypes.map((__typename) => [__typename, true])),
{
Expand Down Expand Up @@ -330,8 +333,18 @@ export type AccessorOptions = {
};

export const createObjectAccessor = <TSchemaType extends GeneratedSchemaObject>(
meta: Meta
meta: Omit<Meta, 'isFetching'>
) => {
Object.defineProperty(meta, 'isFetching', {
get() {
// Check if this selection or any of its descendants are being fetched
const cacheKey = meta.selection.cacheKeys.join('.');
return isFetchingCacheKey(cacheKey);
},
enumerable: true,
configurable: true,
});

const {
cache: { data },
context: { schema },
Expand Down Expand Up @@ -361,7 +374,7 @@ export const createObjectAccessor = <TSchemaType extends GeneratedSchemaObject>(
: objectProxyHandler
);

$meta.set(proxy, meta);
$meta.set(proxy, meta as Meta);

return proxy;
};
Expand Down Expand Up @@ -543,7 +556,16 @@ const arrayProxyHandler: ProxyHandler<CacheObject[]> = {
},
};

export const createArrayAccessor = (meta: Meta) => {
export const createArrayAccessor = (meta: Omit<Meta, 'isFetching'>) => {
Object.defineProperty(meta, 'isFetching', {
get() {
// Check if this selection or any of its descendants are being fetched
const cacheKey = meta.selection.cacheKeys.join('.');
return isFetchingCacheKey(cacheKey);
},
enumerable: true,
configurable: true,
});
const { cache, context, selection } = meta;

if (!Array.isArray(cache.data)) {
Expand All @@ -565,7 +587,7 @@ export const createArrayAccessor = (meta: Meta) => {

const proxy = new Proxy(cache.data, arrayProxyHandler);

$meta.set(proxy, meta);
$meta.set(proxy, meta as Meta);

return proxy;
};
29 changes: 25 additions & 4 deletions packages/gqty/src/Client/resolveSelections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import { GQtyError, doRetry } from '../Error';
import { notifyFetch, notifyRetry } from '../Helpers/useMetaStateHack';
import { buildQuery } from '../QueryBuilder';
import type { QueryPayload } from '../Schema';
import type { Selection } from '../Selection';
import {
decrementFetchingCacheKey,
incrementFetchingCacheKey,
type Selection,
} from '../Selection';
import type { Debugger } from './debugger';
import { isWsClient, type GQtyWsClient } from './subscriber';

Expand Down Expand Up @@ -42,8 +46,20 @@ export const fetchSelections = <
fetchOptions,
operationName,
}: FetchSelectionsOptions
): Promise<FetchResult<TData>[]> =>
Promise.all(
): Promise<FetchResult<TData>[]> => {
const fetchingCacheKeys = new Set<string>();
for (const selection of selections) {
const keys = selection.cacheKeys;
for (let i = 1; i <= keys.length; i++) {
fetchingCacheKeys.add(keys.slice(0, i).join('.'));
}
}

for (const cacheKey of fetchingCacheKeys) {
incrementFetchingCacheKey(cacheKey);
}

return Promise.all(
buildQuery(selections, operationName).map(
async ({
query,
Expand Down Expand Up @@ -92,7 +108,12 @@ export const fetchSelections = <
return await promise;
}
)
);
).finally(() => {
for (const cacheKey of fetchingCacheKeys) {
decrementFetchingCacheKey(cacheKey);
}
});
};

export type SubscribeSelectionOptions = FetchSelectionsOptions & {
onComplete?: () => void;
Expand Down
19 changes: 19 additions & 0 deletions packages/gqty/src/Selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,25 @@ export type SelectionSnapshot = Array<
[string | number, SelectionOptions] | [string | number]
>;

/** Global map tracking active fetches per cache key */
const fetchingCacheKeys = new Map<string, number>();

export const isFetchingCacheKey = (cacheKey: string) =>
fetchingCacheKeys.has(cacheKey);

export const incrementFetchingCacheKey = (cacheKey: string) => {
fetchingCacheKeys.set(cacheKey, (fetchingCacheKeys.get(cacheKey) ?? 0) + 1);
};

export const decrementFetchingCacheKey = (cacheKey: string) => {
const count = (fetchingCacheKeys.get(cacheKey) ?? 0) - 1;
if (count <= 0) {
fetchingCacheKeys.delete(cacheKey);
} else {
fetchingCacheKeys.set(cacheKey, count);
}
};

export class Selection {
readonly children = new Map<string | number, Selection>();

Expand Down
Loading
Loading