@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 +107 -0
- package/dist/index.d.mts +42 -30
- package/dist/index.mjs +51 -0
- package/package.json +1 -1
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,
|
|
8
|
-
run: <T>(options: RunOptions, inner: (app: Run<AmbientContext &
|
|
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
|
|
35
|
-
type
|
|
36
|
-
type
|
|
37
|
-
|
|
38
|
-
declare function
|
|
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,
|
|
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,
|
|
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
|