Skip to content

Commit 58ee54a

Browse files
authored
Merge pull request #237 from dahlia/context-clone
2 parents 87f95fc + 04840f5 commit 58ee54a

File tree

7 files changed

+168
-18
lines changed

7 files changed

+168
-18
lines changed

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)