@spoosh/core 0.13.3 → 0.14.1
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 +160 -2
- package/dist/index.d.ts +160 -2
- package/dist/index.js +376 -0
- package/dist/index.mjs +376 -0
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -210,6 +210,7 @@ type EventEmitter = {
|
|
|
210
210
|
declare function createEventEmitter(): EventEmitter;
|
|
211
211
|
|
|
212
212
|
type Subscriber = () => void;
|
|
213
|
+
type DataChangeCallback = (key: string, oldData: unknown, newData: unknown) => void;
|
|
213
214
|
type CacheEntryWithKey<TData = unknown, TError = unknown> = {
|
|
214
215
|
key: string;
|
|
215
216
|
entry: CacheEntry<TData, TError>;
|
|
@@ -241,6 +242,11 @@ type StateManager = {
|
|
|
241
242
|
setPendingPromise: (key: string, promise: Promise<unknown> | undefined) => void;
|
|
242
243
|
/** Get a pending promise for a query key */
|
|
243
244
|
getPendingPromise: (key: string) => Promise<unknown> | undefined;
|
|
245
|
+
/**
|
|
246
|
+
* Register a callback to be invoked when cache data changes.
|
|
247
|
+
* @returns Unsubscribe function
|
|
248
|
+
*/
|
|
249
|
+
onDataChange: (callback: DataChangeCallback) => () => void;
|
|
244
250
|
clear: () => void;
|
|
245
251
|
};
|
|
246
252
|
declare function createStateManager(): StateManager;
|
|
@@ -429,7 +435,7 @@ interface EventTracer {
|
|
|
429
435
|
emit(msg: string, options?: EventOptions): void;
|
|
430
436
|
}
|
|
431
437
|
|
|
432
|
-
type OperationType = "read" | "write" | "infiniteRead";
|
|
438
|
+
type OperationType = "read" | "write" | "infiniteRead" | "queue";
|
|
433
439
|
type LifecyclePhase = "onMount" | "onUnmount" | "onUpdate";
|
|
434
440
|
type OperationState<TData = unknown, TError = unknown> = {
|
|
435
441
|
data: TData | undefined;
|
|
@@ -594,8 +600,11 @@ type PluginTypeConfig = {
|
|
|
594
600
|
writeOptions?: object;
|
|
595
601
|
infiniteReadOptions?: object;
|
|
596
602
|
writeTriggerOptions?: object;
|
|
603
|
+
queueOptions?: object;
|
|
604
|
+
queueTriggerOptions?: object;
|
|
597
605
|
readResult?: object;
|
|
598
606
|
writeResult?: object;
|
|
607
|
+
queueResult?: object;
|
|
599
608
|
instanceApi?: object;
|
|
600
609
|
};
|
|
601
610
|
/**
|
|
@@ -1024,6 +1033,15 @@ type ExtractInfiniteReadOptions<T> = T extends SpooshPlugin<infer Types> ? Types
|
|
|
1024
1033
|
type ExtractWriteTriggerOptions<T> = T extends SpooshPlugin<infer Types> ? Types extends {
|
|
1025
1034
|
writeTriggerOptions: infer W;
|
|
1026
1035
|
} ? W : object : object;
|
|
1036
|
+
type ExtractQueueOptions<T> = T extends SpooshPlugin<infer Types> ? Types extends {
|
|
1037
|
+
queueOptions: infer Q;
|
|
1038
|
+
} ? Q : object : object;
|
|
1039
|
+
type ExtractQueueTriggerOptions<T> = T extends SpooshPlugin<infer Types> ? Types extends {
|
|
1040
|
+
queueTriggerOptions: infer Q;
|
|
1041
|
+
} ? Q : object : object;
|
|
1042
|
+
type ExtractQueueResult<T> = T extends SpooshPlugin<infer Types> ? Types extends {
|
|
1043
|
+
queueResult: infer Q;
|
|
1044
|
+
} ? Q : object : object;
|
|
1027
1045
|
type ExtractReadResult<T> = T extends SpooshPlugin<infer Types> ? Types extends {
|
|
1028
1046
|
readResult: infer R;
|
|
1029
1047
|
} ? R : object : object;
|
|
@@ -1039,10 +1057,13 @@ type MergePluginOptions<TPlugins extends readonly SpooshPlugin<PluginTypeConfig>
|
|
|
1039
1057
|
write: UnionToIntersection<ExtractWriteOptions<TPlugins[number]>>;
|
|
1040
1058
|
infiniteRead: UnionToIntersection<ExtractInfiniteReadOptions<TPlugins[number]>>;
|
|
1041
1059
|
writeTrigger: UnionToIntersection<ExtractWriteTriggerOptions<TPlugins[number]>>;
|
|
1060
|
+
queue: UnionToIntersection<ExtractQueueOptions<TPlugins[number]>>;
|
|
1061
|
+
queueTrigger: UnionToIntersection<ExtractQueueTriggerOptions<TPlugins[number]>>;
|
|
1042
1062
|
};
|
|
1043
1063
|
type MergePluginResults<TPlugins extends readonly SpooshPlugin<PluginTypeConfig>[]> = {
|
|
1044
1064
|
read: UnionToIntersection<ExtractReadResult<TPlugins[number]>>;
|
|
1045
1065
|
write: UnionToIntersection<ExtractWriteResult<TPlugins[number]>>;
|
|
1066
|
+
queue: UnionToIntersection<ExtractQueueResult<TPlugins[number]>>;
|
|
1046
1067
|
};
|
|
1047
1068
|
type MergePluginInstanceApi<TPlugins extends readonly SpooshPlugin<PluginTypeConfig>[], TSchema = unknown> = ResolveInstanceApi<UnionToIntersection<ExtractInstanceApi<TPlugins[number]>>, TSchema, MergePluginOptions<TPlugins>["read"]>;
|
|
1048
1069
|
type PluginRegistry<TPlugins extends SpooshPlugin<PluginTypeConfig>[]> = {
|
|
@@ -1430,6 +1451,22 @@ type WriteSelectorPathMethods<TSchema, TPath extends string, TDefaultError> = Fi
|
|
|
1430
1451
|
* Used by useWrite for selecting endpoints. All input goes to trigger().
|
|
1431
1452
|
*/
|
|
1432
1453
|
type WriteSelectorClient<TSchema, TDefaultError = unknown> = <TPath extends WritePaths<TSchema> | (string & {})>(path: TPath) => HasWriteMethod<TSchema, TPath> extends true ? WriteSelectorPathMethods<TSchema, TPath, TDefaultError> : never;
|
|
1454
|
+
/**
|
|
1455
|
+
* Method function type for queue selectors - accepts no arguments.
|
|
1456
|
+
* All input (body, query, params) is passed to trigger() instead.
|
|
1457
|
+
*/
|
|
1458
|
+
type QueueSelectorMethodFn<TMethodConfig, TDefaultError, TUserPath extends string> = () => Promise<MethodResponse<TMethodConfig, TDefaultError, TUserPath>>;
|
|
1459
|
+
/**
|
|
1460
|
+
* Queue selector path methods - all HTTP methods, accepting no arguments.
|
|
1461
|
+
*/
|
|
1462
|
+
type QueueSelectorPathMethods<TSchema, TPath extends string, TDefaultError> = FindMatchingKey<TSchema, TPath> extends infer TKey ? TKey extends keyof TSchema ? Simplify<{
|
|
1463
|
+
[M in HttpMethod as M extends keyof TSchema[TKey] ? M : never]: M extends keyof TSchema[TKey] ? QueueSelectorMethodFn<TSchema[TKey][M], TDefaultError, TPath> : never;
|
|
1464
|
+
}> : never : never;
|
|
1465
|
+
/**
|
|
1466
|
+
* Queue selector client - all HTTP methods, accepting no arguments.
|
|
1467
|
+
* Used by useQueue for selecting endpoints. All input goes to trigger().
|
|
1468
|
+
*/
|
|
1469
|
+
type QueueSelectorClient<TSchema, TDefaultError = unknown> = <TPath extends SchemaPaths<TSchema> | (string & {})>(path: TPath) => QueueSelectorPathMethods<TSchema, TPath, TDefaultError>;
|
|
1433
1470
|
|
|
1434
1471
|
type PluginArray = readonly SpooshPlugin<PluginTypeConfig>[];
|
|
1435
1472
|
interface SpooshConfig<TPlugins extends PluginArray = PluginArray> {
|
|
@@ -2019,4 +2056,125 @@ type CreateInfiniteReadOptions<TData, TItem, TError, TRequest> = {
|
|
|
2019
2056
|
};
|
|
2020
2057
|
declare function createInfiniteReadController<TData, TItem, TError, TRequest extends InfiniteRequestOptions = InfiniteRequestOptions>(options: CreateInfiniteReadOptions<TData, TItem, TError, TRequest>): InfiniteReadController<TData, TItem, TError>;
|
|
2021
2058
|
|
|
2022
|
-
|
|
2059
|
+
/**
|
|
2060
|
+
* Status of an item in the queue.
|
|
2061
|
+
*/
|
|
2062
|
+
type QueueItemStatus = "pending" | "running" | "success" | "error" | "aborted";
|
|
2063
|
+
/**
|
|
2064
|
+
* Represents a single item in the queue.
|
|
2065
|
+
*/
|
|
2066
|
+
interface QueueItem<TData = unknown, TError = unknown, TMeta = Record<string, unknown>> {
|
|
2067
|
+
/** Unique identifier for this queue item */
|
|
2068
|
+
id: string;
|
|
2069
|
+
/** Current status of the item */
|
|
2070
|
+
status: QueueItemStatus;
|
|
2071
|
+
/** Response data on success */
|
|
2072
|
+
data?: TData;
|
|
2073
|
+
/** Error on failure */
|
|
2074
|
+
error?: TError;
|
|
2075
|
+
/** Original trigger input */
|
|
2076
|
+
input?: {
|
|
2077
|
+
body?: unknown;
|
|
2078
|
+
query?: unknown;
|
|
2079
|
+
params?: Record<string, string | number>;
|
|
2080
|
+
};
|
|
2081
|
+
/** Plugin-contributed metadata (e.g., progress, transformedData) */
|
|
2082
|
+
meta?: TMeta;
|
|
2083
|
+
}
|
|
2084
|
+
/**
|
|
2085
|
+
* Statistics information for the queue.
|
|
2086
|
+
*/
|
|
2087
|
+
interface QueueStats {
|
|
2088
|
+
/** Number of pending items waiting to run */
|
|
2089
|
+
pending: number;
|
|
2090
|
+
/** Number of currently running items */
|
|
2091
|
+
running: number;
|
|
2092
|
+
/** Number of settled items (success, error, or aborted) */
|
|
2093
|
+
settled: number;
|
|
2094
|
+
/** Number of successful items */
|
|
2095
|
+
success: number;
|
|
2096
|
+
/** Number of failed items (error or aborted) */
|
|
2097
|
+
failed: number;
|
|
2098
|
+
/** Total number of items in queue */
|
|
2099
|
+
total: number;
|
|
2100
|
+
/** Completion percentage (0-100) */
|
|
2101
|
+
percentage: number;
|
|
2102
|
+
}
|
|
2103
|
+
/**
|
|
2104
|
+
* Input type for queue trigger.
|
|
2105
|
+
*/
|
|
2106
|
+
interface QueueTriggerInput {
|
|
2107
|
+
/** Custom ID for this queue item. If not provided, one will be auto-generated. */
|
|
2108
|
+
id?: string;
|
|
2109
|
+
body?: unknown;
|
|
2110
|
+
query?: unknown;
|
|
2111
|
+
params?: Record<string, string | number>;
|
|
2112
|
+
}
|
|
2113
|
+
/**
|
|
2114
|
+
* Queue controller instance.
|
|
2115
|
+
* Framework-agnostic - can be used directly in Angular, Vue, etc.
|
|
2116
|
+
*/
|
|
2117
|
+
interface QueueController<TData = unknown, TError = unknown, TMeta = Record<string, unknown>> {
|
|
2118
|
+
/** Add item to queue and execute. Returns promise that resolves when item completes. */
|
|
2119
|
+
trigger: (input: QueueTriggerInput) => Promise<SpooshResponse<TData, TError>>;
|
|
2120
|
+
/** Get current queue state */
|
|
2121
|
+
getQueue: () => QueueItem<TData, TError, TMeta>[];
|
|
2122
|
+
/** Get queue statistics */
|
|
2123
|
+
getStats: () => QueueStats;
|
|
2124
|
+
/** Subscribe to queue state changes */
|
|
2125
|
+
subscribe: (callback: () => void) => () => void;
|
|
2126
|
+
/** Abort item by ID, or all items if no ID provided */
|
|
2127
|
+
abort: (id?: string) => void;
|
|
2128
|
+
/** Retry failed/aborted item by ID, or all failed items if no ID provided */
|
|
2129
|
+
retry: (id?: string) => Promise<void>;
|
|
2130
|
+
/** Remove specific item by ID (aborts if active) */
|
|
2131
|
+
remove: (id: string) => void;
|
|
2132
|
+
/** Remove all settled items (success, error, aborted). Keeps pending/running. */
|
|
2133
|
+
removeSettled: () => void;
|
|
2134
|
+
/** Abort all and clear entire queue */
|
|
2135
|
+
clear: () => void;
|
|
2136
|
+
/** Update the concurrency limit */
|
|
2137
|
+
setConcurrency: (concurrency: number) => void;
|
|
2138
|
+
}
|
|
2139
|
+
/**
|
|
2140
|
+
* Configuration for creating a queue controller.
|
|
2141
|
+
*/
|
|
2142
|
+
interface QueueControllerConfig {
|
|
2143
|
+
/** API path */
|
|
2144
|
+
path: string;
|
|
2145
|
+
/** HTTP method */
|
|
2146
|
+
method: string;
|
|
2147
|
+
/** Maximum concurrent operations. Defaults to 3. */
|
|
2148
|
+
concurrency?: number;
|
|
2149
|
+
/** Operation type for plugin middleware */
|
|
2150
|
+
operationType: "read" | "write" | "queue";
|
|
2151
|
+
/** Hook-level plugin options (e.g., progress, retries) */
|
|
2152
|
+
hookOptions?: Record<string, unknown>;
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
interface QueueControllerContext {
|
|
2156
|
+
api: unknown;
|
|
2157
|
+
stateManager: StateManager;
|
|
2158
|
+
eventEmitter: EventEmitter;
|
|
2159
|
+
pluginExecutor: InstancePluginExecutor;
|
|
2160
|
+
}
|
|
2161
|
+
declare function createQueueController<TData, TError, TMeta = Record<string, unknown>>(config: QueueControllerConfig, context: QueueControllerContext): QueueController<TData, TError, TMeta>;
|
|
2162
|
+
|
|
2163
|
+
/**
|
|
2164
|
+
* Semaphore for controlling concurrent access.
|
|
2165
|
+
* Used to limit the number of concurrent operations in the queue.
|
|
2166
|
+
*/
|
|
2167
|
+
declare class Semaphore {
|
|
2168
|
+
private max;
|
|
2169
|
+
private current;
|
|
2170
|
+
private waiting;
|
|
2171
|
+
constructor(max: number);
|
|
2172
|
+
acquire(): Promise<boolean>;
|
|
2173
|
+
release(): void;
|
|
2174
|
+
setConcurrency(max: number): void;
|
|
2175
|
+
reset(): void;
|
|
2176
|
+
getCurrent(): number;
|
|
2177
|
+
getWaitingCount(): number;
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
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
|
@@ -210,6 +210,7 @@ type EventEmitter = {
|
|
|
210
210
|
declare function createEventEmitter(): EventEmitter;
|
|
211
211
|
|
|
212
212
|
type Subscriber = () => void;
|
|
213
|
+
type DataChangeCallback = (key: string, oldData: unknown, newData: unknown) => void;
|
|
213
214
|
type CacheEntryWithKey<TData = unknown, TError = unknown> = {
|
|
214
215
|
key: string;
|
|
215
216
|
entry: CacheEntry<TData, TError>;
|
|
@@ -241,6 +242,11 @@ type StateManager = {
|
|
|
241
242
|
setPendingPromise: (key: string, promise: Promise<unknown> | undefined) => void;
|
|
242
243
|
/** Get a pending promise for a query key */
|
|
243
244
|
getPendingPromise: (key: string) => Promise<unknown> | undefined;
|
|
245
|
+
/**
|
|
246
|
+
* Register a callback to be invoked when cache data changes.
|
|
247
|
+
* @returns Unsubscribe function
|
|
248
|
+
*/
|
|
249
|
+
onDataChange: (callback: DataChangeCallback) => () => void;
|
|
244
250
|
clear: () => void;
|
|
245
251
|
};
|
|
246
252
|
declare function createStateManager(): StateManager;
|
|
@@ -429,7 +435,7 @@ interface EventTracer {
|
|
|
429
435
|
emit(msg: string, options?: EventOptions): void;
|
|
430
436
|
}
|
|
431
437
|
|
|
432
|
-
type OperationType = "read" | "write" | "infiniteRead";
|
|
438
|
+
type OperationType = "read" | "write" | "infiniteRead" | "queue";
|
|
433
439
|
type LifecyclePhase = "onMount" | "onUnmount" | "onUpdate";
|
|
434
440
|
type OperationState<TData = unknown, TError = unknown> = {
|
|
435
441
|
data: TData | undefined;
|
|
@@ -594,8 +600,11 @@ type PluginTypeConfig = {
|
|
|
594
600
|
writeOptions?: object;
|
|
595
601
|
infiniteReadOptions?: object;
|
|
596
602
|
writeTriggerOptions?: object;
|
|
603
|
+
queueOptions?: object;
|
|
604
|
+
queueTriggerOptions?: object;
|
|
597
605
|
readResult?: object;
|
|
598
606
|
writeResult?: object;
|
|
607
|
+
queueResult?: object;
|
|
599
608
|
instanceApi?: object;
|
|
600
609
|
};
|
|
601
610
|
/**
|
|
@@ -1024,6 +1033,15 @@ type ExtractInfiniteReadOptions<T> = T extends SpooshPlugin<infer Types> ? Types
|
|
|
1024
1033
|
type ExtractWriteTriggerOptions<T> = T extends SpooshPlugin<infer Types> ? Types extends {
|
|
1025
1034
|
writeTriggerOptions: infer W;
|
|
1026
1035
|
} ? W : object : object;
|
|
1036
|
+
type ExtractQueueOptions<T> = T extends SpooshPlugin<infer Types> ? Types extends {
|
|
1037
|
+
queueOptions: infer Q;
|
|
1038
|
+
} ? Q : object : object;
|
|
1039
|
+
type ExtractQueueTriggerOptions<T> = T extends SpooshPlugin<infer Types> ? Types extends {
|
|
1040
|
+
queueTriggerOptions: infer Q;
|
|
1041
|
+
} ? Q : object : object;
|
|
1042
|
+
type ExtractQueueResult<T> = T extends SpooshPlugin<infer Types> ? Types extends {
|
|
1043
|
+
queueResult: infer Q;
|
|
1044
|
+
} ? Q : object : object;
|
|
1027
1045
|
type ExtractReadResult<T> = T extends SpooshPlugin<infer Types> ? Types extends {
|
|
1028
1046
|
readResult: infer R;
|
|
1029
1047
|
} ? R : object : object;
|
|
@@ -1039,10 +1057,13 @@ type MergePluginOptions<TPlugins extends readonly SpooshPlugin<PluginTypeConfig>
|
|
|
1039
1057
|
write: UnionToIntersection<ExtractWriteOptions<TPlugins[number]>>;
|
|
1040
1058
|
infiniteRead: UnionToIntersection<ExtractInfiniteReadOptions<TPlugins[number]>>;
|
|
1041
1059
|
writeTrigger: UnionToIntersection<ExtractWriteTriggerOptions<TPlugins[number]>>;
|
|
1060
|
+
queue: UnionToIntersection<ExtractQueueOptions<TPlugins[number]>>;
|
|
1061
|
+
queueTrigger: UnionToIntersection<ExtractQueueTriggerOptions<TPlugins[number]>>;
|
|
1042
1062
|
};
|
|
1043
1063
|
type MergePluginResults<TPlugins extends readonly SpooshPlugin<PluginTypeConfig>[]> = {
|
|
1044
1064
|
read: UnionToIntersection<ExtractReadResult<TPlugins[number]>>;
|
|
1045
1065
|
write: UnionToIntersection<ExtractWriteResult<TPlugins[number]>>;
|
|
1066
|
+
queue: UnionToIntersection<ExtractQueueResult<TPlugins[number]>>;
|
|
1046
1067
|
};
|
|
1047
1068
|
type MergePluginInstanceApi<TPlugins extends readonly SpooshPlugin<PluginTypeConfig>[], TSchema = unknown> = ResolveInstanceApi<UnionToIntersection<ExtractInstanceApi<TPlugins[number]>>, TSchema, MergePluginOptions<TPlugins>["read"]>;
|
|
1048
1069
|
type PluginRegistry<TPlugins extends SpooshPlugin<PluginTypeConfig>[]> = {
|
|
@@ -1430,6 +1451,22 @@ type WriteSelectorPathMethods<TSchema, TPath extends string, TDefaultError> = Fi
|
|
|
1430
1451
|
* Used by useWrite for selecting endpoints. All input goes to trigger().
|
|
1431
1452
|
*/
|
|
1432
1453
|
type WriteSelectorClient<TSchema, TDefaultError = unknown> = <TPath extends WritePaths<TSchema> | (string & {})>(path: TPath) => HasWriteMethod<TSchema, TPath> extends true ? WriteSelectorPathMethods<TSchema, TPath, TDefaultError> : never;
|
|
1454
|
+
/**
|
|
1455
|
+
* Method function type for queue selectors - accepts no arguments.
|
|
1456
|
+
* All input (body, query, params) is passed to trigger() instead.
|
|
1457
|
+
*/
|
|
1458
|
+
type QueueSelectorMethodFn<TMethodConfig, TDefaultError, TUserPath extends string> = () => Promise<MethodResponse<TMethodConfig, TDefaultError, TUserPath>>;
|
|
1459
|
+
/**
|
|
1460
|
+
* Queue selector path methods - all HTTP methods, accepting no arguments.
|
|
1461
|
+
*/
|
|
1462
|
+
type QueueSelectorPathMethods<TSchema, TPath extends string, TDefaultError> = FindMatchingKey<TSchema, TPath> extends infer TKey ? TKey extends keyof TSchema ? Simplify<{
|
|
1463
|
+
[M in HttpMethod as M extends keyof TSchema[TKey] ? M : never]: M extends keyof TSchema[TKey] ? QueueSelectorMethodFn<TSchema[TKey][M], TDefaultError, TPath> : never;
|
|
1464
|
+
}> : never : never;
|
|
1465
|
+
/**
|
|
1466
|
+
* Queue selector client - all HTTP methods, accepting no arguments.
|
|
1467
|
+
* Used by useQueue for selecting endpoints. All input goes to trigger().
|
|
1468
|
+
*/
|
|
1469
|
+
type QueueSelectorClient<TSchema, TDefaultError = unknown> = <TPath extends SchemaPaths<TSchema> | (string & {})>(path: TPath) => QueueSelectorPathMethods<TSchema, TPath, TDefaultError>;
|
|
1433
1470
|
|
|
1434
1471
|
type PluginArray = readonly SpooshPlugin<PluginTypeConfig>[];
|
|
1435
1472
|
interface SpooshConfig<TPlugins extends PluginArray = PluginArray> {
|
|
@@ -2019,4 +2056,125 @@ type CreateInfiniteReadOptions<TData, TItem, TError, TRequest> = {
|
|
|
2019
2056
|
};
|
|
2020
2057
|
declare function createInfiniteReadController<TData, TItem, TError, TRequest extends InfiniteRequestOptions = InfiniteRequestOptions>(options: CreateInfiniteReadOptions<TData, TItem, TError, TRequest>): InfiniteReadController<TData, TItem, TError>;
|
|
2021
2058
|
|
|
2022
|
-
|
|
2059
|
+
/**
|
|
2060
|
+
* Status of an item in the queue.
|
|
2061
|
+
*/
|
|
2062
|
+
type QueueItemStatus = "pending" | "running" | "success" | "error" | "aborted";
|
|
2063
|
+
/**
|
|
2064
|
+
* Represents a single item in the queue.
|
|
2065
|
+
*/
|
|
2066
|
+
interface QueueItem<TData = unknown, TError = unknown, TMeta = Record<string, unknown>> {
|
|
2067
|
+
/** Unique identifier for this queue item */
|
|
2068
|
+
id: string;
|
|
2069
|
+
/** Current status of the item */
|
|
2070
|
+
status: QueueItemStatus;
|
|
2071
|
+
/** Response data on success */
|
|
2072
|
+
data?: TData;
|
|
2073
|
+
/** Error on failure */
|
|
2074
|
+
error?: TError;
|
|
2075
|
+
/** Original trigger input */
|
|
2076
|
+
input?: {
|
|
2077
|
+
body?: unknown;
|
|
2078
|
+
query?: unknown;
|
|
2079
|
+
params?: Record<string, string | number>;
|
|
2080
|
+
};
|
|
2081
|
+
/** Plugin-contributed metadata (e.g., progress, transformedData) */
|
|
2082
|
+
meta?: TMeta;
|
|
2083
|
+
}
|
|
2084
|
+
/**
|
|
2085
|
+
* Statistics information for the queue.
|
|
2086
|
+
*/
|
|
2087
|
+
interface QueueStats {
|
|
2088
|
+
/** Number of pending items waiting to run */
|
|
2089
|
+
pending: number;
|
|
2090
|
+
/** Number of currently running items */
|
|
2091
|
+
running: number;
|
|
2092
|
+
/** Number of settled items (success, error, or aborted) */
|
|
2093
|
+
settled: number;
|
|
2094
|
+
/** Number of successful items */
|
|
2095
|
+
success: number;
|
|
2096
|
+
/** Number of failed items (error or aborted) */
|
|
2097
|
+
failed: number;
|
|
2098
|
+
/** Total number of items in queue */
|
|
2099
|
+
total: number;
|
|
2100
|
+
/** Completion percentage (0-100) */
|
|
2101
|
+
percentage: number;
|
|
2102
|
+
}
|
|
2103
|
+
/**
|
|
2104
|
+
* Input type for queue trigger.
|
|
2105
|
+
*/
|
|
2106
|
+
interface QueueTriggerInput {
|
|
2107
|
+
/** Custom ID for this queue item. If not provided, one will be auto-generated. */
|
|
2108
|
+
id?: string;
|
|
2109
|
+
body?: unknown;
|
|
2110
|
+
query?: unknown;
|
|
2111
|
+
params?: Record<string, string | number>;
|
|
2112
|
+
}
|
|
2113
|
+
/**
|
|
2114
|
+
* Queue controller instance.
|
|
2115
|
+
* Framework-agnostic - can be used directly in Angular, Vue, etc.
|
|
2116
|
+
*/
|
|
2117
|
+
interface QueueController<TData = unknown, TError = unknown, TMeta = Record<string, unknown>> {
|
|
2118
|
+
/** Add item to queue and execute. Returns promise that resolves when item completes. */
|
|
2119
|
+
trigger: (input: QueueTriggerInput) => Promise<SpooshResponse<TData, TError>>;
|
|
2120
|
+
/** Get current queue state */
|
|
2121
|
+
getQueue: () => QueueItem<TData, TError, TMeta>[];
|
|
2122
|
+
/** Get queue statistics */
|
|
2123
|
+
getStats: () => QueueStats;
|
|
2124
|
+
/** Subscribe to queue state changes */
|
|
2125
|
+
subscribe: (callback: () => void) => () => void;
|
|
2126
|
+
/** Abort item by ID, or all items if no ID provided */
|
|
2127
|
+
abort: (id?: string) => void;
|
|
2128
|
+
/** Retry failed/aborted item by ID, or all failed items if no ID provided */
|
|
2129
|
+
retry: (id?: string) => Promise<void>;
|
|
2130
|
+
/** Remove specific item by ID (aborts if active) */
|
|
2131
|
+
remove: (id: string) => void;
|
|
2132
|
+
/** Remove all settled items (success, error, aborted). Keeps pending/running. */
|
|
2133
|
+
removeSettled: () => void;
|
|
2134
|
+
/** Abort all and clear entire queue */
|
|
2135
|
+
clear: () => void;
|
|
2136
|
+
/** Update the concurrency limit */
|
|
2137
|
+
setConcurrency: (concurrency: number) => void;
|
|
2138
|
+
}
|
|
2139
|
+
/**
|
|
2140
|
+
* Configuration for creating a queue controller.
|
|
2141
|
+
*/
|
|
2142
|
+
interface QueueControllerConfig {
|
|
2143
|
+
/** API path */
|
|
2144
|
+
path: string;
|
|
2145
|
+
/** HTTP method */
|
|
2146
|
+
method: string;
|
|
2147
|
+
/** Maximum concurrent operations. Defaults to 3. */
|
|
2148
|
+
concurrency?: number;
|
|
2149
|
+
/** Operation type for plugin middleware */
|
|
2150
|
+
operationType: "read" | "write" | "queue";
|
|
2151
|
+
/** Hook-level plugin options (e.g., progress, retries) */
|
|
2152
|
+
hookOptions?: Record<string, unknown>;
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
interface QueueControllerContext {
|
|
2156
|
+
api: unknown;
|
|
2157
|
+
stateManager: StateManager;
|
|
2158
|
+
eventEmitter: EventEmitter;
|
|
2159
|
+
pluginExecutor: InstancePluginExecutor;
|
|
2160
|
+
}
|
|
2161
|
+
declare function createQueueController<TData, TError, TMeta = Record<string, unknown>>(config: QueueControllerConfig, context: QueueControllerContext): QueueController<TData, TError, TMeta>;
|
|
2162
|
+
|
|
2163
|
+
/**
|
|
2164
|
+
* Semaphore for controlling concurrent access.
|
|
2165
|
+
* Used to limit the number of concurrent operations in the queue.
|
|
2166
|
+
*/
|
|
2167
|
+
declare class Semaphore {
|
|
2168
|
+
private max;
|
|
2169
|
+
private current;
|
|
2170
|
+
private waiting;
|
|
2171
|
+
constructor(max: number);
|
|
2172
|
+
acquire(): Promise<boolean>;
|
|
2173
|
+
release(): void;
|
|
2174
|
+
setConcurrency(max: number): void;
|
|
2175
|
+
reset(): void;
|
|
2176
|
+
getCurrent(): number;
|
|
2177
|
+
getWaitingCount(): number;
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
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,
|
|
@@ -765,6 +767,7 @@ function createStateManager() {
|
|
|
765
767
|
const cache = /* @__PURE__ */ new Map();
|
|
766
768
|
const subscribers = /* @__PURE__ */ new Map();
|
|
767
769
|
const pendingPromises = /* @__PURE__ */ new Map();
|
|
770
|
+
const dataChangeCallbacks = /* @__PURE__ */ new Set();
|
|
768
771
|
const notifySubscribers = (key) => {
|
|
769
772
|
const subs = subscribers.get(key);
|
|
770
773
|
subs?.forEach((cb) => cb());
|
|
@@ -784,6 +787,7 @@ function createStateManager() {
|
|
|
784
787
|
},
|
|
785
788
|
setCache(key, entry) {
|
|
786
789
|
const existing = cache.get(key);
|
|
790
|
+
const oldData = existing?.state.data;
|
|
787
791
|
if (existing) {
|
|
788
792
|
existing.state = { ...existing.state, ...entry.state };
|
|
789
793
|
if (entry.tags) {
|
|
@@ -796,6 +800,10 @@ function createStateManager() {
|
|
|
796
800
|
existing.stale = entry.stale;
|
|
797
801
|
}
|
|
798
802
|
notifySubscribers(key);
|
|
803
|
+
const newData = existing.state.data;
|
|
804
|
+
if (oldData !== newData) {
|
|
805
|
+
dataChangeCallbacks.forEach((cb) => cb(key, oldData, newData));
|
|
806
|
+
}
|
|
799
807
|
} else {
|
|
800
808
|
const newEntry = {
|
|
801
809
|
state: entry.state ?? createInitialState(),
|
|
@@ -807,6 +815,10 @@ function createStateManager() {
|
|
|
807
815
|
};
|
|
808
816
|
cache.set(key, newEntry);
|
|
809
817
|
notifySubscribers(key);
|
|
818
|
+
const newData = newEntry.state.data;
|
|
819
|
+
if (oldData !== newData) {
|
|
820
|
+
dataChangeCallbacks.forEach((cb) => cb(key, oldData, newData));
|
|
821
|
+
}
|
|
810
822
|
}
|
|
811
823
|
},
|
|
812
824
|
deleteCache(key) {
|
|
@@ -911,10 +923,17 @@ function createStateManager() {
|
|
|
911
923
|
getPendingPromise(key) {
|
|
912
924
|
return pendingPromises.get(key);
|
|
913
925
|
},
|
|
926
|
+
onDataChange(callback) {
|
|
927
|
+
dataChangeCallbacks.add(callback);
|
|
928
|
+
return () => {
|
|
929
|
+
dataChangeCallbacks.delete(callback);
|
|
930
|
+
};
|
|
931
|
+
},
|
|
914
932
|
clear() {
|
|
915
933
|
cache.clear();
|
|
916
934
|
subscribers.clear();
|
|
917
935
|
pendingPromises.clear();
|
|
936
|
+
dataChangeCallbacks.clear();
|
|
918
937
|
}
|
|
919
938
|
};
|
|
920
939
|
}
|
|
@@ -1829,3 +1848,360 @@ function createInfiniteReadController(options) {
|
|
|
1829
1848
|
};
|
|
1830
1849
|
return controller;
|
|
1831
1850
|
}
|
|
1851
|
+
|
|
1852
|
+
// src/queue/semaphore.ts
|
|
1853
|
+
var Semaphore = class {
|
|
1854
|
+
constructor(max) {
|
|
1855
|
+
this.max = max;
|
|
1856
|
+
}
|
|
1857
|
+
current = 0;
|
|
1858
|
+
waiting = [];
|
|
1859
|
+
async acquire() {
|
|
1860
|
+
if (this.current < this.max) {
|
|
1861
|
+
this.current++;
|
|
1862
|
+
return true;
|
|
1863
|
+
}
|
|
1864
|
+
return new Promise((resolve) => this.waiting.push(resolve));
|
|
1865
|
+
}
|
|
1866
|
+
release() {
|
|
1867
|
+
if (this.current > 0) {
|
|
1868
|
+
this.current--;
|
|
1869
|
+
}
|
|
1870
|
+
if (this.waiting.length > 0) {
|
|
1871
|
+
this.current++;
|
|
1872
|
+
this.waiting.shift()(true);
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
setConcurrency(max) {
|
|
1876
|
+
const previousMax = this.max;
|
|
1877
|
+
this.max = max;
|
|
1878
|
+
if (max > previousMax) {
|
|
1879
|
+
const slotsToRelease = Math.min(max - previousMax, this.waiting.length);
|
|
1880
|
+
for (let i = 0; i < slotsToRelease; i++) {
|
|
1881
|
+
this.current++;
|
|
1882
|
+
this.waiting.shift()(true);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
reset() {
|
|
1887
|
+
this.current = 0;
|
|
1888
|
+
while (this.waiting.length > 0) {
|
|
1889
|
+
const resolve = this.waiting.shift();
|
|
1890
|
+
resolve(false);
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
getCurrent() {
|
|
1894
|
+
return this.current;
|
|
1895
|
+
}
|
|
1896
|
+
getWaitingCount() {
|
|
1897
|
+
return this.waiting.length;
|
|
1898
|
+
}
|
|
1899
|
+
};
|
|
1900
|
+
|
|
1901
|
+
// src/queue/controller.ts
|
|
1902
|
+
var DEFAULT_CONCURRENCY = 3;
|
|
1903
|
+
function createQueueController(config, context) {
|
|
1904
|
+
const { path, method, operationType, hookOptions = {} } = config;
|
|
1905
|
+
const concurrency = config.concurrency ?? DEFAULT_CONCURRENCY;
|
|
1906
|
+
const { api, stateManager, eventEmitter, pluginExecutor } = context;
|
|
1907
|
+
const semaphore = new Semaphore(concurrency);
|
|
1908
|
+
const queue = [];
|
|
1909
|
+
const abortControllers = /* @__PURE__ */ new Map();
|
|
1910
|
+
const subscribers = /* @__PURE__ */ new Set();
|
|
1911
|
+
const itemPromises = /* @__PURE__ */ new Map();
|
|
1912
|
+
let cachedQueueSnapshot = [];
|
|
1913
|
+
const notify = () => {
|
|
1914
|
+
cachedQueueSnapshot = [...queue];
|
|
1915
|
+
subscribers.forEach((cb) => cb());
|
|
1916
|
+
};
|
|
1917
|
+
const generateId = () => `q-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1918
|
+
const updateItem = (id, update) => {
|
|
1919
|
+
const item = queue.find((i) => i.id === id);
|
|
1920
|
+
if (item) {
|
|
1921
|
+
Object.assign(item, update);
|
|
1922
|
+
}
|
|
1923
|
+
};
|
|
1924
|
+
const executeItem = async (item) => {
|
|
1925
|
+
const acquired = await semaphore.acquire();
|
|
1926
|
+
if (!acquired || item.status === "aborted") {
|
|
1927
|
+
if (acquired) {
|
|
1928
|
+
semaphore.release();
|
|
1929
|
+
}
|
|
1930
|
+
const response = {
|
|
1931
|
+
error: new Error("Aborted"),
|
|
1932
|
+
aborted: true
|
|
1933
|
+
};
|
|
1934
|
+
itemPromises.get(item.id)?.resolve(response);
|
|
1935
|
+
itemPromises.delete(item.id);
|
|
1936
|
+
return response;
|
|
1937
|
+
}
|
|
1938
|
+
const abortController = new AbortController();
|
|
1939
|
+
abortControllers.set(item.id, abortController);
|
|
1940
|
+
const queryKey = stateManager.createQueryKey({
|
|
1941
|
+
path,
|
|
1942
|
+
method,
|
|
1943
|
+
options: { ...item.input, _queueId: item.id }
|
|
1944
|
+
});
|
|
1945
|
+
const unsubscribeMeta = stateManager.subscribeCache(queryKey, () => {
|
|
1946
|
+
const cacheEntry = stateManager.getCache(queryKey);
|
|
1947
|
+
const meta = cacheEntry?.meta ? Object.fromEntries(cacheEntry.meta) : void 0;
|
|
1948
|
+
if (meta) {
|
|
1949
|
+
updateItem(item.id, { meta });
|
|
1950
|
+
notify();
|
|
1951
|
+
}
|
|
1952
|
+
});
|
|
1953
|
+
try {
|
|
1954
|
+
updateItem(item.id, { status: "running" });
|
|
1955
|
+
notify();
|
|
1956
|
+
const { body, query, params, ...triggerOptions } = item.input ?? {};
|
|
1957
|
+
const pluginContext = pluginExecutor.createContext({
|
|
1958
|
+
operationType,
|
|
1959
|
+
path,
|
|
1960
|
+
method,
|
|
1961
|
+
queryKey,
|
|
1962
|
+
tags: [],
|
|
1963
|
+
requestTimestamp: Date.now(),
|
|
1964
|
+
request: {
|
|
1965
|
+
headers: {},
|
|
1966
|
+
body,
|
|
1967
|
+
query,
|
|
1968
|
+
params,
|
|
1969
|
+
signal: abortController.signal
|
|
1970
|
+
},
|
|
1971
|
+
temp: /* @__PURE__ */ new Map(),
|
|
1972
|
+
pluginOptions: { ...hookOptions, ...triggerOptions },
|
|
1973
|
+
stateManager,
|
|
1974
|
+
eventEmitter
|
|
1975
|
+
});
|
|
1976
|
+
const coreFetch = async () => {
|
|
1977
|
+
const pathMethods = api(
|
|
1978
|
+
path
|
|
1979
|
+
);
|
|
1980
|
+
const methodFn = pathMethods[method];
|
|
1981
|
+
const { transport, transportOptions } = pluginContext.request;
|
|
1982
|
+
return methodFn({
|
|
1983
|
+
body,
|
|
1984
|
+
query,
|
|
1985
|
+
params,
|
|
1986
|
+
signal: abortController.signal,
|
|
1987
|
+
transport,
|
|
1988
|
+
transportOptions
|
|
1989
|
+
});
|
|
1990
|
+
};
|
|
1991
|
+
const response = await pluginExecutor.executeMiddleware(
|
|
1992
|
+
operationType,
|
|
1993
|
+
pluginContext,
|
|
1994
|
+
coreFetch
|
|
1995
|
+
);
|
|
1996
|
+
const cacheEntry = stateManager.getCache(queryKey);
|
|
1997
|
+
const meta = cacheEntry?.meta ? Object.fromEntries(cacheEntry.meta) : void 0;
|
|
1998
|
+
if (response.error) {
|
|
1999
|
+
updateItem(item.id, { status: "error", error: response.error, meta });
|
|
2000
|
+
itemPromises.get(item.id)?.resolve(response);
|
|
2001
|
+
} else {
|
|
2002
|
+
updateItem(item.id, { status: "success", data: response.data, meta });
|
|
2003
|
+
itemPromises.get(item.id)?.resolve(response);
|
|
2004
|
+
}
|
|
2005
|
+
return response;
|
|
2006
|
+
} catch (err) {
|
|
2007
|
+
const isAborted = abortController.signal.aborted;
|
|
2008
|
+
if (isAborted) {
|
|
2009
|
+
updateItem(item.id, { status: "aborted" });
|
|
2010
|
+
} else {
|
|
2011
|
+
updateItem(item.id, { status: "error", error: err });
|
|
2012
|
+
}
|
|
2013
|
+
const errorResponse = {
|
|
2014
|
+
error: err,
|
|
2015
|
+
aborted: isAborted
|
|
2016
|
+
};
|
|
2017
|
+
itemPromises.get(item.id)?.resolve(errorResponse);
|
|
2018
|
+
return errorResponse;
|
|
2019
|
+
} finally {
|
|
2020
|
+
unsubscribeMeta();
|
|
2021
|
+
abortControllers.delete(item.id);
|
|
2022
|
+
itemPromises.delete(item.id);
|
|
2023
|
+
notify();
|
|
2024
|
+
semaphore.release();
|
|
2025
|
+
}
|
|
2026
|
+
};
|
|
2027
|
+
return {
|
|
2028
|
+
trigger(input) {
|
|
2029
|
+
const { id: customId, ...requestInput } = input;
|
|
2030
|
+
const id = customId ?? generateId();
|
|
2031
|
+
const item = {
|
|
2032
|
+
id,
|
|
2033
|
+
status: "pending",
|
|
2034
|
+
input: requestInput
|
|
2035
|
+
};
|
|
2036
|
+
queue.push(item);
|
|
2037
|
+
notify();
|
|
2038
|
+
const promise = new Promise(
|
|
2039
|
+
(resolve, reject) => {
|
|
2040
|
+
itemPromises.set(id, { resolve, reject });
|
|
2041
|
+
}
|
|
2042
|
+
);
|
|
2043
|
+
executeItem(item);
|
|
2044
|
+
return promise;
|
|
2045
|
+
},
|
|
2046
|
+
getQueue: () => cachedQueueSnapshot,
|
|
2047
|
+
getStats: () => {
|
|
2048
|
+
let pending = 0;
|
|
2049
|
+
let running = 0;
|
|
2050
|
+
let success = 0;
|
|
2051
|
+
let failed = 0;
|
|
2052
|
+
for (const item of queue) {
|
|
2053
|
+
if (item.status === "pending") pending++;
|
|
2054
|
+
else if (item.status === "running") running++;
|
|
2055
|
+
else if (item.status === "success") success++;
|
|
2056
|
+
else if (item.status === "error" || item.status === "aborted") failed++;
|
|
2057
|
+
}
|
|
2058
|
+
const settled = success + failed;
|
|
2059
|
+
const total = queue.length;
|
|
2060
|
+
return {
|
|
2061
|
+
pending,
|
|
2062
|
+
running,
|
|
2063
|
+
settled,
|
|
2064
|
+
success,
|
|
2065
|
+
failed,
|
|
2066
|
+
total,
|
|
2067
|
+
percentage: total > 0 ? Math.round(settled / total * 100) : 0
|
|
2068
|
+
};
|
|
2069
|
+
},
|
|
2070
|
+
subscribe: (callback) => {
|
|
2071
|
+
subscribers.add(callback);
|
|
2072
|
+
return () => subscribers.delete(callback);
|
|
2073
|
+
},
|
|
2074
|
+
abort: (id) => {
|
|
2075
|
+
const queryKeysToDiscard = [];
|
|
2076
|
+
const abortedResponse = {
|
|
2077
|
+
error: new Error("Aborted"),
|
|
2078
|
+
aborted: true
|
|
2079
|
+
};
|
|
2080
|
+
if (id) {
|
|
2081
|
+
const item = queue.find((i) => i.id === id);
|
|
2082
|
+
if (item && (item.status === "pending" || item.status === "running")) {
|
|
2083
|
+
const wasPending = item.status === "pending";
|
|
2084
|
+
abortControllers.get(id)?.abort();
|
|
2085
|
+
updateItem(id, { status: "aborted" });
|
|
2086
|
+
if (wasPending) {
|
|
2087
|
+
itemPromises.get(id)?.resolve(abortedResponse);
|
|
2088
|
+
itemPromises.delete(id);
|
|
2089
|
+
}
|
|
2090
|
+
const queryKey = stateManager.createQueryKey({
|
|
2091
|
+
path,
|
|
2092
|
+
method,
|
|
2093
|
+
options: { ...item.input, _queueId: item.id }
|
|
2094
|
+
});
|
|
2095
|
+
queryKeysToDiscard.push(queryKey);
|
|
2096
|
+
notify();
|
|
2097
|
+
}
|
|
2098
|
+
} else {
|
|
2099
|
+
for (const item of queue) {
|
|
2100
|
+
if (item.status === "pending" || item.status === "running") {
|
|
2101
|
+
abortControllers.get(item.id)?.abort();
|
|
2102
|
+
const wasPending = item.status === "pending";
|
|
2103
|
+
updateItem(item.id, { status: "aborted" });
|
|
2104
|
+
if (wasPending) {
|
|
2105
|
+
itemPromises.get(item.id)?.resolve(abortedResponse);
|
|
2106
|
+
itemPromises.delete(item.id);
|
|
2107
|
+
}
|
|
2108
|
+
const queryKey = stateManager.createQueryKey({
|
|
2109
|
+
path,
|
|
2110
|
+
method,
|
|
2111
|
+
options: { ...item.input, _queueId: item.id }
|
|
2112
|
+
});
|
|
2113
|
+
queryKeysToDiscard.push(queryKey);
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
notify();
|
|
2117
|
+
}
|
|
2118
|
+
if (queryKeysToDiscard.length > 0) {
|
|
2119
|
+
eventEmitter.emit("spoosh:queue-abort", {
|
|
2120
|
+
queryKeys: queryKeysToDiscard
|
|
2121
|
+
});
|
|
2122
|
+
}
|
|
2123
|
+
},
|
|
2124
|
+
retry: async (id) => {
|
|
2125
|
+
const items = id ? queue.filter(
|
|
2126
|
+
(i) => i.id === id && (i.status === "error" || i.status === "aborted")
|
|
2127
|
+
) : queue.filter((i) => i.status === "error" || i.status === "aborted");
|
|
2128
|
+
const promises = [];
|
|
2129
|
+
for (const item of items) {
|
|
2130
|
+
updateItem(item.id, { status: "pending", error: void 0 });
|
|
2131
|
+
const promise = new Promise(
|
|
2132
|
+
(resolve) => {
|
|
2133
|
+
itemPromises.set(item.id, { resolve, reject: () => {
|
|
2134
|
+
} });
|
|
2135
|
+
}
|
|
2136
|
+
);
|
|
2137
|
+
promises.push(promise);
|
|
2138
|
+
executeItem(item);
|
|
2139
|
+
}
|
|
2140
|
+
await Promise.all(promises);
|
|
2141
|
+
},
|
|
2142
|
+
remove: (id) => {
|
|
2143
|
+
const abortedResponse = {
|
|
2144
|
+
error: new Error("Removed"),
|
|
2145
|
+
aborted: true
|
|
2146
|
+
};
|
|
2147
|
+
const item = queue.find((i) => i.id === id);
|
|
2148
|
+
if (item) {
|
|
2149
|
+
if (item.status === "pending") {
|
|
2150
|
+
item.status = "aborted";
|
|
2151
|
+
itemPromises.get(id)?.resolve(abortedResponse);
|
|
2152
|
+
itemPromises.delete(id);
|
|
2153
|
+
} else if (item.status === "running") {
|
|
2154
|
+
abortControllers.get(id)?.abort();
|
|
2155
|
+
}
|
|
2156
|
+
const idx = queue.findIndex((i) => i.id === id);
|
|
2157
|
+
if (idx !== -1) {
|
|
2158
|
+
queue.splice(idx, 1);
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
notify();
|
|
2162
|
+
},
|
|
2163
|
+
removeSettled: () => {
|
|
2164
|
+
const active = queue.filter(
|
|
2165
|
+
(i) => i.status === "pending" || i.status === "running"
|
|
2166
|
+
);
|
|
2167
|
+
queue.length = 0;
|
|
2168
|
+
queue.push(...active);
|
|
2169
|
+
notify();
|
|
2170
|
+
},
|
|
2171
|
+
clear: () => {
|
|
2172
|
+
const queryKeysToDiscard = [];
|
|
2173
|
+
for (const item of queue) {
|
|
2174
|
+
if (item.status === "pending" || item.status === "running") {
|
|
2175
|
+
abortControllers.get(item.id)?.abort();
|
|
2176
|
+
item.status = "aborted";
|
|
2177
|
+
const abortedResponse = {
|
|
2178
|
+
error: new Error("Aborted"),
|
|
2179
|
+
aborted: true
|
|
2180
|
+
};
|
|
2181
|
+
itemPromises.get(item.id)?.resolve(abortedResponse);
|
|
2182
|
+
itemPromises.delete(item.id);
|
|
2183
|
+
const queryKey = stateManager.createQueryKey({
|
|
2184
|
+
path,
|
|
2185
|
+
method,
|
|
2186
|
+
options: { ...item.input, _queueId: item.id }
|
|
2187
|
+
});
|
|
2188
|
+
queryKeysToDiscard.push(queryKey);
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
if (queryKeysToDiscard.length > 0) {
|
|
2192
|
+
eventEmitter.emit("spoosh:queue-clear", {
|
|
2193
|
+
queryKeys: queryKeysToDiscard
|
|
2194
|
+
});
|
|
2195
|
+
}
|
|
2196
|
+
queue.length = 0;
|
|
2197
|
+
semaphore.reset();
|
|
2198
|
+
notify();
|
|
2199
|
+
},
|
|
2200
|
+
setConcurrency: (newConcurrency) => {
|
|
2201
|
+
if (newConcurrency < 1 || !Number.isInteger(newConcurrency)) {
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2204
|
+
semaphore.setConcurrency(newConcurrency);
|
|
2205
|
+
}
|
|
2206
|
+
};
|
|
2207
|
+
}
|
package/dist/index.mjs
CHANGED
|
@@ -698,6 +698,7 @@ function createStateManager() {
|
|
|
698
698
|
const cache = /* @__PURE__ */ new Map();
|
|
699
699
|
const subscribers = /* @__PURE__ */ new Map();
|
|
700
700
|
const pendingPromises = /* @__PURE__ */ new Map();
|
|
701
|
+
const dataChangeCallbacks = /* @__PURE__ */ new Set();
|
|
701
702
|
const notifySubscribers = (key) => {
|
|
702
703
|
const subs = subscribers.get(key);
|
|
703
704
|
subs?.forEach((cb) => cb());
|
|
@@ -717,6 +718,7 @@ function createStateManager() {
|
|
|
717
718
|
},
|
|
718
719
|
setCache(key, entry) {
|
|
719
720
|
const existing = cache.get(key);
|
|
721
|
+
const oldData = existing?.state.data;
|
|
720
722
|
if (existing) {
|
|
721
723
|
existing.state = { ...existing.state, ...entry.state };
|
|
722
724
|
if (entry.tags) {
|
|
@@ -729,6 +731,10 @@ function createStateManager() {
|
|
|
729
731
|
existing.stale = entry.stale;
|
|
730
732
|
}
|
|
731
733
|
notifySubscribers(key);
|
|
734
|
+
const newData = existing.state.data;
|
|
735
|
+
if (oldData !== newData) {
|
|
736
|
+
dataChangeCallbacks.forEach((cb) => cb(key, oldData, newData));
|
|
737
|
+
}
|
|
732
738
|
} else {
|
|
733
739
|
const newEntry = {
|
|
734
740
|
state: entry.state ?? createInitialState(),
|
|
@@ -740,6 +746,10 @@ function createStateManager() {
|
|
|
740
746
|
};
|
|
741
747
|
cache.set(key, newEntry);
|
|
742
748
|
notifySubscribers(key);
|
|
749
|
+
const newData = newEntry.state.data;
|
|
750
|
+
if (oldData !== newData) {
|
|
751
|
+
dataChangeCallbacks.forEach((cb) => cb(key, oldData, newData));
|
|
752
|
+
}
|
|
743
753
|
}
|
|
744
754
|
},
|
|
745
755
|
deleteCache(key) {
|
|
@@ -844,10 +854,17 @@ function createStateManager() {
|
|
|
844
854
|
getPendingPromise(key) {
|
|
845
855
|
return pendingPromises.get(key);
|
|
846
856
|
},
|
|
857
|
+
onDataChange(callback) {
|
|
858
|
+
dataChangeCallbacks.add(callback);
|
|
859
|
+
return () => {
|
|
860
|
+
dataChangeCallbacks.delete(callback);
|
|
861
|
+
};
|
|
862
|
+
},
|
|
847
863
|
clear() {
|
|
848
864
|
cache.clear();
|
|
849
865
|
subscribers.clear();
|
|
850
866
|
pendingPromises.clear();
|
|
867
|
+
dataChangeCallbacks.clear();
|
|
851
868
|
}
|
|
852
869
|
};
|
|
853
870
|
}
|
|
@@ -1762,8 +1779,366 @@ function createInfiniteReadController(options) {
|
|
|
1762
1779
|
};
|
|
1763
1780
|
return controller;
|
|
1764
1781
|
}
|
|
1782
|
+
|
|
1783
|
+
// src/queue/semaphore.ts
|
|
1784
|
+
var Semaphore = class {
|
|
1785
|
+
constructor(max) {
|
|
1786
|
+
this.max = max;
|
|
1787
|
+
}
|
|
1788
|
+
current = 0;
|
|
1789
|
+
waiting = [];
|
|
1790
|
+
async acquire() {
|
|
1791
|
+
if (this.current < this.max) {
|
|
1792
|
+
this.current++;
|
|
1793
|
+
return true;
|
|
1794
|
+
}
|
|
1795
|
+
return new Promise((resolve) => this.waiting.push(resolve));
|
|
1796
|
+
}
|
|
1797
|
+
release() {
|
|
1798
|
+
if (this.current > 0) {
|
|
1799
|
+
this.current--;
|
|
1800
|
+
}
|
|
1801
|
+
if (this.waiting.length > 0) {
|
|
1802
|
+
this.current++;
|
|
1803
|
+
this.waiting.shift()(true);
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
setConcurrency(max) {
|
|
1807
|
+
const previousMax = this.max;
|
|
1808
|
+
this.max = max;
|
|
1809
|
+
if (max > previousMax) {
|
|
1810
|
+
const slotsToRelease = Math.min(max - previousMax, this.waiting.length);
|
|
1811
|
+
for (let i = 0; i < slotsToRelease; i++) {
|
|
1812
|
+
this.current++;
|
|
1813
|
+
this.waiting.shift()(true);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
reset() {
|
|
1818
|
+
this.current = 0;
|
|
1819
|
+
while (this.waiting.length > 0) {
|
|
1820
|
+
const resolve = this.waiting.shift();
|
|
1821
|
+
resolve(false);
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
getCurrent() {
|
|
1825
|
+
return this.current;
|
|
1826
|
+
}
|
|
1827
|
+
getWaitingCount() {
|
|
1828
|
+
return this.waiting.length;
|
|
1829
|
+
}
|
|
1830
|
+
};
|
|
1831
|
+
|
|
1832
|
+
// src/queue/controller.ts
|
|
1833
|
+
var DEFAULT_CONCURRENCY = 3;
|
|
1834
|
+
function createQueueController(config, context) {
|
|
1835
|
+
const { path, method, operationType, hookOptions = {} } = config;
|
|
1836
|
+
const concurrency = config.concurrency ?? DEFAULT_CONCURRENCY;
|
|
1837
|
+
const { api, stateManager, eventEmitter, pluginExecutor } = context;
|
|
1838
|
+
const semaphore = new Semaphore(concurrency);
|
|
1839
|
+
const queue = [];
|
|
1840
|
+
const abortControllers = /* @__PURE__ */ new Map();
|
|
1841
|
+
const subscribers = /* @__PURE__ */ new Set();
|
|
1842
|
+
const itemPromises = /* @__PURE__ */ new Map();
|
|
1843
|
+
let cachedQueueSnapshot = [];
|
|
1844
|
+
const notify = () => {
|
|
1845
|
+
cachedQueueSnapshot = [...queue];
|
|
1846
|
+
subscribers.forEach((cb) => cb());
|
|
1847
|
+
};
|
|
1848
|
+
const generateId = () => `q-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1849
|
+
const updateItem = (id, update) => {
|
|
1850
|
+
const item = queue.find((i) => i.id === id);
|
|
1851
|
+
if (item) {
|
|
1852
|
+
Object.assign(item, update);
|
|
1853
|
+
}
|
|
1854
|
+
};
|
|
1855
|
+
const executeItem = async (item) => {
|
|
1856
|
+
const acquired = await semaphore.acquire();
|
|
1857
|
+
if (!acquired || item.status === "aborted") {
|
|
1858
|
+
if (acquired) {
|
|
1859
|
+
semaphore.release();
|
|
1860
|
+
}
|
|
1861
|
+
const response = {
|
|
1862
|
+
error: new Error("Aborted"),
|
|
1863
|
+
aborted: true
|
|
1864
|
+
};
|
|
1865
|
+
itemPromises.get(item.id)?.resolve(response);
|
|
1866
|
+
itemPromises.delete(item.id);
|
|
1867
|
+
return response;
|
|
1868
|
+
}
|
|
1869
|
+
const abortController = new AbortController();
|
|
1870
|
+
abortControllers.set(item.id, abortController);
|
|
1871
|
+
const queryKey = stateManager.createQueryKey({
|
|
1872
|
+
path,
|
|
1873
|
+
method,
|
|
1874
|
+
options: { ...item.input, _queueId: item.id }
|
|
1875
|
+
});
|
|
1876
|
+
const unsubscribeMeta = stateManager.subscribeCache(queryKey, () => {
|
|
1877
|
+
const cacheEntry = stateManager.getCache(queryKey);
|
|
1878
|
+
const meta = cacheEntry?.meta ? Object.fromEntries(cacheEntry.meta) : void 0;
|
|
1879
|
+
if (meta) {
|
|
1880
|
+
updateItem(item.id, { meta });
|
|
1881
|
+
notify();
|
|
1882
|
+
}
|
|
1883
|
+
});
|
|
1884
|
+
try {
|
|
1885
|
+
updateItem(item.id, { status: "running" });
|
|
1886
|
+
notify();
|
|
1887
|
+
const { body, query, params, ...triggerOptions } = item.input ?? {};
|
|
1888
|
+
const pluginContext = pluginExecutor.createContext({
|
|
1889
|
+
operationType,
|
|
1890
|
+
path,
|
|
1891
|
+
method,
|
|
1892
|
+
queryKey,
|
|
1893
|
+
tags: [],
|
|
1894
|
+
requestTimestamp: Date.now(),
|
|
1895
|
+
request: {
|
|
1896
|
+
headers: {},
|
|
1897
|
+
body,
|
|
1898
|
+
query,
|
|
1899
|
+
params,
|
|
1900
|
+
signal: abortController.signal
|
|
1901
|
+
},
|
|
1902
|
+
temp: /* @__PURE__ */ new Map(),
|
|
1903
|
+
pluginOptions: { ...hookOptions, ...triggerOptions },
|
|
1904
|
+
stateManager,
|
|
1905
|
+
eventEmitter
|
|
1906
|
+
});
|
|
1907
|
+
const coreFetch = async () => {
|
|
1908
|
+
const pathMethods = api(
|
|
1909
|
+
path
|
|
1910
|
+
);
|
|
1911
|
+
const methodFn = pathMethods[method];
|
|
1912
|
+
const { transport, transportOptions } = pluginContext.request;
|
|
1913
|
+
return methodFn({
|
|
1914
|
+
body,
|
|
1915
|
+
query,
|
|
1916
|
+
params,
|
|
1917
|
+
signal: abortController.signal,
|
|
1918
|
+
transport,
|
|
1919
|
+
transportOptions
|
|
1920
|
+
});
|
|
1921
|
+
};
|
|
1922
|
+
const response = await pluginExecutor.executeMiddleware(
|
|
1923
|
+
operationType,
|
|
1924
|
+
pluginContext,
|
|
1925
|
+
coreFetch
|
|
1926
|
+
);
|
|
1927
|
+
const cacheEntry = stateManager.getCache(queryKey);
|
|
1928
|
+
const meta = cacheEntry?.meta ? Object.fromEntries(cacheEntry.meta) : void 0;
|
|
1929
|
+
if (response.error) {
|
|
1930
|
+
updateItem(item.id, { status: "error", error: response.error, meta });
|
|
1931
|
+
itemPromises.get(item.id)?.resolve(response);
|
|
1932
|
+
} else {
|
|
1933
|
+
updateItem(item.id, { status: "success", data: response.data, meta });
|
|
1934
|
+
itemPromises.get(item.id)?.resolve(response);
|
|
1935
|
+
}
|
|
1936
|
+
return response;
|
|
1937
|
+
} catch (err) {
|
|
1938
|
+
const isAborted = abortController.signal.aborted;
|
|
1939
|
+
if (isAborted) {
|
|
1940
|
+
updateItem(item.id, { status: "aborted" });
|
|
1941
|
+
} else {
|
|
1942
|
+
updateItem(item.id, { status: "error", error: err });
|
|
1943
|
+
}
|
|
1944
|
+
const errorResponse = {
|
|
1945
|
+
error: err,
|
|
1946
|
+
aborted: isAborted
|
|
1947
|
+
};
|
|
1948
|
+
itemPromises.get(item.id)?.resolve(errorResponse);
|
|
1949
|
+
return errorResponse;
|
|
1950
|
+
} finally {
|
|
1951
|
+
unsubscribeMeta();
|
|
1952
|
+
abortControllers.delete(item.id);
|
|
1953
|
+
itemPromises.delete(item.id);
|
|
1954
|
+
notify();
|
|
1955
|
+
semaphore.release();
|
|
1956
|
+
}
|
|
1957
|
+
};
|
|
1958
|
+
return {
|
|
1959
|
+
trigger(input) {
|
|
1960
|
+
const { id: customId, ...requestInput } = input;
|
|
1961
|
+
const id = customId ?? generateId();
|
|
1962
|
+
const item = {
|
|
1963
|
+
id,
|
|
1964
|
+
status: "pending",
|
|
1965
|
+
input: requestInput
|
|
1966
|
+
};
|
|
1967
|
+
queue.push(item);
|
|
1968
|
+
notify();
|
|
1969
|
+
const promise = new Promise(
|
|
1970
|
+
(resolve, reject) => {
|
|
1971
|
+
itemPromises.set(id, { resolve, reject });
|
|
1972
|
+
}
|
|
1973
|
+
);
|
|
1974
|
+
executeItem(item);
|
|
1975
|
+
return promise;
|
|
1976
|
+
},
|
|
1977
|
+
getQueue: () => cachedQueueSnapshot,
|
|
1978
|
+
getStats: () => {
|
|
1979
|
+
let pending = 0;
|
|
1980
|
+
let running = 0;
|
|
1981
|
+
let success = 0;
|
|
1982
|
+
let failed = 0;
|
|
1983
|
+
for (const item of queue) {
|
|
1984
|
+
if (item.status === "pending") pending++;
|
|
1985
|
+
else if (item.status === "running") running++;
|
|
1986
|
+
else if (item.status === "success") success++;
|
|
1987
|
+
else if (item.status === "error" || item.status === "aborted") failed++;
|
|
1988
|
+
}
|
|
1989
|
+
const settled = success + failed;
|
|
1990
|
+
const total = queue.length;
|
|
1991
|
+
return {
|
|
1992
|
+
pending,
|
|
1993
|
+
running,
|
|
1994
|
+
settled,
|
|
1995
|
+
success,
|
|
1996
|
+
failed,
|
|
1997
|
+
total,
|
|
1998
|
+
percentage: total > 0 ? Math.round(settled / total * 100) : 0
|
|
1999
|
+
};
|
|
2000
|
+
},
|
|
2001
|
+
subscribe: (callback) => {
|
|
2002
|
+
subscribers.add(callback);
|
|
2003
|
+
return () => subscribers.delete(callback);
|
|
2004
|
+
},
|
|
2005
|
+
abort: (id) => {
|
|
2006
|
+
const queryKeysToDiscard = [];
|
|
2007
|
+
const abortedResponse = {
|
|
2008
|
+
error: new Error("Aborted"),
|
|
2009
|
+
aborted: true
|
|
2010
|
+
};
|
|
2011
|
+
if (id) {
|
|
2012
|
+
const item = queue.find((i) => i.id === id);
|
|
2013
|
+
if (item && (item.status === "pending" || item.status === "running")) {
|
|
2014
|
+
const wasPending = item.status === "pending";
|
|
2015
|
+
abortControllers.get(id)?.abort();
|
|
2016
|
+
updateItem(id, { status: "aborted" });
|
|
2017
|
+
if (wasPending) {
|
|
2018
|
+
itemPromises.get(id)?.resolve(abortedResponse);
|
|
2019
|
+
itemPromises.delete(id);
|
|
2020
|
+
}
|
|
2021
|
+
const queryKey = stateManager.createQueryKey({
|
|
2022
|
+
path,
|
|
2023
|
+
method,
|
|
2024
|
+
options: { ...item.input, _queueId: item.id }
|
|
2025
|
+
});
|
|
2026
|
+
queryKeysToDiscard.push(queryKey);
|
|
2027
|
+
notify();
|
|
2028
|
+
}
|
|
2029
|
+
} else {
|
|
2030
|
+
for (const item of queue) {
|
|
2031
|
+
if (item.status === "pending" || item.status === "running") {
|
|
2032
|
+
abortControllers.get(item.id)?.abort();
|
|
2033
|
+
const wasPending = item.status === "pending";
|
|
2034
|
+
updateItem(item.id, { status: "aborted" });
|
|
2035
|
+
if (wasPending) {
|
|
2036
|
+
itemPromises.get(item.id)?.resolve(abortedResponse);
|
|
2037
|
+
itemPromises.delete(item.id);
|
|
2038
|
+
}
|
|
2039
|
+
const queryKey = stateManager.createQueryKey({
|
|
2040
|
+
path,
|
|
2041
|
+
method,
|
|
2042
|
+
options: { ...item.input, _queueId: item.id }
|
|
2043
|
+
});
|
|
2044
|
+
queryKeysToDiscard.push(queryKey);
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
notify();
|
|
2048
|
+
}
|
|
2049
|
+
if (queryKeysToDiscard.length > 0) {
|
|
2050
|
+
eventEmitter.emit("spoosh:queue-abort", {
|
|
2051
|
+
queryKeys: queryKeysToDiscard
|
|
2052
|
+
});
|
|
2053
|
+
}
|
|
2054
|
+
},
|
|
2055
|
+
retry: async (id) => {
|
|
2056
|
+
const items = id ? queue.filter(
|
|
2057
|
+
(i) => i.id === id && (i.status === "error" || i.status === "aborted")
|
|
2058
|
+
) : queue.filter((i) => i.status === "error" || i.status === "aborted");
|
|
2059
|
+
const promises = [];
|
|
2060
|
+
for (const item of items) {
|
|
2061
|
+
updateItem(item.id, { status: "pending", error: void 0 });
|
|
2062
|
+
const promise = new Promise(
|
|
2063
|
+
(resolve) => {
|
|
2064
|
+
itemPromises.set(item.id, { resolve, reject: () => {
|
|
2065
|
+
} });
|
|
2066
|
+
}
|
|
2067
|
+
);
|
|
2068
|
+
promises.push(promise);
|
|
2069
|
+
executeItem(item);
|
|
2070
|
+
}
|
|
2071
|
+
await Promise.all(promises);
|
|
2072
|
+
},
|
|
2073
|
+
remove: (id) => {
|
|
2074
|
+
const abortedResponse = {
|
|
2075
|
+
error: new Error("Removed"),
|
|
2076
|
+
aborted: true
|
|
2077
|
+
};
|
|
2078
|
+
const item = queue.find((i) => i.id === id);
|
|
2079
|
+
if (item) {
|
|
2080
|
+
if (item.status === "pending") {
|
|
2081
|
+
item.status = "aborted";
|
|
2082
|
+
itemPromises.get(id)?.resolve(abortedResponse);
|
|
2083
|
+
itemPromises.delete(id);
|
|
2084
|
+
} else if (item.status === "running") {
|
|
2085
|
+
abortControllers.get(id)?.abort();
|
|
2086
|
+
}
|
|
2087
|
+
const idx = queue.findIndex((i) => i.id === id);
|
|
2088
|
+
if (idx !== -1) {
|
|
2089
|
+
queue.splice(idx, 1);
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
notify();
|
|
2093
|
+
},
|
|
2094
|
+
removeSettled: () => {
|
|
2095
|
+
const active = queue.filter(
|
|
2096
|
+
(i) => i.status === "pending" || i.status === "running"
|
|
2097
|
+
);
|
|
2098
|
+
queue.length = 0;
|
|
2099
|
+
queue.push(...active);
|
|
2100
|
+
notify();
|
|
2101
|
+
},
|
|
2102
|
+
clear: () => {
|
|
2103
|
+
const queryKeysToDiscard = [];
|
|
2104
|
+
for (const item of queue) {
|
|
2105
|
+
if (item.status === "pending" || item.status === "running") {
|
|
2106
|
+
abortControllers.get(item.id)?.abort();
|
|
2107
|
+
item.status = "aborted";
|
|
2108
|
+
const abortedResponse = {
|
|
2109
|
+
error: new Error("Aborted"),
|
|
2110
|
+
aborted: true
|
|
2111
|
+
};
|
|
2112
|
+
itemPromises.get(item.id)?.resolve(abortedResponse);
|
|
2113
|
+
itemPromises.delete(item.id);
|
|
2114
|
+
const queryKey = stateManager.createQueryKey({
|
|
2115
|
+
path,
|
|
2116
|
+
method,
|
|
2117
|
+
options: { ...item.input, _queueId: item.id }
|
|
2118
|
+
});
|
|
2119
|
+
queryKeysToDiscard.push(queryKey);
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
if (queryKeysToDiscard.length > 0) {
|
|
2123
|
+
eventEmitter.emit("spoosh:queue-clear", {
|
|
2124
|
+
queryKeys: queryKeysToDiscard
|
|
2125
|
+
});
|
|
2126
|
+
}
|
|
2127
|
+
queue.length = 0;
|
|
2128
|
+
semaphore.reset();
|
|
2129
|
+
notify();
|
|
2130
|
+
},
|
|
2131
|
+
setConcurrency: (newConcurrency) => {
|
|
2132
|
+
if (newConcurrency < 1 || !Number.isInteger(newConcurrency)) {
|
|
2133
|
+
return;
|
|
2134
|
+
}
|
|
2135
|
+
semaphore.setConcurrency(newConcurrency);
|
|
2136
|
+
}
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
1765
2139
|
export {
|
|
1766
2140
|
HTTP_METHODS,
|
|
2141
|
+
Semaphore,
|
|
1767
2142
|
Spoosh,
|
|
1768
2143
|
__DEV__,
|
|
1769
2144
|
buildUrl,
|
|
@@ -1777,6 +2152,7 @@ export {
|
|
|
1777
2152
|
createPluginExecutor,
|
|
1778
2153
|
createPluginRegistry,
|
|
1779
2154
|
createProxyHandler,
|
|
2155
|
+
createQueueController,
|
|
1780
2156
|
createSelectorProxy,
|
|
1781
2157
|
createStateManager,
|
|
1782
2158
|
createTracer,
|