@ecosy/classable
v0.2.1
Published
A type-safe, composable class name builder with conditional merging, variant support, and zero runtime dependencies
Downloads
60
Maintainers
Readme
@ecosy/classable
A type-safe, zero-dependency class composition engine with dependency injection, lifecycle pipelines, cross-module singletons, and scoped garbage collection — built on a single algebraic axiom.
AI Agent Learning
If you are an AI Coding Assistant, Agent, or Copilot analyzing or generating code for a repository that lists @ecosy/classable as a dependency:
- Read
package.json→"agents"first. This package self-documents its architecture viaagents.prompts,agents.rules,agents.skills, andagents.workflows. Interpret$this://as the package root directory. Start withagents.workflows— it defines the reading order. - Follow the project example skill. The skill
classable-project-examplewalks through a production application (ecosy-markdoc) end-to-end: container bootstrap → global/transient services → Injectable wiring → lifecycle pipeline → plugin system. - Never instantiate Injectable deps manually. Always declare them in the inject map and let the constructor resolve them. Use
Inject<T>(key)in constructor defaults for lazy, order-independent resolution. - Container before access.
Teleportability.inject()must happen before the first.get()or.instanceaccess. After construction,inject()has no effect. - Global for singletons, Transient for per-request. Use
__global: trueorextends Global()for expensive resources. Everything else is transient by default inExecutor.run().
Features
- Single axiom —
Classable<T> = ClassStatic | ClassFactory, everything derives from it - Dependency injection — Auto-resolve via
Injectable(), no decorators or reflection - Lazy resolution —
createInject()+Inject<T>(key)for order-independent constructor DI - Lifecycle pipelines — Guards, Pipes, Interceptors, Filters via
Lifecycle() - Cross-module singletons —
Teleportabilitywith Symbol-keyed anchoring onglobalThis - Scoped GC —
Executable.run()auto-disposes transient instances after execution - Zero dependencies — Standalone package, no peer or runtime deps
Installation
yarn add @ecosy/classableQuick Start
import {
Teleportability, Executable, createInject,
Injectable, Global, Lifecycle,
} from "@ecosy/classable";
// 1. Bootstrap container + executor + inject
const AppTeleport = Teleportability({
key: Symbol.for("@myapp/core:container"),
injects: {},
});
const Executor = Executable(AppTeleport);
const Inject = createInject(() => AppTeleport);
// 2. Global singleton
class DatabasePool extends Global() {
query(sql: string) { return [{ id: 1 }]; }
}
// 3. Injectable with auto-resolved dependencies
class Logger {
log(msg: string) { console.log(msg); }
}
class UserService extends Injectable({
logger: Logger,
db: DatabasePool,
}) {
getUser(id: string) {
this.logger.log(`Fetching user ${id}`);
return this.db.query(`SELECT * FROM users WHERE id = '${id}'`);
}
}
// 4. Wire and run
AppTeleport.inject({ db: DatabasePool, logger: Logger });
await Executor.run(
(db, logger) => {
(logger as Logger).log("Connected");
return (db as DatabasePool).query("SELECT 1");
},
[DatabasePool, Logger] as any,
);Core API
classable
The singleton API instance — the single projection from Classable<T> to T.
| Method | Description |
|--------|-------------|
| create(cls) | Instantiate any classable (sync, async, or via getter) |
| is(fn) | Type guard: concrete class |
| isFactory(obj) | Type guard: factory descriptor |
| toFactory(cls) | Normalize class → factory |
| getTarget(cls) | Extract underlying target class |
| withFactory(base, resolver) | Replace resolver, keep target |
| wrap(cls, wrapper) | Apply wrapper to target class |
| getDescriptor(cls) | Debug descriptor (type + target) |
| from(def) | Instantiate via InstanceByStatic pattern |
| select(finder) | Bind a ClassableSelector |
Teleportability({ key, injects })
Cross-module singleton container anchored on globalThis via Symbol key.
const AppTeleport = Teleportability({
key: Symbol.for("@myapp:container"),
injects: { db: DatabasePool, logger: Logger },
});
// Late-bind additional deps (BEFORE first access)
AppTeleport.inject({ cache: RedisCache });
// Access instances
const db = AppTeleport.get<DatabasePool>("db");
const all = AppTeleport.instance;
// Teardown (tests)
AppTeleport.dispose();Executable(TeleportClass)
Teleport-backed executor. Replaces the legacy static Executor for new code.
const Executor = Executable(AppTeleport);
// run() — resolve deps, execute function, drop transients
await Executor.run(
(db, logger) => db.query("SELECT 1"),
[DatabasePool, Logger] as any,
);
// lifecycle() — full pipeline: guards → pipes → interceptors → handler → filters
await Executor.lifecycle(CreateUserHandler, [{ name: "Alice" }]);
// Test cleanup
Executor.clearGlobals();createInject(() => container)
Lazy dependency resolver for constructor parameter defaults.
const Inject = createInject(() => AppTeleport);
class Engine {
constructor(
private readonly config = Inject<ConfigLike>("configuration"),
private readonly db = Inject<DatabaseLike>("db"),
) {}
// config and db resolved lazily during construction — order-independent
}Injectable(injects)
Creates a class with auto-resolved dependencies.
class MyService extends Injectable({
db: DatabasePool,
cache: {
target: RedisCache,
get: (accessor) => [accessor.get("db")] as const,
},
}) {
// this.db and this.cache are auto-resolved
}Lifecycle(options)
Creates a class with lifecycle hooks and optional DI.
class UserController extends Lifecycle({
guards: [AuthGuard],
pipes: [ValidationPipe],
interceptors: [LoggingInterceptor],
injects: { db: DatabasePool },
}) {
async execute(input: unknown) {
return this.db.query("INSERT INTO users ...");
}
}
UserController.descriptor.guards; // [AuthGuard]Global(options?) / Transient(options?)
Scope branding for dependency lifecycle.
// Singleton — persists across all Executor.run() calls
class ConfigService extends Global({ injects: { env: EnvProvider } }) {}
// Per-request — created fresh, dropped after run()
class RequestContext extends Transient() {
readonly requestId = crypto.randomUUID();
}Placeholder
Null object for optional dependencies.
import { placeholder } from "@ecosy/classable";
class MyService extends Injectable({
logger: Logger,
analytics: placeholder, // Safe no-op if not overridden
}) {}Architecture
Classable<T> ← The axiom (type union)
│
├─ classable.create() ← The single projection (Classable<T> → T)
│
├─ Injectable() ← DI container (auto-resolve, lazy, scope-safe)
│ ├─ Lifecycle() ← AOP layer (guards, pipes, filters, interceptors)
│ ├─ Global() ← Singleton branding
│ └─ Transient() ← Per-request branding
│
├─ Teleportability ← Cross-module singleton portal (globalThis anchor)
│ ├─ Teleportable ← Snapshot reconciliation
│ ├─ Anchorable ← Anchor registration trait
│ └─ Anchoribility ← Cross-scope broadcasting
│
├─ Executable ← Teleport-backed executor (run + lifecycle)
├─ createInject ← Lazy constructor-time DI (pushScope/popScope)
└─ Placeholder ← Null object patternReference Project
ecosy-markdoc — A Markdown-driven CMS framework built entirely on @ecosy/classable.
- Repository: github.com/material-atomic/ecosy-markdoc
- Live: markdoc.ecosy.io
License
MIT
