feat(contrail): add /xrpc/net.openmeet.event + rsvp endpoints (additive)#580
Open
tompscanlan wants to merge 1 commit into
Open
feat(contrail): add /xrpc/net.openmeet.event + rsvp endpoints (additive)#580tompscanlan wants to merge 1 commit into
tompscanlan wants to merge 1 commit into
Conversation
Adds a new XRPC surface backed by Contrail's index of public ATProto calendar events and RSVPs. The endpoints (listRecords, getRecord) are served by a ContrailProvider that wraps @atmo-dev/contrail@0.6.0 against a dedicated Postgres schema. Populated via a one-shot sync command (npm run contrail:sync); a separate live-ingest deployment can be added later. What this does not change: existing event-query.service.ts, the events table, the publishing pipeline, the bsky-firehose-consumer + bsky-event-processor. Nothing on OM's existing read or write paths is touched. The new endpoints sit alongside the legacy REST surface. The new surface is single-tenant — Contrail indexes the universal public record set, so /xrpc/net.openmeet.* bypasses TenantGuard and the global /api prefix by mounting as raw Express middleware. Three implementation notes worth flagging for review: - ContrailProvider takes a Postgres advisory lock before calling contrail.init(). Without it, concurrent boots across replicas race on ALTER TABLE ADD COLUMN and emit spurious "column already exists" errors. - The Express mount is at /xrpc with an internal req.url filter for /net.openmeet.* methods. Mounting at /xrpc/net.openmeet doesn't work because Express's prefix matcher requires a path boundary after the prefix, and the dot in net.openmeet.event.listRecords is not one. - @atmo-dev/contrail is ESM-only. OM API compiles to CommonJS, where TypeScript rewrites `await import()` to `require()` (which can't load ESM). A small Function-constructor wrapper in contrail-loader.ts keeps the dynamic import opaque to the TS compiler so Node executes the real ESM import at runtime. A local d.ts shim declares the contrail surface we use, avoiding a project-wide moduleResolution change.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a new XRPC surface backed by Contrail's index of public ATProto calendar events and RSVPs. The endpoints (
listRecords,getRecord) are served by aContrailProviderthat wraps@atmo-dev/contrail@0.6.0against a dedicated Postgres schema./xrpc/net.openmeet.event.*and/xrpc/net.openmeet.rsvp.*.npm run contrail:sync); a live-ingest deployment can be added later.TenantGuardand the/apiprefix by mounting as raw Express middleware.What this PR does not change:
event-query.service.ts, the events table, the publishing pipeline,bsky-firehose-consumer,bsky-event-processor. None of OM's existing read or write paths are touched.Three implementation notes worth attention
Advisory lock around
contrail.init()(contrail.provider.ts,contrail-init-lock.ts). Without it, concurrent boots across replicas race onALTER TABLE ADD COLUMNinside Contrail's init and emit "column already exists" errors. The unit test incontrail-init-lock.spec.tsexercises the serialization invariant against a real Postgres.Mount at
/xrpcwith internal NSID filter, not/xrpc/net.openmeet(main.ts). Express's prefix matcher needs a path boundary (/or end) after the mount path; the dot innet.openmeet.event.listRecordsis not one. So we mount at/xrpcand gate inside the handler withreq.url.startsWith('/net.openmeet.').Function-constructor wrapper for the dynamic ESM import (
contrail-loader.ts).@atmo-dev/contrailis ESM-only. OM API compiles to CommonJS, where TypeScript rewritesawait import(...)torequire(...)— broken for ESM. The Function-constructor indirection keeps the import opaque to the TS compiler so Node executes the real ESM import at runtime. A localcontrail.d.tsshim declares the types so we don't need a project-widemoduleResolutionchange.Verification
npm test -- --testPathPattern=contrail-init-lockpasses (2 tests) when run withCONTRAIL_TEST_DATABASE_URLpointing at a Postgres.nest buildclean.src/contrail/smoke-server.ts):listRecords(sorted by rsvpsCount),listRecords?search=meetup,getRecord?...hydrateRsvps=3, andrsvp.listRecords?subjectUri=...all return well-formed responses with real records.Companion changes
mainalready updated:k8s/components/api/base/job-contrail-sync.yamlis a suspended CronJob that runsnpm run contrail:syncinside the API image. Triggered manually:kubectl create job --from=cronjob/contrail-sync contrail-sync-$(date +%s).Known follow-ups (out of scope for this PR)
tsc --noEmitbaseline has 461 pre-existing errors in*.spec.tsfiles (unrelated to this PR; surfaced because the local hook runs project-wide).nest buildpasses becausetsconfig.build.jsonexcludes specs. Cleanup is its own ticket.replicas: 1) for continuous indexing; deferred cutover decisions on read paths and retiringbsky-firehose-consumer.Test plan
npm test -- --testPathPattern=contrail-init-lockpassesnest buildclean/xrpc/net.openmeet.event.listRecords?limit=3against a Postgres populated vianpm run contrail:syncreturns records/xrpc/net.openmeet.event.getRecord?uri=...&hydrateRsvps=3returns event with RSVPs grouped by statusCONTRAIL_DATABASE_URLset (Provider logs a warning, endpoints return 503)