@effect-app/vue 4.0.0-beta.28 → 4.0.0-beta.281

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.
Files changed (125) hide show
  1. package/CHANGELOG.md +2073 -0
  2. package/dist/atomQuery.d.ts +105 -0
  3. package/dist/atomQuery.d.ts.map +1 -0
  4. package/dist/atomQuery.js +283 -0
  5. package/dist/commander.d.ts +634 -0
  6. package/dist/commander.d.ts.map +1 -0
  7. package/dist/commander.js +1070 -0
  8. package/dist/{experimental/confirm.d.ts → confirm.d.ts} +6 -4
  9. package/dist/confirm.d.ts.map +1 -0
  10. package/dist/confirm.js +26 -0
  11. package/dist/errorReporter.d.ts +6 -4
  12. package/dist/errorReporter.d.ts.map +1 -1
  13. package/dist/errorReporter.js +14 -19
  14. package/dist/form.d.ts +14 -5
  15. package/dist/form.d.ts.map +1 -1
  16. package/dist/form.js +46 -13
  17. package/dist/internal/tanstackQuery.d.ts +9 -0
  18. package/dist/internal/tanstackQuery.d.ts.map +1 -0
  19. package/dist/internal/tanstackQuery.js +163 -0
  20. package/dist/intl.d.ts +15 -0
  21. package/dist/intl.d.ts.map +1 -0
  22. package/dist/intl.js +9 -0
  23. package/dist/lib.d.ts +7 -9
  24. package/dist/lib.d.ts.map +1 -1
  25. package/dist/lib.js +35 -10
  26. package/dist/makeClient.d.ts +212 -336
  27. package/dist/makeClient.d.ts.map +1 -1
  28. package/dist/makeClient.js +399 -411
  29. package/dist/makeContext.d.ts.map +1 -1
  30. package/dist/makeIntl.d.ts.map +1 -1
  31. package/dist/{experimental/makeUseCommand.d.ts → makeUseCommand.d.ts} +4 -3
  32. package/dist/makeUseCommand.d.ts.map +1 -0
  33. package/dist/makeUseCommand.js +13 -0
  34. package/dist/mutate.d.ts +106 -40
  35. package/dist/mutate.d.ts.map +1 -1
  36. package/dist/mutate.js +187 -58
  37. package/dist/query.d.ts +139 -45
  38. package/dist/query.d.ts.map +1 -1
  39. package/dist/query.js +330 -122
  40. package/dist/routeParams.d.ts +4 -4
  41. package/dist/routeParams.d.ts.map +1 -1
  42. package/dist/routeParams.js +4 -3
  43. package/dist/runtime.d.ts +1 -14
  44. package/dist/runtime.d.ts.map +1 -1
  45. package/dist/runtime.js +2 -26
  46. package/dist/suspense.d.ts +5 -0
  47. package/dist/suspense.d.ts.map +1 -0
  48. package/dist/suspense.js +12 -0
  49. package/dist/toast.d.ts +2 -0
  50. package/dist/toast.d.ts.map +1 -0
  51. package/dist/toast.js +2 -0
  52. package/dist/withToast.d.ts +2 -0
  53. package/dist/withToast.d.ts.map +1 -0
  54. package/dist/withToast.js +2 -0
  55. package/docs/atom-query-api-redesign.md +207 -0
  56. package/examples/streamMutation.ts +72 -0
  57. package/package.json +30 -94
  58. package/src/atomQuery.ts +381 -0
  59. package/src/commander.ts +3413 -0
  60. package/src/{experimental/confirm.ts → confirm.ts} +13 -15
  61. package/src/errorReporter.ts +65 -75
  62. package/src/form.ts +61 -18
  63. package/src/index.ts +7 -7
  64. package/src/internal/tanstackQuery.ts +250 -0
  65. package/src/intl.ts +12 -0
  66. package/src/lib.ts +49 -21
  67. package/src/makeClient.ts +941 -1159
  68. package/src/makeIntl.ts +2 -2
  69. package/src/{experimental/makeUseCommand.ts → makeUseCommand.ts} +9 -6
  70. package/src/mutate.ts +343 -151
  71. package/src/query.ts +688 -260
  72. package/src/routeParams.ts +7 -7
  73. package/src/runtime.ts +1 -31
  74. package/src/suspense.ts +17 -0
  75. package/src/toast.ts +1 -0
  76. package/src/withToast.ts +1 -0
  77. package/test/Mutation.test.ts +181 -24
  78. package/test/dist/form.test.d.ts.map +1 -1
  79. package/test/dist/lib.test.d.ts.map +1 -0
  80. package/test/dist/patchableQueryAtom.test.d.ts.map +1 -0
  81. package/test/dist/queryOptions.test.d.ts.map +1 -0
  82. package/test/dist/retrySchedule.test.d.ts.map +1 -0
  83. package/test/dist/streamFinal.test.d.ts.map +1 -0
  84. package/test/dist/streamFn.test.d.ts.map +1 -0
  85. package/test/dist/stubs.d.ts +3697 -122
  86. package/test/dist/stubs.d.ts.map +1 -1
  87. package/test/dist/stubs.js +192 -32
  88. package/test/dist/suspense-regression.test.d.ts.map +1 -0
  89. package/test/dist/suspense.test.d.ts.map +1 -0
  90. package/test/form-validation-errors.test.ts +25 -20
  91. package/test/form.test.ts +22 -3
  92. package/test/lib.test.ts +240 -0
  93. package/test/makeClient.test.ts +437 -38
  94. package/test/streamFinal.test.ts +64 -0
  95. package/test/streamFn.test.ts +457 -0
  96. package/test/stubs.ts +228 -43
  97. package/test/suspense-regression.test.ts +115 -0
  98. package/test/suspense.test.ts +160 -0
  99. package/tsconfig.examples.json +20 -0
  100. package/tsconfig.json +3 -1
  101. package/tsconfig.src.json +34 -34
  102. package/tsconfig.test.json +2 -2
  103. package/vitest.config.ts +5 -5
  104. package/dist/experimental/commander.d.ts +0 -359
  105. package/dist/experimental/commander.d.ts.map +0 -1
  106. package/dist/experimental/commander.js +0 -557
  107. package/dist/experimental/confirm.d.ts.map +0 -1
  108. package/dist/experimental/confirm.js +0 -28
  109. package/dist/experimental/intl.d.ts +0 -16
  110. package/dist/experimental/intl.d.ts.map +0 -1
  111. package/dist/experimental/intl.js +0 -5
  112. package/dist/experimental/makeUseCommand.d.ts.map +0 -1
  113. package/dist/experimental/makeUseCommand.js +0 -13
  114. package/dist/experimental/toast.d.ts +0 -47
  115. package/dist/experimental/toast.d.ts.map +0 -1
  116. package/dist/experimental/toast.js +0 -41
  117. package/dist/experimental/withToast.d.ts +0 -25
  118. package/dist/experimental/withToast.d.ts.map +0 -1
  119. package/dist/experimental/withToast.js +0 -45
  120. package/eslint.config.mjs +0 -24
  121. package/src/experimental/commander.ts +0 -1835
  122. package/src/experimental/intl.ts +0 -9
  123. package/src/experimental/toast.ts +0 -66
  124. package/src/experimental/withToast.ts +0 -99
  125. package/tsconfig.json.bak +0 -12
@@ -0,0 +1,250 @@
1
+ import { injectRegistry } from "@effect/atom-vue"
2
+ import { QueryClient, useQuery as useTanstackQuery } from "@tanstack/vue-query"
3
+ import { makeQueryKey, type Req } from "effect-app/client"
4
+ import type { RequestHandlerWithInput } from "effect-app/client/clientFor"
5
+ import { CauseException, ServiceUnavailableError } from "effect-app/client/errors"
6
+ import type * as Context from "effect-app/Context"
7
+ import * as Effect from "effect-app/Effect"
8
+ import * as Option from "effect-app/Option"
9
+ import * as S from "effect-app/Schema"
10
+ import * as Cause from "effect/Cause"
11
+ import { isHttpClientError } from "effect/unstable/http/HttpClientError"
12
+ import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
13
+ import * as Atom from "effect/unstable/reactivity/Atom"
14
+ import { computed, type MaybeRefOrGetter, shallowRef, toValue, watch, type WatchSource } from "vue"
15
+ import { replaceEqualDeep } from "../atomQuery.ts"
16
+ import { reportRuntimeError } from "../lib.ts"
17
+ import type { QueryInvalidator } from "../mutate.ts"
18
+ import type { CustomDefinedInitialQueryOptions, CustomDefinedPlaceholderQueryOptions, CustomUndefinedInitialQueryOptions, CustomUseQueryOptions, MakeQuery2, QueryCacheUpdater, QueryHandle, QueryObserverResult, RefetchOptions } from "../query.ts"
19
+ import { makeRunPromise } from "../runtime.ts"
20
+
21
+ const swrToQuery = <E, A>(r: {
22
+ readonly error: CauseException<E> | null | undefined
23
+ readonly data: A | undefined
24
+ readonly isValidating: boolean
25
+ }): AsyncResult.AsyncResult<A, E> => {
26
+ if (r.error !== undefined && r.error !== null) {
27
+ return AsyncResult.failureWithPrevious(
28
+ r.error.originalCause,
29
+ {
30
+ previous: r.data === undefined ? Option.none() : Option.some(AsyncResult.success(r.data)),
31
+ waiting: r.isValidating
32
+ }
33
+ )
34
+ }
35
+ if (r.data !== undefined) {
36
+ return AsyncResult.success<A, E>(r.data, { waiting: r.isValidating })
37
+ }
38
+
39
+ return AsyncResult.initial(r.isValidating)
40
+ }
41
+
42
+ const recoverCauseException = <A, E>(error: unknown): Effect.Effect<A, E> =>
43
+ error instanceof CauseException
44
+ ? Effect.failCause(error.originalCause)
45
+ : Effect.die(error)
46
+
47
+ const isRetryable = (error: unknown) => {
48
+ if (error instanceof CauseException) {
49
+ return isHttpClientError(error.cause) || S.is(ServiceUnavailableError)(error.cause)
50
+ }
51
+ return false
52
+ }
53
+
54
+ const isInputOption = <I>(value: I | Option.Option<I> | undefined): value is Option.Option<I> => Option.isOption(value)
55
+
56
+ const resolveInput = <I>(
57
+ arg: I | WatchSource<I> | undefined | WatchSource<Option.Option<I>>,
58
+ mode: "optional" | undefined
59
+ ): I | undefined => {
60
+ if (mode === "optional") {
61
+ const option = toValue(arg)
62
+ return isInputOption(option) && Option.isSome(option) ? option.value : undefined
63
+ }
64
+ const value = toValue(arg)
65
+ return isInputOption(value) ? undefined : value
66
+ }
67
+
68
+ const resolveEnabled = <I>(
69
+ arg: I | WatchSource<I> | undefined | WatchSource<Option.Option<I>>,
70
+ options: {
71
+ readonly mode?: "optional" | undefined
72
+ readonly enabled?: MaybeRefOrGetter<boolean | undefined> | undefined
73
+ } | undefined
74
+ ) => {
75
+ if (options?.mode === "optional") {
76
+ return computed(() => {
77
+ const option = toValue(arg)
78
+ return Option.isSome(option)
79
+ })
80
+ }
81
+ return computed(() => {
82
+ const enabled = options?.enabled
83
+ if (enabled === undefined) return true
84
+ return !!toValue(enabled)
85
+ })
86
+ }
87
+
88
+ type LegacyTanstackOptions<A, E, TData> =
89
+ & (
90
+ | CustomUndefinedInitialQueryOptions<A, E, TData>
91
+ | CustomDefinedInitialQueryOptions<A, E, TData>
92
+ | CustomDefinedPlaceholderQueryOptions<A, E, TData>
93
+ | CustomUseQueryOptions<A, E, TData>
94
+ )
95
+ & { readonly mode?: "optional" | undefined }
96
+
97
+ export const makeTanstackQueryClient = () => new QueryClient()
98
+
99
+ export const makeTanstackQueryInvalidator = (queryClient: QueryClient): QueryInvalidator => ({
100
+ invalidateAndAwait: (keys) =>
101
+ Effect.forEach(
102
+ keys,
103
+ (queryKey) => Effect.promise(() => queryClient.invalidateQueries({ queryKey })),
104
+ { discard: true, concurrency: "inherit" }
105
+ )
106
+ })
107
+
108
+ const fullQueryKey = (
109
+ q: { readonly queryKeyProjectionHash?: string },
110
+ queryKey: ReadonlyArray<unknown>,
111
+ input: unknown
112
+ ) => q.queryKeyProjectionHash === undefined ? [...queryKey, input] : [...queryKey, q.queryKeyProjectionHash, input]
113
+
114
+ export const makeTanstackQueryCacheUpdater = (queryClient: QueryClient): QueryCacheUpdater => ({
115
+ update: <I, A, E, R, Request extends Req, Name extends string>(
116
+ _registry: ReturnType<typeof injectRegistry>,
117
+ query: RequestHandlerWithInput<I, A, E, R, Request, Name>,
118
+ input: I,
119
+ updater: (data: NoInfer<A>) => NoInfer<A>
120
+ ) => {
121
+ const queryKey = fullQueryKey(query, makeQueryKey(query), input)
122
+ if (queryClient.getQueryData(queryKey) === undefined) {
123
+ console.warn(`Query ${query.id} has not been used yet; nothing to update`)
124
+ return
125
+ }
126
+ queryClient.setQueryData(queryKey, (data: A | undefined) => data === undefined ? data : updater(data))
127
+ }
128
+ })
129
+
130
+ export const makeTanstackQuery = <R>(
131
+ getRuntime: () => Context.Context<R>,
132
+ queryClient: QueryClient
133
+ ): MakeQuery2<R> => {
134
+ const useQuery: MakeQuery2<R> = <I, A, E, Request extends Req, Name extends string>(
135
+ q: RequestHandlerWithInput<I, A, E, R, Request, Name>
136
+ ) =>
137
+ <TData = A>(
138
+ arg: I | WatchSource<I> | undefined | WatchSource<Option.Option<I>>,
139
+ options?: LegacyTanstackOptions<A, CauseException<E>, TData>
140
+ ) => {
141
+ const runPromise = makeRunPromise(getRuntime())
142
+ const queryKey = makeQueryKey(q)
143
+ const enabled = resolveEnabled(arg, options)
144
+ const structuralSharing = options?.structuralSharing === false ? false : replaceEqualDeep
145
+ const tanstackOptions = {
146
+ ...(options?.staleTime !== undefined ? { staleTime: options.staleTime } : {}),
147
+ ...(typeof options?.gcTime === "number" ? { gcTime: options.gcTime } : {}),
148
+ ...(options?.refetchOnWindowFocus !== undefined ? { refetchOnWindowFocus: options.refetchOnWindowFocus } : {}),
149
+ structuralSharing,
150
+ ...(options?.refetchInterval !== undefined ? { refetchInterval: options.refetchInterval } : {}),
151
+ ...(options?.select !== undefined ? { select: options.select } : {})
152
+ }
153
+ const tanstack = useTanstackQuery<A, CauseException<E>, TData>({
154
+ ...tanstackOptions,
155
+ enabled,
156
+ throwOnError: false,
157
+ retry: (retryCount: number, error: unknown) => isRetryable(error) && retryCount < 5,
158
+ queryKey: computed(() => {
159
+ const input = resolveInput(arg, options?.mode)
160
+ return fullQueryKey(q, queryKey, input)
161
+ }),
162
+ queryFn: ({ signal }: { readonly signal: AbortSignal }) =>
163
+ runPromise(
164
+ q
165
+ .handler(resolveInput(arg, options?.mode)!)
166
+ .pipe(
167
+ Effect.tapCauseIf(Cause.hasDies, (cause) => reportRuntimeError(cause)),
168
+ Effect.withSpan(`query ${q.id}`, {}, { captureStackTrace: false })
169
+ ),
170
+ { signal }
171
+ )
172
+ }, queryClient)
173
+
174
+ const latestSuccess = shallowRef<TData>()
175
+ const result = computed((): AsyncResult.AsyncResult<TData, E> =>
176
+ swrToQuery({
177
+ error: tanstack.error.value,
178
+ data: tanstack.data.value === undefined ? latestSuccess.value : tanstack.data.value,
179
+ isValidating: tanstack.isFetching.value
180
+ })
181
+ )
182
+ watch(result, (value) => latestSuccess.value = Option.getOrUndefined(AsyncResult.value(value)), { immediate: true })
183
+ const seedLatestSuccess = (data: TData) => {
184
+ if (latestSuccess.value === undefined) {
185
+ latestSuccess.value = data
186
+ }
187
+ }
188
+
189
+ const registry = injectRegistry()
190
+ const latestSuccessRef = computed(() => latestSuccess.value)
191
+ const atom = computed<Atom.Atom<AsyncResult.AsyncResult<TData, E>>>(() => Atom.readable(() => result.value))
192
+
193
+ const awaitResult = (): Effect.Effect<TData, E> =>
194
+ Effect
195
+ .tryPromise({
196
+ try: async () => {
197
+ const queryResult = await tanstack.suspense()
198
+ const data = queryResult.data
199
+ if (data === undefined) {
200
+ throw new Error("TanStack query resolved without data")
201
+ }
202
+ seedLatestSuccess(data)
203
+ return data
204
+ },
205
+ catch: (error) => error
206
+ })
207
+ .pipe(
208
+ Effect.catch((error) => recoverCauseException<TData, E>(error))
209
+ )
210
+
211
+ const refetch = (): Effect.Effect<TData, E> =>
212
+ Effect
213
+ .tryPromise({
214
+ try: async () => {
215
+ const queryResult = await tanstack.refetch({ throwOnError: true })
216
+ const data = queryResult.data
217
+ if (data === undefined) {
218
+ throw new Error("TanStack query refetched without data")
219
+ }
220
+ return data
221
+ },
222
+ catch: (error) => error
223
+ })
224
+ .pipe(
225
+ Effect.catch((error) => recoverCauseException<TData, E>(error))
226
+ )
227
+
228
+ const handle: QueryHandle<TData, E> = {
229
+ awaitResult,
230
+ refetch,
231
+ refresh: () => {
232
+ void tanstack.refetch()
233
+ },
234
+ registry,
235
+ atom
236
+ }
237
+
238
+ const fetch = (_options?: RefetchOptions): Effect.Effect<QueryObserverResult<TData, CauseException<E>>> =>
239
+ refetch().pipe(Effect.exit, Effect.map(AsyncResult.fromExit))
240
+
241
+ return [
242
+ result,
243
+ latestSuccessRef,
244
+ fetch,
245
+ handle
246
+ ] as const
247
+ }
248
+
249
+ return useQuery
250
+ }
package/src/intl.ts ADDED
@@ -0,0 +1,12 @@
1
+ import * as Context from "effect-app/Context"
2
+ import { accessCn, accessFn } from "effect-app/Context"
3
+ import { type MakeIntlReturn } from "./makeIntl.ts"
4
+
5
+ type I18nShape = ReturnType<MakeIntlReturn<string>["useIntl"]>
6
+
7
+ export class I18n extends Context.Opaque<I18n, I18nShape>()("I18n") {
8
+ static readonly locale = accessCn(this, "locale")
9
+ static readonly trans = accessFn(this, "trans")
10
+ static readonly formatMessage = accessFn(this, "formatMessage")
11
+ static readonly intl = accessCn(this, "intl")
12
+ }
package/src/lib.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  import { type Pausable, useIntervalFn, type UseIntervalFnOptions } from "@vueuse/core"
2
- import { Cause, type Effect, pipe } from "effect-app"
3
- import { type Req } from "effect-app/client"
4
- import type { ClientForOptions, RequestHandler, RequestHandlerWithInput } from "effect-app/client/clientFor"
2
+ import type { Req } from "effect-app/client"
3
+ import type { RequestHandlerWithInput } from "effect-app/client/clientFor"
4
+ import type * as Effect from "effect-app/Effect"
5
+ import * as Cause from "effect/Cause"
5
6
  import { isHttpClientError } from "effect/unstable/http/HttpClientError"
6
- import type { MaybeRefOrGetter } from "vue"
7
- import { reportError } from "./errorReporter.js"
7
+ import { isProxy, isReactive, isRef, type MaybeRefOrGetter, toRaw } from "vue"
8
+ import { reportError } from "./errorReporter.ts"
8
9
 
9
10
  export * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
10
11
 
@@ -31,15 +32,7 @@ const determineLevel = (cause: Cause.Cause<unknown>) => {
31
32
  export const reportRuntimeError = (cause: Cause.Cause<unknown>, extras?: Record<string, unknown>) =>
32
33
  reportRuntimeError_(cause, extras, determineLevel(cause))
33
34
 
34
- // $Project/$Configuration.Index
35
- // -> "$Project", "$Configuration", "Index"
36
- export const makeQueryKey = ({ id, options }: { id: string; options?: ClientForOptions }) =>
37
- pipe(
38
- id.split("/"),
39
- (split) => split.filter((_) => !options || !options?.skipQueryKey?.includes(_)).map((_) => "$" + _)
40
- )
41
- .join(".")
42
- .split(".")
35
+ export { makeQueryKey } from "effect-app/client"
43
36
 
44
37
  export function pauseWhileProcessing(
45
38
  iv: Pausable,
@@ -75,13 +68,48 @@ export const mapHandler: {
75
68
  self: RequestHandlerWithInput<I, A, E, R, Request, Name>,
76
69
  map: (handler: (i: I) => Effect.Effect<A, E, R>) => (i: I) => Effect.Effect<A2, E2, R2>
77
70
  ): RequestHandlerWithInput<I, A2, E2, R2, Request, Name>
78
- <E, A, R, E2, A2, R2, Request extends Req, Name extends string>(
79
- self: RequestHandler<A, E, R, Request, Name>,
80
- map: (handler: Effect.Effect<A, E, R>) => Effect.Effect<A2, E2, R2>
81
- ): RequestHandler<A2, E2, R2, Request, Name>
82
71
  } = (self: any, map: any): any => ({
83
72
  ...self,
84
- handler: typeof self.handler === "function"
85
- ? (i: any) => map(self.handler as (i: any) => Effect.Effect<any, any, any>)(i)
86
- : map(self.handler)
73
+ handler: (i: any) => map(self.handler as (i: any) => Effect.Effect<any, any, any>)(i)
87
74
  })
75
+
76
+ export function deepToRaw<T>(sourceObj: T): T {
77
+ const objectIterator = (input: any): any => {
78
+ if (isRef(input)) {
79
+ return objectIterator(input.value)
80
+ }
81
+
82
+ const rawInput = isReactive(input) || isProxy(input)
83
+ ? toRaw(input)
84
+ : input
85
+
86
+ if (Array.isArray(rawInput)) {
87
+ return rawInput.map((item) => objectIterator(item))
88
+ }
89
+
90
+ if (rawInput instanceof Map) {
91
+ return new Map(
92
+ Array.from(rawInput.entries(), ([key, value]) => [objectIterator(key), objectIterator(value)])
93
+ )
94
+ }
95
+
96
+ if (rawInput instanceof Set) {
97
+ return new Set(Array.from(rawInput.values(), (value) => objectIterator(value)))
98
+ }
99
+
100
+ if (rawInput instanceof Date) {
101
+ return new Date(rawInput)
102
+ }
103
+
104
+ if (rawInput && typeof rawInput === "object") {
105
+ return Object.keys(rawInput).reduce((acc, key) => {
106
+ acc[key] = objectIterator(rawInput[key])
107
+ return acc
108
+ }, {} as Record<string, unknown>)
109
+ }
110
+
111
+ return rawInput
112
+ }
113
+
114
+ return objectIterator(sourceObj)
115
+ }