polen
Version:
A framework for delightful GraphQL developer portals
474 lines • 15.5 kB
JavaScript
//
//
//
//
//
// Holding Module for Missing @wollybeard/kit Functionality
//
// ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
//
// Code here is meant to be migrated eventually to @wollybeard/kit.
//
//
//
import { Arr, Err, Fs, Http, Path, Undefined } from '@wollybeard/kit';
export const arrayEquals = (a, b) => {
if (a.length !== b.length)
return false;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i])
return false;
}
return true;
};
export const ensureOptionalAbsoluteWithCwd = (pathExp) => {
if (Undefined.is(pathExp))
return process.cwd();
return Path.ensureAbsolute(pathExp, process.cwd());
};
export const ensureOptionalAbsolute = (pathExp, basePathExp) => {
assertPathAbsolute(basePathExp);
if (Undefined.is(pathExp))
return basePathExp;
return Path.ensureAbsolute(pathExp, basePathExp);
};
export const assertPathAbsolute = (pathExpression) => {
if (Path.isAbsolute(pathExpression))
return;
throw new Error(`Path must be absolute: ${pathExpression}`);
};
export const assertOptionalPathAbsolute = (pathExpression, message) => {
if (Undefined.is(pathExpression))
return;
if (Path.isAbsolute(pathExpression))
return;
const message_ = message ?? `Path must be absolute: ${pathExpression}`;
throw new Error(message_);
};
export const pickFirstPathExisting = async (paths) => {
const checks = await Promise.all(paths.map(path => Fs.exists(path).then(exists => exists ? path : undefined)));
return checks.find(maybePath => maybePath !== undefined);
};
export const isSpecifierFromPackage = (specifier, packageName) => {
return specifier === packageName || specifier.startsWith(packageName + `/`);
};
/**
* Filter object properties based on a policy mode and set of keys
*
* @param mode - 'allow' to keep only specified keys, 'deny' to remove specified keys
* @param obj - The object to filter
* @param keys - The keys to process
* @returns A filtered object with proper type inference
*
* @example
* ```ts
* const obj = { a: 1, b: 2, c: 3 }
*
* // Allow mode: keep only 'a' and 'c'
* objPolicyFilter('allow', obj, ['a', 'c']) // { a: 1, c: 3 }
*
* // Deny mode: remove 'a' and 'c'
* objPolicyFilter('deny', obj, ['a', 'c']) // { b: 2 }
* ```
*/
export const objPolicyFilter = (mode, obj, keys) => {
const result = mode === `deny` ? { ...obj } : {};
if (mode === `allow`) {
// For allow mode, only add specified keys
for (const key of keys) {
if (key in obj) {
// @ts-expect-error
result[key] = obj[key];
}
}
}
else {
// For deny mode, remove specified keys
for (const key of keys) {
delete result[key];
}
}
return result;
};
/**
* Filter an object using a predicate function
*
* @param obj - The object to filter
* @param predicate - Function that returns true to keep a key/value pair
* @returns A new object with only the key/value pairs where predicate returned true
*
* @example
* ```ts
* const obj = { a: 1, b: 2, c: 3 }
* objFilter(obj, (k, v) => v > 1) // { b: 2, c: 3 }
* objFilter(obj, k => k !== 'b') // { a: 1, c: 3 }
* ```
*/
export const objFilter = (obj, predicate) => {
const result = {};
// Use Object.keys to get all enumerable own properties
// This matches the behavior of for...in but only for own properties
for (const key of Object.keys(obj)) {
if (predicate(key, obj[key], obj)) {
result[key] = obj[key];
}
}
return result;
};
export const ObjPick = (obj, keys) => {
return objPolicyFilter(`allow`, obj, keys);
};
export const ObjOmit = (obj, keys) => {
return objPolicyFilter(`deny`, obj, keys);
};
export const ObjPartition = (obj, keys) => {
return keys.reduce((acc, key) => {
if (key in acc.omitted) {
// @ts-expect-error omitted already at type level
delete acc.omitted[key];
acc.picked[key] = obj[key];
}
return acc;
}, {
omitted: { ...obj },
picked: {},
});
};
export const ensureEnd = (string, ending) => {
if (string.endsWith(ending))
return string;
return string + ending;
};
export const ResponseInternalServerError = () => new Response(null, {
status: Http.Status.InternalServerError.code,
statusText: Http.Status.InternalServerError.description,
});
/**
* Execute an operation on multiple items, continuing even if some fail
*/
export async function tryCatchMany(items, operation) {
const partitionedResults = await Promise.all(items.map(async (item) => {
const result = await Err.tryCatch(() => operation(item));
if (Err.is(result)) {
const error = result;
error.context = { item };
return error;
}
return result;
})).then(Arr.partitionErrors);
return partitionedResults;
}
/**
* Split an array into chunks of specified size
*
* @param array - The array to chunk
* @param size - The size of each chunk
* @returns Array of chunks
*
* @example
* ```ts
* chunk([1, 2, 3, 4, 5], 2) // [[1, 2], [3, 4], [5]]
* chunk(['a', 'b', 'c'], 3) // [['a', 'b', 'c']]
* ```
*/
export const chunk = (array, size) => {
if (size <= 0)
throw new Error(`Chunk size must be greater than 0`);
if (array.length === 0)
return [];
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
};
/**
* Process items in parallel with configurable options
*
* @param items - Items to process
* @param operation - Async function to apply to each item (with optional index)
* @param options - Configuration options
* @returns Results and errors from processing
*
* @example
* ```ts
* const items = [1, 2, 3, 4, 5]
* const result = await asyncParallel(items, async (n, index) => n * 2, {
* concurrency: 2,
* batchSize: 3,
* failFast: false
* })
* // result.results: [2, 4, 6, 8, 10]
* // result.errors: []
* // result.success: true
* ```
*/
export const asyncParallel = async (items, operation, options = {}) => {
const { concurrency = 10, failFast = false, batchSize } = options;
if (items.length === 0) {
return { results: [], errors: [], success: true };
}
const allResults = [];
const allErrors = [];
// If batchSize is specified, process in batches
if (batchSize !== undefined) {
const batches = chunk(items, batchSize);
let globalIndex = 0;
for (const batch of batches) {
const batchResult = await processBatch(batch, operation, concurrency, failFast, globalIndex);
allResults.push(...batchResult.results);
allErrors.push(...batchResult.errors);
globalIndex += batch.length;
if (failFast && batchResult.errors.length > 0) {
break;
}
}
}
else {
// Process all items with specified concurrency
const result = await processBatch(items, operation, concurrency, failFast, 0);
allResults.push(...result.results);
allErrors.push(...result.errors);
}
return {
results: allResults,
errors: allErrors,
success: allErrors.length === 0,
};
};
/**
* Process a batch of items with limited concurrency
*/
const processBatch = async (items, operation, concurrency, failFast, startIndex = 0) => {
const results = [];
const errors = [];
// Process items in chunks based on concurrency limit
const chunks = chunk(items, concurrency);
let currentIndex = startIndex;
for (const chunkItems of chunks) {
const promises = chunkItems.map(async (item, chunkIndex) => {
const globalIndex = currentIndex + chunkIndex;
try {
const result = await operation(item, globalIndex);
return { success: true, result, item };
}
catch (error) {
const enhancedError = error instanceof Error ? error : new Error(String(error));
Object.assign(enhancedError, { item });
return { success: false, error: enhancedError, item };
}
});
currentIndex += chunkItems.length;
const chunkResults = await Promise.allSettled(promises);
for (const promiseResult of chunkResults) {
if (promiseResult.status === `fulfilled`) {
const { success, result, error, item } = promiseResult.value;
if (success) {
results.push(result);
}
else {
errors.push(error);
if (failFast) {
return { results, errors, success: false };
}
}
}
else {
// This shouldn't happen since we're catching errors above
// But handle it just in case
const error = new Error(`Unexpected promise rejection`);
errors.push(error);
if (failFast) {
return { results, errors, success: false };
}
}
}
}
return { results, errors, success: errors.length === 0 };
};
// /**
// * Reduce an array asynchronously, processing each item in sequence
// *
// * @param items - Array of items to process
// * @param reducer - Async function that takes accumulator and current item
// * @param initial - Initial value for the accumulator
// * @returns Final accumulated value
// *
// * @example
// * ```ts
// * const numbers = [1, 2, 3, 4]
// * const sum = await asyncReduce(numbers, async (acc, n) => acc + n, 0)
// * // sum: 10
// *
// * const transforms = [addHeader, addFooter, minify]
// * const html = await asyncReduce(transforms, async (html, transform) => transform(html), initialHtml)
// * ```
// */
// export const asyncReduce = async <T, R>(
// items: readonly T[],
// reducer: (accumulator: R, current: T, index: number) => Promise<R> | R,
// initial: R,
// ): Promise<R> => {
// let result = initial
// for (let i = 0; i < items.length; i++) {
// const item = items[i]!
// result = await reducer(result, item, i)
// }
// return result
// }
// /**
// * Curried version of asyncReduce for functions that transform a value
// *
// * @param transformers - Array of transformer functions
// * @returns A function that takes an initial value and applies all transformers
// *
// * @example
// * ```ts
// * const transformers = [addHeader, addFooter, minify]
// * const applyTransforms = asyncReduceWith(transformers)
// * const finalHtml = await applyTransforms(initialHtml)
// *
// * // For simple pipelines where each function transforms the same type
// * const htmlPipeline = asyncReduceWith([
// * (html) => html.replace('foo', 'bar'),
// * async (html) => await prettify(html),
// * (html) => html.trim()
// * ])
// * ```
// */
// export const asyncReduceWith = <T>(
// transformers: readonly ((value: T) => Promise<T> | T)[],
// ) => {
// return async (initial: T): Promise<T> => {
// return asyncReduce(transformers, (value, transform) => transform(value), initial)
// }
// }
/**
* Reduce an array asynchronously with context, processing each item in sequence
*
* @param items - Array of items to process
* @param reducer - Async function that takes accumulator, current item, and context
* @param initial - Initial value for the accumulator
* @param context - Context object passed to each reducer call
* @returns Final accumulated value
*
* @example
* ```ts
* const transformers = [transformer1, transformer2]
* const ctx = { request: req, response: res }
* const result = await asyncReduceWithContext(
* transformers,
* async (html, transformer) => transformer(html, ctx),
* initialHtml,
* ctx
* )
* ```
*/
export const asyncReduce = async (items, reducer, initial, context) => {
let result = initial;
for (let i = 0; i < items.length; i++) {
const item = items[i];
result = await reducer(result, item, context, i);
}
return result;
};
/**
* Curried version of asyncReduceWithContext for functions that transform a value with context
*
* @param transformers - Array of transformer functions that take value and context
* @returns A function that takes an initial value and context, and applies all transformers
*
* @example
* ```ts
* const transformers = [
* (html, ctx) => html.replace('{{url}}', ctx.req.url),
* async (html, ctx) => await ctx.minify(html),
* ]
* const applyTransforms = asyncReduceWithContextWith(transformers)
* const finalHtml = await applyTransforms(initialHtml, ctx)
* ```
*/
export const asyncReduceWith = (transformers, context) => {
return async (initial) => {
return asyncReduce(transformers, (value, transform, ctx) => transform(value, ctx), initial, context);
};
};
/**
* Helper function to create a branded value.
*
* This is a simple type assertion helper. For runtime validation,
* combine with validation functions or schemas.
*
* @template $BaseType - The underlying type to brand
* @template $BrandName - The brand name to apply
* @param value - The value to brand
* @returns The value with the brand type applied
*
* @example
* ```ts
* type UserId = Brand<string, 'UserId'>
*
* // Simple branding (no runtime validation)
* const id = brand<string, 'UserId'>('u123')
*
* // With validation (recommended)
* function createUserId(value: string): UserId {
* if (!value.startsWith('u')) {
* throw new Error('User IDs must start with "u"')
* }
* return brand<string, 'UserId'>(value)
* }
* ```
*/
export const brand = (value) => {
return value;
};
/**
* Shallow merge objects while omitting undefined values.
*
* This utility simplifies the common pattern of conditionally spreading objects
* to avoid including undefined values that would override existing values.
*
* @param objects - Objects to merge (later objects override earlier ones). Undefined objects are ignored.
* @returns Merged object with undefined values omitted
*
* @example
* ```ts
* // Instead of:
* const overrides = {
* ...(value1 ? { key1: value1 } : {}),
* ...(value2 ? { key2: value2 } : {}),
* }
*
* // Use:
* const overrides = mergeShallow({
* key1: value1,
* key2: value2,
* })
*
* // Example with config merging:
* const config = mergeShallow(
* defaultConfig,
* userConfig,
* { debug: args.debug, base: args.base }
* )
* // undefined values in the last object won't override earlier values
* ```
*/
export const spreadShallow = (...objects) => {
const result = {};
for (const obj of objects) {
if (obj === undefined)
continue;
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const value = obj[key];
if (value !== undefined) {
result[key] = value;
}
}
}
}
return result;
};
//# sourceMappingURL=kit-temp.js.map