From fcacf32585e4a9b3e8f8648dd650ec13b9c5a28e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 17:46:40 +0000 Subject: [PATCH 01/10] chore: plan command-query tagged requests Agent-Logs-Url: https://github.com/effect-app/libs/sessions/207b39e6-c3a8-4822-b7ad-31589e0a984e Co-authored-by: patroza <42661+patroza@users.noreply.github.com> --- packages/effect-app/src/Schema/ext.ts | 2 +- packages/vue/src/query.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/effect-app/src/Schema/ext.ts b/packages/effect-app/src/Schema/ext.ts index 2726c32b56..8ccdc4a648 100644 --- a/packages/effect-app/src/Schema/ext.ts +++ b/packages/effect-app/src/Schema/ext.ts @@ -100,7 +100,7 @@ export const Literals = (getRuntime: () => Context.Context) => { get value() { return (arr as any)() } - } as any) + }) : ref(arg) const queryKey = makeQueryKey(q) const handler = q.handler From b6b80334d8564b0a46fd6585022a9ed1be55fa88 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 17:50:47 +0000 Subject: [PATCH 02/10] feat: split tagged requests into Query and Command with typed helpers Agent-Logs-Url: https://github.com/effect-app/libs/sessions/207b39e6-c3a8-4822-b7ad-31589e0a984e Co-authored-by: patroza <42661+patroza@users.noreply.github.com> --- .../tagged-request-command-query-split.md | 8 ++ packages/effect-app/src/Schema/ext.ts | 2 +- .../effect-app/src/client/apiClientFactory.ts | 1 + packages/effect-app/src/client/makeClient.ts | 36 +++-- packages/effect-app/test/rpc.test.ts | 4 +- packages/infra/test/controller.test.ts | 12 +- packages/vue/src/makeClient.ts | 123 +++++++++++------- packages/vue/src/query.ts | 2 +- packages/vue/test/makeClient.test.ts | 10 +- packages/vue/test/stubs.ts | 18 ++- 10 files changed, 143 insertions(+), 73 deletions(-) create mode 100644 .changeset/tagged-request-command-query-split.md diff --git a/.changeset/tagged-request-command-query-split.md b/.changeset/tagged-request-command-query-split.md new file mode 100644 index 0000000000..385aa6bc5d --- /dev/null +++ b/.changeset/tagged-request-command-query-split.md @@ -0,0 +1,8 @@ +--- +"effect-app": patch +"@effect-app/vue": patch +--- + +Split `TaggedRequestFor` into `Query` and `Command` factories, and mark generated request classes with `type: "query" | "command"`. + +Vue client helpers now expose query-only helpers (`query`, `suspense`, `fetch`) for query requests and mutation-only helpers (`mutate`, `fetch`) for command requests. diff --git a/packages/effect-app/src/Schema/ext.ts b/packages/effect-app/src/Schema/ext.ts index 8ccdc4a648..2726c32b56 100644 --- a/packages/effect-app/src/Schema/ext.ts +++ b/packages/effect-app/src/Schema/ext.ts @@ -100,7 +100,7 @@ export const Literals = readonly id: string readonly moduleName: string + readonly type: "command" | "query" readonly "~decodingServices"?: unknown } diff --git a/packages/effect-app/src/client/makeClient.ts b/packages/effect-app/src/client/makeClient.ts index 586789590a..c1dd32f6a7 100644 --- a/packages/effect-app/src/client/makeClient.ts +++ b/packages/effect-app/src/client/makeClient.ts @@ -24,7 +24,8 @@ type TaggedRequestForResult< Success extends S.Top, Error extends S.Top, Config, - ModuleName extends string + ModuleName extends string, + Type extends "command" | "query" > = & S.EnhancedClass, {}> & { @@ -36,6 +37,7 @@ type TaggedRequestForResult< readonly "~encodingServices": S.Codec.EncodingServices | S.Codec.EncodingServices readonly id: `${ModuleName}.${Tag}` readonly moduleName: ModuleName + readonly type: Type } export const makeRpcClient = < @@ -84,7 +86,10 @@ export const makeRpcClient = < return RequestClass } - function TaggedRequestFor(moduleName: ModuleName) { + function makeTaggedRequestWithMeta( + moduleName: ModuleName, + type: Type + ) { function TaggedRequestWithMeta(): { ( tag: Tag, @@ -97,7 +102,8 @@ export const makeRpcClient = < SchemaOrFields, ErrorResult, Omit, - ModuleName + ModuleName, + Type > >( tag: Tag, @@ -110,7 +116,8 @@ export const makeRpcClient = < SchemaOrFields, ErrorResult, Omit, - ModuleName + ModuleName, + Type > >( tag: Tag, @@ -123,7 +130,8 @@ export const makeRpcClient = < typeof ForceVoid, ErrorResult, Omit, - ModuleName + ModuleName, + Type > >( tag: Tag, @@ -136,7 +144,8 @@ export const makeRpcClient = < typeof ForceVoid, ErrorResult, Omit, - ModuleName + ModuleName, + Type > ( tag: Tag, @@ -148,7 +157,8 @@ export const makeRpcClient = < typeof ForceVoid, ErrorResult<{}>, Record, - ModuleName + ModuleName, + Type > } { return (( @@ -157,11 +167,19 @@ export const makeRpcClient = < config?: C ) => { const cls = makeRequestClass(tag, fields, config) - Object.assign(cls, { id: `${moduleName}.${tag}`, moduleName }) + Object.assign(cls, { id: `${moduleName}.${tag}`, moduleName, type }) return cls }) as any } - return Object.assign(TaggedRequestWithMeta, { moduleName } as const) + return Object.assign(TaggedRequestWithMeta, { moduleName, type } as const) + } + + function TaggedRequestFor(moduleName: ModuleName) { + return { + moduleName, + Command: makeTaggedRequestWithMeta(moduleName, "command"), + Query: makeTaggedRequestWithMeta(moduleName, "query") + } as const } return { diff --git a/packages/effect-app/test/rpc.test.ts b/packages/effect-app/test/rpc.test.ts index c60529419a..bb5f75f199 100644 --- a/packages/effect-app/test/rpc.test.ts +++ b/packages/effect-app/test/rpc.test.ts @@ -11,7 +11,7 @@ export class RequestContextMap extends RpcContextMap.makeMap({ }) {} const { TaggedRequestFor } = makeRpcClient(RequestContextMap) -const TaggedRequest = TaggedRequestFor("Test") +const TaggedRequest = TaggedRequestFor("Test").Query export class Stats extends TaggedRequest()("Stats", {}, { allowedRoles: ["manager"], @@ -26,6 +26,7 @@ export class Stats extends TaggedRequest()("Stats", {}, { declare const _stats: typeof Stats.Type declare const _statsSuccess: typeof Stats.success.Type declare const _statsError: typeof Stats.error.Type +declare const _statsRequestType: typeof Stats.type test("ForceVoid decodes and encodes as void", () => { expect(S.decodeUnknownSync(ForceVoid)(undefined)).toBe(undefined) @@ -42,4 +43,5 @@ test("ForceVoid decodes and encodes as void", () => { readonly newUsersLastWeek: number }>() expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf<"query">() }) diff --git a/packages/infra/test/controller.test.ts b/packages/infra/test/controller.test.ts index 6116216557..fc9b7d47cf 100644 --- a/packages/infra/test/controller.test.ts +++ b/packages/infra/test/controller.test.ts @@ -204,13 +204,15 @@ export const middleware3 = MiddlewareMaker export const { TaggedRequestFor } = makeRpcClient(RequestContextMap) const Req = TaggedRequestFor("Something") +const Command = Req.Command +const Query = Req.Query -export class Eff extends Req()("Eff", {}, { success: S.Void }) {} -export class Gen extends Req()("Gen", {}) {} +export class Eff extends Command()("Eff", {}, { success: S.Void }) {} +export class Gen extends Command()("Gen", {}) {} expectTypeOf(Eff.error).toEqualTypeOf() -export class DoSomething extends Req()("DoSomething", { +export class DoSomething extends Command()("DoSomething", { id: S.String }, { success: S.Void }) {} @@ -228,11 +230,11 @@ export class DoSomething extends Req()("DoSomething", { // ) // ) -export class GetSomething extends Req()("GetSomething", { +export class GetSomething extends Query()("GetSomething", { id: S.String }, { success: S.String }) {} -export class GetSomething2 extends Req()("GetSomething2", { +export class GetSomething2 extends Query()("GetSomething2", { id: S.String }, { success: S.FiniteFromString }) {} diff --git a/packages/vue/src/makeClient.ts b/packages/vue/src/makeClient.ts index 0126ccbff6..6573c52973 100644 --- a/packages/vue/src/makeClient.ts +++ b/packages/vue/src/makeClient.ts @@ -69,6 +69,20 @@ export type RequestWithExtensions = Req extends : Req extends RequestHandler ? RequestExt : never +type QueryHandler = Req extends + RequestHandlerWithInput + ? Request["type"] extends "query" ? RequestHandlerWithInput : never + : Req extends RequestHandler + ? Request["type"] extends "query" ? RequestHandler : never + : never + +type CommandHandler = Req extends + RequestHandlerWithInput + ? Request["type"] extends "command" ? RequestHandlerWithInput : never + : Req extends RequestHandler + ? Request["type"] extends "command" ? RequestHandler : never + : never + export interface MutationExtensions { /** Defines a Command based on this mutation, taking the `id` of the mutation as the `id` of the Command. * The Mutation function will be taken as the first member of the Command, the Command required input will be the Mutation input. @@ -153,14 +167,16 @@ export type MissingDependencies = { export type Queries = Req extends RequestHandlerWithInput - ? Exclude extends never ? QueriesWithInput - : { - query: MissingDependencies & {} - suspense: MissingDependencies & {} - } + ? Request["type"] extends "query" ? Exclude extends never ? QueriesWithInput + : { + query: MissingDependencies & {} + suspense: MissingDependencies & {} + } + : never : Req extends RequestHandler - ? Exclude extends never ? QueriesWithoutInput - : { query: MissingDependencies & {}; suspense: MissingDependencies & {} } + ? Request["type"] extends "query" ? Exclude extends never ? QueriesWithoutInput + : { query: MissingDependencies & {}; suspense: MissingDependencies & {} } + : never : never const _useMutation = makeMutation() @@ -377,6 +393,9 @@ export const makeClient = ( ) => { const queries = Struct.keys(client).reduce( (acc, key) => { + if (client[key].Request.type !== "query") { + return acc + } ;(acc as any)[camelCase(key) + "Query"] = Object.assign(useQuery(client[key] as any), { id: client[key].id }) @@ -388,15 +407,17 @@ export const makeClient = ( {} as & { // apparently can't get JSDoc in here.. - [Key in keyof typeof client as `${ToCamel}Query`]: Queries["query"] + [Key in keyof typeof client as QueryHandler extends never ? never + : `${ToCamel}Query`]: Queries>["query"] } // todo: or suspense as an Option? & { // apparently can't get JSDoc in here.. - [Key in keyof typeof client as `${ToCamel}SuspenseQuery`]: Queries< - RT, - typeof client[Key] - >["suspense"] + [Key in keyof typeof client as QueryHandler extends never ? never + : `${ToCamel}SuspenseQuery`]: Queries< + RT, + QueryHandler + >["suspense"] } ) return queries @@ -435,16 +456,20 @@ export const makeClient = ( const mutation = useMutation() const mutations = Struct.keys(client).reduce( (acc, key) => { + if (client[key].Request.type !== "command") { + return acc + } const mut: any = mutation(client[key] as any) const wrap = Command.wrap({ mutate: Effect.isEffect(mut) ? () => mut : mut, id: client[key].id }) ;(acc as any)[camelCase(key) + "Mutation"] = Object.assign(mut, { wrap }) return acc }, {} as { - [Key in keyof typeof client as `${ToCamel}Mutation`]: MutationWithExtensions< - RT | RTHooks, - typeof client[Key] - > + [Key in keyof typeof client as CommandHandler extends never ? never + : `${ToCamel}Mutation`]: MutationWithExtensions< + RT | RTHooks, + CommandHandler + > } ) return mutations @@ -464,49 +489,53 @@ export const makeClient = ( const extended = Struct.keys(client).reduce( (acc, key) => { const fn = Command.fn(client[key].id) - const mutate = extendM( - mutation( - client[key] as any, - invalidation?.[key] ? { queryInvalidation: invalidation[key] } : undefined - ), - (mutate) => - Object.assign( - mutate, - { - wrap: Command.wrap({ mutate: Effect.isEffect(mutate) ? () => mutate : mutate, id: client[key].id }) - } - ) - ) - const h_ = client[key].handler const wrapInput = Effect.isEffect(h_) ? () => h_ : (...args: [any]) => h_(...args) const fetch = Effect.isEffect(h_) ? h_ : wrapInput ;(acc as any)[key] = Object.assign( - {}, - client[key], - fn, // to get the i18n key etc. - { - fetch, - mutate, - query: useQuery(client[key] as any), - suspense: useSuspenseQuery(client[key] as any), - wrap: Command.wrap({ mutate: wrapInput, id: client[key].id }), - fn - } - ) + client[key].Request.type === "query" + ? { + ...client[key], + ...fn, // to get the i18n key etc. + fetch, + query: useQuery(client[key] as any), + suspense: useSuspenseQuery(client[key] as any), + wrap: Command.wrap({ mutate: wrapInput, id: client[key].id }), + fn + } + : { + mutate: extendM( + mutation( + client[key] as any, + invalidation?.[key] ? { queryInvalidation: invalidation[key] } : undefined + ), + (mutate) => + Object.assign( + mutate, + { + wrap: Command.wrap({ mutate: Effect.isEffect(mutate) ? () => mutate : mutate, id: client[key].id }) + } + ) + ), + ...client[key], + ...fn, // to get the i18n key etc. + fetch, + wrap: Command.wrap({ mutate: wrapInput, id: client[key].id }), + fn + } + ) as any return acc }, {} as { [Key in keyof typeof client]: & typeof client[Key] & RequestWithExtensions - & { - mutate: MutationWithExtensions - Input: typeof client[Key] extends RequestHandlerWithInput ? I : never - } - & Queries + & (QueryHandler extends never ? {} : Queries>) + & (CommandHandler extends never ? {} + : { mutate: MutationWithExtensions> }) + & { Input: typeof client[Key] extends RequestHandlerWithInput ? I : never } } ) return Object.assign(extended, { helpers: { ...mapRequest(client), ...mapMutation(client), ...mapQuery(client) } }) diff --git a/packages/vue/src/query.ts b/packages/vue/src/query.ts index 58d5544c44..9829953a89 100644 --- a/packages/vue/src/query.ts +++ b/packages/vue/src/query.ts @@ -133,7 +133,7 @@ export const makeQuery = (getRuntime: () => Context.Context) => { get value() { return (arr as any)() } - }) + } as any) : ref(arg) const queryKey = makeQueryKey(q) const handler = q.handler diff --git a/packages/vue/test/makeClient.test.ts b/packages/vue/test/makeClient.test.ts index b4034f4b27..03714a86b5 100644 --- a/packages/vue/test/makeClient.test.ts +++ b/packages/vue/test/makeClient.test.ts @@ -9,6 +9,8 @@ it("TaggedRequestFor .moduleName and request .id / .moduleName", () => { expectTypeOf(Something.GetSomething2.moduleName).toEqualTypeOf<"Something">() expectTypeOf(Something.GetSomething2.id).toEqualTypeOf<"Something.GetSomething2">() + expectTypeOf(Something.GetSomething2.type).toEqualTypeOf<"query">() + expectTypeOf(Something.DoSomething.type).toEqualTypeOf<"command">() expectTypeOf(SomethingElse.GetSomething2.moduleName).toEqualTypeOf<"SomethingElse">() expectTypeOf(SomethingElse.GetSomething2.id).toEqualTypeOf<"SomethingElse.GetSomething2">() @@ -64,7 +66,7 @@ it.skip("works", () => { // just for jsdoc / type testing. const a0 = client.GetSomething2.fetch(null as any) - const a00 = client.GetSomething2.mutate(null as any) + const a00 = client.DoSomething.mutate(null as any) const a = client.GetSomething2.suspense(null as any) const b = client.GetSomething2.query(null as any) @@ -86,9 +88,9 @@ it.skip("works", () => { const e2 = client.GetSomething2WithDependencies.query(null as any) const f0 = client.GetSomething2WithDependencies.fn(null as any) - const g = client.GetSomething2.mutate.wrap(null as any) - // @ts-expect-error mutate no longer exposes fn, use client.GetSomething2.fn - const h = client.GetSomething2.mutate.fn(null as any) + const g = client.DoSomething.mutate.wrap(null as any) + // @ts-expect-error mutate no longer exposes fn, use client.DoSomething.fn + const h = client.DoSomething.mutate.fn(null as any) expect(true).toBe(true) console.log({ diff --git a/packages/vue/test/stubs.ts b/packages/vue/test/stubs.ts index be739fd614..166664ec2b 100644 --- a/packages/vue/test/stubs.ts +++ b/packages/vue/test/stubs.ts @@ -95,13 +95,15 @@ export class RequestContextMap extends RpcContextMap.makeMap({}) {} export const { TaggedRequestFor } = makeRpcClient(RequestContextMap) export const SomethingReq = TaggedRequestFor("Something") +const SomethingQuery = SomethingReq.Query +const SomethingCommand = SomethingReq.Command -class SomethingGetSomething2 extends SomethingReq()("GetSomething2", { +class SomethingGetSomething2 extends SomethingQuery()("GetSomething2", { id: S.String }, { success: S.FiniteFromString }) {} class SomethingGetSomething2WithDependencies - extends SomethingReq()("GetSomething2", { + extends SomethingQuery()("GetSomething2", { id: S.String }, { // this is intentilally fake, to simulate a codec that requires a dependency @@ -110,19 +112,25 @@ class SomethingGetSomething2WithDependencies }) {} +class SomethingDoSomething extends SomethingCommand()("DoSomething", { + id: S.String +}, { success: S.FiniteFromString }) {} + export const Something = { GetSomething2: SomethingGetSomething2, - GetSomething2WithDependencies: SomethingGetSomething2WithDependencies + GetSomething2WithDependencies: SomethingGetSomething2WithDependencies, + DoSomething: SomethingDoSomething } export const SomethingElseReq = TaggedRequestFor("SomethingElse") +const SomethingElseQuery = SomethingElseReq.Query -class SomethingElseGetSomething2 extends SomethingElseReq()("GetSomething2", { +class SomethingElseGetSomething2 extends SomethingElseQuery()("GetSomething2", { id: S.String }, { success: S.FiniteFromString }) {} class SomethingElseGetSomething2WithDependencies - extends SomethingElseReq()("GetSomething2", { + extends SomethingElseQuery()("GetSomething2", { id: S.String }, { success: S.FiniteFromString as S.Codec, From 463ad61adb0b66db788c85118edcbc9c4f1f5a61 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 17:54:53 +0000 Subject: [PATCH 03/10] fix: refine vue helper typing and tests for query vs command requests Agent-Logs-Url: https://github.com/effect-app/libs/sessions/207b39e6-c3a8-4822-b7ad-31589e0a984e Co-authored-by: patroza <42661+patroza@users.noreply.github.com> --- packages/vue/src/makeClient.ts | 46 ++++++++++++++++------------ packages/vue/test/makeClient.test.ts | 1 + 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/packages/vue/src/makeClient.ts b/packages/vue/src/makeClient.ts index 6573c52973..32bb85de41 100644 --- a/packages/vue/src/makeClient.ts +++ b/packages/vue/src/makeClient.ts @@ -407,17 +407,21 @@ export const makeClient = ( {} as & { // apparently can't get JSDoc in here.. - [Key in keyof typeof client as QueryHandler extends never ? never - : `${ToCamel}Query`]: Queries>["query"] + [ + Key in keyof typeof client as QueryHandler extends never ? never + : `${ToCamel}Query` + ]: Queries>["query"] } // todo: or suspense as an Option? & { // apparently can't get JSDoc in here.. - [Key in keyof typeof client as QueryHandler extends never ? never - : `${ToCamel}SuspenseQuery`]: Queries< - RT, - QueryHandler - >["suspense"] + [ + Key in keyof typeof client as QueryHandler extends never ? never + : `${ToCamel}SuspenseQuery` + ]: Queries< + RT, + QueryHandler + >["suspense"] } ) return queries @@ -465,11 +469,13 @@ export const makeClient = ( return acc }, {} as { - [Key in keyof typeof client as CommandHandler extends never ? never - : `${ToCamel}Mutation`]: MutationWithExtensions< - RT | RTHooks, - CommandHandler - > + [ + Key in keyof typeof client as CommandHandler extends never ? never + : `${ToCamel}Mutation` + ]: MutationWithExtensions< + RT | RTHooks, + CommandHandler + > } ) return mutations @@ -488,6 +494,7 @@ export const makeClient = ( const invalidation = queryInvalidation?.(client) const extended = Struct.keys(client).reduce( (acc, key) => { + const requestType = client[key].Request.type const fn = Command.fn(client[key].id) const h_ = client[key].handler const wrapInput = Effect.isEffect(h_) @@ -495,15 +502,14 @@ export const makeClient = ( : (...args: [any]) => h_(...args) const fetch = Effect.isEffect(h_) ? h_ : wrapInput ;(acc as any)[key] = Object.assign( - client[key].Request.type === "query" + requestType === "query" ? { ...client[key], ...fn, // to get the i18n key etc. fetch, query: useQuery(client[key] as any), suspense: useSuspenseQuery(client[key] as any), - wrap: Command.wrap({ mutate: wrapInput, id: client[key].id }), - fn + wrap: Command.wrap({ mutate: wrapInput, id: client[key].id }) } : { mutate: extendM( @@ -515,17 +521,19 @@ export const makeClient = ( Object.assign( mutate, { - wrap: Command.wrap({ mutate: Effect.isEffect(mutate) ? () => mutate : mutate, id: client[key].id }) + wrap: Command.wrap({ + mutate: Effect.isEffect(mutate) ? () => mutate : mutate, + id: client[key].id + }) } ) ), ...client[key], ...fn, // to get the i18n key etc. fetch, - wrap: Command.wrap({ mutate: wrapInput, id: client[key].id }), - fn + wrap: Command.wrap({ mutate: wrapInput, id: client[key].id }) } - ) as any + ) return acc }, {} as { diff --git a/packages/vue/test/makeClient.test.ts b/packages/vue/test/makeClient.test.ts index 03714a86b5..617fefa7ef 100644 --- a/packages/vue/test/makeClient.test.ts +++ b/packages/vue/test/makeClient.test.ts @@ -81,6 +81,7 @@ it.skip("works", () => { null as any ) const e0000 = + // @ts-expect-error query request does not match Command.wrap mutation signature Command.wrap(client.GetSomething2WithDependencies)((_) => _ as Effect.Effect).handle // @ts-expect-error dependencies required that are not provided const e1 = client.GetSomething2WithDependencies.suspense(null as any) From a6e39b09489306938d0321710ed70ba3f2a0ace1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 18:04:35 +0000 Subject: [PATCH 04/10] fix: remove query command helpers and gate request helpers to commands Agent-Logs-Url: https://github.com/effect-app/libs/sessions/a83dabc4-7697-47f9-b57a-cff535a0200d Co-authored-by: patroza <42661+patroza@users.noreply.github.com> --- packages/vue/src/makeClient.ts | 44 +++++++++++++++++++++------- packages/vue/test/makeClient.test.ts | 8 +++-- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/packages/vue/src/makeClient.ts b/packages/vue/src/makeClient.ts index 32bb85de41..9175bbbeee 100644 --- a/packages/vue/src/makeClient.ts +++ b/packages/vue/src/makeClient.ts @@ -21,7 +21,7 @@ const mapHandler = ( map: (self: Effect.Effect, i: I) => Effect.Effect ) => Effect.isEffect(handler) ? map(handler, undefined as any) : (i: I) => map(handler(i), i) -export interface RequestExtensions { +export interface CommandRequestExtensions { /** Defines a Command based on this call, taking the `id` of the call as the `id` of the Command. * The Request function will be taken as the first member of the Command, the Command required input will be the Request input. * see Command.wrap for details */ @@ -39,7 +39,7 @@ export interface RequestExtWithInput< A, E, R -> extends Commander.CommandContextLocal, RequestExtensions { +> extends Commander.CommandContextLocal, CommandRequestExtensions { /** * Request the endpoint with input */ @@ -55,7 +55,7 @@ export interface RequestExt< > extends Commander.CommandContextLocal, Commander.CommanderWrap, - RequestExtensions + CommandRequestExtensions { /** * Request the endpoint @@ -63,12 +63,26 @@ export interface RequestExt< fetch: Effect.Effect } -export type RequestWithExtensions = Req extends +export type CommandRequestWithExtensions = Req extends RequestHandlerWithInput ? RequestExtWithInput : Req extends RequestHandler ? RequestExt : never +export interface QueryExtensionsWithInput { + fetch: (i: I) => Effect.Effect +} + +export interface QueryExtensions { + fetch: Effect.Effect +} + +export type QueryRequestWithExtensions = Req extends + RequestHandlerWithInput + ? QueryExtensionsWithInput + : Req extends RequestHandler ? QueryExtensions + : never + type QueryHandler = Req extends RequestHandlerWithInput ? Request["type"] extends "query" ? RequestHandlerWithInput : never @@ -433,6 +447,9 @@ export const makeClient = ( const Command = useCommand() const mutations = Struct.keys(client).reduce( (acc, key) => { + if (client[key].Request.type !== "command") { + return acc + } const mut = client[key].handler const fn = Command.fn(client[key].id) const wrap = Command.wrap({ mutate: Effect.isEffect(mut) ? () => mut : mut, id: client[key].id }) @@ -444,9 +461,12 @@ export const makeClient = ( return acc }, {} as { - [Key in keyof typeof client as `${ToCamel}Request`]: RequestWithExtensions< + [ + Key in keyof typeof client as CommandHandler extends never ? never + : `${ToCamel}Request` + ]: CommandRequestWithExtensions< RT | RTHooks, - typeof client[Key] + CommandHandler > } ) @@ -505,11 +525,9 @@ export const makeClient = ( requestType === "query" ? { ...client[key], - ...fn, // to get the i18n key etc. fetch, query: useQuery(client[key] as any), - suspense: useSuspenseQuery(client[key] as any), - wrap: Command.wrap({ mutate: wrapInput, id: client[key].id }) + suspense: useSuspenseQuery(client[key] as any) } : { mutate: extendM( @@ -539,8 +557,12 @@ export const makeClient = ( {} as { [Key in keyof typeof client]: & typeof client[Key] - & RequestWithExtensions - & (QueryHandler extends never ? {} : Queries>) + & (QueryHandler extends never ? {} + : + & QueryRequestWithExtensions> + & Queries>) + & (CommandHandler extends never ? {} + : CommandRequestWithExtensions>) & (CommandHandler extends never ? {} : { mutate: MutationWithExtensions> }) & { Input: typeof client[Key] extends RequestHandlerWithInput ? I : never } diff --git a/packages/vue/test/makeClient.test.ts b/packages/vue/test/makeClient.test.ts index 617fefa7ef..a54c567374 100644 --- a/packages/vue/test/makeClient.test.ts +++ b/packages/vue/test/makeClient.test.ts @@ -70,13 +70,16 @@ it.skip("works", () => { const a = client.GetSomething2.suspense(null as any) const b = client.GetSomething2.query(null as any) + // @ts-expect-error query requests no longer expose command helpers const e = client.GetSomething2.wrap(null as any) + // @ts-expect-error query requests no longer expose command helpers const f = client.GetSomething2.fn(null as any) - // @ts-expect-error dependencies required that are not provided + // @ts-expect-error query requests no longer expose command helpers const e0 = client.GetSomething2WithDependencies.wrap().handle // not available as we require dependencies not provided by the runtime - // @ts-expect-error dependencies required that are not provided + // @ts-expect-error query requests no longer match Command.wrap mutation signature const e000 = Command.wrap(client.GetSomething2WithDependencies)().handle // not available as we require dependencies not provided by the runtime + // @ts-expect-error query requests no longer expose command helpers const e00 = client.GetSomething2WithDependencies.wrap((_) => _ as Effect.Effect).handle( null as any ) @@ -87,6 +90,7 @@ it.skip("works", () => { const e1 = client.GetSomething2WithDependencies.suspense(null as any) // @ts-expect-error dependencies required that are not provided const e2 = client.GetSomething2WithDependencies.query(null as any) + // @ts-expect-error query requests no longer expose command helpers const f0 = client.GetSomething2WithDependencies.fn(null as any) const g = client.DoSomething.mutate.wrap(null as any) From fd01992b8a0b2ac4f9acc503835c7e3ee93b07cf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 18:09:24 +0000 Subject: [PATCH 05/10] fix: rename vue request helper from fetch to request Agent-Logs-Url: https://github.com/effect-app/libs/sessions/94200174-a762-4f4f-9694-fcde39079683 Co-authored-by: patroza <42661+patroza@users.noreply.github.com> --- packages/vue/CHANGELOG.md | 4 ++-- packages/vue/src/makeClient.ts | 14 +++++++------- packages/vue/test/makeClient.test.ts | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/vue/CHANGELOG.md b/packages/vue/CHANGELOG.md index 4c1149bc65..745250fadf 100644 --- a/packages/vue/CHANGELOG.md +++ b/packages/vue/CHANGELOG.md @@ -4,9 +4,9 @@ ### Minor Changes -- 6a3d364: Client entries are now plain objects; use `.fetch` to invoke the request. +- 6a3d364: Client entries are now plain objects; use `.request` to invoke the request. - `client.Xxx` no longer is callable or an `Effect` itself. Call `client.Xxx.fetch(input)` (or `client.Xxx.fetch` for input-less requests) instead. `.mutate`, `.query`, `.suspense`, `.wrap`, and `.fn` are unchanged. + `client.Xxx` no longer is callable or an `Effect` itself. Call `client.Xxx.request(input)` (or `client.Xxx.request` for input-less requests) instead. `.mutate`, `.query`, `.suspense`, `.wrap`, and `.fn` are unchanged. ### Patch Changes diff --git a/packages/vue/src/makeClient.ts b/packages/vue/src/makeClient.ts index 9175bbbeee..530ebd6474 100644 --- a/packages/vue/src/makeClient.ts +++ b/packages/vue/src/makeClient.ts @@ -43,7 +43,7 @@ export interface RequestExtWithInput< /** * Request the endpoint with input */ - fetch: (i: I) => Effect.Effect + request: (i: I) => Effect.Effect } export interface RequestExt< @@ -60,7 +60,7 @@ export interface RequestExt< /** * Request the endpoint */ - fetch: Effect.Effect + request: Effect.Effect } export type CommandRequestWithExtensions = Req extends @@ -70,11 +70,11 @@ export type CommandRequestWithExtensions = Req extends : never export interface QueryExtensionsWithInput { - fetch: (i: I) => Effect.Effect + request: (i: I) => Effect.Effect } export interface QueryExtensions { - fetch: Effect.Effect + request: Effect.Effect } export type QueryRequestWithExtensions = Req extends @@ -520,12 +520,12 @@ export const makeClient = ( const wrapInput = Effect.isEffect(h_) ? () => h_ : (...args: [any]) => h_(...args) - const fetch = Effect.isEffect(h_) ? h_ : wrapInput + const request = Effect.isEffect(h_) ? h_ : wrapInput ;(acc as any)[key] = Object.assign( requestType === "query" ? { ...client[key], - fetch, + request, query: useQuery(client[key] as any), suspense: useSuspenseQuery(client[key] as any) } @@ -548,7 +548,7 @@ export const makeClient = ( ), ...client[key], ...fn, // to get the i18n key etc. - fetch, + request, wrap: Command.wrap({ mutate: wrapInput, id: client[key].id }) } ) diff --git a/packages/vue/test/makeClient.test.ts b/packages/vue/test/makeClient.test.ts index a54c567374..83d946c579 100644 --- a/packages/vue/test/makeClient.test.ts +++ b/packages/vue/test/makeClient.test.ts @@ -65,7 +65,7 @@ it.skip("works", () => { const Command = useExperimental() // just for jsdoc / type testing. - const a0 = client.GetSomething2.fetch(null as any) + const a0 = client.GetSomething2.request(null as any) const a00 = client.DoSomething.mutate(null as any) const a = client.GetSomething2.suspense(null as any) const b = client.GetSomething2.query(null as any) From c059b818b127c74f34860d875ecdc463a209ab47 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 18:13:59 +0000 Subject: [PATCH 06/10] test: refine makeClient command wrap type checks Agent-Logs-Url: https://github.com/effect-app/libs/sessions/0f13e328-67ff-4d47-a725-8bd28ad338d4 Co-authored-by: patroza <42661+patroza@users.noreply.github.com> --- packages/vue/test/makeClient.test.ts | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/packages/vue/test/makeClient.test.ts b/packages/vue/test/makeClient.test.ts index 83d946c579..dd7d5af083 100644 --- a/packages/vue/test/makeClient.test.ts +++ b/packages/vue/test/makeClient.test.ts @@ -1,5 +1,4 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { type Effect } from "effect-app" import { makeQueryKey } from "../src/lib.js" import { Something, SomethingElse, SomethingElseReq, SomethingReq, useClient, useExperimental } from "./stubs.js" @@ -76,16 +75,9 @@ it.skip("works", () => { const f = client.GetSomething2.fn(null as any) // @ts-expect-error query requests no longer expose command helpers - const e0 = client.GetSomething2WithDependencies.wrap().handle // not available as we require dependencies not provided by the runtime - // @ts-expect-error query requests no longer match Command.wrap mutation signature - const e000 = Command.wrap(client.GetSomething2WithDependencies)().handle // not available as we require dependencies not provided by the runtime - // @ts-expect-error query requests no longer expose command helpers - const e00 = client.GetSomething2WithDependencies.wrap((_) => _ as Effect.Effect).handle( - null as any - ) - const e0000 = - // @ts-expect-error query request does not match Command.wrap mutation signature - Command.wrap(client.GetSomething2WithDependencies)((_) => _ as Effect.Effect).handle + const e0 = client.GetSomething2WithDependencies.wrap + // @ts-expect-error query request does not match Command.wrap mutation signature + const e000 = Command.wrap(client.GetSomething2WithDependencies) // @ts-expect-error dependencies required that are not provided const e1 = client.GetSomething2WithDependencies.suspense(null as any) // @ts-expect-error dependencies required that are not provided @@ -93,6 +85,7 @@ it.skip("works", () => { // @ts-expect-error query requests no longer expose command helpers const f0 = client.GetSomething2WithDependencies.fn(null as any) + const g0 = client.DoSomething.wrap(null as any) const g = client.DoSomething.mutate.wrap(null as any) // @ts-expect-error mutate no longer exposes fn, use client.DoSomething.fn const h = client.DoSomething.mutate.fn(null as any) @@ -105,13 +98,12 @@ it.skip("works", () => { b, e, e0, - e00, e000, - e0000, e1, e2, f, f0, + g0, g, h }) From 312cdf1b115db8d3f894380b5f56e18e19bf5c72 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 18:20:44 +0000 Subject: [PATCH 07/10] docs: add request-kind and client helper JSDoc Agent-Logs-Url: https://github.com/effect-app/libs/sessions/4258e834-6915-4825-a2b0-a32258f1510e Co-authored-by: patroza <42661+patroza@users.noreply.github.com> --- packages/effect-app/src/client/makeClient.ts | 15 +++++- packages/vue/src/makeClient.ts | 48 +++++++++++++------- 2 files changed, 45 insertions(+), 18 deletions(-) diff --git a/packages/effect-app/src/client/makeClient.ts b/packages/effect-app/src/client/makeClient.ts index c1dd32f6a7..36b39ba540 100644 --- a/packages/effect-app/src/client/makeClient.ts +++ b/packages/effect-app/src/client/makeClient.ts @@ -175,10 +175,21 @@ export const makeRpcClient = < } function TaggedRequestFor(moduleName: ModuleName) { + const Query = makeTaggedRequestWithMeta(moduleName, "query") + const Command = makeTaggedRequestWithMeta(moduleName, "command") + return { moduleName, - Command: makeTaggedRequestWithMeta(moduleName, "command"), - Query: makeTaggedRequestWithMeta(moduleName, "query") + /** + * Create query request classes for this module. + * Queries read state and should not mutate server state. + */ + Query, + /** + * Create command request classes for this module. + * Commands mutate state and should avoid returning complex read models. + */ + Command } as const } diff --git a/packages/vue/src/makeClient.ts b/packages/vue/src/makeClient.ts index 530ebd6474..414d75b7ef 100644 --- a/packages/vue/src/makeClient.ts +++ b/packages/vue/src/makeClient.ts @@ -41,7 +41,8 @@ export interface RequestExtWithInput< R > extends Commander.CommandContextLocal, CommandRequestExtensions { /** - * Request the endpoint with input + * Send the request to the endpoint and return the raw Effect response. + * This does not perform query cache invalidation. */ request: (i: I) => Effect.Effect } @@ -58,7 +59,8 @@ export interface RequestExt< CommandRequestExtensions { /** - * Request the endpoint + * Send the request to the endpoint and return the raw Effect response. + * This does not perform query cache invalidation. */ request: Effect.Effect } @@ -70,10 +72,18 @@ export type CommandRequestWithExtensions = Req extends : never export interface QueryExtensionsWithInput { + /** + * Send the request to the endpoint and return the raw Effect response. + * This does not set up query state tracking. + */ request: (i: I) => Effect.Effect } export interface QueryExtensions { + /** + * Send the request to the endpoint and return the raw Effect response. + * This does not set up query state tracking. + */ request: Effect.Effect } @@ -114,17 +124,21 @@ export interface MutationExtWithInput< R > extends MutationExtensions { /** - * Call the endpoint with input - * Invalidate queries based on namespace of this mutation. - * Do not use for queries. + * Send the request to the endpoint and return the raw Effect response. + * Also invalidates query caches using the request namespace by default. + * Namespace invalidation targets parent namespace keys + * (for example `$project/$configuration.get` invalidates `$project`). + * Override invalidation in client options via `queryInvalidation`. */ (i: I): Effect.Effect } /** - * Call the endpoint - * Invalidate queries based on namespace of this mutation. - * Do not use for queries. + * Send the request to the endpoint and return the raw Effect response. + * Also invalidates query caches using the request namespace by default. + * Namespace invalidation targets parent namespace keys + * (for example `$project/$configuration.get` invalidates `$project`). + * Override invalidation in client options via `queryInvalidation`. */ export interface MutationExt< RT, @@ -149,27 +163,29 @@ declare const useSuspenseQuery_: QueryImpl["useSuspenseQuery"] export interface QueriesWithInput { /** - * Effect results are passed to the caller, including errors. + * Read helper for query requests. + * Runs as a tracked Vue Query and returns reactive state. + * Queries read state and should not be used to mutate it. */ query: ReturnType> // TODO or suspense as Option? /** - * The difference with useQuery is that this function will return a Promise you can await in the Setup, - * which ensures that either there always is a latest value, or an error occurs on load. - * So that Suspense and error boundaries can be used. + * Like `.query`, but returns a Promise for setup-time awaiting. + * Use this when integrating with Vue Suspense / error boundaries. */ suspense: ReturnType> } export interface QueriesWithoutInput { /** - * Effect results are passed to the caller, including errors. + * Read helper for query requests. + * Runs as a tracked Vue Query and returns reactive state. + * Queries read state and should not be used to mutate it. */ query: ReturnType> // TODO or suspense as Option? /** - * The difference with useQuery is that this function will return a Promise you can await in the Setup, - * which ensures that either there always is a latest value, or an error occurs on load. - * So that Suspense and error boundaries can be used. + * Like `.query`, but returns a Promise for setup-time awaiting. + * Use this when integrating with Vue Suspense / error boundaries. */ suspense: ReturnType> } From c3034761e9dde629a81b2cf299ce4e5ee306dfc8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 18:25:48 +0000 Subject: [PATCH 08/10] refactor: reshape MutationExtWithInput type alias Agent-Logs-Url: https://github.com/effect-app/libs/sessions/df606d86-8d4f-4d9b-9c8e-3af0b7dfbfc8 Co-authored-by: patroza <42661+patroza@users.noreply.github.com> --- packages/vue/src/makeClient.ts | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/vue/src/makeClient.ts b/packages/vue/src/makeClient.ts index 414d75b7ef..dff2170a97 100644 --- a/packages/vue/src/makeClient.ts +++ b/packages/vue/src/makeClient.ts @@ -115,23 +115,25 @@ export interface MutationExtensions { } /** my other doc */ -export interface MutationExtWithInput< +export type MutationExtWithInput< RT, Id extends string, I, A, E, R -> extends MutationExtensions { - /** - * Send the request to the endpoint and return the raw Effect response. - * Also invalidates query caches using the request namespace by default. - * Namespace invalidation targets parent namespace keys - * (for example `$project/$configuration.get` invalidates `$project`). - * Override invalidation in client options via `queryInvalidation`. - */ - (i: I): Effect.Effect -} +> = + & MutationExtensions + & { + /** + * Send the request to the endpoint and return the raw Effect response. + * Also invalidates query caches using the request namespace by default. + * Namespace invalidation targets parent namespace keys + * (for example `$project/$configuration.get` invalidates `$project`). + * Override invalidation in client options via `queryInvalidation`. + */ + (i: I): Effect.Effect + } /** * Send the request to the endpoint and return the raw Effect response. From a2099264d1d59cb4ebaa713ac3439dab7d893097 Mon Sep 17 00:00:00 2001 From: Patrick Roza Date: Tue, 21 Apr 2026 20:27:18 +0200 Subject: [PATCH 09/10] Revert "refactor: reshape MutationExtWithInput type alias" This reverts commit c3034761e9dde629a81b2cf299ce4e5ee306dfc8. --- packages/vue/src/makeClient.ts | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/vue/src/makeClient.ts b/packages/vue/src/makeClient.ts index dff2170a97..414d75b7ef 100644 --- a/packages/vue/src/makeClient.ts +++ b/packages/vue/src/makeClient.ts @@ -115,25 +115,23 @@ export interface MutationExtensions { } /** my other doc */ -export type MutationExtWithInput< +export interface MutationExtWithInput< RT, Id extends string, I, A, E, R -> = - & MutationExtensions - & { - /** - * Send the request to the endpoint and return the raw Effect response. - * Also invalidates query caches using the request namespace by default. - * Namespace invalidation targets parent namespace keys - * (for example `$project/$configuration.get` invalidates `$project`). - * Override invalidation in client options via `queryInvalidation`. - */ - (i: I): Effect.Effect - } +> extends MutationExtensions { + /** + * Send the request to the endpoint and return the raw Effect response. + * Also invalidates query caches using the request namespace by default. + * Namespace invalidation targets parent namespace keys + * (for example `$project/$configuration.get` invalidates `$project`). + * Override invalidation in client options via `queryInvalidation`. + */ + (i: I): Effect.Effect +} /** * Send the request to the endpoint and return the raw Effect response. From 40902fc5950617caa4ad57d8d3ab27e46fb70480 Mon Sep 17 00:00:00 2001 From: Patrick Roza Date: Tue, 21 Apr 2026 20:30:00 +0200 Subject: [PATCH 10/10] ffs --- packages/vue/src/query.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vue/src/query.ts b/packages/vue/src/query.ts index 9829953a89..e4e97cc976 100644 --- a/packages/vue/src/query.ts +++ b/packages/vue/src/query.ts @@ -127,13 +127,13 @@ export const makeQuery = (getRuntime: () => Context.Context) => { const runPromise = makeRunPromise(getRuntime()) const arr = arg const req: { value: I } = !arg - ? undefined + ? undefined as any : typeof arr === "function" ? ({ get value() { return (arr as any)() } - } as any) + }) : ref(arg) const queryKey = makeQueryKey(q) const handler = q.handler