@effect-app/vue 4.0.0-beta.309 → 4.0.0-beta.310

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.
@@ -7,6 +7,33 @@ import * as Hash from "effect/Hash"
7
7
  // cached-within-ttl) — the atom equivalent of the former tanstack query cache.
8
8
  type Entry = { readonly key: ReadonlyArray<unknown>; readonly reads: DataDependencies.DataDependencies }
9
9
  const readDependencies = new Map<number, Entry>()
10
+ export type QueryInvalidationMode = "await" | "soft"
11
+ type InvalidationModeEntry = { awaitSubscribers: number; softSubscribers: number }
12
+ const invalidationModes = new Map<number, InvalidationModeEntry>()
13
+
14
+ export const registerQueryInvalidationMode = (
15
+ key: ReadonlyArray<unknown>,
16
+ mode: QueryInvalidationMode
17
+ ): () => void => {
18
+ const hash = Hash.hash(key)
19
+ const entry = invalidationModes.get(hash) ?? { awaitSubscribers: 0, softSubscribers: 0 }
20
+ if (mode === "soft") entry.softSubscribers++
21
+ else entry.awaitSubscribers++
22
+ invalidationModes.set(hash, entry)
23
+ let active = true
24
+ return () => {
25
+ if (!active) return
26
+ active = false
27
+ if (mode === "soft") entry.softSubscribers--
28
+ else entry.awaitSubscribers--
29
+ if (entry.awaitSubscribers === 0 && entry.softSubscribers === 0) invalidationModes.delete(hash)
30
+ }
31
+ }
32
+
33
+ export const getQueryInvalidationMode = (key: ReadonlyArray<unknown>): QueryInvalidationMode => {
34
+ const entry = invalidationModes.get(Hash.hash(key))
35
+ return entry !== undefined && entry.awaitSubscribers === 0 && entry.softSubscribers > 0 ? "soft" : "await"
36
+ }
10
37
 
11
38
  export const setQueryReadDependencies = (
12
39
  key: ReadonlyArray<unknown>,
@@ -40,3 +67,18 @@ export const getDerivedInvalidationKeys = (
40
67
  }
41
68
  return keys
42
69
  }
70
+
71
+ export const partitionInvalidationKeys = (
72
+ keys: ReadonlyArray<ReadonlyArray<unknown>>
73
+ ): {
74
+ readonly awaitKeys: ReadonlyArray<ReadonlyArray<unknown>>
75
+ readonly softKeys: ReadonlyArray<ReadonlyArray<unknown>>
76
+ } => {
77
+ const awaitKeys: Array<ReadonlyArray<unknown>> = []
78
+ const softKeys: Array<ReadonlyArray<unknown>> = []
79
+ for (const key of keys) {
80
+ if (getQueryInvalidationMode(key) === "soft") softKeys.push(key)
81
+ else awaitKeys.push(key)
82
+ }
83
+ return { awaitKeys, softKeys }
84
+ }
@@ -15,7 +15,7 @@ import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
15
15
  import * as Atom from "effect/unstable/reactivity/Atom"
16
16
  import { computed, type MaybeRefOrGetter, shallowRef, toValue, watch, type WatchSource } from "vue"
17
17
  import { replaceEqualDeep } from "../atomQuery.ts"
18
- import { clearQueryReadDependencies, setQueryReadDependencies } from "../dependencyMetadata.ts"
18
+ import { clearQueryReadDependencies, registerQueryInvalidationMode, setQueryReadDependencies } from "../dependencyMetadata.ts"
19
19
  import { reportRuntimeError } from "../lib.ts"
20
20
  import type { QueryInvalidator } from "../mutate.ts"
21
21
  import type { CustomDefinedInitialQueryOptions, CustomDefinedPlaceholderQueryOptions, CustomUndefinedInitialQueryOptions, CustomUseQueryOptions, MakeQuery2, QueryCacheUpdater, QueryHandle, QueryObserverResult, RefetchOptions } from "../query.ts"
@@ -111,6 +111,15 @@ export const makeTanstackQueryInvalidator = (queryClient: QueryClient): QueryInv
111
111
  },
112
112
  { discard: true, concurrency: "inherit" }
113
113
  )
114
+ }),
115
+ invalidateSoft: (keys) =>
116
+ Effect.gen(function*() {
117
+ const span = yield* Effect.currentParentSpan.pipe(Effect.orElseSucceed(() => undefined))
118
+ yield* Effect.forEach(
119
+ keys,
120
+ (queryKey) => Effect.promise(() => queryClient.invalidateQueries({ queryKey }, { updateMeta: { span } })),
121
+ { discard: true, concurrency: "inherit" }
122
+ )
114
123
  })
115
124
  })
116
125
 
@@ -165,15 +174,21 @@ export const makeTanstackQuery = <R>(
165
174
  ...(options?.refetchInterval !== undefined ? { refetchInterval: options.refetchInterval } : {}),
166
175
  ...(options?.select !== undefined ? { select: options.select } : {})
167
176
  }
177
+ const resolvedQueryKey = computed(() => {
178
+ const input = resolveInput(arg, options?.mode)
179
+ return fullQueryKey(q, queryKey, input)
180
+ })
181
+ watch(
182
+ resolvedQueryKey,
183
+ (key, _, onCleanup) => onCleanup(registerQueryInvalidationMode(key, options?.invalidation ?? "await")),
184
+ { immediate: true }
185
+ )
168
186
  const tanstack = useTanstackQuery<A, CauseException<E>, TData>({
169
187
  ...tanstackOptions,
170
188
  enabled,
171
189
  throwOnError: false,
172
190
  retry: (retryCount: number, error: unknown) => isRetryable(error) && retryCount < 5,
173
- queryKey: computed(() => {
174
- const input = resolveInput(arg, options?.mode)
175
- return fullQueryKey(q, queryKey, input)
176
- }),
191
+ queryKey: resolvedQueryKey,
177
192
  queryFn: (
178
193
  { meta, signal }: {
179
194
  readonly meta?: { readonly span?: Tracer.AnySpan | undefined } | undefined
package/src/makeClient.ts CHANGED
@@ -19,7 +19,7 @@ import * as Struct from "effect/Struct"
19
19
  import type * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
20
20
  import * as Reactivity from "effect/unstable/reactivity/Reactivity"
21
21
  import { type ComputedRef, effectScope, onBeforeUnmount, onScopeDispose, ref, type WatchSource } from "vue"
22
- import { type AtomClientRuntime, invalidateAndAwait, makeAtomClientRuntime } from "./atomQuery.ts"
22
+ import { type AtomClientRuntime, invalidateAndAwait, invalidateSoft, makeAtomClientRuntime } from "./atomQuery.ts"
23
23
  import { type Commander, CommanderStatic, type Progress } from "./commander.ts"
24
24
  import { makeTanstackQuery, makeTanstackQueryCacheUpdater, makeTanstackQueryClient, makeTanstackQueryInvalidator } from "./internal/tanstackQuery.ts"
25
25
  import { type I18n } from "./intl.ts"
@@ -720,6 +720,10 @@ const makeResolvedAtomQueryInvalidator = <R>(getContext: () => Context.Context<R
720
720
  invalidateAndAwait: (keys) =>
721
721
  invalidateAndAwait(keys).pipe(
722
722
  Effect.provideService(Reactivity.Reactivity, getReactivity())
723
+ ),
724
+ invalidateSoft: (keys) =>
725
+ invalidateSoft(keys).pipe(
726
+ Effect.provideService(Reactivity.Reactivity, getReactivity())
723
727
  )
724
728
  }
725
729
  }
package/src/mutate.ts CHANGED
@@ -13,8 +13,9 @@ import * as Stream from "effect/Stream"
13
13
  import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
14
14
  import type * as Reactivity from "effect/unstable/reactivity/Reactivity"
15
15
  import { computed, type ComputedRef, shallowRef } from "vue"
16
- import { invalidateAndAwait } from "./atomQuery.ts"
17
- import { getDerivedInvalidationKeys } from "./dependencyMetadata.ts"
16
+ import { invalidateAndAwait, invalidateSoft } from "./atomQuery.ts"
17
+ import { getDerivedInvalidationKeys, partitionInvalidationKeys } from "./dependencyMetadata.ts"
18
+ import { reportRuntimeError } from "./lib.ts"
18
19
 
19
20
  export type GetQueryKey = (h: { id: string; options?: ClientForOptions }) => string[]
20
21
 
@@ -119,10 +120,12 @@ export type QueryInvalidationEffect<R = never> = (
119
120
  ) => Effect.Effect<void, never, R>
120
121
  export interface QueryInvalidator<R = never> {
121
122
  readonly invalidateAndAwait: QueryInvalidationEffect<R>
123
+ readonly invalidateSoft?: QueryInvalidationEffect<R>
122
124
  }
123
125
 
124
126
  export const atomQueryInvalidator: QueryInvalidator<Reactivity.Reactivity> = {
125
- invalidateAndAwait
127
+ invalidateAndAwait,
128
+ invalidateSoft
126
129
  }
127
130
 
128
131
  export const combineQueryInvalidators = <R>(
@@ -133,6 +136,12 @@ export const combineQueryInvalidators = <R>(
133
136
  invalidators,
134
137
  (invalidator) => invalidator.invalidateAndAwait(keys),
135
138
  { discard: true, concurrency: "inherit" }
139
+ ),
140
+ invalidateSoft: (keys) =>
141
+ Effect.forEach(
142
+ invalidators,
143
+ (invalidator) => invalidator.invalidateSoft?.(keys) ?? Effect.void,
144
+ { discard: true, concurrency: "inherit" }
136
145
  )
137
146
  })
138
147
 
@@ -322,10 +331,14 @@ const buildInvalidateCache = <RInvalidator>(
322
331
 
323
332
  if (!isReadonlyArrayNonEmpty(keys)) return Effect.void
324
333
 
334
+ const { awaitKeys, softKeys } = partitionInvalidationKeys(keys)
335
+
325
336
  return Effect
326
337
  .andThen(
327
338
  Effect.annotateCurrentSpan({
328
339
  keys,
340
+ awaitKeys,
341
+ softKeys,
329
342
  clientKeys,
330
343
  serverKeys,
331
344
  derivedKeys,
@@ -333,7 +346,19 @@ const buildInvalidateCache = <RInvalidator>(
333
346
  }),
334
347
  // refetch + AWAIT every live query registered under these keys, so by the time the
335
348
  // mutation resolves the affected queries are fresh.
336
- queryInvalidator.invalidateAndAwait(keys)
349
+ Effect.gen(function*() {
350
+ if (softKeys.length > 0) {
351
+ yield* (queryInvalidator.invalidateSoft?.(softKeys) ?? Effect.void).pipe(
352
+ Effect.catchCause((cause) =>
353
+ reportRuntimeError(cause, { invalidation: "soft", keys: softKeys }).pipe(Effect.asVoid)
354
+ ),
355
+ Effect.forkDetach({ startImmediately: true })
356
+ )
357
+ }
358
+ if (awaitKeys.length > 0) {
359
+ yield* queryInvalidator.invalidateAndAwait(awaitKeys)
360
+ }
361
+ })
337
362
  )
338
363
  .pipe(
339
364
  Effect.tap(Effect.sleep(0.1)), // allow for refs to update etc
package/src/query.ts CHANGED
@@ -18,6 +18,7 @@ import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
18
18
  import * as Atom from "effect/unstable/reactivity/Atom"
19
19
  import { computed, type ComputedRef, effectScope, type MaybeRefOrGetter, onBeforeUnmount, onMounted, onScopeDispose, ref, toValue, type WatchSource } from "vue"
20
20
  import { type AtomClientRuntime, type AtomQueryOptions, awaitAtomResult, buildQueryFamily, buildStreamQueryFamily, disabledQueryAtom, isStaleResult, queryKeyForAtom, refreshAtomWithCurrentSpan, staleTimeMsOf, withQueryOptions } from "./atomQuery.ts"
21
+ import type { QueryInvalidationMode } from "./dependencyMetadata.ts"
21
22
  import type { LiveQueryOptions } from "./liveQueryInvalidation.ts"
22
23
  import { latestDefined } from "./suspense.ts"
23
24
 
@@ -203,6 +204,7 @@ export interface CustomUseQueryOptions<
203
204
  /** poll: re-fetch every N ms (tanstack refetchInterval) */
204
205
  readonly refetchInterval?: number
205
206
  readonly live?: boolean | LiveQueryOptions
207
+ readonly invalidation?: QueryInvalidationMode
206
208
  readonly select?: (data: TQueryFnData) => TData
207
209
  /** accepted for source compatibility; not used by the atom engine */
208
210
  readonly retry?: boolean | number
@@ -256,6 +258,7 @@ export interface AtomQueryNewOptions<TQueryFnData = unknown, TData = TQueryFnDat
256
258
  readonly refreshEvery?: number
257
259
  readonly refetchInterval?: number
258
260
  readonly live?: boolean | LiveQueryOptions
261
+ readonly invalidation?: QueryInvalidationMode
259
262
  readonly select?: (data: TQueryFnData) => TData
260
263
  }
261
264
 
@@ -327,6 +330,7 @@ const normalizeQueryOptions = (options?: {
327
330
  readonly refetchInterval?: number
328
331
  readonly refreshEvery?: number
329
332
  readonly live?: boolean | LiveQueryOptions
333
+ readonly invalidation?: QueryInvalidationMode
330
334
  }): AtomQueryOptions => {
331
335
  const out: {
332
336
  staleTime?: number
@@ -335,6 +339,7 @@ const normalizeQueryOptions = (options?: {
335
339
  structuralSharing?: boolean
336
340
  refetchInterval?: number
337
341
  live?: boolean | LiveQueryOptions
342
+ invalidation?: QueryInvalidationMode
338
343
  } = {}
339
344
  if (options?.staleTime !== undefined) out.staleTime = options.staleTime
340
345
  const gcTime = options?.idleTTL ?? options?.gcTime
@@ -345,6 +350,7 @@ const normalizeQueryOptions = (options?: {
345
350
  const refetchInterval = options?.refreshEvery ?? options?.refetchInterval
346
351
  if (refetchInterval !== undefined) out.refetchInterval = refetchInterval
347
352
  if (options?.live !== undefined) out.live = options.live
353
+ if (options?.invalidation !== undefined) out.invalidation = options.invalidation
348
354
  return out
349
355
  }
350
356
 
@@ -521,6 +527,7 @@ const observedAtom = <A, E>(
521
527
  readonly refetchInterval?: number
522
528
  readonly refreshEvery?: number
523
529
  readonly live?: boolean | LiveQueryOptions
530
+ readonly invalidation?: QueryInvalidationMode
524
531
  }
525
532
  ): Atom.Atom<AsyncResult.AsyncResult<A, E>> =>
526
533
  withQueryOptions(atom, normalizeQueryOptions(options), queryKeyForAtom(atom))
@@ -13,7 +13,7 @@ import { TestClock } from "effect/testing"
13
13
  import * as Reactivity from "effect/unstable/reactivity/Reactivity"
14
14
  import { createApp, effectScope, ref } from "vue"
15
15
  import { awaitAtomResult, buildQueryFamily, invalidateAndAwait, makeAtomClientRuntime } from "../src/atomQuery.js"
16
- import { clearQueryReadDependencies, getDerivedInvalidationKeys, setQueryReadDependencies } from "../src/dependencyMetadata.js"
16
+ import { clearQueryReadDependencies, getDerivedInvalidationKeys, partitionInvalidationKeys, registerQueryInvalidationMode, setQueryReadDependencies } from "../src/dependencyMetadata.js"
17
17
  import { makeTanstackQuery, makeTanstackQueryInvalidator } from "../src/internal/tanstackQuery.js"
18
18
  import { invalidateQueries, makeStreamMutation2, type MutationOptionsBase } from "../src/mutate.js"
19
19
 
@@ -58,6 +58,30 @@ it("getDerivedInvalidationKeys returns keys of queries whose reads intersect the
58
58
  }
59
59
  })
60
60
 
61
+ it("uses the strictest active query invalidation mode", () => {
62
+ const overviewKey = ["$Overview", "$List", undefined]
63
+ const pickListKey = ["$PickList", "$List", undefined]
64
+
65
+ const unregisterSoft = registerQueryInvalidationMode(overviewKey, "soft")
66
+ const unregisterAwait = registerQueryInvalidationMode(pickListKey, "await")
67
+ const unregisterStrictOverview = registerQueryInvalidationMode(overviewKey, "await")
68
+ try {
69
+ expect(partitionInvalidationKeys([overviewKey, pickListKey])).toEqual({
70
+ awaitKeys: [overviewKey, pickListKey],
71
+ softKeys: []
72
+ })
73
+ unregisterStrictOverview()
74
+ expect(partitionInvalidationKeys([overviewKey, pickListKey])).toEqual({
75
+ awaitKeys: [pickListKey],
76
+ softKeys: [overviewKey]
77
+ })
78
+ } finally {
79
+ unregisterStrictOverview()
80
+ unregisterSoft()
81
+ unregisterAwait()
82
+ }
83
+ })
84
+
61
85
  it("clearing read dependencies drops the query from derivation", () => {
62
86
  const inventoryKey = ["$Inventory", "List", undefined]
63
87
  setQueryReadDependencies(inventoryKey, new Set([repo]))
@@ -135,7 +159,7 @@ it("atom engine: a query records its read deps so a command's writes derive it",
135
159
 
136
160
  const unmount = defaultRegistry.mount(atom)
137
161
  try {
138
- await Effect.runPromise(awaitAtomResult(defaultRegistry, atom) as any)
162
+ await Effect.runPromise(awaitAtomResult(defaultRegistry, atom))
139
163
  expect(runs).toBe(1)
140
164
 
141
165
  const fullKey = [...makeQueryKey(self), undefined]
@@ -224,7 +248,7 @@ it("atom engine: disposing the query atom clears its recorded reads", async () =
224
248
  const fullKey = [...makeQueryKey(self), undefined]
225
249
 
226
250
  const unmount = defaultRegistry.mount(atom)
227
- await Effect.runPromise(awaitAtomResult(defaultRegistry, atom) as any)
251
+ await Effect.runPromise(awaitAtomResult(defaultRegistry, atom))
228
252
  expect(getDerivedInvalidationKeys(new Set([atomRepo]))).toContainEqual(fullKey)
229
253
 
230
254
  // Disposing the registry runs the atom's finalizers, including `trackReadDependencies`.
@@ -291,12 +315,12 @@ const makeAtomHarness = (queryRepo: DataDependencies.DataDependency): EngineHarn
291
315
  return {
292
316
  queryFullKey: [...makeQueryKey(self), undefined],
293
317
  serverInvalidationKey: makeQueryKey(self),
294
- fetchInitial: () => Effect.runPromise(awaitAtomResult(defaultRegistry, atom) as any),
318
+ fetchInitial: () => Effect.runPromise(awaitAtomResult(defaultRegistry, atom)),
295
319
  runs: () => runs,
296
320
  runCommand: (options, command) =>
297
321
  Effect.runPromise(
298
322
  invalidateQueries({ id: "MatrixAtom.Save" }, options, invalidator)(command, { id: "x" })
299
- .pipe(Effect.andThen(awaitAtomResult(defaultRegistry, atom).pipe(Effect.exit))) as any
323
+ .pipe(Effect.andThen(awaitAtomResult(defaultRegistry, atom).pipe(Effect.exit)))
300
324
  ),
301
325
  dispose: () => {
302
326
  unmount()