68 lines
2.3 KiB
TypeScript
68 lines
2.3 KiB
TypeScript
import { operationSignal, raceWithAbortSignal } from "../utils/abort.ts";
|
|
import type { AuthOperationOptions, Credential, CredentialInfo, CredentialStore } from "./types.ts";
|
|
|
|
/**
|
|
* Default in-memory credential store. Apps inject persistent stores.
|
|
* Keyed by `Provider.id`, one credential per provider; see `CredentialStore`.
|
|
* Writes are serialized per provider through a promise chain.
|
|
*/
|
|
export class InMemoryCredentialStore implements CredentialStore {
|
|
private credentials = new Map<string, Credential>();
|
|
private chains = new Map<string, Promise<unknown>>();
|
|
|
|
/** Serialize tasks per provider id without releasing the chain before active work settles. */
|
|
private enqueue<T>(providerId: string, task: () => Promise<T>, options?: AuthOperationOptions): Promise<T> {
|
|
const signal = operationSignal(options?.signal);
|
|
const previous = this.chains.get(providerId) ?? Promise.resolve();
|
|
const queued = (async () => {
|
|
await previous.catch(() => {});
|
|
signal.throwIfAborted();
|
|
return task();
|
|
})();
|
|
const tail = queued.catch(() => {});
|
|
this.chains.set(providerId, tail);
|
|
void tail.then(() => {
|
|
if (this.chains.get(providerId) === tail) this.chains.delete(providerId);
|
|
});
|
|
return raceWithAbortSignal(queued, signal);
|
|
}
|
|
|
|
async read(providerId: string, options?: AuthOperationOptions): Promise<Credential | undefined> {
|
|
options?.signal?.throwIfAborted();
|
|
return this.credentials.get(providerId);
|
|
}
|
|
|
|
async list(options?: AuthOperationOptions): Promise<readonly CredentialInfo[]> {
|
|
options?.signal?.throwIfAborted();
|
|
return [...this.credentials].map(([providerId, credential]) => ({ providerId, type: credential.type }));
|
|
}
|
|
|
|
modify(
|
|
providerId: string,
|
|
fn: (current: Credential | undefined) => Promise<Credential | undefined>,
|
|
options?: AuthOperationOptions,
|
|
): Promise<Credential | undefined> {
|
|
return this.enqueue(
|
|
providerId,
|
|
async () => {
|
|
const current = this.credentials.get(providerId);
|
|
const next = await fn(current);
|
|
options?.signal?.throwIfAborted();
|
|
if (next !== undefined) this.credentials.set(providerId, next);
|
|
return next ?? current;
|
|
},
|
|
options,
|
|
);
|
|
}
|
|
|
|
delete(providerId: string, options?: AuthOperationOptions): Promise<void> {
|
|
return this.enqueue(
|
|
providerId,
|
|
async () => {
|
|
this.credentials.delete(providerId);
|
|
},
|
|
options,
|
|
);
|
|
}
|
|
}
|