@spoosh/core 0.13.2 → 0.14.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.d.mts +156 -2
- package/dist/index.d.ts +156 -2
- package/dist/index.js +378 -4
- package/dist/index.mjs +378 -4
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -62,6 +62,7 @@ declare function urlencoded<T>(value: T): SpooshBody<T>;
|
|
|
62
62
|
declare function resolveRequestBody(rawBody: unknown): {
|
|
63
63
|
body: BodyInit;
|
|
64
64
|
headers?: Record<string, string>;
|
|
65
|
+
removeHeaders?: string[];
|
|
65
66
|
} | undefined;
|
|
66
67
|
|
|
67
68
|
interface TransportResponse {
|
|
@@ -428,7 +429,7 @@ interface EventTracer {
|
|
|
428
429
|
emit(msg: string, options?: EventOptions): void;
|
|
429
430
|
}
|
|
430
431
|
|
|
431
|
-
type OperationType = "read" | "write" | "infiniteRead";
|
|
432
|
+
type OperationType = "read" | "write" | "infiniteRead" | "queue";
|
|
432
433
|
type LifecyclePhase = "onMount" | "onUnmount" | "onUpdate";
|
|
433
434
|
type OperationState<TData = unknown, TError = unknown> = {
|
|
434
435
|
data: TData | undefined;
|
|
@@ -593,8 +594,11 @@ type PluginTypeConfig = {
|
|
|
593
594
|
writeOptions?: object;
|
|
594
595
|
infiniteReadOptions?: object;
|
|
595
596
|
writeTriggerOptions?: object;
|
|
597
|
+
queueOptions?: object;
|
|
598
|
+
queueTriggerOptions?: object;
|
|
596
599
|
readResult?: object;
|
|
597
600
|
writeResult?: object;
|
|
601
|
+
queueResult?: object;
|
|
598
602
|
instanceApi?: object;
|
|
599
603
|
};
|
|
600
604
|
/**
|
|
@@ -1023,6 +1027,15 @@ type ExtractInfiniteReadOptions<T> = T extends SpooshPlugin<infer Types> ? Types
|
|
|
1023
1027
|
type ExtractWriteTriggerOptions<T> = T extends SpooshPlugin<infer Types> ? Types extends {
|
|
1024
1028
|
writeTriggerOptions: infer W;
|
|
1025
1029
|
} ? W : object : object;
|
|
1030
|
+
type ExtractQueueOptions<T> = T extends SpooshPlugin<infer Types> ? Types extends {
|
|
1031
|
+
queueOptions: infer Q;
|
|
1032
|
+
} ? Q : object : object;
|
|
1033
|
+
type ExtractQueueTriggerOptions<T> = T extends SpooshPlugin<infer Types> ? Types extends {
|
|
1034
|
+
queueTriggerOptions: infer Q;
|
|
1035
|
+
} ? Q : object : object;
|
|
1036
|
+
type ExtractQueueResult<T> = T extends SpooshPlugin<infer Types> ? Types extends {
|
|
1037
|
+
queueResult: infer Q;
|
|
1038
|
+
} ? Q : object : object;
|
|
1026
1039
|
type ExtractReadResult<T> = T extends SpooshPlugin<infer Types> ? Types extends {
|
|
1027
1040
|
readResult: infer R;
|
|
1028
1041
|
} ? R : object : object;
|
|
@@ -1038,10 +1051,13 @@ type MergePluginOptions<TPlugins extends readonly SpooshPlugin<PluginTypeConfig>
|
|
|
1038
1051
|
write: UnionToIntersection<ExtractWriteOptions<TPlugins[number]>>;
|
|
1039
1052
|
infiniteRead: UnionToIntersection<ExtractInfiniteReadOptions<TPlugins[number]>>;
|
|
1040
1053
|
writeTrigger: UnionToIntersection<ExtractWriteTriggerOptions<TPlugins[number]>>;
|
|
1054
|
+
queue: UnionToIntersection<ExtractQueueOptions<TPlugins[number]>>;
|
|
1055
|
+
queueTrigger: UnionToIntersection<ExtractQueueTriggerOptions<TPlugins[number]>>;
|
|
1041
1056
|
};
|
|
1042
1057
|
type MergePluginResults<TPlugins extends readonly SpooshPlugin<PluginTypeConfig>[]> = {
|
|
1043
1058
|
read: UnionToIntersection<ExtractReadResult<TPlugins[number]>>;
|
|
1044
1059
|
write: UnionToIntersection<ExtractWriteResult<TPlugins[number]>>;
|
|
1060
|
+
queue: UnionToIntersection<ExtractQueueResult<TPlugins[number]>>;
|
|
1045
1061
|
};
|
|
1046
1062
|
type MergePluginInstanceApi<TPlugins extends readonly SpooshPlugin<PluginTypeConfig>[], TSchema = unknown> = ResolveInstanceApi<UnionToIntersection<ExtractInstanceApi<TPlugins[number]>>, TSchema, MergePluginOptions<TPlugins>["read"]>;
|
|
1047
1063
|
type PluginRegistry<TPlugins extends SpooshPlugin<PluginTypeConfig>[]> = {
|
|
@@ -1429,6 +1445,22 @@ type WriteSelectorPathMethods<TSchema, TPath extends string, TDefaultError> = Fi
|
|
|
1429
1445
|
* Used by useWrite for selecting endpoints. All input goes to trigger().
|
|
1430
1446
|
*/
|
|
1431
1447
|
type WriteSelectorClient<TSchema, TDefaultError = unknown> = <TPath extends WritePaths<TSchema> | (string & {})>(path: TPath) => HasWriteMethod<TSchema, TPath> extends true ? WriteSelectorPathMethods<TSchema, TPath, TDefaultError> : never;
|
|
1448
|
+
/**
|
|
1449
|
+
* Method function type for queue selectors - accepts no arguments.
|
|
1450
|
+
* All input (body, query, params) is passed to trigger() instead.
|
|
1451
|
+
*/
|
|
1452
|
+
type QueueSelectorMethodFn<TMethodConfig, TDefaultError, TUserPath extends string> = () => Promise<MethodResponse<TMethodConfig, TDefaultError, TUserPath>>;
|
|
1453
|
+
/**
|
|
1454
|
+
* Queue selector path methods - all HTTP methods, accepting no arguments.
|
|
1455
|
+
*/
|
|
1456
|
+
type QueueSelectorPathMethods<TSchema, TPath extends string, TDefaultError> = FindMatchingKey<TSchema, TPath> extends infer TKey ? TKey extends keyof TSchema ? Simplify<{
|
|
1457
|
+
[M in HttpMethod as M extends keyof TSchema[TKey] ? M : never]: M extends keyof TSchema[TKey] ? QueueSelectorMethodFn<TSchema[TKey][M], TDefaultError, TPath> : never;
|
|
1458
|
+
}> : never : never;
|
|
1459
|
+
/**
|
|
1460
|
+
* Queue selector client - all HTTP methods, accepting no arguments.
|
|
1461
|
+
* Used by useQueue for selecting endpoints. All input goes to trigger().
|
|
1462
|
+
*/
|
|
1463
|
+
type QueueSelectorClient<TSchema, TDefaultError = unknown> = <TPath extends SchemaPaths<TSchema> | (string & {})>(path: TPath) => QueueSelectorPathMethods<TSchema, TPath, TDefaultError>;
|
|
1432
1464
|
|
|
1433
1465
|
type PluginArray = readonly SpooshPlugin<PluginTypeConfig>[];
|
|
1434
1466
|
interface SpooshConfig<TPlugins extends PluginArray = PluginArray> {
|
|
@@ -1723,6 +1755,7 @@ declare function setHeaders(requestOptions: {
|
|
|
1723
1755
|
* Returns undefined if no Content-Type is set.
|
|
1724
1756
|
*/
|
|
1725
1757
|
declare function getContentType(headers?: HeadersInit): string | undefined;
|
|
1758
|
+
declare function removeHeaderKeys(headers: HeadersInit | undefined, keysToRemove: string[]): HeadersInit | undefined;
|
|
1726
1759
|
|
|
1727
1760
|
declare function objectToFormData(obj: Record<string, unknown>): FormData;
|
|
1728
1761
|
|
|
@@ -2017,4 +2050,125 @@ type CreateInfiniteReadOptions<TData, TItem, TError, TRequest> = {
|
|
|
2017
2050
|
};
|
|
2018
2051
|
declare function createInfiniteReadController<TData, TItem, TError, TRequest extends InfiniteRequestOptions = InfiniteRequestOptions>(options: CreateInfiniteReadOptions<TData, TItem, TError, TRequest>): InfiniteReadController<TData, TItem, TError>;
|
|
2019
2052
|
|
|
2020
|
-
|
|
2053
|
+
/**
|
|
2054
|
+
* Status of an item in the queue.
|
|
2055
|
+
*/
|
|
2056
|
+
type QueueItemStatus = "pending" | "running" | "success" | "error" | "aborted";
|
|
2057
|
+
/**
|
|
2058
|
+
* Represents a single item in the queue.
|
|
2059
|
+
*/
|
|
2060
|
+
interface QueueItem<TData = unknown, TError = unknown, TMeta = Record<string, unknown>> {
|
|
2061
|
+
/** Unique identifier for this queue item */
|
|
2062
|
+
id: string;
|
|
2063
|
+
/** Current status of the item */
|
|
2064
|
+
status: QueueItemStatus;
|
|
2065
|
+
/** Response data on success */
|
|
2066
|
+
data?: TData;
|
|
2067
|
+
/** Error on failure */
|
|
2068
|
+
error?: TError;
|
|
2069
|
+
/** Original trigger input */
|
|
2070
|
+
input?: {
|
|
2071
|
+
body?: unknown;
|
|
2072
|
+
query?: unknown;
|
|
2073
|
+
params?: Record<string, string | number>;
|
|
2074
|
+
};
|
|
2075
|
+
/** Plugin-contributed metadata (e.g., progress, transformedData) */
|
|
2076
|
+
meta?: TMeta;
|
|
2077
|
+
}
|
|
2078
|
+
/**
|
|
2079
|
+
* Statistics information for the queue.
|
|
2080
|
+
*/
|
|
2081
|
+
interface QueueStats {
|
|
2082
|
+
/** Number of pending items waiting to run */
|
|
2083
|
+
pending: number;
|
|
2084
|
+
/** Number of currently running items */
|
|
2085
|
+
running: number;
|
|
2086
|
+
/** Number of settled items (success, error, or aborted) */
|
|
2087
|
+
settled: number;
|
|
2088
|
+
/** Number of successful items */
|
|
2089
|
+
success: number;
|
|
2090
|
+
/** Number of failed items (error or aborted) */
|
|
2091
|
+
failed: number;
|
|
2092
|
+
/** Total number of items in queue */
|
|
2093
|
+
total: number;
|
|
2094
|
+
/** Completion percentage (0-100) */
|
|
2095
|
+
percentage: number;
|
|
2096
|
+
}
|
|
2097
|
+
/**
|
|
2098
|
+
* Input type for queue trigger.
|
|
2099
|
+
*/
|
|
2100
|
+
interface QueueTriggerInput {
|
|
2101
|
+
/** Custom ID for this queue item. If not provided, one will be auto-generated. */
|
|
2102
|
+
id?: string;
|
|
2103
|
+
body?: unknown;
|
|
2104
|
+
query?: unknown;
|
|
2105
|
+
params?: Record<string, string | number>;
|
|
2106
|
+
}
|
|
2107
|
+
/**
|
|
2108
|
+
* Queue controller instance.
|
|
2109
|
+
* Framework-agnostic - can be used directly in Angular, Vue, etc.
|
|
2110
|
+
*/
|
|
2111
|
+
interface QueueController<TData = unknown, TError = unknown, TMeta = Record<string, unknown>> {
|
|
2112
|
+
/** Add item to queue and execute. Returns promise that resolves when item completes. */
|
|
2113
|
+
trigger: (input: QueueTriggerInput) => Promise<SpooshResponse<TData, TError>>;
|
|
2114
|
+
/** Get current queue state */
|
|
2115
|
+
getQueue: () => QueueItem<TData, TError, TMeta>[];
|
|
2116
|
+
/** Get queue statistics */
|
|
2117
|
+
getStats: () => QueueStats;
|
|
2118
|
+
/** Subscribe to queue state changes */
|
|
2119
|
+
subscribe: (callback: () => void) => () => void;
|
|
2120
|
+
/** Abort item by ID, or all items if no ID provided */
|
|
2121
|
+
abort: (id?: string) => void;
|
|
2122
|
+
/** Retry failed/aborted item by ID, or all failed items if no ID provided */
|
|
2123
|
+
retry: (id?: string) => Promise<void>;
|
|
2124
|
+
/** Remove specific item by ID (aborts if active) */
|
|
2125
|
+
remove: (id: string) => void;
|
|
2126
|
+
/** Remove all settled items (success, error, aborted). Keeps pending/running. */
|
|
2127
|
+
removeSettled: () => void;
|
|
2128
|
+
/** Abort all and clear entire queue */
|
|
2129
|
+
clear: () => void;
|
|
2130
|
+
/** Update the concurrency limit */
|
|
2131
|
+
setConcurrency: (concurrency: number) => void;
|
|
2132
|
+
}
|
|
2133
|
+
/**
|
|
2134
|
+
* Configuration for creating a queue controller.
|
|
2135
|
+
*/
|
|
2136
|
+
interface QueueControllerConfig {
|
|
2137
|
+
/** API path */
|
|
2138
|
+
path: string;
|
|
2139
|
+
/** HTTP method */
|
|
2140
|
+
method: string;
|
|
2141
|
+
/** Maximum concurrent operations. Defaults to 3. */
|
|
2142
|
+
concurrency?: number;
|
|
2143
|
+
/** Operation type for plugin middleware */
|
|
2144
|
+
operationType: "read" | "write" | "queue";
|
|
2145
|
+
/** Hook-level plugin options (e.g., progress, retries) */
|
|
2146
|
+
hookOptions?: Record<string, unknown>;
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
interface QueueControllerContext {
|
|
2150
|
+
api: unknown;
|
|
2151
|
+
stateManager: StateManager;
|
|
2152
|
+
eventEmitter: EventEmitter;
|
|
2153
|
+
pluginExecutor: InstancePluginExecutor;
|
|
2154
|
+
}
|
|
2155
|
+
declare function createQueueController<TData, TError, TMeta = Record<string, unknown>>(config: QueueControllerConfig, context: QueueControllerContext): QueueController<TData, TError, TMeta>;
|
|
2156
|
+
|
|
2157
|
+
/**
|
|
2158
|
+
* Semaphore for controlling concurrent access.
|
|
2159
|
+
* Used to limit the number of concurrent operations in the queue.
|
|
2160
|
+
*/
|
|
2161
|
+
declare class Semaphore {
|
|
2162
|
+
private max;
|
|
2163
|
+
private current;
|
|
2164
|
+
private waiting;
|
|
2165
|
+
constructor(max: number);
|
|
2166
|
+
acquire(): Promise<boolean>;
|
|
2167
|
+
release(): void;
|
|
2168
|
+
setConcurrency(max: number): void;
|
|
2169
|
+
reset(): void;
|
|
2170
|
+
getCurrent(): number;
|
|
2171
|
+
getWaitingCount(): number;
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
export { type AnyRequestOptions, type ApiSchema, type BuiltInEvents, type CacheEntry, type CacheEntryWithKey, type CapturedCall, type ComputeRequestOptions, type CoreRequestOptionsBase, type CreateInfiniteReadOptions, type CreateOperationOptions, type DataAwareCallback, type DataAwareTransform, type DevtoolEvents, type EventEmitter, type EventListener, type EventOptions, type EventTracer, type ExtractBody$1 as ExtractBody, type ExtractData, type ExtractError, type ExtractMethodOptions, type ExtractParamNames, type ExtractQuery$1 as ExtractQuery, type FetchDirection, type FetchExecutor, type FindMatchingKey, HTTP_METHODS, type HasParams, type HasReadMethod, type HasWriteMethod, type HeadersInitOrGetter, type HttpMethod, type HttpMethodKey, type InfiniteReadController, type InfiniteReadState, type InfiniteRequestOptions, type InstanceApiContext, type InstanceApiResolvers, type InstancePluginExecutor, type LifecyclePhase, type MergePluginInstanceApi, type MergePluginOptions, type MergePluginResults, type MethodOptionsMap, type OperationController, type OperationState, type OperationType, type PageContext, type PluginAccessor, type PluginArray, type PluginContext, type PluginContextBase, type PluginContextExtensions, type PluginContextInput, type PluginExecutor, type PluginExportsRegistry, type PluginFactory, type PluginHandler, type PluginLifecycle, type PluginMiddleware, type PluginRegistry, type PluginRequestOptions, type PluginResolvers, type PluginResponseHandler, type PluginResultResolvers, type PluginTypeConfig, type PluginUpdateHandler, type QueueController, type QueueControllerConfig, type QueueControllerContext, type QueueItem, type QueueItemStatus, type QueueSelectorClient, type QueueStats, type QueueTriggerInput, type ReadClient, type ReadPaths, type ReadSchemaHelper, type RefetchEvent, type RequestCompleteEvent, type RequestOptions$1 as RequestOptions, type RequestTracer, type ResolveInstanceApi, type ResolveResultTypes, type ResolveSchemaTypes, type ResolveTypes, type ResolverContext, type SchemaPaths, type SelectedEndpoint, type SelectorFunction, type SelectorResult, Semaphore, type SetupContext, type Simplify, Spoosh, type SpooshBody, type SpooshClient, type SpooshConfig, type SpooshInstance, type SpooshOptions, type SpooshOptionsInput, type SpooshPlugin, type SpooshResponse, type SpooshSchema, type StandaloneEvent, type StateManager, type StripPrefix, type TagMode, type TagOptions, type Trace, type TraceColor, type TraceEvent, type TraceInfo, type TraceListener, type TraceOptions, type TraceStage, type Transport, type TransportOption, type TransportOptionsMap, type TransportResponse, type WriteClient, type WriteMethod, type WritePaths, type WriteSchemaHelper, type WriteSelectorClient, __DEV__, buildUrl, clone, containsFile, createClient, createEventEmitter, createInfiniteReadController, createInitialState, createOperationController, createPluginExecutor, createPluginRegistry, createProxyHandler, createQueueController, createSelectorProxy, createStateManager, createTracer, executeFetch, extractMethodFromSelector, extractPathFromSelector, fetchTransport, form, generateTags, getContentType, isAbortError, isJsonBody, isNetworkError, isSpooshBody, json, mergeHeaders, objectToFormData, objectToUrlEncoded, removeHeaderKeys, resolveHeadersToRecord, resolvePath, resolvePathString, resolveRequestBody, resolveTags, setHeaders, sortObjectKeys, urlencoded, xhrTransport };
|
package/dist/index.d.ts
CHANGED
|
@@ -62,6 +62,7 @@ declare function urlencoded<T>(value: T): SpooshBody<T>;
|
|
|
62
62
|
declare function resolveRequestBody(rawBody: unknown): {
|
|
63
63
|
body: BodyInit;
|
|
64
64
|
headers?: Record<string, string>;
|
|
65
|
+
removeHeaders?: string[];
|
|
65
66
|
} | undefined;
|
|
66
67
|
|
|
67
68
|
interface TransportResponse {
|
|
@@ -428,7 +429,7 @@ interface EventTracer {
|
|
|
428
429
|
emit(msg: string, options?: EventOptions): void;
|
|
429
430
|
}
|
|
430
431
|
|
|
431
|
-
type OperationType = "read" | "write" | "infiniteRead";
|
|
432
|
+
type OperationType = "read" | "write" | "infiniteRead" | "queue";
|
|
432
433
|
type LifecyclePhase = "onMount" | "onUnmount" | "onUpdate";
|
|
433
434
|
type OperationState<TData = unknown, TError = unknown> = {
|
|
434
435
|
data: TData | undefined;
|
|
@@ -593,8 +594,11 @@ type PluginTypeConfig = {
|
|
|
593
594
|
writeOptions?: object;
|
|
594
595
|
infiniteReadOptions?: object;
|
|
595
596
|
writeTriggerOptions?: object;
|
|
597
|
+
queueOptions?: object;
|
|
598
|
+
queueTriggerOptions?: object;
|
|
596
599
|
readResult?: object;
|
|
597
600
|
writeResult?: object;
|
|
601
|
+
queueResult?: object;
|
|
598
602
|
instanceApi?: object;
|
|
599
603
|
};
|
|
600
604
|
/**
|
|
@@ -1023,6 +1027,15 @@ type ExtractInfiniteReadOptions<T> = T extends SpooshPlugin<infer Types> ? Types
|
|
|
1023
1027
|
type ExtractWriteTriggerOptions<T> = T extends SpooshPlugin<infer Types> ? Types extends {
|
|
1024
1028
|
writeTriggerOptions: infer W;
|
|
1025
1029
|
} ? W : object : object;
|
|
1030
|
+
type ExtractQueueOptions<T> = T extends SpooshPlugin<infer Types> ? Types extends {
|
|
1031
|
+
queueOptions: infer Q;
|
|
1032
|
+
} ? Q : object : object;
|
|
1033
|
+
type ExtractQueueTriggerOptions<T> = T extends SpooshPlugin<infer Types> ? Types extends {
|
|
1034
|
+
queueTriggerOptions: infer Q;
|
|
1035
|
+
} ? Q : object : object;
|
|
1036
|
+
type ExtractQueueResult<T> = T extends SpooshPlugin<infer Types> ? Types extends {
|
|
1037
|
+
queueResult: infer Q;
|
|
1038
|
+
} ? Q : object : object;
|
|
1026
1039
|
type ExtractReadResult<T> = T extends SpooshPlugin<infer Types> ? Types extends {
|
|
1027
1040
|
readResult: infer R;
|
|
1028
1041
|
} ? R : object : object;
|
|
@@ -1038,10 +1051,13 @@ type MergePluginOptions<TPlugins extends readonly SpooshPlugin<PluginTypeConfig>
|
|
|
1038
1051
|
write: UnionToIntersection<ExtractWriteOptions<TPlugins[number]>>;
|
|
1039
1052
|
infiniteRead: UnionToIntersection<ExtractInfiniteReadOptions<TPlugins[number]>>;
|
|
1040
1053
|
writeTrigger: UnionToIntersection<ExtractWriteTriggerOptions<TPlugins[number]>>;
|
|
1054
|
+
queue: UnionToIntersection<ExtractQueueOptions<TPlugins[number]>>;
|
|
1055
|
+
queueTrigger: UnionToIntersection<ExtractQueueTriggerOptions<TPlugins[number]>>;
|
|
1041
1056
|
};
|
|
1042
1057
|
type MergePluginResults<TPlugins extends readonly SpooshPlugin<PluginTypeConfig>[]> = {
|
|
1043
1058
|
read: UnionToIntersection<ExtractReadResult<TPlugins[number]>>;
|
|
1044
1059
|
write: UnionToIntersection<ExtractWriteResult<TPlugins[number]>>;
|
|
1060
|
+
queue: UnionToIntersection<ExtractQueueResult<TPlugins[number]>>;
|
|
1045
1061
|
};
|
|
1046
1062
|
type MergePluginInstanceApi<TPlugins extends readonly SpooshPlugin<PluginTypeConfig>[], TSchema = unknown> = ResolveInstanceApi<UnionToIntersection<ExtractInstanceApi<TPlugins[number]>>, TSchema, MergePluginOptions<TPlugins>["read"]>;
|
|
1047
1063
|
type PluginRegistry<TPlugins extends SpooshPlugin<PluginTypeConfig>[]> = {
|
|
@@ -1429,6 +1445,22 @@ type WriteSelectorPathMethods<TSchema, TPath extends string, TDefaultError> = Fi
|
|
|
1429
1445
|
* Used by useWrite for selecting endpoints. All input goes to trigger().
|
|
1430
1446
|
*/
|
|
1431
1447
|
type WriteSelectorClient<TSchema, TDefaultError = unknown> = <TPath extends WritePaths<TSchema> | (string & {})>(path: TPath) => HasWriteMethod<TSchema, TPath> extends true ? WriteSelectorPathMethods<TSchema, TPath, TDefaultError> : never;
|
|
1448
|
+
/**
|
|
1449
|
+
* Method function type for queue selectors - accepts no arguments.
|
|
1450
|
+
* All input (body, query, params) is passed to trigger() instead.
|
|
1451
|
+
*/
|
|
1452
|
+
type QueueSelectorMethodFn<TMethodConfig, TDefaultError, TUserPath extends string> = () => Promise<MethodResponse<TMethodConfig, TDefaultError, TUserPath>>;
|
|
1453
|
+
/**
|
|
1454
|
+
* Queue selector path methods - all HTTP methods, accepting no arguments.
|
|
1455
|
+
*/
|
|
1456
|
+
type QueueSelectorPathMethods<TSchema, TPath extends string, TDefaultError> = FindMatchingKey<TSchema, TPath> extends infer TKey ? TKey extends keyof TSchema ? Simplify<{
|
|
1457
|
+
[M in HttpMethod as M extends keyof TSchema[TKey] ? M : never]: M extends keyof TSchema[TKey] ? QueueSelectorMethodFn<TSchema[TKey][M], TDefaultError, TPath> : never;
|
|
1458
|
+
}> : never : never;
|
|
1459
|
+
/**
|
|
1460
|
+
* Queue selector client - all HTTP methods, accepting no arguments.
|
|
1461
|
+
* Used by useQueue for selecting endpoints. All input goes to trigger().
|
|
1462
|
+
*/
|
|
1463
|
+
type QueueSelectorClient<TSchema, TDefaultError = unknown> = <TPath extends SchemaPaths<TSchema> | (string & {})>(path: TPath) => QueueSelectorPathMethods<TSchema, TPath, TDefaultError>;
|
|
1432
1464
|
|
|
1433
1465
|
type PluginArray = readonly SpooshPlugin<PluginTypeConfig>[];
|
|
1434
1466
|
interface SpooshConfig<TPlugins extends PluginArray = PluginArray> {
|
|
@@ -1723,6 +1755,7 @@ declare function setHeaders(requestOptions: {
|
|
|
1723
1755
|
* Returns undefined if no Content-Type is set.
|
|
1724
1756
|
*/
|
|
1725
1757
|
declare function getContentType(headers?: HeadersInit): string | undefined;
|
|
1758
|
+
declare function removeHeaderKeys(headers: HeadersInit | undefined, keysToRemove: string[]): HeadersInit | undefined;
|
|
1726
1759
|
|
|
1727
1760
|
declare function objectToFormData(obj: Record<string, unknown>): FormData;
|
|
1728
1761
|
|
|
@@ -2017,4 +2050,125 @@ type CreateInfiniteReadOptions<TData, TItem, TError, TRequest> = {
|
|
|
2017
2050
|
};
|
|
2018
2051
|
declare function createInfiniteReadController<TData, TItem, TError, TRequest extends InfiniteRequestOptions = InfiniteRequestOptions>(options: CreateInfiniteReadOptions<TData, TItem, TError, TRequest>): InfiniteReadController<TData, TItem, TError>;
|
|
2019
2052
|
|
|
2020
|
-
|
|
2053
|
+
/**
|
|
2054
|
+
* Status of an item in the queue.
|
|
2055
|
+
*/
|
|
2056
|
+
type QueueItemStatus = "pending" | "running" | "success" | "error" | "aborted";
|
|
2057
|
+
/**
|
|
2058
|
+
* Represents a single item in the queue.
|
|
2059
|
+
*/
|
|
2060
|
+
interface QueueItem<TData = unknown, TError = unknown, TMeta = Record<string, unknown>> {
|
|
2061
|
+
/** Unique identifier for this queue item */
|
|
2062
|
+
id: string;
|
|
2063
|
+
/** Current status of the item */
|
|
2064
|
+
status: QueueItemStatus;
|
|
2065
|
+
/** Response data on success */
|
|
2066
|
+
data?: TData;
|
|
2067
|
+
/** Error on failure */
|
|
2068
|
+
error?: TError;
|
|
2069
|
+
/** Original trigger input */
|
|
2070
|
+
input?: {
|
|
2071
|
+
body?: unknown;
|
|
2072
|
+
query?: unknown;
|
|
2073
|
+
params?: Record<string, string | number>;
|
|
2074
|
+
};
|
|
2075
|
+
/** Plugin-contributed metadata (e.g., progress, transformedData) */
|
|
2076
|
+
meta?: TMeta;
|
|
2077
|
+
}
|
|
2078
|
+
/**
|
|
2079
|
+
* Statistics information for the queue.
|
|
2080
|
+
*/
|
|
2081
|
+
interface QueueStats {
|
|
2082
|
+
/** Number of pending items waiting to run */
|
|
2083
|
+
pending: number;
|
|
2084
|
+
/** Number of currently running items */
|
|
2085
|
+
running: number;
|
|
2086
|
+
/** Number of settled items (success, error, or aborted) */
|
|
2087
|
+
settled: number;
|
|
2088
|
+
/** Number of successful items */
|
|
2089
|
+
success: number;
|
|
2090
|
+
/** Number of failed items (error or aborted) */
|
|
2091
|
+
failed: number;
|
|
2092
|
+
/** Total number of items in queue */
|
|
2093
|
+
total: number;
|
|
2094
|
+
/** Completion percentage (0-100) */
|
|
2095
|
+
percentage: number;
|
|
2096
|
+
}
|
|
2097
|
+
/**
|
|
2098
|
+
* Input type for queue trigger.
|
|
2099
|
+
*/
|
|
2100
|
+
interface QueueTriggerInput {
|
|
2101
|
+
/** Custom ID for this queue item. If not provided, one will be auto-generated. */
|
|
2102
|
+
id?: string;
|
|
2103
|
+
body?: unknown;
|
|
2104
|
+
query?: unknown;
|
|
2105
|
+
params?: Record<string, string | number>;
|
|
2106
|
+
}
|
|
2107
|
+
/**
|
|
2108
|
+
* Queue controller instance.
|
|
2109
|
+
* Framework-agnostic - can be used directly in Angular, Vue, etc.
|
|
2110
|
+
*/
|
|
2111
|
+
interface QueueController<TData = unknown, TError = unknown, TMeta = Record<string, unknown>> {
|
|
2112
|
+
/** Add item to queue and execute. Returns promise that resolves when item completes. */
|
|
2113
|
+
trigger: (input: QueueTriggerInput) => Promise<SpooshResponse<TData, TError>>;
|
|
2114
|
+
/** Get current queue state */
|
|
2115
|
+
getQueue: () => QueueItem<TData, TError, TMeta>[];
|
|
2116
|
+
/** Get queue statistics */
|
|
2117
|
+
getStats: () => QueueStats;
|
|
2118
|
+
/** Subscribe to queue state changes */
|
|
2119
|
+
subscribe: (callback: () => void) => () => void;
|
|
2120
|
+
/** Abort item by ID, or all items if no ID provided */
|
|
2121
|
+
abort: (id?: string) => void;
|
|
2122
|
+
/** Retry failed/aborted item by ID, or all failed items if no ID provided */
|
|
2123
|
+
retry: (id?: string) => Promise<void>;
|
|
2124
|
+
/** Remove specific item by ID (aborts if active) */
|
|
2125
|
+
remove: (id: string) => void;
|
|
2126
|
+
/** Remove all settled items (success, error, aborted). Keeps pending/running. */
|
|
2127
|
+
removeSettled: () => void;
|
|
2128
|
+
/** Abort all and clear entire queue */
|
|
2129
|
+
clear: () => void;
|
|
2130
|
+
/** Update the concurrency limit */
|
|
2131
|
+
setConcurrency: (concurrency: number) => void;
|
|
2132
|
+
}
|
|
2133
|
+
/**
|
|
2134
|
+
* Configuration for creating a queue controller.
|
|
2135
|
+
*/
|
|
2136
|
+
interface QueueControllerConfig {
|
|
2137
|
+
/** API path */
|
|
2138
|
+
path: string;
|
|
2139
|
+
/** HTTP method */
|
|
2140
|
+
method: string;
|
|
2141
|
+
/** Maximum concurrent operations. Defaults to 3. */
|
|
2142
|
+
concurrency?: number;
|
|
2143
|
+
/** Operation type for plugin middleware */
|
|
2144
|
+
operationType: "read" | "write" | "queue";
|
|
2145
|
+
/** Hook-level plugin options (e.g., progress, retries) */
|
|
2146
|
+
hookOptions?: Record<string, unknown>;
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
interface QueueControllerContext {
|
|
2150
|
+
api: unknown;
|
|
2151
|
+
stateManager: StateManager;
|
|
2152
|
+
eventEmitter: EventEmitter;
|
|
2153
|
+
pluginExecutor: InstancePluginExecutor;
|
|
2154
|
+
}
|
|
2155
|
+
declare function createQueueController<TData, TError, TMeta = Record<string, unknown>>(config: QueueControllerConfig, context: QueueControllerContext): QueueController<TData, TError, TMeta>;
|
|
2156
|
+
|
|
2157
|
+
/**
|
|
2158
|
+
* Semaphore for controlling concurrent access.
|
|
2159
|
+
* Used to limit the number of concurrent operations in the queue.
|
|
2160
|
+
*/
|
|
2161
|
+
declare class Semaphore {
|
|
2162
|
+
private max;
|
|
2163
|
+
private current;
|
|
2164
|
+
private waiting;
|
|
2165
|
+
constructor(max: number);
|
|
2166
|
+
acquire(): Promise<boolean>;
|
|
2167
|
+
release(): void;
|
|
2168
|
+
setConcurrency(max: number): void;
|
|
2169
|
+
reset(): void;
|
|
2170
|
+
getCurrent(): number;
|
|
2171
|
+
getWaitingCount(): number;
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
export { type AnyRequestOptions, type ApiSchema, type BuiltInEvents, type CacheEntry, type CacheEntryWithKey, type CapturedCall, type ComputeRequestOptions, type CoreRequestOptionsBase, type CreateInfiniteReadOptions, type CreateOperationOptions, type DataAwareCallback, type DataAwareTransform, type DevtoolEvents, type EventEmitter, type EventListener, type EventOptions, type EventTracer, type ExtractBody$1 as ExtractBody, type ExtractData, type ExtractError, type ExtractMethodOptions, type ExtractParamNames, type ExtractQuery$1 as ExtractQuery, type FetchDirection, type FetchExecutor, type FindMatchingKey, HTTP_METHODS, type HasParams, type HasReadMethod, type HasWriteMethod, type HeadersInitOrGetter, type HttpMethod, type HttpMethodKey, type InfiniteReadController, type InfiniteReadState, type InfiniteRequestOptions, type InstanceApiContext, type InstanceApiResolvers, type InstancePluginExecutor, type LifecyclePhase, type MergePluginInstanceApi, type MergePluginOptions, type MergePluginResults, type MethodOptionsMap, type OperationController, type OperationState, type OperationType, type PageContext, type PluginAccessor, type PluginArray, type PluginContext, type PluginContextBase, type PluginContextExtensions, type PluginContextInput, type PluginExecutor, type PluginExportsRegistry, type PluginFactory, type PluginHandler, type PluginLifecycle, type PluginMiddleware, type PluginRegistry, type PluginRequestOptions, type PluginResolvers, type PluginResponseHandler, type PluginResultResolvers, type PluginTypeConfig, type PluginUpdateHandler, type QueueController, type QueueControllerConfig, type QueueControllerContext, type QueueItem, type QueueItemStatus, type QueueSelectorClient, type QueueStats, type QueueTriggerInput, type ReadClient, type ReadPaths, type ReadSchemaHelper, type RefetchEvent, type RequestCompleteEvent, type RequestOptions$1 as RequestOptions, type RequestTracer, type ResolveInstanceApi, type ResolveResultTypes, type ResolveSchemaTypes, type ResolveTypes, type ResolverContext, type SchemaPaths, type SelectedEndpoint, type SelectorFunction, type SelectorResult, Semaphore, type SetupContext, type Simplify, Spoosh, type SpooshBody, type SpooshClient, type SpooshConfig, type SpooshInstance, type SpooshOptions, type SpooshOptionsInput, type SpooshPlugin, type SpooshResponse, type SpooshSchema, type StandaloneEvent, type StateManager, type StripPrefix, type TagMode, type TagOptions, type Trace, type TraceColor, type TraceEvent, type TraceInfo, type TraceListener, type TraceOptions, type TraceStage, type Transport, type TransportOption, type TransportOptionsMap, type TransportResponse, type WriteClient, type WriteMethod, type WritePaths, type WriteSchemaHelper, type WriteSelectorClient, __DEV__, buildUrl, clone, containsFile, createClient, createEventEmitter, createInfiniteReadController, createInitialState, createOperationController, createPluginExecutor, createPluginRegistry, createProxyHandler, createQueueController, createSelectorProxy, createStateManager, createTracer, executeFetch, extractMethodFromSelector, extractPathFromSelector, fetchTransport, form, generateTags, getContentType, isAbortError, isJsonBody, isNetworkError, isSpooshBody, json, mergeHeaders, objectToFormData, objectToUrlEncoded, removeHeaderKeys, resolveHeadersToRecord, resolvePath, resolvePathString, resolveRequestBody, resolveTags, setHeaders, sortObjectKeys, urlencoded, xhrTransport };
|
package/dist/index.js
CHANGED
|
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var src_exports = {};
|
|
22
22
|
__export(src_exports, {
|
|
23
23
|
HTTP_METHODS: () => HTTP_METHODS,
|
|
24
|
+
Semaphore: () => Semaphore,
|
|
24
25
|
Spoosh: () => Spoosh,
|
|
25
26
|
__DEV__: () => __DEV__,
|
|
26
27
|
buildUrl: () => buildUrl,
|
|
@@ -34,6 +35,7 @@ __export(src_exports, {
|
|
|
34
35
|
createPluginExecutor: () => createPluginExecutor,
|
|
35
36
|
createPluginRegistry: () => createPluginRegistry,
|
|
36
37
|
createProxyHandler: () => createProxyHandler,
|
|
38
|
+
createQueueController: () => createQueueController,
|
|
37
39
|
createSelectorProxy: () => createSelectorProxy,
|
|
38
40
|
createStateManager: () => createStateManager,
|
|
39
41
|
createTracer: () => createTracer,
|
|
@@ -52,6 +54,7 @@ __export(src_exports, {
|
|
|
52
54
|
mergeHeaders: () => mergeHeaders,
|
|
53
55
|
objectToFormData: () => objectToFormData,
|
|
54
56
|
objectToUrlEncoded: () => objectToUrlEncoded,
|
|
57
|
+
removeHeaderKeys: () => removeHeaderKeys,
|
|
55
58
|
resolveHeadersToRecord: () => resolveHeadersToRecord,
|
|
56
59
|
resolvePath: () => resolvePath,
|
|
57
60
|
resolvePathString: () => resolvePathString,
|
|
@@ -171,6 +174,15 @@ function getContentType(headers) {
|
|
|
171
174
|
const headersObj = new Headers(headers);
|
|
172
175
|
return headersObj.get("content-type") ?? void 0;
|
|
173
176
|
}
|
|
177
|
+
function removeHeaderKeys(headers, keysToRemove) {
|
|
178
|
+
if (!headers) return void 0;
|
|
179
|
+
const headersObj = new Headers(headers);
|
|
180
|
+
for (const key of keysToRemove) {
|
|
181
|
+
headersObj.delete(key);
|
|
182
|
+
}
|
|
183
|
+
const entries = [...headersObj.entries()];
|
|
184
|
+
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
|
|
185
|
+
}
|
|
174
186
|
|
|
175
187
|
// src/utils/objectToFormData.ts
|
|
176
188
|
function objectToFormData(obj) {
|
|
@@ -278,7 +290,8 @@ function resolveRequestBody(rawBody) {
|
|
|
278
290
|
switch (body.kind) {
|
|
279
291
|
case "form":
|
|
280
292
|
return {
|
|
281
|
-
body: objectToFormData(body.value)
|
|
293
|
+
body: objectToFormData(body.value),
|
|
294
|
+
removeHeaders: ["Content-Type"]
|
|
282
295
|
};
|
|
283
296
|
case "json":
|
|
284
297
|
return {
|
|
@@ -303,6 +316,9 @@ function resolveRequestBody(rawBody) {
|
|
|
303
316
|
headers: { "Content-Type": "application/json" }
|
|
304
317
|
};
|
|
305
318
|
}
|
|
319
|
+
if (rawBody instanceof FormData) {
|
|
320
|
+
return { body: rawBody, removeHeaders: ["Content-Type"] };
|
|
321
|
+
}
|
|
306
322
|
return { body: rawBody };
|
|
307
323
|
}
|
|
308
324
|
|
|
@@ -590,12 +606,13 @@ async function executeCoreFetch(config) {
|
|
|
590
606
|
const resolved = resolveRequestBody(requestOptions.body);
|
|
591
607
|
if (resolved) {
|
|
592
608
|
fetchInit.body = resolved.body;
|
|
609
|
+
if (resolved.removeHeaders) {
|
|
610
|
+
headers = removeHeaderKeys(headers, resolved.removeHeaders);
|
|
611
|
+
}
|
|
593
612
|
if (resolved.headers) {
|
|
594
613
|
headers = await mergeHeaders(headers, resolved.headers);
|
|
595
|
-
if (headers) {
|
|
596
|
-
fetchInit.headers = headers;
|
|
597
|
-
}
|
|
598
614
|
}
|
|
615
|
+
fetchInit.headers = headers;
|
|
599
616
|
}
|
|
600
617
|
}
|
|
601
618
|
const resolvedTransport = resolveTransport(
|
|
@@ -1814,3 +1831,360 @@ function createInfiniteReadController(options) {
|
|
|
1814
1831
|
};
|
|
1815
1832
|
return controller;
|
|
1816
1833
|
}
|
|
1834
|
+
|
|
1835
|
+
// src/queue/semaphore.ts
|
|
1836
|
+
var Semaphore = class {
|
|
1837
|
+
constructor(max) {
|
|
1838
|
+
this.max = max;
|
|
1839
|
+
}
|
|
1840
|
+
current = 0;
|
|
1841
|
+
waiting = [];
|
|
1842
|
+
async acquire() {
|
|
1843
|
+
if (this.current < this.max) {
|
|
1844
|
+
this.current++;
|
|
1845
|
+
return true;
|
|
1846
|
+
}
|
|
1847
|
+
return new Promise((resolve) => this.waiting.push(resolve));
|
|
1848
|
+
}
|
|
1849
|
+
release() {
|
|
1850
|
+
if (this.current > 0) {
|
|
1851
|
+
this.current--;
|
|
1852
|
+
}
|
|
1853
|
+
if (this.waiting.length > 0) {
|
|
1854
|
+
this.current++;
|
|
1855
|
+
this.waiting.shift()(true);
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
setConcurrency(max) {
|
|
1859
|
+
const previousMax = this.max;
|
|
1860
|
+
this.max = max;
|
|
1861
|
+
if (max > previousMax) {
|
|
1862
|
+
const slotsToRelease = Math.min(max - previousMax, this.waiting.length);
|
|
1863
|
+
for (let i = 0; i < slotsToRelease; i++) {
|
|
1864
|
+
this.current++;
|
|
1865
|
+
this.waiting.shift()(true);
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
reset() {
|
|
1870
|
+
this.current = 0;
|
|
1871
|
+
while (this.waiting.length > 0) {
|
|
1872
|
+
const resolve = this.waiting.shift();
|
|
1873
|
+
resolve(false);
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
getCurrent() {
|
|
1877
|
+
return this.current;
|
|
1878
|
+
}
|
|
1879
|
+
getWaitingCount() {
|
|
1880
|
+
return this.waiting.length;
|
|
1881
|
+
}
|
|
1882
|
+
};
|
|
1883
|
+
|
|
1884
|
+
// src/queue/controller.ts
|
|
1885
|
+
var DEFAULT_CONCURRENCY = 3;
|
|
1886
|
+
function createQueueController(config, context) {
|
|
1887
|
+
const { path, method, operationType, hookOptions = {} } = config;
|
|
1888
|
+
const concurrency = config.concurrency ?? DEFAULT_CONCURRENCY;
|
|
1889
|
+
const { api, stateManager, eventEmitter, pluginExecutor } = context;
|
|
1890
|
+
const semaphore = new Semaphore(concurrency);
|
|
1891
|
+
const queue = [];
|
|
1892
|
+
const abortControllers = /* @__PURE__ */ new Map();
|
|
1893
|
+
const subscribers = /* @__PURE__ */ new Set();
|
|
1894
|
+
const itemPromises = /* @__PURE__ */ new Map();
|
|
1895
|
+
let cachedQueueSnapshot = [];
|
|
1896
|
+
const notify = () => {
|
|
1897
|
+
cachedQueueSnapshot = [...queue];
|
|
1898
|
+
subscribers.forEach((cb) => cb());
|
|
1899
|
+
};
|
|
1900
|
+
const generateId = () => `q-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1901
|
+
const updateItem = (id, update) => {
|
|
1902
|
+
const item = queue.find((i) => i.id === id);
|
|
1903
|
+
if (item) {
|
|
1904
|
+
Object.assign(item, update);
|
|
1905
|
+
}
|
|
1906
|
+
};
|
|
1907
|
+
const executeItem = async (item) => {
|
|
1908
|
+
const acquired = await semaphore.acquire();
|
|
1909
|
+
if (!acquired || item.status === "aborted") {
|
|
1910
|
+
if (acquired) {
|
|
1911
|
+
semaphore.release();
|
|
1912
|
+
}
|
|
1913
|
+
const response = {
|
|
1914
|
+
error: new Error("Aborted"),
|
|
1915
|
+
aborted: true
|
|
1916
|
+
};
|
|
1917
|
+
itemPromises.get(item.id)?.resolve(response);
|
|
1918
|
+
itemPromises.delete(item.id);
|
|
1919
|
+
return response;
|
|
1920
|
+
}
|
|
1921
|
+
const abortController = new AbortController();
|
|
1922
|
+
abortControllers.set(item.id, abortController);
|
|
1923
|
+
const queryKey = stateManager.createQueryKey({
|
|
1924
|
+
path,
|
|
1925
|
+
method,
|
|
1926
|
+
options: { ...item.input, _queueId: item.id }
|
|
1927
|
+
});
|
|
1928
|
+
const unsubscribeMeta = stateManager.subscribeCache(queryKey, () => {
|
|
1929
|
+
const cacheEntry = stateManager.getCache(queryKey);
|
|
1930
|
+
const meta = cacheEntry?.meta ? Object.fromEntries(cacheEntry.meta) : void 0;
|
|
1931
|
+
if (meta) {
|
|
1932
|
+
updateItem(item.id, { meta });
|
|
1933
|
+
notify();
|
|
1934
|
+
}
|
|
1935
|
+
});
|
|
1936
|
+
try {
|
|
1937
|
+
updateItem(item.id, { status: "running" });
|
|
1938
|
+
notify();
|
|
1939
|
+
const { body, query, params, ...triggerOptions } = item.input ?? {};
|
|
1940
|
+
const pluginContext = pluginExecutor.createContext({
|
|
1941
|
+
operationType,
|
|
1942
|
+
path,
|
|
1943
|
+
method,
|
|
1944
|
+
queryKey,
|
|
1945
|
+
tags: [],
|
|
1946
|
+
requestTimestamp: Date.now(),
|
|
1947
|
+
request: {
|
|
1948
|
+
headers: {},
|
|
1949
|
+
body,
|
|
1950
|
+
query,
|
|
1951
|
+
params,
|
|
1952
|
+
signal: abortController.signal
|
|
1953
|
+
},
|
|
1954
|
+
temp: /* @__PURE__ */ new Map(),
|
|
1955
|
+
pluginOptions: { ...hookOptions, ...triggerOptions },
|
|
1956
|
+
stateManager,
|
|
1957
|
+
eventEmitter
|
|
1958
|
+
});
|
|
1959
|
+
const coreFetch = async () => {
|
|
1960
|
+
const pathMethods = api(
|
|
1961
|
+
path
|
|
1962
|
+
);
|
|
1963
|
+
const methodFn = pathMethods[method];
|
|
1964
|
+
const { transport, transportOptions } = pluginContext.request;
|
|
1965
|
+
return methodFn({
|
|
1966
|
+
body,
|
|
1967
|
+
query,
|
|
1968
|
+
params,
|
|
1969
|
+
signal: abortController.signal,
|
|
1970
|
+
transport,
|
|
1971
|
+
transportOptions
|
|
1972
|
+
});
|
|
1973
|
+
};
|
|
1974
|
+
const response = await pluginExecutor.executeMiddleware(
|
|
1975
|
+
operationType,
|
|
1976
|
+
pluginContext,
|
|
1977
|
+
coreFetch
|
|
1978
|
+
);
|
|
1979
|
+
const cacheEntry = stateManager.getCache(queryKey);
|
|
1980
|
+
const meta = cacheEntry?.meta ? Object.fromEntries(cacheEntry.meta) : void 0;
|
|
1981
|
+
if (response.error) {
|
|
1982
|
+
updateItem(item.id, { status: "error", error: response.error, meta });
|
|
1983
|
+
itemPromises.get(item.id)?.resolve(response);
|
|
1984
|
+
} else {
|
|
1985
|
+
updateItem(item.id, { status: "success", data: response.data, meta });
|
|
1986
|
+
itemPromises.get(item.id)?.resolve(response);
|
|
1987
|
+
}
|
|
1988
|
+
return response;
|
|
1989
|
+
} catch (err) {
|
|
1990
|
+
const isAborted = abortController.signal.aborted;
|
|
1991
|
+
if (isAborted) {
|
|
1992
|
+
updateItem(item.id, { status: "aborted" });
|
|
1993
|
+
} else {
|
|
1994
|
+
updateItem(item.id, { status: "error", error: err });
|
|
1995
|
+
}
|
|
1996
|
+
const errorResponse = {
|
|
1997
|
+
error: err,
|
|
1998
|
+
aborted: isAborted
|
|
1999
|
+
};
|
|
2000
|
+
itemPromises.get(item.id)?.resolve(errorResponse);
|
|
2001
|
+
return errorResponse;
|
|
2002
|
+
} finally {
|
|
2003
|
+
unsubscribeMeta();
|
|
2004
|
+
abortControllers.delete(item.id);
|
|
2005
|
+
itemPromises.delete(item.id);
|
|
2006
|
+
notify();
|
|
2007
|
+
semaphore.release();
|
|
2008
|
+
}
|
|
2009
|
+
};
|
|
2010
|
+
return {
|
|
2011
|
+
trigger(input) {
|
|
2012
|
+
const { id: customId, ...requestInput } = input;
|
|
2013
|
+
const id = customId ?? generateId();
|
|
2014
|
+
const item = {
|
|
2015
|
+
id,
|
|
2016
|
+
status: "pending",
|
|
2017
|
+
input: requestInput
|
|
2018
|
+
};
|
|
2019
|
+
queue.push(item);
|
|
2020
|
+
notify();
|
|
2021
|
+
const promise = new Promise(
|
|
2022
|
+
(resolve, reject) => {
|
|
2023
|
+
itemPromises.set(id, { resolve, reject });
|
|
2024
|
+
}
|
|
2025
|
+
);
|
|
2026
|
+
executeItem(item);
|
|
2027
|
+
return promise;
|
|
2028
|
+
},
|
|
2029
|
+
getQueue: () => cachedQueueSnapshot,
|
|
2030
|
+
getStats: () => {
|
|
2031
|
+
let pending = 0;
|
|
2032
|
+
let running = 0;
|
|
2033
|
+
let success = 0;
|
|
2034
|
+
let failed = 0;
|
|
2035
|
+
for (const item of queue) {
|
|
2036
|
+
if (item.status === "pending") pending++;
|
|
2037
|
+
else if (item.status === "running") running++;
|
|
2038
|
+
else if (item.status === "success") success++;
|
|
2039
|
+
else if (item.status === "error" || item.status === "aborted") failed++;
|
|
2040
|
+
}
|
|
2041
|
+
const settled = success + failed;
|
|
2042
|
+
const total = queue.length;
|
|
2043
|
+
return {
|
|
2044
|
+
pending,
|
|
2045
|
+
running,
|
|
2046
|
+
settled,
|
|
2047
|
+
success,
|
|
2048
|
+
failed,
|
|
2049
|
+
total,
|
|
2050
|
+
percentage: total > 0 ? Math.round(settled / total * 100) : 0
|
|
2051
|
+
};
|
|
2052
|
+
},
|
|
2053
|
+
subscribe: (callback) => {
|
|
2054
|
+
subscribers.add(callback);
|
|
2055
|
+
return () => subscribers.delete(callback);
|
|
2056
|
+
},
|
|
2057
|
+
abort: (id) => {
|
|
2058
|
+
const queryKeysToDiscard = [];
|
|
2059
|
+
const abortedResponse = {
|
|
2060
|
+
error: new Error("Aborted"),
|
|
2061
|
+
aborted: true
|
|
2062
|
+
};
|
|
2063
|
+
if (id) {
|
|
2064
|
+
const item = queue.find((i) => i.id === id);
|
|
2065
|
+
if (item && (item.status === "pending" || item.status === "running")) {
|
|
2066
|
+
const wasPending = item.status === "pending";
|
|
2067
|
+
abortControllers.get(id)?.abort();
|
|
2068
|
+
updateItem(id, { status: "aborted" });
|
|
2069
|
+
if (wasPending) {
|
|
2070
|
+
itemPromises.get(id)?.resolve(abortedResponse);
|
|
2071
|
+
itemPromises.delete(id);
|
|
2072
|
+
}
|
|
2073
|
+
const queryKey = stateManager.createQueryKey({
|
|
2074
|
+
path,
|
|
2075
|
+
method,
|
|
2076
|
+
options: { ...item.input, _queueId: item.id }
|
|
2077
|
+
});
|
|
2078
|
+
queryKeysToDiscard.push(queryKey);
|
|
2079
|
+
notify();
|
|
2080
|
+
}
|
|
2081
|
+
} else {
|
|
2082
|
+
for (const item of queue) {
|
|
2083
|
+
if (item.status === "pending" || item.status === "running") {
|
|
2084
|
+
abortControllers.get(item.id)?.abort();
|
|
2085
|
+
const wasPending = item.status === "pending";
|
|
2086
|
+
updateItem(item.id, { status: "aborted" });
|
|
2087
|
+
if (wasPending) {
|
|
2088
|
+
itemPromises.get(item.id)?.resolve(abortedResponse);
|
|
2089
|
+
itemPromises.delete(item.id);
|
|
2090
|
+
}
|
|
2091
|
+
const queryKey = stateManager.createQueryKey({
|
|
2092
|
+
path,
|
|
2093
|
+
method,
|
|
2094
|
+
options: { ...item.input, _queueId: item.id }
|
|
2095
|
+
});
|
|
2096
|
+
queryKeysToDiscard.push(queryKey);
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
notify();
|
|
2100
|
+
}
|
|
2101
|
+
if (queryKeysToDiscard.length > 0) {
|
|
2102
|
+
eventEmitter.emit("spoosh:queue-abort", {
|
|
2103
|
+
queryKeys: queryKeysToDiscard
|
|
2104
|
+
});
|
|
2105
|
+
}
|
|
2106
|
+
},
|
|
2107
|
+
retry: async (id) => {
|
|
2108
|
+
const items = id ? queue.filter(
|
|
2109
|
+
(i) => i.id === id && (i.status === "error" || i.status === "aborted")
|
|
2110
|
+
) : queue.filter((i) => i.status === "error" || i.status === "aborted");
|
|
2111
|
+
const promises = [];
|
|
2112
|
+
for (const item of items) {
|
|
2113
|
+
updateItem(item.id, { status: "pending", error: void 0 });
|
|
2114
|
+
const promise = new Promise(
|
|
2115
|
+
(resolve) => {
|
|
2116
|
+
itemPromises.set(item.id, { resolve, reject: () => {
|
|
2117
|
+
} });
|
|
2118
|
+
}
|
|
2119
|
+
);
|
|
2120
|
+
promises.push(promise);
|
|
2121
|
+
executeItem(item);
|
|
2122
|
+
}
|
|
2123
|
+
await Promise.all(promises);
|
|
2124
|
+
},
|
|
2125
|
+
remove: (id) => {
|
|
2126
|
+
const abortedResponse = {
|
|
2127
|
+
error: new Error("Removed"),
|
|
2128
|
+
aborted: true
|
|
2129
|
+
};
|
|
2130
|
+
const item = queue.find((i) => i.id === id);
|
|
2131
|
+
if (item) {
|
|
2132
|
+
if (item.status === "pending") {
|
|
2133
|
+
item.status = "aborted";
|
|
2134
|
+
itemPromises.get(id)?.resolve(abortedResponse);
|
|
2135
|
+
itemPromises.delete(id);
|
|
2136
|
+
} else if (item.status === "running") {
|
|
2137
|
+
abortControllers.get(id)?.abort();
|
|
2138
|
+
}
|
|
2139
|
+
const idx = queue.findIndex((i) => i.id === id);
|
|
2140
|
+
if (idx !== -1) {
|
|
2141
|
+
queue.splice(idx, 1);
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
notify();
|
|
2145
|
+
},
|
|
2146
|
+
removeSettled: () => {
|
|
2147
|
+
const active = queue.filter(
|
|
2148
|
+
(i) => i.status === "pending" || i.status === "running"
|
|
2149
|
+
);
|
|
2150
|
+
queue.length = 0;
|
|
2151
|
+
queue.push(...active);
|
|
2152
|
+
notify();
|
|
2153
|
+
},
|
|
2154
|
+
clear: () => {
|
|
2155
|
+
const queryKeysToDiscard = [];
|
|
2156
|
+
for (const item of queue) {
|
|
2157
|
+
if (item.status === "pending" || item.status === "running") {
|
|
2158
|
+
abortControllers.get(item.id)?.abort();
|
|
2159
|
+
item.status = "aborted";
|
|
2160
|
+
const abortedResponse = {
|
|
2161
|
+
error: new Error("Aborted"),
|
|
2162
|
+
aborted: true
|
|
2163
|
+
};
|
|
2164
|
+
itemPromises.get(item.id)?.resolve(abortedResponse);
|
|
2165
|
+
itemPromises.delete(item.id);
|
|
2166
|
+
const queryKey = stateManager.createQueryKey({
|
|
2167
|
+
path,
|
|
2168
|
+
method,
|
|
2169
|
+
options: { ...item.input, _queueId: item.id }
|
|
2170
|
+
});
|
|
2171
|
+
queryKeysToDiscard.push(queryKey);
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
if (queryKeysToDiscard.length > 0) {
|
|
2175
|
+
eventEmitter.emit("spoosh:queue-clear", {
|
|
2176
|
+
queryKeys: queryKeysToDiscard
|
|
2177
|
+
});
|
|
2178
|
+
}
|
|
2179
|
+
queue.length = 0;
|
|
2180
|
+
semaphore.reset();
|
|
2181
|
+
notify();
|
|
2182
|
+
},
|
|
2183
|
+
setConcurrency: (newConcurrency) => {
|
|
2184
|
+
if (newConcurrency < 1 || !Number.isInteger(newConcurrency)) {
|
|
2185
|
+
return;
|
|
2186
|
+
}
|
|
2187
|
+
semaphore.setConcurrency(newConcurrency);
|
|
2188
|
+
}
|
|
2189
|
+
};
|
|
2190
|
+
}
|
package/dist/index.mjs
CHANGED
|
@@ -105,6 +105,15 @@ function getContentType(headers) {
|
|
|
105
105
|
const headersObj = new Headers(headers);
|
|
106
106
|
return headersObj.get("content-type") ?? void 0;
|
|
107
107
|
}
|
|
108
|
+
function removeHeaderKeys(headers, keysToRemove) {
|
|
109
|
+
if (!headers) return void 0;
|
|
110
|
+
const headersObj = new Headers(headers);
|
|
111
|
+
for (const key of keysToRemove) {
|
|
112
|
+
headersObj.delete(key);
|
|
113
|
+
}
|
|
114
|
+
const entries = [...headersObj.entries()];
|
|
115
|
+
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
|
|
116
|
+
}
|
|
108
117
|
|
|
109
118
|
// src/utils/objectToFormData.ts
|
|
110
119
|
function objectToFormData(obj) {
|
|
@@ -212,7 +221,8 @@ function resolveRequestBody(rawBody) {
|
|
|
212
221
|
switch (body.kind) {
|
|
213
222
|
case "form":
|
|
214
223
|
return {
|
|
215
|
-
body: objectToFormData(body.value)
|
|
224
|
+
body: objectToFormData(body.value),
|
|
225
|
+
removeHeaders: ["Content-Type"]
|
|
216
226
|
};
|
|
217
227
|
case "json":
|
|
218
228
|
return {
|
|
@@ -237,6 +247,9 @@ function resolveRequestBody(rawBody) {
|
|
|
237
247
|
headers: { "Content-Type": "application/json" }
|
|
238
248
|
};
|
|
239
249
|
}
|
|
250
|
+
if (rawBody instanceof FormData) {
|
|
251
|
+
return { body: rawBody, removeHeaders: ["Content-Type"] };
|
|
252
|
+
}
|
|
240
253
|
return { body: rawBody };
|
|
241
254
|
}
|
|
242
255
|
|
|
@@ -524,12 +537,13 @@ async function executeCoreFetch(config) {
|
|
|
524
537
|
const resolved = resolveRequestBody(requestOptions.body);
|
|
525
538
|
if (resolved) {
|
|
526
539
|
fetchInit.body = resolved.body;
|
|
540
|
+
if (resolved.removeHeaders) {
|
|
541
|
+
headers = removeHeaderKeys(headers, resolved.removeHeaders);
|
|
542
|
+
}
|
|
527
543
|
if (resolved.headers) {
|
|
528
544
|
headers = await mergeHeaders(headers, resolved.headers);
|
|
529
|
-
if (headers) {
|
|
530
|
-
fetchInit.headers = headers;
|
|
531
|
-
}
|
|
532
545
|
}
|
|
546
|
+
fetchInit.headers = headers;
|
|
533
547
|
}
|
|
534
548
|
}
|
|
535
549
|
const resolvedTransport = resolveTransport(
|
|
@@ -1748,8 +1762,366 @@ function createInfiniteReadController(options) {
|
|
|
1748
1762
|
};
|
|
1749
1763
|
return controller;
|
|
1750
1764
|
}
|
|
1765
|
+
|
|
1766
|
+
// src/queue/semaphore.ts
|
|
1767
|
+
var Semaphore = class {
|
|
1768
|
+
constructor(max) {
|
|
1769
|
+
this.max = max;
|
|
1770
|
+
}
|
|
1771
|
+
current = 0;
|
|
1772
|
+
waiting = [];
|
|
1773
|
+
async acquire() {
|
|
1774
|
+
if (this.current < this.max) {
|
|
1775
|
+
this.current++;
|
|
1776
|
+
return true;
|
|
1777
|
+
}
|
|
1778
|
+
return new Promise((resolve) => this.waiting.push(resolve));
|
|
1779
|
+
}
|
|
1780
|
+
release() {
|
|
1781
|
+
if (this.current > 0) {
|
|
1782
|
+
this.current--;
|
|
1783
|
+
}
|
|
1784
|
+
if (this.waiting.length > 0) {
|
|
1785
|
+
this.current++;
|
|
1786
|
+
this.waiting.shift()(true);
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
setConcurrency(max) {
|
|
1790
|
+
const previousMax = this.max;
|
|
1791
|
+
this.max = max;
|
|
1792
|
+
if (max > previousMax) {
|
|
1793
|
+
const slotsToRelease = Math.min(max - previousMax, this.waiting.length);
|
|
1794
|
+
for (let i = 0; i < slotsToRelease; i++) {
|
|
1795
|
+
this.current++;
|
|
1796
|
+
this.waiting.shift()(true);
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
reset() {
|
|
1801
|
+
this.current = 0;
|
|
1802
|
+
while (this.waiting.length > 0) {
|
|
1803
|
+
const resolve = this.waiting.shift();
|
|
1804
|
+
resolve(false);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
getCurrent() {
|
|
1808
|
+
return this.current;
|
|
1809
|
+
}
|
|
1810
|
+
getWaitingCount() {
|
|
1811
|
+
return this.waiting.length;
|
|
1812
|
+
}
|
|
1813
|
+
};
|
|
1814
|
+
|
|
1815
|
+
// src/queue/controller.ts
|
|
1816
|
+
var DEFAULT_CONCURRENCY = 3;
|
|
1817
|
+
function createQueueController(config, context) {
|
|
1818
|
+
const { path, method, operationType, hookOptions = {} } = config;
|
|
1819
|
+
const concurrency = config.concurrency ?? DEFAULT_CONCURRENCY;
|
|
1820
|
+
const { api, stateManager, eventEmitter, pluginExecutor } = context;
|
|
1821
|
+
const semaphore = new Semaphore(concurrency);
|
|
1822
|
+
const queue = [];
|
|
1823
|
+
const abortControllers = /* @__PURE__ */ new Map();
|
|
1824
|
+
const subscribers = /* @__PURE__ */ new Set();
|
|
1825
|
+
const itemPromises = /* @__PURE__ */ new Map();
|
|
1826
|
+
let cachedQueueSnapshot = [];
|
|
1827
|
+
const notify = () => {
|
|
1828
|
+
cachedQueueSnapshot = [...queue];
|
|
1829
|
+
subscribers.forEach((cb) => cb());
|
|
1830
|
+
};
|
|
1831
|
+
const generateId = () => `q-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1832
|
+
const updateItem = (id, update) => {
|
|
1833
|
+
const item = queue.find((i) => i.id === id);
|
|
1834
|
+
if (item) {
|
|
1835
|
+
Object.assign(item, update);
|
|
1836
|
+
}
|
|
1837
|
+
};
|
|
1838
|
+
const executeItem = async (item) => {
|
|
1839
|
+
const acquired = await semaphore.acquire();
|
|
1840
|
+
if (!acquired || item.status === "aborted") {
|
|
1841
|
+
if (acquired) {
|
|
1842
|
+
semaphore.release();
|
|
1843
|
+
}
|
|
1844
|
+
const response = {
|
|
1845
|
+
error: new Error("Aborted"),
|
|
1846
|
+
aborted: true
|
|
1847
|
+
};
|
|
1848
|
+
itemPromises.get(item.id)?.resolve(response);
|
|
1849
|
+
itemPromises.delete(item.id);
|
|
1850
|
+
return response;
|
|
1851
|
+
}
|
|
1852
|
+
const abortController = new AbortController();
|
|
1853
|
+
abortControllers.set(item.id, abortController);
|
|
1854
|
+
const queryKey = stateManager.createQueryKey({
|
|
1855
|
+
path,
|
|
1856
|
+
method,
|
|
1857
|
+
options: { ...item.input, _queueId: item.id }
|
|
1858
|
+
});
|
|
1859
|
+
const unsubscribeMeta = stateManager.subscribeCache(queryKey, () => {
|
|
1860
|
+
const cacheEntry = stateManager.getCache(queryKey);
|
|
1861
|
+
const meta = cacheEntry?.meta ? Object.fromEntries(cacheEntry.meta) : void 0;
|
|
1862
|
+
if (meta) {
|
|
1863
|
+
updateItem(item.id, { meta });
|
|
1864
|
+
notify();
|
|
1865
|
+
}
|
|
1866
|
+
});
|
|
1867
|
+
try {
|
|
1868
|
+
updateItem(item.id, { status: "running" });
|
|
1869
|
+
notify();
|
|
1870
|
+
const { body, query, params, ...triggerOptions } = item.input ?? {};
|
|
1871
|
+
const pluginContext = pluginExecutor.createContext({
|
|
1872
|
+
operationType,
|
|
1873
|
+
path,
|
|
1874
|
+
method,
|
|
1875
|
+
queryKey,
|
|
1876
|
+
tags: [],
|
|
1877
|
+
requestTimestamp: Date.now(),
|
|
1878
|
+
request: {
|
|
1879
|
+
headers: {},
|
|
1880
|
+
body,
|
|
1881
|
+
query,
|
|
1882
|
+
params,
|
|
1883
|
+
signal: abortController.signal
|
|
1884
|
+
},
|
|
1885
|
+
temp: /* @__PURE__ */ new Map(),
|
|
1886
|
+
pluginOptions: { ...hookOptions, ...triggerOptions },
|
|
1887
|
+
stateManager,
|
|
1888
|
+
eventEmitter
|
|
1889
|
+
});
|
|
1890
|
+
const coreFetch = async () => {
|
|
1891
|
+
const pathMethods = api(
|
|
1892
|
+
path
|
|
1893
|
+
);
|
|
1894
|
+
const methodFn = pathMethods[method];
|
|
1895
|
+
const { transport, transportOptions } = pluginContext.request;
|
|
1896
|
+
return methodFn({
|
|
1897
|
+
body,
|
|
1898
|
+
query,
|
|
1899
|
+
params,
|
|
1900
|
+
signal: abortController.signal,
|
|
1901
|
+
transport,
|
|
1902
|
+
transportOptions
|
|
1903
|
+
});
|
|
1904
|
+
};
|
|
1905
|
+
const response = await pluginExecutor.executeMiddleware(
|
|
1906
|
+
operationType,
|
|
1907
|
+
pluginContext,
|
|
1908
|
+
coreFetch
|
|
1909
|
+
);
|
|
1910
|
+
const cacheEntry = stateManager.getCache(queryKey);
|
|
1911
|
+
const meta = cacheEntry?.meta ? Object.fromEntries(cacheEntry.meta) : void 0;
|
|
1912
|
+
if (response.error) {
|
|
1913
|
+
updateItem(item.id, { status: "error", error: response.error, meta });
|
|
1914
|
+
itemPromises.get(item.id)?.resolve(response);
|
|
1915
|
+
} else {
|
|
1916
|
+
updateItem(item.id, { status: "success", data: response.data, meta });
|
|
1917
|
+
itemPromises.get(item.id)?.resolve(response);
|
|
1918
|
+
}
|
|
1919
|
+
return response;
|
|
1920
|
+
} catch (err) {
|
|
1921
|
+
const isAborted = abortController.signal.aborted;
|
|
1922
|
+
if (isAborted) {
|
|
1923
|
+
updateItem(item.id, { status: "aborted" });
|
|
1924
|
+
} else {
|
|
1925
|
+
updateItem(item.id, { status: "error", error: err });
|
|
1926
|
+
}
|
|
1927
|
+
const errorResponse = {
|
|
1928
|
+
error: err,
|
|
1929
|
+
aborted: isAborted
|
|
1930
|
+
};
|
|
1931
|
+
itemPromises.get(item.id)?.resolve(errorResponse);
|
|
1932
|
+
return errorResponse;
|
|
1933
|
+
} finally {
|
|
1934
|
+
unsubscribeMeta();
|
|
1935
|
+
abortControllers.delete(item.id);
|
|
1936
|
+
itemPromises.delete(item.id);
|
|
1937
|
+
notify();
|
|
1938
|
+
semaphore.release();
|
|
1939
|
+
}
|
|
1940
|
+
};
|
|
1941
|
+
return {
|
|
1942
|
+
trigger(input) {
|
|
1943
|
+
const { id: customId, ...requestInput } = input;
|
|
1944
|
+
const id = customId ?? generateId();
|
|
1945
|
+
const item = {
|
|
1946
|
+
id,
|
|
1947
|
+
status: "pending",
|
|
1948
|
+
input: requestInput
|
|
1949
|
+
};
|
|
1950
|
+
queue.push(item);
|
|
1951
|
+
notify();
|
|
1952
|
+
const promise = new Promise(
|
|
1953
|
+
(resolve, reject) => {
|
|
1954
|
+
itemPromises.set(id, { resolve, reject });
|
|
1955
|
+
}
|
|
1956
|
+
);
|
|
1957
|
+
executeItem(item);
|
|
1958
|
+
return promise;
|
|
1959
|
+
},
|
|
1960
|
+
getQueue: () => cachedQueueSnapshot,
|
|
1961
|
+
getStats: () => {
|
|
1962
|
+
let pending = 0;
|
|
1963
|
+
let running = 0;
|
|
1964
|
+
let success = 0;
|
|
1965
|
+
let failed = 0;
|
|
1966
|
+
for (const item of queue) {
|
|
1967
|
+
if (item.status === "pending") pending++;
|
|
1968
|
+
else if (item.status === "running") running++;
|
|
1969
|
+
else if (item.status === "success") success++;
|
|
1970
|
+
else if (item.status === "error" || item.status === "aborted") failed++;
|
|
1971
|
+
}
|
|
1972
|
+
const settled = success + failed;
|
|
1973
|
+
const total = queue.length;
|
|
1974
|
+
return {
|
|
1975
|
+
pending,
|
|
1976
|
+
running,
|
|
1977
|
+
settled,
|
|
1978
|
+
success,
|
|
1979
|
+
failed,
|
|
1980
|
+
total,
|
|
1981
|
+
percentage: total > 0 ? Math.round(settled / total * 100) : 0
|
|
1982
|
+
};
|
|
1983
|
+
},
|
|
1984
|
+
subscribe: (callback) => {
|
|
1985
|
+
subscribers.add(callback);
|
|
1986
|
+
return () => subscribers.delete(callback);
|
|
1987
|
+
},
|
|
1988
|
+
abort: (id) => {
|
|
1989
|
+
const queryKeysToDiscard = [];
|
|
1990
|
+
const abortedResponse = {
|
|
1991
|
+
error: new Error("Aborted"),
|
|
1992
|
+
aborted: true
|
|
1993
|
+
};
|
|
1994
|
+
if (id) {
|
|
1995
|
+
const item = queue.find((i) => i.id === id);
|
|
1996
|
+
if (item && (item.status === "pending" || item.status === "running")) {
|
|
1997
|
+
const wasPending = item.status === "pending";
|
|
1998
|
+
abortControllers.get(id)?.abort();
|
|
1999
|
+
updateItem(id, { status: "aborted" });
|
|
2000
|
+
if (wasPending) {
|
|
2001
|
+
itemPromises.get(id)?.resolve(abortedResponse);
|
|
2002
|
+
itemPromises.delete(id);
|
|
2003
|
+
}
|
|
2004
|
+
const queryKey = stateManager.createQueryKey({
|
|
2005
|
+
path,
|
|
2006
|
+
method,
|
|
2007
|
+
options: { ...item.input, _queueId: item.id }
|
|
2008
|
+
});
|
|
2009
|
+
queryKeysToDiscard.push(queryKey);
|
|
2010
|
+
notify();
|
|
2011
|
+
}
|
|
2012
|
+
} else {
|
|
2013
|
+
for (const item of queue) {
|
|
2014
|
+
if (item.status === "pending" || item.status === "running") {
|
|
2015
|
+
abortControllers.get(item.id)?.abort();
|
|
2016
|
+
const wasPending = item.status === "pending";
|
|
2017
|
+
updateItem(item.id, { status: "aborted" });
|
|
2018
|
+
if (wasPending) {
|
|
2019
|
+
itemPromises.get(item.id)?.resolve(abortedResponse);
|
|
2020
|
+
itemPromises.delete(item.id);
|
|
2021
|
+
}
|
|
2022
|
+
const queryKey = stateManager.createQueryKey({
|
|
2023
|
+
path,
|
|
2024
|
+
method,
|
|
2025
|
+
options: { ...item.input, _queueId: item.id }
|
|
2026
|
+
});
|
|
2027
|
+
queryKeysToDiscard.push(queryKey);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
notify();
|
|
2031
|
+
}
|
|
2032
|
+
if (queryKeysToDiscard.length > 0) {
|
|
2033
|
+
eventEmitter.emit("spoosh:queue-abort", {
|
|
2034
|
+
queryKeys: queryKeysToDiscard
|
|
2035
|
+
});
|
|
2036
|
+
}
|
|
2037
|
+
},
|
|
2038
|
+
retry: async (id) => {
|
|
2039
|
+
const items = id ? queue.filter(
|
|
2040
|
+
(i) => i.id === id && (i.status === "error" || i.status === "aborted")
|
|
2041
|
+
) : queue.filter((i) => i.status === "error" || i.status === "aborted");
|
|
2042
|
+
const promises = [];
|
|
2043
|
+
for (const item of items) {
|
|
2044
|
+
updateItem(item.id, { status: "pending", error: void 0 });
|
|
2045
|
+
const promise = new Promise(
|
|
2046
|
+
(resolve) => {
|
|
2047
|
+
itemPromises.set(item.id, { resolve, reject: () => {
|
|
2048
|
+
} });
|
|
2049
|
+
}
|
|
2050
|
+
);
|
|
2051
|
+
promises.push(promise);
|
|
2052
|
+
executeItem(item);
|
|
2053
|
+
}
|
|
2054
|
+
await Promise.all(promises);
|
|
2055
|
+
},
|
|
2056
|
+
remove: (id) => {
|
|
2057
|
+
const abortedResponse = {
|
|
2058
|
+
error: new Error("Removed"),
|
|
2059
|
+
aborted: true
|
|
2060
|
+
};
|
|
2061
|
+
const item = queue.find((i) => i.id === id);
|
|
2062
|
+
if (item) {
|
|
2063
|
+
if (item.status === "pending") {
|
|
2064
|
+
item.status = "aborted";
|
|
2065
|
+
itemPromises.get(id)?.resolve(abortedResponse);
|
|
2066
|
+
itemPromises.delete(id);
|
|
2067
|
+
} else if (item.status === "running") {
|
|
2068
|
+
abortControllers.get(id)?.abort();
|
|
2069
|
+
}
|
|
2070
|
+
const idx = queue.findIndex((i) => i.id === id);
|
|
2071
|
+
if (idx !== -1) {
|
|
2072
|
+
queue.splice(idx, 1);
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
notify();
|
|
2076
|
+
},
|
|
2077
|
+
removeSettled: () => {
|
|
2078
|
+
const active = queue.filter(
|
|
2079
|
+
(i) => i.status === "pending" || i.status === "running"
|
|
2080
|
+
);
|
|
2081
|
+
queue.length = 0;
|
|
2082
|
+
queue.push(...active);
|
|
2083
|
+
notify();
|
|
2084
|
+
},
|
|
2085
|
+
clear: () => {
|
|
2086
|
+
const queryKeysToDiscard = [];
|
|
2087
|
+
for (const item of queue) {
|
|
2088
|
+
if (item.status === "pending" || item.status === "running") {
|
|
2089
|
+
abortControllers.get(item.id)?.abort();
|
|
2090
|
+
item.status = "aborted";
|
|
2091
|
+
const abortedResponse = {
|
|
2092
|
+
error: new Error("Aborted"),
|
|
2093
|
+
aborted: true
|
|
2094
|
+
};
|
|
2095
|
+
itemPromises.get(item.id)?.resolve(abortedResponse);
|
|
2096
|
+
itemPromises.delete(item.id);
|
|
2097
|
+
const queryKey = stateManager.createQueryKey({
|
|
2098
|
+
path,
|
|
2099
|
+
method,
|
|
2100
|
+
options: { ...item.input, _queueId: item.id }
|
|
2101
|
+
});
|
|
2102
|
+
queryKeysToDiscard.push(queryKey);
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
if (queryKeysToDiscard.length > 0) {
|
|
2106
|
+
eventEmitter.emit("spoosh:queue-clear", {
|
|
2107
|
+
queryKeys: queryKeysToDiscard
|
|
2108
|
+
});
|
|
2109
|
+
}
|
|
2110
|
+
queue.length = 0;
|
|
2111
|
+
semaphore.reset();
|
|
2112
|
+
notify();
|
|
2113
|
+
},
|
|
2114
|
+
setConcurrency: (newConcurrency) => {
|
|
2115
|
+
if (newConcurrency < 1 || !Number.isInteger(newConcurrency)) {
|
|
2116
|
+
return;
|
|
2117
|
+
}
|
|
2118
|
+
semaphore.setConcurrency(newConcurrency);
|
|
2119
|
+
}
|
|
2120
|
+
};
|
|
2121
|
+
}
|
|
1751
2122
|
export {
|
|
1752
2123
|
HTTP_METHODS,
|
|
2124
|
+
Semaphore,
|
|
1753
2125
|
Spoosh,
|
|
1754
2126
|
__DEV__,
|
|
1755
2127
|
buildUrl,
|
|
@@ -1763,6 +2135,7 @@ export {
|
|
|
1763
2135
|
createPluginExecutor,
|
|
1764
2136
|
createPluginRegistry,
|
|
1765
2137
|
createProxyHandler,
|
|
2138
|
+
createQueueController,
|
|
1766
2139
|
createSelectorProxy,
|
|
1767
2140
|
createStateManager,
|
|
1768
2141
|
createTracer,
|
|
@@ -1781,6 +2154,7 @@ export {
|
|
|
1781
2154
|
mergeHeaders,
|
|
1782
2155
|
objectToFormData,
|
|
1783
2156
|
objectToUrlEncoded,
|
|
2157
|
+
removeHeaderKeys,
|
|
1784
2158
|
resolveHeadersToRecord,
|
|
1785
2159
|
resolvePath,
|
|
1786
2160
|
resolvePathString,
|