refactor(core): make boot and middleware ordering deterministic
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
package dev.relism.flash.extension;
|
||||
|
||||
import dev.relism.flash.models.RequestHandler;
|
||||
import dev.relism.flash.routing.Middleware;
|
||||
import dev.relism.flash.routing.MiddlewareNode;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -18,5 +18,5 @@ import java.util.List;
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface AnnotationProcessor {
|
||||
List<Middleware> process(Class<? extends RequestHandler> handlerClass);
|
||||
List<MiddlewareNode> process(Class<? extends RequestHandler> handlerClass);
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package dev.relism.flash.extension;
|
||||
|
||||
/**
|
||||
* Semantic execution phases for {@link FlashExtension#priority()}.
|
||||
*
|
||||
* <p>Phase determines the order in which annotation-processor middlewares are injected into
|
||||
* the chain. Lower value = runs earlier (outermost wrapper = first at request time).
|
||||
*
|
||||
* <pre>
|
||||
* Request ──► EARLY middlewares ──► DEFAULT middlewares ──► LATE middlewares ──► handler
|
||||
* </pre>
|
||||
*
|
||||
* <p>Within the same phase, extensions execute in install order (sort is stable).
|
||||
* Raw integers are valid for fine-grained ordering within a phase
|
||||
* (e.g. {@code ExtensionPhase.EARLY.value + 10}).
|
||||
*/
|
||||
public enum ExtensionPhase {
|
||||
|
||||
/** Security guards, rate limiting — must short-circuit before expensive processing. */
|
||||
EARLY(100),
|
||||
|
||||
/** Normal application extensions. Default when {@link FlashExtension#priority()} is not overridden. */
|
||||
DEFAULT(500),
|
||||
|
||||
/** Observability, logging, diagnostics — must observe after all business logic. */
|
||||
LATE(900);
|
||||
|
||||
/** The integer priority value used for sorting. */
|
||||
public final int value;
|
||||
|
||||
ExtensionPhase(int value) { this.value = value; }
|
||||
}
|
||||
@@ -9,6 +9,8 @@ import dev.relism.flash.models.SimpleHandler;
|
||||
import dev.relism.flash.routing.AbstractRouter;
|
||||
import dev.relism.flash.routing.AbstractWsRouter;
|
||||
import dev.relism.flash.routing.Middleware;
|
||||
import dev.relism.flash.routing.MiddlewareGraph;
|
||||
import dev.relism.flash.routing.MiddlewareNode;
|
||||
import dev.relism.flash.routing.routers.fastpathrouter.FastPathRouterImpl;
|
||||
import dev.relism.flash.routing.routers.fastpathrouter.FastPathWsRouterImpl;
|
||||
import dev.relism.flash.websocket.WebSocketEndpoint;
|
||||
@@ -19,7 +21,6 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Consumer;
|
||||
@@ -41,10 +42,9 @@ import java.util.function.Consumer;
|
||||
*
|
||||
* <h3>Startup sequence (both {@link #start()} and {@link #startAndBlock()})</h3>
|
||||
* <ol>
|
||||
* <li>Extensions sorted by {@link FlashExtension#priority()} — lower first.</li>
|
||||
* <li>All {@link FlashExtension#provide} — register services, processors, listeners.</li>
|
||||
* <li>All {@link FlashExtension#configure} declarations.</li>
|
||||
* <li>{@link FlashContext#resolveAll()} — topo-sort, cycle detection.</li>
|
||||
* <li>All {@link FlashExtension#routes} — routes registered, services available.</li>
|
||||
* <li>Ready callbacks register routes with resolved services.</li>
|
||||
* <li>Compile all routes into one flat FSM — zero prefix scanning at runtime.</li>
|
||||
* <li>Accept loop started.</li>
|
||||
* </ol>
|
||||
@@ -62,7 +62,7 @@ public final class FlashApp extends FlashRegistrar<FlashApp> {
|
||||
private final ServerHandle server;
|
||||
private final FlashContext ctx = new FlashContext();
|
||||
private final List<FlashExtension> extensions = new ArrayList<>();
|
||||
private final List<Middleware> globalMiddlewares = new ArrayList<>();
|
||||
private final List<MiddlewareNode> globalMiddlewares = new ArrayList<>();
|
||||
private final List<RouteDefinition> deferredRoutes = new ArrayList<>();
|
||||
private final List<WsRouteDefinition> deferredWsRoutes = new ArrayList<>();
|
||||
|
||||
@@ -131,10 +131,7 @@ public final class FlashApp extends FlashRegistrar<FlashApp> {
|
||||
// ── Extensions ────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Registers an extension for two-phase installation at startup.
|
||||
* Install order is irrelevant — all {@link FlashExtension#provide} calls complete
|
||||
* before any {@link FlashExtension#routes} call begins.
|
||||
* Extensions are sorted by {@link FlashExtension#priority()} before execution.
|
||||
* Registers a declarative extension contribution.
|
||||
*/
|
||||
public FlashApp install(FlashExtension ext) {
|
||||
extensions.add(ext);
|
||||
@@ -174,7 +171,7 @@ public final class FlashApp extends FlashRegistrar<FlashApp> {
|
||||
// ── FlashRegistrar impl ───────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
protected void addRoute(HttpMethod method, String path, RequestHandler handler, List<Middleware> mw) {
|
||||
protected void addRoute(HttpMethod method, String path, RequestHandler handler, List<MiddlewareNode> mw) {
|
||||
deferredRoutes.add(new RouteDefinition(
|
||||
method, path, handler, List.of(), mw,
|
||||
!(handler instanceof SimpleHandler), ctx, "/"));
|
||||
@@ -186,27 +183,25 @@ public final class FlashApp extends FlashRegistrar<FlashApp> {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addMiddleware(Middleware mw) { globalMiddlewares.add(mw); }
|
||||
protected void addMiddleware(MiddlewareNode mw) { globalMiddlewares.add(mw); }
|
||||
|
||||
// ── Boot ─────────────────────────────────────────────────────────────────
|
||||
|
||||
private void boot() {
|
||||
extensions.sort(Comparator.comparingInt(FlashExtension::priority));
|
||||
extensions.forEach(e -> e.provide(ctx));
|
||||
ctx.resolveAll();
|
||||
extensions.forEach(e -> e.routes(this, ctx));
|
||||
extensions.forEach(e -> e.configure(this, ctx));
|
||||
ctx.complete();
|
||||
compile();
|
||||
compileWs();
|
||||
router.compile();
|
||||
wsRouter.compile();
|
||||
}
|
||||
|
||||
// ── Compilation ──────────────────────────────────────────────────────────
|
||||
|
||||
private static final Middleware[] EMPTY_MW = new Middleware[0];
|
||||
|
||||
/** Middleware chain order per route: Global → Scope → Annotation → Explicit. */
|
||||
private void compile() {
|
||||
for (RouteDefinition def : deferredRoutes) {
|
||||
List<Middleware> injected;
|
||||
List<MiddlewareNode> injected;
|
||||
if (def.classBasedHandler()) {
|
||||
injected = def.ctx().processors().stream()
|
||||
.flatMap(p -> p.process(def.handler().getClass()).stream())
|
||||
@@ -215,7 +210,8 @@ public final class FlashApp extends FlashRegistrar<FlashApp> {
|
||||
} else {
|
||||
injected = List.of();
|
||||
}
|
||||
Middleware[] all = concat(globalMiddlewares, def.scopeMiddlewares(), injected, def.explicitMiddlewares());
|
||||
Middleware[] all = MiddlewareGraph.order(def.method() + " " + def.path(),
|
||||
concat(globalMiddlewares, def.scopeMiddlewares(), injected, def.explicitMiddlewares()));
|
||||
emitEvent(def, all);
|
||||
router.doRegister(def.method(), def.path(), def.handler(), all);
|
||||
}
|
||||
@@ -248,16 +244,12 @@ public final class FlashApp extends FlashRegistrar<FlashApp> {
|
||||
listeners.forEach(l -> l.onRoute(event));
|
||||
}
|
||||
|
||||
private static Middleware[] concat(List<Middleware> global, List<Middleware> scope,
|
||||
List<Middleware> injected, List<Middleware> explicit) {
|
||||
private static List<MiddlewareNode> concat(List<MiddlewareNode> global, List<MiddlewareNode> scope,
|
||||
List<MiddlewareNode> injected, List<MiddlewareNode> explicit) {
|
||||
int total = global.size() + scope.size() + injected.size() + explicit.size();
|
||||
if (total == 0) return EMPTY_MW;
|
||||
Middleware[] all = new Middleware[total];
|
||||
int i = 0;
|
||||
for (Middleware m : global) all[i++] = m;
|
||||
for (Middleware m : scope) all[i++] = m;
|
||||
for (Middleware m : injected) all[i++] = m;
|
||||
for (Middleware m : explicit) all[i++] = m;
|
||||
if (total == 0) return List.of();
|
||||
List<MiddlewareNode> all = new ArrayList<>(total);
|
||||
all.addAll(global); all.addAll(scope); all.addAll(injected); all.addAll(explicit);
|
||||
return all;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,176 +1,160 @@
|
||||
package dev.relism.flash.extension;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Central service registry and boot-time hook coordinator.
|
||||
*
|
||||
* <p>Every handler, extension, and scope shares one (or a child of one) {@code FlashContext}.
|
||||
* Three capabilities:
|
||||
* <ol>
|
||||
* <li><b>Service registry</b> — {@link #provide}/{@link #supply}/{@link #require}/{@link #find}.</li>
|
||||
* <li><b>Annotation processors</b> — middleware injection from handler annotations at boot.</li>
|
||||
* <li><b>Route listeners</b> — boot-time observation of the route graph.</li>
|
||||
* </ol>
|
||||
*
|
||||
* <h3>Eager vs lazy registration</h3>
|
||||
* <ul>
|
||||
* <li>{@link #provide(Class, Object)} — instance is already constructed, registered immediately.</li>
|
||||
* <li>{@link #supply(Class, Supplier)} — factory is registered; it runs once, at
|
||||
* {@link #resolveAll()} time (called by {@link FlashApp#start()}) after all
|
||||
* {@link FlashExtension#provide} phases complete. The factory may call
|
||||
* {@link #require} for its own dependencies — the runtime resolves in topological
|
||||
* order automatically and reports circular dependencies with the full cycle path.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>A child context (via {@link #child()}) inherits parent services and processors.
|
||||
* Services provided on the child are scoped and invisible to the parent.
|
||||
*/
|
||||
public class FlashContext {
|
||||
/** Deterministic boot-time service graph, frozen before handlers are initialised. */
|
||||
public final class FlashContext {
|
||||
private enum State { DECLARING, RESOLVING, READY }
|
||||
|
||||
private final FlashContext parent;
|
||||
private final Map<Class<?>, Object> registry = new LinkedHashMap<>();
|
||||
private final Map<Class<?>, Supplier<?>> pending = new LinkedHashMap<>();
|
||||
private final List<AnnotationProcessor> processors = new ArrayList<>();
|
||||
private final List<RouteListener> routeListeners = new ArrayList<>();
|
||||
|
||||
// Lazy caches — nulled whenever the corresponding list is mutated.
|
||||
private final Map<Class<?>, Binding<?>> bindings = new LinkedHashMap<>();
|
||||
private final List<AnnotationProcessor> processors = new ArrayList<>();
|
||||
private final List<RouteListener> routeListeners = new ArrayList<>();
|
||||
private final List<Runnable> readyCallbacks = new ArrayList<>();
|
||||
private final List<FlashContext> children = new ArrayList<>();
|
||||
private final Deque<Class<?>> resolutionPath = new ArrayDeque<>();
|
||||
private State state = State.DECLARING;
|
||||
private List<AnnotationProcessor> cachedProcessors;
|
||||
private List<RouteListener> cachedListeners;
|
||||
private List<RouteListener> cachedListeners;
|
||||
|
||||
// DFS stack — tracks in-progress resolutions to detect circular dependencies.
|
||||
private final LinkedHashSet<Class<?>> resolutionStack = new LinkedHashSet<>();
|
||||
|
||||
public FlashContext() { this.parent = null; }
|
||||
public FlashContext() { parent = null; }
|
||||
private FlashContext(FlashContext parent) { this.parent = parent; }
|
||||
|
||||
/** Creates a child context that inherits this context's services and processors. */
|
||||
public FlashContext child() { return new FlashContext(this); }
|
||||
public FlashContext child() {
|
||||
requireDeclaring();
|
||||
FlashContext child = new FlashContext(this);
|
||||
children.add(child);
|
||||
return child;
|
||||
}
|
||||
|
||||
// ── Service registry ─────────────────────────────────────────────────────
|
||||
|
||||
/** Registers an already-constructed {@code instance} under {@code type}. */
|
||||
/** Binds an already-created singleton. Duplicate bindings are always an error. */
|
||||
public <T> void provide(Class<T> type, T instance) {
|
||||
registry.put(type, instance);
|
||||
declare(type, new Binding<>(type, List.of(), ignored -> Objects.requireNonNull(instance, "instance")));
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a lazy factory for {@code type}. The factory runs once at
|
||||
* {@link #resolveAll()} time (or on the first {@link #require} call for this type)
|
||||
* and may call {@link #require} for its own dependencies — topological order
|
||||
* is resolved automatically.
|
||||
*
|
||||
* <pre>{@code
|
||||
* ctx.supply(JwtValidator.class, () ->
|
||||
* new JwtValidator(ctx.require(OidcProviderMetadata.class).jwksUri()));
|
||||
* }</pre>
|
||||
*/
|
||||
/** Declares a no-dependency boot factory. */
|
||||
public <T> void supply(Class<T> type, Supplier<T> factory) {
|
||||
pending.put(type, factory);
|
||||
declare(type, new Binding<>(type, List.of(), ignored -> factory.get()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the service for {@code type}. Checks own scope first, then parent chain.
|
||||
* Lazy-registered types are resolved on first access. Circular dependencies throw
|
||||
* {@link IllegalStateException} with the full cycle path.
|
||||
*
|
||||
* @throws IllegalStateException if the service is not found anywhere in the context chain
|
||||
*/
|
||||
/** Declares a boot factory and its complete dependency set. */
|
||||
public <T> void supply(Class<T> type, ServiceFactory<T> factory, Class<?>... dependencies) {
|
||||
Objects.requireNonNull(factory, "factory");
|
||||
declare(type, new Binding<>(type, List.of(dependencies), factory));
|
||||
}
|
||||
|
||||
/** One-dependency factory with no application-side context lookup. */
|
||||
public <A, T> void supply(Class<T> type, Class<A> dependency, Function<A, T> factory) {
|
||||
supply(type, ignored -> factory.apply(require(dependency)), dependency);
|
||||
}
|
||||
|
||||
/** Registers work materialised after all services are resolved. */
|
||||
public void onReady(Runnable callback) { requireDeclaring(); readyCallbacks.add(Objects.requireNonNull(callback)); }
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T require(Class<T> type) {
|
||||
Object val = registry.get(type);
|
||||
if (val != null) return (T) val;
|
||||
if (pending.containsKey(type)) return resolve(type);
|
||||
if (state == State.DECLARING)
|
||||
throw new IllegalStateException("Service graph is still being declared; use FlashContext.onReady(...)");
|
||||
Binding<?> binding = bindings.get(type);
|
||||
if (binding != null) {
|
||||
verifyDeclaredDependency(type);
|
||||
return (T) resolve((Binding<Object>) binding);
|
||||
}
|
||||
if (parent != null) return parent.require(type);
|
||||
throw new IllegalStateException(
|
||||
"Service not found: " + type.getSimpleName() +
|
||||
" — register it via FlashContext.provide()/supply() or install the required extension");
|
||||
throw new IllegalStateException("No provider declared for " + type.getName() + dependencyTrace());
|
||||
}
|
||||
|
||||
/** Returns the service for {@code type}, or empty if not found in this scope or any parent. */
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> Optional<T> find(Class<T> type) {
|
||||
Object val = registry.get(type);
|
||||
if (val != null) return Optional.of((T) val);
|
||||
if (pending.containsKey(type)) return Optional.of(resolve(type));
|
||||
return parent != null ? parent.find(type) : Optional.empty();
|
||||
if (state == State.DECLARING)
|
||||
throw new IllegalStateException("Service graph is still being declared; use FlashContext.onReady(...)");
|
||||
if (bindings.containsKey(type)) return Optional.of(require(type));
|
||||
return parent == null ? Optional.empty() : parent.find(type);
|
||||
}
|
||||
|
||||
/** Alias for {@link #find} — prefer when semantics are "this may or may not exist". */
|
||||
public <T> Optional<T> optional(Class<T> type) { return find(type); }
|
||||
|
||||
/**
|
||||
* Eagerly resolves all pending lazy suppliers in topological order.
|
||||
* Called once by {@link FlashApp#start()} after all {@link FlashExtension#provide}
|
||||
* phases complete. Any circular dependency is reported with the full cycle path.
|
||||
*/
|
||||
void resolveAll() {
|
||||
new ArrayList<>(pending.keySet()).forEach(this::resolve);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> T resolve(Class<?> type) {
|
||||
Object already = registry.get(type);
|
||||
if (already != null) return (T) already; // resolved during an earlier DFS branch
|
||||
|
||||
if (!resolutionStack.add(type)) {
|
||||
// type is already on the current DFS path → circular dependency
|
||||
List<Class<?>> cycle = new ArrayList<>(resolutionStack);
|
||||
cycle.add(type);
|
||||
StringBuilder msg = new StringBuilder("Circular dependency: ");
|
||||
for (int i = 0; i < cycle.size(); i++) {
|
||||
if (i > 0) msg.append(" → ");
|
||||
msg.append(cycle.get(i).getSimpleName());
|
||||
}
|
||||
throw new IllegalStateException(msg.toString());
|
||||
}
|
||||
|
||||
Supplier<?> factory = pending.get(type);
|
||||
Object instance = factory.get(); // recursive require() calls happen here
|
||||
registry.put(type, instance);
|
||||
pending.remove(type);
|
||||
resolutionStack.remove(type);
|
||||
return (T) instance;
|
||||
}
|
||||
|
||||
// ── Annotation processors ────────────────────────────────────────────────
|
||||
|
||||
/** Registers an {@link AnnotationProcessor}. Processors run once per class-based handler at boot. */
|
||||
public void addAnnotationProcessor(AnnotationProcessor processor) {
|
||||
processors.add(processor);
|
||||
cachedProcessors = null;
|
||||
requireDeclaring(); processors.add(Objects.requireNonNull(processor)); cachedProcessors = null;
|
||||
}
|
||||
public void addRouteListener(RouteListener listener) {
|
||||
requireDeclaring(); routeListeners.add(Objects.requireNonNull(listener)); cachedListeners = null;
|
||||
}
|
||||
|
||||
/** All processors visible from this context: parent-first, then own. Cached after first call. */
|
||||
List<AnnotationProcessor> processors() {
|
||||
if (cachedProcessors != null) return cachedProcessors;
|
||||
if (parent == null) return cachedProcessors = List.copyOf(processors);
|
||||
List<AnnotationProcessor> p = parent.processors();
|
||||
if (processors.isEmpty()) return cachedProcessors = p;
|
||||
List<AnnotationProcessor> merged = new ArrayList<>(p.size() + processors.size());
|
||||
merged.addAll(p);
|
||||
merged.addAll(processors);
|
||||
return cachedProcessors = List.copyOf(merged);
|
||||
List<AnnotationProcessor> all = parent == null ? new ArrayList<>() : new ArrayList<>(parent.processors());
|
||||
all.addAll(processors); return cachedProcessors = List.copyOf(all);
|
||||
}
|
||||
|
||||
// ── Route listeners ──────────────────────────────────────────────────────
|
||||
|
||||
/** Registers a boot-time {@link RouteListener}. Zero overhead on the request hot-path. */
|
||||
public void addRouteListener(RouteListener listener) {
|
||||
routeListeners.add(listener);
|
||||
cachedListeners = null;
|
||||
}
|
||||
|
||||
/** All route listeners visible from this context: parent-first, then own. Cached after first call. */
|
||||
List<RouteListener> routeListeners() {
|
||||
if (cachedListeners != null) return cachedListeners;
|
||||
if (parent == null) return cachedListeners = List.copyOf(routeListeners);
|
||||
List<RouteListener> p = parent.routeListeners();
|
||||
if (routeListeners.isEmpty()) return cachedListeners = p;
|
||||
List<RouteListener> merged = new ArrayList<>(p.size() + routeListeners.size());
|
||||
merged.addAll(p);
|
||||
merged.addAll(routeListeners);
|
||||
return cachedListeners = List.copyOf(merged);
|
||||
List<RouteListener> all = parent == null ? new ArrayList<>() : new ArrayList<>(parent.routeListeners());
|
||||
all.addAll(routeListeners); return cachedListeners = List.copyOf(all);
|
||||
}
|
||||
}
|
||||
|
||||
void resolveAll() {
|
||||
if (state != State.DECLARING) throw new IllegalStateException("Service graph has already been closed");
|
||||
state = State.RESOLVING;
|
||||
for (Binding<?> binding : bindings.values()) resolveUnchecked(binding);
|
||||
for (FlashContext child : children) child.resolveAll();
|
||||
state = State.READY;
|
||||
}
|
||||
void runReadyCallbacks() {
|
||||
if (state != State.READY) throw new IllegalStateException("Service graph is not ready");
|
||||
for (Runnable callback : List.copyOf(readyCallbacks)) callback.run();
|
||||
readyCallbacks.clear();
|
||||
for (FlashContext child : children) child.runReadyCallbacks();
|
||||
}
|
||||
|
||||
/** Completes graph resolution and runs all deferred materialisation callbacks once. */
|
||||
public void complete() {
|
||||
resolveAll();
|
||||
runReadyCallbacks();
|
||||
}
|
||||
|
||||
private <T> void declare(Class<T> type, Binding<T> binding) {
|
||||
requireDeclaring(); Objects.requireNonNull(type, "type");
|
||||
if (bindings.putIfAbsent(type, binding) != null)
|
||||
throw new IllegalStateException("Duplicate provider declared for " + type.getName());
|
||||
}
|
||||
private void requireDeclaring() {
|
||||
if (state != State.DECLARING) throw new IllegalStateException("Flash service declarations are closed");
|
||||
}
|
||||
@SuppressWarnings("unchecked") private void resolveUnchecked(Binding<?> binding) { resolve((Binding<Object>) binding); }
|
||||
private <T> T resolve(Binding<T> binding) {
|
||||
if (binding.instance != null) return binding.instance;
|
||||
if (binding.resolving) throw cycle(binding.type);
|
||||
binding.resolving = true; resolutionPath.addLast(binding.type);
|
||||
try {
|
||||
for (Class<?> dependency : binding.dependencies) require(dependency);
|
||||
return binding.instance = Objects.requireNonNull(binding.factory.create(this),
|
||||
() -> "Provider returned null for " + binding.type.getName());
|
||||
} finally {
|
||||
resolutionPath.removeLast(); binding.resolving = false;
|
||||
}
|
||||
}
|
||||
private IllegalStateException cycle(Class<?> type) {
|
||||
StringBuilder out = new StringBuilder("Circular service dependency: ");
|
||||
for (Class<?> node : resolutionPath) out.append(node.getSimpleName()).append(" -> ");
|
||||
return new IllegalStateException(out.append(type.getSimpleName()).toString());
|
||||
}
|
||||
private String dependencyTrace() {
|
||||
return resolutionPath.isEmpty() ? "" : " (required while creating " + resolutionPath.peekLast().getName() + ')';
|
||||
}
|
||||
private void verifyDeclaredDependency(Class<?> type) {
|
||||
if (resolutionPath.isEmpty()) return;
|
||||
Class<?> owner = resolutionPath.peekLast();
|
||||
Binding<?> binding = bindings.get(owner);
|
||||
if (binding != null && !binding.dependencies.contains(type))
|
||||
throw new IllegalStateException(owner.getName() + " requested undeclared dependency " + type.getName());
|
||||
}
|
||||
|
||||
@FunctionalInterface public interface ServiceFactory<T> { T create(FlashContext services); }
|
||||
private static final class Binding<T> {
|
||||
final Class<T> type; final List<Class<?>> dependencies; final ServiceFactory<T> factory;
|
||||
T instance; boolean resolving;
|
||||
Binding(Class<T> type, List<Class<?>> dependencies, ServiceFactory<T> factory) {
|
||||
this.type = type; this.dependencies = dependencies; this.factory = factory;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,70 +1,17 @@
|
||||
package dev.relism.flash.extension;
|
||||
|
||||
/**
|
||||
* Two-phase contract for all Flash extensions.
|
||||
* One declarative contribution to a Flash application.
|
||||
*
|
||||
* <p>Extension lifecycle inside {@link FlashApp#start()}:
|
||||
* <ol>
|
||||
* <li>Extensions are sorted by {@link #priority()} — lower value runs first.</li>
|
||||
* <li><b>Provide phase</b> — {@link #provide(FlashContext)} is called for <em>all</em>
|
||||
* installed extensions. Use this phase to register services, annotation processors,
|
||||
* and route listeners. Never call {@link FlashContext#require} here.</li>
|
||||
* <li>Context resolution — {@link FlashContext#resolveAll()} performs topological
|
||||
* resolution of lazy suppliers. Circular or missing dependencies fail here with
|
||||
* a clear message before any request is served.</li>
|
||||
* <li><b>Routes phase</b> — {@link #routes(FlashRegistrar, FlashContext)} is called for
|
||||
* all extensions. All services are resolved; {@link FlashContext#require} is safe.</li>
|
||||
* </ol>
|
||||
*
|
||||
* <h3>Priority and middleware ordering</h3>
|
||||
* {@link #priority()} controls the order annotation processors are registered, which
|
||||
* determines the annotation-layer middleware chain position:
|
||||
* <pre>
|
||||
* Request ──► EARLY processors' mw ──► DEFAULT processors' mw ──► LATE processors' mw ──► handler
|
||||
* </pre>
|
||||
* Use {@link ExtensionPhase} constants for semantic ordering:
|
||||
* <pre>{@code
|
||||
* @Override public int priority() { return ExtensionPhase.EARLY.value; }
|
||||
* }</pre>
|
||||
*
|
||||
* <h3>Example</h3>
|
||||
* <pre>{@code
|
||||
* public class MetricsExtension implements FlashExtension {
|
||||
*
|
||||
* @Override public int priority() { return ExtensionPhase.LATE.value; }
|
||||
*
|
||||
* @Override
|
||||
* public void provide(FlashContext ctx) {
|
||||
* ctx.provide(MetricsRegistry.class, new PromMetricsRegistry());
|
||||
* }
|
||||
*
|
||||
* @Override
|
||||
* public void routes(FlashRegistrar<?> app, FlashContext ctx) {
|
||||
* app.get("/metrics", (req, res) -> ctx.require(MetricsRegistry.class).scrape());
|
||||
* }
|
||||
* }
|
||||
* }</pre>
|
||||
* <p>Extensions never control lifecycle ordering. During {@link #configure}, they declare
|
||||
* services, processors, listeners and ready callbacks. Flash closes declarations, validates and
|
||||
* resolves the complete service graph, then executes ready callbacks to materialise routes.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface FlashExtension {
|
||||
|
||||
/**
|
||||
* Phase 1 — register services and processors.
|
||||
* Safe: {@link FlashContext#provide}, {@link FlashContext#supply},
|
||||
* {@link FlashContext#addAnnotationProcessor}, {@link FlashContext#addRouteListener}.
|
||||
* Unsafe: {@link FlashContext#require} (services not yet resolved).
|
||||
* Declares this extension's contribution. {@code ctx.require(...)} is intentionally illegal
|
||||
* here: work needing resolved services belongs in {@link FlashContext#onReady(Runnable)}.
|
||||
*/
|
||||
default void provide(FlashContext ctx) {}
|
||||
|
||||
/**
|
||||
* Phase 2 — register routes. All services are fully resolved.
|
||||
* {@link FlashContext#require} is safe here.
|
||||
*/
|
||||
default void routes(FlashRegistrar<?> app, FlashContext ctx) {}
|
||||
|
||||
/**
|
||||
* Execution priority. Lower = earlier in the annotation middleware chain.
|
||||
* Tie-breaking: same value → install order (sort is stable).
|
||||
* Default: {@link ExtensionPhase#DEFAULT} (500).
|
||||
*/
|
||||
default int priority() { return ExtensionPhase.DEFAULT.value; }
|
||||
void configure(FlashRegistrar<?> app, FlashContext ctx);
|
||||
}
|
||||
|
||||
@@ -6,11 +6,14 @@ import dev.relism.flash.models.RequestHandler;
|
||||
import dev.relism.flash.models.SimpleHandler;
|
||||
import dev.relism.flash.websocket.WebSocketEndpoint;
|
||||
import dev.relism.flash.routing.Middleware;
|
||||
import dev.relism.flash.routing.MiddlewareKey;
|
||||
import dev.relism.flash.routing.MiddlewareNode;
|
||||
import dev.relism.flash.routing.Route;
|
||||
import dev.relism.flash.routing.Routes;
|
||||
import dev.relism.flash.routing.Ws;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* Common route-registration surface shared by {@link FlashApp} and {@link FlashScope}.
|
||||
@@ -24,14 +27,15 @@ import java.util.List;
|
||||
* app.get("/admin", handler, oidc.requireRole("admin"), rateLimiter)
|
||||
* }</pre>
|
||||
*
|
||||
* <p>{@link FlashExtension#routes} receives a {@code FlashRegistrar<?>} for route
|
||||
* registration. Extension installation ({@code install()}) is only available on
|
||||
* <p>Extensions receive a {@link FlashApp} during their single configure declaration.
|
||||
* Extension installation ({@code install()}) is only available on
|
||||
* {@link FlashApp} — scoped install is intentionally unsupported.
|
||||
*
|
||||
* @param <SELF> concrete registrar type — enables fluent chaining without casting
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public abstract class FlashRegistrar<SELF extends FlashRegistrar<SELF>> {
|
||||
private static final AtomicLong INLINE_KEYS = new AtomicLong();
|
||||
|
||||
// ── HTTP method registration ──────────────────────────────────────────────
|
||||
|
||||
@@ -47,6 +51,18 @@ public abstract class FlashRegistrar<SELF extends FlashRegistrar<SELF>> {
|
||||
public final SELF purge (String path, SimpleHandler.FunctionalHandler h, Middleware... mw) { return route(HttpMethod.PURGE, path, h, mw); }
|
||||
public final SELF query (String path, SimpleHandler.FunctionalHandler h, Middleware... mw) { return route(HttpMethod.QUERY, path, h, mw); }
|
||||
|
||||
public final SELF getWith (String path, SimpleHandler.FunctionalHandler h, MiddlewareNode... mw) { return route(HttpMethod.GET, path, h, mw); }
|
||||
public final SELF postWith (String path, SimpleHandler.FunctionalHandler h, MiddlewareNode... mw) { return route(HttpMethod.POST, path, h, mw); }
|
||||
public final SELF putWith (String path, SimpleHandler.FunctionalHandler h, MiddlewareNode... mw) { return route(HttpMethod.PUT, path, h, mw); }
|
||||
public final SELF deleteWith (String path, SimpleHandler.FunctionalHandler h, MiddlewareNode... mw) { return route(HttpMethod.DELETE, path, h, mw); }
|
||||
public final SELF patchWith (String path, SimpleHandler.FunctionalHandler h, MiddlewareNode... mw) { return route(HttpMethod.PATCH, path, h, mw); }
|
||||
public final SELF optionsWith(String path, SimpleHandler.FunctionalHandler h, MiddlewareNode... mw) { return route(HttpMethod.OPTIONS, path, h, mw); }
|
||||
public final SELF headWith (String path, SimpleHandler.FunctionalHandler h, MiddlewareNode... mw) { return route(HttpMethod.HEAD, path, h, mw); }
|
||||
public final SELF traceWith (String path, SimpleHandler.FunctionalHandler h, MiddlewareNode... mw) { return route(HttpMethod.TRACE, path, h, mw); }
|
||||
public final SELF connectWith(String path, SimpleHandler.FunctionalHandler h, MiddlewareNode... mw) { return route(HttpMethod.CONNECT, path, h, mw); }
|
||||
public final SELF purgeWith (String path, SimpleHandler.FunctionalHandler h, MiddlewareNode... mw) { return route(HttpMethod.PURGE, path, h, mw); }
|
||||
public final SELF queryWith (String path, SimpleHandler.FunctionalHandler h, MiddlewareNode... mw) { return route(HttpMethod.QUERY, path, h, mw); }
|
||||
|
||||
// ── Middleware ────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -56,7 +72,13 @@ public abstract class FlashRegistrar<SELF extends FlashRegistrar<SELF>> {
|
||||
* Order-independent: middleware is resolved at {@link FlashApp#start()}.
|
||||
*/
|
||||
public final SELF use(Middleware... middlewares) {
|
||||
for (Middleware m : middlewares) addMiddleware(m);
|
||||
for (Middleware m : middlewares) addMiddleware(inline(m));
|
||||
return (SELF) this;
|
||||
}
|
||||
|
||||
/** Adds named middleware nodes whose ordering constraints are compiled at boot. */
|
||||
public final SELF use(MiddlewareNode... middlewares) {
|
||||
for (MiddlewareNode m : middlewares) addMiddleware(m);
|
||||
return (SELF) this;
|
||||
}
|
||||
|
||||
@@ -93,18 +115,29 @@ public abstract class FlashRegistrar<SELF extends FlashRegistrar<SELF>> {
|
||||
* Subclasses may prepend a namespace prefix and inject scope middlewares before storing.
|
||||
*/
|
||||
protected abstract void addRoute(HttpMethod method, String path,
|
||||
RequestHandler handler, List<Middleware> mw);
|
||||
RequestHandler handler, List<MiddlewareNode> mw);
|
||||
|
||||
protected abstract void addWsRoute(String path, WebSocketEndpoint endpoint);
|
||||
|
||||
/** Registers a middleware in this registrar's own scope (global or scope-level). */
|
||||
protected abstract void addMiddleware(Middleware mw);
|
||||
protected abstract void addMiddleware(MiddlewareNode mw);
|
||||
|
||||
private SELF route(HttpMethod method, String path, SimpleHandler.FunctionalHandler h, Middleware[] mw) {
|
||||
MiddlewareNode[] nodes = new MiddlewareNode[mw.length];
|
||||
for (int i = 0; i < mw.length; i++) nodes[i] = inline(mw[i]);
|
||||
addRoute(method, path, new SimpleHandler(h), mw.length == 0 ? List.of() : List.of(nodes));
|
||||
return (SELF) this;
|
||||
}
|
||||
|
||||
private SELF route(HttpMethod method, String path, SimpleHandler.FunctionalHandler h, MiddlewareNode[] mw) {
|
||||
addRoute(method, path, new SimpleHandler(h), mw.length == 0 ? List.of() : List.of(mw));
|
||||
return (SELF) this;
|
||||
}
|
||||
|
||||
private static MiddlewareNode inline(Middleware middleware) {
|
||||
return MiddlewareNode.of(MiddlewareKey.of("flash.inline." + INLINE_KEYS.incrementAndGet()), middleware);
|
||||
}
|
||||
|
||||
protected static RequestHandler instantiate(Class<?> cls) {
|
||||
try { return (RequestHandler) cls.getDeclaredConstructor().newInstance(); }
|
||||
catch (Exception e) {
|
||||
|
||||
@@ -3,7 +3,7 @@ package dev.relism.flash.extension;
|
||||
import dev.relism.flash.http.HttpMethod;
|
||||
import dev.relism.flash.models.RequestHandler;
|
||||
import dev.relism.flash.models.SimpleHandler;
|
||||
import dev.relism.flash.routing.Middleware;
|
||||
import dev.relism.flash.routing.MiddlewareNode;
|
||||
import dev.relism.flash.routing.PathUtils;
|
||||
import dev.relism.flash.websocket.WebSocketEndpoint;
|
||||
|
||||
@@ -28,7 +28,7 @@ public final class FlashScope extends FlashRegistrar<FlashScope> {
|
||||
|
||||
private final String namespace;
|
||||
private final FlashContext ctx;
|
||||
private final List<Middleware> scopeMiddlewares = new ArrayList<>();
|
||||
private final List<MiddlewareNode> scopeMiddlewares = new ArrayList<>();
|
||||
private final List<RouteDefinition> deferredRoutes = new ArrayList<>();
|
||||
private final List<WsRouteDefinition> deferredWsRoutes = new ArrayList<>();
|
||||
|
||||
@@ -44,8 +44,8 @@ public final class FlashScope extends FlashRegistrar<FlashScope> {
|
||||
// ── FlashRegistrar impl ───────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
protected void addRoute(HttpMethod method, String path, RequestHandler handler, List<Middleware> mw) {
|
||||
List<Middleware> scopeMw = scopeMiddlewares.isEmpty() ? List.of() : List.copyOf(scopeMiddlewares);
|
||||
protected void addRoute(HttpMethod method, String path, RequestHandler handler, List<MiddlewareNode> mw) {
|
||||
List<MiddlewareNode> scopeMw = scopeMiddlewares.isEmpty() ? List.of() : List.copyOf(scopeMiddlewares);
|
||||
deferredRoutes.add(new RouteDefinition(
|
||||
method, ns(path), handler, scopeMw, mw,
|
||||
!(handler instanceof SimpleHandler), ctx, namespace));
|
||||
@@ -57,7 +57,7 @@ public final class FlashScope extends FlashRegistrar<FlashScope> {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addMiddleware(Middleware mw) { scopeMiddlewares.add(mw); }
|
||||
protected void addMiddleware(MiddlewareNode mw) { scopeMiddlewares.add(mw); }
|
||||
|
||||
// ── Internal (called by FlashApp.mount) ───────────────────────────────────
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ package dev.relism.flash.extension;
|
||||
|
||||
import dev.relism.flash.http.HttpMethod;
|
||||
import dev.relism.flash.models.RequestHandler;
|
||||
import dev.relism.flash.routing.Middleware;
|
||||
import dev.relism.flash.routing.MiddlewareNode;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -26,8 +26,8 @@ record RouteDefinition(
|
||||
HttpMethod method,
|
||||
String path,
|
||||
RequestHandler handler,
|
||||
List<Middleware> scopeMiddlewares,
|
||||
List<Middleware> explicitMiddlewares,
|
||||
List<MiddlewareNode> scopeMiddlewares,
|
||||
List<MiddlewareNode> explicitMiddlewares,
|
||||
boolean classBasedHandler,
|
||||
FlashContext ctx,
|
||||
String namespace
|
||||
|
||||
@@ -32,6 +32,9 @@ import java.nio.charset.StandardCharsets;
|
||||
*/
|
||||
public abstract class AbstractRouter {
|
||||
|
||||
/** Eagerly validates and compiles this route graph before traffic is accepted. */
|
||||
public void compile() {}
|
||||
|
||||
// Pre-encoded prod JSON error bodies — zero allocation on error paths.
|
||||
private static final byte[] JSON_404 = "{\"error\":\"Not Found\",\"status\":404}"
|
||||
.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
@@ -8,6 +8,9 @@ import dev.relism.flash.websocket.WebSocketHandler;
|
||||
|
||||
public abstract class AbstractWsRouter {
|
||||
|
||||
/** Eagerly validates and compiles this WebSocket route graph before traffic is accepted. */
|
||||
public void compile() {}
|
||||
|
||||
public final AbstractWsRouter register(HttpMethod method, String path, WebSocketHandler handler) {
|
||||
return addRoute(method, PathUtils.sanitize(path), handler);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package dev.relism.flash.routing;
|
||||
|
||||
import dev.relism.flash.models.RequestHandler;
|
||||
import dev.relism.flash.models.SimpleHandler;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/** Boot-only DAG compiler for a route's middleware nodes. */
|
||||
public final class MiddlewareGraph {
|
||||
private MiddlewareGraph() {}
|
||||
|
||||
public static Middleware[] order(String route, List<MiddlewareNode> nodes) {
|
||||
if (nodes.isEmpty()) return new Middleware[0];
|
||||
Map<MiddlewareKey, Integer> index = new LinkedHashMap<>();
|
||||
for (int i = 0; i < nodes.size(); i++) {
|
||||
MiddlewareKey key = nodes.get(i).key();
|
||||
if (index.putIfAbsent(key, i) != null)
|
||||
throw new IllegalStateException("Duplicate middleware " + key.value() + " on " + route);
|
||||
}
|
||||
List<Set<Integer>> outgoing = new ArrayList<>(nodes.size());
|
||||
int[] incoming = new int[nodes.size()];
|
||||
for (int i = 0; i < nodes.size(); i++) outgoing.add(new LinkedHashSet<>());
|
||||
for (int source = 0; source < nodes.size(); source++) {
|
||||
for (MiddlewareNode.Constraint c : nodes.get(source).constraints()) {
|
||||
Integer target = index.get(c.target());
|
||||
if (target == null) {
|
||||
if (c.required()) throw new IllegalStateException("Middleware " + nodes.get(source).key().value()
|
||||
+ " on " + route + " requires " + c.target().value() + " to be present");
|
||||
continue;
|
||||
}
|
||||
int from = c.relation() == MiddlewareNode.Relation.AFTER ? target : source;
|
||||
int to = c.relation() == MiddlewareNode.Relation.AFTER ? source : target;
|
||||
if (outgoing.get(from).add(to)) incoming[to]++;
|
||||
}
|
||||
}
|
||||
PriorityQueue<Integer> ready = new PriorityQueue<>();
|
||||
for (int i = 0; i < incoming.length; i++) if (incoming[i] == 0) ready.add(i);
|
||||
Middleware[] ordered = new Middleware[nodes.size()];
|
||||
int out = 0;
|
||||
while (!ready.isEmpty()) {
|
||||
int current = ready.remove();
|
||||
ordered[out++] = nodes.get(current).middleware();
|
||||
for (int next : outgoing.get(current)) if (--incoming[next] == 0) ready.add(next);
|
||||
}
|
||||
if (out != nodes.size()) throw new IllegalStateException("Middleware ordering cycle on " + route);
|
||||
return ordered;
|
||||
}
|
||||
|
||||
/** Pre-composes a sorted chain once at boot. */
|
||||
public static RequestHandler compose(RequestHandler handler, Middleware[] ordered) {
|
||||
RequestHandler current = handler;
|
||||
for (int i = ordered.length - 1; i >= 0; i--) current = new SimpleHandler(ordered[i].wrap(current));
|
||||
return current;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package dev.relism.flash.routing;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/** Stable boot-time identity of a middleware node. Never consulted while handling a request. */
|
||||
public record MiddlewareKey(String value) {
|
||||
public MiddlewareKey {
|
||||
if (value == null || value.isBlank()) throw new IllegalArgumentException("Middleware key cannot be blank");
|
||||
}
|
||||
public static MiddlewareKey of(String value) { return new MiddlewareKey(value); }
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package dev.relism.flash.routing;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* A named middleware contribution and its ordering constraints.
|
||||
*
|
||||
* <p>Constraints are resolved only while Flash compiles a route. The resulting handler chain
|
||||
* contains no keys, graphs, ordering checks or additional request-path allocations.
|
||||
*/
|
||||
public final class MiddlewareNode {
|
||||
private final MiddlewareKey key;
|
||||
private final Middleware middleware;
|
||||
private final List<Constraint> constraints = new ArrayList<>();
|
||||
|
||||
private MiddlewareNode(MiddlewareKey key, Middleware middleware) {
|
||||
this.key = Objects.requireNonNull(key, "key");
|
||||
this.middleware = Objects.requireNonNull(middleware, "middleware");
|
||||
}
|
||||
|
||||
public static MiddlewareNode of(MiddlewareKey key, Middleware middleware) { return new MiddlewareNode(key, middleware); }
|
||||
public MiddlewareNode after(MiddlewareKey key) { constraints.add(new Constraint(key, Relation.AFTER, true)); return this; }
|
||||
public MiddlewareNode afterIfPresent(MiddlewareKey key) { constraints.add(new Constraint(key, Relation.AFTER, false)); return this; }
|
||||
public MiddlewareNode before(MiddlewareKey key) { constraints.add(new Constraint(key, Relation.BEFORE, true)); return this; }
|
||||
public MiddlewareNode beforeIfPresent(MiddlewareKey key) { constraints.add(new Constraint(key, Relation.BEFORE, false)); return this; }
|
||||
|
||||
public MiddlewareKey key() { return key; }
|
||||
Middleware middleware() { return middleware; }
|
||||
List<Constraint> constraints() { return List.copyOf(constraints); }
|
||||
|
||||
enum Relation { BEFORE, AFTER }
|
||||
record Constraint(MiddlewareKey target, Relation relation, boolean required) {
|
||||
Constraint { Objects.requireNonNull(target, "target"); }
|
||||
}
|
||||
}
|
||||
+3
@@ -92,4 +92,7 @@ public class FastPathRouterImpl extends AbstractRouter {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void compile() { ensureCompiled(); }
|
||||
}
|
||||
|
||||
+3
@@ -56,6 +56,9 @@ public final class FastPathWsRouterImpl extends AbstractWsRouter {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void compile() { ensureCompiled(); }
|
||||
|
||||
private static final class Context {
|
||||
private static final ThreadLocal<MatchResult<WebSocketHandler>> RESULT =
|
||||
ThreadLocal.withInitial(() -> new MatchResult<>(32, 128));
|
||||
|
||||
Reference in New Issue
Block a user