Refine domain classes: behavior tags, extra-tag passthrough, cleanups
tests / tests (push) Failing after 5m10s

Iterate on @welshman/domain following review:

- base: add `group`/`protect`/`expires` behavior tags (parsed in base, emitted
  via addBehaviorTags before hashing) and an `extraTags` passthrough (opt-in via
  reservedTagKeys) so tag carry-over lives in one place; migrate Handler, Comment,
  Thread onto it. Comment gains nested root/parent ref structs + setters.
- List: fix inverted keepTags; add clearTags/clearPublicTags/clearPrivateTags and
  use them in the relay/server list setters.
- RelayList: preserve complementary read/write capability instead of dropping
  modeless entries.
- Split Relay/Room membership ops into per-kind classes (RelayAddMember/
  RelayRemoveMember, RoomAddMember/RoomRemoveMember) over a shared base.
- TimeEvent (renamed from CalendarEvent): derive "D" day tags in toTemplate.
- Feed: default to an empty feed, fail parse when the "feed" tag is missing.
- RelaySet added; CommunityList renamed to GroupList; predicate bare add/remove
  mutators; RoomMeta uses randomId; PollResponse.selections drops pollType.
- Remove ChannelList, FileServerList, Settings, and event-asserting getAddress/
  display accessors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V67tPYdvh1qCkjEBhJGZUR
This commit is contained in:
2026-06-18 22:42:10 +00:00
parent 99f5233e05
commit 5e142e4db4
26 changed files with 353 additions and 582 deletions
+29 -60
View File
@@ -1,85 +1,54 @@
import {uniq} from "@welshman/lib"
import {ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER, getTagValue, getPubkeyTagValues} from "@welshman/util"
import {ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER, getPubkeyTagValues} from "@welshman/util"
import type {EventTemplate, TrustedEvent} from "@welshman/util"
import type {ISigner} from "@welshman/signer"
import {DomainObject} from "./base.js"
export type RoomMembershipOpValues = {
kind: number
h: string
export type RoomMembershipValues = {
pubkeys: string[]
}
export const makeRoomMembershipOpValues = (
values: Partial<RoomMembershipOpValues> = {},
): RoomMembershipOpValues => ({
kind: ROOM_ADD_MEMBER,
h: "",
export const makeRoomMembershipValues = (
values: Partial<RoomMembershipValues> = {},
): RoomMembershipValues => ({
pubkeys: [],
...values,
})
export const makeRoomAddMember = (h: string, pubkeys: string[]) =>
RoomMembershipOp.init({kind: ROOM_ADD_MEMBER, h, pubkeys})
export const makeRoomRemoveMember = (h: string, pubkeys: string[]) =>
RoomMembershipOp.init({kind: ROOM_REMOVE_MEMBER, h, pubkeys})
// NIP-29 moderation op for adding (kind 9000) or removing (kind 9001) room
// members. These are regular (non-addressable) events carrying the group id in
// the "h" tag and the affected pubkeys in "p" tags. The two kinds share an
// identical shape, so they're merged into one kind-discriminated class.
// members. Regular (non-addressable) events carrying the affected pubkeys in "p"
// tags; the target group id is the base `group` ("h") behavior tag. Add and
// remove share this shape; each is its own concrete class fixing the kind.
//
// Because the base DomainObject treats `kind` as a fixed value and asserts
// event.kind === this.kind in parse(), `kind` is a mutable instance field here:
// it's seeded from values.kind in normalizeValues, and parse() is overridden to
// adopt the event's kind before normalizing.
export class RoomMembershipOp extends DomainObject<RoomMembershipOpValues> {
kind = ROOM_ADD_MEMBER
values = makeRoomMembershipOpValues()
// Flotilla's membership replay treats RoomAddMember => member, RoomRemoveMember
// => not a member.
export abstract class RoomMembershipOp extends DomainObject<RoomMembershipValues> {
values = makeRoomMembershipValues()
protected normalizeValues(values: Partial<RoomMembershipOpValues> = {}) {
const normalized = makeRoomMembershipOpValues(values)
this.kind = normalized.kind
return normalized
protected normalizeValues(values: Partial<RoomMembershipValues> = {}) {
return makeRoomMembershipValues(values)
}
protected parseEvent(event: TrustedEvent): Partial<RoomMembershipOpValues> {
return {
kind: event.kind,
h: getTagValue("h", event.tags) || "",
pubkeys: uniq(getPubkeyTagValues(event.tags)),
}
}
async parse(event: TrustedEvent, signer?: ISigner) {
this.event = event
this.kind = event.kind
this.values = this.normalizeValues(await this.parseEvent(event, signer))
return this
}
h() {
return this.values.h
protected parseEvent(event: TrustedEvent): Partial<RoomMembershipValues> {
return {pubkeys: uniq(getPubkeyTagValues(event.tags))}
}
pubkeys() {
return this.values.pubkeys
}
isAdd() {
return this.kind === ROOM_ADD_MEMBER
}
async toTemplate(): Promise<EventTemplate> {
const tags: string[][] = [
["h", this.values.h],
...this.values.pubkeys.map(pk => ["p", pk]),
]
return {kind: this.kind, tags, content: ""}
return {
kind: this.kind,
tags: this.values.pubkeys.map(pk => ["p", pk]),
content: "",
}
}
}
export class RoomAddMember extends RoomMembershipOp {
readonly kind = ROOM_ADD_MEMBER
}
export class RoomRemoveMember extends RoomMembershipOp {
readonly kind = ROOM_REMOVE_MEMBER
}