@brickflow/http 0.0.15 → 0.0.17
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/README.md +249 -284
- package/dist/create-get.d.ts +12 -0
- package/dist/create-get.d.ts.map +1 -0
- package/dist/http.d.ts +26 -41
- package/dist/http.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +48 -21
- package/dist/index.mjs.map +1 -1
- package/dist/nuxt-BsZgB52H.js +233 -0
- package/dist/nuxt-BsZgB52H.js.map +1 -0
- package/dist/nuxt.d.ts +25 -42
- package/dist/nuxt.d.ts.map +1 -1
- package/dist/nuxt.mjs +2 -222
- package/package.json +10 -9
- package/src/app.d.ts +7 -0
- package/src/create-get.ts +43 -0
- package/src/http.ts +286 -0
- package/src/index.ts +3 -0
- package/src/nuxt.ts +354 -0
- package/src/utils.ts +50 -0
- package/dist/nuxt.mjs.map +0 -1
- package/dist/utils-DMSINHi5.js +0 -39
- package/dist/utils-DMSINHi5.js.map +0 -1
package/src/http.ts
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
export interface CreateHttpOptions {
|
|
2
|
+
arrayMode?: HttpArrayMode
|
|
3
|
+
baseURL: string
|
|
4
|
+
fetch?: typeof fetch
|
|
5
|
+
headers?: (() => HttpHeaders) | HttpHeaders
|
|
6
|
+
onResponse?: (response: HttpAnyResponse) => Promise<void> | void
|
|
7
|
+
timeout?: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type GetConfig<TKey extends HttpKey = HttpKey> = HttpConfigFull<TKey> & {
|
|
11
|
+
retry?: {
|
|
12
|
+
delay?: number
|
|
13
|
+
retries?: number
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type HttpAnyResponse = HttpResponse<HttpResponseData<HttpKey>, HttpKey, HttpConfigFull<HttpKey>>
|
|
18
|
+
|
|
19
|
+
export type HttpArrayMode = 'json' | 'repeat'
|
|
20
|
+
|
|
21
|
+
export type HttpClient = {
|
|
22
|
+
get: <T extends HttpKey, TConfig extends GetConfig<T> = GetConfig<T>>(
|
|
23
|
+
url: T,
|
|
24
|
+
config?: TConfig,
|
|
25
|
+
) => Promise<HttpResponse<HttpResponseData<T>, T, TConfig>>
|
|
26
|
+
post: <T extends HttpKey, TConfig extends PostConfig<T> = PostConfig<T>>(
|
|
27
|
+
url: T,
|
|
28
|
+
data?: FormData | Record<string, unknown>,
|
|
29
|
+
config?: TConfig,
|
|
30
|
+
) => Promise<HttpResponse<HttpResponseData<T>, T, TConfig>>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
34
|
+
export interface HttpConfig<TKey extends HttpKey = HttpKey> {}
|
|
35
|
+
export interface HttpEndpoint {}
|
|
36
|
+
export type HttpErrorGuard<TData = unknown, TError extends TData = TData> = (payload: TData) => payload is TError
|
|
37
|
+
|
|
38
|
+
export type HttpKey = Extract<keyof HttpSchema, string>
|
|
39
|
+
export type HttpParam = Record<string, boolean | number | string | string[] | undefined>
|
|
40
|
+
|
|
41
|
+
export interface HttpResponse<
|
|
42
|
+
T = unknown,
|
|
43
|
+
TKey extends HttpKey = HttpKey,
|
|
44
|
+
TConfig extends HttpConfigFull<TKey> = HttpConfigFull<TKey>,
|
|
45
|
+
> {
|
|
46
|
+
config: HttpResponseConfig<TKey, TConfig>
|
|
47
|
+
data: T
|
|
48
|
+
status: number
|
|
49
|
+
}
|
|
50
|
+
export type HttpResponseConfig<
|
|
51
|
+
TKey extends HttpKey = HttpKey,
|
|
52
|
+
TConfig extends HttpConfig<TKey> = HttpConfig<TKey>,
|
|
53
|
+
> = Omit<TConfig, 'signal'> & { url: string }
|
|
54
|
+
|
|
55
|
+
export type HttpResponseData<TKey extends HttpKey = HttpKey> = HttpSchema[TKey]
|
|
56
|
+
|
|
57
|
+
export type PostConfig<TKey extends HttpKey = HttpKey> = HttpConfigFull<TKey>
|
|
58
|
+
|
|
59
|
+
type HttpConfigFull<TKey extends HttpKey = HttpKey> = HttpConfig<TKey> & {
|
|
60
|
+
params?: HttpParam
|
|
61
|
+
signal?: AbortSignal | AbortSignal[]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type HttpHeaders = Record<string, string | undefined>
|
|
65
|
+
|
|
66
|
+
type HttpSchema = keyof HttpEndpoint extends never ? Record<string, unknown> : HttpEndpoint
|
|
67
|
+
|
|
68
|
+
export function createHttp(options: CreateHttpOptions): HttpClient {
|
|
69
|
+
const arrayMode = options.arrayMode ?? 'json'
|
|
70
|
+
const clientFetch = options.fetch ?? fetch
|
|
71
|
+
const timeout = options.timeout ?? 80000
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
get<T extends HttpKey, TConfig extends GetConfig<T> = GetConfig<T>>(url: T, config?: TConfig) {
|
|
75
|
+
const requestConfig = (config ?? {}) as TConfig
|
|
76
|
+
const requestUrl = `${joinUrl(options.baseURL, url)}${toQueryString(requestConfig.params, arrayMode)}`
|
|
77
|
+
const { delay = 300, retries = 3 } = requestConfig.retry ?? {}
|
|
78
|
+
|
|
79
|
+
const attemptRequest = async (attempt: number): Promise<HttpResponse<HttpResponseData<T>, T, TConfig>> => {
|
|
80
|
+
try {
|
|
81
|
+
const result = await clientFetch(requestUrl, {
|
|
82
|
+
credentials: 'include',
|
|
83
|
+
headers: resolveHeaders(options.headers),
|
|
84
|
+
method: 'GET',
|
|
85
|
+
signal: createSignal(requestConfig.signal, timeout),
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const parsedResult = await result.json()
|
|
89
|
+
const response: HttpResponse<HttpResponseData<T>, T, TConfig> = {
|
|
90
|
+
config: createResponseConfig<T, TConfig>(joinUrl(options.baseURL, url), requestConfig),
|
|
91
|
+
data: parsedResult,
|
|
92
|
+
status: result.status,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!result.ok && isRetryableStatus(result.status) && attempt < retries) {
|
|
96
|
+
await wait(getRetryDelay(attempt, delay))
|
|
97
|
+
return attemptRequest(attempt + 1)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
await options.onResponse?.(response as HttpAnyResponse)
|
|
101
|
+
return response
|
|
102
|
+
} catch (err) {
|
|
103
|
+
if (isAbortError(err) || attempt >= retries) {
|
|
104
|
+
throw err
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
await wait(getRetryDelay(attempt, delay))
|
|
108
|
+
return attemptRequest(attempt + 1)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return attemptRequest(0)
|
|
113
|
+
},
|
|
114
|
+
async post<T extends HttpKey, TConfig extends PostConfig<T> = PostConfig<T>>(
|
|
115
|
+
url: T,
|
|
116
|
+
data?: FormData | Record<string, unknown>,
|
|
117
|
+
config?: TConfig,
|
|
118
|
+
) {
|
|
119
|
+
const requestConfig = (config ?? {}) as TConfig
|
|
120
|
+
const isForm = isFormData(data)
|
|
121
|
+
const requestUrl = `${joinUrl(options.baseURL, url)}${toQueryString(requestConfig.params, arrayMode)}`
|
|
122
|
+
const response = await clientFetch(requestUrl, {
|
|
123
|
+
body: isForm || data === undefined ? data : JSON.stringify(data),
|
|
124
|
+
credentials: 'include',
|
|
125
|
+
headers: resolveHeaders(
|
|
126
|
+
options.headers,
|
|
127
|
+
data !== undefined && !isForm ? { 'Content-Type': 'application/json' } : undefined,
|
|
128
|
+
),
|
|
129
|
+
method: 'POST',
|
|
130
|
+
signal: createSignal(requestConfig.signal, timeout),
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
let parsedResult: Awaited<ReturnType<typeof response.json>> = {}
|
|
134
|
+
try {
|
|
135
|
+
parsedResult = await response.json()
|
|
136
|
+
} catch {
|
|
137
|
+
parsedResult = {}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const parsedResponse: HttpResponse<HttpResponseData<T>, T, TConfig> = {
|
|
141
|
+
config: createResponseConfig<T, TConfig>(joinUrl(options.baseURL, url), requestConfig),
|
|
142
|
+
data: parsedResult,
|
|
143
|
+
status: response.status,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
await options.onResponse?.(parsedResponse as HttpAnyResponse)
|
|
147
|
+
return parsedResponse
|
|
148
|
+
},
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function createAnySignal(signals: AbortSignal[]): AbortSignal {
|
|
153
|
+
if (typeof AbortSignal?.any === 'function') {
|
|
154
|
+
return AbortSignal.any(signals)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const controller = new AbortController()
|
|
158
|
+
|
|
159
|
+
signals.filter(Boolean).forEach((signal) => {
|
|
160
|
+
signal.addEventListener('abort', () => controller.abort(), { once: true })
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
return controller.signal
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function createResponseConfig<TKey extends HttpKey, TConfig extends HttpConfigFull<TKey>>(
|
|
167
|
+
url: string,
|
|
168
|
+
config: TConfig,
|
|
169
|
+
): HttpResponseConfig<TKey, TConfig> {
|
|
170
|
+
const responseConfig = { ...config } as Record<string, unknown>
|
|
171
|
+
delete responseConfig.signal
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
...responseConfig,
|
|
175
|
+
url,
|
|
176
|
+
} as HttpResponseConfig<TKey, TConfig>
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function createSignal(signal: AbortSignal | AbortSignal[] | undefined, timeout: number): AbortSignal {
|
|
180
|
+
const timeoutSignal = createTimeoutSignal(timeout)
|
|
181
|
+
|
|
182
|
+
if (Array.isArray(signal)) {
|
|
183
|
+
return createAnySignal([...signal, timeoutSignal])
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (signal) {
|
|
187
|
+
return createAnySignal([signal, timeoutSignal])
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return timeoutSignal
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function createTimeoutSignal(ms: number): AbortSignal {
|
|
194
|
+
if (typeof AbortSignal?.timeout === 'function') {
|
|
195
|
+
return AbortSignal.timeout(ms)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const controller = new AbortController()
|
|
199
|
+
const timeoutId = setTimeout(() => controller.abort(), ms)
|
|
200
|
+
|
|
201
|
+
controller.signal.addEventListener('abort', () => clearTimeout(timeoutId), {
|
|
202
|
+
once: true,
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
return controller.signal
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function getRetryDelay(attempt: number, delay: number): number {
|
|
209
|
+
return delay * 2 ** attempt
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function isAbortError(err: unknown): boolean {
|
|
213
|
+
return err instanceof DOMException && err.name === 'AbortError'
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function isFormData(value: FormData | Record<string, unknown> | undefined): value is FormData {
|
|
217
|
+
return typeof FormData !== 'undefined' && value instanceof FormData
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function isRetryableStatus(status: number): boolean {
|
|
221
|
+
return status >= 500 || status === 429
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function joinUrl(baseURL: string, url: string): string {
|
|
225
|
+
const normalizedBase = baseURL.endsWith('/') ? baseURL.slice(0, -1) : baseURL
|
|
226
|
+
const normalizedUrl = url.startsWith('/') ? url.slice(1) : url
|
|
227
|
+
|
|
228
|
+
return `${normalizedBase}/${normalizedUrl}`
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function resolveHeaders(
|
|
232
|
+
value: CreateHttpOptions['headers'],
|
|
233
|
+
extraHeaders?: Record<string, string>,
|
|
234
|
+
): Record<string, string> {
|
|
235
|
+
const headers = typeof value === 'function' ? value() : value
|
|
236
|
+
|
|
237
|
+
return Object.entries({
|
|
238
|
+
...headers,
|
|
239
|
+
...extraHeaders,
|
|
240
|
+
}).reduce<Record<string, string>>((acc, [key, headerValue]) => {
|
|
241
|
+
if (typeof headerValue === 'string') {
|
|
242
|
+
acc[key] = headerValue
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return acc
|
|
246
|
+
}, {})
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function serializeQueryValue(
|
|
250
|
+
urlParams: URLSearchParams,
|
|
251
|
+
key: string,
|
|
252
|
+
value: HttpParam[string],
|
|
253
|
+
arrayMode: HttpArrayMode,
|
|
254
|
+
): void {
|
|
255
|
+
if (value === undefined) {
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (Array.isArray(value)) {
|
|
260
|
+
if (arrayMode === 'json') {
|
|
261
|
+
urlParams.append(key, JSON.stringify(value))
|
|
262
|
+
return
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
value.forEach((item) => {
|
|
266
|
+
urlParams.append(`${key}[]`, item)
|
|
267
|
+
})
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
urlParams.append(key, String(value))
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function toQueryString(params?: HttpParam, arrayMode: HttpArrayMode = 'json'): string {
|
|
275
|
+
const urlParams = new URLSearchParams()
|
|
276
|
+
|
|
277
|
+
Object.entries(params ?? {}).forEach(([key, value]) => {
|
|
278
|
+
serializeQueryValue(urlParams, key, value, arrayMode)
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
return urlParams.size > 0 ? `?${urlParams}` : ''
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function wait(ms: number): Promise<void> {
|
|
285
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
286
|
+
}
|
package/src/index.ts
ADDED
package/src/nuxt.ts
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { useLazyAsyncData, useState } from 'nuxt/app'
|
|
2
|
+
import { onScopeDispose, shallowReactive, shallowRef } from 'vue'
|
|
3
|
+
|
|
4
|
+
import type { GetConfig, HttpClient, HttpErrorGuard, HttpKey, HttpParam, HttpResponseData } from './http'
|
|
5
|
+
|
|
6
|
+
import { createURL, hashData } from './utils'
|
|
7
|
+
|
|
8
|
+
const DAY = 1000 * 60 * 60 * 24
|
|
9
|
+
const DEFAULT_CHANNEL_NAME = 'http-tab-sync'
|
|
10
|
+
const DEFAULT_TTL = DAY * 7
|
|
11
|
+
export interface CreateUseHttpDependencies {
|
|
12
|
+
channelName?: string
|
|
13
|
+
getCache: () => null | UseHttpCache
|
|
14
|
+
getHttpClient: () => HttpClient
|
|
15
|
+
isDev?: () => boolean
|
|
16
|
+
isError?: HttpErrorGuard<unknown>
|
|
17
|
+
ttl?: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type HttpError = HttpErrorMap[keyof HttpErrorMap]
|
|
21
|
+
export interface HttpErrorMap {}
|
|
22
|
+
export type HttpSuccessData<TKey extends HttpKey = HttpKey> = Exclude<HttpResponseData<TKey>, HttpError>
|
|
23
|
+
|
|
24
|
+
export interface UseHttpCache {
|
|
25
|
+
deleteKeysWithPart: (part: string) => Promise<void>
|
|
26
|
+
get: <T>(key: string) => Promise<null | UseHttpCacheEntry<T>>
|
|
27
|
+
set: <T>(key: string, value: T, ttl: number) => Promise<void>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface UseHttpCacheEntry<T> {
|
|
31
|
+
hash: string
|
|
32
|
+
value: T
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface UseHttpFn {
|
|
36
|
+
<T extends HttpKey, P extends HttpParam>(options: UseHttpOptions<T, P>): Promise<UseHttpResult<T, P>>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface UseHttpOptions<T extends HttpKey, P extends HttpParam> {
|
|
40
|
+
effect?: UseHttpEffect<T, P>
|
|
41
|
+
initParams?: P
|
|
42
|
+
isError?: (payload: HttpResponseData<T>) => boolean
|
|
43
|
+
lazy?: true
|
|
44
|
+
mapParams?: (params?: P) => P
|
|
45
|
+
server?: boolean
|
|
46
|
+
url: T
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface UseHttpResult<T extends HttpKey, P extends HttpParam> {
|
|
50
|
+
data: HttpSuccessData<T> | null
|
|
51
|
+
error: null | UseHttpError<T>
|
|
52
|
+
fetch: UseHttpFetch<P>
|
|
53
|
+
hasFirstData: boolean
|
|
54
|
+
hasFreshData: boolean
|
|
55
|
+
pending: boolean
|
|
56
|
+
pendingCache: boolean
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type BroadcastMessage = {
|
|
60
|
+
data: unknown
|
|
61
|
+
fullUrl: string
|
|
62
|
+
params: HttpParam
|
|
63
|
+
type: 'STATE_UPDATE'
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type UseHttpEffect<T extends HttpKey, P extends HttpParam> = (
|
|
67
|
+
data: HttpResponseData<T>,
|
|
68
|
+
config: UseHttpEffectConfig<P>,
|
|
69
|
+
) => void
|
|
70
|
+
type UseHttpEffectConfig<P extends HttpParam> = {
|
|
71
|
+
cached: boolean
|
|
72
|
+
params: P
|
|
73
|
+
}
|
|
74
|
+
type UseHttpError<T extends HttpKey> = Extract<HttpResponseData<T>, HttpError>
|
|
75
|
+
|
|
76
|
+
type UseHttpFetch<P extends HttpParam> = (params?: P, opt?: { signal: AbortSignal }) => Promise<void>
|
|
77
|
+
|
|
78
|
+
export function createUseHttp(dependencies: CreateUseHttpDependencies): UseHttpFn {
|
|
79
|
+
let channel: BroadcastChannel | null
|
|
80
|
+
|
|
81
|
+
const useHttp: UseHttpFn = async <T extends HttpKey, P extends HttpParam>(
|
|
82
|
+
options: UseHttpOptions<T, P>,
|
|
83
|
+
): Promise<UseHttpResult<T, P>> => {
|
|
84
|
+
const mapParams = (params?: P): P => {
|
|
85
|
+
if (options.mapParams) {
|
|
86
|
+
return options.mapParams(params)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return params ?? ({} as P)
|
|
90
|
+
}
|
|
91
|
+
const effect = options.effect
|
|
92
|
+
const isError = createIsErrorGuard(options.isError, dependencies.isError)
|
|
93
|
+
|
|
94
|
+
const buildUrl = createURL
|
|
95
|
+
const initFullUrl = buildUrl(options.url, mapParams(options.initParams))
|
|
96
|
+
const httpClient = dependencies.getHttpClient()
|
|
97
|
+
const cache = import.meta.client ? (dependencies.getCache?.() ?? null) : null
|
|
98
|
+
let hasDataFromServer = false
|
|
99
|
+
|
|
100
|
+
const result = shallowReactive({
|
|
101
|
+
data: null as null | unknown,
|
|
102
|
+
error: null as null | unknown,
|
|
103
|
+
fetch: async (_params?: P, _opt?: { signal: AbortSignal }): Promise<void> => await undefined,
|
|
104
|
+
hasFirstData: false,
|
|
105
|
+
hasFreshData: false,
|
|
106
|
+
pending: true,
|
|
107
|
+
pendingCache: true,
|
|
108
|
+
})
|
|
109
|
+
const setError = (value: null | unknown): void => {
|
|
110
|
+
result.error = value
|
|
111
|
+
}
|
|
112
|
+
const setData = (value: null | unknown): void => {
|
|
113
|
+
result.data = value
|
|
114
|
+
}
|
|
115
|
+
const syncResult = (payload: HttpResponseData<T> | null | undefined): void => {
|
|
116
|
+
if (payload === null || payload === undefined) {
|
|
117
|
+
setData(null)
|
|
118
|
+
setError(null)
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (isError(payload)) {
|
|
123
|
+
setData(null)
|
|
124
|
+
setError(payload)
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
setData(payload)
|
|
129
|
+
setError(null)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const controller = new AbortController()
|
|
133
|
+
const serverData = useState<null | unknown>(`http-${initFullUrl}`, () => null)
|
|
134
|
+
|
|
135
|
+
if (options.server && import.meta.server) {
|
|
136
|
+
const paramsReactive = shallowRef(mapParams(options.initParams))
|
|
137
|
+
const ssr = await useLazyAsyncData(initFullUrl, async () => {
|
|
138
|
+
return await httpClient.get<T>(options.url, {
|
|
139
|
+
params: paramsReactive.value,
|
|
140
|
+
} as GetConfig<T>)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
result.fetch = async (params?: P) => {
|
|
144
|
+
paramsReactive.value = mapParams(params)
|
|
145
|
+
await ssr.refresh()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
serverData.value = ssr.data.value?.data ?? null
|
|
149
|
+
const serverPayload = serverData.value as HttpResponseData<T>
|
|
150
|
+
syncResult(serverPayload)
|
|
151
|
+
|
|
152
|
+
result.pending = false
|
|
153
|
+
result.pendingCache = false
|
|
154
|
+
result.hasFirstData = true
|
|
155
|
+
result.hasFreshData = true
|
|
156
|
+
|
|
157
|
+
if (serverData.value) {
|
|
158
|
+
effect?.(serverData.value as HttpResponseData<T>, {
|
|
159
|
+
cached: false,
|
|
160
|
+
params: mapParams(options.initParams),
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (result.data) {
|
|
165
|
+
hasDataFromServer = true
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (import.meta.client) {
|
|
170
|
+
channel = getChannel(dependencies.channelName ?? DEFAULT_CHANNEL_NAME, channel)
|
|
171
|
+
const fullUrlHistory: Record<string, true> = {}
|
|
172
|
+
|
|
173
|
+
function onMessage(event: MessageEvent<Partial<BroadcastMessage>>): void {
|
|
174
|
+
if (event.data.fullUrl && fullUrlHistory[event.data.fullUrl]) {
|
|
175
|
+
const eventPayload = event.data.data as HttpResponseData<T> | undefined
|
|
176
|
+
|
|
177
|
+
if (eventPayload) {
|
|
178
|
+
effect?.(eventPayload, {
|
|
179
|
+
cached: false,
|
|
180
|
+
params: event.data.params as P,
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
syncResult(eventPayload)
|
|
185
|
+
|
|
186
|
+
result.hasFirstData = true
|
|
187
|
+
result.hasFreshData = true
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
channel?.addEventListener('message', onMessage)
|
|
192
|
+
|
|
193
|
+
if (serverData.value) {
|
|
194
|
+
const clientServerPayload = serverData.value as HttpResponseData<T>
|
|
195
|
+
syncResult(clientServerPayload)
|
|
196
|
+
result.pending = false
|
|
197
|
+
result.pendingCache = false
|
|
198
|
+
result.hasFirstData = true
|
|
199
|
+
result.hasFreshData = true
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const raceCondition: Record<string, number> = {}
|
|
203
|
+
const ttl = dependencies.ttl ?? DEFAULT_TTL
|
|
204
|
+
|
|
205
|
+
const runFetch = async (params?: P, fetchOpt?: { signal?: AbortSignal }): Promise<void> => {
|
|
206
|
+
const mappedParams = mapParams(params)
|
|
207
|
+
const fullUrl = buildUrl(options.url, mappedParams)
|
|
208
|
+
const fetchId = Date.now() + getRandom(0, 300)
|
|
209
|
+
|
|
210
|
+
if (raceCondition[fullUrl]) {
|
|
211
|
+
console.info('Race Condition affect', fullUrl)
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
raceCondition[fullUrl] = fetchId
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
result.pending = true
|
|
219
|
+
result.pendingCache = true
|
|
220
|
+
|
|
221
|
+
const cachedFetch = cache && !dependencies.isDev?.() ? await cache.get<unknown>(fullUrl) : null
|
|
222
|
+
|
|
223
|
+
if (cachedFetch) {
|
|
224
|
+
const cachedPayload = cachedFetch.value as HttpResponseData<T>
|
|
225
|
+
|
|
226
|
+
effect?.(cachedPayload, {
|
|
227
|
+
cached: true,
|
|
228
|
+
params: mappedParams,
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
syncResult(cachedPayload)
|
|
232
|
+
result.hasFirstData = true
|
|
233
|
+
|
|
234
|
+
result.pendingCache = false
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (controller.signal.aborted || fetchOpt?.signal?.aborted) {
|
|
238
|
+
throw new DOMException('Aborted', 'AbortError')
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const signalHttp = fetchOpt?.signal ? [controller.signal, fetchOpt.signal] : controller.signal
|
|
242
|
+
const response = await httpClient.get<T>(options.url, {
|
|
243
|
+
params: mappedParams,
|
|
244
|
+
signal: signalHttp,
|
|
245
|
+
} as GetConfig<T>)
|
|
246
|
+
const responsePayload = response.data as HttpResponseData<T>
|
|
247
|
+
|
|
248
|
+
effect?.(responsePayload, {
|
|
249
|
+
cached: false,
|
|
250
|
+
params: mappedParams,
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
if (isError(responsePayload)) {
|
|
254
|
+
syncResult(responsePayload)
|
|
255
|
+
} else {
|
|
256
|
+
syncResult(responsePayload)
|
|
257
|
+
const successData = responsePayload
|
|
258
|
+
|
|
259
|
+
fullUrlHistory[fullUrl] = true
|
|
260
|
+
channel?.postMessage({
|
|
261
|
+
data: normalizeBroadcastValue(successData),
|
|
262
|
+
fullUrl,
|
|
263
|
+
params: normalizeBroadcastValue(mappedParams),
|
|
264
|
+
type: 'STATE_UPDATE',
|
|
265
|
+
} satisfies BroadcastMessage)
|
|
266
|
+
|
|
267
|
+
if (cache) {
|
|
268
|
+
if (cachedFetch) {
|
|
269
|
+
const newHash = await hashData(responsePayload)
|
|
270
|
+
if (newHash !== cachedFetch.hash) {
|
|
271
|
+
await cache.deleteKeysWithPart(options.url)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (response.status === 200) {
|
|
276
|
+
await cache.set(fullUrl, successData, ttl)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
result.hasFirstData = true
|
|
282
|
+
result.hasFreshData = true
|
|
283
|
+
} catch (error) {
|
|
284
|
+
console.error(error)
|
|
285
|
+
} finally {
|
|
286
|
+
if (raceCondition[fullUrl] === fetchId) {
|
|
287
|
+
delete raceCondition[fullUrl]
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
result.pending = false
|
|
291
|
+
result.pendingCache = false
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
result.fetch = runFetch
|
|
296
|
+
|
|
297
|
+
if (!hasDataFromServer && options.lazy !== true) {
|
|
298
|
+
runFetch(options.initParams)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
onScopeDispose(() => {
|
|
302
|
+
channel?.removeEventListener('message', onMessage)
|
|
303
|
+
controller.abort(`Http Abort -> onScopeDispose ${options.url}`)
|
|
304
|
+
|
|
305
|
+
if (serverData.value) {
|
|
306
|
+
serverData.value = null
|
|
307
|
+
}
|
|
308
|
+
})
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return result as UseHttpResult<T, P>
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return useHttp
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function createIsErrorGuard<TData>(
|
|
318
|
+
localGuard?: (payload: TData) => boolean,
|
|
319
|
+
globalGuard?: HttpErrorGuard<unknown>,
|
|
320
|
+
): (payload: TData) => boolean {
|
|
321
|
+
if (localGuard) {
|
|
322
|
+
return localGuard
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (globalGuard) {
|
|
326
|
+
return globalGuard as (payload: TData) => boolean
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return (): boolean => false
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function getChannel(name: string, currentChannel?: BroadcastChannel | null): BroadcastChannel | null {
|
|
333
|
+
if (currentChannel) {
|
|
334
|
+
return currentChannel
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (import.meta.server || typeof BroadcastChannel !== 'function') {
|
|
338
|
+
return null
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return new BroadcastChannel(name)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function getRandom(min: number, max: number): number {
|
|
345
|
+
return Math.floor(Math.random() * (max - min + 1)) + min
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function normalizeBroadcastValue<T>(payload: T): T {
|
|
349
|
+
try {
|
|
350
|
+
return structuredClone(payload)
|
|
351
|
+
} catch {
|
|
352
|
+
return JSON.parse(JSON.stringify(payload)) as T
|
|
353
|
+
}
|
|
354
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { HttpParam } from './http'
|
|
2
|
+
|
|
3
|
+
export function createURL(url: string, params?: HttpParam): string {
|
|
4
|
+
const urlParams = new URLSearchParams()
|
|
5
|
+
|
|
6
|
+
Object.entries(params ?? {}).forEach(([key, value]) => {
|
|
7
|
+
if (value === undefined) {
|
|
8
|
+
return
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (Array.isArray(value)) {
|
|
12
|
+
value.forEach((item) => {
|
|
13
|
+
urlParams.append(`${key}[]`, item)
|
|
14
|
+
})
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
urlParams.append(key, String(value))
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const query = urlParams.size > 0 ? `?${urlParams}` : ''
|
|
22
|
+
return urlParams.size > 0 ? `${url}${query}` : url
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function fastDevHash(data: unknown): string {
|
|
26
|
+
const str = JSON.stringify(data)
|
|
27
|
+
let hash = 0
|
|
28
|
+
|
|
29
|
+
for (let i = 0; i < str.length; i++) {
|
|
30
|
+
const chr = str.charCodeAt(i)
|
|
31
|
+
hash = (hash << 5) - hash + chr
|
|
32
|
+
hash |= 0
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return Math.abs(hash).toString(16)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function hashData(data: unknown): Promise<string> {
|
|
39
|
+
const subtle = globalThis.crypto?.subtle
|
|
40
|
+
|
|
41
|
+
if (subtle) {
|
|
42
|
+
const encoded = new TextEncoder().encode(JSON.stringify(data))
|
|
43
|
+
const buffer = await subtle.digest('SHA-1', encoded)
|
|
44
|
+
const array = Array.from(new Uint8Array(buffer))
|
|
45
|
+
|
|
46
|
+
return array.map((value) => value.toString(16).padStart(2, '0')).join('')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return fastDevHash(data)
|
|
50
|
+
}
|