@spoosh/core 0.1.0-beta.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/LICENSE +21 -0
- package/README.md +247 -0
- package/dist/index.d.mts +1318 -0
- package/dist/index.d.ts +1318 -0
- package/dist/index.js +1441 -0
- package/dist/index.mjs +1418 -0
- package/package.json +44 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1441 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
HTTP_METHODS: () => HTTP_METHODS2,
|
|
24
|
+
applyMiddlewares: () => applyMiddlewares,
|
|
25
|
+
buildUrl: () => buildUrl,
|
|
26
|
+
composeMiddlewares: () => composeMiddlewares,
|
|
27
|
+
createClient: () => createClient,
|
|
28
|
+
createEventEmitter: () => createEventEmitter,
|
|
29
|
+
createInfiniteReadController: () => createInfiniteReadController,
|
|
30
|
+
createInitialState: () => createInitialState,
|
|
31
|
+
createMiddleware: () => createMiddleware,
|
|
32
|
+
createOperationController: () => createOperationController,
|
|
33
|
+
createPluginExecutor: () => createPluginExecutor,
|
|
34
|
+
createPluginRegistry: () => createPluginRegistry,
|
|
35
|
+
createProxyHandler: () => createProxyHandler,
|
|
36
|
+
createSelectorProxy: () => createSelectorProxy,
|
|
37
|
+
createSpoosh: () => createSpoosh,
|
|
38
|
+
createStateManager: () => createStateManager,
|
|
39
|
+
executeFetch: () => executeFetch,
|
|
40
|
+
extractMethodFromSelector: () => extractMethodFromSelector,
|
|
41
|
+
extractPathFromSelector: () => extractPathFromSelector,
|
|
42
|
+
generateTags: () => generateTags,
|
|
43
|
+
isJsonBody: () => isJsonBody,
|
|
44
|
+
mergeHeaders: () => mergeHeaders,
|
|
45
|
+
objectToFormData: () => objectToFormData,
|
|
46
|
+
objectToUrlEncoded: () => objectToUrlEncoded,
|
|
47
|
+
resolveHeadersToRecord: () => resolveHeadersToRecord,
|
|
48
|
+
resolvePath: () => resolvePath,
|
|
49
|
+
resolveTags: () => resolveTags,
|
|
50
|
+
setHeaders: () => setHeaders,
|
|
51
|
+
sortObjectKeys: () => sortObjectKeys
|
|
52
|
+
});
|
|
53
|
+
module.exports = __toCommonJS(src_exports);
|
|
54
|
+
|
|
55
|
+
// src/middleware.ts
|
|
56
|
+
function createMiddleware(name, phase, handler) {
|
|
57
|
+
return { name, phase, handler };
|
|
58
|
+
}
|
|
59
|
+
async function applyMiddlewares(context, middlewares, phase) {
|
|
60
|
+
const phaseMiddlewares = middlewares.filter((m) => m.phase === phase);
|
|
61
|
+
let ctx = context;
|
|
62
|
+
for (const middleware of phaseMiddlewares) {
|
|
63
|
+
ctx = await middleware.handler(ctx);
|
|
64
|
+
}
|
|
65
|
+
return ctx;
|
|
66
|
+
}
|
|
67
|
+
function composeMiddlewares(...middlewareLists) {
|
|
68
|
+
return middlewareLists.flat().filter(Boolean);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/utils/buildUrl.ts
|
|
72
|
+
function stringifyQuery(query) {
|
|
73
|
+
const parts = [];
|
|
74
|
+
for (const [key, value] of Object.entries(query)) {
|
|
75
|
+
if (value === void 0 || value === null || value === "") {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
parts.push(
|
|
79
|
+
`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return parts.join("&");
|
|
83
|
+
}
|
|
84
|
+
function buildUrl(baseUrl, path, query) {
|
|
85
|
+
const isAbsolute = /^https?:\/\//.test(baseUrl);
|
|
86
|
+
if (isAbsolute) {
|
|
87
|
+
const normalizedBase = baseUrl.replace(/\/?$/, "/");
|
|
88
|
+
const url = new URL(path.join("/"), normalizedBase);
|
|
89
|
+
if (query) {
|
|
90
|
+
url.search = stringifyQuery(query);
|
|
91
|
+
}
|
|
92
|
+
return url.toString();
|
|
93
|
+
}
|
|
94
|
+
const cleanBase = `/${baseUrl.replace(/^\/|\/$/g, "")}`;
|
|
95
|
+
const pathStr = path.length > 0 ? `/${path.join("/")}` : "";
|
|
96
|
+
const queryStr = query ? stringifyQuery(query) : "";
|
|
97
|
+
return `${cleanBase}${pathStr}${queryStr ? `?${queryStr}` : ""}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/utils/generateTags.ts
|
|
101
|
+
function generateTags(path) {
|
|
102
|
+
return path.map((_, i) => path.slice(0, i + 1).join("/"));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/utils/isJsonBody.ts
|
|
106
|
+
function isJsonBody(body) {
|
|
107
|
+
if (body === null || body === void 0) return false;
|
|
108
|
+
if (body instanceof FormData) return false;
|
|
109
|
+
if (body instanceof Blob) return false;
|
|
110
|
+
if (body instanceof ArrayBuffer) return false;
|
|
111
|
+
if (body instanceof URLSearchParams) return false;
|
|
112
|
+
if (body instanceof ReadableStream) return false;
|
|
113
|
+
if (typeof body === "string") return false;
|
|
114
|
+
return typeof body === "object";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/utils/mergeHeaders.ts
|
|
118
|
+
async function resolveHeaders(headers) {
|
|
119
|
+
if (!headers) return void 0;
|
|
120
|
+
if (typeof headers === "function") {
|
|
121
|
+
return await headers();
|
|
122
|
+
}
|
|
123
|
+
return headers;
|
|
124
|
+
}
|
|
125
|
+
function headersInitToRecord(headers) {
|
|
126
|
+
return Object.fromEntries(new Headers(headers));
|
|
127
|
+
}
|
|
128
|
+
async function resolveHeadersToRecord(headers) {
|
|
129
|
+
const resolved = await resolveHeaders(headers);
|
|
130
|
+
if (!resolved) return {};
|
|
131
|
+
return headersInitToRecord(resolved);
|
|
132
|
+
}
|
|
133
|
+
async function mergeHeaders(defaultHeaders, requestHeaders) {
|
|
134
|
+
const resolved1 = await resolveHeaders(defaultHeaders);
|
|
135
|
+
const resolved2 = await resolveHeaders(requestHeaders);
|
|
136
|
+
if (!resolved1 && !resolved2) return void 0;
|
|
137
|
+
if (!resolved1) return resolved2;
|
|
138
|
+
if (!resolved2) return resolved1;
|
|
139
|
+
return {
|
|
140
|
+
...Object.fromEntries(new Headers(resolved1)),
|
|
141
|
+
...Object.fromEntries(new Headers(resolved2))
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function setHeaders(requestOptions, newHeaders) {
|
|
145
|
+
const existing = requestOptions.headers;
|
|
146
|
+
if (!existing || typeof existing === "object" && !Array.isArray(existing) && !(existing instanceof Headers)) {
|
|
147
|
+
requestOptions.headers = {
|
|
148
|
+
...existing,
|
|
149
|
+
...newHeaders
|
|
150
|
+
};
|
|
151
|
+
} else {
|
|
152
|
+
requestOptions.headers = {
|
|
153
|
+
...newHeaders
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/utils/objectToFormData.ts
|
|
159
|
+
function objectToFormData(obj) {
|
|
160
|
+
const formData = new FormData();
|
|
161
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
162
|
+
if (value === null || value === void 0) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (value instanceof Blob || value instanceof File) {
|
|
166
|
+
formData.append(key, value);
|
|
167
|
+
} else if (Array.isArray(value)) {
|
|
168
|
+
for (const entry of value) {
|
|
169
|
+
if (entry instanceof Blob || entry instanceof File) {
|
|
170
|
+
formData.append(key, entry);
|
|
171
|
+
} else if (typeof entry === "object" && entry !== null) {
|
|
172
|
+
formData.append(key, JSON.stringify(entry));
|
|
173
|
+
} else {
|
|
174
|
+
formData.append(key, String(entry));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} else if (typeof value === "object") {
|
|
178
|
+
formData.append(key, JSON.stringify(value));
|
|
179
|
+
} else {
|
|
180
|
+
formData.append(key, String(value));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return formData;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// src/utils/objectToUrlEncoded.ts
|
|
187
|
+
function objectToUrlEncoded(obj) {
|
|
188
|
+
const params = new URLSearchParams();
|
|
189
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
190
|
+
if (value === void 0 || value === null) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (Array.isArray(value)) {
|
|
194
|
+
for (const item of value) {
|
|
195
|
+
if (item !== void 0 && item !== null) {
|
|
196
|
+
params.append(key, String(item));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} else if (typeof value === "object") {
|
|
200
|
+
params.append(key, JSON.stringify(value));
|
|
201
|
+
} else {
|
|
202
|
+
params.append(key, String(value));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return params.toString();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/utils/sortObjectKeys.ts
|
|
209
|
+
function sortObjectKeys(obj, seen = /* @__PURE__ */ new WeakSet()) {
|
|
210
|
+
if (obj === null || typeof obj !== "object") return obj;
|
|
211
|
+
if (seen.has(obj)) {
|
|
212
|
+
return "[Circular]";
|
|
213
|
+
}
|
|
214
|
+
seen.add(obj);
|
|
215
|
+
if (Array.isArray(obj)) {
|
|
216
|
+
return obj.map((item) => sortObjectKeys(item, seen));
|
|
217
|
+
}
|
|
218
|
+
return Object.keys(obj).sort().reduce(
|
|
219
|
+
(sorted, key) => {
|
|
220
|
+
sorted[key] = sortObjectKeys(
|
|
221
|
+
obj[key],
|
|
222
|
+
seen
|
|
223
|
+
);
|
|
224
|
+
return sorted;
|
|
225
|
+
},
|
|
226
|
+
{}
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/utils/path-utils.ts
|
|
231
|
+
function resolveTags(options, resolvedPath) {
|
|
232
|
+
const customTags = options?.tags;
|
|
233
|
+
const additionalTags = options?.additionalTags ?? [];
|
|
234
|
+
const baseTags = customTags ?? generateTags(resolvedPath);
|
|
235
|
+
return [...baseTags, ...additionalTags];
|
|
236
|
+
}
|
|
237
|
+
function resolvePath(path, params) {
|
|
238
|
+
if (!params) return path;
|
|
239
|
+
return path.map((segment) => {
|
|
240
|
+
if (segment.startsWith(":")) {
|
|
241
|
+
const paramName = segment.slice(1);
|
|
242
|
+
const value = params[paramName];
|
|
243
|
+
if (value === void 0) {
|
|
244
|
+
throw new Error(`Missing path parameter: ${paramName}`);
|
|
245
|
+
}
|
|
246
|
+
return String(value);
|
|
247
|
+
}
|
|
248
|
+
return segment;
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// src/fetch.ts
|
|
253
|
+
var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
254
|
+
var isNetworkError = (err) => err instanceof TypeError;
|
|
255
|
+
var isAbortError = (err) => err instanceof DOMException && err.name === "AbortError";
|
|
256
|
+
async function executeFetch(baseUrl, path, method, defaultOptions, requestOptions, nextTags) {
|
|
257
|
+
const middlewares = defaultOptions.middlewares ?? [];
|
|
258
|
+
let context = {
|
|
259
|
+
baseUrl,
|
|
260
|
+
path,
|
|
261
|
+
method,
|
|
262
|
+
defaultOptions,
|
|
263
|
+
requestOptions,
|
|
264
|
+
metadata: {}
|
|
265
|
+
};
|
|
266
|
+
if (middlewares.length > 0) {
|
|
267
|
+
context = await applyMiddlewares(context, middlewares, "before");
|
|
268
|
+
}
|
|
269
|
+
const response = await executeCoreFetch({
|
|
270
|
+
baseUrl: context.baseUrl,
|
|
271
|
+
path: context.path,
|
|
272
|
+
method: context.method,
|
|
273
|
+
defaultOptions: context.defaultOptions,
|
|
274
|
+
requestOptions: context.requestOptions,
|
|
275
|
+
middlewareFetchInit: context.fetchInit,
|
|
276
|
+
nextTags
|
|
277
|
+
});
|
|
278
|
+
context.response = response;
|
|
279
|
+
if (middlewares.length > 0) {
|
|
280
|
+
context = await applyMiddlewares(context, middlewares, "after");
|
|
281
|
+
}
|
|
282
|
+
return context.response;
|
|
283
|
+
}
|
|
284
|
+
function buildInputFields(requestOptions) {
|
|
285
|
+
const fields = {};
|
|
286
|
+
if (requestOptions?.query !== void 0) {
|
|
287
|
+
fields.query = requestOptions.query;
|
|
288
|
+
}
|
|
289
|
+
if (requestOptions?.body !== void 0) {
|
|
290
|
+
fields.body = requestOptions.body;
|
|
291
|
+
}
|
|
292
|
+
if (requestOptions?.formData !== void 0) {
|
|
293
|
+
fields.formData = requestOptions.formData;
|
|
294
|
+
}
|
|
295
|
+
if (requestOptions?.urlEncoded !== void 0) {
|
|
296
|
+
fields.urlEncoded = requestOptions.urlEncoded;
|
|
297
|
+
}
|
|
298
|
+
if (requestOptions?.params !== void 0) {
|
|
299
|
+
fields.params = requestOptions.params;
|
|
300
|
+
}
|
|
301
|
+
if (Object.keys(fields).length === 0) {
|
|
302
|
+
return {};
|
|
303
|
+
}
|
|
304
|
+
return { input: fields };
|
|
305
|
+
}
|
|
306
|
+
async function executeCoreFetch(config) {
|
|
307
|
+
const {
|
|
308
|
+
baseUrl,
|
|
309
|
+
path,
|
|
310
|
+
method,
|
|
311
|
+
defaultOptions,
|
|
312
|
+
requestOptions,
|
|
313
|
+
middlewareFetchInit,
|
|
314
|
+
nextTags
|
|
315
|
+
} = config;
|
|
316
|
+
const {
|
|
317
|
+
middlewares: _,
|
|
318
|
+
headers: defaultHeaders,
|
|
319
|
+
...fetchDefaults
|
|
320
|
+
} = defaultOptions;
|
|
321
|
+
void _;
|
|
322
|
+
const inputFields = buildInputFields(requestOptions);
|
|
323
|
+
const maxRetries = requestOptions?.retries ?? 3;
|
|
324
|
+
const baseDelay = requestOptions?.retryDelay ?? 1e3;
|
|
325
|
+
const retryCount = maxRetries === false ? 0 : maxRetries;
|
|
326
|
+
const url = buildUrl(baseUrl, path, requestOptions?.query);
|
|
327
|
+
let headers = await mergeHeaders(defaultHeaders, requestOptions?.headers);
|
|
328
|
+
const fetchInit = {
|
|
329
|
+
...fetchDefaults,
|
|
330
|
+
...middlewareFetchInit,
|
|
331
|
+
method
|
|
332
|
+
};
|
|
333
|
+
if (headers) {
|
|
334
|
+
fetchInit.headers = headers;
|
|
335
|
+
}
|
|
336
|
+
fetchInit.cache = requestOptions?.cache ?? fetchDefaults?.cache;
|
|
337
|
+
if (nextTags) {
|
|
338
|
+
const autoTags = generateTags(path);
|
|
339
|
+
const userNext = requestOptions?.next;
|
|
340
|
+
fetchInit.next = {
|
|
341
|
+
tags: userNext?.tags ?? autoTags,
|
|
342
|
+
...userNext?.revalidate !== void 0 && {
|
|
343
|
+
revalidate: userNext.revalidate
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
if (requestOptions?.signal) {
|
|
348
|
+
fetchInit.signal = requestOptions.signal;
|
|
349
|
+
}
|
|
350
|
+
if (requestOptions?.formData !== void 0) {
|
|
351
|
+
fetchInit.body = objectToFormData(
|
|
352
|
+
requestOptions.formData
|
|
353
|
+
);
|
|
354
|
+
} else if (requestOptions?.urlEncoded !== void 0) {
|
|
355
|
+
fetchInit.body = objectToUrlEncoded(
|
|
356
|
+
requestOptions.urlEncoded
|
|
357
|
+
);
|
|
358
|
+
headers = await mergeHeaders(headers, {
|
|
359
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
360
|
+
});
|
|
361
|
+
if (headers) {
|
|
362
|
+
fetchInit.headers = headers;
|
|
363
|
+
}
|
|
364
|
+
} else if (requestOptions?.body !== void 0) {
|
|
365
|
+
if (isJsonBody(requestOptions.body)) {
|
|
366
|
+
fetchInit.body = JSON.stringify(requestOptions.body);
|
|
367
|
+
headers = await mergeHeaders(headers, {
|
|
368
|
+
"Content-Type": "application/json"
|
|
369
|
+
});
|
|
370
|
+
if (headers) {
|
|
371
|
+
fetchInit.headers = headers;
|
|
372
|
+
}
|
|
373
|
+
} else {
|
|
374
|
+
fetchInit.body = requestOptions.body;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
let lastError;
|
|
378
|
+
for (let attempt = 0; attempt <= retryCount; attempt++) {
|
|
379
|
+
try {
|
|
380
|
+
const res = await fetch(url, fetchInit);
|
|
381
|
+
const status = res.status;
|
|
382
|
+
const resHeaders = res.headers;
|
|
383
|
+
const contentType = resHeaders.get("content-type");
|
|
384
|
+
const isJson = contentType?.includes("application/json");
|
|
385
|
+
const body = isJson ? await res.json() : res;
|
|
386
|
+
if (res.ok) {
|
|
387
|
+
return {
|
|
388
|
+
status,
|
|
389
|
+
data: body,
|
|
390
|
+
headers: resHeaders,
|
|
391
|
+
error: void 0,
|
|
392
|
+
...inputFields
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
status,
|
|
397
|
+
error: body,
|
|
398
|
+
headers: resHeaders,
|
|
399
|
+
data: void 0,
|
|
400
|
+
...inputFields
|
|
401
|
+
};
|
|
402
|
+
} catch (err) {
|
|
403
|
+
if (isAbortError(err)) {
|
|
404
|
+
return {
|
|
405
|
+
status: 0,
|
|
406
|
+
error: err,
|
|
407
|
+
data: void 0,
|
|
408
|
+
aborted: true,
|
|
409
|
+
...inputFields
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
lastError = err;
|
|
413
|
+
if (isNetworkError(err) && attempt < retryCount) {
|
|
414
|
+
const delayMs = baseDelay * Math.pow(2, attempt);
|
|
415
|
+
await delay(delayMs);
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
return { status: 0, error: lastError, data: void 0, ...inputFields };
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return { status: 0, error: lastError, data: void 0, ...inputFields };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// src/proxy/handler.ts
|
|
425
|
+
var HTTP_METHODS = {
|
|
426
|
+
$get: "GET",
|
|
427
|
+
$post: "POST",
|
|
428
|
+
$put: "PUT",
|
|
429
|
+
$patch: "PATCH",
|
|
430
|
+
$delete: "DELETE"
|
|
431
|
+
};
|
|
432
|
+
function createProxyHandler(config) {
|
|
433
|
+
const {
|
|
434
|
+
baseUrl,
|
|
435
|
+
defaultOptions,
|
|
436
|
+
path = [],
|
|
437
|
+
fetchExecutor = executeFetch,
|
|
438
|
+
nextTags
|
|
439
|
+
} = config;
|
|
440
|
+
const handler = {
|
|
441
|
+
get(_target, prop) {
|
|
442
|
+
if (typeof prop === "symbol") return void 0;
|
|
443
|
+
const method = HTTP_METHODS[prop];
|
|
444
|
+
if (method) {
|
|
445
|
+
return (options) => fetchExecutor(
|
|
446
|
+
baseUrl,
|
|
447
|
+
path,
|
|
448
|
+
method,
|
|
449
|
+
defaultOptions,
|
|
450
|
+
options,
|
|
451
|
+
nextTags
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
return createProxyHandler({
|
|
455
|
+
baseUrl,
|
|
456
|
+
defaultOptions,
|
|
457
|
+
path: [...path, prop],
|
|
458
|
+
fetchExecutor,
|
|
459
|
+
nextTags
|
|
460
|
+
});
|
|
461
|
+
},
|
|
462
|
+
// Handles function call syntax for dynamic segments: api.posts("123"), api.users(userId)
|
|
463
|
+
// Q. Why allow this syntax?
|
|
464
|
+
// A. To support dynamic type inference in frameworks where property access with variables is not possible.
|
|
465
|
+
// Eg. api.posts[":id"].$get() <-- TypeScript sees this as bracket notation with a string literal, can't infer param types
|
|
466
|
+
// But api.posts(":id").$get() <-- TypeScript can capture ":id" as a template literal type, enabling params: { id: string } inference
|
|
467
|
+
apply(_target, _thisArg, args) {
|
|
468
|
+
const [segment] = args;
|
|
469
|
+
return createProxyHandler({
|
|
470
|
+
baseUrl,
|
|
471
|
+
defaultOptions,
|
|
472
|
+
path: [...path, segment],
|
|
473
|
+
fetchExecutor,
|
|
474
|
+
nextTags
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
const noop = () => {
|
|
479
|
+
};
|
|
480
|
+
return new Proxy(noop, handler);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// src/proxy/selector-proxy.ts
|
|
484
|
+
var HTTP_METHODS2 = [
|
|
485
|
+
"$get",
|
|
486
|
+
"$post",
|
|
487
|
+
"$put",
|
|
488
|
+
"$patch",
|
|
489
|
+
"$delete"
|
|
490
|
+
];
|
|
491
|
+
function createSelectorProxy(onCapture) {
|
|
492
|
+
const createProxy = (path = []) => {
|
|
493
|
+
return new Proxy(() => {
|
|
494
|
+
}, {
|
|
495
|
+
get(_, prop) {
|
|
496
|
+
if (HTTP_METHODS2.includes(prop)) {
|
|
497
|
+
const selectorFn = (options) => {
|
|
498
|
+
onCapture?.({
|
|
499
|
+
call: { path, method: prop, options },
|
|
500
|
+
selector: null
|
|
501
|
+
});
|
|
502
|
+
return Promise.resolve({ data: void 0 });
|
|
503
|
+
};
|
|
504
|
+
selectorFn.__selectorPath = path;
|
|
505
|
+
selectorFn.__selectorMethod = prop;
|
|
506
|
+
onCapture?.({
|
|
507
|
+
call: null,
|
|
508
|
+
selector: { path, method: prop }
|
|
509
|
+
});
|
|
510
|
+
return selectorFn;
|
|
511
|
+
}
|
|
512
|
+
return createProxy([...path, prop]);
|
|
513
|
+
},
|
|
514
|
+
// Handles function call syntax for dynamic segments: api.posts("123"), api.users(userId)
|
|
515
|
+
apply(_, __, args) {
|
|
516
|
+
const [segment] = args;
|
|
517
|
+
return createProxy([...path, segment]);
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
};
|
|
521
|
+
return createProxy();
|
|
522
|
+
}
|
|
523
|
+
function extractPathFromSelector(fn) {
|
|
524
|
+
return fn.__selectorPath ?? [];
|
|
525
|
+
}
|
|
526
|
+
function extractMethodFromSelector(fn) {
|
|
527
|
+
return fn.__selectorMethod;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// src/state/manager.ts
|
|
531
|
+
function createInitialState() {
|
|
532
|
+
return {
|
|
533
|
+
data: void 0,
|
|
534
|
+
error: void 0,
|
|
535
|
+
timestamp: 0
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
function generateSelfTagFromKey(key) {
|
|
539
|
+
try {
|
|
540
|
+
const parsed = JSON.parse(key);
|
|
541
|
+
return parsed.path?.join("/");
|
|
542
|
+
} catch {
|
|
543
|
+
return void 0;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
function createStateManager() {
|
|
547
|
+
const cache = /* @__PURE__ */ new Map();
|
|
548
|
+
const subscribers = /* @__PURE__ */ new Map();
|
|
549
|
+
const pendingPromises = /* @__PURE__ */ new Map();
|
|
550
|
+
const notifySubscribers = (key) => {
|
|
551
|
+
const subs = subscribers.get(key);
|
|
552
|
+
subs?.forEach((cb) => cb());
|
|
553
|
+
};
|
|
554
|
+
return {
|
|
555
|
+
createQueryKey({ path, method, options }) {
|
|
556
|
+
return JSON.stringify(
|
|
557
|
+
sortObjectKeys({
|
|
558
|
+
path,
|
|
559
|
+
method,
|
|
560
|
+
options
|
|
561
|
+
})
|
|
562
|
+
);
|
|
563
|
+
},
|
|
564
|
+
getCache(key) {
|
|
565
|
+
return cache.get(key);
|
|
566
|
+
},
|
|
567
|
+
setCache(key, entry) {
|
|
568
|
+
const existing = cache.get(key);
|
|
569
|
+
if (existing) {
|
|
570
|
+
existing.state = { ...existing.state, ...entry.state };
|
|
571
|
+
if (entry.tags) {
|
|
572
|
+
existing.tags = entry.tags;
|
|
573
|
+
}
|
|
574
|
+
if (entry.previousData !== void 0) {
|
|
575
|
+
existing.previousData = entry.previousData;
|
|
576
|
+
}
|
|
577
|
+
if (entry.stale !== void 0) {
|
|
578
|
+
existing.stale = entry.stale;
|
|
579
|
+
}
|
|
580
|
+
notifySubscribers(key);
|
|
581
|
+
} else {
|
|
582
|
+
const newEntry = {
|
|
583
|
+
state: entry.state ?? createInitialState(),
|
|
584
|
+
tags: entry.tags ?? [],
|
|
585
|
+
pluginResult: /* @__PURE__ */ new Map(),
|
|
586
|
+
selfTag: generateSelfTagFromKey(key),
|
|
587
|
+
previousData: entry.previousData,
|
|
588
|
+
stale: entry.stale
|
|
589
|
+
};
|
|
590
|
+
cache.set(key, newEntry);
|
|
591
|
+
notifySubscribers(key);
|
|
592
|
+
}
|
|
593
|
+
},
|
|
594
|
+
deleteCache(key) {
|
|
595
|
+
cache.delete(key);
|
|
596
|
+
},
|
|
597
|
+
subscribeCache(key, callback) {
|
|
598
|
+
let subs = subscribers.get(key);
|
|
599
|
+
if (!subs) {
|
|
600
|
+
subs = /* @__PURE__ */ new Set();
|
|
601
|
+
subscribers.set(key, subs);
|
|
602
|
+
}
|
|
603
|
+
subs.add(callback);
|
|
604
|
+
return () => {
|
|
605
|
+
subs.delete(callback);
|
|
606
|
+
if (subs.size === 0) {
|
|
607
|
+
subscribers.delete(key);
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
},
|
|
611
|
+
getCacheByTags(tags) {
|
|
612
|
+
for (const entry of cache.values()) {
|
|
613
|
+
const hasMatch = entry.tags.some((tag) => tags.includes(tag));
|
|
614
|
+
if (hasMatch && entry.state.data !== void 0) {
|
|
615
|
+
return entry;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return void 0;
|
|
619
|
+
},
|
|
620
|
+
getCacheEntriesByTags(tags) {
|
|
621
|
+
const entries = [];
|
|
622
|
+
cache.forEach((entry, key) => {
|
|
623
|
+
const hasMatch = entry.tags.some((tag) => tags.includes(tag));
|
|
624
|
+
if (hasMatch) {
|
|
625
|
+
entries.push({
|
|
626
|
+
key,
|
|
627
|
+
entry
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
return entries;
|
|
632
|
+
},
|
|
633
|
+
getCacheEntriesBySelfTag(selfTag) {
|
|
634
|
+
const entries = [];
|
|
635
|
+
cache.forEach((entry, key) => {
|
|
636
|
+
if (entry.selfTag === selfTag) {
|
|
637
|
+
entries.push({
|
|
638
|
+
key,
|
|
639
|
+
entry
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
return entries;
|
|
644
|
+
},
|
|
645
|
+
setPluginResult(key, data) {
|
|
646
|
+
const entry = cache.get(key);
|
|
647
|
+
if (entry) {
|
|
648
|
+
for (const [name, value] of Object.entries(data)) {
|
|
649
|
+
entry.pluginResult.set(name, value);
|
|
650
|
+
}
|
|
651
|
+
notifySubscribers(key);
|
|
652
|
+
}
|
|
653
|
+
},
|
|
654
|
+
markStale(tags) {
|
|
655
|
+
cache.forEach((entry) => {
|
|
656
|
+
const hasMatch = entry.tags.some((tag) => tags.includes(tag));
|
|
657
|
+
if (hasMatch) {
|
|
658
|
+
entry.stale = true;
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
},
|
|
662
|
+
getAllCacheEntries() {
|
|
663
|
+
const entries = [];
|
|
664
|
+
cache.forEach((entry, key) => {
|
|
665
|
+
entries.push({
|
|
666
|
+
key,
|
|
667
|
+
entry
|
|
668
|
+
});
|
|
669
|
+
});
|
|
670
|
+
return entries;
|
|
671
|
+
},
|
|
672
|
+
getSize() {
|
|
673
|
+
return cache.size;
|
|
674
|
+
},
|
|
675
|
+
setPendingPromise(key, promise) {
|
|
676
|
+
if (promise === void 0) {
|
|
677
|
+
pendingPromises.delete(key);
|
|
678
|
+
} else {
|
|
679
|
+
pendingPromises.set(key, promise);
|
|
680
|
+
}
|
|
681
|
+
},
|
|
682
|
+
getPendingPromise(key) {
|
|
683
|
+
return pendingPromises.get(key);
|
|
684
|
+
},
|
|
685
|
+
clear() {
|
|
686
|
+
cache.clear();
|
|
687
|
+
subscribers.clear();
|
|
688
|
+
pendingPromises.clear();
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// src/events/emitter.ts
|
|
694
|
+
function createEventEmitter() {
|
|
695
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
696
|
+
return {
|
|
697
|
+
on(event, callback) {
|
|
698
|
+
if (!listeners.has(event)) {
|
|
699
|
+
listeners.set(event, /* @__PURE__ */ new Set());
|
|
700
|
+
}
|
|
701
|
+
listeners.get(event).add(callback);
|
|
702
|
+
return () => {
|
|
703
|
+
listeners.get(event)?.delete(callback);
|
|
704
|
+
};
|
|
705
|
+
},
|
|
706
|
+
emit(event, payload) {
|
|
707
|
+
listeners.get(event)?.forEach((cb) => cb(payload));
|
|
708
|
+
},
|
|
709
|
+
off(event, callback) {
|
|
710
|
+
listeners.get(event)?.delete(callback);
|
|
711
|
+
},
|
|
712
|
+
clear() {
|
|
713
|
+
listeners.clear();
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// src/plugins/executor.ts
|
|
719
|
+
function validateDependencies(plugins) {
|
|
720
|
+
const names = new Set(plugins.map((p) => p.name));
|
|
721
|
+
for (const plugin of plugins) {
|
|
722
|
+
for (const dep of plugin.dependencies ?? []) {
|
|
723
|
+
if (!names.has(dep)) {
|
|
724
|
+
throw new Error(
|
|
725
|
+
`Plugin "${plugin.name}" depends on "${dep}" which is not registered`
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
function sortByDependencies(plugins) {
|
|
732
|
+
const sorted = [];
|
|
733
|
+
const visited = /* @__PURE__ */ new Set();
|
|
734
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
735
|
+
const pluginMap = new Map(plugins.map((p) => [p.name, p]));
|
|
736
|
+
function visit(plugin) {
|
|
737
|
+
if (visited.has(plugin.name)) return;
|
|
738
|
+
if (visiting.has(plugin.name)) {
|
|
739
|
+
throw new Error(
|
|
740
|
+
`Circular dependency detected involving "${plugin.name}"`
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
visiting.add(plugin.name);
|
|
744
|
+
for (const dep of plugin.dependencies ?? []) {
|
|
745
|
+
const depPlugin = pluginMap.get(dep);
|
|
746
|
+
if (depPlugin) visit(depPlugin);
|
|
747
|
+
}
|
|
748
|
+
visiting.delete(plugin.name);
|
|
749
|
+
visited.add(plugin.name);
|
|
750
|
+
sorted.push(plugin);
|
|
751
|
+
}
|
|
752
|
+
for (const plugin of plugins) {
|
|
753
|
+
visit(plugin);
|
|
754
|
+
}
|
|
755
|
+
return sorted;
|
|
756
|
+
}
|
|
757
|
+
function createPluginExecutor(initialPlugins = []) {
|
|
758
|
+
validateDependencies(initialPlugins);
|
|
759
|
+
const plugins = sortByDependencies(initialPlugins);
|
|
760
|
+
const frozenPlugins = Object.freeze([...plugins]);
|
|
761
|
+
const createPluginAccessor = (context) => ({
|
|
762
|
+
get(name) {
|
|
763
|
+
const plugin = plugins.find((p) => p.name === name);
|
|
764
|
+
return plugin?.exports?.(context);
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
const executeLifecycleImpl = async (phase, operationType, context) => {
|
|
768
|
+
for (const plugin of plugins) {
|
|
769
|
+
if (!plugin.operations.includes(operationType)) {
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
const handler = plugin.lifecycle?.[phase];
|
|
773
|
+
if (!handler) {
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
await handler(context);
|
|
777
|
+
}
|
|
778
|
+
};
|
|
779
|
+
const executeUpdateLifecycleImpl = async (operationType, context, previousContext) => {
|
|
780
|
+
for (const plugin of plugins) {
|
|
781
|
+
if (!plugin.operations.includes(operationType)) {
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
const handler = plugin.lifecycle?.onUpdate;
|
|
785
|
+
if (!handler) {
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
await handler(
|
|
789
|
+
context,
|
|
790
|
+
previousContext
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
return {
|
|
795
|
+
executeLifecycle: executeLifecycleImpl,
|
|
796
|
+
executeUpdateLifecycle: executeUpdateLifecycleImpl,
|
|
797
|
+
async executeMiddleware(operationType, context, coreFetch) {
|
|
798
|
+
const applicablePlugins = plugins.filter(
|
|
799
|
+
(p) => p.operations.includes(operationType)
|
|
800
|
+
);
|
|
801
|
+
const middlewares = applicablePlugins.filter((p) => p.middleware).map((p) => p.middleware);
|
|
802
|
+
let response;
|
|
803
|
+
if (middlewares.length === 0) {
|
|
804
|
+
response = await coreFetch();
|
|
805
|
+
} else {
|
|
806
|
+
const chain = middlewares.reduceRight(
|
|
807
|
+
(next, middleware) => {
|
|
808
|
+
return () => middleware(
|
|
809
|
+
context,
|
|
810
|
+
next
|
|
811
|
+
);
|
|
812
|
+
},
|
|
813
|
+
coreFetch
|
|
814
|
+
);
|
|
815
|
+
response = await chain();
|
|
816
|
+
}
|
|
817
|
+
for (const plugin of applicablePlugins) {
|
|
818
|
+
if (plugin.onResponse) {
|
|
819
|
+
await plugin.onResponse(
|
|
820
|
+
context,
|
|
821
|
+
response
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
return response;
|
|
826
|
+
},
|
|
827
|
+
getPlugins() {
|
|
828
|
+
return frozenPlugins;
|
|
829
|
+
},
|
|
830
|
+
createContext(input) {
|
|
831
|
+
const ctx = input;
|
|
832
|
+
ctx.plugins = createPluginAccessor(ctx);
|
|
833
|
+
ctx.headers = {};
|
|
834
|
+
ctx.setHeaders = (newHeaders) => {
|
|
835
|
+
ctx.headers = { ...ctx.headers, ...newHeaders };
|
|
836
|
+
ctx.requestOptions.headers = ctx.headers;
|
|
837
|
+
};
|
|
838
|
+
return ctx;
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// src/plugins/registry.ts
|
|
844
|
+
function createPluginRegistry(plugins) {
|
|
845
|
+
return {
|
|
846
|
+
plugins,
|
|
847
|
+
_options: {}
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// src/createSpoosh.ts
|
|
852
|
+
function createSpoosh(config) {
|
|
853
|
+
const {
|
|
854
|
+
baseUrl,
|
|
855
|
+
defaultOptions = {},
|
|
856
|
+
plugins = []
|
|
857
|
+
} = config;
|
|
858
|
+
const api = createProxyHandler({ baseUrl, defaultOptions });
|
|
859
|
+
const stateManager = createStateManager();
|
|
860
|
+
const eventEmitter = createEventEmitter();
|
|
861
|
+
const pluginExecutor = createPluginExecutor([...plugins]);
|
|
862
|
+
return {
|
|
863
|
+
api,
|
|
864
|
+
stateManager,
|
|
865
|
+
eventEmitter,
|
|
866
|
+
pluginExecutor,
|
|
867
|
+
config: {
|
|
868
|
+
baseUrl,
|
|
869
|
+
defaultOptions
|
|
870
|
+
},
|
|
871
|
+
_types: {
|
|
872
|
+
schema: void 0,
|
|
873
|
+
defaultError: void 0,
|
|
874
|
+
plugins
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// src/createClient.ts
|
|
880
|
+
function createClient(config) {
|
|
881
|
+
const { baseUrl, defaultOptions = {}, middlewares = [] } = config;
|
|
882
|
+
const optionsWithMiddlewares = { ...defaultOptions, middlewares };
|
|
883
|
+
return createProxyHandler({
|
|
884
|
+
baseUrl,
|
|
885
|
+
defaultOptions: optionsWithMiddlewares,
|
|
886
|
+
nextTags: true
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// src/operations/controller.ts
|
|
891
|
+
function createOperationController(options) {
|
|
892
|
+
const {
|
|
893
|
+
operationType,
|
|
894
|
+
path,
|
|
895
|
+
method,
|
|
896
|
+
tags,
|
|
897
|
+
requestOptions: initialRequestOptions,
|
|
898
|
+
stateManager,
|
|
899
|
+
eventEmitter,
|
|
900
|
+
pluginExecutor,
|
|
901
|
+
fetchFn,
|
|
902
|
+
hookId
|
|
903
|
+
} = options;
|
|
904
|
+
const queryKey = stateManager.createQueryKey({
|
|
905
|
+
path,
|
|
906
|
+
method,
|
|
907
|
+
options: initialRequestOptions
|
|
908
|
+
});
|
|
909
|
+
let abortController = null;
|
|
910
|
+
const metadata = /* @__PURE__ */ new Map();
|
|
911
|
+
let pluginOptions = void 0;
|
|
912
|
+
const initialState = createInitialState();
|
|
913
|
+
let cachedState = initialState;
|
|
914
|
+
let currentRequestTimestamp = Date.now();
|
|
915
|
+
let isFirstExecute = true;
|
|
916
|
+
const createContext = (requestOptions = {}, requestTimestamp = Date.now()) => {
|
|
917
|
+
const cached = stateManager.getCache(queryKey);
|
|
918
|
+
const state = cached?.state ?? createInitialState();
|
|
919
|
+
const resolvedTags = pluginOptions?.tags ?? tags;
|
|
920
|
+
return pluginExecutor.createContext({
|
|
921
|
+
operationType,
|
|
922
|
+
path,
|
|
923
|
+
method,
|
|
924
|
+
queryKey,
|
|
925
|
+
tags: resolvedTags,
|
|
926
|
+
requestTimestamp,
|
|
927
|
+
hookId,
|
|
928
|
+
requestOptions: { ...initialRequestOptions, ...requestOptions },
|
|
929
|
+
state,
|
|
930
|
+
metadata,
|
|
931
|
+
pluginOptions,
|
|
932
|
+
abort: () => abortController?.abort(),
|
|
933
|
+
stateManager,
|
|
934
|
+
eventEmitter
|
|
935
|
+
});
|
|
936
|
+
};
|
|
937
|
+
const updateState = (updater) => {
|
|
938
|
+
const cached = stateManager.getCache(queryKey);
|
|
939
|
+
if (cached) {
|
|
940
|
+
stateManager.setCache(queryKey, {
|
|
941
|
+
state: { ...cached.state, ...updater }
|
|
942
|
+
});
|
|
943
|
+
} else {
|
|
944
|
+
stateManager.setCache(queryKey, {
|
|
945
|
+
state: { ...createInitialState(), ...updater },
|
|
946
|
+
tags
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
const controller = {
|
|
951
|
+
async execute(opts, executeOptions) {
|
|
952
|
+
const { force = false } = executeOptions ?? {};
|
|
953
|
+
if (!isFirstExecute) {
|
|
954
|
+
currentRequestTimestamp = Date.now();
|
|
955
|
+
}
|
|
956
|
+
isFirstExecute = false;
|
|
957
|
+
const context = createContext(opts, currentRequestTimestamp);
|
|
958
|
+
if (force) {
|
|
959
|
+
context.forceRefetch = true;
|
|
960
|
+
}
|
|
961
|
+
context.headers = await resolveHeadersToRecord(
|
|
962
|
+
context.requestOptions.headers
|
|
963
|
+
);
|
|
964
|
+
context.requestOptions.headers = context.headers;
|
|
965
|
+
const coreFetch = async () => {
|
|
966
|
+
abortController = new AbortController();
|
|
967
|
+
context.requestOptions.signal = abortController.signal;
|
|
968
|
+
const fetchPromise = (async () => {
|
|
969
|
+
try {
|
|
970
|
+
const response = await fetchFn(context.requestOptions);
|
|
971
|
+
context.response = response;
|
|
972
|
+
if (response.data !== void 0 && !response.error) {
|
|
973
|
+
updateState({
|
|
974
|
+
data: response.data,
|
|
975
|
+
error: void 0,
|
|
976
|
+
timestamp: Date.now()
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
return response;
|
|
980
|
+
} catch (err) {
|
|
981
|
+
const errorResponse = {
|
|
982
|
+
status: 0,
|
|
983
|
+
error: err,
|
|
984
|
+
data: void 0
|
|
985
|
+
};
|
|
986
|
+
context.response = errorResponse;
|
|
987
|
+
return errorResponse;
|
|
988
|
+
}
|
|
989
|
+
})();
|
|
990
|
+
stateManager.setPendingPromise(queryKey, fetchPromise);
|
|
991
|
+
fetchPromise.finally(() => {
|
|
992
|
+
stateManager.setPendingPromise(queryKey, void 0);
|
|
993
|
+
});
|
|
994
|
+
return fetchPromise;
|
|
995
|
+
};
|
|
996
|
+
return pluginExecutor.executeMiddleware(
|
|
997
|
+
operationType,
|
|
998
|
+
context,
|
|
999
|
+
coreFetch
|
|
1000
|
+
);
|
|
1001
|
+
},
|
|
1002
|
+
getState() {
|
|
1003
|
+
const cached = stateManager.getCache(queryKey);
|
|
1004
|
+
if (cached) {
|
|
1005
|
+
cachedState = cached.state;
|
|
1006
|
+
} else {
|
|
1007
|
+
cachedState = initialState;
|
|
1008
|
+
}
|
|
1009
|
+
return cachedState;
|
|
1010
|
+
},
|
|
1011
|
+
subscribe(callback) {
|
|
1012
|
+
return stateManager.subscribeCache(queryKey, callback);
|
|
1013
|
+
},
|
|
1014
|
+
abort() {
|
|
1015
|
+
abortController?.abort();
|
|
1016
|
+
abortController = null;
|
|
1017
|
+
},
|
|
1018
|
+
async refetch() {
|
|
1019
|
+
return this.execute();
|
|
1020
|
+
},
|
|
1021
|
+
mount() {
|
|
1022
|
+
currentRequestTimestamp = Date.now();
|
|
1023
|
+
isFirstExecute = true;
|
|
1024
|
+
const context = createContext({}, currentRequestTimestamp);
|
|
1025
|
+
pluginExecutor.executeLifecycle("onMount", operationType, context);
|
|
1026
|
+
},
|
|
1027
|
+
unmount() {
|
|
1028
|
+
const context = createContext({}, currentRequestTimestamp);
|
|
1029
|
+
pluginExecutor.executeLifecycle("onUnmount", operationType, context);
|
|
1030
|
+
},
|
|
1031
|
+
update(previousContext) {
|
|
1032
|
+
const context = createContext({}, currentRequestTimestamp);
|
|
1033
|
+
pluginExecutor.executeUpdateLifecycle(
|
|
1034
|
+
operationType,
|
|
1035
|
+
context,
|
|
1036
|
+
previousContext
|
|
1037
|
+
);
|
|
1038
|
+
},
|
|
1039
|
+
getContext() {
|
|
1040
|
+
return createContext({}, currentRequestTimestamp);
|
|
1041
|
+
},
|
|
1042
|
+
setPluginOptions(options2) {
|
|
1043
|
+
pluginOptions = options2;
|
|
1044
|
+
},
|
|
1045
|
+
setMetadata(key, value) {
|
|
1046
|
+
metadata.set(key, value);
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1049
|
+
return controller;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// src/operations/infinite-controller.ts
|
|
1053
|
+
function createTrackerKey(path, method, baseOptions) {
|
|
1054
|
+
return JSON.stringify({
|
|
1055
|
+
path,
|
|
1056
|
+
method,
|
|
1057
|
+
baseOptions,
|
|
1058
|
+
type: "infinite-tracker"
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
function createPageKey(path, method, baseOptions, pageRequest) {
|
|
1062
|
+
return JSON.stringify({
|
|
1063
|
+
path,
|
|
1064
|
+
method,
|
|
1065
|
+
baseOptions,
|
|
1066
|
+
pageRequest
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
function shallowMergeRequest(initial, override) {
|
|
1070
|
+
return {
|
|
1071
|
+
query: override.query ? { ...initial.query, ...override.query } : initial.query,
|
|
1072
|
+
params: override.params ? { ...initial.params, ...override.params } : initial.params,
|
|
1073
|
+
body: override.body !== void 0 ? override.body : initial.body
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
function collectPageData(pageKeys, stateManager, pageRequests, initialRequest) {
|
|
1077
|
+
const allResponses = [];
|
|
1078
|
+
const allRequests = [];
|
|
1079
|
+
for (const key of pageKeys) {
|
|
1080
|
+
const cached = stateManager.getCache(key);
|
|
1081
|
+
if (cached?.state?.data !== void 0) {
|
|
1082
|
+
allResponses.push(cached.state.data);
|
|
1083
|
+
allRequests.push(pageRequests.get(key) ?? initialRequest);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
return { allResponses, allRequests };
|
|
1087
|
+
}
|
|
1088
|
+
function createInitialInfiniteState() {
|
|
1089
|
+
return {
|
|
1090
|
+
data: void 0,
|
|
1091
|
+
allResponses: void 0,
|
|
1092
|
+
allRequests: void 0,
|
|
1093
|
+
canFetchNext: false,
|
|
1094
|
+
canFetchPrev: false,
|
|
1095
|
+
error: void 0
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
function createInfiniteReadController(options) {
|
|
1099
|
+
const {
|
|
1100
|
+
path,
|
|
1101
|
+
method,
|
|
1102
|
+
tags,
|
|
1103
|
+
initialRequest,
|
|
1104
|
+
baseOptionsForKey,
|
|
1105
|
+
canFetchNext,
|
|
1106
|
+
canFetchPrev,
|
|
1107
|
+
nextPageRequest,
|
|
1108
|
+
prevPageRequest,
|
|
1109
|
+
merger,
|
|
1110
|
+
stateManager,
|
|
1111
|
+
eventEmitter,
|
|
1112
|
+
pluginExecutor,
|
|
1113
|
+
fetchFn,
|
|
1114
|
+
hookId
|
|
1115
|
+
} = options;
|
|
1116
|
+
let pageKeys = [];
|
|
1117
|
+
let pageRequests = /* @__PURE__ */ new Map();
|
|
1118
|
+
const subscribers = /* @__PURE__ */ new Set();
|
|
1119
|
+
let abortController = null;
|
|
1120
|
+
const pendingFetches = /* @__PURE__ */ new Set();
|
|
1121
|
+
let pluginOptions = void 0;
|
|
1122
|
+
let fetchingDirection = null;
|
|
1123
|
+
let latestError = void 0;
|
|
1124
|
+
let cachedState = createInitialInfiniteState();
|
|
1125
|
+
const trackerKey = createTrackerKey(path, method, baseOptionsForKey);
|
|
1126
|
+
let pageSubscriptions = [];
|
|
1127
|
+
let refetchUnsubscribe = null;
|
|
1128
|
+
const loadFromTracker = () => {
|
|
1129
|
+
const cached = stateManager.getCache(trackerKey);
|
|
1130
|
+
const trackerData = cached?.state?.data;
|
|
1131
|
+
if (trackerData) {
|
|
1132
|
+
pageKeys = trackerData.pageKeys;
|
|
1133
|
+
pageRequests = new Map(Object.entries(trackerData.pageRequests));
|
|
1134
|
+
}
|
|
1135
|
+
};
|
|
1136
|
+
const saveToTracker = () => {
|
|
1137
|
+
stateManager.setCache(trackerKey, {
|
|
1138
|
+
state: {
|
|
1139
|
+
data: {
|
|
1140
|
+
pageKeys,
|
|
1141
|
+
pageRequests: Object.fromEntries(pageRequests)
|
|
1142
|
+
},
|
|
1143
|
+
error: void 0,
|
|
1144
|
+
timestamp: Date.now()
|
|
1145
|
+
},
|
|
1146
|
+
tags
|
|
1147
|
+
});
|
|
1148
|
+
};
|
|
1149
|
+
const computeState = () => {
|
|
1150
|
+
if (pageKeys.length === 0) {
|
|
1151
|
+
return {
|
|
1152
|
+
...createInitialInfiniteState(),
|
|
1153
|
+
error: latestError
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
const { allResponses, allRequests } = collectPageData(
|
|
1157
|
+
pageKeys,
|
|
1158
|
+
stateManager,
|
|
1159
|
+
pageRequests,
|
|
1160
|
+
initialRequest
|
|
1161
|
+
);
|
|
1162
|
+
if (allResponses.length === 0) {
|
|
1163
|
+
return {
|
|
1164
|
+
data: void 0,
|
|
1165
|
+
allResponses: void 0,
|
|
1166
|
+
allRequests: void 0,
|
|
1167
|
+
canFetchNext: false,
|
|
1168
|
+
canFetchPrev: false,
|
|
1169
|
+
error: latestError
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
const lastResponse = allResponses.at(-1);
|
|
1173
|
+
const firstResponse = allResponses.at(0);
|
|
1174
|
+
const lastRequest = allRequests.at(-1) ?? initialRequest;
|
|
1175
|
+
const firstRequest = allRequests.at(0) ?? initialRequest;
|
|
1176
|
+
const canNext = canFetchNext({
|
|
1177
|
+
response: lastResponse,
|
|
1178
|
+
allResponses,
|
|
1179
|
+
request: lastRequest
|
|
1180
|
+
});
|
|
1181
|
+
const canPrev = canFetchPrev ? canFetchPrev({
|
|
1182
|
+
response: firstResponse,
|
|
1183
|
+
allResponses,
|
|
1184
|
+
request: firstRequest
|
|
1185
|
+
}) : false;
|
|
1186
|
+
const mergedData = merger(allResponses);
|
|
1187
|
+
return {
|
|
1188
|
+
data: mergedData,
|
|
1189
|
+
allResponses,
|
|
1190
|
+
allRequests,
|
|
1191
|
+
canFetchNext: canNext,
|
|
1192
|
+
canFetchPrev: canPrev,
|
|
1193
|
+
error: latestError
|
|
1194
|
+
};
|
|
1195
|
+
};
|
|
1196
|
+
const notify = () => {
|
|
1197
|
+
cachedState = computeState();
|
|
1198
|
+
subscribers.forEach((cb) => cb());
|
|
1199
|
+
};
|
|
1200
|
+
const subscribeToPages = () => {
|
|
1201
|
+
pageSubscriptions.forEach((unsub) => unsub());
|
|
1202
|
+
pageSubscriptions = pageKeys.map(
|
|
1203
|
+
(key) => stateManager.subscribeCache(key, notify)
|
|
1204
|
+
);
|
|
1205
|
+
};
|
|
1206
|
+
const createContext = (pageKey) => {
|
|
1207
|
+
const initialState = {
|
|
1208
|
+
data: void 0,
|
|
1209
|
+
error: void 0,
|
|
1210
|
+
timestamp: 0
|
|
1211
|
+
};
|
|
1212
|
+
return pluginExecutor.createContext({
|
|
1213
|
+
operationType: "infiniteRead",
|
|
1214
|
+
path,
|
|
1215
|
+
method,
|
|
1216
|
+
queryKey: pageKey,
|
|
1217
|
+
tags,
|
|
1218
|
+
requestTimestamp: Date.now(),
|
|
1219
|
+
hookId,
|
|
1220
|
+
requestOptions: {},
|
|
1221
|
+
state: initialState,
|
|
1222
|
+
metadata: /* @__PURE__ */ new Map(),
|
|
1223
|
+
pluginOptions,
|
|
1224
|
+
abort: () => abortController?.abort(),
|
|
1225
|
+
stateManager,
|
|
1226
|
+
eventEmitter
|
|
1227
|
+
});
|
|
1228
|
+
};
|
|
1229
|
+
const doFetch = async (direction, requestOverride) => {
|
|
1230
|
+
const mergedRequest = shallowMergeRequest(initialRequest, requestOverride);
|
|
1231
|
+
const pageKey = createPageKey(
|
|
1232
|
+
path,
|
|
1233
|
+
method,
|
|
1234
|
+
baseOptionsForKey,
|
|
1235
|
+
mergedRequest
|
|
1236
|
+
);
|
|
1237
|
+
const pendingPromise = stateManager.getPendingPromise(pageKey);
|
|
1238
|
+
if (pendingPromise || pendingFetches.has(pageKey)) {
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
pendingFetches.add(pageKey);
|
|
1242
|
+
fetchingDirection = direction;
|
|
1243
|
+
notify();
|
|
1244
|
+
abortController = new AbortController();
|
|
1245
|
+
const signal = abortController.signal;
|
|
1246
|
+
const context = createContext(pageKey);
|
|
1247
|
+
const coreFetch = async () => {
|
|
1248
|
+
const fetchPromise = (async () => {
|
|
1249
|
+
try {
|
|
1250
|
+
const response = await fetchFn(mergedRequest, signal);
|
|
1251
|
+
context.response = response;
|
|
1252
|
+
if (signal.aborted) {
|
|
1253
|
+
return {
|
|
1254
|
+
status: 0,
|
|
1255
|
+
data: void 0,
|
|
1256
|
+
aborted: true
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
if (response.data !== void 0 && !response.error) {
|
|
1260
|
+
pageRequests.set(pageKey, mergedRequest);
|
|
1261
|
+
if (direction === "next") {
|
|
1262
|
+
if (!pageKeys.includes(pageKey)) {
|
|
1263
|
+
pageKeys = [...pageKeys, pageKey];
|
|
1264
|
+
}
|
|
1265
|
+
} else {
|
|
1266
|
+
if (!pageKeys.includes(pageKey)) {
|
|
1267
|
+
pageKeys = [pageKey, ...pageKeys];
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
saveToTracker();
|
|
1271
|
+
subscribeToPages();
|
|
1272
|
+
stateManager.setCache(pageKey, {
|
|
1273
|
+
state: {
|
|
1274
|
+
data: response.data,
|
|
1275
|
+
error: void 0,
|
|
1276
|
+
timestamp: Date.now()
|
|
1277
|
+
},
|
|
1278
|
+
tags,
|
|
1279
|
+
stale: false
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
if (response.data !== void 0 && !response.error) {
|
|
1283
|
+
latestError = void 0;
|
|
1284
|
+
} else if (response.error) {
|
|
1285
|
+
latestError = response.error;
|
|
1286
|
+
}
|
|
1287
|
+
return response;
|
|
1288
|
+
} catch (err) {
|
|
1289
|
+
if (signal.aborted) {
|
|
1290
|
+
return {
|
|
1291
|
+
status: 0,
|
|
1292
|
+
data: void 0,
|
|
1293
|
+
aborted: true
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
const errorResponse = {
|
|
1297
|
+
status: 0,
|
|
1298
|
+
error: err,
|
|
1299
|
+
data: void 0
|
|
1300
|
+
};
|
|
1301
|
+
context.response = errorResponse;
|
|
1302
|
+
latestError = err;
|
|
1303
|
+
return errorResponse;
|
|
1304
|
+
} finally {
|
|
1305
|
+
pendingFetches.delete(pageKey);
|
|
1306
|
+
fetchingDirection = null;
|
|
1307
|
+
stateManager.setPendingPromise(pageKey, void 0);
|
|
1308
|
+
notify();
|
|
1309
|
+
}
|
|
1310
|
+
})();
|
|
1311
|
+
stateManager.setPendingPromise(pageKey, fetchPromise);
|
|
1312
|
+
return fetchPromise;
|
|
1313
|
+
};
|
|
1314
|
+
await pluginExecutor.executeMiddleware("infiniteRead", context, coreFetch);
|
|
1315
|
+
};
|
|
1316
|
+
const controller = {
|
|
1317
|
+
getState() {
|
|
1318
|
+
return cachedState;
|
|
1319
|
+
},
|
|
1320
|
+
getFetchingDirection() {
|
|
1321
|
+
return fetchingDirection;
|
|
1322
|
+
},
|
|
1323
|
+
subscribe(callback) {
|
|
1324
|
+
subscribers.add(callback);
|
|
1325
|
+
return () => subscribers.delete(callback);
|
|
1326
|
+
},
|
|
1327
|
+
async fetchNext() {
|
|
1328
|
+
if (pageKeys.length === 0) {
|
|
1329
|
+
await doFetch("next", {});
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
const { allResponses, allRequests } = collectPageData(
|
|
1333
|
+
pageKeys,
|
|
1334
|
+
stateManager,
|
|
1335
|
+
pageRequests,
|
|
1336
|
+
initialRequest
|
|
1337
|
+
);
|
|
1338
|
+
if (allResponses.length === 0) return;
|
|
1339
|
+
const lastResponse = allResponses.at(-1);
|
|
1340
|
+
const lastRequest = allRequests.at(-1) ?? initialRequest;
|
|
1341
|
+
const canNext = canFetchNext({
|
|
1342
|
+
response: lastResponse,
|
|
1343
|
+
allResponses,
|
|
1344
|
+
request: lastRequest
|
|
1345
|
+
});
|
|
1346
|
+
if (!canNext) return;
|
|
1347
|
+
const nextRequest = nextPageRequest({
|
|
1348
|
+
response: lastResponse,
|
|
1349
|
+
allResponses,
|
|
1350
|
+
request: lastRequest
|
|
1351
|
+
});
|
|
1352
|
+
await doFetch("next", nextRequest);
|
|
1353
|
+
},
|
|
1354
|
+
async fetchPrev() {
|
|
1355
|
+
if (!canFetchPrev || !prevPageRequest) return;
|
|
1356
|
+
if (pageKeys.length === 0) return;
|
|
1357
|
+
const { allResponses, allRequests } = collectPageData(
|
|
1358
|
+
pageKeys,
|
|
1359
|
+
stateManager,
|
|
1360
|
+
pageRequests,
|
|
1361
|
+
initialRequest
|
|
1362
|
+
);
|
|
1363
|
+
if (allResponses.length === 0) return;
|
|
1364
|
+
const firstResponse = allResponses.at(0);
|
|
1365
|
+
const firstRequest = allRequests.at(0) ?? initialRequest;
|
|
1366
|
+
const canPrev = canFetchPrev({
|
|
1367
|
+
response: firstResponse,
|
|
1368
|
+
allResponses,
|
|
1369
|
+
request: firstRequest
|
|
1370
|
+
});
|
|
1371
|
+
if (!canPrev) return;
|
|
1372
|
+
const prevRequest = prevPageRequest({
|
|
1373
|
+
response: firstResponse,
|
|
1374
|
+
allResponses,
|
|
1375
|
+
request: firstRequest
|
|
1376
|
+
});
|
|
1377
|
+
await doFetch("prev", prevRequest);
|
|
1378
|
+
},
|
|
1379
|
+
async refetch() {
|
|
1380
|
+
for (const key of pageKeys) {
|
|
1381
|
+
stateManager.deleteCache(key);
|
|
1382
|
+
}
|
|
1383
|
+
pageKeys = [];
|
|
1384
|
+
pageRequests.clear();
|
|
1385
|
+
pageSubscriptions.forEach((unsub) => unsub());
|
|
1386
|
+
pageSubscriptions = [];
|
|
1387
|
+
latestError = void 0;
|
|
1388
|
+
saveToTracker();
|
|
1389
|
+
fetchingDirection = "next";
|
|
1390
|
+
notify();
|
|
1391
|
+
await doFetch("next", {});
|
|
1392
|
+
},
|
|
1393
|
+
abort() {
|
|
1394
|
+
abortController?.abort();
|
|
1395
|
+
abortController = null;
|
|
1396
|
+
},
|
|
1397
|
+
mount() {
|
|
1398
|
+
loadFromTracker();
|
|
1399
|
+
cachedState = computeState();
|
|
1400
|
+
subscribeToPages();
|
|
1401
|
+
const context = createContext(trackerKey);
|
|
1402
|
+
pluginExecutor.executeLifecycle("onMount", "infiniteRead", context);
|
|
1403
|
+
refetchUnsubscribe = eventEmitter.on("refetch", (event) => {
|
|
1404
|
+
const isRelevant = event.queryKey === trackerKey || pageKeys.includes(event.queryKey);
|
|
1405
|
+
if (isRelevant) {
|
|
1406
|
+
controller.refetch();
|
|
1407
|
+
}
|
|
1408
|
+
});
|
|
1409
|
+
const isStale = pageKeys.some((key) => {
|
|
1410
|
+
const cached = stateManager.getCache(key);
|
|
1411
|
+
return cached?.stale === true;
|
|
1412
|
+
});
|
|
1413
|
+
if (isStale) {
|
|
1414
|
+
controller.refetch();
|
|
1415
|
+
}
|
|
1416
|
+
},
|
|
1417
|
+
unmount() {
|
|
1418
|
+
const context = createContext(trackerKey);
|
|
1419
|
+
pluginExecutor.executeLifecycle("onUnmount", "infiniteRead", context);
|
|
1420
|
+
pageSubscriptions.forEach((unsub) => unsub());
|
|
1421
|
+
pageSubscriptions = [];
|
|
1422
|
+
refetchUnsubscribe?.();
|
|
1423
|
+
refetchUnsubscribe = null;
|
|
1424
|
+
},
|
|
1425
|
+
update(previousContext) {
|
|
1426
|
+
const context = createContext(trackerKey);
|
|
1427
|
+
pluginExecutor.executeUpdateLifecycle(
|
|
1428
|
+
"infiniteRead",
|
|
1429
|
+
context,
|
|
1430
|
+
previousContext
|
|
1431
|
+
);
|
|
1432
|
+
},
|
|
1433
|
+
getContext() {
|
|
1434
|
+
return createContext(trackerKey);
|
|
1435
|
+
},
|
|
1436
|
+
setPluginOptions(opts) {
|
|
1437
|
+
pluginOptions = opts;
|
|
1438
|
+
}
|
|
1439
|
+
};
|
|
1440
|
+
return controller;
|
|
1441
|
+
}
|