Skip to content

core&ui: contest balloon notice - #1071

Merged
undefined-moe merged 7 commits into
masterfrom
contest-balloon-notify
Oct 30, 2025
Merged

core&ui: contest balloon notice#1071
undefined-moe merged 7 commits into
masterfrom
contest-balloon-notify

Conversation

@undefined-moe

@undefined-moe undefined-moe commented Oct 29, 2025

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Messages can target multiple recipients.
    • First-solve contest balloon notifications show team avatars and localized text.
    • Notifications support built-in i18n handling and faster delivery.
  • Bug Fixes

    • Message deletion now only allowed by the original sender.
    • Clearer WebAuthn error when a challenge is missing.
    • Contest balloon updates fall back to full reload if fragment missing.
  • Chores

    • More resilient database index creation.
  • Other

    • Read-message handler removed; notification content rendering refined.

@coderabbitai

coderabbitai Bot commented Oct 29, 2025

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

Walkthrough

Message delivery now supports multi-recipient messages: MessageDoc.to accepts number | number[]. Home message handling aggregates messages per recipient, moves template assignment after processing, restricts deletion to message senders, and removes the postRead handler. MessageModel.send includes to in the payload and uses insertOne; setFlag was removed. Contest model adds first-balloon detection and a new exported apply(ctx) hook that emits events and sends localized messages. DB index creation is wrapped in try/catch with logging. UI messaging and notification components add i18n handling, avatar propagation, and adjusted notification behavior/timing.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Areas requiring extra attention:

  • packages/hydrooj/src/model/contest.ts — new apply(ctx) hook, first-balloon detection, event emit vs broadcast, async side effects and index creation.
  • packages/hydrooj/src/handler/home.ts and packages/hydrooj/src/interface.ts — multi-recipient parsing/aggregation, to type change, deletion permission tightening, and template relocation.
  • packages/hydrooj/src/model/message.ts — payload change to include to, switch from insertMany to insertOne, and removal of setFlag; inspect callers and migration impact.
  • packages/ui-default/components/message/worker.ts and packages/ui-default/components/message/index.page.ts — i18n processing flow, ack/i18n key handling, system notification logic, and avatar propagation.
  • packages/hydrooj/src/service/db.ts — index creation error handling and decision to continue after failures.

Possibly related PRs

  • core&ui: add contest private files #1053 — Modifies contest balloon behavior and messaging; appears to overlap with contest apply/first-balloon changes.
  • Hydro v5 #951 — Broad messaging subsystem changes; likely related to MessageDoc.to type and handler/UI message updates.

Suggested reviewers

  • pandadtdyy

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "core&ui: contest balloon notice" directly addresses the primary objective of this changeset. The main feature being introduced is a contest balloon notice system, as evidenced by the new apply(ctx: Context) function in packages/hydrooj/src/model/contest.ts that subscribes to contest balloon events and sends localized messages when first balloons are detected. The supporting changes across home.ts, interface.ts, message.ts, and UI components are infrastructure improvements to enable this core feature. The title uses clear terminology that accurately summarizes the feature without being vague or overly broad.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e017904 and c263a8e.

📒 Files selected for processing (4)
  • packages/hydrooj/locales/zh.yaml (1 hunks)
  • packages/hydrooj/src/model/contest.ts (7 hunks)
  • packages/ui-default/components/notification/index.ts (1 hunks)
  • packages/ui-default/components/notification/notification.page.styl (2 hunks)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
packages/hydrooj/src/model/contest.ts (4)

1111-1128: Harden event handler: add error handling and null-guards; avoid unhandled rejections

  • IIFE has no catch; failures (e.g., DB/network) will produce unhandled rejections.
  • team/tdoc/pdoc may be null; access to team.avatar/pdoc.title would throw.

Apply this patch:

@@
-    ctx.on('contest/balloon', (domainId, tid, bdoc) => {
+    ctx.on('contest/balloon', (domainId, tid, bdoc) => {
         if (!bdoc.first) return;
-        (async () => {
+        (async () => {
             const tsdocs = await getMultiStatus(domainId, { docId: tid, subscribe: 1 }).toArray();
             const uids = Array.from<number>(new Set(tsdocs.map((tsdoc) => tsdoc.uid)));
-            const [team, tdoc, pdoc] = await Promise.all([
+            const [team, tdoc, pdoc] = await Promise.all([
                 UserModel.getById(domainId, bdoc.uid),
                 get(domainId, tid),
                 ProblemModel.get(domainId, bdoc.pid),
             ]);
-            await MessageModel.send(1, uids, JSON.stringify({
+            if (!team || !tdoc || !pdoc || !uids.length) return;
+            await MessageModel.send(1, uids, JSON.stringify({
                 message: 'Team {0} is the first to solve problem {1} ({2})',
-                avatar: avatar(team.avatar),
+                avatar: avatar(team.avatar),
                 params: [team.uname, getAlphabeticId(tdoc.pids.indexOf(bdoc.pid)), pdoc.title],
             }), MessageModel.FLAG_I18N | MessageModel.FLAG_RICHTEXT);
-        })();
+        })().catch((err) => ctx.logger?.error?.('contest/balloon notify failed: %o', err));
     });

1123-1127: Message content/i18n keying

You set FLAG_I18N with a literal English template. If your i18n expects stable keys, prefer a namespaced key (e.g., 'contest.balloon.first') and move the English to locale files.


1125-1126: Problem label fallback

getAlphabeticId(tdoc.pids.indexOf(bdoc.pid)) returns '?' if pid not found. If that’s possible (e.g., rejudge outside pids), consider guarding to avoid confusing output.


1129-1133: LGTM: indexes “basic” and partial “first” are the right constraints for data integrity. Consider adding a read index for { domainId:1, tid:1, pid:1, _id:1 } if you foresee frequent range scans by pid.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 15c58e0 and 720a292.

📒 Files selected for processing (9)
  • packages/hydrooj/src/handler/home.ts (2 hunks)
  • packages/hydrooj/src/handler/user.ts (1 hunks)
  • packages/hydrooj/src/interface.ts (1 hunks)
  • packages/hydrooj/src/model/contest.ts (7 hunks)
  • packages/hydrooj/src/model/message.ts (1 hunks)
  • packages/hydrooj/src/service/db.ts (2 hunks)
  • packages/ui-default/components/message/index.page.ts (4 hunks)
  • packages/ui-default/components/message/worker.ts (2 hunks)
  • packages/ui-default/pages/contest_balloon.page.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
packages/hydrooj/src/service/db.ts (4)
packages/hydrooj/src/model/document.ts (1)
  • coll (19-19)
packages/hydrooj/src/model/user.ts (1)
  • coll (22-22)
packages/hydrooj/src/model/discussion.ts (1)
  • coll (42-42)
packages/hydrooj/src/model/oplog.ts (1)
  • coll (8-8)
packages/hydrooj/src/handler/user.ts (1)
framework/framework/error.ts (1)
  • ForbiddenError (50-50)
packages/ui-default/components/message/index.page.ts (3)
packages/ui-default/constant/message.js (8)
  • FLAG_I18N (5-5)
  • FLAG_I18N (5-5)
  • FLAG_ALERT (2-2)
  • FLAG_ALERT (2-2)
  • FLAG_INFO (4-4)
  • FLAG_INFO (4-4)
  • FLAG_RICHTEXT (3-3)
  • FLAG_RICHTEXT (3-3)
packages/ui-default/utils/base.ts (1)
  • i18n (14-17)
packages/ui-default/components/notification/index.ts (1)
  • Notification (26-83)
packages/ui-default/pages/contest_balloon.page.tsx (1)
packages/ui-default/utils/pjax.js (1)
  • pjax (10-10)
packages/hydrooj/src/model/contest.ts (6)
framework/utils/lib/common.ts (2)
  • sleep (130-134)
  • getAlphabeticId (240-247)
packages/hydrooj/src/model/record.ts (1)
  • RecordModel (20-283)
packages/hydrooj/src/model/message.ts (1)
  • apply (78-84)
packages/hydrooj/src/model/document.ts (2)
  • apply (412-447)
  • getMultiStatus (284-288)
packages/hydrooj/src/context.ts (2)
  • Context (31-41)
  • Context (68-88)
packages/hydrooj/src/model/problem.ts (2)
  • getMultiStatus (228-230)
  • ProblemModel (78-706)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Analyze (javascript)
  • GitHub Check: build
  • GitHub Check: build
🔇 Additional comments (23)
packages/hydrooj/src/service/db.ts (1)

123-127: Verify that silently continuing after index creation failures is acceptable.

Index creation is now wrapped in try/catch blocks that log errors but continue execution. While this prevents startup failures, missing indexes can cause:

  • Significant query performance degradation
  • Unexpected query timeouts or failures
  • Difficult-to-diagnose production issues

Consider whether index creation failures should halt initialization or if there's a monitoring/alerting mechanism to surface these errors beyond logs.

Also applies to: 144-148

packages/ui-default/components/message/worker.ts (2)

43-44: LGTM! Improved notification handling with inter-port messaging.

Moving notification creation from the worker context to the main thread via postMessage is the correct approach, as workers have limited access to browser APIs. The timeout adjustment from 5s to 3s provides quicker user feedback.


90-91: LGTM! Added proper port lifecycle cleanup.

The 'close' and 'error' event handlers ensure ports are removed from the set when they become invalid, preventing memory leaks and message delivery to dead ports.

packages/hydrooj/src/handler/user.ts (1)

177-177: LGTM! More specific error messaging.

Adding the 'no-challenge' message to ForbiddenError improves debuggability for WebAuthn authentication failures.

packages/hydrooj/src/model/message.ts (1)

24-28: LGTM! Efficient multi-recipient message storage.

The shift from inserting one document per recipient (insertMany) to a single document with multiple recipients (insertOne) is more storage-efficient while maintaining broadcast functionality. The to field normalization to an array (line 24) ensures consistent handling of both single and multiple recipients.

packages/hydrooj/src/interface.ts (1)

314-314: LGTM! Interface change enables multi-recipient messaging.

Updating to from number to number | number[] is the foundation for the multi-recipient message support implemented across this PR. Consumers of this interface now need to handle both single and array recipients appropriately.

packages/hydrooj/src/handler/home.ts (2)

569-587: LGTM! Proper multi-recipient message handling.

The updated logic correctly handles the new to field as either a number or array:

  • Line 571: Flattens potential arrays when collecting UIDs
  • Lines 577-586: Creates parsed entries for each target user, ensuring both sent and received messages are properly organized

This aligns well with the MessageDoc.to interface change.


607-607: Verify the impact of restricting deletion to senders only.

Message deletion is now limited to senders (msg.from === this.user._id). Previously, recipients could also delete messages. This is more secure but represents a breaking behavioral change that may affect user expectations.

Consider whether this aligns with your intended UX - users may expect to be able to delete messages from their inbox.

packages/ui-default/components/message/index.page.ts (4)

12-23: LGTM! Avatar preservation for i18n messages.

Line 18 correctly preserves the avatar from i18n content into msg.mdoc.avatar for consistent rendering. This ensures avatars are available regardless of whether the message uses i18n formatting.


24-44: LGTM! Dual notification system with proper gating.

The systemNotification parameter creates two distinct notification paths:

  • In-app notifications (default): Shows alerts/info via dialogs and VjNotification components (lines 24-44)
  • Browser notifications (systemNotification=true): Uses the native Notification API with distinct handling for system messages (uid === 1) vs user messages (lines 46-61)

The early returns (lines 34, 43, 60) prevent unintended fallthrough between notification types.

Also applies to: 46-61


63-78: LGTM! Avatar support in message rendering.

Line 69 conditionally includes the avatar in VjNotification when available for system messages, enhancing the visual presentation of notifications.


94-95: LGTM! Worker notification routing.

The worker correctly routes 'notification' type messages to onmessage with systemNotification=true, ensuring browser notifications are triggered from the worker context.

packages/ui-default/pages/contest_balloon.page.tsx (1)

88-89: No changes needed—selector spelling is correct and intentional.

The verification confirms that constest_balloon-tbody is not a typo. Both the selector in the code and the actual DOM element in the template use the identical spelling consistently. The selector will match successfully, and the PJAX logic works as intended.

Likely an incorrect or invalid review comment.

packages/hydrooj/src/model/contest.ts (10)

4-7: Imports look fine; adding sleep/Context is consistent with new usage.


15-15: avatar import acknowledged.


22-25: New model imports (MessageModel, ProblemModel, RecordModel, UserModel) align with refactor.


238-241: LGTM: switching to UserModel.getListForRender for ACM scoreboard is correct and keeps projection logic centralized.


412-415: LGTM: same refactor for OI scoreboard; consistent with ACM path.


780-784: LGTM: same refactor for Homework scoreboard; consistent use across rules.


882-882: Confirm bus.emit reaches ctx.on('contest/balloon') listeners across workers

Previous patterns use bus.parallel for cross-module events. Ensure emit here propagates to the same bus ctx.on is attached to; otherwise, first-solve notifications may not fire in clustered deployments.


1111-1116: Audience selection: confirm subscribe: 1 semantics

You filter tsdocs by subscribe: 1. Ensure this field actually represents “wants contest notifications”; otherwise, some participants may miss the balloon notice. Consider fallback to participants of the contest if the flag is absent.


1136-1138: LGTM: exporting apply on the global surface is consistent with other models.


1123-1126: MessageModel.send correctly supports array recipients and indexes are adequate

The send method signature already accepts to: number | number[] (line 19 of message.ts), normalizes arrays consistently, and stores each message as a single document with to as an array field. The message collection index { to: 1, _id: -1 } works correctly with array values through MongoDB's automatic multikey indexing—queries like getByUser(uid) that match on the to field will use this index properly. The contest.ts usage at line 1122 passing uids array is supported as designed.

Comment on lines +858 to +876
const balloon = await collBalloon.find({ domainId, tid, pid }).project({ uid: 1 }).toArray();
if (balloon.find((i) => i.uid === uid)) return null;
let isFirst = !balloon.length;
if (isFirst) {
let pending: RecordDoc[] = [];
do {
if (pending.length) await sleep(500); // eslint-disable-line no-await-in-loop
pending = await RecordModel.getMulti(domainId, { // eslint-disable-line no-await-in-loop
pid, contest: tid, _id: { $lt: rid }, status: {
$in: [
STATUS.STATUS_WAITING, STATUS.STATUS_COMPILING,
STATUS.STATUS_JUDGING, STATUS.STATUS_FETCHED,
STATUS.STATUS_ACCEPTED,
],
},
}).limit(1).toArray();
} while (pending.length && !pending.some((i) => i.status === STATUS.STATUS_ACCEPTED));
if (pending.some((i) => i.status === STATUS.STATUS_ACCEPTED)) isFirst = false;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Unbounded wait and missing duplicate-key handling in first-solve detection can stall updates and throw on races

  • The do/while loop can wait indefinitely if a prior record stays in WAITING/COMPILING/JUDGING/FETCHED for long; updateStatus awaits addBalloon, blocking judge flow.
  • On concurrent inserts, partial unique index ('first')/basic unique collisions can throw E11000; no retry/downgrade path.

Recommend: cap wait time and handle duplicate-key by retrying without first or returning gracefully.

Apply this patch:

@@
-    let isFirst = !balloon.length;
+    let isFirst = !balloon.length;
     if (isFirst) {
-        let pending: RecordDoc[] = [];
-        do {
-            if (pending.length) await sleep(500); // eslint-disable-line no-await-in-loop
-            pending = await RecordModel.getMulti(domainId, { // eslint-disable-line no-await-in-loop
+        let pending: RecordDoc[] = [];
+        const waitDeadline = Date.now() + 60_000; // cap wait to 60s to avoid stalling judge path
+        do {
+            if (pending.length) await sleep(500); // eslint-disable-line no-await-in-loop
+            if (Date.now() > waitDeadline) break;
+            pending = await RecordModel.getMulti(domainId, { // eslint-disable-line no-await-in-loop
                 pid, contest: tid, _id: { $lt: rid }, status: {
                     $in: [
                         STATUS.STATUS_WAITING, STATUS.STATUS_COMPILING,
                         STATUS.STATUS_JUDGING, STATUS.STATUS_FETCHED,
                         STATUS.STATUS_ACCEPTED,
                     ],
                 },
             }).limit(1).toArray();
-        } while (pending.length && !pending.some((i) => i.status === STATUS.STATUS_ACCEPTED));
+        } while (pending.length && !pending.some((i) => i.status === STATUS.STATUS_ACCEPTED));
         if (pending.some((i) => i.status === STATUS.STATUS_ACCEPTED)) isFirst = false;
     }
@@
-    await collBalloon.insertOne(newBdoc);
+    try {
+        await collBalloon.insertOne(newBdoc);
+    } catch (e: any) {
+        if (e?.code === 11000) {
+            // Re-check: if user's balloon already exists, treat as no-op
+            const exists = await collBalloon.findOne({ domainId, tid, pid, uid });
+            if (exists) return null;
+            // If 'first' is already taken, retry insert without first flag
+            if (isFirst) {
+                const { first, ...rest } = newBdoc as any;
+                await collBalloon.insertOne(rest);
+            } else {
+                throw e;
+            }
+        } else {
+            throw e;
+        }
+    }

Committable suggestion skipped: line range outside the PR's diff.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 720a292 and e278e79.

📒 Files selected for processing (1)
  • packages/ui-default/components/message/index.page.ts (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/ui-default/components/message/index.page.ts (3)
packages/ui-default/constant/message.js (8)
  • FLAG_I18N (5-5)
  • FLAG_I18N (5-5)
  • FLAG_ALERT (2-2)
  • FLAG_ALERT (2-2)
  • FLAG_INFO (4-4)
  • FLAG_INFO (4-4)
  • FLAG_RICHTEXT (3-3)
  • FLAG_RICHTEXT (3-3)
packages/ui-default/utils/base.ts (1)
  • i18n (14-17)
packages/ui-default/components/notification/index.ts (1)
  • Notification (26-83)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: build
  • GitHub Check: Analyze (javascript)
  • GitHub Check: build

Comment thread packages/ui-default/components/message/index.page.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
packages/hydrooj/src/model/contest.ts (1)

858-876: Critical issues remain from previous review: unbounded wait and missing duplicate-key handling.

The issues flagged in the previous review comment are still present:

  1. Lines 862-874: The do-while loop can wait indefinitely if a prior record remains in WAITING/COMPILING/JUDGING/FETCHED status, blocking the judge flow since updateStatus awaits addBalloon.

  2. Line 880: insertOne can throw E11000 on concurrent inserts due to the unique indexes (basic and partial 'first' index). No duplicate-key handling or retry logic is present.

The previous review provided a detailed fix including a 60-second timeout cap and E11000 handling with graceful retry. Please refer to that suggestion.

Also applies to: 880-880

🧹 Nitpick comments (3)
packages/ui-default/components/message/index.page.ts (2)

13-13: Remove or guard debug console.log.

This debug statement should either be removed for production or wrapped in an environment check like if (process.env.NODE_ENV !== 'production') to avoid console noise.

- console.log('Received message', msg, systemNotification);
+ if (process.env.NODE_ENV !== 'production') console.log('Received message', msg, systemNotification);

49-49: Extract system user ID to a constant.

The magic number 1 (system user ID) appears multiple times. Consider extracting it to a named constant like SYSTEM_USER_ID for better maintainability and clarity.

Define the constant at the top of the file:

const SYSTEM_USER_ID = 1;

Then update the comparisons:

- msg.udoc._id === 1 ? msg.mdoc.content.split('\n')[0] : msg.udoc.uname || 'Hydro Notification',
+ msg.udoc._id === SYSTEM_USER_ID ? msg.mdoc.content.split('\n')[0] : msg.udoc.uname || 'Hydro Notification',
- ...(msg.udoc._id === 1)
+ ...(msg.udoc._id === SYSTEM_USER_ID)

Also applies to: 66-66

packages/ui-default/components/message/worker.ts (1)

43-45: Good delegation pattern, but consider cleanup.

Delegating notification creation to the page context (via ports[0]?.postMessage) is the correct approach since the Notification API must be called from a window context, not a worker. The reduced timeout (3000ms) also improves responsiveness.

Note: The debug console.log on line 43 should be removed or guarded with an environment check.

- console.log('Sending as system notification');
+ if (process.env.NODE_ENV !== 'production') console.log('Sending as system notification');
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e278e79 and e358c3d.

📒 Files selected for processing (3)
  • packages/hydrooj/src/model/contest.ts (7 hunks)
  • packages/ui-default/components/message/index.page.ts (3 hunks)
  • packages/ui-default/components/message/worker.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
packages/ui-default/components/message/index.page.ts (3)
packages/ui-default/constant/message.js (4)
  • FLAG_I18N (5-5)
  • FLAG_I18N (5-5)
  • FLAG_RICHTEXT (3-3)
  • FLAG_RICHTEXT (3-3)
packages/ui-default/components/notification/index.ts (1)
  • Notification (26-83)
packages/ui-default/utils/base.ts (1)
  • i18n (14-17)
packages/hydrooj/src/model/contest.ts (6)
framework/utils/lib/common.ts (2)
  • sleep (130-134)
  • getAlphabeticId (240-247)
packages/hydrooj/src/model/record.ts (1)
  • RecordModel (20-283)
packages/hydrooj/src/model/message.ts (1)
  • apply (78-84)
packages/hydrooj/src/model/document.ts (1)
  • apply (412-447)
packages/hydrooj/src/context.ts (2)
  • Context (31-41)
  • Context (68-88)
packages/hydrooj/src/model/problem.ts (1)
  • ProblemModel (78-706)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: build
  • GitHub Check: Analyze (javascript)
  • GitHub Check: build
🔇 Additional comments (5)
packages/hydrooj/src/model/contest.ts (3)

4-6: LGTM! Import additions support the new functionality.

The new imports are necessary for the first-balloon detection logic (sleep), event subscription (Context), user avatar formatting (avatar), and model-based data access (MessageModel, ProblemModel, RecordModel, UserModel).

Also applies to: 15-15, 22-25


240-240: LGTM! Consistent refactoring to use UserModel wrapper.

The migration from direct user data access to UserModel.getListForRender improves code organization and maintains consistency across ACM, OI, and homework scoreboard implementations.

Also applies to: 414-414, 783-783


1136-1138: LGTM! Properly exports the new apply function.

The apply function is correctly added to the global model registry, following the standard pattern for model initialization hooks.

packages/ui-default/components/message/index.page.ts (2)

95-96: LGTM: Notification delegation to page context.

Routing worker-originated notifications through onmessage(payload, true) correctly delegates browser Notification creation to the page context where the Notification API is available and can respect the document visibility state.


45-61: The type safety concern is not substantiated; the code is correct.

After tracing the complete message flow:

  1. Flag processing (lines 14-23): If FLAG_I18N is set, content is parsed from JSON and converted back to a string via i18n(). If not set, it remains a plain string from the backend.

  2. Early returns (lines 25-37): Messages with FLAG_ALERT or FLAG_INFO exit before reaching the systemNotification block, so those paths don't apply.

  3. System notification source: System notifications are created via MessageModel.sendNotification() (backend message.ts line 73), which sends with FLAG_RICHTEXT (not FLAG_I18N) and contains a plain string.

By the time the systemNotification block executes (line 45), msg.mdoc.content is guaranteed to be a string, making the .split('\n') calls safe.

Comment on lines +1111 to +1134
export async function apply(ctx: Context) {
ctx.on('contest/balloon', (domainId, tid, bdoc) => {
if (!bdoc.first) return;
(async () => {
const tsdocs = await getMultiStatus(domainId, { docId: tid, subscribe: 1 }).toArray();
const uids = Array.from<number>(new Set(tsdocs.map((tsdoc) => tsdoc.uid)));
const [team, tdoc, pdoc] = await Promise.all([
UserModel.getById(domainId, bdoc.uid),
get(domainId, tid),
ProblemModel.get(domainId, bdoc.pid),
]);
await MessageModel.send(1, uids, JSON.stringify({
message: 'Team {0} is the first to solve problem {1} ({2})',
avatar: avatar(team.avatar),
params: [team.uname, getAlphabeticId(tdoc.pids.indexOf(bdoc.pid)), pdoc.title],
}), MessageModel.FLAG_I18N);
})();
});
await ctx.db.ensureIndexes(
collBalloon,
{ key: { domainId: 1, tid: 1, pid: 1, uid: 1 }, unique: true, name: 'basic' },
{ key: { domainId: 1, tid: 1, pid: 1 }, unique: true, name: 'first', partialFilterExpression: { first: true } },
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Add error handling to async IIFE and verify method signatures.

  1. Line 1114: The async IIFE (async () => { ... })(); has no error handling. If any operation inside throws (e.g., database queries, MessageModel.send), it will result in an unhandled promise rejection.

  2. Lines 1129-1133: Index creation is not wrapped in try-catch despite the AI summary claiming it should be. If ctx.db.ensureIndexes fails, the entire apply function will reject.

  3. Line 1132: The partial unique index on first can throw E11000 during concurrent first-solve attempts, which relates to the race condition in addBalloon (line 880).

Apply this diff to add error handling:

     ctx.on('contest/balloon', (domainId, tid, bdoc) => {
         if (!bdoc.first) return;
         (async () => {
             const tsdocs = await getMultiStatus(domainId, { docId: tid, subscribe: 1 }).toArray();
             const uids = Array.from<number>(new Set(tsdocs.map((tsdoc) => tsdoc.uid)));
             const [team, tdoc, pdoc] = await Promise.all([
                 UserModel.getById(domainId, bdoc.uid),
                 get(domainId, tid),
                 ProblemModel.get(domainId, bdoc.pid),
             ]);
             await MessageModel.send(1, uids, JSON.stringify({
                 message: 'Team {0} is the first to solve problem {1} ({2})',
                 avatar: avatar(team.avatar),
                 params: [team.uname, getAlphabeticId(tdoc.pids.indexOf(bdoc.pid)), pdoc.title],
             }), MessageModel.FLAG_I18N);
-        })();
+        })().catch((err) => {
+            ctx.logger.error('Failed to send first balloon notification:', err);
+        });
     });
-    await ctx.db.ensureIndexes(
+    try {
+        await ctx.db.ensureIndexes(
-        collBalloon,
-        { key: { domainId: 1, tid: 1, pid: 1, uid: 1 }, unique: true, name: 'basic' },
-        { key: { domainId: 1, tid: 1, pid: 1 }, unique: true, name: 'first', partialFilterExpression: { first: true } },
-    );
+            collBalloon,
+            { key: { domainId: 1, tid: 1, pid: 1, uid: 1 }, unique: true, name: 'basic' },
+            { key: { domainId: 1, tid: 1, pid: 1 }, unique: true, name: 'first', partialFilterExpression: { first: true } },
+        );
+    } catch (err) {
+        ctx.logger.warn('Failed to create contest balloon indexes:', err);
+    }

Run the following script to verify that the methods used exist:


🏁 Script executed:

#!/bin/bash
# Verify UserModel.getById, MessageModel.send, and MessageModel.FLAG_I18N exist

echo "=== Checking UserModel.getById ==="
ast-grep --pattern $'class UserModel {
  $$$
  getById($$$) {
    $$$
  }
  $$$
}' 

echo "=== Checking MessageModel.send signature ==="
ast-grep --pattern $'class MessageModel {
  $$$
  send($$$) {
    $$$
  }
  $$$
}'

echo "=== Checking MessageModel.FLAG_I18N ==="
rg -n "FLAG_I18N" --type=ts -g "!node_modules"

Length of output: 5982



---

🏁 Script executed:

```shell
#!/bin/bash
# Search for UserModel.getById with more flexible patterns

echo "=== Searching for UserModel.getById (flexible) ==="
rg -n "UserModel\.getById" --type=ts -C3

echo "=== Checking if ctx.logger exists in apply function context ==="
rg -n "ctx\.logger" packages/hydrooj/src/model/contest.ts -C2

echo "=== Checking async IIFE error handling patterns in contest.ts ==="
rg -n "\.catch\(" packages/hydrooj/src/model/contest.ts -B2 -A2

Length of output: 12345


🏁 Script executed:

#!/bin/bash
# Verify MessageModel.sendInfo signature (alternative pattern)
echo "=== Checking MessageModel.sendInfo implementation ==="
rg -n "static async sendInfo" packages/hydrooj/src/model/message.ts -A10

Length of output: 438



---

🏁 Script executed:

```shell
#!/bin/bash
# Search for Context type definition and properties
echo "=== Searching for Context interface definition ==="
rg -n "interface Context" --type=ts -A15

echo "=== Checking error handling in other apply functions in contest.ts ==="
rg -n "export async function apply" packages/hydrooj/src/model/contest.ts -A30

echo "=== Looking for logger usage patterns in contest.ts ==="
rg -n "logger\|console\.\|throw\|Error" packages/hydrooj/src/model/contest.ts | head -20

Length of output: 20664


🏁 Script executed:

#!/bin/bash
# Check how other event handlers in contest.ts handle errors
echo "=== Event handlers in contest.ts that might show error patterns ==="
rg -n "ctx\.on\(" packages/hydrooj/src/model/contest.ts -A10 | head -50

Length of output: 704



---

🏁 Script executed:

```shell
#!/bin/bash
# Verify ctx.logger is available in Cordis-based Context
echo "=== Checking ctx.logger usage in hydrooj package ==="
rg -n "ctx\.logger\." packages/hydrooj/src --type=ts | head -15

echo "=== Checking how event handlers with async IIFE are handled elsewhere ==="
rg -n "ctx\.on\(" packages/hydrooj/src --type=ts -A8 | grep -E "(ctx\.on|catch|then)" | head -20

Length of output: 3201


Add error handling to async IIFE and wrap ensureIndexes in try-catch.

The async IIFE at line 1114-1127 fires without error handling, risking unhandled promise rejections if database queries or MessageModel.send fail. The ensureIndexes call at line 1129 is awaited but unprotected—if it fails, the entire module initialization will fail.

Apply this diff:

     ctx.on('contest/balloon', (domainId, tid, bdoc) => {
         if (!bdoc.first) return;
         (async () => {
             const tsdocs = await getMultiStatus(domainId, { docId: tid, subscribe: 1 }).toArray();
             const uids = Array.from<number>(new Set(tsdocs.map((tsdoc) => tsdoc.uid)));
             const [team, tdoc, pdoc] = await Promise.all([
                 UserModel.getById(domainId, bdoc.uid),
                 get(domainId, tid),
                 ProblemModel.get(domainId, bdoc.pid),
             ]);
             await MessageModel.send(1, uids, JSON.stringify({
                 message: 'Team {0} is the first to solve problem {1} ({2})',
                 avatar: avatar(team.avatar),
                 params: [team.uname, getAlphabeticId(tdoc.pids.indexOf(bdoc.pid)), pdoc.title],
             }), MessageModel.FLAG_I18N);
-        })();
+        })().catch((err) => {
+            ctx.logger.error('Failed to send first balloon notification:', err);
+        });
     });
-    await ctx.db.ensureIndexes(
+    try {
+        await ctx.db.ensureIndexes(
-        collBalloon,
-        { key: { domainId: 1, tid: 1, pid: 1, uid: 1 }, unique: true, name: 'basic' },
-        { key: { domainId: 1, tid: 1, pid: 1 }, unique: true, name: 'first', partialFilterExpression: { first: true } },
-    );
+            collBalloon,
+            { key: { domainId: 1, tid: 1, pid: 1, uid: 1 }, unique: true, name: 'basic' },
+            { key: { domainId: 1, tid: 1, pid: 1 }, unique: true, name: 'first', partialFilterExpression: { first: true } },
+        );
+    } catch (err) {
+        ctx.logger.warn('Failed to create contest balloon indexes:', err);
+    }
🤖 Prompt for AI Agents
In packages/hydrooj/src/model/contest.ts around lines 1111-1134, wrap the async
IIFE invoked in the 'contest/balloon' handler in a try/catch so any errors from
getMultiStatus, UserModel/ProblemModel/get, or MessageModel.send are caught and
logged (use ctx.logger.error if available, otherwise console.error) to avoid
unhandled promise rejections; also wrap the awaited ctx.db.ensureIndexes call in
its own try/catch and log any failure (and decide whether to swallow or rethrow
based on desired startup semantics) so index creation errors are handled instead
of crashing module initialization.

Comment thread packages/ui-default/components/message/worker.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e358c3d and ad7b2b8.

📒 Files selected for processing (2)
  • packages/ui-default/components/message/index.page.ts (2 hunks)
  • packages/ui-default/components/message/worker.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/ui-default/components/message/index.page.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages/ui-default/components/message/worker.ts (2)
packages/ui-default/constant/message.js (2)
  • FLAG_I18N (5-5)
  • FLAG_I18N (5-5)
packages/ui-default/utils/base.ts (1)
  • i18n (14-17)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: build
  • GitHub Check: Analyze (javascript)
  • GitHub Check: build

Comment thread packages/ui-default/components/message/worker.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
packages/ui-default/components/message/worker.ts (2)

17-21: Consider stronger typing for the ack payload.

The payload?: any field loses type safety. Since this is used to pass translated message data back from i18n processing (line 37), consider defining a more specific type that ensures structural compatibility with the original message object.

For example:

+interface MessagePayload {
+  udoc: { _id: number; uname?: string; avatarUrl?: string };
+  mdoc: { _id: string; content: string; flag: number; avatar?: string };
+}
+
 interface RequestAckPayload {
   type: 'ack';
   id: string;
-  payload?: any;
+  payload?: MessagePayload;
 }

32-38: Good solution for the i18n issue, but needs documentation.

The refactored approach successfully resolves the previous critical issue where i18n would crash in the SharedWorker context. By delegating translation to the main thread via the 'i18n' broadcast (line 38) and receiving the translated payload through the ack callback (line 37), you've created a worker-safe i18n mechanism.

However, this two-part ack system (_id for acknowledgment, _id-i18n for translation) is non-obvious and would benefit from comments explaining:

  1. The expected round-trip flow
  2. What structure the i18n ack payload should have
  3. Why both ack mechanisms are needed

Consider adding a brief comment block:

+  // Set up ack handlers: one for user acknowledgment, one for i18n translation payload
   ack[message.mdoc._id] = () => { acked = true; };
   ack[`${message.mdoc._id}-i18n`] = (v) => { payload = v; };
+  // Request i18n translation from main thread if needed
   if (message.mdoc.flag & FLAG_I18N) broadcastMsg({ type: 'i18n', payload: message });
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ad7b2b8 and e017904.

📒 Files selected for processing (2)
  • packages/ui-default/components/message/index.page.ts (3 hunks)
  • packages/ui-default/components/message/worker.ts (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/ui-default/components/message/index.page.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages/ui-default/components/message/worker.ts (1)
packages/ui-default/constant/message.js (4)
  • FLAG_I18N (5-5)
  • FLAG_I18N (5-5)
  • FLAG_INFO (4-4)
  • FLAG_INFO (4-4)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Analyze (javascript)
  • GitHub Check: build
  • GitHub Check: build
🔇 Additional comments (4)
packages/ui-default/components/message/worker.ts (4)

4-4: LGTM!

The FLAG_I18N import is correctly added and used throughout the file.


103-107: LGTM!

The ack handler correctly passes the payload to the callback (line 105), enabling the i18n mechanism. The optional chaining safely handles cases where the ack arrives after the timeout cleanup (lines 40-41).


50-61: Avatar property usage is intentional and correct—no changes needed.

The code appropriately uses different avatar properties based on message type:

  • System notifications (udoc._id === 1) use mdoc.avatar from the message document, which is populated from i18n message content
  • User messages use udoc.avatarUrl, which is computed from udoc.avatar in the handler

This reflects the different data structures: system messages have avatar in the message document, while user messages have avatar information in the user document. The property names are semantically distinct by design.


39-47: Timing concern is valid but likely manageable; consider monitoring in production.

The 3000ms timeout provides a reasonable margin for synchronous i18n translation plus inter-frame messaging. However, the risk is real if the main thread stalls during this window:

  • Translation itself is synchronous (fast, <10ms typical)
  • Risk window: If main thread blocks before the ack callback executes, the i18n ack arrives after the timeout
  • Graceful degradation: The optional chaining on line 105 ensures late acks are safely ignored, and untranslated messages are shown instead of breaking

The code handles this correctly. The recommendation to test on lower-end devices and add telemetry to measure actual i18n ack latency is sound—consider tracking postMessage round-trip timing to validate the 3000ms assumption holds in practice. If telemetry shows consistent late acks (>2000ms), the timeout should be increased.

@undefined-moe
undefined-moe merged commit 053978d into master Oct 30, 2025
4 of 8 checks passed
@undefined-moe
undefined-moe deleted the contest-balloon-notify branch October 30, 2025 17:57
@coderabbitai coderabbitai Bot mentioned this pull request Jul 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant