Skip to content

Commit 94f077e

Browse files
committed
Fix FederationBuilder.build() multiple calls error
Implement proper cloning of mutable state to prevent errors when build() is called multiple times on the same builder instance.
1 parent 7f77f90 commit 94f077e

6 files changed

Lines changed: 80 additions & 13 deletions

File tree

CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ To be released. Note that 1.6.0 was skipped due to a mistake in the versioning.
2727

2828
- Added `Router.trailingSlashInsensitive` property.
2929

30+
- Added `Router.clone()` method.
31+
3032
- Implemented HTTP Message Signatures ([RFC 9421]) with double-knocking.
3133
Currently, it only works with RSA-PKCS#1-v1.5. [[#208]]
3234

fedify/federation/builder.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ test("FederationBuilder", async (t) => {
102102
impl.router.build("inbox", { identifier: "user1" }),
103103
"/users/user1/inbox",
104104
);
105+
106+
await builder.build({ kv }); // Ensure build can be called multiple times
105107
},
106108
);
107109

fedify/federation/builder.ts

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -104,26 +104,46 @@ export class FederationBuilderImpl<TContextData>
104104
const { FederationImpl } = await import("./middleware.ts");
105105
const f = new FederationImpl(options);
106106

107+
// In order to ensure `build()` can be called multiple times and
108+
// each instance does not share their state, we clone everything
109+
// that is mutable. This includes the router and callbacks.
110+
107111
// Assign the existing router instance but preserve the settings
108112
// Keep the original trailingSlashInsensitive configuration
109113
const trailingSlashInsensitiveValue = f.router.trailingSlashInsensitive;
110-
f.router = this.router;
114+
f.router = this.router.clone();
111115
f.router.trailingSlashInsensitive = trailingSlashInsensitiveValue;
112116
f._initializeRouter();
113117

114-
f.actorCallbacks = this.actorCallbacks;
118+
f.actorCallbacks = this.actorCallbacks == null
119+
? undefined
120+
: { ...this.actorCallbacks };
115121
f.nodeInfoDispatcher = this.nodeInfoDispatcher;
116-
f.objectCallbacks = this.objectCallbacks;
117-
f.objectTypeIds = this.objectTypeIds;
122+
f.objectCallbacks = { ...this.objectCallbacks };
123+
f.objectTypeIds = { ...this.objectTypeIds };
118124
f.inboxPath = this.inboxPath;
119-
f.inboxCallbacks = this.inboxCallbacks;
120-
f.outboxCallbacks = this.outboxCallbacks;
121-
f.followingCallbacks = this.followingCallbacks;
122-
f.followersCallbacks = this.followersCallbacks;
123-
f.likedCallbacks = this.likedCallbacks;
124-
f.featuredCallbacks = this.featuredCallbacks;
125-
f.featuredTagsCallbacks = this.featuredTagsCallbacks;
126-
f.inboxListeners = this.inboxListeners;
125+
f.inboxCallbacks = this.inboxCallbacks == null
126+
? undefined
127+
: { ...this.inboxCallbacks };
128+
f.outboxCallbacks = this.outboxCallbacks == null
129+
? undefined
130+
: { ...this.outboxCallbacks };
131+
f.followingCallbacks = this.followingCallbacks == null
132+
? undefined
133+
: { ...this.followingCallbacks };
134+
f.followersCallbacks = this.followersCallbacks == null
135+
? undefined
136+
: { ...this.followersCallbacks };
137+
f.likedCallbacks = this.likedCallbacks == null
138+
? undefined
139+
: { ...this.likedCallbacks };
140+
f.featuredCallbacks = this.featuredCallbacks == null
141+
? undefined
142+
: { ...this.featuredCallbacks };
143+
f.featuredTagsCallbacks = this.featuredTagsCallbacks == null
144+
? undefined
145+
: { ...this.featuredTagsCallbacks };
146+
f.inboxListeners = this.inboxListeners?.clone();
127147
f.inboxErrorHandler = this.inboxErrorHandler;
128148
f.sharedInboxKeyDispatcher = this.sharedInboxKeyDispatcher;
129149
return f;

fedify/federation/inbox.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ export class InboxListenerSet<TContextData> {
2727
this.#listeners = new Map();
2828
}
2929

30+
clone(): InboxListenerSet<TContextData> {
31+
const clone = new InboxListenerSet<TContextData>();
32+
clone.#listeners = new Map(this.#listeners);
33+
return clone;
34+
}
35+
3036
add<TActivity extends Activity>(
3137
// deno-lint-ignore no-explicit-any
3238
type: new (...args: any[]) => TActivity,

fedify/federation/router.test.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { assertEquals, assertThrows } from "@std/assert";
1+
import { assert, assertEquals, assertFalse, assertThrows } from "@std/assert";
22
import { test } from "../testing/mod.ts";
33
import { Router, RouterError, type RouterOptions } from "./router.ts";
44

@@ -13,6 +13,21 @@ function setUp(options: RouterOptions = {}): Router {
1313
return router;
1414
}
1515

16+
test("Router.clone()", () => {
17+
const original = setUp();
18+
const clone = original.clone();
19+
clone.add("/users/{name}/friends", "friends");
20+
21+
assert(clone.has("friends"));
22+
assertEquals(clone.route("/users/alice/friends"), {
23+
name: "friends",
24+
template: "/users/{name}/friends",
25+
values: { name: "alice" },
26+
});
27+
assertFalse(original.has("friends"));
28+
assertEquals(original.route("/users/alice/friends"), null);
29+
});
30+
1631
test("Router.add()", () => {
1732
const router = new Router();
1833
assertEquals(router.add("/users", "users"), new Set());

fedify/federation/router.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// @ts-ignore TS7016
2+
import { cloneDeep } from "@es-toolkit/es-toolkit";
23
import { Router as InnerRouter } from "uri-template-router";
34
import { parseTemplate, type Template } from "url-template";
45

@@ -34,6 +35,17 @@ export interface RouterRouteResult {
3435
values: Record<string, string>;
3536
}
3637

38+
function cloneInnerRouter(router: InnerRouter): InnerRouter {
39+
const clone = new InnerRouter();
40+
clone.nid = router.nid;
41+
clone.fsm = cloneDeep(router.fsm);
42+
clone.routeSet = new Set(router.routeSet);
43+
clone.templateRouteMap = new Map(router.templateRouteMap);
44+
clone.valueRouteMap = new Map(router.valueRouteMap);
45+
clone.hierarchy = cloneDeep(router.hierarchy);
46+
return clone;
47+
}
48+
3749
/**
3850
* URL router and constructor based on URI Template
3951
* ([RFC 6570](https://tools.ietf.org/html/rfc6570)).
@@ -60,6 +72,16 @@ export class Router {
6072
this.trailingSlashInsensitive = options.trailingSlashInsensitive ?? false;
6173
}
6274

75+
clone(): Router {
76+
const clone = new Router({
77+
trailingSlashInsensitive: this.trailingSlashInsensitive,
78+
});
79+
clone.#router = cloneInnerRouter(this.#router);
80+
clone.#templates = { ...this.#templates };
81+
clone.#templateStrings = { ...this.#templateStrings };
82+
return clone;
83+
}
84+
6385
/**
6486
* Checks if a path name exists in the router.
6587
* @param name The name of the path.

0 commit comments

Comments
 (0)