@effect-app/vue 4.0.0-beta.30 → 4.0.0-beta.301
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 +2234 -0
- package/dist/atomQuery.d.ts +111 -0
- package/dist/atomQuery.d.ts.map +1 -0
- package/dist/atomQuery.js +386 -0
- package/dist/commander.d.ts +634 -0
- package/dist/commander.d.ts.map +1 -0
- package/dist/commander.js +1070 -0
- package/dist/{experimental/confirm.d.ts → confirm.d.ts} +6 -4
- package/dist/confirm.d.ts.map +1 -0
- package/dist/confirm.js +26 -0
- package/dist/dependencyMetadata.d.ts +10 -0
- package/dist/dependencyMetadata.d.ts.map +1 -0
- package/dist/dependencyMetadata.js +29 -0
- package/dist/errorReporter.d.ts +6 -4
- package/dist/errorReporter.d.ts.map +1 -1
- package/dist/errorReporter.js +14 -19
- package/dist/form.d.ts +14 -5
- package/dist/form.d.ts.map +1 -1
- package/dist/form.js +46 -13
- package/dist/internal/tanstackQuery.d.ts +9 -0
- package/dist/internal/tanstackQuery.d.ts.map +1 -0
- package/dist/internal/tanstackQuery.js +194 -0
- package/dist/intl.d.ts +15 -0
- package/dist/intl.d.ts.map +1 -0
- package/dist/intl.js +9 -0
- package/dist/lib.d.ts +7 -9
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +35 -10
- package/dist/makeClient.d.ts +236 -336
- package/dist/makeClient.d.ts.map +1 -1
- package/dist/makeClient.js +416 -408
- package/dist/makeContext.d.ts.map +1 -1
- package/dist/makeIntl.d.ts.map +1 -1
- package/dist/{experimental/makeUseCommand.d.ts → makeUseCommand.d.ts} +4 -3
- package/dist/makeUseCommand.d.ts.map +1 -0
- package/dist/makeUseCommand.js +13 -0
- package/dist/mutate.d.ts +108 -40
- package/dist/mutate.d.ts.map +1 -1
- package/dist/mutate.js +211 -58
- package/dist/query.d.ts +150 -46
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +370 -122
- package/dist/routeParams.d.ts +4 -4
- package/dist/routeParams.d.ts.map +1 -1
- package/dist/routeParams.js +4 -3
- package/dist/runtime.d.ts +1 -14
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +2 -26
- package/dist/suspense.d.ts +19 -0
- package/dist/suspense.d.ts.map +1 -0
- package/dist/suspense.js +38 -0
- package/dist/toast.d.ts +2 -0
- package/dist/toast.d.ts.map +1 -0
- package/dist/toast.js +2 -0
- package/dist/withToast.d.ts +2 -0
- package/dist/withToast.d.ts.map +1 -0
- package/dist/withToast.js +2 -0
- package/docs/atom-query-api-redesign.md +207 -0
- package/docs/query-key-invalidation.md +106 -0
- package/examples/streamMutation.ts +72 -0
- package/package.json +36 -95
- package/src/atomQuery.ts +540 -0
- package/src/commander.ts +3413 -0
- package/src/{experimental/confirm.ts → confirm.ts} +13 -15
- package/src/dependencyMetadata.ts +38 -0
- package/src/errorReporter.ts +65 -75
- package/src/form.ts +61 -18
- package/src/index.ts +7 -7
- package/src/internal/tanstackQuery.ts +289 -0
- package/src/intl.ts +12 -0
- package/src/lib.ts +49 -21
- package/src/makeClient.ts +967 -1159
- package/src/makeIntl.ts +2 -2
- package/src/{experimental/makeUseCommand.ts → makeUseCommand.ts} +9 -6
- package/src/mutate.ts +370 -152
- package/src/query.ts +742 -260
- package/src/routeParams.ts +7 -7
- package/src/runtime.ts +1 -31
- package/src/suspense.ts +43 -0
- package/src/toast.ts +1 -0
- package/src/withToast.ts +1 -0
- package/test/Mutation.test.ts +181 -24
- package/test/dependencyInvalidation.test.ts +373 -0
- package/test/dist/dependencyInvalidation.test.d.ts.map +1 -0
- package/test/dist/form.test.d.ts.map +1 -1
- package/test/dist/interrupt-refetch-repro.test.d.ts.map +1 -0
- package/test/dist/lib.test.d.ts.map +1 -0
- package/test/dist/query-span.test.d.ts.map +1 -0
- package/test/dist/queryOptions.test.d.ts.map +1 -0
- package/test/dist/streamFinal.test.d.ts.map +1 -0
- package/test/dist/streamFn.test.d.ts.map +1 -0
- package/test/dist/stubs.d.ts +3756 -122
- package/test/dist/stubs.d.ts.map +1 -1
- package/test/dist/stubs.js +192 -32
- package/test/dist/suspense-regression.test.d.ts.map +1 -0
- package/test/dist/suspense.test.d.ts.map +1 -0
- package/test/form-validation-errors.test.ts +25 -20
- package/test/form.test.ts +22 -3
- package/test/interrupt-refetch-repro.test.ts +623 -0
- package/test/lib.test.ts +240 -0
- package/test/makeClient.test.ts +437 -38
- package/test/query-span.test.ts +92 -0
- package/test/queryOptions.test.ts +88 -0
- package/test/streamFinal.test.ts +64 -0
- package/test/streamFn.test.ts +457 -0
- package/test/stubs.ts +228 -43
- package/test/suspense-regression.test.ts +168 -0
- package/test/suspense.test.ts +160 -0
- package/tsconfig.examples.json +20 -0
- package/tsconfig.json +3 -1
- package/tsconfig.src.json +34 -34
- package/tsconfig.test.json +2 -2
- package/vitest.config.ts +5 -5
- package/dist/experimental/commander.d.ts +0 -359
- package/dist/experimental/commander.d.ts.map +0 -1
- package/dist/experimental/commander.js +0 -557
- package/dist/experimental/confirm.d.ts.map +0 -1
- package/dist/experimental/confirm.js +0 -28
- package/dist/experimental/intl.d.ts +0 -16
- package/dist/experimental/intl.d.ts.map +0 -1
- package/dist/experimental/intl.js +0 -5
- package/dist/experimental/makeUseCommand.d.ts.map +0 -1
- package/dist/experimental/makeUseCommand.js +0 -13
- package/dist/experimental/toast.d.ts +0 -47
- package/dist/experimental/toast.d.ts.map +0 -1
- package/dist/experimental/toast.js +0 -41
- package/dist/experimental/withToast.d.ts +0 -25
- package/dist/experimental/withToast.d.ts.map +0 -1
- package/dist/experimental/withToast.js +0 -45
- package/eslint.config.mjs +0 -24
- package/src/experimental/commander.ts +0 -1835
- package/src/experimental/intl.ts +0 -9
- package/src/experimental/toast.ts +0 -66
- package/src/experimental/withToast.ts +0 -99
- package/tsconfig.json.bak +0 -12
package/src/makeIntl.ts
CHANGED
|
@@ -3,8 +3,8 @@ import { createIntl, createIntlCache, type Formatters, type IntlFormatters, type
|
|
|
3
3
|
import { typedKeysOf } from "effect-app/utils"
|
|
4
4
|
import { type FormatXMLElementFn, type Options as IntlMessageFormatOptions, type PrimitiveType } from "intl-messageformat"
|
|
5
5
|
import { type Ref, watch } from "vue"
|
|
6
|
-
import { translate } from "./form.
|
|
7
|
-
import { makeContext } from "./makeContext.
|
|
6
|
+
import { translate } from "./form.ts"
|
|
7
|
+
import { makeContext } from "./makeContext.ts"
|
|
8
8
|
|
|
9
9
|
export interface MakeIntlReturn<Locale extends string> extends ReturnType<typeof makeIntl<Locale>> {}
|
|
10
10
|
|
|
@@ -1,26 +1,29 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import * as Effect from "effect-app/Effect"
|
|
2
|
+
import type * as Layer from "effect-app/Layer"
|
|
3
|
+
import { Commander, type CommanderImpl, CommanderStatic } from "./commander.ts"
|
|
3
4
|
|
|
4
5
|
type X<X> = X
|
|
5
6
|
|
|
6
7
|
// helps retain JSDoc
|
|
7
8
|
export interface CommanderResolved<RT, RTHooks>
|
|
8
|
-
extends
|
|
9
|
+
extends
|
|
10
|
+
X<typeof CommanderStatic>,
|
|
11
|
+
Pick<CommanderImpl<RT, RTHooks>, "fn" | "wrap" | "streamWrap" | "streamFn" | "alt" | "alt2">
|
|
9
12
|
{
|
|
10
13
|
}
|
|
11
14
|
|
|
12
15
|
export const makeUseCommand = Effect.fnUntraced(
|
|
13
16
|
function*<R = never, RTHooks = never>(rtHooks: Layer.Layer<RTHooks, never, R>) {
|
|
14
17
|
const cmndr = yield* Commander
|
|
15
|
-
const runtime = yield* Effect.
|
|
18
|
+
const runtime = yield* Effect.context<R>()
|
|
16
19
|
|
|
17
20
|
const comm = cmndr(runtime, rtHooks)
|
|
18
21
|
|
|
19
|
-
const command = {
|
|
22
|
+
const command: CommanderResolved<R, RTHooks> = {
|
|
20
23
|
...comm,
|
|
21
24
|
...CommanderStatic
|
|
22
25
|
}
|
|
23
26
|
|
|
24
|
-
return command
|
|
27
|
+
return command
|
|
25
28
|
}
|
|
26
29
|
)
|
package/src/mutate.ts
CHANGED
|
@@ -1,23 +1,60 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import { type
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
2
|
+
import { DataDependencies, type InvalidationKey, InvalidationKeysFromServer, makeInvalidationKeysService, makeQueryKey, type Req } from "effect-app/client"
|
|
3
|
+
import type { ClientForOptions, RequestHandlerWithInput } from "effect-app/client/clientFor"
|
|
4
|
+
import type { InvalidateQueryInstruction } from "effect-app/client/makeClient"
|
|
5
|
+
import * as Effect from "effect-app/Effect"
|
|
6
6
|
import { tuple } from "effect-app/Function"
|
|
7
|
+
import * as Option from "effect-app/Option"
|
|
8
|
+
import { isReadonlyArrayNonEmpty } from "effect/Array"
|
|
9
|
+
import type * as Cause from "effect/Cause"
|
|
10
|
+
import * as Exit from "effect/Exit"
|
|
11
|
+
import * as Ref from "effect/Ref"
|
|
12
|
+
import * as Stream from "effect/Stream"
|
|
7
13
|
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
|
|
14
|
+
import type * as Reactivity from "effect/unstable/reactivity/Reactivity"
|
|
8
15
|
import { computed, type ComputedRef, shallowRef } from "vue"
|
|
9
|
-
import {
|
|
16
|
+
import { invalidateAndAwait } from "./atomQuery.ts"
|
|
17
|
+
import { getDerivedInvalidationKeys } from "./dependencyMetadata.ts"
|
|
10
18
|
|
|
11
|
-
export
|
|
19
|
+
export type GetQueryKey = (h: { id: string; options?: ClientForOptions }) => string[]
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Default heuristic: invalidate the parent namespace of the action.
|
|
23
|
+
* e.g. `$project/$configuration.get` -> `["$project"]`
|
|
24
|
+
* e.g. `$project/$configuration/$something.get` -> `["$project","$configuration"]`
|
|
25
|
+
*/
|
|
26
|
+
export const defaultGetQueryKey: GetQueryKey = (h) => {
|
|
12
27
|
const key = makeQueryKey(h)
|
|
13
28
|
const ns = key.filter((_) => _.startsWith("$"))
|
|
14
|
-
// we invalidate the parent namespace e.g $project/$configuration.get, we invalidate $project
|
|
15
|
-
// for $project/$configuration/$something.get, we invalidate $project/$configuration
|
|
16
29
|
const k = ns.length ? ns.length > 1 ? ns.slice(0, ns.length - 1) : ns : undefined
|
|
17
30
|
if (!k) throw new Error("empty query key for: " + h.id)
|
|
18
31
|
return k
|
|
19
32
|
}
|
|
20
33
|
|
|
34
|
+
let activeGetQueryKey: GetQueryKey = defaultGetQueryKey
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Override the default query-key heuristic used by mutations for cache
|
|
38
|
+
* invalidation. Call once at app bootstrap. Pass `undefined` to restore the
|
|
39
|
+
* built-in default.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* // invalidate the full namespace of the action (no parent collapse)
|
|
44
|
+
* setDefaultGetQueryKey((h) => {
|
|
45
|
+
* const key = makeQueryKey(h)
|
|
46
|
+
* const ns = key.filter((_) => _.startsWith("$"))
|
|
47
|
+
* if (!ns.length) throw new Error("empty query key for: " + h.id)
|
|
48
|
+
* return ns
|
|
49
|
+
* })
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export const setDefaultGetQueryKey = (fn: GetQueryKey | undefined) => {
|
|
53
|
+
activeGetQueryKey = fn ?? defaultGetQueryKey
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const getQueryKey: GetQueryKey = (h) => activeGetQueryKey(h)
|
|
57
|
+
|
|
21
58
|
export function mutationResultToVue<A, E>(
|
|
22
59
|
mutationResult: AsyncResult.AsyncResult<A, E>
|
|
23
60
|
): Res<A, E> {
|
|
@@ -67,197 +104,378 @@ export function make<A, E, R>(self: Effect.Effect<A, E, R>) {
|
|
|
67
104
|
return tuple(result, latestSuccess, execute)
|
|
68
105
|
}
|
|
69
106
|
|
|
70
|
-
|
|
107
|
+
/**
|
|
108
|
+
* An entry for `queryInvalidation`: a raw query key, an `{ id }` handler ref, or a
|
|
109
|
+
* `{ filters }` shape. The atom engine acts on entries that carry a concrete query
|
|
110
|
+
* key: raw arrays, handler refs, or `{ filters: { queryKey } }`. Predicate-only
|
|
111
|
+
* filters have no exact-key reactivity equivalent and fail fast.
|
|
112
|
+
*/
|
|
113
|
+
export type QueryKeyInvalidationFilters = {
|
|
114
|
+
readonly queryKey: ReadonlyArray<unknown>
|
|
115
|
+
}
|
|
116
|
+
export type InvalidationEntry = InvalidateQueryInstruction<QueryKeyInvalidationFilters>
|
|
117
|
+
export type QueryInvalidationEffect<R = never> = (
|
|
118
|
+
keys: ReadonlyArray<ReadonlyArray<unknown>>
|
|
119
|
+
) => Effect.Effect<void, never, R>
|
|
120
|
+
export interface QueryInvalidator<R = never> {
|
|
121
|
+
readonly invalidateAndAwait: QueryInvalidationEffect<R>
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const atomQueryInvalidator: QueryInvalidator<Reactivity.Reactivity> = {
|
|
125
|
+
invalidateAndAwait
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export const combineQueryInvalidators = <R>(
|
|
129
|
+
...invalidators: ReadonlyArray<QueryInvalidator<R>>
|
|
130
|
+
): QueryInvalidator<R> => ({
|
|
131
|
+
invalidateAndAwait: (keys) =>
|
|
132
|
+
Effect.forEach(
|
|
133
|
+
invalidators,
|
|
134
|
+
(invalidator) => invalidator.invalidateAndAwait(keys),
|
|
135
|
+
{ discard: true, concurrency: "inherit" }
|
|
136
|
+
)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
const isRecord = (value: unknown): value is { readonly [key: string]: unknown } =>
|
|
140
|
+
typeof value === "object" && value !== null
|
|
141
|
+
|
|
142
|
+
const isQueryKey = (entry: InvalidationEntry): entry is ReadonlyArray<string> => Array.isArray(entry)
|
|
143
|
+
|
|
144
|
+
const queryKeyFromFilters = (
|
|
145
|
+
entry: Exclude<InvalidationEntry, ReadonlyArray<string>>
|
|
146
|
+
): ReadonlyArray<unknown> | undefined => {
|
|
147
|
+
if (!("filters" in entry)) return undefined
|
|
148
|
+
const filters = entry.filters
|
|
149
|
+
if (!isRecord(filters)) return undefined
|
|
150
|
+
const queryKey = filters["queryKey"]
|
|
151
|
+
return Array.isArray(queryKey) ? queryKey : undefined
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface MutationOptionsBase<A = unknown, B = A, E2 = never, R2 = never> {
|
|
71
155
|
/**
|
|
72
156
|
* By default we invalidate one level of the query key, e.g $project/$configuration.get, we invalidate $project.
|
|
73
|
-
* This can be overridden by providing a function that returns an array of filters and options
|
|
157
|
+
* This can be overridden by providing a function that returns an array of filters and options,
|
|
158
|
+
* or RPC handlers directly (their query keys are derived automatically).
|
|
159
|
+
*
|
|
160
|
+
* @example
|
|
161
|
+
* ```ts
|
|
162
|
+
* queryInvalidation: (queryKey) => [
|
|
163
|
+
* { filters: { queryKey } },
|
|
164
|
+
* GetMe,
|
|
165
|
+
* PackListIndex
|
|
166
|
+
* ]
|
|
167
|
+
* ```
|
|
74
168
|
*/
|
|
75
|
-
queryInvalidation?: (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
/** @deprecated prefer more basic @see MutationOptionsBase and separate useMutation from Command.fn */
|
|
82
|
-
export interface MutationOptions<A, E, R, A2 = A, E2 = E, R2 = R, I = void> extends MutationOptionsBase {
|
|
169
|
+
queryInvalidation?: (
|
|
170
|
+
defaultKey: string[],
|
|
171
|
+
name: string,
|
|
172
|
+
input?: unknown,
|
|
173
|
+
output?: Exit.Exit<unknown, unknown>
|
|
174
|
+
) => InvalidationEntry[]
|
|
83
175
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
176
|
+
* Run an additional Effect after the mutation succeeds. Its output becomes the
|
|
177
|
+
* final result returned to the caller. Query cache is invalidated once on
|
|
178
|
+
* mutation exit and again after this Effect completes. Useful for long-running
|
|
179
|
+
* operations (e.g. polling a background job) where you want the caller to
|
|
180
|
+
* receive the downstream result and the cache to refresh once it is ready.
|
|
86
181
|
*
|
|
87
|
-
* @
|
|
182
|
+
* @example
|
|
183
|
+
* ```ts
|
|
184
|
+
* useMutation(startExportCommand, {
|
|
185
|
+
* select: (result) => pollUntilDone(result.jobId)
|
|
186
|
+
* // caller receives the pollUntilDone output, not the original result
|
|
187
|
+
* })
|
|
188
|
+
* ```
|
|
88
189
|
*/
|
|
89
|
-
|
|
190
|
+
select?: (result: A) => Effect.Effect<B, E2, R2>
|
|
90
191
|
}
|
|
91
192
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
// for (let i = 0; i < ns.length; i++) {
|
|
99
|
-
// nses.push(ns.slice(0, i + 1).join("/"))
|
|
100
|
-
// }
|
|
101
|
-
*/
|
|
102
|
-
|
|
103
|
-
export const asResult: {
|
|
104
|
-
<A, E, R>(
|
|
105
|
-
handler: Effect.Effect<A, E, R>
|
|
106
|
-
): readonly [ComputedRef<AsyncResult.AsyncResult<A, E>>, Effect.Effect<Exit.Exit<A, E>, never, R>]
|
|
107
|
-
<Args extends readonly any[], A, E, R>(
|
|
108
|
-
handler: (...args: Args) => Effect.Effect<A, E, R>
|
|
109
|
-
): readonly [ComputedRef<AsyncResult.AsyncResult<A, E>>, (...args: Args) => Effect.Effect<Exit.Exit<A, E>, never, R>]
|
|
110
|
-
} = <Args extends readonly any[], A, E, R>(
|
|
111
|
-
handler: Effect.Effect<A, E, R> | ((...args: Args) => Effect.Effect<A, E, R>)
|
|
112
|
-
) => {
|
|
193
|
+
export const asResult = <Args extends readonly any[], A, E, R>(
|
|
194
|
+
handler: (...args: Args) => Effect.Effect<A, E, R>
|
|
195
|
+
): readonly [
|
|
196
|
+
ComputedRef<AsyncResult.AsyncResult<A, E>>,
|
|
197
|
+
(...args: Args) => Effect.Effect<Exit.Exit<A, E>, never, R>
|
|
198
|
+
] => {
|
|
113
199
|
const state = shallowRef<AsyncResult.AsyncResult<A, E>>(AsyncResult.initial())
|
|
114
200
|
|
|
115
|
-
const act =
|
|
116
|
-
|
|
201
|
+
const act = (...args: Args) =>
|
|
202
|
+
Effect
|
|
117
203
|
.sync(() => {
|
|
118
204
|
state.value = AsyncResult.initial(true)
|
|
119
205
|
})
|
|
120
206
|
.pipe(
|
|
121
207
|
Effect.andThen(Effect.suspend(() =>
|
|
122
|
-
handler.pipe(
|
|
208
|
+
handler(...args).pipe(
|
|
123
209
|
Effect.exit,
|
|
124
210
|
Effect.tap((exit) => Effect.sync(() => (state.value = AsyncResult.fromExit(exit))))
|
|
125
211
|
)
|
|
126
212
|
))
|
|
127
213
|
)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
214
|
+
|
|
215
|
+
return tuple(computed(() => state.value), act) as any
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Like `asResult`, but for streams. The ref is updated with each emitted value
|
|
220
|
+
* (keeping `waiting: true`) and is finalised (with `waiting: false`) once the
|
|
221
|
+
* stream terminates successfully. Errors are surfaced as `AsyncResult.failure`.
|
|
222
|
+
*/
|
|
223
|
+
export const asStreamResult = <Args extends readonly any[], A, E, R>(
|
|
224
|
+
handler: (...args: Args) => Stream.Stream<A, E, R>
|
|
225
|
+
): readonly [ComputedRef<AsyncResult.AsyncResult<A, E>>, (...args: Args) => Effect.Effect<void, never, R>] => {
|
|
226
|
+
const state = shallowRef<AsyncResult.AsyncResult<A, E>>(AsyncResult.initial())
|
|
227
|
+
|
|
228
|
+
const runStream = (stream: Stream.Stream<A, E, R>): Effect.Effect<void, never, R> =>
|
|
229
|
+
Effect
|
|
230
|
+
.sync(() => {
|
|
231
|
+
state.value = AsyncResult.initial(true)
|
|
232
|
+
})
|
|
233
|
+
.pipe(
|
|
234
|
+
Effect.andThen(
|
|
235
|
+
stream.pipe(
|
|
236
|
+
Stream.runForEach((value) =>
|
|
237
|
+
Effect.sync(() => {
|
|
238
|
+
state.value = AsyncResult.success(value, { waiting: true })
|
|
239
|
+
})
|
|
240
|
+
),
|
|
241
|
+
Effect.exit,
|
|
242
|
+
Effect.flatMap((exit) =>
|
|
243
|
+
Effect.sync(() => {
|
|
244
|
+
if (exit._tag === "Success") {
|
|
245
|
+
const current = state.value
|
|
246
|
+
if (AsyncResult.isSuccess(current)) {
|
|
247
|
+
state.value = AsyncResult.success(current.value, { waiting: false })
|
|
248
|
+
} else {
|
|
249
|
+
state.value = AsyncResult.initial(false)
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
state.value = AsyncResult.failure(exit.cause)
|
|
253
|
+
}
|
|
254
|
+
})
|
|
138
255
|
)
|
|
139
|
-
)
|
|
256
|
+
)
|
|
140
257
|
)
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
const act = (...args: Args) => runStream(handler(...args))
|
|
141
261
|
|
|
142
262
|
return tuple(computed(() => state.value), act) as any
|
|
143
263
|
}
|
|
144
264
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
265
|
+
const buildInvalidateCache = <RInvalidator>(
|
|
266
|
+
self: { id: string; options?: ClientForOptions; disableQueryInvalidation?: boolean },
|
|
267
|
+
queryInvalidation: MutationOptionsBase["queryInvalidation"] | undefined,
|
|
268
|
+
queryInvalidator: QueryInvalidator<RInvalidator>
|
|
149
269
|
) => {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
Effect.promise(() => queryClient.invalidateQueries(filters, { ...options, updateMeta: { span } }))
|
|
158
|
-
)
|
|
159
|
-
)
|
|
160
|
-
|
|
161
|
-
const invalidateCache = Effect.suspend(() => {
|
|
270
|
+
// Concrete reactivity keys to invalidate: a raw query key, one derived from an `{ id }`
|
|
271
|
+
// entry, or a compatibility `{ filters: { queryKey } }` entry.
|
|
272
|
+
// Predicate-only `{ filters }` entries have no exact-key reactivity equivalent and throw.
|
|
273
|
+
const getClientInvalidationKeys = (
|
|
274
|
+
input: unknown,
|
|
275
|
+
output: Exit.Exit<unknown, unknown>
|
|
276
|
+
): ReadonlyArray<ReadonlyArray<unknown>> => {
|
|
162
277
|
const queryKey = getQueryKey(self)
|
|
163
278
|
|
|
164
|
-
if (
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
279
|
+
if (queryInvalidation) {
|
|
280
|
+
const keys: Array<ReadonlyArray<unknown>> = []
|
|
281
|
+
for (const entry of queryInvalidation(queryKey, self.id, input, output)) {
|
|
282
|
+
if (isQueryKey(entry)) {
|
|
283
|
+
keys.push(entry)
|
|
284
|
+
continue
|
|
285
|
+
}
|
|
286
|
+
if ("id" in entry) {
|
|
287
|
+
keys.push(makeQueryKey(entry.options ? { id: entry.id, options: entry.options } : { id: entry.id }))
|
|
288
|
+
continue
|
|
289
|
+
}
|
|
290
|
+
const filterQueryKey = queryKeyFromFilters(entry)
|
|
291
|
+
if (filterQueryKey !== undefined) {
|
|
292
|
+
keys.push(filterQueryKey)
|
|
293
|
+
continue
|
|
294
|
+
}
|
|
295
|
+
throw new Error("Unsupported query invalidation filter: only filters.queryKey is supported")
|
|
168
296
|
}
|
|
297
|
+
return keys
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// No manual `invalidatesQueries`: contribute nothing. Invalidation rides entirely on the
|
|
301
|
+
// repository-derived write-dependencies (plus any server-provided keys) — there is no default
|
|
302
|
+
// namespace invalidation of the command's own resource.
|
|
303
|
+
return []
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const invalidateCache = (
|
|
307
|
+
input: unknown,
|
|
308
|
+
output: Exit.Exit<unknown, unknown>,
|
|
309
|
+
serverKeys: ReadonlyArray<InvalidationKey>,
|
|
310
|
+
writeDependencies: DataDependencies.DataDependencies = DataDependencies.empty()
|
|
311
|
+
) =>
|
|
312
|
+
Effect.suspend(() => {
|
|
313
|
+
if (self.disableQueryInvalidation) return Effect.void
|
|
314
|
+
|
|
315
|
+
const clientKeys = getClientInvalidationKeys(input, output)
|
|
316
|
+
// Derive extra reactivity keys from repository write-dependencies: every live query whose
|
|
317
|
+
// recorded read-dependencies intersect this mutation's writes must be refreshed.
|
|
318
|
+
const derivedKeys = getDerivedInvalidationKeys(writeDependencies)
|
|
319
|
+
// Invalidate exact reactivity keys (= the prefixes query atoms register under). Each key
|
|
320
|
+
// array is hashed structurally, matching the query-side registration.
|
|
321
|
+
const keys: ReadonlyArray<ReadonlyArray<unknown>> = [...clientKeys, ...serverKeys, ...derivedKeys]
|
|
322
|
+
|
|
323
|
+
if (!isReadonlyArrayNonEmpty(keys)) return Effect.void
|
|
324
|
+
|
|
169
325
|
return Effect
|
|
170
326
|
.andThen(
|
|
171
|
-
Effect.annotateCurrentSpan({
|
|
172
|
-
|
|
327
|
+
Effect.annotateCurrentSpan({
|
|
328
|
+
keys,
|
|
329
|
+
clientKeys,
|
|
330
|
+
serverKeys,
|
|
331
|
+
derivedKeys,
|
|
332
|
+
writeDependencies: [...writeDependencies]
|
|
333
|
+
}),
|
|
334
|
+
// refetch + AWAIT every live query registered under these keys, so by the time the
|
|
335
|
+
// mutation resolves the affected queries are fresh.
|
|
336
|
+
queryInvalidator.invalidateAndAwait(keys)
|
|
173
337
|
)
|
|
174
|
-
.pipe(
|
|
175
|
-
|
|
338
|
+
.pipe(
|
|
339
|
+
Effect.tap(Effect.sleep(0.1)), // allow for refs to update etc
|
|
340
|
+
Effect.withSpan("client.query.invalidation", {}, { captureStackTrace: false })
|
|
341
|
+
)
|
|
342
|
+
})
|
|
176
343
|
|
|
177
|
-
|
|
344
|
+
return invalidateCache
|
|
345
|
+
}
|
|
178
346
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
// TODO: should we do this in general on any mutation, regardless of invalidation?
|
|
188
|
-
Effect.sleep(0)
|
|
189
|
-
),
|
|
190
|
-
Effect.withSpan("client.query.invalidation", {}, { captureStackTrace: false })
|
|
191
|
-
)
|
|
192
|
-
})
|
|
347
|
+
export const invalidateQueries = <RInvalidator>(
|
|
348
|
+
self: { id: string; options?: ClientForOptions; disableQueryInvalidation?: boolean },
|
|
349
|
+
options: MutationOptionsBase | undefined,
|
|
350
|
+
queryInvalidator: QueryInvalidator<RInvalidator>
|
|
351
|
+
) => {
|
|
352
|
+
const invalidateCache = buildInvalidateCache(self, options?.queryInvalidation, queryInvalidator)
|
|
353
|
+
|
|
354
|
+
const select = options?.select
|
|
193
355
|
|
|
194
|
-
const handle = <A, E, R>(
|
|
356
|
+
const handle = <A, E, R>(eff: Effect.Effect<A, E, R>, input?: unknown) =>
|
|
357
|
+
Effect.gen(function*() {
|
|
358
|
+
const keysRef = yield* Ref.make<ReadonlyArray<InvalidationKey>>([])
|
|
359
|
+
const readsRef = yield* Ref.make(DataDependencies.empty())
|
|
360
|
+
const writesRef = yield* Ref.make(DataDependencies.empty())
|
|
361
|
+
const dependencyRecorder = DataDependencies.makeDataDependencyRecorder(readsRef, writesRef)
|
|
362
|
+
const result = yield* eff.pipe(
|
|
363
|
+
Effect.provideService(InvalidationKeysFromServer, makeInvalidationKeysService(keysRef)),
|
|
364
|
+
Effect.provideService(DataDependencies.DataDependencyRecorder, dependencyRecorder),
|
|
365
|
+
Effect.onExit((exit) =>
|
|
366
|
+
Effect.gen(function*() {
|
|
367
|
+
const serverKeys = yield* Ref.get(keysRef)
|
|
368
|
+
const writeDependencies = yield* Ref.get(writesRef)
|
|
369
|
+
yield* invalidateCache(input, exit, serverKeys, writeDependencies)
|
|
370
|
+
})
|
|
371
|
+
)
|
|
372
|
+
)
|
|
373
|
+
if (select) {
|
|
374
|
+
return yield* select(result).pipe(
|
|
375
|
+
Effect.onExit((exit) =>
|
|
376
|
+
Effect.gen(function*() {
|
|
377
|
+
const serverKeys = yield* Ref.get(keysRef)
|
|
378
|
+
const writeDependencies = yield* Ref.get(writesRef)
|
|
379
|
+
yield* invalidateCache(input, exit, serverKeys, writeDependencies)
|
|
380
|
+
})
|
|
381
|
+
)
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
return result
|
|
385
|
+
})
|
|
195
386
|
|
|
196
387
|
return handle
|
|
197
388
|
}
|
|
198
389
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
<
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Pass an Effect, e.g from a client action
|
|
211
|
-
* Executes query cache invalidation based on default rules or provided option.
|
|
212
|
-
*/
|
|
213
|
-
<E, A, R, Request extends Req, Id extends string>(
|
|
214
|
-
self: RequestHandler<A, E, R, Request, Id>,
|
|
215
|
-
options?: MutationOptionsBase
|
|
216
|
-
): Effect.Effect<A, E, R> & { readonly id: Id }
|
|
217
|
-
} = <I, E, A, R, Request extends Req, Id extends string>(
|
|
218
|
-
self: RequestHandlerWithInput<I, A, E, R, Request, Id> | RequestHandler<A, E, R, Request, Id>,
|
|
219
|
-
options?: MutationOptionsBase
|
|
220
|
-
) => {
|
|
221
|
-
const queryClient = useQueryClient()
|
|
222
|
-
const handle = invalidateQueries(queryClient, self, options?.queryInvalidation)
|
|
223
|
-
const handler = self.handler
|
|
224
|
-
const r = Effect.isEffect(handler) ? handle(handler) : (i: I) => handle(handler(i))
|
|
390
|
+
/**
|
|
391
|
+
* A callable mutation result. When `I = void` the input argument may be omitted.
|
|
392
|
+
*/
|
|
393
|
+
export interface MutationFn<I, A, E, R, Id extends string> {
|
|
394
|
+
<B = A, E2 = never, R2 = never>(
|
|
395
|
+
input: I,
|
|
396
|
+
options?: MutationOptionsBase<A, B, E2, R2>
|
|
397
|
+
): Effect.Effect<B, E | E2, R | R2>
|
|
398
|
+
readonly id: Id
|
|
399
|
+
}
|
|
225
400
|
|
|
401
|
+
export const makeMutation = <RInvalidator>(queryInvalidator: QueryInvalidator<RInvalidator>) => {
|
|
402
|
+
/**
|
|
403
|
+
* Pass a function that returns an Effect, e.g from a client action.
|
|
404
|
+
* Executes query cache invalidation based on default rules or provided option.
|
|
405
|
+
* When `I = void` the input argument may be omitted.
|
|
406
|
+
*/
|
|
407
|
+
const useMutation = <I, E, A, R, Request extends Req, Id extends string>(
|
|
408
|
+
self: RequestHandlerWithInput<I, A, E, R, Request, Id>
|
|
409
|
+
): MutationFn<I, A, E, R, Id> => {
|
|
410
|
+
const r = (i: I, options?: MutationOptionsBase) =>
|
|
411
|
+
invalidateQueries(self, options, queryInvalidator)(self.handler(i), i)
|
|
226
412
|
return Object.assign(r, { id: self.id }) as any
|
|
227
413
|
}
|
|
228
414
|
return useMutation
|
|
229
415
|
}
|
|
230
416
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
self
|
|
242
|
-
options?: MutationOptionsBase
|
|
243
|
-
): ((i: I) => Effect.Effect<A, E, R>) & { readonly id: Id }
|
|
244
|
-
/**
|
|
245
|
-
* Pass an Effect, e.g from a client action
|
|
246
|
-
* Executes query cache invalidation based on default rules or provided option.
|
|
247
|
-
*/
|
|
248
|
-
<E, A, R, Request extends Req, Id extends string>(
|
|
249
|
-
self: RequestHandler<A, E, R, Request, Id>,
|
|
250
|
-
options?: MutationOptionsBase
|
|
251
|
-
): Effect.Effect<A, E, R> & { readonly id: Id }
|
|
252
|
-
} = <I, E, A, R, Request extends Req, Id extends string>(
|
|
253
|
-
self: RequestHandlerWithInput<I, A, E, R, Request, Id> | RequestHandler<A, E, R, Request, Id>,
|
|
254
|
-
options?: MutationOptionsBase
|
|
255
|
-
) => {
|
|
256
|
-
const handle = invalidateQueries(queryClient, self, options?.queryInvalidation)
|
|
257
|
-
const handler = self.handler
|
|
258
|
-
const r = Effect.isEffect(handler) ? handle(handler) : (i: I) => handle(handler(i))
|
|
259
|
-
|
|
417
|
+
export const useMakeMutation = <RInvalidator>(queryInvalidator: QueryInvalidator<RInvalidator>) => {
|
|
418
|
+
/**
|
|
419
|
+
* Pass a function that returns an Effect, e.g from a client action.
|
|
420
|
+
* Executes query cache invalidation based on default rules or provided option.
|
|
421
|
+
* When `I = void` the input argument may be omitted.
|
|
422
|
+
*/
|
|
423
|
+
const useMutation = <I, E, A, R, Request extends Req, Id extends string>(
|
|
424
|
+
self: RequestHandlerWithInput<I, A, E, R, Request, Id>
|
|
425
|
+
): MutationFn<I, A, E, R, Id> => {
|
|
426
|
+
const r = (i: I, options?: MutationOptionsBase) =>
|
|
427
|
+
invalidateQueries(self, options, queryInvalidator)(self.handler(i), i)
|
|
260
428
|
return Object.assign(r, { id: self.id }) as any
|
|
261
429
|
}
|
|
262
430
|
return useMutation
|
|
263
431
|
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Returns a stream-based mutation factory for use with `streamFn`.
|
|
435
|
+
* The outer Effect sets up per-invocation invalidation scaffolding
|
|
436
|
+
* and returns a stream that triggers query invalidation via `Stream.ensuring` when it completes.
|
|
437
|
+
*
|
|
438
|
+
* Use with `streamFn` / `Command.streamFn(id)(mutateHandler, ...combinators)` so that
|
|
439
|
+
* the command manages its own reactive state internally.
|
|
440
|
+
*/
|
|
441
|
+
export const makeStreamMutation2 = <RInvalidator>(queryInvalidator: QueryInvalidator<RInvalidator>) => {
|
|
442
|
+
return (
|
|
443
|
+
self: {
|
|
444
|
+
id: string
|
|
445
|
+
options?: ClientForOptions
|
|
446
|
+
disableQueryInvalidation?: boolean
|
|
447
|
+
handler: (i: any) => Stream.Stream<any, any, any>
|
|
448
|
+
},
|
|
449
|
+
mergedInvalidation?: MutationOptionsBase["queryInvalidation"]
|
|
450
|
+
) => {
|
|
451
|
+
const invCache = buildInvalidateCache(self, mergedInvalidation, queryInvalidator)
|
|
452
|
+
|
|
453
|
+
const makeInvocationEffect = (input: unknown, source: Stream.Stream<any, any, any>) =>
|
|
454
|
+
Effect.gen(function*() {
|
|
455
|
+
const keysRef = yield* Ref.make<ReadonlyArray<InvalidationKey>>([])
|
|
456
|
+
// Stream metadata can arrive after every emitted RPC value. Accumulate its invalidation
|
|
457
|
+
// keys and flush them once from `ensuring`; invalidating from `add` would refetch live
|
|
458
|
+
// queries once per message and repeatedly cancel the preceding request.
|
|
459
|
+
const invKeys = makeInvalidationKeysService(keysRef)
|
|
460
|
+
const readsRef = yield* Ref.make(DataDependencies.empty())
|
|
461
|
+
const writesRef = yield* Ref.make(DataDependencies.empty())
|
|
462
|
+
const dependencyRecorder = DataDependencies.makeDataDependencyRecorder(readsRef, writesRef)
|
|
463
|
+
const lastRef = yield* Ref.make<any>(undefined)
|
|
464
|
+
return source.pipe(
|
|
465
|
+
Stream.provideService(InvalidationKeysFromServer, invKeys),
|
|
466
|
+
Stream.provideService(DataDependencies.DataDependencyRecorder, dependencyRecorder),
|
|
467
|
+
Stream.tap((v) => Ref.set(lastRef, v)),
|
|
468
|
+
Stream.ensuring(
|
|
469
|
+
Effect.gen(function*() {
|
|
470
|
+
const lastValue = yield* Ref.get(lastRef)
|
|
471
|
+
const serverKeys = yield* Ref.get(keysRef)
|
|
472
|
+
const writeDependencies = yield* Ref.get(writesRef)
|
|
473
|
+
yield* invCache(input, Exit.succeed(lastValue), serverKeys, writeDependencies)
|
|
474
|
+
})
|
|
475
|
+
)
|
|
476
|
+
)
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
return (i: any) => Stream.unwrap(makeInvocationEffect(i, self.handler(i)))
|
|
480
|
+
}
|
|
481
|
+
}
|