@effect-app/vue 4.0.0-beta.271 → 4.0.0-beta.273
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/CHANGELOG.md +16 -0
- package/dist/atomQuery.d.ts +90 -0
- package/dist/atomQuery.d.ts.map +1 -0
- package/dist/atomQuery.js +275 -0
- package/dist/internal/tanstackQuery.d.ts +8 -0
- package/dist/internal/tanstackQuery.d.ts.map +1 -0
- package/dist/internal/tanstackQuery.js +145 -0
- package/dist/makeClient.d.ts +75 -13
- package/dist/makeClient.d.ts.map +1 -1
- package/dist/makeClient.js +203 -78
- package/dist/mutate.d.ts +20 -12
- package/dist/mutate.d.ts.map +1 -1
- package/dist/mutate.js +65 -64
- package/dist/query.d.ts +114 -12
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +275 -179
- package/docs/atom-query-api-redesign.md +191 -0
- package/package.json +2 -2
- package/src/atomQuery.ts +361 -0
- package/src/internal/tanstackQuery.ts +221 -0
- package/src/makeClient.ts +382 -91
- package/src/mutate.ts +101 -110
- package/src/query.ts +564 -243
- package/test/dist/stubs.d.ts +169 -2
- package/test/dist/stubs.d.ts.map +1 -1
- package/test/dist/stubs.js +11 -6
- package/test/makeClient.test.ts +110 -0
- package/test/stubs.ts +10 -5
- package/tsconfig.json.bak +72 -14
package/src/mutate.ts
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import { matchQuery } from "@tanstack/query-core"
|
|
3
|
-
import { type InvalidateOptions, type InvalidateQueryFilters, type QueryClient, useQueryClient } from "@tanstack/vue-query"
|
|
4
2
|
import { type InvalidationKey, InvalidationKeysFromServer, makeInvalidationKeysService, makeQueryKey, type Req } from "effect-app/client"
|
|
5
3
|
import type { ClientForOptions, RequestHandlerWithInput } from "effect-app/client/clientFor"
|
|
6
4
|
import type { InvalidateQueryInstruction } from "effect-app/client/makeClient"
|
|
7
5
|
import * as Effect from "effect-app/Effect"
|
|
8
6
|
import { tuple } from "effect-app/Function"
|
|
9
7
|
import * as Option from "effect-app/Option"
|
|
8
|
+
import { isReadonlyArrayNonEmpty } from "effect/Array"
|
|
10
9
|
import type * as Cause from "effect/Cause"
|
|
11
10
|
import * as Exit from "effect/Exit"
|
|
12
11
|
import * as Ref from "effect/Ref"
|
|
13
12
|
import * as Stream from "effect/Stream"
|
|
14
13
|
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
|
|
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"
|
|
16
17
|
|
|
17
18
|
export type GetQueryKey = (h: { id: string; options?: ClientForOptions }) => string[]
|
|
18
19
|
|
|
@@ -103,11 +104,51 @@ export function make<A, E, R>(self: Effect.Effect<A, E, R>) {
|
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
/**
|
|
106
|
-
* An entry for `queryInvalidation
|
|
107
|
-
* `
|
|
108
|
-
*
|
|
107
|
+
* An entry for `queryInvalidation`: a raw query key, an `{ id }` handler ref, or a
|
|
108
|
+
* `{ filters }` shape. The atom engine acts on entries that carry a concrete query
|
|
109
|
+
* key: raw arrays, handler refs, or `{ filters: { queryKey } }`. Predicate-only
|
|
110
|
+
* filters have no exact-key reactivity equivalent and fail fast.
|
|
109
111
|
*/
|
|
110
|
-
export type
|
|
112
|
+
export type QueryKeyInvalidationFilters = {
|
|
113
|
+
readonly queryKey: ReadonlyArray<unknown>
|
|
114
|
+
}
|
|
115
|
+
export type InvalidationEntry = InvalidateQueryInstruction<QueryKeyInvalidationFilters>
|
|
116
|
+
export type QueryInvalidationEffect<R = never> = (
|
|
117
|
+
keys: ReadonlyArray<ReadonlyArray<unknown>>
|
|
118
|
+
) => Effect.Effect<void, never, R>
|
|
119
|
+
export interface QueryInvalidator<R = never> {
|
|
120
|
+
readonly invalidateAndAwait: QueryInvalidationEffect<R>
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export const atomQueryInvalidator: QueryInvalidator<Reactivity.Reactivity> = {
|
|
124
|
+
invalidateAndAwait
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export const combineQueryInvalidators = <R>(
|
|
128
|
+
...invalidators: ReadonlyArray<QueryInvalidator<R>>
|
|
129
|
+
): QueryInvalidator<R> => ({
|
|
130
|
+
invalidateAndAwait: (keys) =>
|
|
131
|
+
Effect.forEach(
|
|
132
|
+
invalidators,
|
|
133
|
+
(invalidator) => invalidator.invalidateAndAwait(keys),
|
|
134
|
+
{ discard: true, concurrency: "inherit" }
|
|
135
|
+
)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
const isRecord = (value: unknown): value is { readonly [key: string]: unknown } =>
|
|
139
|
+
typeof value === "object" && value !== null
|
|
140
|
+
|
|
141
|
+
const isQueryKey = (entry: InvalidationEntry): entry is ReadonlyArray<string> => Array.isArray(entry)
|
|
142
|
+
|
|
143
|
+
const queryKeyFromFilters = (
|
|
144
|
+
entry: Exclude<InvalidationEntry, ReadonlyArray<string>>
|
|
145
|
+
): ReadonlyArray<unknown> | undefined => {
|
|
146
|
+
if (!("filters" in entry)) return undefined
|
|
147
|
+
const filters = entry.filters
|
|
148
|
+
if (!isRecord(filters)) return undefined
|
|
149
|
+
const queryKey = filters["queryKey"]
|
|
150
|
+
return Array.isArray(queryKey) ? queryKey : undefined
|
|
151
|
+
}
|
|
111
152
|
|
|
112
153
|
export interface MutationOptionsBase<A = unknown, B = A, E2 = never, R2 = never> {
|
|
113
154
|
/**
|
|
@@ -220,56 +261,42 @@ export const asStreamResult = <Args extends readonly any[], A, E, R>(
|
|
|
220
261
|
return tuple(computed(() => state.value), act) as any
|
|
221
262
|
}
|
|
222
263
|
|
|
223
|
-
const buildInvalidateCache = (
|
|
224
|
-
queryClient: QueryClient,
|
|
264
|
+
const buildInvalidateCache = <RInvalidator>(
|
|
225
265
|
self: { id: string; options?: ClientForOptions },
|
|
226
|
-
queryInvalidation
|
|
266
|
+
queryInvalidation: MutationOptionsBase["queryInvalidation"] | undefined,
|
|
267
|
+
queryInvalidator: QueryInvalidator<RInvalidator>
|
|
227
268
|
) => {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const invalidateQueriesFn = (
|
|
234
|
-
filters?: InvalidateQueryFilters,
|
|
235
|
-
options?: InvalidateOptions
|
|
236
|
-
) =>
|
|
237
|
-
Effect.currentSpan.pipe(
|
|
238
|
-
Effect.orElseSucceed(() => null),
|
|
239
|
-
Effect.flatMap((span) =>
|
|
240
|
-
Effect.promise(() => queryClient.invalidateQueries(filters, { ...options, updateMeta: { span } }))
|
|
241
|
-
)
|
|
242
|
-
)
|
|
243
|
-
|
|
244
|
-
const getClientInvalidationTargets = (
|
|
269
|
+
// Concrete reactivity keys to invalidate: a raw query key, one derived from an `{ id }`
|
|
270
|
+
// entry, or a compatibility `{ filters: { queryKey } }` entry.
|
|
271
|
+
// Predicate-only `{ filters }` entries have no exact-key reactivity equivalent and throw.
|
|
272
|
+
const getClientInvalidationKeys = (
|
|
245
273
|
input: unknown,
|
|
246
274
|
output: Exit.Exit<unknown, unknown>
|
|
247
|
-
): ReadonlyArray<
|
|
275
|
+
): ReadonlyArray<ReadonlyArray<unknown>> => {
|
|
248
276
|
const queryKey = getQueryKey(self)
|
|
249
277
|
|
|
250
278
|
if (queryInvalidation) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
279
|
+
const keys: Array<ReadonlyArray<unknown>> = []
|
|
280
|
+
for (const entry of queryInvalidation(queryKey, self.id, input, output)) {
|
|
281
|
+
if (isQueryKey(entry)) {
|
|
282
|
+
keys.push(entry)
|
|
283
|
+
continue
|
|
254
284
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
filters: {
|
|
259
|
-
queryKey: makeQueryKey(obj.options ? { id: obj.id, options: obj.options } : { id: obj.id })
|
|
260
|
-
},
|
|
261
|
-
options: undefined
|
|
262
|
-
}
|
|
285
|
+
if ("id" in entry) {
|
|
286
|
+
keys.push(makeQueryKey(entry.options ? { id: entry.id, options: entry.options } : { id: entry.id }))
|
|
287
|
+
continue
|
|
263
288
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
289
|
+
const filterQueryKey = queryKeyFromFilters(entry)
|
|
290
|
+
if (filterQueryKey !== undefined) {
|
|
291
|
+
keys.push(filterQueryKey)
|
|
292
|
+
continue
|
|
293
|
+
}
|
|
294
|
+
throw new Error("Unsupported query invalidation filter: only filters.queryKey is supported")
|
|
295
|
+
}
|
|
296
|
+
return keys
|
|
270
297
|
}
|
|
271
298
|
|
|
272
|
-
return
|
|
299
|
+
return queryKey ? [queryKey] : []
|
|
273
300
|
}
|
|
274
301
|
|
|
275
302
|
const invalidateCache = (
|
|
@@ -278,57 +305,22 @@ const buildInvalidateCache = (
|
|
|
278
305
|
serverKeys: ReadonlyArray<InvalidationKey>
|
|
279
306
|
) =>
|
|
280
307
|
Effect.suspend(() => {
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
if (!allTargets.length) return Effect.void
|
|
289
|
-
|
|
290
|
-
// Group targets by refetchType + options so each group can be merged into a single
|
|
291
|
-
// invalidateQueries call using a predicate, reducing N calls to 1 in the common case.
|
|
292
|
-
type Group = {
|
|
293
|
-
targets: Array<InvalidationTarget>
|
|
294
|
-
refetchType: InvalidateQueryFilters["refetchType"]
|
|
295
|
-
options: InvalidateOptions | undefined
|
|
296
|
-
}
|
|
297
|
-
const groups = new Map<string, Group>()
|
|
298
|
-
for (const target of allTargets) {
|
|
299
|
-
const key = `${target.filters?.refetchType ?? ""}|${target.options?.cancelRefetch ?? ""}|${
|
|
300
|
-
target.options?.throwOnError?.toString() ?? ""
|
|
301
|
-
}`
|
|
302
|
-
const existing = groups.get(key)
|
|
303
|
-
if (existing) {
|
|
304
|
-
existing.targets.push(target)
|
|
305
|
-
} else {
|
|
306
|
-
groups.set(key, { targets: [target], refetchType: target.filters?.refetchType, options: target.options })
|
|
307
|
-
}
|
|
308
|
-
}
|
|
308
|
+
const clientKeys = getClientInvalidationKeys(input, output)
|
|
309
|
+
// Invalidate exact reactivity keys (= the prefixes query atoms register under). Each key
|
|
310
|
+
// array is hashed structurally, matching the query-side registration.
|
|
311
|
+
const keys: ReadonlyArray<ReadonlyArray<unknown>> = [...clientKeys, ...serverKeys]
|
|
312
|
+
|
|
313
|
+
if (!isReadonlyArrayNonEmpty(keys)) return Effect.void
|
|
309
314
|
|
|
310
315
|
return Effect
|
|
311
316
|
.andThen(
|
|
312
|
-
Effect.annotateCurrentSpan({
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
invalidateQueriesFn(
|
|
317
|
-
{
|
|
318
|
-
...(refetchType !== undefined ? { refetchType } : {}),
|
|
319
|
-
predicate: (query) => targets.some((t) => t.filters ? matchQuery(t.filters, query) : true)
|
|
320
|
-
},
|
|
321
|
-
options
|
|
322
|
-
),
|
|
323
|
-
{ discard: true, concurrency: "inherit" }
|
|
324
|
-
)
|
|
317
|
+
Effect.annotateCurrentSpan({ clientKeys, serverKeys }),
|
|
318
|
+
// refetch + AWAIT every live query registered under these keys, so by the time the
|
|
319
|
+
// mutation resolves the affected queries are fresh.
|
|
320
|
+
queryInvalidator.invalidateAndAwait(keys)
|
|
325
321
|
)
|
|
326
322
|
.pipe(
|
|
327
|
-
Effect.tap(
|
|
328
|
-
// hand over control back to the event loop so that state can be updated..
|
|
329
|
-
// TODO: should we do this in general on any mutation, regardless of invalidation?
|
|
330
|
-
Effect.sleep(0)
|
|
331
|
-
),
|
|
323
|
+
Effect.tap(Effect.sleep(0.1)), // allow for refs to update etc
|
|
332
324
|
Effect.withSpan("client.query.invalidation", {}, { captureStackTrace: false })
|
|
333
325
|
)
|
|
334
326
|
})
|
|
@@ -336,12 +328,12 @@ const buildInvalidateCache = (
|
|
|
336
328
|
return invalidateCache
|
|
337
329
|
}
|
|
338
330
|
|
|
339
|
-
export const invalidateQueries = (
|
|
340
|
-
queryClient: QueryClient,
|
|
331
|
+
export const invalidateQueries = <RInvalidator>(
|
|
341
332
|
self: { id: string; options?: ClientForOptions },
|
|
342
|
-
options
|
|
333
|
+
options: MutationOptionsBase | undefined,
|
|
334
|
+
queryInvalidator: QueryInvalidator<RInvalidator>
|
|
343
335
|
) => {
|
|
344
|
-
const invalidateCache = buildInvalidateCache(
|
|
336
|
+
const invalidateCache = buildInvalidateCache(self, options?.queryInvalidation, queryInvalidator)
|
|
345
337
|
|
|
346
338
|
const select = options?.select
|
|
347
339
|
|
|
@@ -384,7 +376,7 @@ export interface MutationFn<I, A, E, R, Id extends string> {
|
|
|
384
376
|
readonly id: Id
|
|
385
377
|
}
|
|
386
378
|
|
|
387
|
-
export const makeMutation = () => {
|
|
379
|
+
export const makeMutation = <RInvalidator>(queryInvalidator: QueryInvalidator<RInvalidator>) => {
|
|
388
380
|
/**
|
|
389
381
|
* Pass a function that returns an Effect, e.g from a client action.
|
|
390
382
|
* Executes query cache invalidation based on default rules or provided option.
|
|
@@ -393,17 +385,14 @@ export const makeMutation = () => {
|
|
|
393
385
|
const useMutation = <I, E, A, R, Request extends Req, Id extends string>(
|
|
394
386
|
self: RequestHandlerWithInput<I, A, E, R, Request, Id>
|
|
395
387
|
): MutationFn<I, A, E, R, Id> => {
|
|
396
|
-
const
|
|
397
|
-
|
|
388
|
+
const r = (i: I, options?: MutationOptionsBase) =>
|
|
389
|
+
invalidateQueries(self, options, queryInvalidator)(self.handler(i), i)
|
|
398
390
|
return Object.assign(r, { id: self.id }) as any
|
|
399
391
|
}
|
|
400
392
|
return useMutation
|
|
401
393
|
}
|
|
402
394
|
|
|
403
|
-
|
|
404
|
-
export const useMakeMutation = () => {
|
|
405
|
-
const queryClient = useQueryClient()
|
|
406
|
-
|
|
395
|
+
export const useMakeMutation = <RInvalidator>(queryInvalidator: QueryInvalidator<RInvalidator>) => {
|
|
407
396
|
/**
|
|
408
397
|
* Pass a function that returns an Effect, e.g from a client action.
|
|
409
398
|
* Executes query cache invalidation based on default rules or provided option.
|
|
@@ -412,7 +401,8 @@ export const useMakeMutation = () => {
|
|
|
412
401
|
const useMutation = <I, E, A, R, Request extends Req, Id extends string>(
|
|
413
402
|
self: RequestHandlerWithInput<I, A, E, R, Request, Id>
|
|
414
403
|
): MutationFn<I, A, E, R, Id> => {
|
|
415
|
-
const r = (i: I, options?: MutationOptionsBase) =>
|
|
404
|
+
const r = (i: I, options?: MutationOptionsBase) =>
|
|
405
|
+
invalidateQueries(self, options, queryInvalidator)(self.handler(i), i)
|
|
416
406
|
return Object.assign(r, { id: self.id }) as any
|
|
417
407
|
}
|
|
418
408
|
return useMutation
|
|
@@ -425,12 +415,8 @@ export const useMakeMutation = () => {
|
|
|
425
415
|
*
|
|
426
416
|
* Use with `streamFn` / `Command.streamFn(id)(mutateHandler, ...combinators)` so that
|
|
427
417
|
* the command manages its own reactive state internally.
|
|
428
|
-
*
|
|
429
|
-
* Must be called inside a Vue setup context (uses `useQueryClient` internally).
|
|
430
418
|
*/
|
|
431
|
-
export const makeStreamMutation2 = () => {
|
|
432
|
-
const queryClient = useQueryClient()
|
|
433
|
-
|
|
419
|
+
export const makeStreamMutation2 = <RInvalidator>(queryInvalidator: QueryInvalidator<RInvalidator>) => {
|
|
434
420
|
return (
|
|
435
421
|
self: {
|
|
436
422
|
id: string
|
|
@@ -439,12 +425,17 @@ export const makeStreamMutation2 = () => {
|
|
|
439
425
|
},
|
|
440
426
|
mergedInvalidation?: MutationOptionsBase["queryInvalidation"]
|
|
441
427
|
) => {
|
|
442
|
-
const invCache = buildInvalidateCache(
|
|
428
|
+
const invCache = buildInvalidateCache(self, mergedInvalidation, queryInvalidator)
|
|
443
429
|
|
|
444
430
|
const makeInvocationEffect = (input: unknown, source: Stream.Stream<any, any, any>) =>
|
|
445
431
|
Effect.gen(function*() {
|
|
446
432
|
const keysRef = yield* Ref.make<ReadonlyArray<InvalidationKey>>([])
|
|
447
|
-
const invKeys = makeInvalidationKeysService(
|
|
433
|
+
const invKeys = makeInvalidationKeysService(
|
|
434
|
+
keysRef,
|
|
435
|
+
// Stream invalidation is sequenced by the injected query invalidator; this callback
|
|
436
|
+
// returns void to keep the server-side invalidation service effect-free.
|
|
437
|
+
(key) => invCache(input, Exit.succeed(undefined), [key]) as Effect.Effect<void>
|
|
438
|
+
)
|
|
448
439
|
const lastRef = yield* Ref.make<any>(undefined)
|
|
449
440
|
return source.pipe(
|
|
450
441
|
Stream.provideService(InvalidationKeysFromServer, invKeys),
|