@effect-app/vue 4.0.0-beta.272 → 4.0.0-beta.274

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/makeClient.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { isCancelledError, type QueryObserverResult, type RefetchOptions, type UseQueryReturnType } from "@tanstack/vue-query"
3
2
  import { camelCase } from "change-case"
4
3
  import { type ApiClientFactory, type Req } from "effect-app/client"
5
4
  import type { ExtractModuleName, HandlerInput, RequestHandlers, RequestHandlerWithInput, RequestsAny, RequestStreamHandlerWithInput } from "effect-app/client/clientFor"
5
+ import type { CauseException } from "effect-app/client/errors"
6
6
  import type { InvalidationCallback } from "effect-app/client/makeClient"
7
- import type * as Context from "effect-app/Context"
7
+ import * as Context from "effect-app/Context"
8
8
  import * as Effect from "effect-app/Effect"
9
- import type * as Layer from "effect-app/Layer"
9
+ import * as Layer from "effect-app/Layer"
10
10
  import * as S from "effect-app/Schema"
11
11
  import * as Exit from "effect/Exit"
12
12
  import { type Fiber } from "effect/Fiber"
@@ -14,28 +14,37 @@ import * as Hash from "effect/Hash"
14
14
  import type * as ManagedRuntime from "effect/ManagedRuntime"
15
15
  import type * as Stream from "effect/Stream"
16
16
  import * as Struct from "effect/Struct"
17
- import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
18
- import { type ComputedRef, onBeforeUnmount, ref, type WatchSource } from "vue"
17
+ import type * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
18
+ import * as Reactivity from "effect/unstable/reactivity/Reactivity"
19
+ import { computed, type ComputedRef, onBeforeUnmount, ref, type WatchSource } from "vue"
20
+ import { type AtomClientRuntime, invalidateAndAwait, makeAtomClientRuntime } from "./atomQuery.ts"
19
21
  import { type Commander, CommanderStatic, type Progress } from "./commander.ts"
22
+ import { makeTanstackQuery, makeTanstackQueryCacheUpdater, makeTanstackQueryClient, makeTanstackQueryInvalidator } from "./internal/tanstackQuery.ts"
20
23
  import { type I18n } from "./intl.ts"
21
24
  import { type CommanderResolved, makeUseCommand } from "./makeUseCommand.ts"
22
- import { type InvalidationEntry, makeMutation, makeStreamMutation2, type MutationOptionsBase, useMakeMutation } from "./mutate.ts"
23
- import { type CustomUndefinedInitialQueryOptions, makeQuery, makeStreamQuery } from "./query.ts"
25
+ import { atomQueryInvalidator, combineQueryInvalidators, type InvalidationEntry, makeMutation, makeStreamMutation2, type MutationOptionsBase, type QueryInvalidator, useMakeMutation } from "./mutate.ts"
26
+ import { atomQueryCacheUpdater, type AtomQueryNewOptions, combineQueryCacheUpdaters, type CustomUndefinedInitialQueryOptions, makeQuery, makeQueryAtom, makeQueryFamily, makeQueryNew, makeStreamQuery, makeStreamQueryAtom, makeStreamQueryFamily, makeStreamQueryNew, optionalAtomQueryCacheUpdater, type QueryObserverResult, type RefetchOptions, setQueryCacheUpdater, type StreamQueryAtomFamily, type SuspenseQueryView, type UseQueryReturnType } from "./query.ts"
24
27
  import { makeRunPromise } from "./runtime.ts"
25
- import { awaitResolvedSuspenseResult } from "./suspense.ts"
26
28
  import { type Toast } from "./toast.ts"
27
29
 
28
30
  export type { Progress }
29
31
 
30
32
  // TODO: optimize - work from encoded shape directly
31
- const projectHandler = (
32
- handler: (i: any) => Effect.Effect<any, any, any>,
33
- successSchema: S.Top,
34
- projectionSchema: S.Top
33
+ const projectHandler = <
34
+ I,
35
+ A,
36
+ E,
37
+ R,
38
+ SuccessSchema extends S.Top & { readonly "EncodingServices": R },
39
+ ProjSchema extends S.Top & { readonly "DecodingServices": R }
40
+ >(
41
+ handler: (i: I) => Effect.Effect<A, E, R>,
42
+ successSchema: SuccessSchema,
43
+ projectionSchema: ProjSchema
35
44
  ) => {
36
- const encode = S.encodeEffect(successSchema)
37
- const decode = S.decodeEffectConcurrently(projectionSchema)
38
- return (i: any) => handler(i).pipe(Effect.flatMap(encode), Effect.flatMap(decode))
45
+ const encode = S.encodeUnknownEffect(successSchema)
46
+ const decode = S.decodeUnknownEffect(projectionSchema)
47
+ return (i: I) => handler(i).pipe(Effect.flatMap(encode), Effect.flatMap(decode))
39
48
  }
40
49
 
41
50
  const projectionSchemaHash = (schema: S.Top) => String(Hash.hash(schema.ast))
@@ -202,16 +211,38 @@ export type StreamMutation2WithExtensions<RT, Req> = Req extends
202
211
  // eslint-disable-next-line unused-imports/no-unused-vars
203
212
  declare const useQuery_: QueryImpl<any>["useQuery"]
204
213
  // eslint-disable-next-line unused-imports/no-unused-vars
214
+ declare const useQueryNew_: QueryImpl<any>["useQueryNew"]
215
+ // eslint-disable-next-line unused-imports/no-unused-vars
216
+ declare const useQueryAtom_: QueryImpl<any>["useQueryAtom"]
217
+ // eslint-disable-next-line unused-imports/no-unused-vars
218
+ declare const useQueryFamily_: QueryImpl<any>["useQueryFamily"]
219
+ // eslint-disable-next-line unused-imports/no-unused-vars
220
+ declare const useSuspenseQueryNew_: QueryImpl<any>["useSuspenseQueryNew"]
221
+ // eslint-disable-next-line unused-imports/no-unused-vars
205
222
  declare const useSuspenseQuery_: QueryImpl<any>["useSuspenseQuery"]
206
223
  // eslint-disable-next-line unused-imports/no-unused-vars
207
224
  declare const useStreamQuery_: QueryImpl<any>["useStreamQuery"]
225
+ // eslint-disable-next-line unused-imports/no-unused-vars
226
+ declare const useStreamQueryFamily_: QueryImpl<any>["useStreamQueryFamily"]
227
+ // eslint-disable-next-line unused-imports/no-unused-vars
228
+ declare const useStreamQueryAtom_: QueryImpl<any>["useStreamQueryAtom"]
229
+ // eslint-disable-next-line unused-imports/no-unused-vars
230
+ declare const useStreamQueryNew_: QueryImpl<any>["useStreamQueryNew"]
208
231
 
209
232
  export interface ProjectResult<RT, I, B, E, R, Request extends Req, Id extends string> {
210
233
  request: (i: I) => Effect.Effect<B, E, R>
234
+ family: Exclude<R, RT> extends never ? ReturnType<typeof useQueryFamily_<I, E, B, Request, Id>>
235
+ : MissingDependencies<RT, R> & {}
211
236
  query: Exclude<R, RT> extends never ? ReturnType<typeof useQuery_<I, E, B, Request, Id>>
212
237
  : MissingDependencies<RT, R> & {}
238
+ queryNew: Exclude<R, RT> extends never ? ReturnType<typeof useQueryNew_<I, E, B, Request, Id>>
239
+ : MissingDependencies<RT, R> & {}
213
240
  suspense: Exclude<R, RT> extends never ? ReturnType<typeof useSuspenseQuery_<I, E, B, Request, Id>>
214
241
  : MissingDependencies<RT, R> & {}
242
+ suspenseNew: Exclude<R, RT> extends never ? ReturnType<typeof useSuspenseQueryNew_<I, E, B, Request, Id>>
243
+ : MissingDependencies<RT, R> & {}
244
+ atom: Exclude<R, RT> extends never ? ReturnType<typeof useQueryAtom_<I, E, B, Request, Id>>
245
+ : MissingDependencies<RT, R> & {}
215
246
  }
216
247
 
217
248
  export type QueryProjection<RT, HandlerReq> = HandlerReq extends
@@ -240,12 +271,30 @@ export interface QueryResultExtensions<Request extends Req, Id extends string, I
240
271
  * When `I = void` the input argument may be omitted.
241
272
  */
242
273
  query: ReturnType<typeof useQuery_<I, E, A, Request, Id>>
274
+ /**
275
+ * Atom-native query helper with object return shape.
276
+ * Additive migration surface; `.query()` remains the compatibility tuple API.
277
+ */
278
+ queryNew: ReturnType<typeof useQueryNew_<I, E, A, Request, Id>>
243
279
  // TODO or suspense as Option?
244
280
  /**
245
281
  * Like `.query`, but returns a Promise for setup-time awaiting.
246
282
  * Use this when integrating with Vue Suspense / error boundaries.
247
283
  */
248
284
  suspense: ReturnType<typeof useSuspenseQuery_<I, E, A, Request, Id>>
285
+ /**
286
+ * Promise-based setup helper with object return shape.
287
+ * Additive migration surface; `.suspense()` remains the compatibility tuple API.
288
+ */
289
+ suspenseNew: ReturnType<typeof useSuspenseQueryNew_<I, E, A, Request, Id>>
290
+ /**
291
+ * Raw query atom for composition outside Vue refs.
292
+ */
293
+ atom: ReturnType<typeof useQueryAtom_<I, E, A, Request, Id>>
294
+ /**
295
+ * Raw query atom family for composing query graphs before choosing a Vue observer.
296
+ */
297
+ family: ReturnType<typeof useQueryFamily_<I, E, A, Request, Id>>
249
298
  }
250
299
 
251
300
  export type MissingDependencies<RT, R> = {
@@ -256,26 +305,45 @@ export type MissingDependencies<RT, R> = {
256
305
  export type Queries<RT, Req> = Req extends
257
306
  RequestHandlerWithInput<infer I, infer A, infer E, infer R, infer Request, infer Id>
258
307
  ? Request["type"] extends "query" ? Exclude<R, RT> extends never ? QueryResultExtensions<Request, Id, I, A, E>
259
- : { query: MissingDependencies<RT, R> & {}; suspense: MissingDependencies<RT, R> & {} }
308
+ : {
309
+ atom: MissingDependencies<RT, R> & {}
310
+ family: MissingDependencies<RT, R> & {}
311
+ query: MissingDependencies<RT, R> & {}
312
+ queryNew: MissingDependencies<RT, R> & {}
313
+ suspense: MissingDependencies<RT, R> & {}
314
+ suspenseNew: MissingDependencies<RT, R> & {}
315
+ }
260
316
  : never
261
317
  : never
262
318
 
263
319
  export interface StreamQueryExtensions<Request extends Req, Id extends string, I, A, E> {
320
+ atom: ReturnType<typeof useStreamQueryAtom_<I, E, A, Request, Id>>
321
+ family: StreamQueryAtomFamily<I, A, E>
264
322
  /**
265
323
  * Stream helper for query-stream requests.
266
- * Runs as a tracked Vue Query and returns reactive state with accumulated chunks.
267
- * Data is an array of all chunks received so far.
324
+ * Legacy compatibility helper. Collects the whole stream into an array before
325
+ * publishing data.
268
326
  * When `I = void` the input argument may be omitted.
269
327
  */
270
328
  query: ReturnType<typeof useStreamQuery_<I, E, A, Request, Id>>
329
+ /**
330
+ * Atom-native stream query helper. Exposes incremental pull state and a `pull`
331
+ * command instead of collecting the whole stream first.
332
+ */
333
+ queryNew: ReturnType<typeof useStreamQueryNew_<I, E, A, Request, Id>>
271
334
  }
272
335
  export type StreamQueries<RT, HandlerReq> = HandlerReq extends
273
336
  RequestStreamHandlerWithInput<infer I, infer A, infer E, infer R, infer Request, infer Id, infer _Final>
274
337
  ? Exclude<R, RT> extends never ? StreamQueryExtensions<Request, Id, I, A, E>
275
- : { query: MissingDependencies<RT, R> & {} }
338
+ : {
339
+ atom: MissingDependencies<RT, R> & {}
340
+ family: MissingDependencies<RT, R> & {}
341
+ query: MissingDependencies<RT, R> & {}
342
+ queryNew: MissingDependencies<RT, R> & {}
343
+ }
276
344
  : never
277
345
 
278
- const _useMutation = makeMutation()
346
+ const _useMutation = makeMutation(atomQueryInvalidator)
279
347
 
280
348
  const wrapWithSpan = (self: { id: string }, mut: any) => {
281
349
  const span = (eff: Effect.Effect<any, any, any>) =>
@@ -308,8 +376,8 @@ export const useMutation: typeof _useMutation = (<
308
376
  * Executes query cache invalidation based on default rules or provided option.
309
377
  * adds a span with the mutation id
310
378
  */
311
- export const useMutationInt = (): typeof _useMutation => {
312
- const _useMutation = useMakeMutation()
379
+ export const useMutationInt = (queryInvalidator: QueryInvalidator): typeof _useMutation => {
380
+ const _useMutation = useMakeMutation(queryInvalidator)
313
381
  return (<
314
382
  I,
315
383
  E,
@@ -328,13 +396,31 @@ export const useMutationInt = (): typeof _useMutation => {
328
396
 
329
397
  export type ClientFrom<M extends RequestsAny> = RequestHandlers<never, never, M, ExtractModuleName<M>>
330
398
 
399
+ export interface MakeClientOptions {
400
+ /**
401
+ * Selects the engine behind legacy `.query()` / `.suspense()` helpers.
402
+ * Atom-native `.atom()` / `.family()` / `.queryNew()` / `.suspenseNew()` always use Atom.
403
+ */
404
+ readonly legacyQueryEngine?: "atom" | "tanstack"
405
+ }
406
+
331
407
  export class QueryImpl<R> {
332
408
  readonly getRuntime: () => Context.Context<R>
333
409
 
334
- constructor(getRuntime: () => Context.Context<R>) {
410
+ constructor(
411
+ getRuntime: () => Context.Context<R>,
412
+ getAtomRt: () => AtomClientRuntime,
413
+ legacyUseQuery?: ReturnType<typeof makeQuery<R>>
414
+ ) {
335
415
  this.getRuntime = getRuntime
336
- this.useQuery = makeQuery(this.getRuntime)
337
- this.useStreamQuery = makeStreamQuery(this.getRuntime)
416
+ this.useQuery = legacyUseQuery ?? makeQuery(this.getRuntime, getAtomRt)
417
+ this.useQueryNew = makeQueryNew(this.getRuntime, getAtomRt)
418
+ this.useQueryAtom = makeQueryAtom(this.getRuntime, getAtomRt)
419
+ this.useQueryFamily = makeQueryFamily(this.getRuntime, getAtomRt)
420
+ this.useStreamQuery = makeStreamQuery(this.getRuntime, getAtomRt)
421
+ this.useStreamQueryFamily = makeStreamQueryFamily(this.getRuntime, getAtomRt)
422
+ this.useStreamQueryAtom = makeStreamQueryAtom(this.getRuntime, getAtomRt)
423
+ this.useStreamQueryNew = makeStreamQueryNew(this.getRuntime, getAtomRt)
338
424
  }
339
425
  /**
340
426
  * Effect results are passed to the caller, including errors.
@@ -342,12 +428,24 @@ export class QueryImpl<R> {
342
428
  */
343
429
  readonly useQuery: ReturnType<typeof makeQuery<R>>
344
430
 
431
+ readonly useQueryNew: ReturnType<typeof makeQueryNew<R>>
432
+
433
+ readonly useQueryAtom: ReturnType<typeof makeQueryAtom<R>>
434
+
435
+ readonly useQueryFamily: ReturnType<typeof makeQueryFamily<R>>
436
+
345
437
  /**
346
438
  * Stream results are accumulated as an array of chunks and returned as reactive state.
347
439
  * @deprecated use client helpers instead (.query())
348
440
  */
349
441
  readonly useStreamQuery: ReturnType<typeof makeStreamQuery<R>>
350
442
 
443
+ readonly useStreamQueryFamily: ReturnType<typeof makeStreamQueryFamily<R>>
444
+
445
+ readonly useStreamQueryAtom: ReturnType<typeof makeStreamQueryAtom<R>>
446
+
447
+ readonly useStreamQueryNew: ReturnType<typeof makeStreamQueryNew<R>>
448
+
351
449
  /**
352
450
  * The difference with useQuery is that this function will return a Promise you can await in the Setup,
353
451
  * which ensures that either there always is a latest value, or an error occurs on load.
@@ -370,7 +468,10 @@ export class QueryImpl<R> {
370
468
  >(
371
469
  self: RequestHandlerWithInput<I, A, E, R, Request, Name>
372
470
  ): {
373
- <TData = A>(arg: I | WatchSource<I>, options?: CustomUndefinedInitialQueryOptions<A, E, TData>): Promise<
471
+ <TData = A>(
472
+ arg: I | WatchSource<I>,
473
+ options?: CustomUndefinedInitialQueryOptions<A, CauseException<E>, TData>
474
+ ): Promise<
374
475
  readonly [
375
476
  ComputedRef<AsyncResult.AsyncResult<TData, E>>,
376
477
  ComputedRef<TData>,
@@ -385,10 +486,19 @@ export class QueryImpl<R> {
385
486
  self: RequestHandlerWithInput<I, A, E, R, Request, Name>
386
487
  ) => {
387
488
  const runPromise = makeRunPromise(this.getRuntime())
388
- const q = this.useQuery(self as any) as any
389
- return (argOrOptions?: any, options?: any) => {
390
- const [resultRef, latestRef, fetch, uqrt] = q(argOrOptions, { ...options, suspense: true } // experimental_prefetchInRender: true }
391
- )
489
+ const q = this.useQuery(self)
490
+ return <TData = A>(
491
+ argOrOptions: I | WatchSource<I>,
492
+ options?: CustomUndefinedInitialQueryOptions<A, CauseException<E>, TData>
493
+ ) => {
494
+ const [resultRef, latestRef, fetch, uqrt] = q<TData>(argOrOptions, options)
495
+ const latestDefinedRef = computed<TData>(() => {
496
+ const latest = latestRef.value
497
+ if (latest === undefined) {
498
+ throw new Error("Internal Error: suspense resolved without a latest value")
499
+ }
500
+ return latest
501
+ })
392
502
 
393
503
  const isMounted = ref(true)
394
504
  onBeforeUnmount(() => {
@@ -397,36 +507,89 @@ export class QueryImpl<R> {
397
507
 
398
508
  // @effect-diagnostics effect/missingEffectError:off
399
509
  const eff = Effect.gen(function*() {
400
- // we want to throw on error so that we can catch cancelled error and skip handling it
401
- // what's the difference with just calling `fetch` ?
402
- // we will receive a CancelledError which we will have to ignore in our ErrorBoundary, otherwise the user ends up on an error page even if the user e.g cancelled a navigation
403
- const r = yield* Effect.tryPromise(() => uqrt.suspense()).pipe(
404
- Effect.catchTag("UnknownError", (err) =>
405
- isCancelledError(err.cause)
406
- ? Effect.interrupt
407
- : Effect.die(err.cause))
408
- )
510
+ const exit = yield* uqrt.awaitResult().pipe(Effect.exit)
409
511
  if (!isMounted.value) {
410
512
  return yield* Effect.interrupt
411
513
  }
412
- const result = yield* awaitResolvedSuspenseResult(resultRef)
413
- if (AsyncResult.isInitial(result)) {
414
- console.error("Internal Error: Promise should be resolved already", {
415
- self,
416
- argOrOptions,
417
- options,
418
- r,
419
- resultRef
420
- })
421
- return yield* Effect.die(
422
- "Internal Error: Promise should be resolved already"
423
- )
514
+ if (Exit.isFailure(exit)) {
515
+ return yield* Exit.failCause(exit.cause)
516
+ }
517
+
518
+ return [resultRef, latestDefinedRef, fetch, uqrt] as const
519
+ })
520
+
521
+ return runPromise(eff)
522
+ }
523
+ }
524
+
525
+ readonly useSuspenseQueryNew: {
526
+ <
527
+ I,
528
+ E,
529
+ A,
530
+ Request extends Req,
531
+ Name extends string
532
+ >(
533
+ self: RequestHandlerWithInput<I, A, E, R, Request, Name>
534
+ ): {
535
+ <TData = A>(
536
+ arg: I | WatchSource<I>,
537
+ options?: AtomQueryNewOptions<A, TData>
538
+ ): Promise<SuspenseQueryView<TData, E>>
539
+ }
540
+ } = <I, E, A, Request extends Req, Name extends string>(
541
+ self: RequestHandlerWithInput<I, A, E, R, Request, Name>
542
+ ) => {
543
+ const runPromise = makeRunPromise(this.getRuntime())
544
+ const q = this.useQueryNew(self)
545
+ return <TData = A>(
546
+ argOrOptions: I | WatchSource<I>,
547
+ options?: AtomQueryNewOptions<A, TData>
548
+ ) => {
549
+ const view = q<TData>(argOrOptions, options)
550
+ const data = computed<TData>(() => {
551
+ const latest = view.data.value
552
+ if (latest === undefined) {
553
+ throw new Error("Internal Error: suspenseNew resolved without a latest value")
554
+ }
555
+ return latest
556
+ })
557
+
558
+ const isMounted = ref(true)
559
+ onBeforeUnmount(() => {
560
+ isMounted.value = false
561
+ })
562
+
563
+ // @effect-diagnostics effect/missingEffectError:off
564
+ const eff = Effect.gen(function*() {
565
+ const exit = yield* view.awaitResult().pipe(Effect.exit)
566
+ if (!isMounted.value) {
567
+ return yield* Effect.interrupt
424
568
  }
425
- if (AsyncResult.isFailure(result)) {
426
- return yield* Exit.failCause(result.cause)
569
+ if (Exit.isFailure(exit)) {
570
+ return yield* Exit.failCause(exit.cause)
427
571
  }
428
572
 
429
- return [resultRef, latestRef, fetch, uqrt] as const
573
+ const fetch = (_options?: RefetchOptions) => view.refetch()
574
+ const handle = {
575
+ awaitResult: view.awaitResult,
576
+ refetch: view.refetch,
577
+ refresh: view.refresh,
578
+ registry: view.registry,
579
+ atom: view.atom
580
+ }
581
+ return Object.assign(
582
+ [
583
+ view.result,
584
+ data,
585
+ fetch,
586
+ handle
587
+ ] as const,
588
+ {
589
+ ...view,
590
+ data
591
+ }
592
+ )
430
593
  })
431
594
 
432
595
  return runPromise(eff)
@@ -493,11 +656,31 @@ type ClientForArgs<
493
656
  >
494
657
  ]
495
658
 
659
+ const makeResolvedAtomQueryInvalidator = <R>(getContext: () => Context.Context<R>): QueryInvalidator => {
660
+ let reactivity: Context.Service.Shape<typeof Reactivity.Reactivity> | undefined
661
+ const getReactivity = () => {
662
+ if (reactivity !== undefined) return reactivity
663
+ const service = Context.getOrUndefined(getContext(), Reactivity.Reactivity)
664
+ if (service === undefined) {
665
+ throw new Error("Reactivity service is missing from the client runtime")
666
+ }
667
+ return reactivity = service
668
+ }
669
+
670
+ return {
671
+ invalidateAndAwait: (keys) =>
672
+ invalidateAndAwait(keys).pipe(
673
+ Effect.provideService(Reactivity.Reactivity, getReactivity())
674
+ )
675
+ }
676
+ }
677
+
496
678
  export const makeClient = <RT_, RTHooks>(
497
679
  // global, but only accessible after startup has completed
498
680
  getBaseMrt: () => ManagedRuntime.ManagedRuntime<RT_ | Mix, never>,
499
681
  clientFor_: ReturnType<typeof ApiClientFactory["makeFor"]>,
500
- rtHooks: Layer.Layer<RTHooks, never, Mix>
682
+ rtHooks: Layer.Layer<RTHooks, never, Mix>,
683
+ options?: MakeClientOptions
501
684
  ) => {
502
685
  type RT = RT_ | Mix
503
686
  const getBaseRt = () => managedRuntimeRt(getBaseMrt())
@@ -505,16 +688,105 @@ export const makeClient = <RT_, RTHooks>(
505
688
  let cmd: Effect.Success<typeof makeCommand>
506
689
  const useCommand = () => cmd ??= getBaseMrt().runSync(makeCommand)
507
690
 
691
+ // one AtomRuntime for the query engine, built lazily from the live app context;
692
+ // shares the ManagedRuntime memoMap so layers + Reactivity are the same instances.
693
+ let atomRt: AtomClientRuntime | undefined
694
+ const getAtomRt =
695
+ () => (atomRt ??= makeAtomClientRuntime(() => Layer.succeedContext(getBaseRt()), getBaseMrt().memoMap))
696
+
697
+ const legacyQueryEngine = options?.legacyQueryEngine ?? "tanstack"
698
+ let tanstackQueryClient: ReturnType<typeof makeTanstackQueryClient> | undefined
699
+ const getTanstackQueryClient = () => tanstackQueryClient ??= makeTanstackQueryClient()
700
+ const atomInvalidator = makeResolvedAtomQueryInvalidator(getBaseRt)
701
+ const queryInvalidator = legacyQueryEngine === "tanstack"
702
+ ? combineQueryInvalidators(atomInvalidator, makeTanstackQueryInvalidator(getTanstackQueryClient()))
703
+ : atomInvalidator
704
+ setQueryCacheUpdater(
705
+ legacyQueryEngine === "tanstack"
706
+ ? combineQueryCacheUpdaters(
707
+ makeTanstackQueryCacheUpdater(getTanstackQueryClient()),
708
+ optionalAtomQueryCacheUpdater
709
+ )
710
+ : atomQueryCacheUpdater
711
+ )
712
+
508
713
  let m: ReturnType<typeof useMutationInt>
509
- const useMutation = () => m ??= useMutationInt()
714
+ const useMutation = () => m ??= useMutationInt(queryInvalidator)
510
715
 
511
716
  let sm2: ReturnType<typeof makeStreamMutation2>
512
- const useStreamMutation2 = () => sm2 ??= makeStreamMutation2()
717
+ const useStreamMutation2 = () => sm2 ??= makeStreamMutation2(queryInvalidator)
513
718
 
514
- const query = new QueryImpl(getBaseRt)
719
+ const legacyUseQuery = legacyQueryEngine === "tanstack"
720
+ ? makeTanstackQuery(getBaseRt, getTanstackQueryClient())
721
+ : undefined
722
+ const query = new QueryImpl(getBaseRt, getAtomRt, legacyUseQuery)
515
723
  const useQuery = query.useQuery
724
+ const useQueryNew = query.useQueryNew
725
+ const useQueryAtom = query.useQueryAtom
726
+ const useQueryFamily = query.useQueryFamily
516
727
  const useSuspenseQuery = query.useSuspenseQuery
728
+ const useSuspenseQueryNew = query.useSuspenseQueryNew
517
729
  const useStreamQuery = query.useStreamQuery
730
+ const useStreamQueryFamily = query.useStreamQueryFamily
731
+ const useStreamQueryAtom = query.useStreamQueryAtom
732
+ const useStreamQueryNew = query.useStreamQueryNew
733
+
734
+ const isQueryHandler = <H extends { readonly Request: Req }>(handler: H): handler is QueryHandler<H> =>
735
+ handler.Request.type === "query" && !handler.Request.stream
736
+
737
+ const isStreamQueryHandler = <H extends { readonly Request: Req }>(handler: H): handler is QueryStreamHandler<H> =>
738
+ handler.Request.type === "query" && handler.Request.stream
739
+
740
+ const queryHelpersFor = <I, A, E, Request extends Req, Name extends string>(
741
+ handler: RequestHandlerWithInput<I, A, E, RT, Request, Name>
742
+ ) => ({
743
+ family: useQueryFamily(handler),
744
+ atom: useQueryAtom(handler),
745
+ query: useQuery(handler),
746
+ queryNew: useQueryNew(handler),
747
+ suspense: useSuspenseQuery(handler),
748
+ suspenseNew: useSuspenseQueryNew(handler)
749
+ })
750
+
751
+ const streamQueryHelpersFor = <I, A, E, Request extends Req, Name extends string>(
752
+ handler: RequestStreamHandlerWithInput<I, A, E, RT, Request, Name>
753
+ ) => ({
754
+ family: useStreamQueryFamily(handler),
755
+ atom: useStreamQueryAtom(handler),
756
+ query: useStreamQuery(handler),
757
+ queryNew: useStreamQueryNew(handler)
758
+ })
759
+
760
+ const projectQueryFor = <
761
+ I,
762
+ A,
763
+ E,
764
+ Request extends Req & { readonly success: S.Top & { readonly "EncodingServices": RT } },
765
+ Name extends string
766
+ >(
767
+ handler: RequestHandlerWithInput<I, A, E, RT, Request, Name>
768
+ ) =>
769
+ <ProjSchema extends S.Top & { readonly "DecodingServices": RT }>(projectionSchema: ProjSchema) => {
770
+ const successSchema = handler.Request.success
771
+ const projectionHash = projectionSchemaHash(projectionSchema)
772
+ const projected = projectHandler(handler.handler, successSchema, projectionSchema)
773
+ const projectedHandler = {
774
+ handler: projected,
775
+ id: handler.id,
776
+ Request: handler.Request,
777
+ queryKeyProjectionHash: projectionHash
778
+ }
779
+ if (handler.options) {
780
+ return {
781
+ request: projected,
782
+ ...queryHelpersFor({ ...projectedHandler, options: handler.options })
783
+ }
784
+ }
785
+ return {
786
+ request: projected,
787
+ ...queryHelpersFor(projectedHandler)
788
+ }
789
+ }
518
790
 
519
791
  const mergeInvalidation = (
520
792
  a?: MutationOptionsBase["queryInvalidation"],
@@ -555,23 +827,51 @@ export const makeClient = <RT_, RTHooks>(
555
827
  ) => {
556
828
  const queries = Struct.keys(client).reduce(
557
829
  (acc, key) => {
558
- const requestType = client[key].Request.type
559
- const isStream = client[key].Request.stream
560
- if (requestType === "query" && !isStream) {
561
- ;(acc as any)[camelCase(key) + "Query"] = Object.assign(useQuery(client[key] as any), {
830
+ const handler = client[key]
831
+ if (isQueryHandler(handler)) {
832
+ Object.assign(acc, {
833
+ [camelCase(key) + "QueryFamily"]: Object.assign(useQueryFamily(handler), {
834
+ id: client[key].id
835
+ })
836
+ })
837
+ ;(acc as any)[camelCase(key) + "Query"] = Object.assign(useQuery(handler), {
562
838
  id: client[key].id
563
839
  })
564
- ;(acc as any)[camelCase(key) + "SuspenseQuery"] = Object.assign(useSuspenseQuery(client[key] as any), {
840
+ ;(acc as any)[camelCase(key) + "QueryNew"] = Object.assign(useQueryNew(handler), {
565
841
  id: client[key].id
566
842
  })
567
- } else if (requestType === "query" && isStream) {
568
- ;(acc as any)[camelCase(key) + "Query"] = Object.assign(useStreamQuery(client[key] as any), {
843
+ ;(acc as any)[camelCase(key) + "SuspenseQuery"] = Object.assign(useSuspenseQuery(handler), {
569
844
  id: client[key].id
570
845
  })
846
+ ;(acc as any)[camelCase(key) + "SuspenseQueryNew"] = Object.assign(
847
+ useSuspenseQueryNew(handler),
848
+ {
849
+ id: client[key].id
850
+ }
851
+ )
852
+ } else if (isStreamQueryHandler(handler)) {
853
+ const streamHelpers = streamQueryHelpersFor(handler)
854
+ Object.assign(acc, {
855
+ [camelCase(key) + "QueryFamily"]: Object.assign(streamHelpers.family, {
856
+ id: client[key].id
857
+ }),
858
+ [camelCase(key) + "Query"]: Object.assign(streamHelpers.query, {
859
+ id: client[key].id
860
+ }),
861
+ [camelCase(key) + "QueryNew"]: Object.assign(streamHelpers.queryNew, {
862
+ id: client[key].id
863
+ })
864
+ })
571
865
  }
572
866
  return acc
573
867
  },
574
868
  {} as
869
+ & {
870
+ [
871
+ Key in keyof typeof client as QueryHandler<typeof client[Key]> extends never ? never
872
+ : `${ToCamel<string & Key>}QueryFamily`
873
+ ]: Queries<RT, QueryHandler<typeof client[Key]>>["family"]
874
+ }
575
875
  & {
576
876
  // apparently can't get JSDoc in here..
577
877
  [
@@ -579,6 +879,12 @@ export const makeClient = <RT_, RTHooks>(
579
879
  : `${ToCamel<string & Key>}Query`
580
880
  ]: Queries<RT, QueryHandler<typeof client[Key]>>["query"]
581
881
  }
882
+ & {
883
+ [
884
+ Key in keyof typeof client as QueryHandler<typeof client[Key]> extends never ? never
885
+ : `${ToCamel<string & Key>}QueryNew`
886
+ ]: Queries<RT, QueryHandler<typeof client[Key]>>["queryNew"]
887
+ }
582
888
  // todo: or suspense as an Option?
583
889
  & {
584
890
  // apparently can't get JSDoc in here..
@@ -590,12 +896,33 @@ export const makeClient = <RT_, RTHooks>(
590
896
  QueryHandler<typeof client[Key]>
591
897
  >["suspense"]
592
898
  }
899
+ & {
900
+ [
901
+ Key in keyof typeof client as QueryHandler<typeof client[Key]> extends never ? never
902
+ : `${ToCamel<string & Key>}SuspenseQueryNew`
903
+ ]: Queries<
904
+ RT,
905
+ QueryHandler<typeof client[Key]>
906
+ >["suspenseNew"]
907
+ }
908
+ & {
909
+ [
910
+ Key in keyof typeof client as QueryStreamHandler<typeof client[Key]> extends never ? never
911
+ : `${ToCamel<string & Key>}QueryFamily`
912
+ ]: StreamQueries<RT, QueryStreamHandler<typeof client[Key]>>["family"]
913
+ }
593
914
  & {
594
915
  [
595
916
  Key in keyof typeof client as QueryStreamHandler<typeof client[Key]> extends never ? never
596
917
  : `${ToCamel<string & Key>}Query`
597
918
  ]: StreamQueries<RT, QueryStreamHandler<typeof client[Key]>>["query"]
598
919
  }
920
+ & {
921
+ [
922
+ Key in keyof typeof client as QueryStreamHandler<typeof client[Key]> extends never ? never
923
+ : `${ToCamel<string & Key>}QueryNew`
924
+ ]: StreamQueries<RT, QueryStreamHandler<typeof client[Key]>>["queryNew"]
925
+ }
599
926
  )
600
927
  return queries
601
928
  }
@@ -705,41 +1032,25 @@ export const makeClient = <RT_, RTHooks>(
705
1032
  const queryResources = makeQueryResources(invalidationResources)
706
1033
  const extended = Struct.keys(client).reduce(
707
1034
  (acc, key) => {
708
- const requestType = client[key].Request.type
709
- const isStream = client[key].Request.stream
1035
+ const handler = client[key]
1036
+ const requestType = handler.Request.type
1037
+ const isStream = handler.Request.stream
710
1038
  const fn = Command.fn(client[key].id)
711
- const h_ = client[key].handler
1039
+ const h_ = handler.handler
712
1040
  const request = h_
713
1041
  ;(acc as any)[key] = Object.assign(
714
- requestType === "query" && !isStream
1042
+ isQueryHandler(handler)
715
1043
  ? {
716
- ...client[key],
1044
+ ...handler,
717
1045
  request,
718
- query: useQuery(client[key] as any),
719
- suspense: useSuspenseQuery(client[key] as any),
720
- project: (projectionSchema: any) => {
721
- const successSchema = client[key].Request.success
722
- const projectionHash = projectionSchemaHash(projectionSchema)
723
- const projected = projectHandler(h_ as any, successSchema, projectionSchema)
724
- const fakeHandler = {
725
- handler: projected,
726
- id: client[key].id,
727
- Request: client[key].Request,
728
- options: client[key].options,
729
- queryKeyProjectionHash: projectionHash
730
- }
731
- return {
732
- request: projected,
733
- query: useQuery(fakeHandler as any),
734
- suspense: useSuspenseQuery(fakeHandler as any)
735
- }
736
- }
1046
+ ...queryHelpersFor(handler),
1047
+ project: projectQueryFor(handler)
737
1048
  }
738
- : requestType === "query" && isStream
1049
+ : isStreamQueryHandler(handler)
739
1050
  ? {
740
1051
  ...client[key],
741
1052
  request,
742
- query: useStreamQuery(client[key] as any)
1053
+ ...streamQueryHelpersFor(handler)
743
1054
  }
744
1055
  : requestType === "command" && isStream
745
1056
  ? (() => {
@@ -911,7 +1222,8 @@ export const makeClient = <RT_, RTHooks>(
911
1222
  return {
912
1223
  Command,
913
1224
  useCommand,
914
- clientFor
1225
+ clientFor,
1226
+ tanstackQueryClient: getTanstackQueryClient()
915
1227
  }
916
1228
  }
917
1229