feat(teams): the activity feed, its two writers and its retention
TEAMS.md Part 4. `team_activity` takes items from two sources and treats them
identically on the read path: core writes its own membership and rename items
with source='core', and a module pushes game items through
`ctx.teams.activity.push`, which stops throwing and starts working.
Core writing here too is deliberate — the rendering path is exercised by core's
own content from day one, so the feed is never empty on a deployment whose
module pushes nothing.
Three rules shape the model:
- core never composes a summary. It arrives already rendered and is stored
verbatim; core cannot phrase "gained 15,000 gold" for a game whose
vocabulary it does not know.
- visibility fails closed. An item with no stated visibility is `members`.
- a push never throws at its call site. It is called from inside a game-event
handler, and a storage problem of core's must not become the module's
control flow.
Core emits four of the five kinds §4.2 names — `core.forum.thread` has nothing
to emit it until the forum lands in phase 4 — and emits none of them for a
Team's FIRST roster: importing a 155-member guild is one Team arriving, not 155
people joining, and a join per member would bury every real event under the
import and reach the row cap on day one.
Retention ships with the feed rather than after someone notices. A nightly
worker applies an age horizon and a per-Team row cap, both settings; either
alone has a hole, since age lets one busy guild write a million rows inside the
window and a cap keeps a dead Team's feed forever.
The sync now reads member ROWS rather than keys, replacing the `memberKeys`
call rather than adding to it: the feed needs each changing member's display
name and prior `is_leader`, and the upsert is about to overwrite both.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -120,6 +120,7 @@ function buildCtx(id, moduleRoot) {
|
||||
const activity = require('../model/activity/activity.model')
|
||||
const users = require('../model/users/users.model')
|
||||
const teams = require('../model/teams/teamSync.model')
|
||||
const teamActivity = require('../model/teams/teamActivity.model')
|
||||
const { makeLimiter, accountChangeLimiter } = require('../middleware/rateLimit')
|
||||
/* eslint-enable global-require */
|
||||
|
||||
@@ -190,14 +191,20 @@ function buildCtx(id, moduleRoot) {
|
||||
teams: {
|
||||
publish: (event) => teams.publish(event),
|
||||
reconcile: (opts) => teams.request(opts),
|
||||
// §4's activity feed, which lands with the Team pages in phase 3. Declared
|
||||
// in 1.6.0 alongside the rest of the Team surface; calling it before phase 3
|
||||
// throws rather than silently accepting items into a table that does not
|
||||
// exist yet.
|
||||
// §4's activity feed (phase 3). `source` is bound to the CALLING module and
|
||||
// is never taken from the item — a module writes its own items, under its
|
||||
// own name, and items name their Team by the module's own `externalId`, so
|
||||
// there is no id a module could send that reaches another module's Team.
|
||||
//
|
||||
// Like `publish` and `reconcile` above, a failure here never reaches the
|
||||
// module: this is called from inside a game-event handler, and a storage
|
||||
// problem of core's must not become the module's control flow. A rejected
|
||||
// write is logged and the promise still resolves.
|
||||
activity: {
|
||||
push: () => {
|
||||
throw new Error('ctx.teams.activity.push is not available until the Team activity feed lands (TEAMS.md §4)')
|
||||
},
|
||||
push: (items) => teamActivity.push(id, items).then(
|
||||
(stored) => { void stored },
|
||||
(err) => { log.error('ctx.teams.activity.push failed', { module: id, message: err.message }) },
|
||||
),
|
||||
},
|
||||
},
|
||||
// One function, for one caller: the `admin.users.detail` slot router needs
|
||||
|
||||
Reference in New Issue
Block a user