@spoosh/core 0.13.3 → 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 CHANGED
@@ -429,7 +429,7 @@ interface EventTracer {
429
429
  emit(msg: string, options?: EventOptions): void;
430
430
  }
431
431
 
432
- type OperationType = "read" | "write" | "infiniteRead";
432
+ type OperationType = "read" | "write" | "infiniteRead" | "queue";
433
433
  type LifecyclePhase = "onMount" | "onUnmount" | "onUpdate";
434
434
  type OperationState<TData = unknown, TError = unknown> = {
435
435
  data: TData | undefined;
@@ -594,8 +594,11 @@ type PluginTypeConfig = {
594
594
  writeOptions?: object;
595
595
  infiniteReadOptions?: object;
596
596
  writeTriggerOptions?: object;
597
+ queueOptions?: object;
598
+ queueTriggerOptions?: object;
597
599
  readResult?: object;
598
600
  writeResult?: object;
601
+ queueResult?: object;
599
602
  instanceApi?: object;
600
603
  };
601
604
  /**
@@ -1024,6 +1027,15 @@ type ExtractInfiniteReadOptions<T> = T extends SpooshPlugin<infer Types> ? Types
1024
1027
  type ExtractWriteTriggerOptions<T> = T extends SpooshPlugin<infer Types> ? Types extends {
1025
1028
  writeTriggerOptions: infer W;
1026
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;
1027
1039
  type ExtractReadResult<T> = T extends SpooshPlugin<infer Types> ? Types extends {
1028
1040
  readResult: infer R;
1029
1041
  } ? R : object : object;
@@ -1039,10 +1051,13 @@ type MergePluginOptions<TPlugins extends readonly SpooshPlugin<PluginTypeConfig>
1039
1051
  write: UnionToIntersection<ExtractWriteOptions<TPlugins[number]>>;
1040
1052
  infiniteRead: UnionToIntersection<ExtractInfiniteReadOptions<TPlugins[number]>>;
1041
1053
  writeTrigger: UnionToIntersection<ExtractWriteTriggerOptions<TPlugins[number]>>;
1054
+ queue: UnionToIntersection<ExtractQueueOptions<TPlugins[number]>>;
1055
+ queueTrigger: UnionToIntersection<ExtractQueueTriggerOptions<TPlugins[number]>>;
1042
1056
  };
1043
1057
  type MergePluginResults<TPlugins extends readonly SpooshPlugin<PluginTypeConfig>[]> = {
1044
1058
  read: UnionToIntersection<ExtractReadResult<TPlugins[number]>>;
1045
1059
  write: UnionToIntersection<ExtractWriteResult<TPlugins[number]>>;
1060
+ queue: UnionToIntersection<ExtractQueueResult<TPlugins[number]>>;
1046
1061
  };
1047
1062
  type MergePluginInstanceApi<TPlugins extends readonly SpooshPlugin<PluginTypeConfig>[], TSchema = unknown> = ResolveInstanceApi<UnionToIntersection<ExtractInstanceApi<TPlugins[number]>>, TSchema, MergePluginOptions<TPlugins>["read"]>;
1048
1063
  type PluginRegistry<TPlugins extends SpooshPlugin<PluginTypeConfig>[]> = {
@@ -1430,6 +1445,22 @@ type WriteSelectorPathMethods<TSchema, TPath extends string, TDefaultError> = Fi
1430
1445
  * Used by useWrite for selecting endpoints. All input goes to trigger().
1431
1446
  */
1432
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>;
1433
1464
 
1434
1465
  type PluginArray = readonly SpooshPlugin<PluginTypeConfig>[];
1435
1466
  interface SpooshConfig<TPlugins extends PluginArray = PluginArray> {
@@ -2019,4 +2050,125 @@ type CreateInfiniteReadOptions<TData, TItem, TError, TRequest> = {
2019
2050
  };
2020
2051
  declare function createInfiniteReadController<TData, TItem, TError, TRequest extends InfiniteRequestOptions = InfiniteRequestOptions>(options: CreateInfiniteReadOptions<TData, TItem, TError, TRequest>): InfiniteReadController<TData, TItem, TError>;
2021
2052
 
2022
- 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 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, 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, 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 };
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
@@ -429,7 +429,7 @@ interface EventTracer {
429
429
  emit(msg: string, options?: EventOptions): void;
430
430
  }
431
431
 
432
- type OperationType = "read" | "write" | "infiniteRead";
432
+ type OperationType = "read" | "write" | "infiniteRead" | "queue";
433
433
  type LifecyclePhase = "onMount" | "onUnmount" | "onUpdate";
434
434
  type OperationState<TData = unknown, TError = unknown> = {
435
435
  data: TData | undefined;
@@ -594,8 +594,11 @@ type PluginTypeConfig = {
594
594
  writeOptions?: object;
595
595
  infiniteReadOptions?: object;
596
596
  writeTriggerOptions?: object;
597
+ queueOptions?: object;
598
+ queueTriggerOptions?: object;
597
599
  readResult?: object;
598
600
  writeResult?: object;
601
+ queueResult?: object;
599
602
  instanceApi?: object;
600
603
  };
601
604
  /**
@@ -1024,6 +1027,15 @@ type ExtractInfiniteReadOptions<T> = T extends SpooshPlugin<infer Types> ? Types
1024
1027
  type ExtractWriteTriggerOptions<T> = T extends SpooshPlugin<infer Types> ? Types extends {
1025
1028
  writeTriggerOptions: infer W;
1026
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;
1027
1039
  type ExtractReadResult<T> = T extends SpooshPlugin<infer Types> ? Types extends {
1028
1040
  readResult: infer R;
1029
1041
  } ? R : object : object;
@@ -1039,10 +1051,13 @@ type MergePluginOptions<TPlugins extends readonly SpooshPlugin<PluginTypeConfig>
1039
1051
  write: UnionToIntersection<ExtractWriteOptions<TPlugins[number]>>;
1040
1052
  infiniteRead: UnionToIntersection<ExtractInfiniteReadOptions<TPlugins[number]>>;
1041
1053
  writeTrigger: UnionToIntersection<ExtractWriteTriggerOptions<TPlugins[number]>>;
1054
+ queue: UnionToIntersection<ExtractQueueOptions<TPlugins[number]>>;
1055
+ queueTrigger: UnionToIntersection<ExtractQueueTriggerOptions<TPlugins[number]>>;
1042
1056
  };
1043
1057
  type MergePluginResults<TPlugins extends readonly SpooshPlugin<PluginTypeConfig>[]> = {
1044
1058
  read: UnionToIntersection<ExtractReadResult<TPlugins[number]>>;
1045
1059
  write: UnionToIntersection<ExtractWriteResult<TPlugins[number]>>;
1060
+ queue: UnionToIntersection<ExtractQueueResult<TPlugins[number]>>;
1046
1061
  };
1047
1062
  type MergePluginInstanceApi<TPlugins extends readonly SpooshPlugin<PluginTypeConfig>[], TSchema = unknown> = ResolveInstanceApi<UnionToIntersection<ExtractInstanceApi<TPlugins[number]>>, TSchema, MergePluginOptions<TPlugins>["read"]>;
1048
1063
  type PluginRegistry<TPlugins extends SpooshPlugin<PluginTypeConfig>[]> = {
@@ -1430,6 +1445,22 @@ type WriteSelectorPathMethods<TSchema, TPath extends string, TDefaultError> = Fi
1430
1445
  * Used by useWrite for selecting endpoints. All input goes to trigger().
1431
1446
  */
1432
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>;
1433
1464
 
1434
1465
  type PluginArray = readonly SpooshPlugin<PluginTypeConfig>[];
1435
1466
  interface SpooshConfig<TPlugins extends PluginArray = PluginArray> {
@@ -2019,4 +2050,125 @@ type CreateInfiniteReadOptions<TData, TItem, TError, TRequest> = {
2019
2050
  };
2020
2051
  declare function createInfiniteReadController<TData, TItem, TError, TRequest extends InfiniteRequestOptions = InfiniteRequestOptions>(options: CreateInfiniteReadOptions<TData, TItem, TError, TRequest>): InfiniteReadController<TData, TItem, TError>;
2021
2052
 
2022
- 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 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, 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, 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 };
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,
@@ -1829,3 +1831,360 @@ function createInfiniteReadController(options) {
1829
1831
  };
1830
1832
  return controller;
1831
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
@@ -1762,8 +1762,366 @@ function createInfiniteReadController(options) {
1762
1762
  };
1763
1763
  return controller;
1764
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
+ }
1765
2122
  export {
1766
2123
  HTTP_METHODS,
2124
+ Semaphore,
1767
2125
  Spoosh,
1768
2126
  __DEV__,
1769
2127
  buildUrl,
@@ -1777,6 +2135,7 @@ export {
1777
2135
  createPluginExecutor,
1778
2136
  createPluginRegistry,
1779
2137
  createProxyHandler,
2138
+ createQueueController,
1780
2139
  createSelectorProxy,
1781
2140
  createStateManager,
1782
2141
  createTracer,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spoosh/core",
3
- "version": "0.13.3",
3
+ "version": "0.14.0",
4
4
  "license": "MIT",
5
5
  "description": "Type-safe API toolkit with plugin middleware system",
6
6
  "keywords": [