Repository navigation
fix(app-shell): the chatter reads and writes reactions as the member's own sys_comment_reaction records (objectui#12078) - #12090
Conversation
…s own sys_comment_reaction records A reaction click wrote the comment's whole sys_comment.reactions set back with one update, so two members reacting at once left only the later write. Where the deployment has sys_comment_reaction, the chatter now reads the comments' reaction rows in one batched comment_id $in read (paged at 100 ids), groups them into the shape the panel renders, and a click creates or deletes the clicker's own row. Where the object registry earns "absent" (a framework that predates the object), the column path is kept unchanged. Claude-Session: https://claude.ai/code/session_01B1gHb9baeX7oioD5sHVm7z Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B1gHb9baeX7oioD5sHVm7z Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: ① Derived judgmentsInputs: card objectui#12078 (body and all comments); PR #12090 (body, file list, and the net diff against
② Semver level
③ Boundary flagsDev deviations (the
Reviewer flags, none blocking:
Check-runs on the head, read after every run had completed: 43 check-runs, 40 success, 3 skipped by design ( Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #12078
Clause-②: yes
What changed
The record chatter (
RecordDetailViewin@object-ui/app-shell) stores each reaction as the member's ownsys_comment_reactionrecord. It no longer writes the wholesys_comment.reactionscolumn. This follows ruling A, amended, on objectstack-ai/objectstack#22505 (「同意;sys_comment.reactions 可以退役;可以不用考虑历史数据迁移;」) and the director's point6091621568(no read-only aggregate).findonsys_comment_reaction, withcomment_id$inthe 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).{ comment_id, emoji }. The server stampsuser_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 existingdetail.reactionFailederror..objectui-shahold. Where the deployment has nosys_comment_reaction, the column path runs exactly as before, byte for byte. That covers cloud's framework pin56bf27affb(v17). The chatter asks app-shell's existinguseObjectPresence('sys_comment_reaction'); no new probe is written. Following that hook's own contract, only an earnedabsentkeeps 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 isRecordDetailView.tsxand its tests, as declared in the claim.Measurements (the five mechanism assumptions)
The object (objectstack
origin/main86da1949,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 tosys_user, required, stamped from the session on create; a client value is replaced.idandcreated_at.Uniqueness is the index
(comment_id, emoji, user_id),unique: 'organization'. A second identical reaction answers409 UNIQUE_VIOLATION.apiMethodsisget, list, create, delete, bulk, with noupdate.installCommentAccessHooks(authorizeReactionInsert) refuses a reaction on a comment the caller cannot read, and stampsuser_id.owner_only_deletes,created_byequals the caller). So a member deletes their own reaction and not another member's.The fake server in the new pins applies these same three rules.
The cloud framework
56bf27affb. It has nosys-comment-reaction.object.ts(git cat-file -eexits 128; controlsys-comment.object.tsexits 0), andgit grep sys_comment_reactionin itsplugin-audit/srcreturns no hits (control: hits onorigin/main).assertObjectRegistered→objectNotFoundError:404 OBJECT_NOT_FOUNDon find and create alike.data-objectstack'sfindturns 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.origin/maintheobjectmetadata list prunes nothing per caller (createMetaListReadGatereturns the items unchanged forobject), sosys_comment_reactionreadspresentwherever plugin-audit registers it.The
$inread size.$top, andfindDatareturns the full set when no limit is given.findis a GET with the filter as JSON in the query string. Measured with Node 22.22.0 (http.maxHeaderSize16384): 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 answered431at 360 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.What the user sees.
sys_commentupdate, which only the author or a parent editor may make. Now they can, because creating a reaction requires only reading the comment.CommentThread(@object-ui/collaboration) is not on the chatter's path at all. The console chatter renders reactions through plugin-detail'sReactionPickerfromFeedItem.reactions. The only app-shell reference toCommentThreadis a test. No prop changed.RecordDetailView.tsx, four test files beside it and one changeset, and adds noexportline.Clause-②: yesis 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 realRecordDetailViewandReactionPickerover one fake server that several mounted views share, each view signed in as a different member.user_idsent, nosys_commentupdate). The feed renders it grouped, and a fresh read shows👍 2, marked as the clicker's own.👍 2.sys_comment.reactionswrite; 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.Tests 5 failed | 5 passed (10). The concurrent pin went red withexpected [] to deeply equal [ '👍 2' ], and the other four write pins went red with it.git checkout HEAD --, then a blob hash equal to the HEAD blobeba397e8and an emptygit diff HEAD.Verification
Every result below is on HEAD
086db35, the last commit. Each heavy step ran through the shared verify lock.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, onb886ae3, reported type errors in that file, and they were fixed.pnpm exec vitest run --maxWorkers=2 packages/app-shell/src/views/RecordDetailView, covering everyRecordDetailView.*test (the 12 that readsys_commentamong them):Test Files 47 passed (47),Tests 294 passed (294).no-explicit-anyin the new test, the same pattern as its sibling tests. Two more sit on lines this diff edited but did not cause: the existingres: any, and the existing missing-tdependency note.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.mjsandcheck-changeset-no-major.mjs.check-governed-queue-guard.mjs --testanswers NOT GOVERNED for the six paths.detail.reactionFailed.check:eager-closure, because it needs a full console production build. CI'sBundle Analysisruns it on this pull request. No module enters the first-load closure:useObjectPresenceis already reached through the header'ssharedUserFeeds. Only the bytes of the chunk that holdsRecordDetailViewgrow.Acceptance notes
sys_comment_reaction; Retire sys_comment.reactions (ruling A amended on #22505): no aggregate, no data migration, after the console reads reaction records objectstack#22573 retires the column itself. No card is filed for this.erroron a v17 server, the records path runs there: reactions read empty and a click is refused with the error. This is theuseObjectPresencecontract (only an earnedabsentchanges the path). It is a narrow window, and the console's nav and object views need the same registry anyway.409 UNIQUE_VIOLATION. The chip rolls back with the error until the next read shows the stored reaction. Nothing is overwritten.sys_commentread itself has no page size. That is unchanged here and only observed.Session:
https://claude.ai/code/session_01B1gHb9baeX7oioD5sHVm7zGenerated by Claude Code