@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/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
+ }