Fixed a silently-broken DI layer: the hand-rolled IoC container in src/core/ioc.ts keyed every service on t.prototype.name (always undefined), so all registrations collided and resolution returned the wrong instance or undefined.
Symptoms
- The entire DI layer was non-functional, but nothing had run yet so it went unnoticed at write time.
ioc.get(Database)returned the wrong instance orundefined.- Every service registered via
ioc.register(X)collided under a single key.
Root Cause
Both register() and get() in src/core/ioc.ts computed the map key as t.prototype.name.
For any class, SomeClass.prototype.name is undefined — only SomeClass.name returns the class name. Consequences:
- Class registrations collided: every
ioc.register(X)stored under the single keyundefined, so the last registration won and overwrote all prior ones. - Exact registrations never matched:
registerExact("Database", client)stored under the string key"Database", butioc.get(Database)looked up keyundefined— never finding it.
Fix
Changed both register() and get() to key on t.name instead of t.prototype.name.
This also makes registerExact("Database", ...) resolve correctly, because the exported Database is a named class expression:
export const Database = class Database {} as any;so Database.name === "Database" — the class key now matches the string key used by registerExact.
Verification
pnpm exec tsc --noEmit— clean.UsersService,DiscordService, andPermissionServicenow resolve by class name.
Gotcha: services are keyed by
Class.nameResolution depends on the class name being preserved at runtime. Production minification that mangles class names will break DI resolution — every service would resolve to a mangled key and
get()would miss. Keep class names stable (disable name mangling for these classes), or switch to an explicit string-key registry if you intend to minify the backend.
For Agents
When registering or resolving in this IoC: the key is
Class.name. Use a named class expression (const X = class X {}) — an anonymous class expression (const X = class {}) would produce an empty.nameand silently reintroduce the collision. PreferregisterExact("name", instance)only when the string matches the class name.
Related Context
This fix unblocked just-completed user-provisioning work:
UsersService.provision(discordId, permissions)fetches the Discord profile, upserts aUser(idempotent on the uniqueusername), and self-grants permissions viaPermissionService.grantMany(user.id, permissions, user.id).GET /userslist endpoint is wired (paginated, 50/page).
Open design questions
- Permissions are self-granted — no provisioner is tracked (grantor == grantee).
- The upsert keys on
username, notdiscordId(which is not@unique). Two Discord IDs sharing a username, or a username change, would collide/misbehave.
Related
- Decision - Discord Integration via discord.js —
DiscordServiceis one of the services resolved through this IoC; samebinder.ts/bindServices(ioc)pattern. - txArchive 2025 Rewrite