alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
165 lines (142 loc) • 5.07 kB
text/typescript
import {
$hook,
$inject,
createPrimitive,
KIND,
Primitive,
type Static,
type TSchema,
} from "alepha";
import type { DurationLike } from "alepha/datetime";
import type { RetryPrimitiveOptions } from "alepha/retry";
import {
type BatchContext,
type BatchItemState,
type BatchItemStatus,
BatchProvider,
} from "../providers/BatchProvider.ts";
/**
* Creates a batch processing primitive for efficient grouping and processing of multiple operations.
*/
export const $batch = <TItem extends TSchema, TResponse>(
options: BatchPrimitiveOptions<TItem, TResponse>,
): BatchPrimitive<TItem, TResponse> =>
createPrimitive(BatchPrimitive<TItem, TResponse>, options);
// ---------------------------------------------------------------------------------------------------------------------
export interface BatchPrimitiveOptions<TItem extends TSchema, TResponse = any> {
/**
* TypeBox schema for validating each item added to the batch.
*/
schema: TItem;
/**
* The batch processing handler function that processes arrays of validated items.
*/
handler: (items: Static<TItem>[]) => TResponse;
/**
* Maximum number of items to collect before automatically flushing the batch.
*/
maxSize?: number;
/**
* Maximum number of items that can be queued in a single partition.
* If exceeded, push() will throw an error.
*/
maxQueueSize?: number;
/**
* Maximum time to wait before flushing a batch, even if it hasn't reached maxSize.
*/
maxDuration?: DurationLike;
/**
* Function to determine partition keys for grouping items into separate batches.
*/
partitionBy?: (item: Static<TItem>) => string;
/**
* Maximum number of batch handlers that can execute simultaneously.
*/
concurrency?: number;
/**
* Retry configuration for failed batch processing operations.
*/
retry?: Omit<RetryPrimitiveOptions<() => Array<Static<TItem>>>, "handler">;
}
// ---------------------------------------------------------------------------------------------------------------------
export type { BatchItemState, BatchItemStatus };
// ---------------------------------------------------------------------------------------------------------------------
export class BatchPrimitive<
TItem extends TSchema,
TResponse = any,
> extends Primitive<BatchPrimitiveOptions<TItem, TResponse>> {
protected readonly batchProvider = $inject(BatchProvider);
protected readonly context: BatchContext<Static<TItem>, TResponse>;
constructor(
...args: ConstructorParameters<
typeof Primitive<BatchPrimitiveOptions<TItem, TResponse>>
>
) {
super(...args);
this.context = this.batchProvider.createContext(this.alepha, {
handler: this.options.handler,
maxSize: this.options.maxSize,
maxQueueSize: this.options.maxQueueSize,
maxDuration: this.options.maxDuration,
partitionBy: this.options.partitionBy,
concurrency: this.options.concurrency,
retry: this.options.retry,
});
}
/**
* Pushes an item into the batch and returns immediately with a unique ID.
* The item will be processed asynchronously with other items when the batch is flushed.
* Use wait(id) to get the processing result.
*/
public async push(item: Static<TItem>): Promise<string> {
// Validate the item against the schema
const validatedItem = this.alepha.codec.validate(this.options.schema, item);
return this.batchProvider.push(this.context, validatedItem);
}
/**
* Wait for a specific item to be processed and get its result.
* @param id The item ID returned from push()
* @returns The processing result
* @throws If the item doesn't exist or processing failed
*/
public async wait(id: string): Promise<TResponse> {
return this.batchProvider.wait(this.context, id);
}
/**
* Get the current status of an item.
* @param id The item ID returned from push()
* @returns Status information or undefined if item doesn't exist
*/
public status(
id: string,
):
| { status: "pending" | "processing" }
| { status: "completed"; result: TResponse }
| { status: "failed"; error: Error }
| undefined {
return this.batchProvider.status(this.context, id);
}
/**
* Flush all partitions or a specific partition.
*/
public async flush(partitionKey?: string): Promise<void> {
return this.batchProvider.flush(this.context, partitionKey);
}
/**
* Clears completed and failed items from memory.
* Call this periodically in long-running applications to prevent memory leaks.
*
* @param status Optional: only clear items with this specific status ('completed' or 'failed')
* @returns The number of items cleared
*/
public clearCompleted(status?: "completed" | "failed"): number {
return this.batchProvider.clearCompleted(this.context, status);
}
protected readonly onReady = $hook({
on: "ready",
handler: async () => {
await this.batchProvider.markReady(this.context);
},
});
}
$batch[KIND] = BatchPrimitive;