@spaceteams/warp 0.1.0 β†’ 0.2.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,107 @@
1
+ # 🧡 Warp
2
+ > Compose once. Resolve per request.
3
+
4
+ `warp` is a small runtime for composing application components and resolving them
5
+ against an ambient context.
6
+
7
+ It helps structure repositories, services and use cases as an explicit dependency
8
+ graph while keeping request context, transactions, tracing and tests easy to manage.
9
+
10
+ No container magic. No autowiring. Just explicit composition and scoped execution.
11
+
12
+ ---
13
+
14
+ ## Why β€œwarp”?
15
+
16
+ The name comes from two places.
17
+
18
+ In physics, **warp** suggests speed.
19
+ In weaving, the **warp threads** are the fixed threads that hold a fabric together.
20
+
21
+ `warp` plays a similar role in your application:
22
+
23
+ - components form the **threads of a dependency graph**
24
+ - the runtime **weaves them together for a request**
25
+ - cross-cutting concerns like transactions or tracing can be applied **through middleware**
26
+
27
+ ---
28
+
29
+ ## The problem
30
+
31
+ In most Node.js applications, wiring dependencies eventually becomes messy.
32
+
33
+ You end up with one of these situations:
34
+
35
+ - **Manual wiring everywhere**
36
+ ```ts
37
+ const repo = createRepo(prisma)
38
+ const service = createService(repo)
39
+ const usecase = createUseCase(service)
40
+ ```
41
+ - **A container that hides everything**
42
+ ```ts
43
+ container.resolve("CreateUserUseCase")
44
+ ```
45
+
46
+ Manual wiring becomes repetitive.
47
+ Containers often introduce hidden magic and runtime errors.
48
+
49
+ At the same time modern applications need:
50
+
51
+ - request scoped context
52
+ - transactions
53
+ - tracing and metrics
54
+ - easy test overrides
55
+
56
+ warp provides a middle ground.
57
+
58
+ Dependencies stay explicit and typed, but wiring happens once and the runtime handles scoped execution.
59
+
60
+ ## Core idea
61
+ Define a *component graph* once:
62
+ ```ts
63
+ const service = component(serviceFactory, {
64
+ repo: component(repoFactory)
65
+ })
66
+ ```
67
+ Resolve it against the *current request context*:
68
+ ```ts
69
+ const handler = resolve(service, { requestId })
70
+ handler(params)
71
+ ```
72
+ The graph is static.
73
+ The context is supplied when executing.
74
+
75
+ > Compose once, resolve per request.
76
+
77
+ ## Why warp?
78
+
79
+ - explicit dependency graphs
80
+ - typed context
81
+ - request-scoped execution
82
+ - middleware for cross-cutting concerns
83
+ - easy testing with overrides
84
+ - no container magic
85
+
86
+ ## Examples
87
+ You can find more examples in the [examples](../examples) project
88
+
89
+ ## When to use it
90
+
91
+ `warp` is useful if your application has:
92
+
93
+ - repositories, services and use cases
94
+ - request scoped context
95
+ - transactions or tracing
96
+ - complex dependency wiring
97
+ - tests that need to replace dependencies
98
+
99
+ ## When not to use it
100
+
101
+ If your application is small and simple manual wiring is enough, you probably don’t need `warp`.
102
+
103
+ Or if you want to go all-in with [effect](https://effect.website/) anyway.
104
+
105
+ ## Status
106
+
107
+ Early stage, evolving API.
package/dist/index.d.mts CHANGED
@@ -1,41 +1,49 @@
1
- //#region src/middleware.d.ts
2
- type NoRunOptions = NonNullable<unknown>;
3
- type NoScopeContext = NonNullable<unknown>;
4
- type Middleware<AmbientContext, RunOptions = NoRunOptions, ScopeContext = NoScopeContext> = <T>(ctx: AmbientContext, options: Partial<RunOptions>, next: (ctx: AmbientContext & ScopeContext) => Promise<T> | T) => Promise<T> | T;
5
- //#endregion
6
1
  //#region src/run.d.ts
7
- type Run<AmbientContext, RunOptions, ScopeContext = NoScopeContext> = AmbientContext & {
8
- run: <T>(options: RunOptions, inner: (app: Run<AmbientContext & Partial<ScopeContext>, RunOptions, ScopeContext>) => Promise<T> | T) => Promise<T> | T;
2
+ type Run<AmbientContext, ScopeContext = unknown, RunOptions = unknown> = AmbientContext & {
3
+ run: <T>(options: RunOptions, inner: (app: Run<AmbientContext & ScopeContext, ScopeContext, RunOptions>) => Promise<T> | T) => Promise<T> | T;
9
4
  };
10
5
  //#endregion
11
6
  //#region src/component/index.d.ts
12
7
  type NoDeps = NonNullable<unknown>;
13
8
  declare const COMPONENT: unique symbol;
14
- type ComponentRef<Ctx, RunOptions, Out> = {
9
+ type ComponentRef<Ctx, ScopeContext, RunOptions, Out> = {
15
10
  readonly [COMPONENT]: true;
16
11
  readonly __ctx?: Ctx;
12
+ readonly __scopeContext?: ScopeContext;
17
13
  readonly __runOptions?: RunOptions;
18
14
  readonly __out?: Out;
19
15
  };
20
- type ComponentFactory<Ctx, RunOptions, Deps, Out> = (ctx: Run<Ctx & Deps, RunOptions>) => Out;
21
- type ComponentInput<Ctx, RunOptions, Out> = ComponentRef<Ctx, RunOptions, Out> | Out;
22
- type Component<Ctx, RunOptions, Deps, Out> = ComponentRef<Ctx, RunOptions, Out> & {
23
- factory: ComponentFactory<Ctx, RunOptions, Deps, Out>;
24
- deps?: { [K in keyof Deps]: ComponentRef<Ctx, RunOptions, Deps[K]> };
16
+ type ComponentFactory<Ctx, ScopeContext, RunOptions, Deps, Out> = (ctx: Run<Ctx & Deps, ScopeContext, RunOptions>) => Out;
17
+ type ComponentInput<Ctx, ScopeContext, RunOptions, Out> = ComponentRef<Ctx, ScopeContext, RunOptions, Out> | Out;
18
+ type Component<Ctx, ScopeContext, RunOptions, Deps, Out> = ComponentRef<Ctx, ScopeContext, RunOptions, Out> & {
19
+ factory: ComponentFactory<Ctx, ScopeContext, RunOptions, Deps, Out>;
20
+ deps?: { [K in keyof Deps]: ComponentRef<Ctx, ScopeContext, RunOptions, Deps[K]> };
25
21
  name?: string;
26
22
  };
27
- type ComponentDefinition<Ctx, RunOptions, Deps, Out> = {
28
- factory: ComponentFactory<Ctx, RunOptions, Deps, Out>;
29
- deps?: { [K in keyof Deps]: ComponentInput<Ctx, RunOptions, Deps[K]> };
23
+ type ComponentDefinition<Ctx, ScopeContext, RunOptions, Deps, Out> = {
24
+ factory: ComponentFactory<Ctx, ScopeContext, RunOptions, Deps, Out>;
25
+ deps?: { [K in keyof Deps]: ComponentInput<Ctx, ScopeContext, RunOptions, Deps[K]> };
30
26
  name?: string;
31
27
  };
32
- type InferComponentParams<T> = T extends Component<infer Ctx, infer RunOptions, infer Deps, infer Out> ? [Ctx, RunOptions, Deps, Out] : never;
28
+ type InferComponentParams<T> = T extends Component<infer Ctx, infer ScopeContext, infer RunOptions, infer Deps, infer Out> ? [Ctx, ScopeContext, RunOptions, Deps, Out] : never;
33
29
  type InferComponentCtx<T> = InferComponentParams<T>[0];
34
- type InferComponentRunOptions<T> = InferComponentParams<T>[1];
35
- type InferComponentDeps<T> = InferComponentParams<T>[2];
36
- type InferComponentOut<T> = InferComponentParams<T>[3];
37
- declare function brandComponent<T extends object, Ctx, RunOptions, Out>(obj: T): T & ComponentRef<Ctx, RunOptions, Out>;
38
- declare function isComponent(value: unknown): value is Component<unknown, unknown, unknown, unknown>;
30
+ type InferComponentScopeContext<T> = InferComponentParams<T>[1];
31
+ type InferComponentRunOptions<T> = InferComponentParams<T>[2];
32
+ type InferComponentDeps<T> = InferComponentParams<T>[3];
33
+ type InferComponentOut<T> = InferComponentParams<T>[4];
34
+ declare function brandComponent<T extends object, Ctx, ScopeContext, RunOptions, Out>(obj: T): T & ComponentRef<Ctx, ScopeContext, RunOptions, Out>;
35
+ declare function isComponent(value: unknown): value is Component<unknown, unknown, unknown, unknown, unknown>;
36
+ //#endregion
37
+ //#region src/middleware.d.ts
38
+ type NoRunOptions = NonNullable<unknown>;
39
+ type NoScopeContext = NonNullable<unknown>;
40
+ type Middleware<AmbientContext, RunOptions = NoRunOptions, ScopeContext = NoScopeContext> = <T>(ctx: AmbientContext, options: Partial<RunOptions>, next: (ctx: AmbientContext & ScopeContext) => Promise<T> | T) => Promise<T> | T;
41
+ //#endregion
42
+ //#region src/runtime/explain.d.ts
43
+ type ExplainResult = {
44
+ name: string | undefined;
45
+ deps?: Record<string, ExplainResult>;
46
+ };
39
47
  //#endregion
40
48
  //#region src/runtime/runtime.d.ts
41
49
  type SafeIntersect<A, B> = A extends undefined ? B : A & B;
@@ -47,20 +55,24 @@ declare class Runtime<Ctx, ActualContext extends Ctx, ScopeContext, RunOptions,
47
55
  constructor(middleware: Middleware<Ctx, RunOptions, ScopeContext>, ctx: ActualContext);
48
56
  provide<Extension>(ext: Extension): Runtime<Ctx, ActualContext & Extension, ScopeContext, RunOptions, Requirements>;
49
57
  require<Extension>(): Runtime<Ctx, ActualContext, ScopeContext, RunOptions, SafeIntersect<Requirements, Extension>>;
50
- resolve: <Deps, Out>(component: Component<SafeIntersect<Requirements, ActualContext>, RunOptions, Deps, Out>, ...requirements: OptionalArg<Requirements>) => Out | Promise<Out>;
51
- get component(): <Deps, F extends ComponentFactory<SafeIntersect<Requirements, ActualContext>, RunOptions, Deps, ReturnType<F>>>(factory: F, deps?: { [T in keyof Deps]: ComponentInput<SafeIntersect<Requirements, ActualContext>, RunOptions, Deps[T]> } | undefined, opts?: {
58
+ resolve: <Deps, Out>(component: Component<SafeIntersect<Requirements, ActualContext>, ScopeContext, RunOptions, Deps, Out>, ...requirements: OptionalArg<Requirements>) => Out | Promise<Out>;
59
+ get component(): <Deps, F extends ComponentFactory<SafeIntersect<Requirements, ActualContext>, ScopeContext, RunOptions, Deps, ReturnType<F>>>(factory: F, deps?: { [T in keyof Deps]: ComponentInput<SafeIntersect<Requirements, ActualContext>, ScopeContext, RunOptions, Deps[T]> } | undefined, opts?: {
52
60
  name?: string | undefined;
53
- } | undefined) => Component<SafeIntersect<Requirements, ActualContext>, RunOptions, Deps, ReturnType<F>>;
54
- get classComponent(): <Deps, Ctor extends new (deps: Run<SafeIntersect<Requirements, ActualContext> & Deps, RunOptions>) => InstanceType<Ctor>>(ctor: Ctor, deps?: { [K in keyof Deps]: ComponentInput<SafeIntersect<Requirements, ActualContext>, RunOptions, Deps[K]> } | undefined, opts?: {
61
+ } | undefined) => Component<SafeIntersect<Requirements, ActualContext>, ScopeContext, RunOptions, Deps, ReturnType<F>>;
62
+ get classComponent(): <Deps, Ctor extends new (deps: Run<SafeIntersect<Requirements, ActualContext> & Deps, ScopeContext, RunOptions>) => InstanceType<Ctor>>(ctor: Ctor, deps?: { [K in keyof Deps]: ComponentInput<SafeIntersect<Requirements, ActualContext>, ScopeContext, RunOptions, Deps[K]> } | undefined, opts?: {
55
63
  name?: string | undefined;
56
- } | undefined) => Component<SafeIntersect<Requirements, ActualContext>, RunOptions, Deps, InstanceType<Ctor>>;
64
+ } | undefined) => Component<SafeIntersect<Requirements, ActualContext>, ScopeContext, RunOptions, Deps, InstanceType<Ctor>>;
65
+ explain<Deps, Out>(component: Component<SafeIntersect<Requirements, ActualContext>, ScopeContext, RunOptions, Deps, Out>, format: "native"): ExplainResult;
66
+ explain<Deps, Out>(component: Component<SafeIntersect<Requirements, ActualContext>, ScopeContext, RunOptions, Deps, Out>, format: "ascii"): string;
67
+ explain<Deps, Out>(component: Component<SafeIntersect<Requirements, ActualContext>, ScopeContext, RunOptions, Deps, Out>, format: "mermaid"): string;
68
+ explain<Deps, Out>(component: Component<SafeIntersect<Requirements, ActualContext>, ScopeContext, RunOptions, Deps, Out>): ExplainResult;
57
69
  }
58
70
  //#endregion
59
71
  //#region src/runtime/runtime-builder.d.ts
60
- declare class RuntimeBuilder<AmbientContext, Options, ScopeContext = NoScopeContext> {
72
+ declare class RuntimeBuilder<AmbientContext, ScopeContext = unknown, Options = unknown> {
61
73
  private readonly middlewares;
62
74
  constructor(middlewares?: Middleware<AmbientContext, Options, ScopeContext>[]);
63
- use<A, H, S>(mw: Middleware<A, H, S>): RuntimeBuilder<AmbientContext & A, Options & H, ScopeContext & S>;
75
+ use<A, S, H>(mw: Middleware<A, H, S>): RuntimeBuilder<AmbientContext & A, ScopeContext & S, Options & H>;
64
76
  provide<ActualCtx extends AmbientContext>(ctx: ActualCtx): Runtime<AmbientContext, ActualCtx, ScopeContext, Options, undefined>;
65
77
  private buildMiddleware;
66
78
  }
@@ -68,4 +80,4 @@ declare class RuntimeBuilder<AmbientContext, Options, ScopeContext = NoScopeCont
68
80
  //#region src/runtime/index.d.ts
69
81
  declare function buildRuntime(): RuntimeBuilder<Record<string, unknown>, unknown>;
70
82
  //#endregion
71
- export { Component, ComponentDefinition, ComponentFactory, ComponentInput, ComponentRef, InferComponentCtx, InferComponentDeps, InferComponentOut, InferComponentParams, InferComponentRunOptions, Middleware, NoDeps, NoRunOptions, NoScopeContext, Run, brandComponent, buildRuntime, isComponent };
83
+ export { Component, ComponentDefinition, ComponentFactory, ComponentInput, ComponentRef, InferComponentCtx, InferComponentDeps, InferComponentOut, InferComponentParams, InferComponentRunOptions, InferComponentScopeContext, Middleware, NoDeps, NoRunOptions, NoScopeContext, Run, brandComponent, buildRuntime, isComponent };
package/dist/index.mjs CHANGED
@@ -99,6 +99,49 @@ function createResolver(mw) {
99
99
  };
100
100
  }
101
101
  //#endregion
102
+ //#region src/runtime/explain.ts
103
+ const explain = (c) => {
104
+ const deps = {};
105
+ for (const [key, comp] of Object.entries(c.deps ?? {})) deps[key] = explain(comp);
106
+ return {
107
+ name: c.name,
108
+ deps
109
+ };
110
+ };
111
+ const toAsciiTree = (result, prefix = "", isLast = true) => {
112
+ const lines = [];
113
+ const connector = isLast ? "└── " : "β”œβ”€β”€ ";
114
+ lines.push(prefix + connector + result.name);
115
+ const deps = Object.entries(result.deps ?? {});
116
+ const newPrefix = prefix + (isLast ? " " : "β”‚ ");
117
+ deps.forEach(([key, dep], index) => {
118
+ const isLastDep = index === deps.length - 1;
119
+ const connector = isLastDep ? "└── " : "β”œβ”€β”€ ";
120
+ lines.push(`${newPrefix}${connector}[${key}]`);
121
+ const childLines = toAsciiTree(dep, newPrefix + (isLastDep ? " " : "β”‚ "), true).split("\n").slice(1);
122
+ lines.push(...childLines);
123
+ });
124
+ return lines.join("\n");
125
+ };
126
+ const toMermaid = (result, nodeId, isRoot = true) => {
127
+ const lines = [];
128
+ if (isRoot) {
129
+ lines.push("graph TD");
130
+ nodeId = result.name ?? "root";
131
+ }
132
+ const sanitizedId = nodeId.replace(/[^a-zA-Z0-9_]/g, "_");
133
+ if (isRoot) lines.push(` ${sanitizedId}["${result.name}"]`);
134
+ Object.entries(result.deps ?? {}).forEach(([key, dep]) => {
135
+ const sanitizedDepId = (dep.name ? `${sanitizedId}_${dep.name}` : `${sanitizedId}_${key}`).replace(/[^a-zA-Z0-9_]/g, "_");
136
+ const displayName = dep.name ?? key;
137
+ lines.push(` ${sanitizedDepId}["${displayName}"]`);
138
+ lines.push(` ${sanitizedId} -->|${key}| ${sanitizedDepId}`);
139
+ const childLines = toMermaid(dep, sanitizedDepId, false).split("\n").slice(1);
140
+ lines.push(...childLines);
141
+ });
142
+ return lines.join("\n");
143
+ };
144
+ //#endregion
102
145
  //#region src/runtime/runtime.ts
103
146
  var Runtime = class Runtime {
104
147
  resolveFn;
@@ -130,6 +173,14 @@ var Runtime = class Runtime {
130
173
  get classComponent() {
131
174
  return defineClassComponent();
132
175
  }
176
+ explain(component, format = "native") {
177
+ const result = explain(component);
178
+ switch (format) {
179
+ case "ascii": return toAsciiTree(result);
180
+ case "mermaid": return toMermaid(result);
181
+ case "native": return result;
182
+ }
183
+ }
133
184
  };
134
185
  //#endregion
135
186
  //#region src/runtime/runtime-builder.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spaceteams/warp",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Type-safe composition with execution scopes ",
5
5
  "type": "module",
6
6
  "files": [