tsdkarc 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,244 @@
1
+ # <a href="https://arc.tsdk.dev" align="center"><img src="./assets/logo.jpg" align="center" width="30px" height="30px" style="border-radius: 4px;margin-right:4px;" alt="TsdkArc: the Elegant, fully type-safe module composition library" /></a> TsdkArc
2
+
3
+ <a href="https://arc.tsdk.dev">
4
+ <img src="./assets/banner.jpg" width="100%" style="border-radius: 24px" alt="TsdkArc: the Elegant, fully type-safe module composition library" /></a>
5
+
6
+ <div align="center">The Elegant, Fully Type-safe Module Composition Library.
7
+ </div>
8
+
9
+ ---
10
+
11
+ ## Why `TsdkArc`
12
+
13
+ Application codebases grow, the code become coupled and messy — hard to reuse, hard to share.
14
+ `TsdkArc` lets you compose modules like building blocks, nest them, and share them type safely across projects.
15
+
16
+ In **tsdkrc**, Each module declares what it needs and what it provides. Then call `start([modules])` will resolves the full dependency graph, boots modules in order, and returns a typed context.
17
+
18
+ ## Quick Start
19
+
20
+ ```bash
21
+ npm install tsdkarc
22
+ ```
23
+
24
+ ```ts
25
+ import start, { defineModule } from "tsdkarc";
26
+
27
+ interface ConfigSlice {
28
+ config: { port: number; env: string };
29
+ }
30
+
31
+ const configModule = defineModule<ConfigSlice>()({
32
+ name: "config",
33
+ modules: [],
34
+ boot(ctx) {
35
+ ctx.set("config", {
36
+ env: process.env.NODE_ENV ?? "development",
37
+ port: Number(process.env.PORT) || 3000,
38
+ });
39
+ },
40
+ });
41
+
42
+ // Run
43
+ (async () => {
44
+ const app = await start([configModule]);
45
+ console.log(app.ctx.config.port); // 3000
46
+ await app.stop();
47
+ })();
48
+ ```
49
+
50
+ ---
51
+
52
+ ## Core Concepts
53
+
54
+ | Term | Description |
55
+ | ----------- | ----------------------------------------------------------------------------------------------- |
56
+ | **Slice** | The shape a module adds to the shared context (`{ key: Type }`) |
57
+ | **Module** | Declares dependencies (`modules`), registers values (`ctx.set`), and optionally tears them down |
58
+ | **Context** | The merged union of all slices — fully typed at each module's boundary |
59
+
60
+ ## API Outline
61
+
62
+ ```ts
63
+ defineModule<OwnSlice>()({
64
+ name: string,
65
+ modules: Module[],
66
+ boot?(ctx): void | Promise<void>,
67
+ beforeBoot?(ctx): void | Promise<void>,
68
+ afterBoot?(ctx): void | Promise<void>,
69
+ shutdown?(ctx): void | Promise<void>,
70
+ beforeShutdown?(ctx): void | Promise<void>,
71
+ afterShutdown?(ctx): void | Promise<void>,
72
+ })
73
+
74
+ start(modules: Module[], hooks?: {
75
+ beforeBoot?(ctx): void | Promise<void>,
76
+ afterBoot?(ctx): void | Promise<void>,
77
+ beforeShutdown?(ctx): void | Promise<void>,
78
+ afterShutdown?(ctx): void | Promise<void>,
79
+ }): Promise<{ ctx, stop() }>
80
+ ```
81
+
82
+ ---
83
+
84
+ ## Dependency Chain
85
+
86
+ Downstream modules declare upstream modules and get their context fully typed.
87
+
88
+ ```ts
89
+ interface DbSlice {
90
+ db: Pool;
91
+ }
92
+
93
+ const dbModule = defineModule<DbSlice>()({
94
+ name: "db",
95
+ modules: [configModule], // ctx.config is typed here
96
+ async boot(ctx) {
97
+ const pool = new Pool({ connectionString: ctx.config.databaseUrl });
98
+ await pool.connect();
99
+ ctx.set("db", pool);
100
+ },
101
+ async shutdown(ctx) {
102
+ await ctx.db.end();
103
+ },
104
+ });
105
+
106
+ interface ServerSlice {
107
+ server: http.Server;
108
+ }
109
+
110
+ const serverModule = defineModule<ServerSlice>()({
111
+ name: "server",
112
+ modules: [configModule, dbModule], // ctx.config + ctx.db both typed
113
+ boot(ctx) {
114
+ ctx.set("server", http.createServer(myHandler));
115
+ },
116
+ afterBoot(ctx) {
117
+ ctx.server.listen(ctx.config.port);
118
+ },
119
+ shutdown(ctx) {
120
+ ctx.server.close();
121
+ },
122
+ });
123
+
124
+ const app = await start([serverModule]);
125
+ await app.stop();
126
+ ```
127
+
128
+ `start()` walks the dependency graph and deduplicates — each module boots exactly once regardless of how many times it appears in `modules` arrays.
129
+
130
+ ---
131
+
132
+ ## Global Hooks
133
+
134
+ Use the second argument to `start()` for process-level concerns that need access to the full context.
135
+
136
+ ```ts
137
+ const app = await start([serverModule], {
138
+ afterBoot(ctx) {
139
+ process.on("uncaughtException", (err) => console.error(err));
140
+ },
141
+ beforeShutdown(ctx) {
142
+ console.info("shutting down");
143
+ },
144
+ afterEachBoot(ctx) {
145
+ console.info("booting", mod.name);
146
+ },
147
+ beforeEachShutdown(ctx, mod) {
148
+ console.info("shutting down", mod.name);
149
+ },
150
+ });
151
+ ```
152
+
153
+ ---
154
+
155
+ ## Patterns
156
+
157
+ **Register anything, not just data.** Functions, class instances, and middleware are all valid context values.
158
+
159
+ ```ts
160
+ interface AuthSlice {
161
+ authenticate: (req: Request, res: Response, next: NextFunction) => void;
162
+ }
163
+
164
+ const authModule = defineModule<AuthSlice>()({
165
+ name: "auth",
166
+ modules: [],
167
+ boot(ctx) {
168
+ ctx.set("authenticate", (req, res, next) => {
169
+ if (!req.headers.authorization) return res.status(401).end();
170
+ next();
171
+ });
172
+ },
173
+ });
174
+ ```
175
+
176
+ **Compose small modules.** Each module is independently testable and replaceable. Complex wiring is just a chain of dependencies.
177
+
178
+ ```ts
179
+ // config → db → cache → queue → server
180
+ const app = await start([serverModule]);
181
+ ```
182
+
183
+ ---
184
+
185
+ ## Lifecycle
186
+
187
+ ```
188
+ beforeBoot → boot → afterBoot → [running] → beforeShutdown → shutdown → afterShutdown
189
+ ```
190
+
191
+ | Hook | Purpose |
192
+ | -------------------- | ------------------------------------------------------------------------------ |
193
+ | `beforeBoot` | Pre-setup, Called once before the first module begins booting. |
194
+ | `boot` | Register values on `ctx` via `ctx.set()` |
195
+ | `afterBoot` | Called once after the last module has finished booting. |
196
+ | `beforeShutdown` | Called once before the first module begins shutting down. |
197
+ | `shutdown` | Release resources |
198
+ | `afterShutdown` | Called once after the last module has finished shutting down. |
199
+ | `beforeEachBoot` | Called before each individual module boots, in boot order. |
200
+ | `afterEachBoot` | Called after each individual module finishes booting, in boot order. |
201
+ | `beforeEachShutdown` | Called before each individual module shuts down, in shutdown order. |
202
+ | `afterEachShutdown` | Called after each individual module finishes shutting down, in shutdown order. |
203
+
204
+ ---
205
+
206
+ ## API
207
+
208
+ ### `defineModule<Slice>()(config)`
209
+
210
+ | Field | Type | Description |
211
+ | ---------------- | -------------------------- | ----------------------------- |
212
+ | `name` | `string` | Unique module identifier |
213
+ | `description` | `string` | Optional description |
214
+ | `modules` | `Module<Slice>[]` | Declared dependencies |
215
+ | `beforeBoot` | `(ctx) => void \| Promise` | Runs before boot |
216
+ | `boot` | `(ctx) => void \| Promise` | Register values on ctx |
217
+ | `afterBoot` | `(ctx) => void \| Promise` | Runs after all modules booted |
218
+ | `beforeShutdown` | `(ctx) => void \| Promise` | Runs before shutdown |
219
+ | `shutdown` | `(ctx) => void \| Promise` | Release resources |
220
+ | `afterShutdown` | `(ctx) => void \| Promise` | Runs after shutdown |
221
+
222
+ ### `start(roots, options?)` → `{ ctx, stop }`
223
+
224
+ | Field | Description |
225
+ | --------- | -------------------------------------- |
226
+ | `roots` | Top-level modules (deps auto-resolved) |
227
+ | `options` | Global lifecycle hooks |
228
+ | `ctx` | Merged, fully-typed context |
229
+ | `stop` | Triggers full shutdown sequence |
230
+
231
+ ## Projects You May Also Be Interested In
232
+
233
+ - [xior](https://github.com/suhaotian/xior) - A tiny but powerful fetch wrapper with plugins support and axios-like API
234
+ - [tsdk](https://github.com/tsdk-monorepo/tsdk) - Type-safe API development CLI tool for TypeScript projects
235
+ - [broad-infinite-list](https://github.com/suhaotian/broad-infinite-list) - ⚡ High performance and Bidirectional infinite scrolling list component for React and Vue3
236
+ - [littkk](https://github.com/suhaotian/littkk) - 🧞‍♂️ Shows and hides UI elements on scroll.
237
+
238
+ ## Reporting Issues
239
+
240
+ Found an issue? Please feel free to [create issue](https://github.com/tsdk-monorepo/tsdkarc/issues/new)
241
+
242
+ ## Support
243
+
244
+ If you find this project helpful, consider [buying me a coffee](https://github.com/tsdk-monorepo/tsdkarc/stargazers).
Binary file
Binary file
package/esm/index.d.ts ADDED
@@ -0,0 +1,175 @@
1
+ /**
2
+ * tsdkarc.ts
3
+ *
4
+ * Minimal module lifecycle manager.
5
+ *
6
+ * Exports: Module, defineModule, ContextWriter, Logger, LifecycleHooks, StartOptions, start.
7
+ *
8
+ * Type helpers (internal):
9
+ * UnionToIntersection<U> — U1 | U2 | U3 → U1 & U2 & U3
10
+ * SliceOf<M> — extracts S (full context) from Module<S, Sl>
11
+ * MergeSlices<Tuple> — intersects all S from a modules tuple
12
+ * FullContext<Deps, Own> — MergeSlices<Deps> & Own
13
+ *
14
+ * Internal functions use AnyModule (Module<object, object>) at boundaries
15
+ * where generic params are intentionally erased — the runtime does not need them.
16
+ */
17
+ /**
18
+ * ContextWriter<S, Sl>
19
+ *
20
+ * Reading — direct property access on Readonly<S>: ctx.db, ctx.config
21
+ * Writing — set() restricted to OwnSlice keys only: ctx.set('auth', ...)
22
+ *
23
+ * Readonly<S> prevents ctx.db = ... at the type level.
24
+ * Attempting to write a dep key via set() is also a type error.
25
+ *
26
+ * Note: 'set' is a reserved key — no slice may use 'set' as a property name.
27
+ */
28
+ export type ContextWriter<S extends object, Sl extends object = S> = Readonly<S> & {
29
+ set<K extends Exclude<keyof Sl, "set">>(key: K, value: Sl[K]): void;
30
+ };
31
+ /** Converts U1 | U2 | U3 into U1 & U2 & U3 via contravariance. */
32
+ type UnionToIntersection<U> = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
33
+ /** Extracts full context S from a Module — so transitive deps propagate upward. */
34
+ type SliceOf<M> = M extends Module<infer S, object> ? S : never;
35
+ /** Merges full context S from all modules in a tuple into one intersection. */
36
+ type MergeSlices<T extends readonly AnyModule[]> = UnionToIntersection<SliceOf<T[number]>> extends object ? UnionToIntersection<SliceOf<T[number]>> : Record<never, never>;
37
+ /** Full context seen by a module = all deps merged context + own slice. */
38
+ type FullContext<Deps extends readonly AnyModule[], Own extends object> = MergeSlices<Deps> & Own;
39
+ /** Opaque alias used at internal boundaries where generic params are erased. */
40
+ type AnyModule = Module<object, object>;
41
+ /**
42
+ * Global lifecycle hooks for start().
43
+ *
44
+ * Each fires exactly once — not per-module:
45
+ * beforeBoot — before the first module boots
46
+ * afterBoot — after the last module has booted
47
+ * beforeShutdown — before the first module shuts down
48
+ * afterShutdown — after the last module has shut down
49
+ *
50
+ * Per-module variants (beforeEach*, afterEach*) fire once per module,
51
+ * in boot/shutdown order, with the current module passed as the second argument.
52
+ */
53
+ export interface LifecycleHooks<S extends object = Record<never, never>> {
54
+ /** Called once before the first module begins booting. */
55
+ beforeBoot?(ctx: ContextWriter<S>): Promise<void> | void;
56
+ /** Called once after the last module has finished booting. */
57
+ afterBoot?(ctx: ContextWriter<S>): Promise<void> | void;
58
+ /** Called once before the first module begins shutting down. */
59
+ beforeShutdown?(ctx: ContextWriter<S>): Promise<void> | void;
60
+ /** Called once after the last module has finished shutting down. */
61
+ afterShutdown?(ctx: ContextWriter<S>): Promise<void> | void;
62
+ /** Called before each individual module boots, in boot order. */
63
+ beforeEachBoot?(ctx: ContextWriter<S>,
64
+ /** The module about to boot. */
65
+ module: Module<any>): Promise<void> | void;
66
+ /** Called after each individual module finishes booting, in boot order. */
67
+ afterEachBoot?(ctx: ContextWriter<S>,
68
+ /** The module that just finished booting. */
69
+ module: Module<any>): Promise<void> | void;
70
+ /** Called before each individual module shuts down, in shutdown order. */
71
+ beforeEachShutdown?(ctx: ContextWriter<S>,
72
+ /** The module about to shut down. */
73
+ module: Module<any>): Promise<void> | void;
74
+ /** Called after each individual module finishes shutting down, in shutdown order. */
75
+ afterEachShutdown?(ctx: ContextWriter<S>,
76
+ /** The module that just finished shutting down. */
77
+ module: Module<any>): Promise<void> | void;
78
+ }
79
+ /**
80
+ * Module<S, Sl>
81
+ *
82
+ * Per-module hooks fire scoped to this module's own boot/shutdown.
83
+ * They receive ContextWriter<S, Sl> — same as boot/shutdown.
84
+ */
85
+ export interface Module<S extends object, Sl extends object = S> {
86
+ name: string;
87
+ description?: string;
88
+ modules?: AnyModule[];
89
+ boot?(ctx: ContextWriter<S, Sl>): Promise<void> | void;
90
+ shutdown?(ctx: ContextWriter<S, Sl>): Promise<void> | void;
91
+ /** Called immediately before this module's own boot(). */
92
+ beforeBoot?(ctx: ContextWriter<S, Sl>): Promise<void> | void;
93
+ /** Called immediately after this module's own boot(). */
94
+ afterBoot?(ctx: ContextWriter<S, Sl>): Promise<void> | void;
95
+ /** Called immediately before this module's own shutdown(). */
96
+ beforeShutdown?(ctx: ContextWriter<S, Sl>): Promise<void> | void;
97
+ /** Called immediately after this module's own shutdown(). */
98
+ afterShutdown?(ctx: ContextWriter<S, Sl>): Promise<void> | void;
99
+ }
100
+ /**
101
+ * Define a module with full context inferred from its dependency tuple.
102
+ *
103
+ * OwnSlice first — pass only what this module contributes:
104
+ * defineModule<AuthSlice>()({ modules: [db, redis] as const, ... })
105
+ *
106
+ * Deps is inferred from modules — no need to pass it explicitly.
107
+ * Omit OwnSlice entirely if this module contributes nothing to context:
108
+ * defineModule()({ modules: [db] as const, ... })
109
+ *
110
+ * Inside boot/shutdown:
111
+ * ctx.db — read dep directly (Readonly<S>)
112
+ * ctx.set('auth') — write own slice only
113
+ *
114
+ * @param def module definition
115
+ */
116
+ export declare function defineModule<OwnSlice extends object = Record<never, never>>(): <const Deps extends readonly AnyModule[] = []>(def: {
117
+ name: string;
118
+ description?: string;
119
+ modules?: Deps;
120
+ boot?(ctx: ContextWriter<FullContext<Deps, OwnSlice>, OwnSlice>): Promise<void> | void;
121
+ shutdown?(ctx: ContextWriter<FullContext<Deps, OwnSlice>, OwnSlice>): Promise<void> | void;
122
+ beforeBoot?(ctx: ContextWriter<FullContext<Deps, OwnSlice>, OwnSlice>): Promise<void> | void;
123
+ afterBoot?(ctx: ContextWriter<FullContext<Deps, OwnSlice>, OwnSlice>): Promise<void> | void;
124
+ beforeShutdown?(ctx: ContextWriter<FullContext<Deps, OwnSlice>, OwnSlice>): Promise<void> | void;
125
+ afterShutdown?(ctx: ContextWriter<FullContext<Deps, OwnSlice>, OwnSlice>): Promise<void> | void;
126
+ }) => Module<FullContext<Deps, OwnSlice>, OwnSlice>;
127
+ /**
128
+ * Options for start(). Flat intersection — no nesting required:
129
+ * start([db], { log, afterBoot: async (ctx) => ... })
130
+ *
131
+ * S is inferred from the roots tuple — no explicit type param needed.
132
+ */
133
+ export type StartOptions<S extends object = Record<never, never>> = LifecycleHooks<S>;
134
+ /**
135
+ * Boot all modules reachable from roots, in dependency order.
136
+ * Deduplicates by name across the entire tree — pass only roots.
137
+ * Returns stop() for graceful shutdown. stop() is idempotent.
138
+ *
139
+ * Context type is inferred from the roots tuple automatically.
140
+ *
141
+ * Global hook order:
142
+ * beforeBoot → [each: beforeBoot → boot → afterBoot] → afterBoot
143
+ * beforeShutdown → [each: beforeShutdown → shutdown → afterShutdown] → afterShutdown
144
+ *
145
+ * Global hooks do NOT fire during rollback — rollback is error recovery.
146
+ *
147
+ * Throws if:
148
+ * - same name resolves to two different instances (collision)
149
+ * - a circular dependency is detected
150
+ * - any boot() fails (already-booted modules are rolled back)
151
+ *
152
+ * @param roots root Module instances — tree walked recursively
153
+ * @param options optional log + global lifecycle hooks
154
+ */
155
+ export default function start<const Roots extends readonly AnyModule[]>(roots: Roots, options?: StartOptions<MergeSlices<Roots>>): Promise<{
156
+ stop(): Promise<void>;
157
+ ctx: MergeSlices<Roots>;
158
+ }>;
159
+ /**
160
+ * Walk the module tree from roots, dedupe by name, topoSort.
161
+ * Throws on name collision (same name, different instance).
162
+ */
163
+ export declare function resolveModules(roots: AnyModule[]): AnyModule[];
164
+ /**
165
+ * DFS topological sort over the registry.
166
+ * Throws on circular dependency.
167
+ */
168
+ export declare function topoSort(registry: Map<string, AnyModule>): AnyModule[];
169
+ /**
170
+ * Shut down already-booted modules in reverse order.
171
+ * Best-effort: logs errors, never throws.
172
+ * Global beforeShutdown/afterShutdown do NOT fire — rollback is error recovery.
173
+ */
174
+ export declare function rollback(booted: AnyModule[], modWriter: ContextWriter<object, object>): Promise<void>;
175
+ export {};
package/esm/index.js ADDED
@@ -0,0 +1,214 @@
1
+ /**
2
+ * tsdkarc.ts
3
+ *
4
+ * Minimal module lifecycle manager.
5
+ *
6
+ * Exports: Module, defineModule, ContextWriter, Logger, LifecycleHooks, StartOptions, start.
7
+ *
8
+ * Type helpers (internal):
9
+ * UnionToIntersection<U> — U1 | U2 | U3 → U1 & U2 & U3
10
+ * SliceOf<M> — extracts S (full context) from Module<S, Sl>
11
+ * MergeSlices<Tuple> — intersects all S from a modules tuple
12
+ * FullContext<Deps, Own> — MergeSlices<Deps> & Own
13
+ *
14
+ * Internal functions use AnyModule (Module<object, object>) at boundaries
15
+ * where generic params are intentionally erased — the runtime does not need them.
16
+ */
17
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
18
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
19
+ return new (P || (P = Promise))(function (resolve, reject) {
20
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
21
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
22
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
23
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
24
+ });
25
+ };
26
+ // ---------------------------------------------------------------------------
27
+ // defineModule
28
+ // ---------------------------------------------------------------------------
29
+ /**
30
+ * Define a module with full context inferred from its dependency tuple.
31
+ *
32
+ * OwnSlice first — pass only what this module contributes:
33
+ * defineModule<AuthSlice>()({ modules: [db, redis] as const, ... })
34
+ *
35
+ * Deps is inferred from modules — no need to pass it explicitly.
36
+ * Omit OwnSlice entirely if this module contributes nothing to context:
37
+ * defineModule()({ modules: [db] as const, ... })
38
+ *
39
+ * Inside boot/shutdown:
40
+ * ctx.db — read dep directly (Readonly<S>)
41
+ * ctx.set('auth') — write own slice only
42
+ *
43
+ * @param def module definition
44
+ */
45
+ export function defineModule() {
46
+ return function (def) {
47
+ return def;
48
+ };
49
+ }
50
+ // ---------------------------------------------------------------------------
51
+ // start
52
+ // ---------------------------------------------------------------------------
53
+ /**
54
+ * Boot all modules reachable from roots, in dependency order.
55
+ * Deduplicates by name across the entire tree — pass only roots.
56
+ * Returns stop() for graceful shutdown. stop() is idempotent.
57
+ *
58
+ * Context type is inferred from the roots tuple automatically.
59
+ *
60
+ * Global hook order:
61
+ * beforeBoot → [each: beforeBoot → boot → afterBoot] → afterBoot
62
+ * beforeShutdown → [each: beforeShutdown → shutdown → afterShutdown] → afterShutdown
63
+ *
64
+ * Global hooks do NOT fire during rollback — rollback is error recovery.
65
+ *
66
+ * Throws if:
67
+ * - same name resolves to two different instances (collision)
68
+ * - a circular dependency is detected
69
+ * - any boot() fails (already-booted modules are rolled back)
70
+ *
71
+ * @param roots root Module instances — tree walked recursively
72
+ * @param options optional log + global lifecycle hooks
73
+ */
74
+ export default function start(roots_1) {
75
+ return __awaiter(this, arguments, void 0, function* (roots, options = {}) {
76
+ var _a, _b, _c;
77
+ const { beforeBoot, afterBoot, beforeShutdown, afterShutdown, beforeEachBoot, afterEachBoot, beforeEachShutdown, afterEachShutdown, } = options;
78
+ const ctx = {};
79
+ const writer = makeWriter(ctx);
80
+ const modWriter = writer;
81
+ const booted = [];
82
+ let stopped = false;
83
+ const sorted = resolveModules([...roots]);
84
+ function doStop() {
85
+ return __awaiter(this, void 0, void 0, function* () {
86
+ var _a, _b, _c;
87
+ if (stopped)
88
+ return;
89
+ stopped = true;
90
+ yield (beforeShutdown === null || beforeShutdown === void 0 ? void 0 : beforeShutdown(writer));
91
+ for (const mod of [...booted].reverse()) {
92
+ try {
93
+ yield (beforeEachShutdown === null || beforeEachShutdown === void 0 ? void 0 : beforeEachShutdown(writer, mod));
94
+ yield ((_a = mod.beforeShutdown) === null || _a === void 0 ? void 0 : _a.call(mod, modWriter));
95
+ yield ((_b = mod.shutdown) === null || _b === void 0 ? void 0 : _b.call(mod, modWriter));
96
+ yield (afterEachShutdown === null || afterEachShutdown === void 0 ? void 0 : afterEachShutdown(writer, mod));
97
+ yield ((_c = mod.afterShutdown) === null || _c === void 0 ? void 0 : _c.call(mod, modWriter));
98
+ }
99
+ catch (err) {
100
+ throw new Error(`Module "${mod.name}" stop failed: ${errorMessage(err)}`);
101
+ }
102
+ }
103
+ yield (afterShutdown === null || afterShutdown === void 0 ? void 0 : afterShutdown(writer));
104
+ });
105
+ }
106
+ yield (beforeBoot === null || beforeBoot === void 0 ? void 0 : beforeBoot(writer));
107
+ for (const mod of sorted) {
108
+ try {
109
+ yield (beforeEachBoot === null || beforeEachBoot === void 0 ? void 0 : beforeEachBoot(writer, mod));
110
+ yield ((_a = mod.beforeBoot) === null || _a === void 0 ? void 0 : _a.call(mod, modWriter));
111
+ yield ((_b = mod.boot) === null || _b === void 0 ? void 0 : _b.call(mod, modWriter));
112
+ yield (afterEachBoot === null || afterEachBoot === void 0 ? void 0 : afterEachBoot(writer, mod));
113
+ yield ((_c = mod.afterBoot) === null || _c === void 0 ? void 0 : _c.call(mod, modWriter));
114
+ booted.push(mod);
115
+ }
116
+ catch (err) {
117
+ stopped = true;
118
+ yield rollback(booted, modWriter);
119
+ throw new Error(`Module "${mod.name}" boot failed: ${errorMessage(err)}`);
120
+ }
121
+ }
122
+ yield (afterBoot === null || afterBoot === void 0 ? void 0 : afterBoot(writer));
123
+ return { stop: doStop, ctx: modWriter };
124
+ });
125
+ }
126
+ // ---------------------------------------------------------------------------
127
+ // Internal helpers
128
+ // ---------------------------------------------------------------------------
129
+ /**
130
+ * Walk the module tree from roots, dedupe by name, topoSort.
131
+ * Throws on name collision (same name, different instance).
132
+ */
133
+ export function resolveModules(roots) {
134
+ const registry = new Map();
135
+ function collect(mod) {
136
+ var _a;
137
+ const existing = registry.get(mod.name);
138
+ if (existing) {
139
+ if (existing !== mod) {
140
+ throw new Error(`Module name collision: "${mod.name}" registered with two different instances`);
141
+ }
142
+ return;
143
+ }
144
+ registry.set(mod.name, mod);
145
+ for (const dep of (_a = mod.modules) !== null && _a !== void 0 ? _a : [])
146
+ collect(dep);
147
+ }
148
+ for (const root of roots)
149
+ collect(root);
150
+ return topoSort(registry);
151
+ }
152
+ /**
153
+ * DFS topological sort over the registry.
154
+ * Throws on circular dependency.
155
+ */
156
+ export function topoSort(registry) {
157
+ const visited = new Set();
158
+ const inStack = new Set();
159
+ const order = [];
160
+ function visit(name) {
161
+ var _a;
162
+ if (visited.has(name))
163
+ return;
164
+ if (inStack.has(name))
165
+ throw new Error(`Circular dependency detected at: "${name}"`);
166
+ const mod = registry.get(name);
167
+ if (!mod)
168
+ return;
169
+ inStack.add(name);
170
+ for (const dep of (_a = mod.modules) !== null && _a !== void 0 ? _a : [])
171
+ visit(dep.name);
172
+ inStack.delete(name);
173
+ visited.add(name);
174
+ order.push(mod);
175
+ }
176
+ for (const name of registry.keys())
177
+ visit(name);
178
+ return order;
179
+ }
180
+ /**
181
+ * Shut down already-booted modules in reverse order.
182
+ * Best-effort: logs errors, never throws.
183
+ * Global beforeShutdown/afterShutdown do NOT fire — rollback is error recovery.
184
+ */
185
+ export function rollback(booted, modWriter) {
186
+ return __awaiter(this, void 0, void 0, function* () {
187
+ var _a, _b, _c;
188
+ for (const mod of [...booted].reverse()) {
189
+ try {
190
+ yield ((_a = mod.beforeShutdown) === null || _a === void 0 ? void 0 : _a.call(mod, modWriter));
191
+ yield ((_b = mod.shutdown) === null || _b === void 0 ? void 0 : _b.call(mod, modWriter));
192
+ yield ((_c = mod.afterShutdown) === null || _c === void 0 ? void 0 : _c.call(mod, modWriter));
193
+ }
194
+ catch (err) {
195
+ throw new Error(`Module "${mod.name}" rollback_shutdown_failed: ${errorMessage(err)}`);
196
+ }
197
+ }
198
+ });
199
+ }
200
+ /**
201
+ * ContextWriter backed by a plain Record.
202
+ * Direct property access for reads (via Proxy), set() for writes.
203
+ * Cast to ContextWriter<S> at the start() boundary.
204
+ */
205
+ function makeWriter(ctx) {
206
+ return Object.assign(ctx, {
207
+ set(key, value) {
208
+ ctx[key] = value;
209
+ },
210
+ });
211
+ }
212
+ function errorMessage(err) {
213
+ return err instanceof Error ? err.message : String(err);
214
+ }
@@ -0,0 +1 @@
1
+ {"fileNames":["../node_modules/typescript/lib/lib.es5.d.ts","../node_modules/typescript/lib/lib.es2015.d.ts","../node_modules/typescript/lib/lib.dom.d.ts","../node_modules/typescript/lib/lib.es2015.core.d.ts","../node_modules/typescript/lib/lib.es2015.collection.d.ts","../node_modules/typescript/lib/lib.es2015.generator.d.ts","../node_modules/typescript/lib/lib.es2015.iterable.d.ts","../node_modules/typescript/lib/lib.es2015.promise.d.ts","../node_modules/typescript/lib/lib.es2015.proxy.d.ts","../node_modules/typescript/lib/lib.es2015.reflect.d.ts","../node_modules/typescript/lib/lib.es2015.symbol.d.ts","../node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../node_modules/typescript/lib/lib.decorators.d.ts","../node_modules/typescript/lib/lib.decorators.legacy.d.ts","../src/index.ts","../node_modules/@types/deep-eql/index.d.ts","../node_modules/assertion-error/index.d.ts","../node_modules/@types/chai/index.d.ts","../node_modules/@types/estree/index.d.ts"],"fileIdsList":[[16,17]],"fileInfos":[{"version":"c430d44666289dae81f30fa7b2edebf186ecc91a2d4c71266ea6ae76388792e1","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"080941d9f9ff9307f7e27a83bcd888b7c8270716c39af943532438932ec1d0b9","affectsGlobalScope":true,"impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","affectsGlobalScope":true,"impliedFormat":1},{"version":"0559b1f683ac7505ae451f9a96ce4c3c92bdc71411651ca6ddb0e88baaaad6a3","affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true,"impliedFormat":1},{"version":"fb0f136d372979348d59b3f5020b4cdb81b5504192b1cacff5d1fbba29378aa1","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"66d99e5eea1d583803f80816a7e5b264c4bac636deb853502918e2044ef1a5f7","signature":"5d3723bdf23914654d36eda00c37a16b499e89400a515fbecdb9b3741429f4e0"},{"version":"427fe2004642504828c1476d0af4270e6ad4db6de78c0b5da3e4c5ca95052a99","impliedFormat":1},{"version":"2eeffcee5c1661ddca53353929558037b8cf305ffb86a803512982f99bcab50d","impliedFormat":99},{"version":"9afb4cb864d297e4092a79ee2871b5d3143ea14153f62ef0bb04ede25f432030","affectsGlobalScope":true,"impliedFormat":99},{"version":"151ff381ef9ff8da2da9b9663ebf657eac35c4c9a19183420c05728f31a6761d","impliedFormat":1}],"root":[15],"options":{"declaration":true,"downlevelIteration":true,"emitDecoratorMetadata":true,"esModuleInterop":true,"experimentalDecorators":true,"module":5,"noImplicitAny":true,"outDir":"./","skipLibCheck":true,"strict":true,"strictPropertyInitialization":false,"target":2},"referencedMap":[[18,1]],"version":"5.9.3"}
package/lib/index.d.ts ADDED
@@ -0,0 +1,175 @@
1
+ /**
2
+ * tsdkarc.ts
3
+ *
4
+ * Minimal module lifecycle manager.
5
+ *
6
+ * Exports: Module, defineModule, ContextWriter, Logger, LifecycleHooks, StartOptions, start.
7
+ *
8
+ * Type helpers (internal):
9
+ * UnionToIntersection<U> — U1 | U2 | U3 → U1 & U2 & U3
10
+ * SliceOf<M> — extracts S (full context) from Module<S, Sl>
11
+ * MergeSlices<Tuple> — intersects all S from a modules tuple
12
+ * FullContext<Deps, Own> — MergeSlices<Deps> & Own
13
+ *
14
+ * Internal functions use AnyModule (Module<object, object>) at boundaries
15
+ * where generic params are intentionally erased — the runtime does not need them.
16
+ */
17
+ /**
18
+ * ContextWriter<S, Sl>
19
+ *
20
+ * Reading — direct property access on Readonly<S>: ctx.db, ctx.config
21
+ * Writing — set() restricted to OwnSlice keys only: ctx.set('auth', ...)
22
+ *
23
+ * Readonly<S> prevents ctx.db = ... at the type level.
24
+ * Attempting to write a dep key via set() is also a type error.
25
+ *
26
+ * Note: 'set' is a reserved key — no slice may use 'set' as a property name.
27
+ */
28
+ export type ContextWriter<S extends object, Sl extends object = S> = Readonly<S> & {
29
+ set<K extends Exclude<keyof Sl, "set">>(key: K, value: Sl[K]): void;
30
+ };
31
+ /** Converts U1 | U2 | U3 into U1 & U2 & U3 via contravariance. */
32
+ type UnionToIntersection<U> = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
33
+ /** Extracts full context S from a Module — so transitive deps propagate upward. */
34
+ type SliceOf<M> = M extends Module<infer S, object> ? S : never;
35
+ /** Merges full context S from all modules in a tuple into one intersection. */
36
+ type MergeSlices<T extends readonly AnyModule[]> = UnionToIntersection<SliceOf<T[number]>> extends object ? UnionToIntersection<SliceOf<T[number]>> : Record<never, never>;
37
+ /** Full context seen by a module = all deps merged context + own slice. */
38
+ type FullContext<Deps extends readonly AnyModule[], Own extends object> = MergeSlices<Deps> & Own;
39
+ /** Opaque alias used at internal boundaries where generic params are erased. */
40
+ type AnyModule = Module<object, object>;
41
+ /**
42
+ * Global lifecycle hooks for start().
43
+ *
44
+ * Each fires exactly once — not per-module:
45
+ * beforeBoot — before the first module boots
46
+ * afterBoot — after the last module has booted
47
+ * beforeShutdown — before the first module shuts down
48
+ * afterShutdown — after the last module has shut down
49
+ *
50
+ * Per-module variants (beforeEach*, afterEach*) fire once per module,
51
+ * in boot/shutdown order, with the current module passed as the second argument.
52
+ */
53
+ export interface LifecycleHooks<S extends object = Record<never, never>> {
54
+ /** Called once before the first module begins booting. */
55
+ beforeBoot?(ctx: ContextWriter<S>): Promise<void> | void;
56
+ /** Called once after the last module has finished booting. */
57
+ afterBoot?(ctx: ContextWriter<S>): Promise<void> | void;
58
+ /** Called once before the first module begins shutting down. */
59
+ beforeShutdown?(ctx: ContextWriter<S>): Promise<void> | void;
60
+ /** Called once after the last module has finished shutting down. */
61
+ afterShutdown?(ctx: ContextWriter<S>): Promise<void> | void;
62
+ /** Called before each individual module boots, in boot order. */
63
+ beforeEachBoot?(ctx: ContextWriter<S>,
64
+ /** The module about to boot. */
65
+ module: Module<any>): Promise<void> | void;
66
+ /** Called after each individual module finishes booting, in boot order. */
67
+ afterEachBoot?(ctx: ContextWriter<S>,
68
+ /** The module that just finished booting. */
69
+ module: Module<any>): Promise<void> | void;
70
+ /** Called before each individual module shuts down, in shutdown order. */
71
+ beforeEachShutdown?(ctx: ContextWriter<S>,
72
+ /** The module about to shut down. */
73
+ module: Module<any>): Promise<void> | void;
74
+ /** Called after each individual module finishes shutting down, in shutdown order. */
75
+ afterEachShutdown?(ctx: ContextWriter<S>,
76
+ /** The module that just finished shutting down. */
77
+ module: Module<any>): Promise<void> | void;
78
+ }
79
+ /**
80
+ * Module<S, Sl>
81
+ *
82
+ * Per-module hooks fire scoped to this module's own boot/shutdown.
83
+ * They receive ContextWriter<S, Sl> — same as boot/shutdown.
84
+ */
85
+ export interface Module<S extends object, Sl extends object = S> {
86
+ name: string;
87
+ description?: string;
88
+ modules?: AnyModule[];
89
+ boot?(ctx: ContextWriter<S, Sl>): Promise<void> | void;
90
+ shutdown?(ctx: ContextWriter<S, Sl>): Promise<void> | void;
91
+ /** Called immediately before this module's own boot(). */
92
+ beforeBoot?(ctx: ContextWriter<S, Sl>): Promise<void> | void;
93
+ /** Called immediately after this module's own boot(). */
94
+ afterBoot?(ctx: ContextWriter<S, Sl>): Promise<void> | void;
95
+ /** Called immediately before this module's own shutdown(). */
96
+ beforeShutdown?(ctx: ContextWriter<S, Sl>): Promise<void> | void;
97
+ /** Called immediately after this module's own shutdown(). */
98
+ afterShutdown?(ctx: ContextWriter<S, Sl>): Promise<void> | void;
99
+ }
100
+ /**
101
+ * Define a module with full context inferred from its dependency tuple.
102
+ *
103
+ * OwnSlice first — pass only what this module contributes:
104
+ * defineModule<AuthSlice>()({ modules: [db, redis] as const, ... })
105
+ *
106
+ * Deps is inferred from modules — no need to pass it explicitly.
107
+ * Omit OwnSlice entirely if this module contributes nothing to context:
108
+ * defineModule()({ modules: [db] as const, ... })
109
+ *
110
+ * Inside boot/shutdown:
111
+ * ctx.db — read dep directly (Readonly<S>)
112
+ * ctx.set('auth') — write own slice only
113
+ *
114
+ * @param def module definition
115
+ */
116
+ export declare function defineModule<OwnSlice extends object = Record<never, never>>(): <const Deps extends readonly AnyModule[] = []>(def: {
117
+ name: string;
118
+ description?: string;
119
+ modules?: Deps;
120
+ boot?(ctx: ContextWriter<FullContext<Deps, OwnSlice>, OwnSlice>): Promise<void> | void;
121
+ shutdown?(ctx: ContextWriter<FullContext<Deps, OwnSlice>, OwnSlice>): Promise<void> | void;
122
+ beforeBoot?(ctx: ContextWriter<FullContext<Deps, OwnSlice>, OwnSlice>): Promise<void> | void;
123
+ afterBoot?(ctx: ContextWriter<FullContext<Deps, OwnSlice>, OwnSlice>): Promise<void> | void;
124
+ beforeShutdown?(ctx: ContextWriter<FullContext<Deps, OwnSlice>, OwnSlice>): Promise<void> | void;
125
+ afterShutdown?(ctx: ContextWriter<FullContext<Deps, OwnSlice>, OwnSlice>): Promise<void> | void;
126
+ }) => Module<FullContext<Deps, OwnSlice>, OwnSlice>;
127
+ /**
128
+ * Options for start(). Flat intersection — no nesting required:
129
+ * start([db], { log, afterBoot: async (ctx) => ... })
130
+ *
131
+ * S is inferred from the roots tuple — no explicit type param needed.
132
+ */
133
+ export type StartOptions<S extends object = Record<never, never>> = LifecycleHooks<S>;
134
+ /**
135
+ * Boot all modules reachable from roots, in dependency order.
136
+ * Deduplicates by name across the entire tree — pass only roots.
137
+ * Returns stop() for graceful shutdown. stop() is idempotent.
138
+ *
139
+ * Context type is inferred from the roots tuple automatically.
140
+ *
141
+ * Global hook order:
142
+ * beforeBoot → [each: beforeBoot → boot → afterBoot] → afterBoot
143
+ * beforeShutdown → [each: beforeShutdown → shutdown → afterShutdown] → afterShutdown
144
+ *
145
+ * Global hooks do NOT fire during rollback — rollback is error recovery.
146
+ *
147
+ * Throws if:
148
+ * - same name resolves to two different instances (collision)
149
+ * - a circular dependency is detected
150
+ * - any boot() fails (already-booted modules are rolled back)
151
+ *
152
+ * @param roots root Module instances — tree walked recursively
153
+ * @param options optional log + global lifecycle hooks
154
+ */
155
+ export default function start<const Roots extends readonly AnyModule[]>(roots: Roots, options?: StartOptions<MergeSlices<Roots>>): Promise<{
156
+ stop(): Promise<void>;
157
+ ctx: MergeSlices<Roots>;
158
+ }>;
159
+ /**
160
+ * Walk the module tree from roots, dedupe by name, topoSort.
161
+ * Throws on name collision (same name, different instance).
162
+ */
163
+ export declare function resolveModules(roots: AnyModule[]): AnyModule[];
164
+ /**
165
+ * DFS topological sort over the registry.
166
+ * Throws on circular dependency.
167
+ */
168
+ export declare function topoSort(registry: Map<string, AnyModule>): AnyModule[];
169
+ /**
170
+ * Shut down already-booted modules in reverse order.
171
+ * Best-effort: logs errors, never throws.
172
+ * Global beforeShutdown/afterShutdown do NOT fire — rollback is error recovery.
173
+ */
174
+ export declare function rollback(booted: AnyModule[], modWriter: ContextWriter<object, object>): Promise<void>;
175
+ export {};
package/lib/index.js ADDED
@@ -0,0 +1,221 @@
1
+ "use strict";
2
+ /**
3
+ * tsdkarc.ts
4
+ *
5
+ * Minimal module lifecycle manager.
6
+ *
7
+ * Exports: Module, defineModule, ContextWriter, Logger, LifecycleHooks, StartOptions, start.
8
+ *
9
+ * Type helpers (internal):
10
+ * UnionToIntersection<U> — U1 | U2 | U3 → U1 & U2 & U3
11
+ * SliceOf<M> — extracts S (full context) from Module<S, Sl>
12
+ * MergeSlices<Tuple> — intersects all S from a modules tuple
13
+ * FullContext<Deps, Own> — MergeSlices<Deps> & Own
14
+ *
15
+ * Internal functions use AnyModule (Module<object, object>) at boundaries
16
+ * where generic params are intentionally erased — the runtime does not need them.
17
+ */
18
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
19
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
20
+ return new (P || (P = Promise))(function (resolve, reject) {
21
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
22
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
23
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
24
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
25
+ });
26
+ };
27
+ Object.defineProperty(exports, "__esModule", { value: true });
28
+ exports.defineModule = defineModule;
29
+ exports.default = start;
30
+ exports.resolveModules = resolveModules;
31
+ exports.topoSort = topoSort;
32
+ exports.rollback = rollback;
33
+ // ---------------------------------------------------------------------------
34
+ // defineModule
35
+ // ---------------------------------------------------------------------------
36
+ /**
37
+ * Define a module with full context inferred from its dependency tuple.
38
+ *
39
+ * OwnSlice first — pass only what this module contributes:
40
+ * defineModule<AuthSlice>()({ modules: [db, redis] as const, ... })
41
+ *
42
+ * Deps is inferred from modules — no need to pass it explicitly.
43
+ * Omit OwnSlice entirely if this module contributes nothing to context:
44
+ * defineModule()({ modules: [db] as const, ... })
45
+ *
46
+ * Inside boot/shutdown:
47
+ * ctx.db — read dep directly (Readonly<S>)
48
+ * ctx.set('auth') — write own slice only
49
+ *
50
+ * @param def module definition
51
+ */
52
+ function defineModule() {
53
+ return function (def) {
54
+ return def;
55
+ };
56
+ }
57
+ // ---------------------------------------------------------------------------
58
+ // start
59
+ // ---------------------------------------------------------------------------
60
+ /**
61
+ * Boot all modules reachable from roots, in dependency order.
62
+ * Deduplicates by name across the entire tree — pass only roots.
63
+ * Returns stop() for graceful shutdown. stop() is idempotent.
64
+ *
65
+ * Context type is inferred from the roots tuple automatically.
66
+ *
67
+ * Global hook order:
68
+ * beforeBoot → [each: beforeBoot → boot → afterBoot] → afterBoot
69
+ * beforeShutdown → [each: beforeShutdown → shutdown → afterShutdown] → afterShutdown
70
+ *
71
+ * Global hooks do NOT fire during rollback — rollback is error recovery.
72
+ *
73
+ * Throws if:
74
+ * - same name resolves to two different instances (collision)
75
+ * - a circular dependency is detected
76
+ * - any boot() fails (already-booted modules are rolled back)
77
+ *
78
+ * @param roots root Module instances — tree walked recursively
79
+ * @param options optional log + global lifecycle hooks
80
+ */
81
+ function start(roots_1) {
82
+ return __awaiter(this, arguments, void 0, function* (roots, options = {}) {
83
+ var _a, _b, _c;
84
+ const { beforeBoot, afterBoot, beforeShutdown, afterShutdown, beforeEachBoot, afterEachBoot, beforeEachShutdown, afterEachShutdown, } = options;
85
+ const ctx = {};
86
+ const writer = makeWriter(ctx);
87
+ const modWriter = writer;
88
+ const booted = [];
89
+ let stopped = false;
90
+ const sorted = resolveModules([...roots]);
91
+ function doStop() {
92
+ return __awaiter(this, void 0, void 0, function* () {
93
+ var _a, _b, _c;
94
+ if (stopped)
95
+ return;
96
+ stopped = true;
97
+ yield (beforeShutdown === null || beforeShutdown === void 0 ? void 0 : beforeShutdown(writer));
98
+ for (const mod of [...booted].reverse()) {
99
+ try {
100
+ yield (beforeEachShutdown === null || beforeEachShutdown === void 0 ? void 0 : beforeEachShutdown(writer, mod));
101
+ yield ((_a = mod.beforeShutdown) === null || _a === void 0 ? void 0 : _a.call(mod, modWriter));
102
+ yield ((_b = mod.shutdown) === null || _b === void 0 ? void 0 : _b.call(mod, modWriter));
103
+ yield (afterEachShutdown === null || afterEachShutdown === void 0 ? void 0 : afterEachShutdown(writer, mod));
104
+ yield ((_c = mod.afterShutdown) === null || _c === void 0 ? void 0 : _c.call(mod, modWriter));
105
+ }
106
+ catch (err) {
107
+ throw new Error(`Module "${mod.name}" stop failed: ${errorMessage(err)}`);
108
+ }
109
+ }
110
+ yield (afterShutdown === null || afterShutdown === void 0 ? void 0 : afterShutdown(writer));
111
+ });
112
+ }
113
+ yield (beforeBoot === null || beforeBoot === void 0 ? void 0 : beforeBoot(writer));
114
+ for (const mod of sorted) {
115
+ try {
116
+ yield (beforeEachBoot === null || beforeEachBoot === void 0 ? void 0 : beforeEachBoot(writer, mod));
117
+ yield ((_a = mod.beforeBoot) === null || _a === void 0 ? void 0 : _a.call(mod, modWriter));
118
+ yield ((_b = mod.boot) === null || _b === void 0 ? void 0 : _b.call(mod, modWriter));
119
+ yield (afterEachBoot === null || afterEachBoot === void 0 ? void 0 : afterEachBoot(writer, mod));
120
+ yield ((_c = mod.afterBoot) === null || _c === void 0 ? void 0 : _c.call(mod, modWriter));
121
+ booted.push(mod);
122
+ }
123
+ catch (err) {
124
+ stopped = true;
125
+ yield rollback(booted, modWriter);
126
+ throw new Error(`Module "${mod.name}" boot failed: ${errorMessage(err)}`);
127
+ }
128
+ }
129
+ yield (afterBoot === null || afterBoot === void 0 ? void 0 : afterBoot(writer));
130
+ return { stop: doStop, ctx: modWriter };
131
+ });
132
+ }
133
+ // ---------------------------------------------------------------------------
134
+ // Internal helpers
135
+ // ---------------------------------------------------------------------------
136
+ /**
137
+ * Walk the module tree from roots, dedupe by name, topoSort.
138
+ * Throws on name collision (same name, different instance).
139
+ */
140
+ function resolveModules(roots) {
141
+ const registry = new Map();
142
+ function collect(mod) {
143
+ var _a;
144
+ const existing = registry.get(mod.name);
145
+ if (existing) {
146
+ if (existing !== mod) {
147
+ throw new Error(`Module name collision: "${mod.name}" registered with two different instances`);
148
+ }
149
+ return;
150
+ }
151
+ registry.set(mod.name, mod);
152
+ for (const dep of (_a = mod.modules) !== null && _a !== void 0 ? _a : [])
153
+ collect(dep);
154
+ }
155
+ for (const root of roots)
156
+ collect(root);
157
+ return topoSort(registry);
158
+ }
159
+ /**
160
+ * DFS topological sort over the registry.
161
+ * Throws on circular dependency.
162
+ */
163
+ function topoSort(registry) {
164
+ const visited = new Set();
165
+ const inStack = new Set();
166
+ const order = [];
167
+ function visit(name) {
168
+ var _a;
169
+ if (visited.has(name))
170
+ return;
171
+ if (inStack.has(name))
172
+ throw new Error(`Circular dependency detected at: "${name}"`);
173
+ const mod = registry.get(name);
174
+ if (!mod)
175
+ return;
176
+ inStack.add(name);
177
+ for (const dep of (_a = mod.modules) !== null && _a !== void 0 ? _a : [])
178
+ visit(dep.name);
179
+ inStack.delete(name);
180
+ visited.add(name);
181
+ order.push(mod);
182
+ }
183
+ for (const name of registry.keys())
184
+ visit(name);
185
+ return order;
186
+ }
187
+ /**
188
+ * Shut down already-booted modules in reverse order.
189
+ * Best-effort: logs errors, never throws.
190
+ * Global beforeShutdown/afterShutdown do NOT fire — rollback is error recovery.
191
+ */
192
+ function rollback(booted, modWriter) {
193
+ return __awaiter(this, void 0, void 0, function* () {
194
+ var _a, _b, _c;
195
+ for (const mod of [...booted].reverse()) {
196
+ try {
197
+ yield ((_a = mod.beforeShutdown) === null || _a === void 0 ? void 0 : _a.call(mod, modWriter));
198
+ yield ((_b = mod.shutdown) === null || _b === void 0 ? void 0 : _b.call(mod, modWriter));
199
+ yield ((_c = mod.afterShutdown) === null || _c === void 0 ? void 0 : _c.call(mod, modWriter));
200
+ }
201
+ catch (err) {
202
+ throw new Error(`Module "${mod.name}" rollback_shutdown_failed: ${errorMessage(err)}`);
203
+ }
204
+ }
205
+ });
206
+ }
207
+ /**
208
+ * ContextWriter backed by a plain Record.
209
+ * Direct property access for reads (via Proxy), set() for writes.
210
+ * Cast to ContextWriter<S> at the start() boundary.
211
+ */
212
+ function makeWriter(ctx) {
213
+ return Object.assign(ctx, {
214
+ set(key, value) {
215
+ ctx[key] = value;
216
+ },
217
+ });
218
+ }
219
+ function errorMessage(err) {
220
+ return err instanceof Error ? err.message : String(err);
221
+ }
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "tsdkarc",
3
+ "version": "1.0.0",
4
+ "description": "Elegant and Fully type-safe module composition library",
5
+ "repository": "tsdk-monorepo/tsdkarc",
6
+ "bugs": "https://github.com/tsdk-monorepo/tsdkarc/issues",
7
+ "homepage": "https://www.npmjs.com/package/tsdkarc",
8
+ "main": "lib/index.js",
9
+ "module": "esm/index.js",
10
+ "types": "./lib/index.d.ts",
11
+ "files": [
12
+ "lib/*",
13
+ "esm/*",
14
+ "assets",
15
+ "README.md"
16
+ ],
17
+ "keywords": [
18
+ "typesafe",
19
+ "tsdk",
20
+ "dependency injection",
21
+ "di"
22
+ ],
23
+ "license": "MIT"
24
+ }