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