-
Notifications
You must be signed in to change notification settings - Fork 36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Invalidation should trigger a call to getParameter to get a fresh context #981
Comments
Hey @strblr, thank you for all the feedback so far ❤️ I wouldn't categorize it as a We can allow passing a second (optional) function parameter to the graphql-live-query/packages/in-memory-live-query-store/src/InMemoryLiveQueryStore.ts Line 233 in 9d35033
(execute: typeof defaultExecute, contextFactory: Context | (initialContext: Context) => PromiseOrValue<Context>) => What do you think? Would you be open to creating the PR for this? |
Thank you for your quick response @n1ru4l, I'm glad if my feedback was useful ! If I understand your idea correctly, it would be to pass the initial graphql-live-query/packages/in-memory-live-query-store/src/InMemoryLiveQueryStore.ts Lines 354 to 359 in 9d35033
Like this: [originalContextSymbol]: await contextFactory(contextValue), ? |
@strblr Exactly! |
@n1ru4l This would make |
I tried to find a workaround without changing the library. I don't understand why something like this doesn't work: import { execute as defaultExecute, ExecutionArgs } from "graphql";
import { flowRight } from "lodash-es";
// ...
const live = new InMemoryLiveQueryStore();
const makeLazyContextExecute = (contextFactory: () => unknown) => (
execute: typeof defaultExecute
) => async (args: ExecutionArgs) =>
execute({ ...args, contextValue: await contextFactory() });
registerSocketIOGraphQLServer({
socketServer,
getParameter: async ({ graphQLPayload: { extensions } }) => ({
execute: flowRight(
applyLiveQueryJSONPatchGenerator,
flowRight(
makeLazyContextExecute(() => getContext(extensions?.authorization)),
live.makeExecute
)(defaultExecute)
),
graphQLExecutionParameter: { schema }
})
});
// getContext creates a new context with new dataloaders It's a bit like the idea you suggested, only using external composition to create a special In my understanding, this should totally discard the initial Thanks a lot! |
Fixed it. There were two issues:
Although hacky, this works: const makeLazyContextExecute = (
contextFactory: (previous: unknown) => unknown
) => (execute: typeof defaultExecute) => async (args: ExecutionArgs) =>
execute({ ...args, contextValue: await contextFactory(args.contextValue) });
registerSocketIOGraphQLServer({
socketServer,
getParameter: async ({ graphQLPayload: { extensions } }) => ({
execute: flowRight(
applyLiveQueryJSONPatchGenerator,
flowRight(
live.makeExecute,
makeLazyContextExecute(async previous => {
const ctx = await getContext(extensions?.authorization);
const sym =
isObject(previous) && Object.getOwnPropertySymbols(previous)[0];
return !sym ? ctx : { ...previous, [sym]: ctx };
})
)(defaultExecute)
),
graphQLExecutionParameter: { schema }
})
}); (Exporting I'll still try to PR the solution you suggested @n1ru4l |
@strblr Yeah, I think the simpler API would be very convenient for library users! Looking forward to your PR! |
Hi! Did anything come out of this? :) |
Had a quick go at an envelop-implementation that keeps the API relatively simple for the user. Works for both live and non-live queries. Lemme know what you think! Edit: It also implements patch delivery format based on context. useLiveQuery.ts: export interface UseLiveQueryOptions<Context extends Record<string, any>> {
applyLiveQueryPatchGenerator?: (
context: Context,
) => ReturnType<typeof createApplyLiveQueryPatchGenerator> | null;
contextFactory: (previous: Context) => unknown;
liveQueryStore: InMemoryLiveQueryStore;
}
const makeLazyContextExecute =
<Context extends Record<string, any>>(
contextFactory: (previous: Context) => unknown,
) =>
(execute: typeof defaultExecute) =>
makeStrongExecute(async (args) => {
const liveQuerySymbol =
isObject(args.contextValue) &&
Object.getOwnPropertySymbols(args.contextValue)[0];
if (liveQuerySymbol) {
const ctx = await contextFactory(args.contextValue[liveQuerySymbol]);
return execute({
...args,
contextValue: { ...args.contextValue, [liveQuerySymbol]: ctx },
});
} else {
const ctx = await contextFactory(args.contextValue);
return execute({
...args,
contextValue: ctx,
});
}
});
const makeWithPatchDeliveryExecute =
<Context extends Record<string, any>>(opts: UseLiveQueryOptions<Context>) =>
(execute: typeof defaultExecute) =>
makeStrongExecute(async (args: TypedExecutionArgs<Context>) => {
const applyLiveQueryPatchGenerator = opts.applyLiveQueryPatchGenerator
? opts.applyLiveQueryPatchGenerator(args.contextValue)
: undefined;
if (applyLiveQueryPatchGenerator) {
return pipe(args, execute, applyLiveQueryPatchGenerator);
}
return pipe(args, execute);
});
export const useLiveQuery = <Context extends Record<string, any>>(
opts: UseLiveQueryOptions<Context>,
): Plugin<Record<string, any>> => {
return {
onContextBuilding: ({ extendContext }) => {
extendContext({
liveQueryStore: opts.liveQueryStore,
});
},
onExecute: ({ executeFn, setExecuteFn }) => {
const execute = pipe(
executeFn,
makeLazyContextExecute(opts.contextFactory),
opts.liveQueryStore.makeExecute,
makeWithPatchDeliveryExecute(opts),
);
setExecuteFn(execute);
},
onValidate: ({ addValidationRule }) => {
addValidationRule(NoLiveMixedWithDeferStreamRule);
},
};
}; makeStrongExecute.ts: export function makeStrongExecute<ReturnType>(
weakExecute: (args: ExecutionArgs) => ReturnType,
) {
function execute(args: ExecutionArgs): ReturnType;
function execute(
schema: GraphQLSchema,
document: DocumentNode,
rootValue?: any,
contextValue?: any,
variableValues?: Maybe<{ [key: string]: any }>,
operationName?: Maybe<string>,
fieldResolver?: Maybe<GraphQLFieldResolver<any, any>>,
typeResolver?: Maybe<GraphQLTypeResolver<any, any>>,
): ReturnType;
function execute(
argsOrSchema: ExecutionArgs | GraphQLSchema,
...otherArgs: any[]
): ReturnType {
if (argsOrSchema instanceof GraphQLSchema) {
return weakExecute({
contextValue: otherArgs[2],
document: otherArgs[0],
fieldResolver: otherArgs[5],
operationName: otherArgs[4],
rootValue: otherArgs[1],
schema: argsOrSchema,
typeResolver: otherArgs[6],
variableValues: otherArgs[3],
});
}
return weakExecute(argsOrSchema);
}
return execute;
} usage.ts: function rescopeContext(context: Context) {
return { ...context, scope: v4() };
}
function getLiveQueryPatchGenerator(context: Context) {
const { patchDeliveryFormat } = context;
if (patchDeliveryFormat === PatchDeliveryFormat.JSONDIFFPATCH) {
return applyLiveQueryJSONDiffPatchGenerator;
}
if (patchDeliveryFormat === PatchDeliveryFormat.JSONPATCH) {
return applyLiveQueryJSONPatchGenerator;
}
return null;
}
useLiveQuery({
applyLiveQueryPatchGenerator: getLiveQueryPatchGenerator,
contextFactory: rescopeContext,
liveQueryStore,
}), |
Hey,
For some context, I was facing the following bug: #980 (comment)
If I'm not mistaken, getParameter is not re-called when re-executing an invalidated query. This is I think the cause of the bug: dataloaders are instantiated when building the
contextValue
, so ingetParameter
. Thus, subsequent invalidation would make use of old dataloaders, with stale cached data in it. If any resolver depends on dataloaders, invalidation re-execution would use outdated data.This seems like a major bug / limitation, dataloaders are very much needed in large apps. I'd suggest including the call to
getParameter
inside the routine that is launched on invalidation.The text was updated successfully, but these errors were encountered: