This commit is contained in:
Morten Olsen
2025-10-23 13:47:07 +02:00
commit b851dc3006
91 changed files with 7578 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
type CoalescingQueueOptions<T> = {
action: () => Promise<T>;
};
const createResolvable = <T>() => {
// eslint-disable-next-line
let resolve: (item: T) => void = () => { };
// eslint-disable-next-line
let reject: (item: T) => void = () => { };
const promise = new Promise<T>((res, rej) => {
resolve = res;
reject = rej;
});
return { resolve, reject, promise };
};
type Resolveable<T> = ReturnType<typeof createResolvable<T>>;
class CoalescingQueue<T> {
#options: CoalescingQueueOptions<T>;
#next?: Resolveable<T>;
#current?: Promise<T>;
constructor(options: CoalescingQueueOptions<T>) {
this.#options = options;
}
#start = () => {
if (this.#current) {
return;
}
const next = this.#next;
if (next) {
const action = this.#options.action();
this.#current = action;
this.#next = undefined;
action.then(next.resolve);
action.catch(next.reject);
action.finally(() => {
this.#current = undefined;
this.#start();
});
}
};
public run = async () => {
if (!this.#next) {
this.#next = createResolvable<T>();
}
const next = this.#next;
this.#start();
return next.promise;
};
}
export { CoalescingQueue };

View File

@@ -0,0 +1,4 @@
const API_GROUP = 'playground.homelab.olsen.cloud';
const API_VERSION = `${API_GROUP}/v1`;
export { API_VERSION, API_GROUP };

View File

@@ -0,0 +1,65 @@
type EventListener<T extends unknown[]> = (...args: T) => void | Promise<void>;
type OnOptions = {
abortSignal?: AbortSignal;
};
class EventEmitter<T extends Record<string, (...args: ExplicitAny[]) => void | Promise<void>>> {
#listeners = new Map<keyof T, Set<EventListener<ExplicitAny>>>();
on = <K extends keyof T>(event: K, callback: EventListener<Parameters<T[K]>>, options: OnOptions = {}) => {
const { abortSignal } = options;
if (!this.#listeners.has(event)) {
this.#listeners.set(event, new Set());
}
const callbackClone = (...args: Parameters<T[K]>) => callback(...args);
const abortController = new AbortController();
const listeners = this.#listeners.get(event);
if (!listeners) {
throw new Error('Event registration failed');
}
abortSignal?.addEventListener('abort', abortController.abort);
listeners.add(callbackClone);
abortController.signal.addEventListener('abort', () => {
this.#listeners.set(event, listeners?.difference(new Set([callbackClone])));
});
return abortController.abort;
};
once = <K extends keyof T>(event: K, callback: EventListener<Parameters<T[K]>>, options: OnOptions = {}) => {
const abortController = new AbortController();
options.abortSignal?.addEventListener('abort', abortController.abort);
return this.on(
event,
async (...args) => {
abortController.abort();
await callback(...args);
},
{
...options,
abortSignal: abortController.signal,
},
);
};
emit = <K extends keyof T>(event: K, ...args: Parameters<T[K]>) => {
const listeners = this.#listeners.get(event);
if (!listeners) {
return;
}
for (const listener of listeners) {
listener(...args);
}
};
emitAsync = async <K extends keyof T>(event: K, ...args: Parameters<T[K]>) => {
const listeners = this.#listeners.get(event);
if (!listeners) {
return;
}
await Promise.all(listeners.values().map((listener) => listener(...args)));
};
}
export { EventEmitter };

2
packages/utils/src/global.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
declare type ExplicitAny = any;

View File

@@ -0,0 +1,33 @@
function isDeepSubset<T>(actual: ExplicitAny, expected: T): expected is T {
if (typeof expected !== 'object' || expected === null) {
return actual === expected;
}
if (typeof actual !== 'object' || actual === null) {
return false;
}
if (Array.isArray(expected)) {
if (!Array.isArray(actual)) {
return false;
}
return expected.every((expectedItem) => actual.some((actualItem) => isDeepSubset(actualItem, expectedItem)));
}
// Iterate over the keys of the expected object
for (const key in expected) {
if (Object.prototype.hasOwnProperty.call(expected, key)) {
if (!Object.prototype.hasOwnProperty.call(actual, key)) {
return false;
}
if (!isDeepSubset(actual[key], expected[key])) {
return false;
}
}
}
return true;
};
export { isDeepSubset };

View File

@@ -0,0 +1,41 @@
import PQueue from 'p-queue';
import pRetry from 'p-retry';
type QueueCreateOptions = ConstructorParameters<typeof PQueue>[0];
type QueueAddOptions = Parameters<typeof pRetry>[1] & {
retries?: number;
};
type QueueOptions = QueueCreateOptions & {
retries?: number;
};
class Queue {
#options: QueueOptions;
#queue: PQueue;
constructor(options: QueueOptions = {}) {
this.#options = options;
this.#queue = new PQueue(options);
}
public get concurrency() {
return this.#queue.concurrency;
}
public set concurrency(value: number) {
this.#queue.concurrency = value;
}
public add = async <T>(task: () => Promise<T>, options: QueueAddOptions = {}) => {
const withRetry = () =>
pRetry(task, {
retries: options.retries || this.#options.retries || 1,
});
return this.#queue.add(withRetry);
};
}
export { Queue };

View File

@@ -0,0 +1,51 @@
const destroy = Symbol('destroy');
const instanceKey = Symbol('instances');
type ServiceDependency<T> = new (services: Services) => T & {
[destroy]?: () => Promise<void> | void;
};
class Services {
[instanceKey]: Map<ServiceDependency<unknown>, unknown>;
constructor() {
this[instanceKey] = new Map();
}
public get = <T>(service: ServiceDependency<T>) => {
if (!this[instanceKey].has(service)) {
this[instanceKey].set(service, new service(this));
}
const instance = this[instanceKey].get(service);
if (!instance) {
throw new Error('Could not generate instance');
}
return instance as T;
};
public set = <T>(service: ServiceDependency<T>, instance: Partial<T>) => {
this[instanceKey].set(service, instance);
};
public clone = () => {
const services = new Services();
services[instanceKey] = Object.fromEntries(this[instanceKey].entries());
};
public destroy = async () => {
await Promise.all(
this[instanceKey].values().map(async (instance) => {
if (
typeof instance === 'object' &&
instance &&
destroy in instance &&
typeof instance[destroy] === 'function'
) {
await instance[destroy]();
}
}),
);
};
}
export { Services, destroy };