Skip to content

Move federation logic into @drfed/federation - #112

Merged
dahlia merged 1 commit into
mainfrom
packages/federation
Oct 6, 2026
Merged

dahlia merged 1 commit into
mainfrom
packages/federation

Conversation

@dahlia

@dahlia dahlia commented Oct 6, 2026

Copy link
Copy Markdown
Member

Closes #102.

The ActivityPub dispatchers, vocabulary serializers, instance host rules, and activity delivery tracking lived in @drfed/graphql, although none of them need a GraphQL schema. This PR moves them into a new @drfed/federation package that depends on @drfed/models and not on GraphQL. @drfed/drfed still builds the federation at startup and routes requests between it and GraphQL.

How it is split

Hackers' Pub registers its handlers by side effect on a module-level builder. DrFed's buildFederation(db) instead closes over a database and returns a fresh builder on every call, so each group of handlers became an internal registration function that receives the builder. src/index.ts creates the builder and calls them in the original order. A new test builds federations from two databases holding the same actor ID and checks that each serves its own row.

The package has four public entry points:

  • The root exports buildFederation(), createFederation(), createInboundRecorder(), and deliverActivity().
  • @drfed/federation/object exports the query selections plus toObject() and toCreate(), so GraphQL mutations and ActivityPub responses use the same serializers.
  • @drfed/federation/origin is packages/graphql/src/origin.ts moved unchanged, so server routing and federation lookups keep one definition of an instance host.
  • @drfed/federation/activity-delivery exports the delivery recording functions that packages/graphql/src/activity-delivery/entry.ts used to re-export.

The activity delivery code from #101 had to move too, since createFederation() wraps the KV store, tracer, meter, and queues with it and the inbox listener calls markHandled(). Only the Pothos types in entry.ts stay in @drfed/graphql.

@drfed/graphql/federation and @drfed/graphql/origin are removed, and @drfed/graphql/activity-delivery no longer re-exports runtime functions. There are no compatibility re-exports, since nothing has been released and every consumer is in this repository. packages/graphql/README.md maps the old imports to the new ones.

Things worth a look

Tests that need only a database moved to the new package with their own harness, including outbound.test.ts and queue.test.ts. The database-only seed fixtures and the remote.test.ts inbox helper are duplicated so that each package's tests stay self-contained. Tests that go through GraphQL, such as inbound.test.ts, remain in @drfed/graphql.

The new package adds DOM to its TypeScript lib. @drfed/graphql gets DOM typings transitively through graphql-yoga, and the moved tests read untyped JSON bodies from Response.json(). Adding the lib avoided type assertions throughout those checks.

Log categories move from ["drfed", "graphql", "federation"] and ["drfed", "graphql", "activity-delivery"] to ["drfed", "federation"] and ["drfed", "federation", "activity-delivery"].

Verification

Besides mise run build, mise run check, and mise run test from a clean tree, the four server packages were packed and installed outside the repository. The installed drfed-server migrated an empty PGlite directory and, after one instance was seeded, served GraphQL and the actor, object, Create, outbox, and WebFinger responses, and recorded an unsigned inbox POST as an unverified inbound delivery. A strict TypeScript consumer of every entry point type-checks without repository path aliases.

AI assistance

Claude Code (Claude Opus 5.5) drafted the plan, wrote the code, tests, and docs, resolved the rebase onto the activity delivery work, and ran the verification above. Codex (GPT-6 Astra) reviewed the plan over three rounds; its feedback led to the public/internal split and the cross-database test. Codex and Claude Code (Claude Fable 5.1) then reviewed the diff before and after the rebase and found nothing actionable.

DrFed's ActivityPub dispatchers, inbox listeners, vocabulary
serialization, and activity delivery tracking lived in @drfed/graphql,
so serving ActivityPub required the GraphQL package even though it
never needs a schema.  They now live in a new @drfed/federation package
that depends on @drfed/models but not on GraphQL, following the
structure of Hackers' Pub's federation package while keeping DrFed's
fresh-builder factory.

The new package exposes four subpaths:

 -  @drfed/federation: buildFederation(db) and the default
    createFederation(db, options), unchanged in signature and behavior,
    plus createInboundRecorder(), deliverActivity(), and the
    TrackedFederation type.  buildFederation() still creates a fresh
    builder on every call and passes it to register functions in
    actor.ts, object-dispatchers.ts, collection.ts, and inbox.ts, in the
    same registration order as before.  Nothing registers on a
    module-level builder.
 -  @drfed/federation/activity-delivery: the delivery recording
    functions that @drfed/graphql/activity-delivery used to re-export.
 -  @drfed/federation/object: objectSelection, activitySelection,
    toObject(), toCreate(), and the StoredObject/StoredCreate input
    types.  GraphQL mutations keep using these to build stored JSON-LD
    documents, so stored documents and ActivityPub responses share one
    set of serialization rules.
 -  @drfed/federation/origin: the instance host rules, moved verbatim
    from packages/graphql/src/origin.ts so that server routing and
    federation lookups cannot disagree about instance hosts.

The activity delivery runtime from #101 had to move along with the
dispatchers: createFederation() wraps the KV store, tracer, meter, and
queues with it, the inbox listener calls markHandled(), and the builder
registers its permanent-failure handler.  Its nine GraphQL-free modules
moved unchanged into src/activity-delivery/; only the Pothos types in
packages/graphql/src/activity-delivery/entry.ts stay behind.

The Public addressing and served-activity predicates moved to an
internal visibility.ts shared by the Create dispatcher, outbox pages,
and outbox counter.  Registration helpers stay internal and are not
exported.

@drfed/graphql drops its ./federation and ./origin subpaths, and
./activity-delivery no longer re-exports runtime functions, without
compatibility shims, since no version has been released and every
consumer is in this repository.  Its README lists the replacement
imports.  @drfed/graphql and @drfed/drfed import from
@drfed/federation, and mise.toml's build:server task builds the new
package.  No tsconfig path alias is added for @drfed/federation:
mapping its root to source while its subpaths resolve to dist/ would
give TrackedFederation two incompatible unique-symbol brands.

The intended observable changes are the logger categories, from
["drfed", "graphql", "federation"] to ["drfed", "federation"] and from
["drfed", "graphql", "activity-delivery"] to
["drfed", "federation", "activity-delivery"].

Tests that need only a database and a federation moved to the new
package with their own temporary-database harness, a copy of the
database-only seed fixtures, and a copy of the remote inbox helper;
this includes the outbound and queue delivery tests.  The
GraphQL-dependent ones remain in @drfed/graphql, including
activitypub-integration.test.ts and the inbound delivery tests; the
test that checked collection items over both ActivityPub and GraphQL
was split between the two.  A new regression test builds federations
from two databases holding the same actor identifier and checks that
each one serves its own database's actor, which a shared builder would
break.  The new package adds DOM to its TypeScript lib so the moved
tests keep reading untyped JSON response bodies, as they did under
@drfed/graphql, which gets DOM typings transitively through
graphql-yoga.

Verified with a clean `mise run build`, `mise run check`, and
`mise run test`; with `mise run dev`; and by installing the packed
tarballs of all four server packages outside the repository, starting
the installed drfed-server on an empty PGlite directory, seeding an
instance, checking GraphQL, actor, object, Create, outbox, and
WebFinger responses, checking that an unsigned inbox POST is recorded
as an unverified inbound delivery, and type-checking a strict
TypeScript consumer of every @drfed/federation subpath.

Closes #102

AI provenance: Claude Code (Claude Opus 5.5) read the issue and the
Hackers' Pub reference code, drafted the design and refactoring plan,
implemented the move, resolved the rebase onto the activity delivery
work by moving its runtime into the new package, wrote the new tests
and documentation, and ran the build, check, test, dev-server, and
packed-installation verification.  Codex (GPT-6 Astra) reviewed the
plan over three rounds; its feedback separated the public serializer
subpath from internal dispatcher registration and shared predicates,
added the cross-database isolation test, and extended the
packed-package verification.  Codex (GPT-6 Astra) and Claude Code
(Claude Fable 5.1) then reviewed the change before and after the
rebase and reported no actionable findings, so neither review changed
the code.

Assisted-by: Claude Code:claude-opus-5-5
Assisted-by: Codex:gpt-6-astra
@dahlia dahlia added this to the DrFed 0.1.0 milestone Oct 6, 2026
@dahlia dahlia self-assigned this Oct 6, 2026
@dahlia
dahlia merged commit a2b30e5 into main Oct 6, 2026
11 checks passed
@dahlia
dahlia deleted the packages/federation branch October 6, 2026 08:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

Move ActivityPub federation logic into @drfed/federation

4 participants