Skip to content

Commit 1d68abf

Browse files
committed
Context.clone() method
Implements clone() method for all context types (Context, RequestContext, InboxContext) to create new contexts with modified data while preserving other properties. Updates documentation and tests accordingly.
1 parent c832e72 commit 1d68abf

7 files changed

Lines changed: 168 additions & 18 deletions

File tree

CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ To be released. Note that 1.6.0 was skipped due to a mistake in the versioning.
1414
- Added `Context.federation` property to access the `Federation`
1515
object from the context. [[#235]]
1616

17+
- Added `Context.clone()` method. [[#237]]
18+
1719
- Introduced `FederationBuilder` for creating a federation instance with
1820
a builder pattern.
1921

@@ -43,6 +45,7 @@ To be released. Note that 1.6.0 was skipped due to a mistake in the versioning.
4345
[#208]: https://github.com/fedify-dev/fedify/issues/208
4446
[#227]: https://github.com/fedify-dev/fedify/issues/227
4547
[#235]: https://github.com/fedify-dev/fedify/pull/235
48+
[#237]: https://github.com/fedify-dev/fedify/pull/237
4649

4750

4851
Version 1.5.3

docs/manual/context.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,3 +482,21 @@ if (isActor(actor)) {
482482
}
483483
}
484484
~~~~
485+
486+
487+
Replacing the context data
488+
--------------------------
489+
490+
*This API is available since Fedify 1.6.0.*
491+
492+
You can replace the context data by calling the `Context.clone()` method.
493+
This is useful when you want to create a new context based on the existing one
494+
but with different data. The following shows an example of replacing the
495+
context data:
496+
497+
~~~~ typescript twoslash
498+
import { type Context } from "@fedify/fedify";
499+
const ctx = null as unknown as Context<{ foo: string; bar: number }>;
500+
// ---cut-before---
501+
const newCtx = ctx.clone({ ...ctx.data, foo: "new value" });
502+
~~~~

fedify/federation/context.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,16 @@ export interface Context<TContextData> {
8484
*/
8585
readonly federation: Federation<TContextData>;
8686

87+
/**
88+
* Creates a new context with the same properties as this one,
89+
* but with the given data.
90+
* @param data The new data to associate with the context.
91+
* @returns A new context with the same properties as this one,
92+
* but with the given data.
93+
* @since 1.6.0
94+
*/
95+
clone(data: TContextData): Context<TContextData>;
96+
8797
/**
8898
* Builds the URI of the NodeInfo document.
8999
* @returns The NodeInfo URI.
@@ -429,6 +439,16 @@ export interface RequestContext<TContextData> extends Context<TContextData> {
429439
*/
430440
readonly url: URL;
431441

442+
/**
443+
* Creates a new context with the same properties as this one,
444+
* but with the given data.
445+
* @param data The new data to associate with the context.
446+
* @returns A new context with the same properties as this one,
447+
* but with the given data.
448+
* @since 1.6.0
449+
*/
450+
clone(data: TContextData): RequestContext<TContextData>;
451+
432452
/**
433453
* Gets an {@link Actor} object for the given identifier.
434454
* @param identifier The actor's identifier.
@@ -539,6 +559,16 @@ export interface InboxContext<TContextData> extends Context<TContextData> {
539559
*/
540560
recipient: string | null;
541561

562+
/**
563+
* Creates a new context with the same properties as this one,
564+
* but with the given data.
565+
* @param data The new data to associate with the context.
566+
* @returns A new context with the same properties as this one,
567+
* but with the given data.
568+
* @since 1.6.0
569+
*/
570+
clone(data: TContextData): InboxContext<TContextData>;
571+
542572
/**
543573
* Forwards a received activity to the recipients' inboxes. The forwarded
544574
* activity will be signed in HTTP Signatures by the forwarder, but its

fedify/federation/handler.test.ts

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1180,7 +1180,7 @@ test("handleInbox()", async () => {
11801180
recipient: null,
11811181
context: unsignedContext,
11821182
inboxContextFactory(_activity) {
1183-
return createInboxContext(unsignedContext);
1183+
return createInboxContext({ ...unsignedContext, clone: undefined });
11841184
},
11851185
...inboxOptions,
11861186
actorDispatcher: undefined,
@@ -1193,7 +1193,11 @@ test("handleInbox()", async () => {
11931193
recipient: "nobody",
11941194
context: unsignedContext,
11951195
inboxContextFactory(_activity) {
1196-
return createInboxContext({ ...unsignedContext, recipient: "nobody" });
1196+
return createInboxContext({
1197+
...unsignedContext,
1198+
clone: undefined,
1199+
recipient: "nobody",
1200+
});
11971201
},
11981202
...inboxOptions,
11991203
});
@@ -1205,7 +1209,7 @@ test("handleInbox()", async () => {
12051209
recipient: null,
12061210
context: unsignedContext,
12071211
inboxContextFactory(_activity) {
1208-
return createInboxContext(unsignedContext);
1212+
return createInboxContext({ ...unsignedContext, clone: undefined });
12091213
},
12101214
...inboxOptions,
12111215
});
@@ -1216,7 +1220,11 @@ test("handleInbox()", async () => {
12161220
recipient: "someone",
12171221
context: unsignedContext,
12181222
inboxContextFactory(_activity) {
1219-
return createInboxContext({ ...unsignedContext, recipient: "someone" });
1223+
return createInboxContext({
1224+
...unsignedContext,
1225+
clone: undefined,
1226+
recipient: "someone",
1227+
});
12201228
},
12211229
...inboxOptions,
12221230
});
@@ -1240,7 +1248,7 @@ test("handleInbox()", async () => {
12401248
recipient: null,
12411249
context: signedContext,
12421250
inboxContextFactory(_activity) {
1243-
return createInboxContext(unsignedContext);
1251+
return createInboxContext({ ...unsignedContext, clone: undefined });
12441252
},
12451253
...inboxOptions,
12461254
});
@@ -1251,7 +1259,11 @@ test("handleInbox()", async () => {
12511259
recipient: "someone",
12521260
context: signedContext,
12531261
inboxContextFactory(_activity) {
1254-
return createInboxContext({ ...unsignedContext, recipient: "someone" });
1262+
return createInboxContext({
1263+
...unsignedContext,
1264+
clone: undefined,
1265+
recipient: "someone",
1266+
});
12551267
},
12561268
...inboxOptions,
12571269
});
@@ -1262,7 +1274,7 @@ test("handleInbox()", async () => {
12621274
recipient: null,
12631275
context: unsignedContext,
12641276
inboxContextFactory(_activity) {
1265-
return createInboxContext(unsignedContext);
1277+
return createInboxContext({ ...unsignedContext, clone: undefined });
12661278
},
12671279
...inboxOptions,
12681280
skipSignatureVerification: true,
@@ -1274,7 +1286,11 @@ test("handleInbox()", async () => {
12741286
recipient: "someone",
12751287
context: unsignedContext,
12761288
inboxContextFactory(_activity) {
1277-
return createInboxContext({ ...unsignedContext, recipient: "someone" });
1289+
return createInboxContext({
1290+
...unsignedContext,
1291+
clone: undefined,
1292+
recipient: "someone",
1293+
});
12781294
},
12791295
...inboxOptions,
12801296
skipSignatureVerification: true,
@@ -1311,7 +1327,7 @@ test("handleInbox()", async () => {
13111327
recipient: null,
13121328
context: signedContext,
13131329
inboxContextFactory(_activity) {
1314-
return createInboxContext(signedInvalidContext);
1330+
return createInboxContext({ ...signedInvalidContext, clone: undefined });
13151331
},
13161332
...inboxOptions,
13171333
});

fedify/federation/middleware.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,22 @@ test("Federation.createContext()", async (t) => {
726726
);
727727
});
728728

729+
await t.step("Context.clone()", () => {
730+
const federation = createFederation<number>({
731+
kv,
732+
});
733+
const ctx = federation.createContext(new URL("https://example.com/"), 123);
734+
const clone = ctx.clone(456);
735+
assertStrictEquals(clone.canonicalOrigin, ctx.canonicalOrigin);
736+
assertStrictEquals(clone.origin, ctx.origin);
737+
assertEquals(clone.data, 456);
738+
assertEquals(clone.host, ctx.host);
739+
assertEquals(clone.hostname, ctx.hostname);
740+
assertStrictEquals(clone.documentLoader, ctx.documentLoader);
741+
assertStrictEquals(clone.contextLoader, ctx.contextLoader);
742+
assertStrictEquals(clone.federation, ctx.federation);
743+
});
744+
729745
mf.mock("GET@/.well-known/nodeinfo", (req) => {
730746
assertEquals(new URL(req.url).host, "example.com");
731747
assertEquals(req.headers.get("User-Agent"), "CustomUserAgent/1.2.3");
@@ -875,6 +891,24 @@ test("Federation.createContext()", async (t) => {
875891
);
876892
});
877893

894+
await t.step("RequestContext.clone()", () => {
895+
const federation = createFederation<number>({
896+
kv,
897+
});
898+
const req = new Request("https://example.com/");
899+
const ctx = federation.createContext(req, 123);
900+
const clone = ctx.clone(456);
901+
assertStrictEquals(clone.request, ctx.request);
902+
assertEquals(clone.url, ctx.url);
903+
assertEquals(clone.data, 456);
904+
assertEquals(clone.origin, ctx.origin);
905+
assertEquals(clone.host, ctx.host);
906+
assertEquals(clone.hostname, ctx.hostname);
907+
assertStrictEquals(clone.documentLoader, ctx.documentLoader);
908+
assertStrictEquals(clone.contextLoader, ctx.contextLoader);
909+
assertStrictEquals(clone.federation, ctx.federation);
910+
});
911+
878912
mf.uninstall();
879913
});
880914

fedify/federation/middleware.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1474,6 +1474,18 @@ export class ContextImpl<TContextData> implements Context<TContextData> {
14741474
invokedFromActorKeyPairsDispatcher;
14751475
}
14761476

1477+
clone(data: TContextData): Context<TContextData> {
1478+
return new ContextImpl<TContextData>({
1479+
url: this.url,
1480+
federation: this.federation,
1481+
data,
1482+
documentLoader: this.documentLoader,
1483+
contextLoader: this.contextLoader,
1484+
invokedFromActorKeyPairsDispatcher:
1485+
this.invokedFromActorKeyPairsDispatcher,
1486+
});
1487+
}
1488+
14771489
toInboxContext(
14781490
recipient: string | null,
14791491
activity: unknown,
@@ -2461,6 +2473,21 @@ class RequestContextImpl<TContextData> extends ContextImpl<TContextData>
24612473
this.url = options.url;
24622474
}
24632475

2476+
override clone(data: TContextData): RequestContext<TContextData> {
2477+
return new RequestContextImpl<TContextData>({
2478+
url: this.url,
2479+
federation: this.federation,
2480+
data,
2481+
documentLoader: this.documentLoader,
2482+
contextLoader: this.contextLoader,
2483+
invokedFromActorKeyPairsDispatcher:
2484+
this.invokedFromActorKeyPairsDispatcher,
2485+
invokedFromActorDispatcher: this.#invokedFromActorDispatcher,
2486+
invokedFromObjectDispatcher: this.#invokedFromObjectDispatcher,
2487+
request: this.request,
2488+
});
2489+
}
2490+
24642491
async getActor(identifier: string): Promise<Actor | null> {
24652492
if (
24662493
this.federation.actorCallbacks == null ||
@@ -2579,6 +2606,24 @@ export class InboxContextImpl<TContextData> extends ContextImpl<TContextData>
25792606
this.activityType = activityType;
25802607
}
25812608

2609+
override clone(data: TContextData): InboxContext<TContextData> {
2610+
return new InboxContextImpl<TContextData>(
2611+
this.recipient,
2612+
this.activity,
2613+
this.activityId,
2614+
this.activityType,
2615+
{
2616+
url: this.url,
2617+
federation: this.federation,
2618+
data,
2619+
documentLoader: this.documentLoader,
2620+
contextLoader: this.contextLoader,
2621+
invokedFromActorKeyPairsDispatcher:
2622+
this.invokedFromActorKeyPairsDispatcher,
2623+
},
2624+
);
2625+
}
2626+
25822627
forwardActivity(
25832628
forwarder:
25842629
| SenderKeyPair

fedify/testing/context.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,21 @@ import { lookupWebFinger as globalLookupWebFinger } from "../webfinger/lookup.ts
1414
import { mockDocumentLoader } from "./docloader.ts";
1515

1616
export function createContext<TContextData>(
17-
{
17+
values: Partial<Context<TContextData>> & {
18+
url?: URL;
19+
data: TContextData;
20+
federation: Federation<TContextData>;
21+
},
22+
): Context<TContextData> {
23+
const {
1824
federation,
19-
url,
25+
url = new URL("http://example.com/"),
2026
canonicalOrigin,
2127
data,
2228
documentLoader,
2329
contextLoader,
2430
tracerProvider,
31+
clone,
2532
getNodeInfoUri,
2633
getActorUri,
2734
getObjectUri,
@@ -41,16 +48,10 @@ export function createContext<TContextData>(
4148
lookupWebFinger,
4249
sendActivity,
4350
routeActivity,
44-
}: Partial<Context<TContextData>> & {
45-
url?: URL;
46-
data: TContextData;
47-
federation: Federation<TContextData>;
48-
},
49-
): Context<TContextData> {
51+
} = values;
5052
function throwRouteError(): URL {
5153
throw new RouterError("Not implemented");
5254
}
53-
url ??= new URL("http://example.com/");
5455
return {
5556
federation,
5657
data,
@@ -61,6 +62,7 @@ export function createContext<TContextData>(
6162
documentLoader: documentLoader ?? mockDocumentLoader,
6263
contextLoader: contextLoader ?? mockDocumentLoader,
6364
tracerProvider: tracerProvider ?? trace.getTracerProvider(),
65+
clone: clone ?? ((data) => createContext({ ...values, data })),
6466
getNodeInfoUri: getNodeInfoUri ?? throwRouteError,
6567
getActorUri: getActorUri ?? throwRouteError,
6668
getObjectUri: getObjectUri ?? throwRouteError,
@@ -118,6 +120,7 @@ export function createRequestContext<TContextData>(
118120
): RequestContext<TContextData> {
119121
return {
120122
...createContext(args),
123+
clone: args.clone ?? ((data) => createRequestContext({ ...args, data })),
121124
request: args.request ?? new Request(args.url),
122125
url: args.url,
123126
getActor: args.getActor ?? (() => Promise.resolve(null)),
@@ -140,6 +143,7 @@ export function createInboxContext<TContextData>(
140143
): InboxContext<TContextData> {
141144
return {
142145
...createContext(args),
146+
clone: args.clone ?? ((data) => createInboxContext({ ...args, data })),
143147
recipient: args.recipient ?? null,
144148
forwardActivity: args.forwardActivity ?? ((_params) => {
145149
throw new Error("Not implemented");

0 commit comments

Comments
 (0)