Repository navigation
Commit de302c7
fix(app-shell): the chatter reads and writes reactions as the member's own sys_comment_reaction records (objectui#12078) (#12090)
Fixes #12078
Clause-②: yes
## What changed
The record chatter (`RecordDetailView` in `@object-ui/app-shell`) stores
each reaction as the member's own `sys_comment_reaction` record. It no
longer writes the whole `sys_comment.reactions` column. This follows
ruling A, amended, on objectstack-ai/objectstack#22505
(「同意;sys_comment.reactions 可以退役;可以不用考虑历史数据迁移;」) and the director's point
`6091621568` (no read-only aggregate).
- **Read.** After the comment read, the chatter makes one `find` on
`sys_comment_reaction`, with `comment_id` `$in` the feed's comment ids.
It groups the rows client-side into the `{ emoji: userIds[] }` shape and
runs them through the same aggregator the column used, so rendering is
unchanged. There is no per-comment read. A thread with more than 100
comments is read in pages of 100 ids, in parallel (see the measurement
below).
- **Write.** A click creates the member's own row, `{ comment_id, emoji
}`. The server stamps `user_id`, so the client sends none. A second
click deletes that row by its id. The id comes from the read, or from
the create that made the row. Writes for one (comment, emoji) run one
after another, so a take-back clicked before its create answers deletes
the row that create made. A refused write puts the reaction back and
raises the existing `detail.reactionFailed` error.
- **The cloud window: dual path, no `.objectui-sha` hold.** Where the
deployment has no `sys_comment_reaction`, the column path runs exactly
as before, byte for byte. That covers cloud's framework pin `56bf27affb`
(v17). The chatter asks app-shell's existing
`useObjectPresence('sys_comment_reaction')`; no new probe is written.
Following that hook's own contract, only an earned `absent` keeps the
column path. An unsettled registry holds the feed read until it answers.
A registry that lists nothing is not read as absence. The file surface
is `RecordDetailView.tsx` and its tests, as declared in the claim.
## Measurements (the five mechanism assumptions)
1. **The object** (objectstack `origin/main` `86da1949`,
`plugin-audit/src/objects/sys-comment-reaction.object.ts`). Its declared
fields:
- `comment_id`: text, required, maxLength 255; an id column, not a
lookup.
- `emoji`: text, required, maxLength 64.
- `user_id`: lookup to `sys_user`, required, stamped from the session on
create; a client value is replaced.
- `id` and `created_at`.
Uniqueness is the index `(comment_id, emoji, user_id)`, `unique:
'organization'`. A second identical reaction answers `409
UNIQUE_VIOLATION`. `apiMethods` is `get, list, create, delete, bulk`,
with no `update`.
- **Create:** `installCommentAccessHooks` (`authorizeReactionInsert`)
refuses a reaction on a comment the caller cannot read, and stamps
`user_id`.
- **Delete:** the platform's own-record floor (`owner_only_deletes`,
`created_by` equals the caller). So a member deletes their own reaction
and not another member's.
- **Read:** a reaction is readable exactly when its comment is.
The fake server in the new pins applies these same three rules.
2. **The cloud framework** `56bf27affb`. It has no
`sys-comment-reaction.object.ts` (`git cat-file -e` exits 128; control
`sys-comment.object.ts` exits 0), and `git grep sys_comment_reaction` in
its `plugin-audit/src` returns no hits (control: hits on `origin/main`).
- Its data door answers an unregistered name with
`assertObjectRegistered` → `objectNotFoundError`: `404 OBJECT_NOT_FOUND`
on find and create alike.
- What the chatter would do there without the gate: `data-objectstack`'s
`find` turns a 404 into `{ data: [] }` and memoises the resource as
missing, so every reaction would read as empty. Every click's create
would then reject, and the row would roll back with the error. The
presence gate keeps the column path there instead.
- On `origin/main` the `object` metadata list prunes nothing per caller
(`createMetaListReadGate` returns the items unchanged for `object`), so
`sys_comment_reaction` reads `present` wherever plugin-audit registers
it.
3. **The `$in` read size.**
- The feed's comment read has no page size. It reads the whole thread
with no `$top`, and `findData` returns the full set when no limit is
given.
- The data door's `find` is a GET with the filter as JSON in the query
string. Measured with Node 22.22.0 (`http.maxHeaderSize` 16384): each
UUID-shaped id costs about 45 bytes, and 100 ids make a 4,588-byte URL.
A local Node server accepted 350 ids and answered `431` at 360 ids.
- So the read is paged at 100 ids (`REACTION_READ_COMMENT_IDS`), under
both Node's limit and an 8 KB proxy request line. A thread of 100
comments or fewer costs exactly one read.
4. **What the user sees.**
- Where the object exists, a reaction stored only in the column is no
longer shown. Its chip disappears, as the maintainer's no-migration
ruling allows; one pin records this.
- Each member's click adds or removes only their own reaction, and two
members reacting at once both stay.
- Read from code, not measured live: a member who can read the record
but not edit it could not react to someone else's comment before,
because the column write is a `sys_comment` update, which only the
author or a parent editor may make. Now they can, because creating a
reaction requires only reading the comment.
- On cloud (object absent) nothing changes.
5. **`CommentThread`** (`@object-ui/collaboration`) is not on the
chatter's path at all. The console chatter renders reactions through
plugin-detail's `ReactionPicker` from `FeedItem.reactions`. The only
app-shell reference to `CommentThread` is a test. No prop changed.
- The published surface is untouched. The diff touches only
`RecordDetailView.tsx`, four test files beside it and one changeset, and
adds no `export` line.
- `Clause-②: yes` is copied from the claim as asked. The surface reading
above is for the in-seat review to judge.
## Pins
New file `RecordDetailView.reactionRecords-12078.test.tsx`. It drives
the real `RecordDetailView` and `ReactionPicker` over one fake server
that several mounted views share, each view signed in as a different
member.
- A member's click on another member's comment creates their own record
and nothing else (no `user_id` sent, no `sys_comment` update). The feed
renders it grouped, and a fresh read shows `👍 2`, marked as the
clicker's own.
- A second click removes it. The delete uses the id the create returned,
and on a fresh mount the id the read returned.
- Two members reacting at once both show. Two views are mounted at once,
both react before either write answers, and the comment's author then
reads `👍 2`.
- One batched read for 3 comments. A 250-comment thread is read in pages
of 100, 100 and 50 ids.
- A reaction stored only in the column is not shown on the records path.
- A refused create rolls back and raises the error once.
- A take-back clicked before the create answers deletes the row that
create made.
- The registry decides the store: absent means the column path, with no
reaction read and a `sys_comment.reactions` write; a registry listing
nothing means the records path; the comment read waits for a registry
that has not answered.
The column-path pins for objectui#11019, objectui#10899 and
objectui#11035 now declare a registry without `sys_comment_reaction`, so
they keep pinning the path cloud runs. A header note in each says so.
**Ablation** (one-time, not kept). On the committed tree (HEAD
`086db35`), the records branch was switched off and the column-only
guard removed, which puts the whole-set column write back on a
deployment that has the object.
- Landing was verified on disk: injected marker count 1, guard count 0.
- Result: `Tests 5 failed | 5 passed (10)`. The concurrent pin went red
with `expected [] to deeply equal [ '👍 2' ]`, and the other four write
pins went red with it.
- Restore was `git checkout HEAD --`, then a blob hash equal to the HEAD
blob `eba397e8` and an empty `git diff HEAD`.
## Verification
Every result below is on HEAD `086db35`, the last commit. Each heavy
step ran through the shared verify lock.
- Dependency closure build: `pnpm --workspace-concurrency=2 --filter
"@object-ui/app-shell^..." build`, 29 packages. VERDICT command-exit 0.
- `pnpm --filter @object-ui/app-shell type-check` (`tsc --noEmit && tsc
-p tsconfig.test.json`): exit 0. The test config includes the new test
file: its first run, on `b886ae3`, reported type errors in that file,
and they were fixed.
- `pnpm exec vitest run --maxWorkers=2
packages/app-shell/src/views/RecordDetailView`, covering every
`RecordDetailView.*` test (the 12 that read `sys_comment` among them):
`Test Files 47 passed (47)`, `Tests 294 passed (294)`.
- eslint on the six touched files: 0 errors. The warnings on added lines
are `no-explicit-any` in the new test, the same pattern as its sibling
tests. Two more sit on lines this diff edited but did not cause: the
existing `res: any`, and the existing missing-`t` dependency note.
- Static checks, all exit 0: `check:new-line-citations` (0 new),
`check:control-bytes`, `check:vi-mock-specifiers`,
`check:vi-mock-inherit`, `check:vi-mock-override-shape`,
`check:test-path-roots`, `check:changeset-claims`,
`check:pending-changeset-literals`, `check-changeset-presence.mjs` and
`check-changeset-no-major.mjs`. `check-governed-queue-guard.mjs --test`
answers NOT GOVERNED for the six paths.
- Not owed: the i18n checks. No locale string changed; the error reuses
`detail.reactionFailed`.
- NOT MEASURED: `check:eager-closure`, because it needs a full console
production build. CI's `Bundle Analysis` runs it on this pull request.
No module enters the first-load closure: `useObjectPresence` is already
reached through the header's `sharedUserFeeds`. Only the bytes of the
chunk that holds `RecordDetailView` grow.
## Acceptance notes
- The column branch is transitional. It can be deleted, with its three
column pins, once cloud's framework carries `sys_comment_reaction`;
objectstack-ai/objectstack#22573 retires the column itself. No card is
filed for this.
- If the registry ever settles as `error` on a v17 server, the records
path runs there: reactions read empty and a click is refused with the
error. This is the `useObjectPresence` contract (only an earned `absent`
changes the path). It is a narrow window, and the console's nav and
object views need the same registry anyway.
- A stale screen: when a member has already reacted in another tab and
clicks the same emoji here, the create answers `409 UNIQUE_VIOLATION`.
The chip rolls back with the error until the next read shows the stored
reaction. Nothing is overwritten.
- The feed's `sys_comment` read itself has no page size. That is
unchanged here and only observed.
Session: `https://claude.ai/code/session_01B1gHb9baeX7oioD5sHVm7z`
---
_Generated by [Claude
Code](https://claude.ai/code/session_01B1gHb9baeX7oioD5sHVm7z)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 4d0ff23 commit de302c7
6 files changed
Lines changed: 848 additions & 20 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
Lines changed: 9 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
20 | 25 | | |
21 | 26 | | |
22 | 27 | | |
| |||
124 | 129 | | |
125 | 130 | | |
126 | 131 | | |
127 | | - | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
128 | 136 | | |
129 | 137 | | |
130 | 138 | | |
| |||
0 commit comments