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