refactor: Introduce granular query types with specific policies for improved caching and data freshness.

This commit is contained in:
pa
2026-03-11 14:37:43 +09:00
parent 14d73b1532
commit a75c4b89f8
23 changed files with 221 additions and 39 deletions
+60
View File
@@ -161,6 +161,59 @@ describe('queryRequest', () => {
expect(args.json.id).toBe('usr_1'); expect(args.json.id).toBe('usr_1');
}); });
test('uses same queryKey for user and user.dialog callers', async () => {
const data = { json: { id: 'usr_1' }, params: { userId: 'usr_1' } };
mockGetUser.mockResolvedValue(data);
mockFetchWithEntityPolicy.mockImplementation(async ({ queryFn }) => ({
data: await queryFn(),
cache: false
}));
await queryRequest.fetch('user', { userId: 'usr_1' });
await queryRequest.fetch('user.dialog', { userId: 'usr_1' });
const baseCall = mockFetchWithEntityPolicy.mock.calls[0][0];
const dialogCall = mockFetchWithEntityPolicy.mock.calls[1][0];
expect(baseCall.queryKey).toEqual(['user', 'usr_1']);
expect(dialogCall.queryKey).toEqual(baseCall.queryKey);
});
test('applies staleTime zero for user.force', async () => {
const data = { json: { id: 'usr_2' }, params: { userId: 'usr_2' } };
mockGetUser.mockResolvedValue(data);
mockFetchWithEntityPolicy.mockImplementation(async ({ queryFn }) => ({
data: await queryFn(),
cache: false
}));
await queryRequest.fetch('user.force', { userId: 'usr_2' });
expect(mockFetchWithEntityPolicy).toHaveBeenCalledWith(
expect.objectContaining({
policy: expect.objectContaining({ staleTime: 0 }),
label: 'user.force'
})
);
});
test('applies staleTime 60000 for user.dialog', async () => {
const data = { json: { id: 'usr_3' }, params: { userId: 'usr_3' } };
mockGetUser.mockResolvedValue(data);
mockFetchWithEntityPolicy.mockImplementation(async ({ queryFn }) => ({
data: await queryFn(),
cache: false
}));
await queryRequest.fetch('user.dialog', { userId: 'usr_3' });
expect(mockFetchWithEntityPolicy).toHaveBeenCalledWith(
expect.objectContaining({
policy: expect.objectContaining({ staleTime: 60_000 }),
label: 'user.dialog'
})
);
});
test('supports worldsByUser option routing', async () => { test('supports worldsByUser option routing', async () => {
const params = { const params = {
userId: 'usr_me', userId: 'usr_me',
@@ -214,4 +267,11 @@ describe('queryRequest', () => {
queryRequest.fetch('missing_resource', {}) queryRequest.fetch('missing_resource', {})
).rejects.toThrow('Unknown query resource'); ).rejects.toThrow('Unknown query resource');
}); });
test('throws on unknown caller variant', async () => {
await expect(
// @ts-expect-error verifying runtime guard
queryRequest.fetch('user.unknown', { userId: 'usr_1' })
).rejects.toThrow('Unknown query resource: user.unknown');
});
}); });
+66 -1
View File
@@ -21,16 +21,64 @@ const registry = Object.freeze({
policy: entityQueryPolicies.user, policy: entityQueryPolicies.user,
queryFn: (params) => userRequest.getUser(params) queryFn: (params) => userRequest.getUser(params)
}, },
'user.dialog': {
key: (params) => queryKeys.user(params.userId),
policy: Object.freeze({
...entityQueryPolicies.user,
staleTime: 60_000
}),
queryFn: (params) => userRequest.getUser(params)
},
'user.force': {
key: (params) => queryKeys.user(params.userId),
policy: Object.freeze({
...entityQueryPolicies.user,
staleTime: 0
}),
queryFn: (params) => userRequest.getUser(params)
},
avatar: { avatar: {
key: (params) => queryKeys.avatar(params.avatarId), key: (params) => queryKeys.avatar(params.avatarId),
policy: entityQueryPolicies.avatar, policy: entityQueryPolicies.avatar,
queryFn: (params) => avatarRequest.getAvatar(params) queryFn: (params) => avatarRequest.getAvatar(params)
}, },
'avatar.dialog': {
key: (params) => queryKeys.avatar(params.avatarId),
policy: Object.freeze({
...entityQueryPolicies.avatar,
staleTime: 120_000
}),
queryFn: (params) => avatarRequest.getAvatar(params)
},
world: { world: {
key: (params) => queryKeys.world(params.worldId), key: (params) => queryKeys.world(params.worldId),
policy: entityQueryPolicies.world, policy: entityQueryPolicies.world,
queryFn: (params) => worldRequest.getWorld(params) queryFn: (params) => worldRequest.getWorld(params)
}, },
'world.dialog': {
key: (params) => queryKeys.world(params.worldId),
policy: Object.freeze({
...entityQueryPolicies.world,
staleTime: 120_000
}),
queryFn: (params) => worldRequest.getWorld(params)
},
'world.location': {
key: (params) => queryKeys.world(params.worldId),
policy: Object.freeze({
...entityQueryPolicies.world,
staleTime: 120_000
}),
queryFn: (params) => worldRequest.getWorld(params)
},
'world.force': {
key: (params) => queryKeys.world(params.worldId),
policy: Object.freeze({
...entityQueryPolicies.world,
staleTime: 0
}),
queryFn: (params) => worldRequest.getWorld(params)
},
worldsByUser: { worldsByUser: {
key: (params) => queryKeys.worldsByUser(params), key: (params) => queryKeys.worldsByUser(params),
policy: entityQueryPolicies.worldCollection, policy: entityQueryPolicies.worldCollection,
@@ -42,6 +90,22 @@ const registry = Object.freeze({
policy: entityQueryPolicies.group, policy: entityQueryPolicies.group,
queryFn: (params) => groupRequest.getGroup(params) queryFn: (params) => groupRequest.getGroup(params)
}, },
'group.dialog': {
key: (params) => queryKeys.group(params.groupId, params.includeRoles),
policy: Object.freeze({
...entityQueryPolicies.group,
staleTime: 120_000
}),
queryFn: (params) => groupRequest.getGroup(params)
},
'group.force': {
key: (params) => queryKeys.group(params.groupId, params.includeRoles),
policy: Object.freeze({
...entityQueryPolicies.group,
staleTime: 0
}),
queryFn: (params) => groupRequest.getGroup(params)
},
groupMember: { groupMember: {
key: (params) => queryKeys.groupMember(params), key: (params) => queryKeys.groupMember(params),
policy: entityQueryPolicies.groupCollection, policy: entityQueryPolicies.groupCollection,
@@ -135,7 +199,8 @@ const queryRequest = {
const { data, cache } = await fetchWithEntityPolicy({ const { data, cache } = await fetchWithEntityPolicy({
queryKey: entry.key(params), queryKey: entry.key(params),
policy: entry.policy, policy: entry.policy,
queryFn: () => entry.queryFn(params) queryFn: () => entry.queryFn(params),
label: resource
}); });
return { return {
+1 -1
View File
@@ -28,7 +28,7 @@
if (props.hint) { if (props.hint) {
username.value = props.hint; username.value = props.hint;
} else if (props.userid) { } else if (props.userid) {
const args = await queryRequest.fetch('user', { userId: props.userid }); const args = await queryRequest.fetch('user.dialog', { userId: props.userid });
if (args?.json?.displayName) { if (args?.json?.displayName) {
username.value = args.json.displayName; username.value = args.json.displayName;
} }
@@ -701,7 +701,7 @@
selectedImageUrl: post.imageUrl selectedImageUrl: post.imageUrl
}; };
} }
queryRequest.fetch('group', { groupId }).then((args) => { queryRequest.fetch('group.dialog', { groupId }).then((args) => {
D.groupRef = args.ref; D.groupRef = args.ref;
}); });
D.visible = true; D.visible = true;
@@ -362,7 +362,7 @@ describe('useGroupModerationData', () => {
const { addGroupMemberToSelection } = useGroupModerationData(deps); const { addGroupMemberToSelection } = useGroupModerationData(deps);
await addGroupMemberToSelection('usr_1'); await addGroupMemberToSelection('usr_1');
expect(queryRequest.fetch).toHaveBeenCalledWith('user', { userId: 'usr_1' }); expect(queryRequest.fetch).toHaveBeenCalledWith('user.dialog', { userId: 'usr_1' });
expect(deps.selection.setSelectedUsers).toHaveBeenCalledWith('usr_1', expect.objectContaining({ expect(deps.selection.setSelectedUsers).toHaveBeenCalledWith('usr_1', expect.objectContaining({
userId: 'usr_1', userId: 'usr_1',
displayName: 'Alice' displayName: 'Alice'
@@ -492,7 +492,7 @@ export function useGroupModerationData(deps) {
selection.setSelectedUsers(member.userId, member); selection.setSelectedUsers(member.userId, member);
return; return;
} }
const userArgs = await queryRequest.fetch('user', { userId }); const userArgs = await queryRequest.fetch('user.dialog', { userId });
member.userId = userArgs.json.id; member.userId = userArgs.json.id;
member.user = userArgs.json; member.user = userArgs.json;
member.displayName = userArgs.json.displayName; member.displayName = userArgs.json.displayName;
+1 -1
View File
@@ -258,7 +258,7 @@
} }
if (D.userId) { if (D.userId) {
queryRequest.fetch('user', { userId: D.userId }).then((args) => { queryRequest.fetch('user.dialog', { userId: D.userId }).then((args) => {
D.userObject = args.ref; D.userObject = args.ref;
D.userIds = [D.userId]; D.userIds = [D.userId];
}); });
@@ -129,7 +129,7 @@
} }
if (D.userId) { if (D.userId) {
queryRequest.fetch('user', { userId: D.userId }).then((args) => { queryRequest.fetch('user.dialog', { userId: D.userId }).then((args) => {
D.userObject = args.ref; D.userObject = args.ref;
}); });
} }
+1 -1
View File
@@ -99,7 +99,7 @@
(visible) => { (visible) => {
if (visible) { if (visible) {
displayName.value = ''; displayName.value = '';
queryRequest.fetch('user', { userId: sendBoopDialog.value.userId }).then((user) => { queryRequest.fetch('user.dialog', { userId: sendBoopDialog.value.userId }).then((user) => {
displayName.value = user.ref.displayName; displayName.value = user.ref.displayName;
}); });
} }
+1 -1
View File
@@ -124,7 +124,7 @@ export function showAvatarDialog(avatarId, options = {}) {
} }
const loadAvatarRequest = forceRefresh const loadAvatarRequest = forceRefresh
? avatarRequest.getAvatar({ avatarId }) ? avatarRequest.getAvatar({ avatarId })
: queryRequest.fetch('avatar', { avatarId }); : queryRequest.fetch('avatar.dialog', { avatarId });
loadAvatarRequest loadAvatarRequest
.then((args) => { .then((args) => {
const ref = applyAvatar(args.json); const ref = applyAvatar(args.json);
+5 -5
View File
@@ -224,10 +224,10 @@ function groupChange(ref, message) {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async function groupOwnerChange(ref, oldUserId, newUserId) { async function groupOwnerChange(ref, oldUserId, newUserId) {
const oldUser = await queryRequest.fetch('user', { const oldUser = await queryRequest.fetch('user.dialog', {
userId: oldUserId userId: oldUserId
}); });
const newUser = await queryRequest.fetch('user', { const newUser = await queryRequest.fetch('user.dialog', {
userId: newUserId userId: newUserId
}); });
const oldDisplayName = oldUser?.ref?.displayName; const oldDisplayName = oldUser?.ref?.displayName;
@@ -343,7 +343,7 @@ export function showGroupDialog(groupId, options = {}) {
D.visible = true; D.visible = true;
D.loading = false; D.loading = false;
queryRequest queryRequest
.fetch('user', { .fetch('user.dialog', {
userId: ref.ownerId userId: ref.ownerId
}) })
.then((args1) => { .then((args1) => {
@@ -375,7 +375,7 @@ export function getGroupDialogGroup(groupId, existingRef) {
const refPromise = existingRef const refPromise = existingRef
? Promise.resolve({ ref: existingRef }) ? Promise.resolve({ ref: existingRef })
: queryRequest : queryRequest
.fetch('group', { groupId, includeRoles: true }) .fetch('group.dialog', { groupId, includeRoles: true })
.then((args) => ({ ref: applyGroup(args.json), args })); .then((args) => ({ ref: applyGroup(args.json), args }));
return refPromise return refPromise
@@ -416,7 +416,7 @@ export function getGroupDialogGroup(groupId, existingRef) {
for (const json of args.json.instances) { for (const json of args.json.instances) {
instanceStore.applyInstance(json); instanceStore.applyInstance(json);
queryRequest queryRequest
.fetch('world', { .fetch('world.dialog', {
worldId: json.world.id worldId: json.world.id
}) })
.then((args1) => { .then((args1) => {
+57
View File
@@ -1,5 +1,16 @@
import { beforeEach, describe, expect, test, vi } from 'vitest'; import { beforeEach, describe, expect, test, vi } from 'vitest';
const mockLogWebRequest = vi.fn();
const mockWithQueryLog = vi.fn(async (fn) => fn());
vi.mock('../../services/appConfig', () => ({
AppDebug: {
debugWebRequests: false
},
logWebRequest: (...args) => mockLogWebRequest(...args),
withQueryLog: (...args) => mockWithQueryLog(...args)
}));
import { import {
_entityCacheInternals, _entityCacheInternals,
fetchWithEntityPolicy, fetchWithEntityPolicy,
@@ -13,6 +24,8 @@ describe('entity query cache helpers', () => {
beforeEach(() => { beforeEach(() => {
queryClient.clear(); queryClient.clear();
vi.restoreAllMocks(); vi.restoreAllMocks();
mockLogWebRequest.mockClear();
mockWithQueryLog.mockClear();
}); });
test('reports cache hit for fresh data', async () => { test('reports cache hit for fresh data', async () => {
@@ -75,6 +88,50 @@ describe('entity query cache helpers', () => {
expect(callCount).toBe(2); expect(callCount).toBe(2);
}); });
test('uses label in logs without changing cache behavior', async () => {
const queryKey = ['user', 'usr_9'];
const policy = {
staleTime: 20000,
gcTime: 90000,
retry: 1,
refetchOnWindowFocus: false
};
const queryFn = vi.fn(async () => ({
json: { id: 'usr_9', updated_at: '2026-01-01T00:00:00.000Z' },
params: { userId: 'usr_9' },
ref: { id: 'usr_9', updated_at: '2026-01-01T00:00:00.000Z' }
}));
const first = await fetchWithEntityPolicy({
queryKey,
policy,
queryFn,
label: 'user.dialog'
});
const second = await fetchWithEntityPolicy({
queryKey,
policy,
queryFn
});
expect(first.cache).toBe(false);
expect(second.cache).toBe(true);
expect(mockLogWebRequest).toHaveBeenNthCalledWith(
1,
'[QUERY FETCH]',
'user.dialog',
queryKey,
expect.any(Object)
);
expect(mockLogWebRequest).toHaveBeenNthCalledWith(
2,
'[QUERY CACHE HIT]',
'user',
queryKey,
expect.any(Object)
);
});
test('does not overwrite newer data with older payload', () => { test('does not overwrite newer data with older payload', () => {
const queryKey = ['world', 'wrld_1']; const queryKey = ['world', 'wrld_1'];
+6 -6
View File
@@ -26,8 +26,8 @@ describe('query policy configuration', () => {
}); });
expect(entityQueryPolicies.group).toMatchObject({ expect(entityQueryPolicies.group).toMatchObject({
staleTime: 60000, staleTime: 300000,
gcTime: 300000, gcTime: 1800000,
retry: 1, retry: 1,
refetchOnWindowFocus: false refetchOnWindowFocus: false
}); });
@@ -89,8 +89,8 @@ describe('query policy configuration', () => {
test('file-related policies', () => { test('file-related policies', () => {
expect(entityQueryPolicies.fileAnalysis).toMatchObject({ expect(entityQueryPolicies.fileAnalysis).toMatchObject({
staleTime: 120000, staleTime: 600000,
gcTime: 600000, gcTime: 3600000,
retry: 1, retry: 1,
refetchOnWindowFocus: false refetchOnWindowFocus: false
}); });
@@ -164,8 +164,8 @@ describe('query policy configuration', () => {
const options = toQueryOptions(entityQueryPolicies.group); const options = toQueryOptions(entityQueryPolicies.group);
expect(options).toEqual({ expect(options).toEqual({
staleTime: 60000, staleTime: 300000,
gcTime: 300000, gcTime: 1800000,
retry: 1, retry: 1,
refetchOnWindowFocus: false refetchOnWindowFocus: false
}); });
+4 -4
View File
@@ -140,10 +140,10 @@ export function patchQueryDataWithRecency({ queryKey, nextData }) {
} }
/** /**
* @param {{queryKey: unknown[], policy: {staleTime: number, gcTime: number, retry: number, refetchOnWindowFocus: boolean}, queryFn: () => Promise<any>}} options * @param {{queryKey: unknown[], policy: {staleTime: number, gcTime: number, retry: number, refetchOnWindowFocus: boolean}, queryFn: () => Promise<any>, label?: string}} options
* @returns {Promise<{data: any, cache: boolean}>} * @returns {Promise<{data: any, cache: boolean}>}
*/ */
export async function fetchWithEntityPolicy({ queryKey, policy, queryFn }) { export async function fetchWithEntityPolicy({ queryKey, policy, queryFn, label }) {
const queryState = queryClient.getQueryState(queryKey); const queryState = queryClient.getQueryState(queryKey);
const isFresh = const isFresh =
Boolean(queryState?.dataUpdatedAt) && Boolean(queryState?.dataUpdatedAt) &&
@@ -157,9 +157,9 @@ export async function fetchWithEntityPolicy({ queryKey, policy, queryFn }) {
}); });
if (isFresh) { if (isFresh) {
logWebRequest('[QUERY CACHE HIT]', queryKey, data); logWebRequest('[QUERY CACHE HIT]', label || queryKey[0], queryKey, data);
} else { } else {
logWebRequest('[QUERY FETCH]', queryKey, data); logWebRequest('[QUERY FETCH]', label || queryKey[0], queryKey, data);
} }
return { return {
+1 -1
View File
@@ -59,7 +59,7 @@ async function getGroupName(data) {
} }
} }
try { try {
const args = await queryRequest.fetch('group', { const args = await queryRequest.fetch('group.dialog', {
groupId groupId
}); });
groupName = args.ref.name; groupName = args.ref.name;
+1 -1
View File
@@ -13,7 +13,7 @@ async function getWorldName(location) {
const L = parseLocation(location); const L = parseLocation(location);
if (L.isRealInstance && L.worldId) { if (L.isRealInstance && L.worldId) {
try { try {
const args = await queryRequest.fetch('world', { const args = await queryRequest.fetch('world.dialog', {
worldId: L.worldId worldId: L.worldId
}); });
worldName = args.ref.name; worldName = args.ref.name;
+2 -2
View File
@@ -363,7 +363,7 @@ export const useGalleryStore = defineStore('Gallery', () => {
const print = args.json; const print = args.json;
const createdAt = getPrintLocalDate(print); const createdAt = getPrintLocalDate(print);
try { try {
const owner = await queryRequest.fetch('user', { const owner = await queryRequest.fetch('user.dialog', {
userId: print.ownerId userId: print.ownerId
}); });
console.log( console.log(
@@ -558,7 +558,7 @@ export const useGalleryStore = defineStore('Gallery', () => {
return; return;
} }
const userArgs = await queryRequest.fetch('user', { const userArgs = await queryRequest.fetch('user.dialog', {
userId: args.json.holderId userId: args.json.holderId
}); });
const displayName = userArgs.json?.displayName ?? ''; const displayName = userArgs.json?.displayName ?? '';
+1 -1
View File
@@ -274,7 +274,7 @@ export const useGroupStore = defineStore('Group', () => {
D.groupRef = {}; D.groupRef = {};
D.auditLogTypes = []; D.auditLogTypes = [];
queryRequest.fetch('group', { groupId }).then((args) => { queryRequest.fetch('group.dialog', { groupId }).then((args) => {
D.groupRef = args.ref; D.groupRef = args.ref;
if (hasGroupPermission(D.groupRef, 'group-audit-view')) { if (hasGroupPermission(D.groupRef, 'group-audit-view')) {
groupRequest.getGroupAuditLogTypes({ groupId }).then((args) => { groupRequest.getGroupAuditLogTypes({ groupId }).then((args) => {
+6 -6
View File
@@ -245,7 +245,7 @@ export const useInstanceStore = defineStore('Instance', () => {
emptyDefault: { id: '', displayName: '' }, emptyDefault: { id: '', displayName: '' },
idAlias: 'userId', idAlias: 'userId',
nameKey: 'displayName', nameKey: 'displayName',
fetchFn: (id) => queryRequest.fetch('user', { userId: id }) fetchFn: (id) => queryRequest.fetch('user.dialog', { userId: id })
}); });
} }
@@ -258,7 +258,7 @@ export const useInstanceStore = defineStore('Instance', () => {
emptyDefault: { id: '', name: '' }, emptyDefault: { id: '', name: '' },
idAlias: 'worldId', idAlias: 'worldId',
nameKey: 'name', nameKey: 'name',
fetchFn: (id) => queryRequest.fetch('world', { worldId: id }) fetchFn: (id) => queryRequest.fetch('world.location', { worldId: id })
}); });
} }
@@ -271,7 +271,7 @@ export const useInstanceStore = defineStore('Instance', () => {
emptyDefault: { id: '', name: '' }, emptyDefault: { id: '', name: '' },
idAlias: 'groupId', idAlias: 'groupId',
nameKey: 'name', nameKey: 'name',
fetchFn: (id) => queryRequest.fetch('group', { groupId: id }) fetchFn: (id) => queryRequest.fetch('group.dialog', { groupId: id })
}); });
} }
@@ -340,7 +340,7 @@ export const useInstanceStore = defineStore('Instance', () => {
!worldStore.cachedWorlds.get(location.worldId)?.name !worldStore.cachedWorlds.get(location.worldId)?.name
) { ) {
queryRequest queryRequest
.fetch('world', { worldId: location.worldId }) .fetch('world.dialog', { worldId: location.worldId })
.then((args) => { .then((args) => {
uiStore.setDialogCrumbLabel( uiStore.setDialogCrumbLabel(
'previous-instances-info', 'previous-instances-info',
@@ -467,7 +467,7 @@ export const useInstanceStore = defineStore('Instance', () => {
}); });
} else { } else {
queryRequest queryRequest
.fetch('world', { .fetch('world.location', {
worldId: currentInstanceLocation.value.worldId worldId: currentInstanceLocation.value.worldId
}) })
.then((args) => { .then((args) => {
@@ -537,7 +537,7 @@ export const useInstanceStore = defineStore('Instance', () => {
ref.$location = parseLocation(ref.location); ref.$location = parseLocation(ref.location);
if (ref.world?.id) { if (ref.world?.id) {
queryRequest queryRequest
.fetch('world', { .fetch('world.location', {
worldId: ref.world.id worldId: ref.world.id
}) })
.then((args) => { .then((args) => {
+1 -1
View File
@@ -221,7 +221,7 @@ export const useDiscordPresenceSettingsStore = defineStore(
groupAccessName: '' groupAccessName: ''
}; };
try { try {
const args = await queryRequest.fetch('world', { const args = await queryRequest.fetch('world.location', {
worldId: L.worldId worldId: L.worldId
}); });
state.lastLocationDetails.worldName = args.ref.name; state.lastLocationDetails.worldName = args.ref.name;
+1 -1
View File
@@ -644,7 +644,7 @@ export const useVrcxStore = defineStore('Vrcx', () => {
toast.error('Invalid local favorite world command'); toast.error('Invalid local favorite world command');
break; break;
} }
queryRequest.fetch('world', { worldId: id }).then(() => { queryRequest.fetch('world.location', { worldId: id }).then(() => {
searchStore.directAccessWorld(id); searchStore.directAccessWorld(id);
addLocalWorldFavorite(id, group); addLocalWorldFavorite(id, group);
}); });
@@ -190,7 +190,7 @@
currentLocation = lastLocationDestination.value; currentLocation = lastLocationDestination.value;
} }
const L = parseLocation(currentLocation); const L = parseLocation(currentLocation);
queryRequest.fetch('world', { worldId: L.worldId }).then((args) => { queryRequest.fetch('world.location', { worldId: L.worldId }).then((args) => {
notificationRequest notificationRequest
.sendInvite( .sendInvite(
{ {
@@ -761,7 +761,7 @@
currentLocation = lastLocationDestination.value; currentLocation = lastLocationDestination.value;
} }
const L = parseLocation(currentLocation); const L = parseLocation(currentLocation);
queryRequest.fetch('world', { worldId: L.worldId }).then((args) => { queryRequest.fetch('world.location', { worldId: L.worldId }).then((args) => {
notificationRequest notificationRequest
.sendInvite( .sendInvite(
{ {