work-in-progress atproto PDS
typescript atproto pds atcute

feat: cached identity resolver

mary.my.id d3d95a2b 81105dc5

verified
+642 -2
+14
packages/danaus/drizzle/identity/20260106131826_whole_micromax/migration.sql
··· 1 + CREATE TABLE `did_doc` ( 2 + `did` text PRIMARY KEY, 3 + `doc` text NOT NULL, 4 + `updated_at` integer NOT NULL 5 + ); 6 + --> statement-breakpoint 7 + CREATE TABLE `handle` ( 8 + `handle` text PRIMARY KEY, 9 + `did` text NOT NULL, 10 + `updated_at` integer NOT NULL 11 + ); 12 + --> statement-breakpoint 13 + CREATE INDEX `did_doc_updated_at_idx` ON `did_doc` (`updated_at`);--> statement-breakpoint 14 + CREATE INDEX `handle_updated_at_idx` ON `handle` (`updated_at`);
+125
packages/danaus/drizzle/identity/20260106131826_whole_micromax/snapshot.json
··· 1 + { 2 + "version": "7", 3 + "dialect": "sqlite", 4 + "id": "05a4b23c-3072-4afe-bb1d-7495fb7fcdad", 5 + "prevIds": [ 6 + "00000000-0000-0000-0000-000000000000" 7 + ], 8 + "ddl": [ 9 + { 10 + "name": "did_doc", 11 + "entityType": "tables" 12 + }, 13 + { 14 + "name": "handle", 15 + "entityType": "tables" 16 + }, 17 + { 18 + "type": "text", 19 + "notNull": false, 20 + "autoincrement": false, 21 + "default": null, 22 + "generated": null, 23 + "name": "did", 24 + "entityType": "columns", 25 + "table": "did_doc" 26 + }, 27 + { 28 + "type": "text", 29 + "notNull": true, 30 + "autoincrement": false, 31 + "default": null, 32 + "generated": null, 33 + "name": "doc", 34 + "entityType": "columns", 35 + "table": "did_doc" 36 + }, 37 + { 38 + "type": "integer", 39 + "notNull": true, 40 + "autoincrement": false, 41 + "default": null, 42 + "generated": null, 43 + "name": "updated_at", 44 + "entityType": "columns", 45 + "table": "did_doc" 46 + }, 47 + { 48 + "type": "text", 49 + "notNull": false, 50 + "autoincrement": false, 51 + "default": null, 52 + "generated": null, 53 + "name": "handle", 54 + "entityType": "columns", 55 + "table": "handle" 56 + }, 57 + { 58 + "type": "text", 59 + "notNull": true, 60 + "autoincrement": false, 61 + "default": null, 62 + "generated": null, 63 + "name": "did", 64 + "entityType": "columns", 65 + "table": "handle" 66 + }, 67 + { 68 + "type": "integer", 69 + "notNull": true, 70 + "autoincrement": false, 71 + "default": null, 72 + "generated": null, 73 + "name": "updated_at", 74 + "entityType": "columns", 75 + "table": "handle" 76 + }, 77 + { 78 + "columns": [ 79 + "did" 80 + ], 81 + "nameExplicit": false, 82 + "name": "did_doc_pk", 83 + "table": "did_doc", 84 + "entityType": "pks" 85 + }, 86 + { 87 + "columns": [ 88 + "handle" 89 + ], 90 + "nameExplicit": false, 91 + "name": "handle_pk", 92 + "table": "handle", 93 + "entityType": "pks" 94 + }, 95 + { 96 + "columns": [ 97 + { 98 + "value": "updated_at", 99 + "isExpression": false 100 + } 101 + ], 102 + "isUnique": false, 103 + "where": null, 104 + "origin": "manual", 105 + "name": "did_doc_updated_at_idx", 106 + "entityType": "indexes", 107 + "table": "did_doc" 108 + }, 109 + { 110 + "columns": [ 111 + { 112 + "value": "updated_at", 113 + "isExpression": false 114 + } 115 + ], 116 + "isUnique": false, 117 + "where": null, 118 + "origin": "manual", 119 + "name": "handle_updated_at_idx", 120 + "entityType": "indexes", 121 + "table": "handle" 122 + } 123 + ], 124 + "renames": [] 125 + }
+2
packages/danaus/package.json
··· 17 17 "css:watch": "tailwindcss -i src/web/styles/main.css -o src/web/styles/main.out.css -w", 18 18 "db:generate:account": "drizzle-kit generate --dialect=sqlite --schema=src/accounts/db/schema.ts --out=drizzle/accounts", 19 19 "db:generate:actor": "drizzle-kit generate --dialect=sqlite --schema=src/actors/db/schema.ts --out=drizzle/actors", 20 + "db:generate:identity": "drizzle-kit generate --dialect=sqlite --schema=src/identity/db/schema.ts --out=drizzle/identity", 20 21 "db:generate:sequencer": "drizzle-kit generate --dialect=sqlite --schema=src/sequencer/db/schema.ts --out=drizzle/sequencer" 21 22 }, 22 23 "dependencies": { ··· 47 48 "hono": "^4.11.3", 48 49 "jose": "^6.1.3", 49 50 "nanoid": "^5.1.6", 51 + "p-queue": "^9.1.0", 50 52 "valibot": "^1.2.0" 51 53 }, 52 54 "devDependencies": {
+58
packages/danaus/src/background.ts
··· 1 + import PQueue from 'p-queue'; 2 + 3 + export interface BackgroundQueueOptions { 4 + /** maximum concurrent tasks (default: 5) */ 5 + concurrency?: number; 6 + } 7 + 8 + /** 9 + * a simple queue for in-process, out-of-band background work. 10 + * tasks are fire-and-forget with error logging. 11 + */ 12 + export class BackgroundQueue implements Disposable { 13 + readonly #queue: PQueue; 14 + #destroyed = false; 15 + 16 + constructor(options: BackgroundQueueOptions = {}) { 17 + this.#queue = new PQueue({ concurrency: options.concurrency ?? 5 }); 18 + } 19 + 20 + /** 21 + * add a task to the background queue. 22 + * task errors are logged but not propagated. 23 + * @param task async function to execute 24 + */ 25 + add(task: () => Promise<void>): void { 26 + if (this.#destroyed) { 27 + return; 28 + } 29 + 30 + this.#queue.add(() => task()).catch((err) => { 31 + console.error('background queue task failed:', err); 32 + }); 33 + } 34 + 35 + /** 36 + * wait for all pending tasks to complete. 37 + */ 38 + async onIdle(): Promise<void> { 39 + await this.#queue.onIdle(); 40 + } 41 + 42 + /** 43 + * stop accepting new tasks and wait for pending tasks to complete. 44 + */ 45 + async destroy(): Promise<void> { 46 + this.#destroyed = true; 47 + await this.#queue.onIdle(); 48 + } 49 + 50 + dispose(): void { 51 + this.#destroyed = true; 52 + this.#queue.clear(); 53 + } 54 + 55 + [Symbol.dispose](): void { 56 + this.dispose(); 57 + } 58 + }
+30 -2
packages/danaus/src/context.ts
··· 15 15 import { S3BlobStore } from './actors/blob-store/s3'; 16 16 import { ActorManager } from './actors/manager'; 17 17 import { AuthVerifier } from './auth/verifier'; 18 + import { BackgroundQueue } from './background'; 18 19 import type { AppConfig } from './config'; 19 20 import { Crawlers } from './crawlers'; 21 + import { CachedDidDocumentResolver } from './identity/cached-did-document-resolver'; 22 + import { CachedHandleResolver } from './identity/cached-handle-resolver'; 23 + import { IdentityCache } from './identity/manager'; 20 24 import { Sequencer } from './sequencer/sequencer'; 21 25 22 26 export interface AppContext { 23 27 config: AppConfig; 28 + 29 + backgroundQueue: BackgroundQueue; 30 + identityCache: IdentityCache; 24 31 25 32 handleResolver: HandleResolver; 26 33 didDocumentResolver: DidDocumentResolver<'plc' | 'web'>; ··· 34 41 } 35 42 36 43 export const createAppContext = (config: AppConfig): AppContext => { 37 - const handleResolver = new CompositeHandleResolver({ 44 + const backgroundQueue = new BackgroundQueue(); 45 + 46 + const identityCache = new IdentityCache({ 47 + location: config.database.identityCacheDbLocation, 48 + walAutoCheckpointDisabled: config.database.walAutoCheckpointDisabled, 49 + backgroundQueue: backgroundQueue, 50 + }); 51 + 52 + const baseHandleResolver = new CompositeHandleResolver({ 38 53 strategy: 'race', 39 54 methods: { 40 55 http: new WellKnownHandleResolver(), ··· 42 57 }, 43 58 }); 44 59 45 - const didDocumentResolver = new CompositeDidDocumentResolver({ 60 + const handleResolver = new CachedHandleResolver({ 61 + cache: identityCache, 62 + resolver: baseHandleResolver, 63 + }); 64 + 65 + const baseDidDocumentResolver = new CompositeDidDocumentResolver({ 46 66 methods: { 47 67 plc: new PlcDidDocumentResolver({ apiUrl: config.identity.plcDirectoryUrl }), 48 68 web: new WebDidDocumentResolver(), 49 69 }, 70 + }); 71 + 72 + const didDocumentResolver = new CachedDidDocumentResolver({ 73 + cache: identityCache, 74 + resolver: baseDidDocumentResolver, 50 75 }); 51 76 52 77 const plcClient = new PlcClient({ ··· 93 118 94 119 return { 95 120 config: config, 121 + 122 + backgroundQueue: backgroundQueue, 123 + identityCache: identityCache, 96 124 97 125 handleResolver: handleResolver, 98 126 didDocumentResolver: didDocumentResolver,
+61
packages/danaus/src/identity/cached-did-document-resolver.ts
··· 1 + import type { DidDocument } from '@atcute/identity'; 2 + import type { DidDocumentResolver, ResolveDidDocumentOptions } from '@atcute/identity-resolver'; 3 + import type { Did } from '@atcute/lexicons/syntax'; 4 + 5 + import type { IdentityCache } from './manager.ts'; 6 + 7 + type AtprotoDidMethod = 'plc' | 'web'; 8 + 9 + export interface CachedDidDocumentResolverOptions { 10 + cache: IdentityCache; 11 + resolver: DidDocumentResolver<AtprotoDidMethod>; 12 + } 13 + 14 + /** 15 + * DID document resolver wrapper that adds caching with stale-while-revalidate. 16 + */ 17 + export class CachedDidDocumentResolver implements DidDocumentResolver<AtprotoDidMethod> { 18 + readonly #cache: IdentityCache; 19 + readonly #resolver: DidDocumentResolver<AtprotoDidMethod>; 20 + 21 + constructor(options: CachedDidDocumentResolverOptions) { 22 + this.#cache = options.cache; 23 + this.#resolver = options.resolver; 24 + } 25 + 26 + async resolve(did: Did<AtprotoDidMethod>, options?: ResolveDidDocumentOptions): Promise<DidDocument> { 27 + // bypass cache if requested 28 + if (options?.noCache) { 29 + const doc = await this.#resolver.resolve(did, options); 30 + this.#cache.setDidDoc(did, doc); 31 + return doc; 32 + } 33 + 34 + // check cache 35 + const cached = this.#cache.getDidDoc(did); 36 + 37 + if (cached && !cached.expired) { 38 + // trigger background refresh if stale 39 + if (cached.stale) { 40 + this.#cache.refreshDidDoc(did, () => this.resolveNoThrow(did)); 41 + } 42 + return cached.value; 43 + } 44 + 45 + // cache miss or expired - fetch fresh 46 + const doc = await this.#resolver.resolve(did, options); 47 + this.#cache.setDidDoc(did, doc); 48 + return doc; 49 + } 50 + 51 + private async resolveNoThrow( 52 + did: Did<AtprotoDidMethod>, 53 + options?: ResolveDidDocumentOptions, 54 + ): Promise<DidDocument | null> { 55 + try { 56 + return await this.#resolver.resolve(did, options); 57 + } catch { 58 + return null; 59 + } 60 + } 61 + }
+55
packages/danaus/src/identity/cached-handle-resolver.ts
··· 1 + import type { HandleResolver, ResolveHandleOptions } from '@atcute/identity-resolver'; 2 + import type { AtprotoDid, Handle } from '@atcute/lexicons/syntax'; 3 + 4 + import type { IdentityCache } from './manager.ts'; 5 + 6 + export interface CachedHandleResolverOptions { 7 + cache: IdentityCache; 8 + resolver: HandleResolver; 9 + } 10 + 11 + /** 12 + * handle resolver wrapper that adds caching with stale-while-revalidate. 13 + */ 14 + export class CachedHandleResolver implements HandleResolver { 15 + readonly #cache: IdentityCache; 16 + readonly #resolver: HandleResolver; 17 + 18 + constructor(options: CachedHandleResolverOptions) { 19 + this.#cache = options.cache; 20 + this.#resolver = options.resolver; 21 + } 22 + 23 + async resolve(handle: Handle, options?: ResolveHandleOptions): Promise<AtprotoDid> { 24 + // bypass cache if requested 25 + if (options?.noCache) { 26 + const did = await this.#resolver.resolve(handle, options); 27 + this.#cache.setHandle(handle, did); 28 + return did; 29 + } 30 + 31 + // check cache 32 + const cached = this.#cache.getHandle(handle); 33 + 34 + if (cached && !cached.expired) { 35 + // trigger background refresh if stale 36 + if (cached.stale) { 37 + this.#cache.refreshHandle(handle, () => this.resolveNoThrow(handle)); 38 + } 39 + return cached.value; 40 + } 41 + 42 + // cache miss or expired - fetch fresh 43 + const did = await this.#resolver.resolve(handle, options); 44 + this.#cache.setHandle(handle, did); 45 + return did; 46 + } 47 + 48 + private async resolveNoThrow(handle: Handle, options?: ResolveHandleOptions): Promise<AtprotoDid | null> { 49 + try { 50 + return await this.#resolver.resolve(handle, options); 51 + } catch { 52 + return null; 53 + } 54 + } 55 + }
+30
packages/danaus/src/identity/db/index.ts
··· 1 + import path from 'node:path'; 2 + import { Database } from 'bun:sqlite'; 3 + 4 + import { drizzle } from 'drizzle-orm/bun-sqlite'; 5 + import { migrate } from 'drizzle-orm/bun-sqlite/migrator'; 6 + 7 + import * as schema from './schema.ts'; 8 + 9 + const MIGRATIONS_DIR = path.resolve(import.meta.dir, '../../../drizzle/identity'); 10 + 11 + export const getIdentityCacheDb = (location: string, walAutoCheckpointDisabled: boolean) => { 12 + const sqliteDb = new Database(location); 13 + sqliteDb.run(`PRAGMA journal_mode = WAL;`); 14 + if (walAutoCheckpointDisabled) { 15 + sqliteDb.run(`PRAGMA wal_autocheckpoint = 0;`); 16 + } 17 + 18 + const db = drizzle({ 19 + client: sqliteDb, 20 + schema: schema, 21 + }); 22 + 23 + migrate(db, { migrationsFolder: MIGRATIONS_DIR }); 24 + 25 + return db; 26 + }; 27 + 28 + export type IdentityCacheDb = ReturnType<typeof getIdentityCacheDb>; 29 + 30 + export { schema as t };
+26
packages/danaus/src/identity/db/schema.ts
··· 1 + import type { DidDocument } from '@atcute/identity'; 2 + import type { AtprotoDid, Handle } from '@atcute/lexicons/syntax'; 3 + 4 + import { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; 5 + 6 + /** cached handle resolutions */ 7 + export const handle = sqliteTable( 8 + 'handle', 9 + { 10 + handle: text().$type<Handle>().primaryKey(), 11 + did: text().$type<AtprotoDid>().notNull(), 12 + updated_at: integer().notNull(), 13 + }, 14 + (t) => [index('handle_updated_at_idx').on(t.updated_at)], 15 + ); 16 + 17 + /** cached DID documents */ 18 + export const didDoc = sqliteTable( 19 + 'did_doc', 20 + { 21 + did: text().$type<AtprotoDid>().primaryKey(), 22 + doc: text({ mode: 'json' }).$type<DidDocument>().notNull(), 23 + updated_at: integer().notNull(), 24 + }, 25 + (t) => [index('did_doc_updated_at_idx').on(t.updated_at)], 26 + );
+212
packages/danaus/src/identity/manager.ts
··· 1 + import type { DidDocument } from '@atcute/identity'; 2 + import type { AtprotoDid, Handle } from '@atcute/lexicons/syntax'; 3 + 4 + import { eq, lt } from 'drizzle-orm'; 5 + 6 + import type { BackgroundQueue } from '#app/background.ts'; 7 + import { HOUR } from '#app/utils/times.ts'; 8 + 9 + import { getIdentityCacheDb, t, type IdentityCacheDb } from './db/index.ts'; 10 + 11 + const DEFAULT_STALE_TTL = HOUR; 12 + const DEFAULT_MAX_TTL = 24 * HOUR; 13 + const DEFAULT_PRUNE_INTERVAL = HOUR; 14 + 15 + export interface IdentityCacheOptions { 16 + location: string; 17 + walAutoCheckpointDisabled: boolean; 18 + backgroundQueue: BackgroundQueue; 19 + /** time before an entry is considered stale (default: 1 hour) */ 20 + staleTtl?: number; 21 + /** time before an entry expires completely (default: 24 hours) */ 22 + maxTtl?: number; 23 + /** interval between pruning runs (default: 1 hour) */ 24 + pruneInterval?: number; 25 + } 26 + 27 + export interface CacheResult<T> { 28 + value: T; 29 + updatedAt: number; 30 + stale: boolean; 31 + expired: boolean; 32 + } 33 + 34 + /** 35 + * SQLite-backed identity cache for handles and DID documents. 36 + * supports stale-while-revalidate pattern with background refresh. 37 + */ 38 + export class IdentityCache implements Disposable { 39 + readonly #db: IdentityCacheDb; 40 + readonly #backgroundQueue: BackgroundQueue; 41 + readonly #staleTtl: number; 42 + readonly #maxTtl: number; 43 + readonly #pruneInterval: Timer; 44 + 45 + constructor(options: IdentityCacheOptions) { 46 + this.#db = getIdentityCacheDb(options.location, options.walAutoCheckpointDisabled); 47 + this.#backgroundQueue = options.backgroundQueue; 48 + this.#staleTtl = options.staleTtl ?? DEFAULT_STALE_TTL; 49 + this.#maxTtl = options.maxTtl ?? DEFAULT_MAX_TTL; 50 + 51 + const pruneIntervalMs = options.pruneInterval ?? DEFAULT_PRUNE_INTERVAL; 52 + this.#pruneInterval = setInterval(() => { 53 + this.#backgroundQueue.add(() => this.pruneExpired()); 54 + }, pruneIntervalMs); 55 + } 56 + 57 + // #region handles 58 + 59 + /** 60 + * get a cached handle resolution. 61 + * @param handle handle to look up 62 + * @returns cache result or null if not found 63 + */ 64 + getHandle(handle: Handle): CacheResult<AtprotoDid> | null { 65 + const row = this.#db.select().from(t.handle).where(eq(t.handle.handle, handle)).get(); 66 + 67 + if (!row) { 68 + return null; 69 + } 70 + 71 + const now = Date.now(); 72 + return { 73 + value: row.did, 74 + updatedAt: row.updated_at, 75 + stale: now > row.updated_at + this.#staleTtl, 76 + expired: now > row.updated_at + this.#maxTtl, 77 + }; 78 + } 79 + 80 + /** 81 + * cache a handle resolution. 82 + * @param handle handle 83 + * @param did resolved DID 84 + */ 85 + setHandle(handle: Handle, did: AtprotoDid): void { 86 + this.#db 87 + .insert(t.handle) 88 + .values({ handle, did, updated_at: Date.now() }) 89 + .onConflictDoUpdate({ 90 + target: t.handle.handle, 91 + set: { did, updated_at: Date.now() }, 92 + }) 93 + .run(); 94 + } 95 + 96 + /** 97 + * remove a handle from cache. 98 + * @param handle handle to remove 99 + */ 100 + clearHandle(handle: Handle): void { 101 + this.#db.delete(t.handle).where(eq(t.handle.handle, handle)).run(); 102 + } 103 + 104 + /** 105 + * queue a background refresh for a handle. 106 + * @param handle handle to refresh 107 + * @param resolve function to resolve the handle 108 + */ 109 + refreshHandle(handle: Handle, resolve: () => Promise<AtprotoDid | null>): void { 110 + this.#backgroundQueue.add(async () => { 111 + const did = await resolve(); 112 + if (did) { 113 + this.setHandle(handle, did); 114 + } else { 115 + this.clearHandle(handle); 116 + } 117 + }); 118 + } 119 + 120 + // #endregion 121 + 122 + // #region DID documents 123 + 124 + /** 125 + * get a cached DID document. 126 + * @param did DID to look up 127 + * @returns cache result or null if not found 128 + */ 129 + getDidDoc(did: AtprotoDid): CacheResult<DidDocument> | null { 130 + const row = this.#db.select().from(t.didDoc).where(eq(t.didDoc.did, did)).get(); 131 + 132 + if (!row) { 133 + return null; 134 + } 135 + 136 + const now = Date.now(); 137 + return { 138 + value: row.doc, 139 + updatedAt: row.updated_at, 140 + stale: now > row.updated_at + this.#staleTtl, 141 + expired: now > row.updated_at + this.#maxTtl, 142 + }; 143 + } 144 + 145 + /** 146 + * cache a DID document. 147 + * @param did DID 148 + * @param doc DID document 149 + */ 150 + setDidDoc(did: AtprotoDid, doc: DidDocument): void { 151 + this.#db 152 + .insert(t.didDoc) 153 + .values({ did, doc, updated_at: Date.now() }) 154 + .onConflictDoUpdate({ 155 + target: t.didDoc.did, 156 + set: { doc, updated_at: Date.now() }, 157 + }) 158 + .run(); 159 + } 160 + 161 + /** 162 + * remove a DID document from cache. 163 + * @param did DID to remove 164 + */ 165 + clearDidDoc(did: AtprotoDid): void { 166 + this.#db.delete(t.didDoc).where(eq(t.didDoc.did, did)).run(); 167 + } 168 + 169 + /** 170 + * queue a background refresh for a DID document. 171 + * @param did DID to refresh 172 + * @param resolve function to resolve the DID document 173 + */ 174 + refreshDidDoc(did: AtprotoDid, resolve: () => Promise<DidDocument | null>): void { 175 + this.#backgroundQueue.add(async () => { 176 + const doc = await resolve(); 177 + if (doc) { 178 + this.setDidDoc(did, doc); 179 + } else { 180 + this.clearDidDoc(did); 181 + } 182 + }); 183 + } 184 + 185 + // #endregion 186 + 187 + /** 188 + * remove all expired entries from the cache. 189 + */ 190 + async pruneExpired(): Promise<void> { 191 + const cutoff = Date.now() - this.#maxTtl; 192 + this.#db.delete(t.handle).where(lt(t.handle.updated_at, cutoff)).run(); 193 + this.#db.delete(t.didDoc).where(lt(t.didDoc.updated_at, cutoff)).run(); 194 + } 195 + 196 + /** 197 + * clear all cached entries. 198 + */ 199 + clear(): void { 200 + this.#db.delete(t.handle).run(); 201 + this.#db.delete(t.didDoc).run(); 202 + } 203 + 204 + dispose(): void { 205 + clearInterval(this.#pruneInterval); 206 + this.#db.$client.close(); 207 + } 208 + 209 + [Symbol.dispose](): void { 210 + this.dispose(); 211 + } 212 + }
+6
packages/danaus/src/pds-server.ts
··· 44 44 await using disposables = new AsyncDisposableStack(); 45 45 46 46 const context = createAppContext(this.config); 47 + 48 + // register cleanup in reverse dependency order 49 + // NOTE: Bun/JSCore quirk - AsyncDisposableStack.use() requires AsyncDisposable, 50 + // doesn't accept Disposable like the spec allows, so we use defer() instead 51 + disposables.defer(() => context.backgroundQueue.dispose()); 52 + disposables.defer(() => context.identityCache.dispose()); 47 53 disposables.defer(() => context.accountManager.dispose()); 48 54 49 55 const { wrap, adapter } = createBunWebSocket();
+23
pnpm-lock.yaml
··· 115 115 nanoid: 116 116 specifier: ^5.1.6 117 117 version: 5.1.6 118 + p-queue: 119 + specifier: ^9.1.0 120 + version: 9.1.0 118 121 valibot: 119 122 specifier: ^1.2.0 120 123 version: 1.2.0(typescript@5.9.3) ··· 1966 1969 eventemitter3@4.0.7: 1967 1970 resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} 1968 1971 1972 + eventemitter3@5.0.1: 1973 + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} 1974 + 1969 1975 events@3.3.0: 1970 1976 resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} 1971 1977 engines: {node: '>=0.8.x'} ··· 2465 2471 resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} 2466 2472 engines: {node: '>=8'} 2467 2473 2474 + p-queue@9.1.0: 2475 + resolution: {integrity: sha512-O/ZPaXuQV29uSLbxWBGGZO1mCQXV2BLIwUr59JUU9SoH76mnYvtms7aafH/isNSNGwuEfP6W/4xD0/TJXxrizw==} 2476 + engines: {node: '>=20'} 2477 + 2468 2478 p-timeout@3.2.0: 2469 2479 resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} 2470 2480 engines: {node: '>=8'} 2471 2481 2482 + p-timeout@7.0.1: 2483 + resolution: {integrity: sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==} 2484 + engines: {node: '>=20'} 2485 + 2472 2486 p-wait-for@3.2.0: 2473 2487 resolution: {integrity: sha512-wpgERjNkLrBiFmkMEjuZJEWKKDrNfHCKA1OhyN1wg1FrLkULbviEy6py1AyJUgZ72YWFbZ38FIpnqvVqAlDUwA==} 2474 2488 engines: {node: '>=8'} ··· 4748 4762 4749 4763 eventemitter3@4.0.7: {} 4750 4764 4765 + eventemitter3@5.0.1: {} 4766 + 4751 4767 events@3.3.0: {} 4752 4768 4753 4769 express-async-errors@3.1.1(express@4.22.1): ··· 5251 5267 eventemitter3: 4.0.7 5252 5268 p-timeout: 3.2.0 5253 5269 5270 + p-queue@9.1.0: 5271 + dependencies: 5272 + eventemitter3: 5.0.1 5273 + p-timeout: 7.0.1 5274 + 5254 5275 p-timeout@3.2.0: 5255 5276 dependencies: 5256 5277 p-finally: 1.0.0 5278 + 5279 + p-timeout@7.0.1: {} 5257 5280 5258 5281 p-wait-for@3.2.0: 5259 5282 dependencies: