Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
ad8d387
Initialize @fedify/uri-template package
2chanhaeng Apr 27, 2026
4b2e287
Update version of @fedify/uri-template
2chanhaeng Apr 28, 2026
6429a2c
Add test for template
2chanhaeng Apr 28, 2026
99a5854
Allow **/*.bench.ts files use `@fedfiy/fixture`
2chanhaeng May 1, 2026
b3772de
Implement RFC 6570 URI template expansion
2chanhaeng May 1, 2026
bb2890b
Add test suite for @fedify/uri-template Template
2chanhaeng May 1, 2026
cbd2358
Report URI template expansion errors
2chanhaeng May 2, 2026
94fff72
Document URI template compatibility
2chanhaeng May 2, 2026
075448d
Refine URI template expansion internals
2chanhaeng May 4, 2026
93481fd
Add URI template matching
2chanhaeng May 4, 2026
0e12d9a
Add test suite for URI template matching
2chanhaeng May 4, 2026
924cb27
Add RFC 6570 Router class
2chanhaeng May 6, 2026
0aeb340
Add Router conformance tests
2chanhaeng May 6, 2026
1d38ae6
Capture legacy Router failures
2chanhaeng May 6, 2026
1fd221e
Reorganize uri-template into per-feature module layouts
2chanhaeng May 6, 2026
31a1657
Rename "symmetric" to "round-trip" in user-facing prose
2chanhaeng May 6, 2026
011363d
Rename router memory pressure scenario factories
2chanhaeng May 6, 2026
85f8a2d
Document operator behavior table and drop dead constant
2chanhaeng May 6, 2026
aadf9ec
Drop unused VariableSpec and align reporter docs with expansion
2chanhaeng May 6, 2026
a3a12b7
Document route-shape gaps in uri-template-router compatibility test
2chanhaeng May 6, 2026
3e973af
Drop "using RegExp" qualifier from Template bench label
2chanhaeng May 6, 2026
cd7bdc0
Add Router#register, batch trie insert, and constructor routes
2chanhaeng May 7, 2026
161e7e3
Move URI template old tests
2chanhaeng May 8, 2026
e0c1965
Group uri-template tests by suite via nested t.step
2chanhaeng May 8, 2026
4e76477
Export `isExpression` from @fedify/uri-template
2chanhaeng May 8, 2026
2822715
Migrate @fedify/fedify to @fedify/uri-template router
2chanhaeng May 8, 2026
b9a54ea
Note uri-template router migration in CHANGES.md
2chanhaeng May 8, 2026
f06d731
Export isPath helper from @fedify/uri-template
2chanhaeng May 8, 2026
c966c68
Tighten match backtracking bounds in @fedify/uri-template
2chanhaeng May 8, 2026
4579d54
Replace match bench with backtracking-pressure cases
2chanhaeng May 8, 2026
96f4bed
Add PR link
2chanhaeng May 9, 2026
ecaa080
Export `assertPath` from @fedify/uri-template
2chanhaeng May 9, 2026
c0f41b5
Replace `Router` from @fedify/fedify with the wrapper of `Router` fro…
2chanhaeng May 9, 2026
72a8945
Apply review feedback from PR #758
2chanhaeng May 9, 2026
e335fce
Fix `consumeUnnamed` over-pruning of valid match decompositions
2chanhaeng May 9, 2026
2f83766
Fix `defaultReporter`
2chanhaeng May 11, 2026
b5157fe
Tighten identifier path validation in FederationBuilder
2chanhaeng May 11, 2026
0b0d828
Accept the empty path so trailing-slash retry can match `/`
2chanhaeng May 11, 2026
6b00559
Polish naming and fixture comments in uri-template tests
2chanhaeng May 11, 2026
4088f51
Centralize expression parse error reporting in the tokenizer
2chanhaeng May 11, 2026
b920d1b
Drop stale router trie entries on re-registration
2chanhaeng May 11, 2026
a8c8c33
Serialize Deno check tasks behind install in `mise test`
2chanhaeng May 11, 2026
8af5855
Make @fedify/uri-template default reporter a no-op
2chanhaeng May 11, 2026
a6afe59
Recover round-trip for associative keys with non-varname characters
2chanhaeng May 11, 2026
37b158a
Split identifier path validation into loose and strict asserts
2chanhaeng May 12, 2026
9e97462
Exclude old/ compat tests from published @fedify/uri-template
2chanhaeng May 12, 2026
12ef49d
Reuse PrioritizedRouteEntry as the Trie entry constraint
2chanhaeng May 12, 2026
2af7cc9
Drop unused throw() helper from TemplateParseError
2chanhaeng May 12, 2026
8ea3dd3
Note compat Router shim breaking changes in changelog
2chanhaeng May 12, 2026
1cce8df
Index router trie at the token level to share dynamic prefixes
2chanhaeng May 14, 2026
89e51c4
Refactor router trie traversals with fold helpers
2chanhaeng May 14, 2026
c9187c8
Fix lockfile
2chanhaeng May 14, 2026
7ffac1b
Remove unnecessary `as Path`
2chanhaeng May 15, 2026
4cd8125
Make Router#register() failure-atomic
2chanhaeng May 15, 2026
80f6b04
Update stale router design in uri-template README
2chanhaeng May 15, 2026
0db4e14
Make registered router patterns immutable
2chanhaeng May 15, 2026
c9e8fc9
Reject path-style {/identifier} identifier routes
2chanhaeng May 16, 2026
4215d05
Forward trailingSlashInsensitive in deprecated Router
2chanhaeng May 16, 2026
9b32a4e
Keep deprecated RouterError instanceof working
2chanhaeng May 16, 2026
715ada6
Keep deprecated Router.route() a nullable probe
2chanhaeng May 16, 2026
233d170
Drop Breaking Change note from changelog
2chanhaeng May 16, 2026
6dbb834
Log deprecated Router warning once per process
2chanhaeng May 16, 2026
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
14 changes: 14 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,13 @@ To be released.
operators distinguish a slow-draining queue from a queue that sees
less traffic. [[#316], [#740], [#759]]

- Replaced Fedify's internal federation routing with
*@fedify/uri-template* for stricter RFC 6570 URI Template expansion and
matching. The deprecated `Router` export from *@fedify/fedify* remains
available for compatibility. [[#418], [#758] by ChanHaeng Lee]

[#316]: https://github.com/fedify-dev/fedify/issues/316
[#418]: https://github.com/fedify-dev/fedify/issues/418
[#619]: https://github.com/fedify-dev/fedify/issues/619
[#735]: https://github.com/fedify-dev/fedify/issues/735
[#736]: https://github.com/fedify-dev/fedify/issues/736
Expand All @@ -102,6 +108,7 @@ To be released.
[#753]: https://github.com/fedify-dev/fedify/pull/753
[#755]: https://github.com/fedify-dev/fedify/pull/755
[#757]: https://github.com/fedify-dev/fedify/pull/757
[#758]: https://github.com/fedify-dev/fedify/pull/758
[#759]: https://github.com/fedify-dev/fedify/pull/759
[#769]: https://github.com/fedify-dev/fedify/pull/769

Expand All @@ -116,6 +123,13 @@ To be released.
- Added a `meterProvider` option to `createFederation()` so mock contexts can
expose a test OpenTelemetry meter provider. [[#316], [#619], [#755]]

### @fedify/uri-template

- Added *@fedify/uri-template*, a dependency-free RFC 6570 URI Template
implementation for expansion, variable extraction, and round-trip route
matching. This package replaces Fedify's previous direct use of
*url-template* and *uri-template-router*. [[#418], [#758] by ChanHaeng Lee]

### @fedify/amqp

- Added `AmqpMessageQueue.getDepth()` for reporting queued, ready, and
Expand Down
5 changes: 5 additions & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@
"urlpattern",
"uuidv7",
"valueparser",
"varspec",
"varname",
"varnames",
"varchar",
"varchars",
"Vinxi",
"vitepress",
"vtsls",
Expand Down
1 change: 1 addition & 0 deletions deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"./packages/sqlite",
"./packages/sveltekit",
"./packages/testing",
"./packages/uri-template",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"./packages/vocab",
"./packages/vocab-runtime",
"./packages/vocab-tools",
Expand Down
6 changes: 1 addition & 5 deletions deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,17 @@ depends = [

[tasks."check:fmt"]
description = "Check code formatting"
wait_for = ["install"]
run = "deno fmt --check"

[tasks."check:lint"]
description = "Check code linting"
wait_for = ["install"]
run = "deno lint"

[tasks."check:types"]
description = "Check TypeScript types"
wait_for = ["install"]
run = "deno check $(deno eval 'import m from \"./deno.json\" with { type: \"json\" }; for (let p of m.workspace) console.log(p)')"

[tasks."check:md"]
Expand All @@ -69,6 +72,7 @@ run = "hongdown --check"
[tasks.check-versions]
description = "Check that all package versions are consistent across the monorepo"
usage = 'flag "--fix" help="Automatically fix version mismatches"'
wait_for = ["install"]
run = '''
if [ "${usage_fix}" = "true" ]; then
deno run --allow-read --allow-write scripts/check_versions.ts --fix
Expand All @@ -79,6 +83,7 @@ fi

[tasks."check:fixture-usage"]
description = "Ensure @fedify/fixture is only used in **/*.test.ts files"
wait_for = ["install"]
run = "deno run --allow-read scripts/check_fixture_usage.ts"

[tasks."check:manifest:workspace-protocol"]
Expand Down
3 changes: 3 additions & 0 deletions packages/fedify/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ Here is the list of packages:
| [@fedify/sqlite](/packages/sqlite/) | [JSR][jsr:@fedify/sqlite] | [npm][npm:@fedify/sqlite] | SQLite driver |
| [@fedify/sveltekit](/packages/sveltekit/) | [JSR][jsr:@fedify/sveltekit] | [npm][npm:@fedify/sveltekit] | SvelteKit integration |
| [@fedify/testing](/packages/testing/) | [JSR][jsr:@fedify/testing] | [npm][npm:@fedify/testing] | Testing utilities |
| [@fedify/uri-template](/packages/uri-template/) | [JSR][jsr:@fedify/uri-template] | [npm][npm:@fedify/uri-template] | RFC 6570 URI Template library |
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this PR adds @fedify/uri-template as a public package, I think the docs site should be updated too, not only this package README. docs/manual/uri-template.md is still written mainly as a Fedify routing guide, and it does not show standalone Template/Router usage, strict/report behavior, or round-trip matching. A short section there that links the package API to the existing Fedify routing guide would make the new package discoverable from fedify.dev.

| [@fedify/vocab](/packages/vocab/) | [JSR][jsr:@fedify/vocab] | [npm][npm:@fedify/vocab] | Activity Vocabulary library |
| [@fedify/vocab-runtime](/packages/vocab-runtime/) | [JSR][jsr:@fedify/vocab-runtime] | [npm][npm:@fedify/vocab-runtime] | Runtime library for code-generated vocab |
| [@fedify/vocab-tools](/packages/vocab-tools/) | [JSR][jsr:@fedify/vocab-tools] | [npm][npm:@fedify/vocab-tools] | Code generation tools for Activity Vocab |
Expand Down Expand Up @@ -176,6 +177,8 @@ Here is the list of packages:
[npm:@fedify/sveltekit]: https://www.npmjs.com/package/@fedify/sveltekit
[jsr:@fedify/testing]: https://jsr.io/@fedify/testing
[npm:@fedify/testing]: https://www.npmjs.com/package/@fedify/testing
[jsr:@fedify/uri-template]: https://jsr.io/@fedify/uri-template
[npm:@fedify/uri-template]: https://www.npmjs.com/package/@fedify/uri-template
[jsr:@fedify/vocab]: https://jsr.io/@fedify/vocab
[npm:@fedify/vocab]: https://www.npmjs.com/package/@fedify/vocab
[jsr:@fedify/vocab-runtime]: https://jsr.io/@fedify/vocab-runtime
Expand Down
4 changes: 1 addition & 3 deletions packages/fedify/deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@
"json-canon": "npm:json-canon@^1.0.1",
"jsonld": "npm:jsonld@^9.0.0",
"pkijs": "npm:pkijs@^3.3.3",
"structured-field-values": "npm:structured-field-values@^2.0.4",
"uri-template-router": "npm:uri-template-router@^1.0.0",
"url-template": "npm:url-template@^3.1.1"
"structured-field-values": "npm:structured-field-values@^2.0.4"
},
"exclude": [
".test-report.xml",
Expand Down
3 changes: 1 addition & 2 deletions packages/fedify/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@
}
},
"dependencies": {
"@fedify/uri-template": "workspace:*",
"@fedify/vocab": "workspace:*",
"@fedify/vocab-runtime": "workspace:*",
"@fedify/webfinger": "workspace:*",
Expand All @@ -153,8 +154,6 @@
"json-canon": "^1.0.1",
"jsonld": "^9.0.0",
"structured-field-values": "^2.0.4",
"uri-template-router": "^1.0.0",
"url-template": "^3.1.1",
"urlpattern-polyfill": "catalog:"
},
"devDependencies": {
Expand Down
131 changes: 129 additions & 2 deletions packages/fedify/src/federation/builder.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { test } from "@fedify/fixture";
import { RouterError } from "@fedify/uri-template";
import { Activity, Note, Person } from "@fedify/vocab";
import { assertEquals, assertExists, assertThrows } from "@std/assert";
import type { Protocol } from "../nodeinfo/types.ts";
Expand All @@ -13,7 +14,6 @@ import type {
} from "./callback.ts";
import { MemoryKvStore } from "./kv.ts";
import type { FederationImpl } from "./middleware.ts";
import { RouterError } from "./router.ts";

test("FederationBuilder", async (t) => {
await t.step(
Expand Down Expand Up @@ -160,6 +160,31 @@ test("FederationBuilder", async (t) => {
},
);

await t.step("should snapshot router state on build", async () => {
const builder = createFederationBuilder<void>();
const kv = new MemoryKvStore();
const noteRouteName = `object:${Note.typeId.href}`;

builder.setActorDispatcher("/users/{identifier}", () => null);
const federation1 = await builder.build({ kv });
const impl1 = federation1 as FederationImpl<void>;

builder.setObjectDispatcher(Note, "/notes/{id}", () => null);
assertEquals(impl1.router.route("/notes/1"), null);

const federation2 = await builder.build({ kv });
const impl2 = federation2 as FederationImpl<void>;
assertEquals(impl2.router.route("/notes/1")?.name, noteRouteName);

impl1.router.add("/leaked/{id}", "leaked");
assertEquals(impl1.router.route("/leaked/1")?.name, "leaked");
assertEquals(impl2.router.route("/leaked/1"), null);

const federation3 = await builder.build({ kv });
const impl3 = federation3 as FederationImpl<void>;
assertEquals(impl3.router.route("/leaked/1"), null);
});

await t.step("should build with default options", async () => {
const builder = createFederationBuilder<void>();
const kv = new MemoryKvStore();
Expand Down Expand Up @@ -211,10 +236,31 @@ test("FederationBuilder", async (t) => {
),
RouterError,
);
assertThrows(
() =>
builderAfterInvalid.setOutboxListeners(
"/users/{identifier:3}/outbox" as `${string}{identifier}${string}`,
),
RouterError,
);
assertThrows(
() =>
builderAfterInvalid.setOutboxListeners(
"/users/{identifier*}/outbox" as `${string}{identifier}${string}`,
),
RouterError,
);
assertThrows(
() =>
builderAfterInvalid.setOutboxListeners(
"/users/{identifier,identifier}/outbox" as `${string}{identifier}${string}`,
),
RouterError,
);
builderAfterInvalid.setOutboxListeners("/users/{identifier}/outbox");

const builder2 = createFederationBuilder<void>();
builder2.setOutboxListeners("/users{/identifier}/outbox");
builder2.setOutboxListeners("/users/{identifier}/outbox");

assertThrows(
() =>
Expand All @@ -231,6 +277,18 @@ test("FederationBuilder", async (t) => {
RouterError,
);

const builder3a = createFederationBuilder<void>();
assertThrows(
() => builder3a.setOutboxListeners("/users{;identifier}/outbox"),
RouterError,
);

const builder3b = createFederationBuilder<void>();
assertThrows(
() => builder3b.setOutboxListeners("/users{.identifier}/outbox"),
RouterError,
);

const builder4 = createFederationBuilder<void>();
assertThrows(
() =>
Expand All @@ -240,8 +298,77 @@ test("FederationBuilder", async (t) => {
),
RouterError,
);

const builder5 = createFederationBuilder<void>();
assertThrows(
() =>
builder5.setOutboxDispatcher(
"/users/{identifier:3}/outbox" as `${string}{identifier}${string}`,
() => ({ items: [] }),
),
RouterError,
);
});

await t.step(
"should reject identifier paths that can match without an identifier",
() => {
// `{/identifier}` is path-style expansion that can match zero
// segments, so the route would match with an empty or missing
// `identifier`, violating the `identifier: string` callback contract.
// See https://github.com/fedify-dev/fedify/pull/758#discussion_r3252548632
type IdPath = `${string}{identifier}${string}`;

assertThrows(
() =>
createFederationBuilder<void>().setActorDispatcher(
"{/identifier}" as IdPath,
() => null,
),
RouterError,
"Path for actor dispatcher must have one variable: {identifier}",
);
assertThrows(
() =>
createFederationBuilder<void>().setActorDispatcher(
"/users{/identifier}" as IdPath,
() => null,
),
RouterError,
);
assertThrows(
() =>
createFederationBuilder<void>().setInboxListeners(
"{/identifier}/inbox" as IdPath,
),
RouterError,
);
assertThrows(
() =>
createFederationBuilder<void>().setInboxListeners(
"/users{/identifier}/inbox" as IdPath,
),
RouterError,
);
assertThrows(
() =>
createFederationBuilder<void>().setOutboxListeners(
"/users{/identifier}/outbox" as IdPath,
),
RouterError,
);

// Simple expansion `{identifier}` must keep working.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This keeps the common /users/{identifier} form working, which is right, but I think we still need a negative case for an empty segment. With the new router, /users/{identifier} matches /users/ with identifier: "". That is still not a concrete identifier, and it can reach the same callback contract that says identifier: string.

The previous router was not exactly safe here either, but this PR changes the observable value shape from a missing binding to an empty string. If the intended Fedify contract is that required actor/inbox/outbox identifiers are non-empty, this should be enforced either by the builder/middleware dispatch path or by a regression test that /users/ is treated as no match for those routes.

createFederationBuilder<void>().setActorDispatcher(
"/users/{identifier}",
() => null,
);
createFederationBuilder<void>().setInboxListeners(
"/users/{identifier}/inbox",
);
},
);

await t.step("should pass build options correctly", async () => {
const builder = createFederationBuilder<number>();
const kv = new MemoryKvStore();
Expand Down
Loading