mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-21 15:53:50 +02:00
add @tanstack/query
This commit is contained in:
133
src/query/__tests__/entityCache.test.js
Normal file
133
src/query/__tests__/entityCache.test.js
Normal file
@@ -0,0 +1,133 @@
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
_entityCacheInternals,
|
||||
fetchWithEntityPolicy,
|
||||
patchAndRefetchActiveQuery,
|
||||
patchQueryDataWithRecency
|
||||
} from '../entityCache';
|
||||
import { queryClient } from '../client';
|
||||
|
||||
describe('entity query cache helpers', () => {
|
||||
beforeEach(() => {
|
||||
queryClient.clear();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('reports cache hit for fresh data', async () => {
|
||||
const queryKey = ['user', 'usr_1'];
|
||||
let callCount = 0;
|
||||
|
||||
const policy = {
|
||||
staleTime: 20000,
|
||||
gcTime: 90000,
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false
|
||||
};
|
||||
|
||||
const queryFn = vi.fn(async () => {
|
||||
callCount++;
|
||||
return {
|
||||
json: { id: 'usr_1', updated_at: '2026-01-01T00:00:00.000Z' },
|
||||
params: { userId: 'usr_1' },
|
||||
ref: { id: 'usr_1', updated_at: '2026-01-01T00:00:00.000Z' }
|
||||
};
|
||||
});
|
||||
|
||||
const first = await fetchWithEntityPolicy({ queryKey, policy, queryFn });
|
||||
const second = await fetchWithEntityPolicy({ queryKey, policy, queryFn });
|
||||
|
||||
expect(first.cache).toBe(false);
|
||||
expect(second.cache).toBe(true);
|
||||
expect(callCount).toBe(1);
|
||||
});
|
||||
|
||||
test('always refetches when staleTime is zero (instance strategy)', async () => {
|
||||
const queryKey = ['instance', 'wrld_1', '12345'];
|
||||
let callCount = 0;
|
||||
|
||||
const policy = {
|
||||
staleTime: 0,
|
||||
gcTime: 10000,
|
||||
retry: 0,
|
||||
refetchOnWindowFocus: false
|
||||
};
|
||||
|
||||
const queryFn = vi.fn(async () => {
|
||||
callCount++;
|
||||
return {
|
||||
json: {
|
||||
id: 'wrld_1:12345',
|
||||
$fetchedAt: new Date().toJSON()
|
||||
},
|
||||
params: { worldId: 'wrld_1', instanceId: '12345' },
|
||||
ref: {
|
||||
id: 'wrld_1:12345',
|
||||
$fetchedAt: new Date().toJSON()
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
await fetchWithEntityPolicy({ queryKey, policy, queryFn });
|
||||
await fetchWithEntityPolicy({ queryKey, policy, queryFn });
|
||||
|
||||
expect(callCount).toBe(2);
|
||||
});
|
||||
|
||||
test('does not overwrite newer data with older payload', () => {
|
||||
const queryKey = ['world', 'wrld_1'];
|
||||
|
||||
patchQueryDataWithRecency({
|
||||
queryKey,
|
||||
nextData: {
|
||||
ref: { id: 'wrld_1', updated_at: '2026-01-01T00:00:00.000Z' }
|
||||
}
|
||||
});
|
||||
|
||||
patchQueryDataWithRecency({
|
||||
queryKey,
|
||||
nextData: {
|
||||
ref: { id: 'wrld_1', updated_at: '2025-01-01T00:00:00.000Z' }
|
||||
}
|
||||
});
|
||||
|
||||
const cached = queryClient.getQueryData(queryKey);
|
||||
expect(cached.ref.updated_at).toBe('2026-01-01T00:00:00.000Z');
|
||||
});
|
||||
|
||||
test('patch and refetch invalidates only active queries for that key', async () => {
|
||||
const invalidateSpy = vi
|
||||
.spyOn(queryClient, 'invalidateQueries')
|
||||
.mockResolvedValue();
|
||||
|
||||
const queryKey = ['avatar', 'avtr_1'];
|
||||
await patchAndRefetchActiveQuery({
|
||||
queryKey,
|
||||
nextData: {
|
||||
ref: { id: 'avtr_1', updated_at: '2026-01-01T00:00:00.000Z' }
|
||||
}
|
||||
});
|
||||
|
||||
expect(invalidateSpy).toHaveBeenCalledWith({
|
||||
queryKey,
|
||||
exact: true,
|
||||
refetchType: 'active'
|
||||
});
|
||||
});
|
||||
|
||||
test('internal recency guard prefers same-or-newer timestamps', () => {
|
||||
const newer = {
|
||||
ref: { id: 'usr_1', updated_at: '2026-02-01T00:00:00.000Z' }
|
||||
};
|
||||
const older = {
|
||||
ref: { id: 'usr_1', updated_at: '2026-01-01T00:00:00.000Z' }
|
||||
};
|
||||
|
||||
expect(_entityCacheInternals.shouldReplaceCurrent(older, newer)).toBe(
|
||||
true
|
||||
);
|
||||
expect(_entityCacheInternals.shouldReplaceCurrent(newer, older)).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
||||
63
src/query/__tests__/keys.test.js
Normal file
63
src/query/__tests__/keys.test.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import { queryKeys } from '../keys';
|
||||
|
||||
describe('query key shapes', () => {
|
||||
test('favorite world keys include owner and tag dimensions', () => {
|
||||
const a = queryKeys.favoriteWorlds({
|
||||
n: 100,
|
||||
offset: 0,
|
||||
ownerId: 'usr_1',
|
||||
userId: 'usr_1',
|
||||
tag: 'worlds1'
|
||||
});
|
||||
const b = queryKeys.favoriteWorlds({
|
||||
n: 100,
|
||||
offset: 0,
|
||||
ownerId: 'usr_2',
|
||||
userId: 'usr_2',
|
||||
tag: 'worlds1'
|
||||
});
|
||||
|
||||
expect(a).not.toEqual(b);
|
||||
});
|
||||
|
||||
test('world list keys include query option discriminator', () => {
|
||||
const base = {
|
||||
userId: 'usr_me',
|
||||
n: 50,
|
||||
offset: 0,
|
||||
sort: 'updated',
|
||||
order: 'descending',
|
||||
user: 'me',
|
||||
releaseStatus: 'all'
|
||||
};
|
||||
|
||||
const defaultKey = queryKeys.worldsByUser(base);
|
||||
const featuredKey = queryKeys.worldsByUser({
|
||||
...base,
|
||||
option: 'featured'
|
||||
});
|
||||
|
||||
expect(defaultKey).not.toEqual(featuredKey);
|
||||
});
|
||||
|
||||
test('group member list keys include sort and role dimensions', () => {
|
||||
const everyone = queryKeys.groupMembers({
|
||||
groupId: 'grp_1',
|
||||
n: 100,
|
||||
offset: 0,
|
||||
sort: 'joinedAt:desc',
|
||||
roleId: ''
|
||||
});
|
||||
const roleScoped = queryKeys.groupMembers({
|
||||
groupId: 'grp_1',
|
||||
n: 100,
|
||||
offset: 0,
|
||||
sort: 'joinedAt:desc',
|
||||
roleId: 'grol_1'
|
||||
});
|
||||
|
||||
expect(everyone).not.toEqual(roleScoped);
|
||||
});
|
||||
});
|
||||
137
src/query/__tests__/policies.test.js
Normal file
137
src/query/__tests__/policies.test.js
Normal file
@@ -0,0 +1,137 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import {
|
||||
entityQueryPolicies,
|
||||
getEntityQueryPolicy,
|
||||
toQueryOptions
|
||||
} from '../policies';
|
||||
|
||||
describe('query policy configuration', () => {
|
||||
test('matches the finalized cache strategy', () => {
|
||||
expect(entityQueryPolicies.user).toMatchObject({
|
||||
staleTime: 20000,
|
||||
gcTime: 90000,
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false
|
||||
});
|
||||
|
||||
expect(entityQueryPolicies.avatar).toMatchObject({
|
||||
staleTime: 60000,
|
||||
gcTime: 300000,
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false
|
||||
});
|
||||
|
||||
expect(entityQueryPolicies.world).toMatchObject({
|
||||
staleTime: 60000,
|
||||
gcTime: 300000,
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false
|
||||
});
|
||||
|
||||
expect(entityQueryPolicies.group).toMatchObject({
|
||||
staleTime: 60000,
|
||||
gcTime: 300000,
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false
|
||||
});
|
||||
|
||||
expect(entityQueryPolicies.groupCollection).toMatchObject({
|
||||
staleTime: 60000,
|
||||
gcTime: 300000,
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false
|
||||
});
|
||||
|
||||
expect(entityQueryPolicies.worldCollection).toMatchObject({
|
||||
staleTime: 60000,
|
||||
gcTime: 300000,
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false
|
||||
});
|
||||
|
||||
expect(entityQueryPolicies.instance).toMatchObject({
|
||||
staleTime: 0,
|
||||
gcTime: 10000,
|
||||
retry: 0,
|
||||
refetchOnWindowFocus: false
|
||||
});
|
||||
|
||||
expect(entityQueryPolicies.friendList).toMatchObject({
|
||||
staleTime: 20000,
|
||||
gcTime: 90000,
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false
|
||||
});
|
||||
|
||||
expect(entityQueryPolicies.favoriteCollection).toMatchObject({
|
||||
staleTime: 60000,
|
||||
gcTime: 300000,
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false
|
||||
});
|
||||
|
||||
expect(entityQueryPolicies.galleryCollection).toMatchObject({
|
||||
staleTime: 60000,
|
||||
gcTime: 300000,
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false
|
||||
});
|
||||
|
||||
expect(entityQueryPolicies.inventoryCollection).toMatchObject({
|
||||
staleTime: 20000,
|
||||
gcTime: 120000,
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false
|
||||
});
|
||||
|
||||
expect(entityQueryPolicies.fileObject).toMatchObject({
|
||||
staleTime: 60000,
|
||||
gcTime: 300000,
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false
|
||||
});
|
||||
});
|
||||
|
||||
test('exposes entity policy lookup', () => {
|
||||
expect(getEntityQueryPolicy('user')).toBe(entityQueryPolicies.user);
|
||||
expect(getEntityQueryPolicy('avatar')).toBe(entityQueryPolicies.avatar);
|
||||
expect(getEntityQueryPolicy('world')).toBe(entityQueryPolicies.world);
|
||||
expect(getEntityQueryPolicy('group')).toBe(entityQueryPolicies.group);
|
||||
expect(getEntityQueryPolicy('groupCollection')).toBe(
|
||||
entityQueryPolicies.groupCollection
|
||||
);
|
||||
expect(getEntityQueryPolicy('worldCollection')).toBe(
|
||||
entityQueryPolicies.worldCollection
|
||||
);
|
||||
expect(getEntityQueryPolicy('instance')).toBe(
|
||||
entityQueryPolicies.instance
|
||||
);
|
||||
expect(getEntityQueryPolicy('friendList')).toBe(
|
||||
entityQueryPolicies.friendList
|
||||
);
|
||||
expect(getEntityQueryPolicy('favoriteCollection')).toBe(
|
||||
entityQueryPolicies.favoriteCollection
|
||||
);
|
||||
expect(getEntityQueryPolicy('galleryCollection')).toBe(
|
||||
entityQueryPolicies.galleryCollection
|
||||
);
|
||||
expect(getEntityQueryPolicy('inventoryCollection')).toBe(
|
||||
entityQueryPolicies.inventoryCollection
|
||||
);
|
||||
expect(getEntityQueryPolicy('fileObject')).toBe(
|
||||
entityQueryPolicies.fileObject
|
||||
);
|
||||
});
|
||||
|
||||
test('normalizes policy values to query options', () => {
|
||||
const options = toQueryOptions(entityQueryPolicies.group);
|
||||
|
||||
expect(options).toEqual({
|
||||
staleTime: 60000,
|
||||
gcTime: 300000,
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false
|
||||
});
|
||||
});
|
||||
});
|
||||
11
src/query/client.js
Normal file
11
src/query/client.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { QueryClient } from '@tanstack/vue-query';
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: true
|
||||
}
|
||||
}
|
||||
});
|
||||
230
src/query/entityCache.js
Normal file
230
src/query/entityCache.js
Normal file
@@ -0,0 +1,230 @@
|
||||
import { queryClient } from './client';
|
||||
import { queryKeys } from './keys';
|
||||
import { toQueryOptions } from './policies';
|
||||
|
||||
const RECENCY_FIELDS = [
|
||||
'updated_at',
|
||||
'updatedAt',
|
||||
'last_activity',
|
||||
'last_login',
|
||||
'memberCountSyncedAt',
|
||||
'$location_at',
|
||||
'$lastFetch',
|
||||
'$fetchedAt',
|
||||
'created_at',
|
||||
'createdAt'
|
||||
];
|
||||
|
||||
function getComparableEntity(data) {
|
||||
if (!data || typeof data !== 'object') {
|
||||
return null;
|
||||
}
|
||||
if (data.ref && typeof data.ref === 'object') {
|
||||
return data.ref;
|
||||
}
|
||||
if (data.json && typeof data.json === 'object' && !Array.isArray(data.json)) {
|
||||
return data.json;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function parseTimestamp(value) {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'string' && value !== '') {
|
||||
const parsed = Date.parse(value);
|
||||
if (!Number.isNaN(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getRecencyTimestamp(data) {
|
||||
const comparable = getComparableEntity(data);
|
||||
if (!comparable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const field of RECENCY_FIELDS) {
|
||||
const ts = parseTimestamp(comparable[field]);
|
||||
if (ts !== null) {
|
||||
return ts;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function shouldReplaceCurrent(currentData, nextData) {
|
||||
if (typeof currentData === 'undefined') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const currentTs = getRecencyTimestamp(currentData);
|
||||
const nextTs = getRecencyTimestamp(nextData);
|
||||
|
||||
if (currentTs !== null && nextTs !== null) {
|
||||
return nextTs >= currentTs;
|
||||
}
|
||||
|
||||
if (currentTs !== null && nextTs === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{queryKey: unknown[], nextData: any}} options
|
||||
*/
|
||||
export function patchQueryDataWithRecency({ queryKey, nextData }) {
|
||||
queryClient.setQueryData(queryKey, (currentData) => {
|
||||
if (!shouldReplaceCurrent(currentData, nextData)) {
|
||||
return currentData;
|
||||
}
|
||||
return nextData;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{queryKey: unknown[], policy: {staleTime: number, gcTime: number, retry: number, refetchOnWindowFocus: boolean}, queryFn: () => Promise<any>}} options
|
||||
* @returns {Promise<{data: any, cache: boolean}>}
|
||||
*/
|
||||
export async function fetchWithEntityPolicy({ queryKey, policy, queryFn }) {
|
||||
const queryState = queryClient.getQueryState(queryKey);
|
||||
const isFresh =
|
||||
Boolean(queryState?.dataUpdatedAt) &&
|
||||
policy.staleTime > 0 &&
|
||||
Date.now() - queryState.dataUpdatedAt < policy.staleTime;
|
||||
|
||||
const data = await queryClient.fetchQuery({
|
||||
queryKey,
|
||||
queryFn,
|
||||
...toQueryOptions(policy)
|
||||
});
|
||||
|
||||
return {
|
||||
data,
|
||||
cache: isFresh
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {unknown[]} queryKey
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function refetchActiveEntityQuery(queryKey) {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey,
|
||||
exact: true,
|
||||
refetchType: 'active'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{queryKey: unknown[], nextData: any}} options
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function patchAndRefetchActiveQuery({ queryKey, nextData }) {
|
||||
patchQueryDataWithRecency({ queryKey, nextData });
|
||||
await refetchActiveEntityQuery(queryKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} ref
|
||||
*/
|
||||
export function patchUserFromEvent(ref) {
|
||||
if (!ref?.id) return;
|
||||
patchQueryDataWithRecency({
|
||||
queryKey: queryKeys.user(ref.id),
|
||||
nextData: {
|
||||
cache: false,
|
||||
json: ref,
|
||||
params: { userId: ref.id },
|
||||
ref
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} ref
|
||||
*/
|
||||
export function patchAvatarFromEvent(ref) {
|
||||
if (!ref?.id) return;
|
||||
patchQueryDataWithRecency({
|
||||
queryKey: queryKeys.avatar(ref.id),
|
||||
nextData: {
|
||||
cache: false,
|
||||
json: ref,
|
||||
params: { avatarId: ref.id },
|
||||
ref
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} ref
|
||||
*/
|
||||
export function patchWorldFromEvent(ref) {
|
||||
if (!ref?.id) return;
|
||||
patchQueryDataWithRecency({
|
||||
queryKey: queryKeys.world(ref.id),
|
||||
nextData: {
|
||||
cache: false,
|
||||
json: ref,
|
||||
params: { worldId: ref.id },
|
||||
ref
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} ref
|
||||
*/
|
||||
export function patchGroupFromEvent(ref) {
|
||||
if (!ref?.id) return;
|
||||
|
||||
const nextData = {
|
||||
cache: false,
|
||||
json: ref,
|
||||
params: { groupId: ref.id },
|
||||
ref
|
||||
};
|
||||
|
||||
patchQueryDataWithRecency({
|
||||
queryKey: queryKeys.group(ref.id, false),
|
||||
nextData
|
||||
});
|
||||
|
||||
patchQueryDataWithRecency({
|
||||
queryKey: queryKeys.group(ref.id, true),
|
||||
nextData
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} ref
|
||||
*/
|
||||
export function patchInstanceFromEvent(ref) {
|
||||
if (!ref?.id) return;
|
||||
|
||||
const [worldId, instanceId] = String(ref.id).split(':');
|
||||
if (!worldId || !instanceId) return;
|
||||
|
||||
patchQueryDataWithRecency({
|
||||
queryKey: queryKeys.instance(worldId, instanceId),
|
||||
nextData: {
|
||||
cache: false,
|
||||
json: ref,
|
||||
params: { worldId, instanceId },
|
||||
ref
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const _entityCacheInternals = {
|
||||
getRecencyTimestamp,
|
||||
shouldReplaceCurrent
|
||||
};
|
||||
14
src/query/index.js
Normal file
14
src/query/index.js
Normal file
@@ -0,0 +1,14 @@
|
||||
export { queryClient } from './client';
|
||||
export { queryKeys } from './keys';
|
||||
export { entityQueryPolicies, getEntityQueryPolicy, toQueryOptions } from './policies';
|
||||
export {
|
||||
fetchWithEntityPolicy,
|
||||
patchAndRefetchActiveQuery,
|
||||
patchQueryDataWithRecency,
|
||||
patchUserFromEvent,
|
||||
patchAvatarFromEvent,
|
||||
patchWorldFromEvent,
|
||||
patchGroupFromEvent,
|
||||
patchInstanceFromEvent,
|
||||
refetchActiveEntityQuery
|
||||
} from './entityCache';
|
||||
149
src/query/keys.js
Normal file
149
src/query/keys.js
Normal file
@@ -0,0 +1,149 @@
|
||||
export const queryKeys = Object.freeze({
|
||||
user: (userId) => ['user', userId],
|
||||
avatar: (avatarId) => ['avatar', avatarId],
|
||||
world: (worldId) => ['world', worldId],
|
||||
group: (groupId, includeRoles = false) => ['group', groupId, Boolean(includeRoles)],
|
||||
groupPosts: ({ groupId, n = 100, offset = 0 } = {}) => [
|
||||
'group',
|
||||
groupId,
|
||||
'posts',
|
||||
{
|
||||
n: Number(n),
|
||||
offset: Number(offset)
|
||||
}
|
||||
],
|
||||
groupMember: ({ groupId, userId } = {}) => ['group', groupId, 'member', userId],
|
||||
groupMembers: ({ groupId, n = 100, offset = 0, sort = '', roleId = '' } = {}) => [
|
||||
'group',
|
||||
groupId,
|
||||
'members',
|
||||
{
|
||||
n: Number(n),
|
||||
offset: Number(offset),
|
||||
sort: String(sort || ''),
|
||||
roleId: String(roleId || '')
|
||||
}
|
||||
],
|
||||
groupGallery: ({ groupId, galleryId, n = 100, offset = 0 } = {}) => [
|
||||
'group',
|
||||
groupId,
|
||||
'gallery',
|
||||
galleryId,
|
||||
{
|
||||
n: Number(n),
|
||||
offset: Number(offset)
|
||||
}
|
||||
],
|
||||
groupCalendar: (groupId) => ['group', groupId, 'calendar'],
|
||||
groupCalendarEvent: ({ groupId, eventId } = {}) => [
|
||||
'group',
|
||||
groupId,
|
||||
'calendarEvent',
|
||||
eventId
|
||||
],
|
||||
instance: (worldId, instanceId) => ['instance', worldId, instanceId],
|
||||
worldsByUser: ({
|
||||
userId,
|
||||
n = 50,
|
||||
offset = 0,
|
||||
sort = '',
|
||||
order = '',
|
||||
user = '',
|
||||
releaseStatus = '',
|
||||
option = ''
|
||||
} = {}) => [
|
||||
'worlds',
|
||||
'user',
|
||||
userId,
|
||||
{
|
||||
n: Number(n),
|
||||
offset: Number(offset),
|
||||
sort: String(sort || ''),
|
||||
order: String(order || ''),
|
||||
user: String(user || ''),
|
||||
releaseStatus: String(releaseStatus || ''),
|
||||
option: String(option || '')
|
||||
}
|
||||
],
|
||||
friends: ({ offline = false, n = 50, offset = 0 } = {}) => [
|
||||
'friends',
|
||||
{
|
||||
offline: Boolean(offline),
|
||||
n: Number(n),
|
||||
offset: Number(offset)
|
||||
}
|
||||
],
|
||||
favoriteLimits: () => ['favorite', 'limits'],
|
||||
favorites: ({ n = 300, offset = 0 } = {}) => [
|
||||
'favorite',
|
||||
'items',
|
||||
{
|
||||
n: Number(n),
|
||||
offset: Number(offset)
|
||||
}
|
||||
],
|
||||
favoriteGroups: ({ n = 50, offset = 0, type = '' } = {}) => [
|
||||
'favorite',
|
||||
'groups',
|
||||
{
|
||||
n: Number(n),
|
||||
offset: Number(offset),
|
||||
type: String(type || '')
|
||||
}
|
||||
],
|
||||
favoriteWorlds: ({ n = 300, offset = 0, ownerId = '', userId = '', tag = '' } = {}) => [
|
||||
'favorite',
|
||||
'worlds',
|
||||
{
|
||||
n: Number(n),
|
||||
offset: Number(offset),
|
||||
ownerId: String(ownerId || ''),
|
||||
userId: String(userId || ''),
|
||||
tag: String(tag || '')
|
||||
}
|
||||
],
|
||||
favoriteAvatars: ({ n = 300, offset = 0, tag = '', ownerId = '', userId = '' } = {}) => [
|
||||
'favorite',
|
||||
'avatars',
|
||||
{
|
||||
n: Number(n),
|
||||
offset: Number(offset),
|
||||
tag: String(tag || ''),
|
||||
ownerId: String(ownerId || ''),
|
||||
userId: String(userId || '')
|
||||
}
|
||||
],
|
||||
galleryFiles: ({ tag = '', n = 100 } = {}) => [
|
||||
'gallery',
|
||||
'files',
|
||||
{
|
||||
tag: String(tag || ''),
|
||||
n: Number(n)
|
||||
}
|
||||
],
|
||||
prints: ({ n = 100 } = {}) => [
|
||||
'gallery',
|
||||
'prints',
|
||||
{
|
||||
n: Number(n)
|
||||
}
|
||||
],
|
||||
print: (printId) => ['gallery', 'print', printId],
|
||||
inventoryItems: ({ n = 100, offset = 0, order = 'newest', types = '' } = {}) => [
|
||||
'inventory',
|
||||
'items',
|
||||
{
|
||||
n: Number(n),
|
||||
offset: Number(offset),
|
||||
order: String(order || 'newest'),
|
||||
types: String(types || '')
|
||||
}
|
||||
],
|
||||
userInventoryItem: ({ inventoryId, userId }) => [
|
||||
'inventory',
|
||||
'item',
|
||||
userId,
|
||||
inventoryId
|
||||
],
|
||||
file: (fileId) => ['file', fileId]
|
||||
});
|
||||
97
src/query/policies.js
Normal file
97
src/query/policies.js
Normal file
@@ -0,0 +1,97 @@
|
||||
const SECOND = 1000;
|
||||
|
||||
export const entityQueryPolicies = Object.freeze({
|
||||
user: Object.freeze({
|
||||
staleTime: 20 * SECOND,
|
||||
gcTime: 90 * SECOND,
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false
|
||||
}),
|
||||
avatar: Object.freeze({
|
||||
staleTime: 60 * SECOND,
|
||||
gcTime: 300 * SECOND,
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false
|
||||
}),
|
||||
world: Object.freeze({
|
||||
staleTime: 60 * SECOND,
|
||||
gcTime: 300 * SECOND,
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false
|
||||
}),
|
||||
group: Object.freeze({
|
||||
staleTime: 60 * SECOND,
|
||||
gcTime: 300 * SECOND,
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false
|
||||
}),
|
||||
groupCollection: Object.freeze({
|
||||
staleTime: 60 * SECOND,
|
||||
gcTime: 300 * SECOND,
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false
|
||||
}),
|
||||
worldCollection: Object.freeze({
|
||||
staleTime: 60 * SECOND,
|
||||
gcTime: 300 * SECOND,
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false
|
||||
}),
|
||||
instance: Object.freeze({
|
||||
staleTime: 0,
|
||||
gcTime: 10 * SECOND,
|
||||
retry: 0,
|
||||
refetchOnWindowFocus: false
|
||||
}),
|
||||
friendList: Object.freeze({
|
||||
staleTime: 20 * SECOND,
|
||||
gcTime: 90 * SECOND,
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false
|
||||
}),
|
||||
favoriteCollection: Object.freeze({
|
||||
staleTime: 60 * SECOND,
|
||||
gcTime: 300 * SECOND,
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false
|
||||
}),
|
||||
galleryCollection: Object.freeze({
|
||||
staleTime: 60 * SECOND,
|
||||
gcTime: 300 * SECOND,
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false
|
||||
}),
|
||||
inventoryCollection: Object.freeze({
|
||||
staleTime: 20 * SECOND,
|
||||
gcTime: 120 * SECOND,
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false
|
||||
}),
|
||||
fileObject: Object.freeze({
|
||||
staleTime: 60 * SECOND,
|
||||
gcTime: 300 * SECOND,
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false
|
||||
})
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {'user'|'avatar'|'world'|'group'|'groupCollection'|'worldCollection'|'instance'|'friendList'|'favoriteCollection'|'galleryCollection'|'inventoryCollection'|'fileObject'} entity
|
||||
* @returns {{staleTime: number, gcTime: number, retry: number, refetchOnWindowFocus: boolean}}
|
||||
*/
|
||||
export function getEntityQueryPolicy(entity) {
|
||||
return entityQueryPolicies[entity];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{staleTime: number, gcTime: number, retry: number, refetchOnWindowFocus: boolean}} policy
|
||||
* @returns {{staleTime: number, gcTime: number, retry: number, refetchOnWindowFocus: boolean}}
|
||||
*/
|
||||
export function toQueryOptions(policy) {
|
||||
return {
|
||||
staleTime: policy.staleTime,
|
||||
gcTime: policy.gcTime,
|
||||
retry: policy.retry,
|
||||
refetchOnWindowFocus: policy.refetchOnWindowFocus
|
||||
};
|
||||
}
|
||||
55
src/query/useEntityQueries.js
Normal file
55
src/query/useEntityQueries.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import { useQuery } from '@tanstack/vue-query';
|
||||
|
||||
import { avatarRequest, groupRequest, instanceRequest, userRequest, worldRequest } from '../api';
|
||||
import { queryKeys } from './keys';
|
||||
import { entityQueryPolicies, toQueryOptions } from './policies';
|
||||
|
||||
export function useUserQuery(userId, options = {}) {
|
||||
return useQuery({
|
||||
...options,
|
||||
queryKey: queryKeys.user(userId),
|
||||
queryFn: () => userRequest.getUser({ userId }),
|
||||
enabled: Boolean(userId),
|
||||
...toQueryOptions(entityQueryPolicies.user)
|
||||
});
|
||||
}
|
||||
|
||||
export function useAvatarQuery(avatarId, options = {}) {
|
||||
return useQuery({
|
||||
...options,
|
||||
queryKey: queryKeys.avatar(avatarId),
|
||||
queryFn: () => avatarRequest.getAvatar({ avatarId }),
|
||||
enabled: Boolean(avatarId),
|
||||
...toQueryOptions(entityQueryPolicies.avatar)
|
||||
});
|
||||
}
|
||||
|
||||
export function useWorldQuery(worldId, options = {}) {
|
||||
return useQuery({
|
||||
...options,
|
||||
queryKey: queryKeys.world(worldId),
|
||||
queryFn: () => worldRequest.getWorld({ worldId }),
|
||||
enabled: Boolean(worldId),
|
||||
...toQueryOptions(entityQueryPolicies.world)
|
||||
});
|
||||
}
|
||||
|
||||
export function useGroupQuery(groupId, includeRoles = false, options = {}) {
|
||||
return useQuery({
|
||||
...options,
|
||||
queryKey: queryKeys.group(groupId, includeRoles),
|
||||
queryFn: () => groupRequest.getGroup({ groupId, includeRoles }),
|
||||
enabled: Boolean(groupId),
|
||||
...toQueryOptions(entityQueryPolicies.group)
|
||||
});
|
||||
}
|
||||
|
||||
export function useInstanceQuery(worldId, instanceId, options = {}) {
|
||||
return useQuery({
|
||||
...options,
|
||||
queryKey: queryKeys.instance(worldId, instanceId),
|
||||
queryFn: () => instanceRequest.getInstance({ worldId, instanceId }),
|
||||
enabled: Boolean(worldId && instanceId),
|
||||
...toQueryOptions(entityQueryPolicies.instance)
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user