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
aee437c
Initialize @fedify/uri-template package
2chanhaeng Apr 27, 2026
10dc8dd
Update version of @fedify/uri-template
2chanhaeng Apr 28, 2026
7d8bc74
Add test for template
2chanhaeng Apr 28, 2026
c0e88cb
Allow **/*.bench.ts files use `@fedfiy/fixture`
2chanhaeng May 1, 2026
fee06d1
Implement RFC 6570 URI template expansion
2chanhaeng May 1, 2026
1610db0
Add test suite for @fedify/uri-template Template
2chanhaeng May 1, 2026
2bc61cc
Report URI template expansion errors
2chanhaeng May 2, 2026
9227d67
Document URI template compatibility
2chanhaeng May 2, 2026
d36a02b
Refine URI template expansion internals
2chanhaeng May 4, 2026
002cc39
Add URI template matching
2chanhaeng May 4, 2026
71acc66
Add test suite for URI template matching
2chanhaeng May 4, 2026
fdfe4f1
Add RFC 6570 Router class
2chanhaeng May 6, 2026
14aabf1
Add Router conformance tests
2chanhaeng May 6, 2026
b4e28cc
Capture legacy Router failures
2chanhaeng May 6, 2026
3fbbebc
Reorganize uri-template into per-feature module layouts
2chanhaeng May 6, 2026
dde858d
Rename "symmetric" to "round-trip" in user-facing prose
2chanhaeng May 6, 2026
f5a9a04
Rename router memory pressure scenario factories
2chanhaeng May 6, 2026
68af501
Document operator behavior table and drop dead constant
2chanhaeng May 6, 2026
c942bb1
Drop unused VariableSpec and align reporter docs with expansion
2chanhaeng May 6, 2026
6449801
Document route-shape gaps in uri-template-router compatibility test
2chanhaeng May 6, 2026
28f7ea8
Drop "using RegExp" qualifier from Template bench label
2chanhaeng May 6, 2026
0559fdb
Add Router#register, batch trie insert, and constructor routes
2chanhaeng May 7, 2026
e23f9d2
Move URI template old tests
2chanhaeng May 8, 2026
aa491b0
Group uri-template tests by suite via nested t.step
2chanhaeng May 8, 2026
303a638
Export `isExpression` from @fedify/uri-template
2chanhaeng May 8, 2026
880515c
Migrate @fedify/fedify to @fedify/uri-template router
2chanhaeng May 8, 2026
37436dc
Note uri-template router migration in CHANGES.md
2chanhaeng May 8, 2026
892f14b
Export isPath helper from @fedify/uri-template
2chanhaeng May 8, 2026
518f4cb
Tighten match backtracking bounds in @fedify/uri-template
2chanhaeng May 8, 2026
8fe9471
Replace match bench with backtracking-pressure cases
2chanhaeng May 8, 2026
06bcedc
Add PR link
2chanhaeng May 9, 2026
37f9f1e
Export `assertPath` from @fedify/uri-template
2chanhaeng May 9, 2026
85a2f1b
Replace `Router` from @fedify/fedify with the wrapper of `Router` fro…
2chanhaeng May 9, 2026
6bfff79
Apply review feedback from PR #758
2chanhaeng May 9, 2026
368d43d
Fix `consumeUnnamed` over-pruning of valid match decompositions
2chanhaeng May 9, 2026
4f353ba
Fix `defaultReporter`
2chanhaeng May 11, 2026
93bef2f
Tighten identifier path validation in FederationBuilder
2chanhaeng May 11, 2026
9c642e9
Accept the empty path so trailing-slash retry can match `/`
2chanhaeng May 11, 2026
7f5ca8a
Polish naming and fixture comments in uri-template tests
2chanhaeng May 11, 2026
7ae28a5
Centralize expression parse error reporting in the tokenizer
2chanhaeng May 11, 2026
d2a702c
Drop stale router trie entries on re-registration
2chanhaeng May 11, 2026
a11cb3b
Serialize Deno check tasks behind install in `mise test`
2chanhaeng May 11, 2026
796395e
Make @fedify/uri-template default reporter a no-op
2chanhaeng May 11, 2026
f086e9d
Recover round-trip for associative keys with non-varname characters
2chanhaeng May 11, 2026
9c84ec0
Split identifier path validation into loose and strict asserts
2chanhaeng May 12, 2026
9e94098
Exclude old/ compat tests from published @fedify/uri-template
2chanhaeng May 12, 2026
3a6dc04
Reuse PrioritizedRouteEntry as the Trie entry constraint
2chanhaeng May 12, 2026
b0bca2f
Drop unused throw() helper from TemplateParseError
2chanhaeng May 12, 2026
c2c33c3
Note compat Router shim breaking changes in changelog
2chanhaeng May 12, 2026
389eea6
Index router trie at the token level to share dynamic prefixes
2chanhaeng May 14, 2026
6e057f7
Refactor router trie traversals with fold helpers
2chanhaeng May 14, 2026
3cd503c
Fix lockfile
2chanhaeng May 14, 2026
13cc78d
Remove unnecessary `as Path`
2chanhaeng May 15, 2026
db56e0b
Make Router#register() failure-atomic
2chanhaeng May 15, 2026
2048900
Update stale router design in uri-template README
2chanhaeng May 15, 2026
e804c0d
Make registered router patterns immutable
2chanhaeng May 15, 2026
85d227b
Reject path-style {/identifier} identifier routes
2chanhaeng May 16, 2026
4e4212d
Forward trailingSlashInsensitive in deprecated Router
2chanhaeng May 16, 2026
391d861
Keep deprecated RouterError instanceof working
2chanhaeng May 16, 2026
b93fb31
Keep deprecated Router.route() a nullable probe
2chanhaeng May 16, 2026
3623012
Drop Breaking Change note from changelog
2chanhaeng May 16, 2026
d41bd90
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 @@ -76,7 +76,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 @@ -86,6 +92,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

### @fedify/fixture
Expand All @@ -99,6 +106,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 |
| [@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.
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