Skip to content

Commit

Permalink
@fedify/fedify/x/fresh module
Browse files Browse the repository at this point in the history
  • Loading branch information
dahlia committed Mar 17, 2024
1 parent 5312d9a commit 3b9de32
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 41 deletions.
8 changes: 8 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ Version 0.4.0

To be released.

- Added `@fedify/fedify/x/fresh` module for integrating with [Fresh]
middleware.

- Added `integrateHandler()` function.
- Added `integrateHandlerOptions()` function.

[Fresh]: https://fresh.deno.dev/


Version 0.3.0
-------------
Expand Down
3 changes: 2 additions & 1 deletion deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"./nodeinfo": "./nodeinfo/mod.ts",
"./runtime": "./runtime/mod.ts",
"./vocab": "./vocab/mod.ts",
"./webfinger": "./webfinger/mod.ts"
"./webfinger": "./webfinger/mod.ts",
"./x/fresh": "./x/fresh.ts"
},
"imports": {
"@cfworker/json-schema": "npm:@cfworker/json-schema@^1.12.8",
Expand Down
13 changes: 13 additions & 0 deletions docs/manual/federation.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,19 @@ as [`Request`] and [`Response`] objects. In that case, you need to convert
the request and response objects to the appropriate types that the `Federation`
object can handle.

> [!NOTE]
> The above example artificially shows a verbose way to integrate
> the `Federation` object with Fresh, so that a user of other web frameworks
> can understand the concept. In practice, you can define a middleware
> using `integrateHandler()` function from `@fedify/fedify/x/fresh` module:
>
> ~~~~ typescript
> import { federation } from "../federation.ts"; // Import the `Federation` object
> import { integrateHandler } from "jsr:@fedify/fedify/x/fresh";
>
> export const handler = integrateHandler(federation, () => undefined);
> ~~~~
> [!TIP]
> In theory, you can directly pass `Federation.handle()` to the [`Deno.serve()`]
> function, but you probably wouldn't want to do that because you want to handle
Expand Down
1 change: 1 addition & 0 deletions examples/blog/import_map.g.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@fedify/fedify/runtime": "../../runtime/mod.ts",
"@fedify/fedify/vocab": "../../vocab/mod.ts",
"@fedify/fedify/webfinger": "../../webfinger/mod.ts",
"@fedify/fedify/x/fresh": "../../x/fresh.ts",
"markdown-it": "npm:markdown-it@^14.0.0",
"preact": "https://esm.sh/[email protected]",
"preact/": "https://esm.sh/[email protected]/",
Expand Down
1 change: 1 addition & 0 deletions examples/blog/import_map.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"@fedify/fedify/runtime": "../../runtime/mod.ts",
"@fedify/fedify/vocab": "../../vocab/mod.ts",
"@fedify/fedify/webfinger": "../../webfinger/mod.ts",
"@fedify/fedify/x/fresh": "../../x/fresh.ts",
"markdown-it": "npm:markdown-it@^14.0.0",
"preact": "https://esm.sh/[email protected]",
"preact/": "https://esm.sh/[email protected]/",
Expand Down
43 changes: 3 additions & 40 deletions examples/blog/routes/_middleware.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,6 @@
import { FreshContext } from "$fresh/server.ts";
import { Handler } from "$fresh/server.ts";
import { federation } from "../federation/mod.ts";
import { integrateHandler } from "@fedify/fedify/x/fresh";

// This is the entry point to the Fedify middleware from the Fresh framework:
export async function handler(request: Request, context: FreshContext) {
// The `federation` object purposes to handle federation-related requests.
// It is responsible for handling, for example, WebFinger queries, actor
// dispatching, and incoming activities to the inbox. The definition of
// the object is in the federation/mod.ts file.
return await federation.handle(request, {
// The context data is not used in this example, but it can be used to
// store data (e.g., database connections) that is shared between
// the different federation-related callbacks:
contextData: undefined,

// If the `federation` object finds a request not responsible for it
// (i.e., not a federation-related request), it will call the `next`
// provided by the Fresh framework to continue the request handling
// by the Fresh:
onNotFound: context.next.bind(context),

// Similar to `onNotFound`, but slightly more tricky one.
// When the `federation` object finds a request not acceptable type-wise
// (i.e., a user-agent doesn't want JSON-LD), it will call the `next`
// provided by the Fresh framework so that it renders HTML if there's some
// page. Otherwise, it will simply return a 406 Not Acceptable response.
// This kind of trick enables the Fedify and Fresh to share the same routes
// and they do content negotiation depending on `Accept` header.
// For instance, in this example, `/users/{handle}` can return JSON-LD
// by the Fedify and redirects to the home page by the Fresh:
async onNotAcceptable(_request: Request) {
const response = await context.next();
if (response.status !== 404) return response;
return new Response("Not acceptable", {
status: 406,
headers: {
"Content-Type": "text/plain",
Vary: "Accept",
},
});
},
});
}
export const handler: Handler = integrateHandler(federation, () => undefined);
108 changes: 108 additions & 0 deletions x/fresh.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* Fedify with Fresh
* =================
*
* This module contains some utilities for integrating Fedify with [Fresh],
* a web framework for Deno.
*
* [Fresh]: https://fresh.deno.dev/
*
* @module
*/
import {
Federation,
FederationHandlerParameters,
} from "../federation/middleware.ts";

interface FreshContext {
next(): Promise<Response>;
}

/**
* Create options for the `federation` object to integrate with Fresh.
*
* @example _middleware.ts
* ``` typescript
* import { FreshContext } from "$fresh/server.ts";
* import { federation } from "federation.ts"; // Import the `Federation` object
*
* export async function handler(request: Request, context: FreshContext) {
* return await federation.handle(request, {
* contextData: undefined,
* ...integrateHandlerOptions(context),
* })
* }
* ```
*
* @param context A Fresh context.
* @returns Options for the {@link Federation.handle} method.
*/
export function integrateHandlerOptions(
context: FreshContext,
): Omit<FederationHandlerParameters<void>, "contextData"> {
return {
// If the `federation` object finds a request not responsible for it
// (i.e., not a federation-related request), it will call the `next`
// provided by the Fresh framework to continue the request handling
// by the Fresh:
onNotFound: context.next.bind(context),

// Similar to `onNotFound`, but slightly more tricky one.
// When the `federation` object finds a request not acceptable type-wise
// (i.e., a user-agent doesn't want JSON-LD), it will call the `next`
// provided by the Fresh framework so that it renders HTML if there's some
// page. Otherwise, it will simply return a 406 Not Acceptable response.
// This kind of trick enables the Fedify and Fresh to share the same routes
// and they do content negotiation depending on `Accept` header:
async onNotAcceptable(_request: Request) {
const response = await context.next();
if (response.status !== 404) return response;
return new Response("Not acceptable", {
status: 406,
headers: {
"Content-Type": "text/plain",
Vary: "Accept",
},
});
},
};
}

/**
* Create a Fresh middleware handler to integrate with the {@link Federation}
* object.
*
* @example _middleware.ts
* ``` typescript
* import { federation } from "federation.ts"; // Import the `Federation` object
*
* export const handler = integrateHandler(federation, () => undefined);
* ```
*
* @param federation A {@link Federation} object to integrate with Fresh.
* @param createContextData A function to create a context data for the
* {@link Federation} object.
* @returns A Fresh middleware handler.
*/
export function integrateHandler<
TContextData,
TFreshContext extends FreshContext,
>(
federation: Federation<TContextData>,
createContextData: (
req: Request,
ctx: TFreshContext,
) => TContextData | Promise<TContextData>,
): (req: Request, ctx: TFreshContext) => Promise<Response> {
return async (
request: Request,
context: TFreshContext,
): Promise<Response> => {
let contextData = createContextData(request, context);
if (contextData instanceof Promise) contextData = await contextData;
return await federation.handle(request, {
contextData,
...integrateHandlerOptions(context),
});
};
}

0 comments on commit 3b9de32

Please sign in to comment.