Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -692,13 +692,30 @@ export interface RouteHandler {
module?: string;
}

/**
* Complete registered route handler and its identifier.
*/
export interface RegisteredRouteHandler {
id: string;
handler: RouteHandler;
}

/**
* Registered route handlers indexed by their proxy id, mirroring the entries
* held by {@link routesProxy}. Pruned by {@link RoutesProxy} so stale handlers
* do not accumulate across module reloads.
*/
const routesList = new Map<string, RouteHandler>();

/**
* Retrieves complete registered route handlers.
*
* @returns Snapshot of the current route registry.
*/
export function getRegisteredRouteHandlers(): RegisteredRouteHandler[] {
return Array.from(routesList, ([id, handler]) => ({ id, handler }));
}

/**
* RegisteringProxy that also prunes {@link routesList} on the same lifecycle
* events that remove the real routes (single unregister or module unload), so
Expand Down
106 changes: 102 additions & 4 deletions src/tests/route-lifecycle.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
import assert from "node:assert";
import {
type ComputedParameter,
getRegisteredRouteHandlers,
getRegisteredRoutes,
HandlerPriority,
RegisterRoute,
type RouteHandler,
routesProxy,
UnregisterRoute,
} from "../index";

interface ProxyCall {
handler?: RouteHandler;
id: string;
kind: "register" | "unregister";
}

const proxyCalls: ProxyCall[] = [];

function handlerAt(location: string): RouteHandler {
return {
location,
Expand All @@ -19,6 +30,25 @@ function handlerAt(location: string): RouteHandler {
} as unknown as RouteHandler;
}

function completeHandlerAt(location: string): RouteHandler {
const parameter: ComputedParameter = {
provider: () => "parameter",
modifiers: [],
};
return {
location,
method: "POST",
mode: "prefix",
parameters: [parameter, null],
properties: { property: parameter },
proto: { controller: location },
callback: function completeRouteCallback() {
return location;
},
priority: HandlerPriority.HIGH,
};
}

function routesAt(location: string): number {
return getRegisteredRoutes().filter((route) => route.location === location)
.length;
Expand All @@ -29,11 +59,9 @@ describe("Route lifecycle", () => {
// test harness (the api module binds the harness-distributed copy). A
// recording provider both keeps the stub-mode proxy from throwing and lets
// the cases assert what reaches the real registry.
const proxyCalls: Array<{ kind: "register" | "unregister"; id: string }> = [];

before(() => {
routesProxy.onRegister((id: string) => {
proxyCalls.push({ kind: "register", id });
routesProxy.onRegister((id: string, handler: RouteHandler) => {
proxyCalls.push({ kind: "register", id, handler });
}, true);
routesProxy.onUnregister((id: string) => {
proxyCalls.push({ kind: "unregister", id });
Expand Down Expand Up @@ -81,3 +109,73 @@ describe("Route lifecycle", () => {
assert.equal(getRegisteredRoutes().length, before);
});
});

describe("getRegisteredRouteHandlers", () => {
const routeIds: number[] = [];

function register(handler: RouteHandler): number {
const id = RegisterRoute(handler);
routeIds.push(id);
return id;
}

afterEach(() => {
for (const id of routeIds.splice(0)) {
UnregisterRoute(id);
}
});

it("returns complete registered handlers without replacing the provider", () => {
const original = completeHandlerAt("/handlers/complete");
const id = register(original);

const registered = getRegisteredRouteHandlers().find(
(registered) => registered.id === id.toString(),
);
assert(registered);
assert.equal(registered.handler.mode, original.mode);
assert.strictEqual(registered.handler.callback, original.callback);
assert.strictEqual(registered.handler.proto, original.proto);
assert.strictEqual(registered.handler.parameters, original.parameters);
assert.strictEqual(registered.handler.properties, original.properties);
assert.equal(registered.handler.priority, original.priority);
assert(Object.hasOwn(registered.handler, "module"));
assert.equal(typeof registered.handler.module, "string");
const providerCall = proxyCalls.at(-1);
assert.equal(providerCall?.kind, "register");
assert.equal(providerCall?.id, id.toString());
assert.strictEqual(providerCall?.handler, registered.handler);
assert.equal(Object.hasOwn(original, "module"), false);
});

it("excludes explicitly unregistered routes", () => {
const id = register(handlerAt("/handlers/unregister"));

UnregisterRoute(id);

assert.equal(
getRegisteredRouteHandlers().some(
(registered) => registered.id === id.toString(),
),
false,
);
});

it("excludes routes removed during module unload", () => {
const id = register(handlerAt("/handlers/module-unload"));
const registered = getRegisteredRouteHandlers().find(
(entry) => entry.id === id.toString(),
);
assert(registered);
const module = registered.handler.module;
assert(module);

routesProxy.unregisterModule(module);

assert.equal(
getRegisteredRouteHandlers().some((entry) => entry.id === id.toString()),
false,
);
assert.equal(routesAt("/handlers/module-unload"), 0);
});
});
Loading