@spaceteams/warp 0.1.0 β 0.1.1
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 +10 -0
- 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
|
@@ -37,6 +37,12 @@ type InferComponentOut<T> = InferComponentParams<T>[3];
|
|
|
37
37
|
declare function brandComponent<T extends object, Ctx, RunOptions, Out>(obj: T): T & ComponentRef<Ctx, RunOptions, Out>;
|
|
38
38
|
declare function isComponent(value: unknown): value is Component<unknown, unknown, unknown, unknown>;
|
|
39
39
|
//#endregion
|
|
40
|
+
//#region src/runtime/explain.d.ts
|
|
41
|
+
type ExplainResult = {
|
|
42
|
+
name: string | undefined;
|
|
43
|
+
deps?: Record<string, ExplainResult>;
|
|
44
|
+
};
|
|
45
|
+
//#endregion
|
|
40
46
|
//#region src/runtime/runtime.d.ts
|
|
41
47
|
type SafeIntersect<A, B> = A extends undefined ? B : A & B;
|
|
42
48
|
type OptionalArg<A> = A extends undefined ? [] : [A];
|
|
@@ -54,6 +60,10 @@ declare class Runtime<Ctx, ActualContext extends Ctx, ScopeContext, RunOptions,
|
|
|
54
60
|
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?: {
|
|
55
61
|
name?: string | undefined;
|
|
56
62
|
} | undefined) => Component<SafeIntersect<Requirements, ActualContext>, RunOptions, Deps, InstanceType<Ctor>>;
|
|
63
|
+
explain<Deps, Out>(component: Component<SafeIntersect<Requirements, ActualContext>, RunOptions, Deps, Out>, format: "native"): ExplainResult;
|
|
64
|
+
explain<Deps, Out>(component: Component<SafeIntersect<Requirements, ActualContext>, RunOptions, Deps, Out>, format: "ascii"): string;
|
|
65
|
+
explain<Deps, Out>(component: Component<SafeIntersect<Requirements, ActualContext>, RunOptions, Deps, Out>, format: "mermaid"): string;
|
|
66
|
+
explain<Deps, Out>(component: Component<SafeIntersect<Requirements, ActualContext>, RunOptions, Deps, Out>): ExplainResult;
|
|
57
67
|
}
|
|
58
68
|
//#endregion
|
|
59
69
|
//#region src/runtime/runtime-builder.d.ts
|
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
|