@ciwergrp/nuxid 1.17.8 → 1.18.0
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/dist/module.json
CHANGED
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
import type { NitroFetchOptions, NitroFetchRequest } from 'nitropack';
|
|
2
|
+
import type { MultiWatchSources, Ref } from 'vue';
|
|
2
3
|
export interface APIResponseCursor<T> {
|
|
3
4
|
data: T[];
|
|
4
|
-
meta:
|
|
5
|
-
[key: string]: any;
|
|
6
|
-
};
|
|
5
|
+
meta: Record<string, any>;
|
|
7
6
|
}
|
|
8
7
|
export interface CursorMetaConfig {
|
|
9
8
|
cursorKey?: string;
|
|
10
9
|
hasMoreKey?: string;
|
|
11
10
|
}
|
|
12
|
-
export interface
|
|
11
|
+
export interface CursorExecuteOptions {
|
|
12
|
+
dedupe?: 'cancel' | 'defer';
|
|
13
|
+
timeout?: number;
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
}
|
|
16
|
+
export interface CursorFetchOptions<TRequest extends NitroFetchRequest = NitroFetchRequest, TResponse extends APIResponseCursor<any> = APIResponseCursor<any>> {
|
|
17
|
+
cache?: 'none' | 'payload' | 'static' | 'both';
|
|
18
|
+
server?: boolean;
|
|
13
19
|
lazy?: boolean;
|
|
14
20
|
immediate?: boolean;
|
|
15
|
-
|
|
21
|
+
default?: () => TResponse | Ref<TResponse | null> | null;
|
|
22
|
+
deep?: boolean;
|
|
23
|
+
dedupe?: 'cancel' | 'defer';
|
|
24
|
+
timeout?: number;
|
|
25
|
+
watch?: MultiWatchSources | false;
|
|
16
26
|
fetcher?: typeof $fetch;
|
|
17
27
|
fetchOptions?: NitroFetchOptions<TRequest>;
|
|
18
28
|
meta?: CursorMetaConfig;
|
|
@@ -20,28 +30,41 @@ export interface CursorFetchOptions<TRequest extends NitroFetchRequest = NitroFe
|
|
|
20
30
|
cursorParam?: string;
|
|
21
31
|
query?: Record<string, any>;
|
|
22
32
|
key?: string;
|
|
33
|
+
pollInterval?: number;
|
|
23
34
|
}
|
|
24
35
|
export interface FetchError<TData = any> {
|
|
25
36
|
statusCode: number | undefined;
|
|
26
37
|
statusMessage: string | undefined;
|
|
27
38
|
_data: TData | undefined;
|
|
28
39
|
}
|
|
29
|
-
export declare
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
40
|
+
export declare const normalizeFetchError: (error: unknown) => FetchError;
|
|
41
|
+
export declare const ensureCursorResponse: <T extends APIResponseCursor<any>>(response: T) => T;
|
|
42
|
+
export declare const extractCursorState: (response: APIResponseCursor<any>, metaConfig?: CursorMetaConfig) => {
|
|
43
|
+
nextCursor: string | null;
|
|
44
|
+
hasNextPage: boolean;
|
|
45
|
+
};
|
|
46
|
+
export declare const mergeCursorResponse: <T extends APIResponseCursor<any>>(current: T | undefined, next: T, itemKey: string) => T;
|
|
47
|
+
export declare const buildKey: (key: string | undefined, url: NitroFetchRequest, query: Record<string, any> | undefined) => string;
|
|
48
|
+
export declare const buildFetchOptions: <TRequest extends NitroFetchRequest>(baseOptions: NitroFetchOptions<TRequest> | undefined, query: Record<string, any> | undefined, signal?: AbortSignal) => NitroFetchOptions<TRequest>;
|
|
49
|
+
export declare const resolveDefaultResponse: <TResponse extends APIResponseCursor<any>>(factory?: CursorFetchOptions<any, TResponse>["default"]) => TResponse | null | undefined;
|
|
50
|
+
export declare function useCursorHttp<T extends Record<string, any> = any, TResponse extends APIResponseCursor<T> = APIResponseCursor<T>, TRequest extends NitroFetchRequest = NitroFetchRequest>(url: TRequest, options?: CursorFetchOptions<TRequest, TResponse>): Promise<{
|
|
51
|
+
data: [TResponse | null | undefined] extends [Ref<any, any>] ? import("@vue/shared").IfAny<Ref<any, any> & TResponse, Ref<Ref<any, any> & TResponse, Ref<any, any> & TResponse>, Ref<any, any> & TResponse> : Ref<import("vue").UnwrapRef<TResponse> | null | undefined, TResponse | import("vue").UnwrapRef<TResponse> | null | undefined>;
|
|
52
|
+
loading: Readonly<Ref<boolean, boolean>>;
|
|
53
|
+
error: Readonly<Ref<{
|
|
33
54
|
readonly statusCode: number | undefined;
|
|
34
55
|
readonly statusMessage: string | undefined;
|
|
35
56
|
readonly _data: any;
|
|
36
|
-
} |
|
|
57
|
+
} | undefined, {
|
|
37
58
|
readonly statusCode: number | undefined;
|
|
38
59
|
readonly statusMessage: string | undefined;
|
|
39
60
|
readonly _data: any;
|
|
40
|
-
} |
|
|
41
|
-
hasNextPage: Readonly<
|
|
42
|
-
isLoadMoreTriggered: Readonly<
|
|
43
|
-
loadMore: () => Promise<void>;
|
|
44
|
-
refresh: () => Promise<void>;
|
|
45
|
-
|
|
61
|
+
} | undefined>>;
|
|
62
|
+
hasNextPage: Readonly<Ref<boolean, boolean>>;
|
|
63
|
+
isLoadMoreTriggered: Readonly<Ref<boolean, boolean>>;
|
|
64
|
+
loadMore: (executeOptions?: CursorExecuteOptions) => Promise<void>;
|
|
65
|
+
refresh: (executeOptions?: CursorExecuteOptions) => Promise<void>;
|
|
66
|
+
execute: (executeOptions?: CursorExecuteOptions) => Promise<void>;
|
|
67
|
+
clear: () => void;
|
|
68
|
+
init: (executeOptions?: CursorExecuteOptions) => Promise<void>;
|
|
46
69
|
}>;
|
|
47
70
|
export default useCursorHttp;
|
|
@@ -1,281 +1,466 @@
|
|
|
1
|
-
import { isRef,
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { isRef, onMounted, onUnmounted, readonly, ref, shallowRef, watch } from "vue";
|
|
2
|
+
const META_CURSOR_CANDIDATES = ["afterCursor", "next_cursor", "nextCursor", "cursor"];
|
|
3
|
+
const META_HAS_MORE_CANDIDATES = ["hasMore", "has_next_page", "hasNextPage", "has_next"];
|
|
4
|
+
const normalizeString = (value, fallback) => {
|
|
5
|
+
if (typeof value !== "string") {
|
|
6
|
+
return fallback;
|
|
7
|
+
}
|
|
8
|
+
const trimmed = value.trim();
|
|
9
|
+
return trimmed.length > 0 ? trimmed : fallback;
|
|
10
|
+
};
|
|
11
|
+
const normalizeCursorValue = (value) => {
|
|
12
|
+
if (value === null || value === void 0) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
if (typeof value === "string") {
|
|
16
|
+
const trimmed = value.trim();
|
|
17
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
18
|
+
}
|
|
19
|
+
return String(value);
|
|
20
|
+
};
|
|
21
|
+
const cloneResponse = (response) => ({
|
|
22
|
+
...response,
|
|
23
|
+
data: [...response.data]
|
|
24
|
+
});
|
|
25
|
+
export const normalizeFetchError = (error) => {
|
|
26
|
+
if (error && typeof error === "object") {
|
|
27
|
+
const cast = error;
|
|
28
|
+
return {
|
|
29
|
+
statusCode: typeof cast.statusCode === "number" ? cast.statusCode : void 0,
|
|
30
|
+
statusMessage: typeof cast.statusMessage === "string" ? cast.statusMessage : cast.message ?? void 0,
|
|
31
|
+
_data: cast.data
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (typeof error === "string") {
|
|
35
|
+
return {
|
|
36
|
+
statusCode: void 0,
|
|
37
|
+
statusMessage: error,
|
|
38
|
+
_data: void 0
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
statusCode: void 0,
|
|
43
|
+
statusMessage: "Unknown error",
|
|
44
|
+
_data: void 0
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
export const ensureCursorResponse = (response) => {
|
|
48
|
+
if (!response || typeof response !== "object" || !Array.isArray(response.data)) {
|
|
49
|
+
throw new Error("Cursor responses must include a data array");
|
|
50
|
+
}
|
|
51
|
+
return response;
|
|
52
|
+
};
|
|
53
|
+
const unwrapReactiveObject = (value) => {
|
|
54
|
+
if (!value || typeof value !== "object") {
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
const unwrapped = Array.isArray(value) ? [] : {};
|
|
58
|
+
for (const key in value) {
|
|
59
|
+
if (!Object.prototype.hasOwnProperty.call(value, key)) {
|
|
60
|
+
continue;
|
|
7
61
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
62
|
+
const current = value[key];
|
|
63
|
+
if (isRef(current)) {
|
|
64
|
+
;
|
|
65
|
+
unwrapped[key] = current.value;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (current && typeof current === "object") {
|
|
69
|
+
;
|
|
70
|
+
unwrapped[key] = unwrapReactiveObject(current);
|
|
71
|
+
continue;
|
|
17
72
|
}
|
|
73
|
+
;
|
|
74
|
+
unwrapped[key] = current;
|
|
75
|
+
}
|
|
76
|
+
return unwrapped;
|
|
77
|
+
};
|
|
78
|
+
const findReactiveSources = (value) => {
|
|
79
|
+
const sources = [];
|
|
80
|
+
if (!value || typeof value !== "object") {
|
|
18
81
|
return sources;
|
|
19
82
|
}
|
|
20
|
-
|
|
21
|
-
if (!
|
|
22
|
-
|
|
83
|
+
for (const key in value) {
|
|
84
|
+
if (!Object.prototype.hasOwnProperty.call(value, key)) {
|
|
85
|
+
continue;
|
|
23
86
|
}
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (isRef(value)) {
|
|
29
|
-
unwrapped[key2] = value.value;
|
|
30
|
-
} else if (typeof value === "object") {
|
|
31
|
-
unwrapped[key2] = unwrapReactiveObject(value);
|
|
32
|
-
} else {
|
|
33
|
-
unwrapped[key2] = value;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
87
|
+
const current = value[key];
|
|
88
|
+
if (isRef(current)) {
|
|
89
|
+
sources.push(current);
|
|
90
|
+
continue;
|
|
36
91
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
function normalizeFetchError(e) {
|
|
40
|
-
if (e && typeof e === "object") {
|
|
41
|
-
const cast = e;
|
|
42
|
-
return {
|
|
43
|
-
statusCode: typeof cast.statusCode === "number" ? cast.statusCode : void 0,
|
|
44
|
-
statusMessage: typeof cast.statusMessage === "string" ? cast.statusMessage : cast.message ?? void 0,
|
|
45
|
-
_data: cast.data
|
|
46
|
-
};
|
|
92
|
+
if (current && typeof current === "object") {
|
|
93
|
+
sources.push(...findReactiveSources(current));
|
|
47
94
|
}
|
|
48
|
-
|
|
49
|
-
|
|
95
|
+
}
|
|
96
|
+
return sources;
|
|
97
|
+
};
|
|
98
|
+
const uniqueSources = (sources) => Array.from(new Set(sources));
|
|
99
|
+
const resolveCursorMetaValue = (meta, explicitKey, candidates) => {
|
|
100
|
+
if (!meta || typeof meta !== "object") {
|
|
101
|
+
return void 0;
|
|
102
|
+
}
|
|
103
|
+
const normalizedKey = normalizeString(explicitKey, "");
|
|
104
|
+
if (normalizedKey) {
|
|
105
|
+
return meta[normalizedKey];
|
|
106
|
+
}
|
|
107
|
+
for (const candidate of candidates) {
|
|
108
|
+
if (candidate in meta) {
|
|
109
|
+
return meta[candidate];
|
|
50
110
|
}
|
|
51
|
-
return { statusCode: void 0, statusMessage: "Unknown error", _data: void 0 };
|
|
52
111
|
}
|
|
112
|
+
return void 0;
|
|
113
|
+
};
|
|
114
|
+
export const extractCursorState = (response, metaConfig) => {
|
|
115
|
+
const cursorValue = resolveCursorMetaValue(response.meta, metaConfig?.cursorKey, META_CURSOR_CANDIDATES);
|
|
116
|
+
const hasMoreValue = resolveCursorMetaValue(response.meta, metaConfig?.hasMoreKey, META_HAS_MORE_CANDIDATES);
|
|
117
|
+
return {
|
|
118
|
+
nextCursor: normalizeCursorValue(cursorValue),
|
|
119
|
+
hasNextPage: Boolean(hasMoreValue)
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
export const mergeCursorResponse = (current, next, itemKey) => {
|
|
123
|
+
if (!current) {
|
|
124
|
+
return cloneResponse(next);
|
|
125
|
+
}
|
|
126
|
+
const knownKeys = new Set(
|
|
127
|
+
current.data.map((item) => item?.[itemKey])
|
|
128
|
+
);
|
|
129
|
+
const newItems = next.data.filter((item) => !knownKeys.has(item?.[itemKey]));
|
|
130
|
+
return {
|
|
131
|
+
...next,
|
|
132
|
+
data: [...current.data, ...newItems]
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
export const buildKey = (key, url, query) => {
|
|
136
|
+
const normalizedKey = normalizeString(key, "");
|
|
137
|
+
if (normalizedKey) {
|
|
138
|
+
return normalizedKey;
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
return `${String(url)}:${JSON.stringify(query ?? {})}`;
|
|
142
|
+
} catch {
|
|
143
|
+
return String(url);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
export const buildFetchOptions = (baseOptions, query, signal) => {
|
|
147
|
+
const currentQuery = {
|
|
148
|
+
...baseOptions?.query ?? {},
|
|
149
|
+
...query ?? {}
|
|
150
|
+
};
|
|
151
|
+
return {
|
|
152
|
+
...baseOptions,
|
|
153
|
+
query: currentQuery,
|
|
154
|
+
signal
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
export const resolveDefaultResponse = (factory) => {
|
|
158
|
+
const value = factory?.();
|
|
159
|
+
return isRef(value) ? value.value : value ?? void 0;
|
|
160
|
+
};
|
|
161
|
+
const createTimeoutAbortController = (timeout) => {
|
|
162
|
+
if (!timeout || timeout <= 0) {
|
|
163
|
+
return void 0;
|
|
164
|
+
}
|
|
165
|
+
const controller = new AbortController();
|
|
166
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
167
|
+
return {
|
|
168
|
+
controller,
|
|
169
|
+
clear: () => clearTimeout(timer)
|
|
170
|
+
};
|
|
171
|
+
};
|
|
172
|
+
const resolveCachedDataGetter = (cache) => {
|
|
173
|
+
if (cache === "none" || cache === void 0) {
|
|
174
|
+
return () => void 0;
|
|
175
|
+
}
|
|
176
|
+
if (cache === "payload") {
|
|
177
|
+
return (key, nuxtApp) => nuxtApp.isHydrating ? nuxtApp.payload.data[key] : void 0;
|
|
178
|
+
}
|
|
179
|
+
if (cache === "static") {
|
|
180
|
+
return (key, nuxtApp) => nuxtApp.static.data[key];
|
|
181
|
+
}
|
|
182
|
+
return (key, nuxtApp) => nuxtApp.isHydrating ? nuxtApp.payload.data[key] : nuxtApp.static.data[key];
|
|
183
|
+
};
|
|
184
|
+
export async function useCursorHttp(url, options) {
|
|
53
185
|
const {
|
|
186
|
+
cache = "none",
|
|
187
|
+
server = true,
|
|
54
188
|
lazy = false,
|
|
55
189
|
immediate = true,
|
|
56
|
-
|
|
190
|
+
default: defaultFactory,
|
|
191
|
+
deep = false,
|
|
192
|
+
dedupe = "cancel",
|
|
193
|
+
timeout,
|
|
194
|
+
watch: watchSources = void 0,
|
|
57
195
|
fetcher,
|
|
58
196
|
fetchOptions,
|
|
59
197
|
query,
|
|
60
198
|
meta,
|
|
61
199
|
itemKey = "id",
|
|
62
200
|
cursorParam = "cursor",
|
|
63
|
-
key
|
|
201
|
+
key,
|
|
202
|
+
pollInterval
|
|
64
203
|
} = options ?? {};
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
204
|
+
const fetcherFn = fetcher ?? globalThis.$fetch;
|
|
205
|
+
if (!fetcherFn) {
|
|
206
|
+
throw new Error("Nuxt $fetch is not available in the current context");
|
|
207
|
+
}
|
|
208
|
+
const { useAsyncData } = await import("#app");
|
|
209
|
+
const { clearNuxtData } = await import("#app");
|
|
210
|
+
const resolvedKey = buildKey(key, url, query ?? fetchOptions?.query);
|
|
211
|
+
const instanceId = cache === "none" ? globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(16).slice(2)}` : "";
|
|
212
|
+
const stateKey = cache === "none" ? `${resolvedKey}:${instanceId}` : resolvedKey;
|
|
213
|
+
const resolvedBaseOptions = shallowRef(
|
|
214
|
+
unwrapReactiveObject(fetchOptions)
|
|
215
|
+
);
|
|
216
|
+
const reactiveSources = uniqueSources([
|
|
217
|
+
...watchSources === false ? [] : Array.isArray(watchSources) ? watchSources : watchSources ? [watchSources] : [],
|
|
218
|
+
...findReactiveSources(fetchOptions),
|
|
219
|
+
...findReactiveSources(query)
|
|
220
|
+
]);
|
|
221
|
+
const data = ref(resolveDefaultResponse(defaultFactory));
|
|
69
222
|
const nextCursor = ref(null);
|
|
70
223
|
const hasNextPage = ref(true);
|
|
224
|
+
const hasResolvedInitial = ref(false);
|
|
71
225
|
const isLoadMoreTriggered = ref(false);
|
|
72
|
-
const
|
|
73
|
-
|
|
226
|
+
const loadMorePending = ref(false);
|
|
227
|
+
const loadMoreError = ref(void 0);
|
|
228
|
+
const pollTimer = ref(null);
|
|
229
|
+
const loadMoreRequest = ref(null);
|
|
230
|
+
const loadMoreAbortController = ref(null);
|
|
231
|
+
const asyncData = await useAsyncData(
|
|
232
|
+
stateKey,
|
|
233
|
+
async (_nuxtApp, { signal }) => {
|
|
234
|
+
const requestOptions = buildFetchOptions(
|
|
235
|
+
resolvedBaseOptions.value,
|
|
236
|
+
unwrapReactiveObject(query),
|
|
237
|
+
signal
|
|
238
|
+
);
|
|
239
|
+
const response = await fetcherFn(url, requestOptions);
|
|
240
|
+
return ensureCursorResponse(response);
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
server,
|
|
244
|
+
lazy,
|
|
245
|
+
immediate,
|
|
246
|
+
default: defaultFactory,
|
|
247
|
+
deep,
|
|
248
|
+
dedupe,
|
|
249
|
+
timeout,
|
|
250
|
+
getCachedData: resolveCachedDataGetter(cache)
|
|
251
|
+
}
|
|
74
252
|
);
|
|
75
|
-
const
|
|
76
|
-
if (
|
|
77
|
-
|
|
253
|
+
const syncResponseState = (response) => {
|
|
254
|
+
if (!response) {
|
|
255
|
+
nextCursor.value = null;
|
|
256
|
+
hasNextPage.value = false;
|
|
257
|
+
hasResolvedInitial.value = false;
|
|
258
|
+
data.value = response ?? resolveDefaultResponse(defaultFactory);
|
|
259
|
+
return;
|
|
78
260
|
}
|
|
79
|
-
const
|
|
80
|
-
|
|
261
|
+
const clonedResponse = cloneResponse(response);
|
|
262
|
+
const cursorState = extractCursorState(clonedResponse, meta);
|
|
263
|
+
data.value = clonedResponse;
|
|
264
|
+
nextCursor.value = cursorState.nextCursor;
|
|
265
|
+
hasNextPage.value = cursorState.hasNextPage;
|
|
266
|
+
hasResolvedInitial.value = true;
|
|
81
267
|
};
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
268
|
+
watch(
|
|
269
|
+
() => asyncData.data.value,
|
|
270
|
+
(response) => {
|
|
271
|
+
syncResponseState(response);
|
|
272
|
+
loadMoreError.value = void 0;
|
|
273
|
+
isLoadMoreTriggered.value = false;
|
|
274
|
+
},
|
|
275
|
+
{ immediate: true }
|
|
276
|
+
);
|
|
277
|
+
if (reactiveSources.length > 0) {
|
|
278
|
+
watch(
|
|
279
|
+
reactiveSources,
|
|
280
|
+
() => {
|
|
281
|
+
resolvedBaseOptions.value = unwrapReactiveObject(fetchOptions);
|
|
282
|
+
void refresh();
|
|
283
|
+
},
|
|
284
|
+
{ flush: "sync" }
|
|
285
|
+
);
|
|
98
286
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
287
|
+
if (cache === "none") {
|
|
288
|
+
onUnmounted(() => {
|
|
289
|
+
clearNuxtData(stateKey);
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
const resetCursorState = () => {
|
|
293
|
+
nextCursor.value = null;
|
|
294
|
+
hasNextPage.value = true;
|
|
295
|
+
hasResolvedInitial.value = false;
|
|
296
|
+
isLoadMoreTriggered.value = false;
|
|
297
|
+
loadMorePending.value = false;
|
|
298
|
+
loadMoreError.value = void 0;
|
|
299
|
+
data.value = resolveDefaultResponse(defaultFactory);
|
|
105
300
|
};
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (value === null || value === void 0) {
|
|
112
|
-
return null;
|
|
113
|
-
}
|
|
114
|
-
if (typeof value === "string") {
|
|
115
|
-
const trimmed = value.trim();
|
|
116
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
117
|
-
}
|
|
118
|
-
return String(value);
|
|
301
|
+
const cancelLoadMore = () => {
|
|
302
|
+
loadMoreAbortController.value?.abort();
|
|
303
|
+
loadMoreAbortController.value = null;
|
|
304
|
+
loadMoreRequest.value = null;
|
|
305
|
+
loadMorePending.value = false;
|
|
119
306
|
};
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const applyResponse = (response, cacheInitial = false) => {
|
|
125
|
-
data.value = cloneResponse(response);
|
|
126
|
-
nextCursor.value = normalizeCursorValue(response.meta?.[cursorKey]);
|
|
127
|
-
hasNextPage.value = response.meta?.[hasMoreKey] ?? false;
|
|
128
|
-
if (cacheInitial) {
|
|
129
|
-
initialResponseState.value = cloneResponse(response);
|
|
130
|
-
}
|
|
307
|
+
const refresh = async (executeOptions) => {
|
|
308
|
+
cancelLoadMore();
|
|
309
|
+
resetCursorState();
|
|
310
|
+
await asyncData.refresh(executeOptions);
|
|
131
311
|
};
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
nextCursor.value = normalizeCursorValue(response.meta?.[cursorKey]);
|
|
149
|
-
hasNextPage.value = response.meta?.[hasMoreKey] ?? false;
|
|
150
|
-
} catch (e) {
|
|
151
|
-
error.value = normalizeFetchError(e);
|
|
152
|
-
} finally {
|
|
153
|
-
loading.value = false;
|
|
154
|
-
}
|
|
155
|
-
})();
|
|
156
|
-
fetchPromise = promise;
|
|
157
|
-
try {
|
|
158
|
-
await promise;
|
|
159
|
-
} finally {
|
|
160
|
-
if (fetchPromise === promise) {
|
|
161
|
-
fetchPromise = null;
|
|
312
|
+
const execute = async (executeOptions) => {
|
|
313
|
+
await refresh(executeOptions);
|
|
314
|
+
};
|
|
315
|
+
const init = async (executeOptions) => {
|
|
316
|
+
await execute(executeOptions);
|
|
317
|
+
};
|
|
318
|
+
const clear = () => {
|
|
319
|
+
cancelLoadMore();
|
|
320
|
+
asyncData.clear();
|
|
321
|
+
clearNuxtData(stateKey);
|
|
322
|
+
resetCursorState();
|
|
323
|
+
};
|
|
324
|
+
const loadMore = async (executeOptions) => {
|
|
325
|
+
if (loadMorePending.value) {
|
|
326
|
+
if (dedupe === "defer") {
|
|
327
|
+
return loadMoreRequest.value ?? Promise.resolve();
|
|
162
328
|
}
|
|
329
|
+
cancelLoadMore();
|
|
163
330
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (loading.value) {
|
|
167
|
-
return fetchPromise ?? Promise.resolve();
|
|
331
|
+
if (!hasResolvedInitial.value) {
|
|
332
|
+
await init(executeOptions);
|
|
168
333
|
}
|
|
169
|
-
|
|
170
|
-
if (useCache && canUseHydrationCache && initialResponseState.value) {
|
|
171
|
-
applyResponse(initialResponseState.value);
|
|
334
|
+
if (!hasResolvedInitial.value || !data.value || !hasNextPage.value) {
|
|
172
335
|
return Promise.resolve();
|
|
173
336
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
337
|
+
isLoadMoreTriggered.value = true;
|
|
338
|
+
loadMorePending.value = true;
|
|
339
|
+
loadMoreError.value = void 0;
|
|
340
|
+
const pendingRequest = (async () => {
|
|
341
|
+
const timeoutHandle = createTimeoutAbortController(executeOptions?.timeout ?? timeout);
|
|
342
|
+
const requestAbortController = new AbortController();
|
|
343
|
+
loadMoreAbortController.value = requestAbortController;
|
|
344
|
+
if (executeOptions?.signal) {
|
|
345
|
+
if (executeOptions.signal.aborted) {
|
|
346
|
+
requestAbortController.abort();
|
|
347
|
+
} else {
|
|
348
|
+
executeOptions.signal.addEventListener("abort", () => requestAbortController.abort(), { once: true });
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (timeoutHandle) {
|
|
352
|
+
const timeoutSignal = timeoutHandle.controller.signal;
|
|
353
|
+
if (timeoutSignal.aborted) {
|
|
354
|
+
requestAbortController.abort();
|
|
355
|
+
} else {
|
|
356
|
+
timeoutSignal.addEventListener("abort", () => requestAbortController.abort(), { once: true });
|
|
357
|
+
}
|
|
358
|
+
}
|
|
177
359
|
try {
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
360
|
+
const baseOptions = resolvedBaseOptions.value ?? {};
|
|
361
|
+
const nextQuery = {
|
|
362
|
+
...unwrapReactiveObject(query) ?? {}
|
|
363
|
+
};
|
|
364
|
+
if (nextCursor.value !== null) {
|
|
365
|
+
nextQuery[normalizeString(cursorParam, "cursor")] = nextCursor.value;
|
|
366
|
+
}
|
|
367
|
+
const requestOptions = buildFetchOptions(baseOptions, nextQuery, requestAbortController.signal);
|
|
368
|
+
const response = ensureCursorResponse(await fetcherFn(url, requestOptions));
|
|
369
|
+
const mergedResponse = mergeCursorResponse(data.value ?? void 0, response, normalizeString(itemKey, "id"));
|
|
370
|
+
const cursorState = extractCursorState(mergedResponse, meta);
|
|
371
|
+
data.value = mergedResponse;
|
|
372
|
+
nextCursor.value = cursorState.nextCursor;
|
|
373
|
+
hasNextPage.value = cursorState.hasNextPage;
|
|
374
|
+
} catch (error2) {
|
|
375
|
+
loadMoreError.value = normalizeFetchError(error2);
|
|
182
376
|
} finally {
|
|
183
|
-
|
|
377
|
+
timeoutHandle?.clear();
|
|
378
|
+
loadMorePending.value = false;
|
|
379
|
+
loadMoreAbortController.value = null;
|
|
380
|
+
loadMoreRequest.value = null;
|
|
184
381
|
}
|
|
185
382
|
})();
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
await promise;
|
|
189
|
-
} finally {
|
|
190
|
-
if (fetchPromise === promise) {
|
|
191
|
-
fetchPromise = null;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
383
|
+
loadMoreRequest.value = pendingRequest;
|
|
384
|
+
await pendingRequest;
|
|
194
385
|
};
|
|
195
386
|
const pollData = async () => {
|
|
196
387
|
try {
|
|
197
|
-
const response =
|
|
388
|
+
const response = ensureCursorResponse(
|
|
389
|
+
await fetcherFn(
|
|
390
|
+
url,
|
|
391
|
+
buildFetchOptions(resolvedBaseOptions.value, unwrapReactiveObject(query))
|
|
392
|
+
)
|
|
393
|
+
);
|
|
198
394
|
if (data.value) {
|
|
199
395
|
const existingIds = new Set(
|
|
200
|
-
data.value.data.map((item) => item[
|
|
201
|
-
);
|
|
202
|
-
const newItems = response.data.filter(
|
|
203
|
-
(item) => !existingIds.has(item[resolvedItemKey])
|
|
396
|
+
data.value.data.map((item) => item?.[normalizeString(itemKey, "id")])
|
|
204
397
|
);
|
|
398
|
+
const newItems = response.data.filter((item) => !existingIds.has(item?.[normalizeString(itemKey, "id")]));
|
|
205
399
|
if (newItems.length > 0) {
|
|
206
|
-
data.value
|
|
400
|
+
data.value = {
|
|
401
|
+
...response,
|
|
402
|
+
data: [...newItems, ...data.value.data]
|
|
403
|
+
};
|
|
207
404
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
console.error("Polling error:", e);
|
|
213
|
-
}
|
|
214
|
-
};
|
|
215
|
-
const loadMore = async () => {
|
|
216
|
-
if (!hasNextPage.value) {
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
isLoadMoreTriggered.value = true;
|
|
220
|
-
const baseParams = currentParams.value ?? {};
|
|
221
|
-
const params = nextCursor.value ? {
|
|
222
|
-
...baseParams,
|
|
223
|
-
query: {
|
|
224
|
-
...baseParams.query ?? {},
|
|
225
|
-
[resolvedCursorParam]: nextCursor.value
|
|
405
|
+
const cursorState = extractCursorState(data.value, meta);
|
|
406
|
+
nextCursor.value = cursorState.nextCursor;
|
|
407
|
+
hasNextPage.value = cursorState.hasNextPage;
|
|
408
|
+
return;
|
|
226
409
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
};
|
|
230
|
-
const refresh = async () => {
|
|
231
|
-
data.value = void 0;
|
|
232
|
-
nextCursor.value = null;
|
|
233
|
-
hasNextPage.value = true;
|
|
234
|
-
isLoadMoreTriggered.value = false;
|
|
235
|
-
await fetchInitialData(currentParams.value, false);
|
|
236
|
-
};
|
|
237
|
-
const init = async () => {
|
|
238
|
-
if (data.value) {
|
|
239
|
-
return;
|
|
410
|
+
syncResponseState(response);
|
|
411
|
+
} catch {
|
|
240
412
|
}
|
|
241
|
-
await fetchInitialData(currentParams.value);
|
|
242
413
|
};
|
|
243
|
-
const reactiveSources = findReactiveSources(fetchOptions);
|
|
244
|
-
if (reactiveSources.length > 0) {
|
|
245
|
-
watch(
|
|
246
|
-
reactiveSources,
|
|
247
|
-
() => {
|
|
248
|
-
currentParams.value = unwrapReactiveObject(fetchOptions) ?? {};
|
|
249
|
-
refresh();
|
|
250
|
-
},
|
|
251
|
-
{ flush: "sync" }
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
414
|
if (pollInterval) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
onBeforeMount(() => {
|
|
265
|
-
void fetchInitialData();
|
|
266
|
-
});
|
|
267
|
-
} else {
|
|
268
|
-
await fetchInitialData();
|
|
269
|
-
}
|
|
415
|
+
onMounted(() => {
|
|
416
|
+
pollTimer.value = setInterval(pollData, pollInterval);
|
|
417
|
+
});
|
|
418
|
+
onUnmounted(() => {
|
|
419
|
+
if (pollTimer.value) {
|
|
420
|
+
clearInterval(pollTimer.value);
|
|
421
|
+
pollTimer.value = null;
|
|
422
|
+
}
|
|
423
|
+
});
|
|
270
424
|
}
|
|
425
|
+
const status = ref("idle");
|
|
426
|
+
watch(
|
|
427
|
+
[() => asyncData.status.value, loadMorePending, loadMoreError],
|
|
428
|
+
([asyncStatus]) => {
|
|
429
|
+
if (loadMoreError.value) {
|
|
430
|
+
status.value = "error";
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
status.value = loadMorePending.value ? "pending" : asyncStatus;
|
|
434
|
+
},
|
|
435
|
+
{ immediate: true }
|
|
436
|
+
);
|
|
437
|
+
const error = ref(void 0);
|
|
438
|
+
watch(
|
|
439
|
+
[() => asyncData.error.value, loadMoreError],
|
|
440
|
+
([initialError]) => {
|
|
441
|
+
error.value = loadMoreError.value ?? (initialError ? normalizeFetchError(initialError) : void 0);
|
|
442
|
+
},
|
|
443
|
+
{ immediate: true }
|
|
444
|
+
);
|
|
445
|
+
const pending = ref(false);
|
|
446
|
+
watch(
|
|
447
|
+
[() => asyncData.pending.value, loadMorePending],
|
|
448
|
+
([initialPending]) => {
|
|
449
|
+
pending.value = Boolean(initialPending || loadMorePending.value);
|
|
450
|
+
},
|
|
451
|
+
{ immediate: true }
|
|
452
|
+
);
|
|
453
|
+
const loading = readonly(pending);
|
|
271
454
|
return {
|
|
272
455
|
data,
|
|
273
|
-
loading
|
|
456
|
+
loading,
|
|
274
457
|
error: readonly(error),
|
|
275
458
|
hasNextPage: readonly(hasNextPage),
|
|
276
459
|
isLoadMoreTriggered: readonly(isLoadMoreTriggered),
|
|
277
460
|
loadMore,
|
|
278
461
|
refresh,
|
|
462
|
+
execute,
|
|
463
|
+
clear,
|
|
279
464
|
init
|
|
280
465
|
};
|
|
281
466
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# useCursorHttp
|
|
2
2
|
|
|
3
|
-
`useCursorHttp` wraps cursor-based APIs for infinite lists. It
|
|
3
|
+
`useCursorHttp` wraps cursor-based APIs for infinite lists. It is Nuxt lifecycle-aware, so it can fetch on the server, hydrate on the client without duplicate requests, and keep `lazy`/`immediate` behavior familiar.
|
|
4
4
|
|
|
5
5
|
## Signature
|
|
6
6
|
|
|
@@ -10,14 +10,24 @@ const result = await useCursorHttp(url, options?)
|
|
|
10
10
|
|
|
11
11
|
## Options
|
|
12
12
|
|
|
13
|
-
- `
|
|
13
|
+
- `server` (boolean): fetch during SSR. Default: `true`.
|
|
14
|
+
- `lazy` (boolean): do not block navigation while fetching. Default: `false`.
|
|
14
15
|
- `immediate` (boolean): run the first fetch on creation. Default: `true`.
|
|
16
|
+
- `cache` (string): caching strategy. Default: `none`.
|
|
17
|
+
- `none`: always fresh; uses a unique instance key and clears cached data on unmount.
|
|
18
|
+
- `payload`: reuse SSR/hydration payload only.
|
|
19
|
+
- `static`: reuse static payload only.
|
|
20
|
+
- `both`: reuse Nuxt payload/static data.
|
|
21
|
+
- `default` (function): initial value before the request resolves.
|
|
22
|
+
- `watch` (sources): auto-refresh when reactive sources change.
|
|
23
|
+
- `dedupe` (string): control overlapping requests (`cancel` or `defer`).
|
|
24
|
+
- `timeout` (number): abort slow requests.
|
|
15
25
|
- `pollInterval` (number): poll interval in ms for new items.
|
|
16
26
|
- `fetcher` (function): custom fetcher. Defaults to Nuxt `$fetch`.
|
|
17
27
|
- `fetchOptions` (object): passed to `$fetch`, supports refs for reactive params.
|
|
18
28
|
- `meta` (object): response meta keys.
|
|
19
|
-
- `meta.cursorKey` (string): key for the next cursor.
|
|
20
|
-
- `meta.hasMoreKey` (string): key for more data.
|
|
29
|
+
- `meta.cursorKey` (string): key for the next cursor.
|
|
30
|
+
- `meta.hasMoreKey` (string): key for more data.
|
|
21
31
|
- `itemKey` (string): unique item id to dedupe polls. Default: `id`.
|
|
22
32
|
- `cursorParam` (string): query param for cursor. Default: `cursor`.
|
|
23
33
|
|
|
@@ -30,6 +40,8 @@ const result = await useCursorHttp(url, options?)
|
|
|
30
40
|
- `isLoadMoreTriggered` (readonly ref)
|
|
31
41
|
- `loadMore()` (function)
|
|
32
42
|
- `refresh()` (function)
|
|
43
|
+
- `execute()` (function)
|
|
44
|
+
- `clear()` (function)
|
|
33
45
|
- `init()` (function)
|
|
34
46
|
|
|
35
47
|
## Examples
|