23 KiB
phase, plan, subsystem, tags, requires, provides, affects, actuals, plan_head_before, plan_head_after, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, coverage, duration, completed, status
| phase | plan | subsystem | tags | requires | provides | affects | actuals | plan_head_before | plan_head_after | tech-stack | key-files | key-decisions | patterns-established | requirements-completed | coverage | duration | completed | status | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 11-jobs-realtime-and-search-infrastructure | 03 | realtime |
|
|
|
|
|
f1077382f5 |
eab2b007f5 |
|
|
|
|
|
|
12h 45m | 2026-09-30 | complete |
Phase 11 Plan 03: lighthouse realtime and the Centrifugo driver Summary
The transport-neutral lighthouse package and its centrifugo driver. Płytarium's Nuxt app gets PHP-shaped connection tokens at GET /api/realtime/token. Every Centrifugo subscribe is re-authorized through a namespace registry with the ported collection and wishlist authorizers. Album writes publish to their collection channel only after commit, through a one-attempt River job enqueued in the write transaction.
Performance
- Duration: 12h 45m wall-clock (2026-09-29T21:51:18Z to 2026-09-30T10:36:24Z). The clock includes long idle gaps between tool calls; the active work was much shorter.
- Started: 2026-09-29T21:51:18Z
- Completed: 2026-09-30T10:36:24Z
- Tasks: 3
- Files modified: 31 (19 in summercms.go, 12 in fonoteka.go)
Accomplishments
- lighthouse core (D-11, D-13).
lighthouse.Frompicks the driver byrealtime.driver:null(the default),log,memory, or a registered driver. An unknown name fails boot and lists the registered drivers.RegisterDriveris an init-time registry. A duplicate name panics.Mountputs user and public routes inGroupand server-to-server routes inGroupRaw, each with the surface middleware followed by the shared middleware. It refuses a UserAuth route when no guard is given (T-11-19).
- Centrifugo driver (D-12).
- The
net/httpclient sendsAuthorization: apikey, times out after 5 s and treats any 2xx as success. Without an API key it sends no request and returnsErrNotConfigured. TokenIssuerhas the five generators. They are HS256 with ordered claims,ForIdentifierencodes an emptyinfoas[], and all refuse an empty secret.TokenHandlerreturns the PHP 401 and 503 bodies.ProxyHandlerchecks the secret in constant time and always answers HTTP 200. A deny gets a generic body, with the reasons only in logs. An allow getsinfo([]when empty) and, on presence channels,allow/overridein PHParray_mergeorder. The body is capped at 64 KiB.
- The
- Channel rules (RT-02).
ParseChannelportsparseChannelexactly.ChannelIDandPHPIntreproduce the PHP 8.5(int)cast. A table test pins 44 inputs printed byphp -r, including saturation and the rule that INF becomes 0.FormatChannelslowercases ASCII and applies the namespace, asBroadcastEventJobdoes.
- Broadcasts (D-06..D-09, RT-03).
- Models broadcast through the
Broadcastablecontract orBind[T]. - The GORM callbacks are installed through
lagoon.OnDatabaseand run in the production boot order. Each enqueues asummer.broadcastjob on the write's*sql.Txinside a savepoint. A failure is logged and never aborts the write. - Deletes are snapshotted from a fresh read before the row goes.
WithoutBroadcasting[T]suppresses one type for one ctx.Service.Emitenqueues one summary event.- The job namespaces the channels, then publishes to one channel or broadcasts to several. A failure is a Warn log, and the job is never retried.
- Models broadcast through the
- fonoteka.go wiring (D-14, D-16).
config/realtime.yamlholds the PHP defaults and the README maps the env names.- The
ws-apibucket allows 120/min, keyed by user id, else by client IP. - A single
lighthouse.Mountcall putsjwt.authbeforethrottle:ws-api(user decision 5). - The
collectionandwishlistauthorizers are ported. - The Album binding applies the PHP channel and payload overrides.
- PROJECT.md records that websockets is no longer an app plugin. REQUIREMENTS.md notes that RT-01 uses a hand-rolled client.
Task Commits
summercms.go:
- Task 1: token route through the neutral package -
cada7a4(feat) - Task 2: subscribe proxy and registry -
79fd705(feat) - Task 3: transactional album broadcasts -
211c413(feat); planning docseab2b00(docs)
fonoteka.go:
- Task 1 -
c59fd76(feat: realtime wiring, ws-api bucket, Mount, config, token smoke test) - Task 2 -
cb78a76(feat: collection and wishlist authorizers, proxy smoke test) - Task 3 -
a223a72(feat: Album binding, broadcast smoke test)
Files Created/Modified
modules/lighthouse/lighthouse.go:Service,From, config reading, job registration, callback installation,DurationSettingmodules/lighthouse/drivers.go:Publisher,Driver,DriverFactory,RegisterDriver, the null/log drivers,MemoryDriver,Publicationmodules/lighthouse/route.go:Route,Surface,Surfaces,Mountmodules/lighthouse/users.go:User,UserLookup,Actor,SystemActor,Service.Actormodules/lighthouse/channel.go,registry.go: the channel rules,PHPInt, client-id ctx,Registry,Result,Allowed,Denied,Authorizermodules/lighthouse/broadcast.go,suppress.go,job.go: the broadcast contract,Bind, the callbacks,WithoutBroadcasting,Emit,BroadcastArgsand the workermodules/lighthouse/channel_test.go: the PHP(int)table, parse/format, registrymodules/lighthouse/centrifugo/*.go:Config/LoadConfig,Client,TokenIssuer,TokenHandler,ProxyHandler,Drivermodules/lighthouse/README.mdand the rootREADME.mdrow- fonoteka.go:
realtime.go,classes/ws/*,plugin.go(field, Boot call,ws-api),routes.go(Mount),config/realtime.yaml,README.md(Configuration),realtime_smoke_test.go, test harness tweaks, go.mod/go.sum tidy
Decisions Made
See key-decisions in the frontmatter.
TDD Gate Compliance
Tasks 2 and 3 are tdd="true". For each, the tests were written first against stubs with the final signatures. The RED runs were recorded with go test -json converted to TAP, and gsd-tools check tdd-red-evidence returned RED_EVIDENCE_OK (target_test_failed) for:
- Task 2:
TestRealtimeSubscribeProxy, where the stub proxy answered 418 and the allow body was expected; andTestChannelIDMatchesPHPwith the other lighthouse unit tests, where the stubChannelIDreturned -1. - Task 3:
TestAlbumBroadcastSmoke, where the stubs installed no callbacks andEmitenqueued nothing. The rollback and wishlist subtests passed at RED, as expected, because they assert that nothing is published.
Gate violation, flagged: there are no separate test(11-03) RED commits. The project CLAUDE.md requires go vet and go test ./... to be green at every commit, so the tests and implementation landed together in the feat commits. Plans 11-01 and 11-02 made the same call.
Deviations from Plan
Auto-fixed Issues
1. [Rule 1 - Bug] JSONB reordered broadcast payload keys
- Found during: Task 3 (TestAlbumBroadcastSmoke GREEN run)
- Issue: River stores job args in a JSONB column, and JSONB sorts object keys. So
{"reason":"bulk_create","count":3}arrived at Centrifugo as{"count":3,"reason":"bulk_create"}, which breaks the PHP byte order that the 11-06 goldens will compare. - Fix:
BroadcastArgskeepsPayload json.RawMessagein Go, but itsMarshalJSON/UnmarshalJSONcarry the payload as a JSON string, whose content JSONB leaves alone. - Files modified: modules/lighthouse/job.go
- Verification: the bulk subtest asserts the exact bytes
{"reason":"bulk_create","count":3}. - Committed in:
211c413
2. [Rule 2 - Correctness/operability] No broadcast jobs when the driver publishes nothing
- Found during: Task 3
- Issue: Every album write would enqueue a River job in two cases where the job would publish nothing: an install without a Centrifugo API key, and every fonoteka test harness (the centrifugo driver without a key). PHP queues the job and then logs a failed publish.
- Fix: The callbacks and
Emitskip the enqueue for the null driver, and for a driver whose optionalEnabled()reports false.centrifugo.Driver.Enabledreports whether an API key is set. Nothing observable is published in either case. The only difference is that no "broadcast failed" warning is logged. - Files modified: modules/lighthouse/broadcast.go, suppress.go, centrifugo/driver.go
- Committed in:
211c413
3. [Rule 3 - Test harness] fonoteka harness boots the centrifugo driver
- Found during: Task 1
- Issue:
bootConfigdid not setrealtime.driver, so every assembled route table used the null driver and carried no realtime routes.TestAllRouteGroupsBootandTestFullRouteTableAuthGroupMutualExclusivitywould then prove nothing about the Mount call. - Fix:
bootConfigsetsrealtime.driver: centrifugo, following D-13 ("Płytarium and its tests always run the Centrifugo driver").TestAllRouteGroupsBootnow also expects thews-apibucket. Both test files are outside the plan's files list. - Files modified: ../fonoteka.go/plugins/golem15/fonoteka/plugin_boot_test.go, routes_bucket_test.go
- Committed in: c59fd76
4. [Hardening, documented] Proxy input handling beyond PHP
- A missing or empty channel is denied. Centrifugo always sends one; PHP would return a 500 TypeError.
- A
userthat is neither a JSON string nor a number (for exampletrue) counts as empty and is denied. PHP would casttrueto 1. - A malformed JSON body is denied with the generic body.
- Registering a namespace twice is a boot error. PHP lets the last registration win. This was already specified in the plan.
- Committed in:
79fd705
5. [Additions] Extra exported surface, all documented in the README and checked with go doc
Service.Logger,Namespace,QueueandTimeoutDurationSettingDefaultDriver,DefaultQueue,DefaultTimeoutandDefaultTTLPHPIntandWithClientIDNewRegistry- the
Callback*name constants - centrifugo:
LoadConfig,NewDriver,NewTokenIssuer,DriverName,Driver.Enabled,Driver.Client,Driver.Issuer,Driver.Config,Config.TrustedProxies(the client IP in secret-failure logs) and theDefault*constants
6. [Additions] Extra tests
TestRealtimeCentrifugoClientRequests: the httptest check of the client bytes, which the plan placed in the Task 1 smoke test.TestRealtimeSubscribeProxyWithoutSecretDenies.- lighthouse unit tests:
TestParseChannel,TestFormatChannels,TestClientIDandTestRegistry.
Total deviations: 3 auto-fixed (1 bug, 1 correctness/operability, 1 test harness), plus 3 documented notes. Impact on plan: Fix 1 is required for payload parity with PHP. Fix 2 keeps unconfigured installs and tests off River. No scope creep.
Issues Encountered
- The shell aliases
rmto interactive mode, so one command stalled on a prompt. It was rerun with/bin/rm -f. Plan 11-01 hit the same thing. - The token signing error path answers 500
{"error":"Internal server error"}. It is unreachable in practice, because the handler checksConfigured()first.
Flagged assumptions (carried from the plan)
- Batch updates through
Model(&T{}).Where(...)have a zero primary key and are not broadcast (Pitfall 8). A soft delete counts as a delete, and a restore is not broadcast. - The automatic Album
created/updatedpayload is built inside the write transaction.SaveAlbumsyncs the artist pivot aftertx.Save, so the payload can carry stale artists. Phase 12's store/update/bulk controllers must useWithoutBroadcasting[models.Album]plusEmit, as PHP does.classes.SerializeAlbumis still the minimal Phase 5 shape until Phase 12. - Several apps that share one
*gorm.DBbroadcast through the service built last, because the callbacks are replaced. Production has one app per handle.
User Setup Required
None for development. For a deployment, set the SUMMER_REALTIME__CENTRIFUGO__* variables listed in the fonoteka.go README (the PHP values carry over unchanged). Point Centrifugo's subscribe proxy at POST /api/realtime/subscribe with the same X-Centrifugo-Secret.
Next Phase Readiness
- Plan 11-06 (tide goldens) can diff the
deletedandcollection.bulk_updatedbytes. It can drive the Go side with the memory driver or the fake-Centrifugo pattern fromrealtime_smoke_test.go. - Plan 11-07 should add:
- the acme-type suppression test (suppressing one type never suppresses another)
- unit tests for
Mountvalidation errors, the log driver,Presence/Unsubscribe, all five token generators with an injected clock, the override merge with extra keys, the method-basedBroadcastablepath (alias, TTL, filter, payloader), andEmitwithout a transaction
- Phase 12 must route album store/update/bulk through
WithoutBroadcastingplusEmit(see above), and must migrategdb.Transactioncall sites tolagoon.Transaction.
Phase: 11-jobs-realtime-and-search-infrastructure Completed: 2026-09-30
Self-Check: PASSED
- All 21 key created files exist on disk.
- summercms.go commits
cada7a4,79fd705,211c413andeab2b00(plus the SUMMARY commit499177a) exist, and so do fonoteka.go commits c59fd76, cb78a76 and a223a72. The fonoteka.go working tree is clean. - After Task 3,
go vet ./... && go test ./...passed in summercms.go. The fonoteka.go vet and test command for all three modules passed, and so did the-race -vrun of TestAlbumBroadcastSmoke, TestRealtimeSubscribeProxy and TestRealtimeTokenRoute (no SKIP, no DATA RACE). - Every acceptance-criteria grep and
go doccheck of all three tasks passed.