@juki-team/commons 0.1.45 → 0.1.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -594,6 +594,8 @@ exports.WebSocketSubscriptionEvent = void 0;
594
594
  WebSocketSubscriptionEvent["UNSUBSCRIBE_CONTEST_CHANGES"] = "UNSUBSCRIBE_CONTEST_CHANGES";
595
595
  WebSocketSubscriptionEvent["SUBSCRIBE_USER_NOTIFICATION"] = "SUBSCRIBE_USER_NOTIFICATION";
596
596
  WebSocketSubscriptionEvent["UNSUBSCRIBE_USER_NOTIFICATION"] = "UNSUBSCRIBE_USER_NOTIFICATION";
597
+ WebSocketSubscriptionEvent["SUBSCRIBE_SUBMISSIONS_JUDGING"] = "SUBSCRIBE_SUBMISSIONS_JUDGING";
598
+ WebSocketSubscriptionEvent["UNSUBSCRIBE_SUBMISSIONS_JUDGING"] = "UNSUBSCRIBE_SUBMISSIONS_JUDGING";
597
599
  })(exports.WebSocketSubscriptionEvent || (exports.WebSocketSubscriptionEvent = {}));
598
600
  exports.WebSocketResponseEvent = void 0;
599
601
  (function (WebSocketResponseEvent) {
@@ -612,6 +614,7 @@ exports.WebSocketResponseEvent = void 0;
612
614
  WebSocketResponseEvent["USER_NOTIFICATION_"] = "USER_NOTIFICATION_";
613
615
  WebSocketResponseEvent["USER_NOTIFICATION_SUBMISSION"] = "USER_NOTIFICATION_SUBMISSION";
614
616
  WebSocketResponseEvent["USER_NOTIFICATION_CONTEST_CLARIFICATION"] = "USER_NOTIFICATION_CONTEST_CLARIFICATION";
617
+ WebSocketResponseEvent["SUBMISSION_JUDGING"] = "SUBMISSION_JUDGING";
615
618
  })(exports.WebSocketResponseEvent || (exports.WebSocketResponseEvent = {}));
616
619
  /**
617
620
  * Roles of a contest, with the same names as the contest members in the DB.
@@ -627,6 +630,25 @@ exports.ContestChannelRole = void 0;
627
630
  ContestChannelRole["GUESTS"] = "guests";
628
631
  ContestChannelRole["SPECTATORS"] = "spectators";
629
632
  })(exports.ContestChannelRole || (exports.ContestChannelRole = {}));
633
+ /**
634
+ * Roles of a problem, the ones computed by `getProblemAccessPermissions` (backend).
635
+ * Each role has its own Ably channel: `problem:{problemKey}:{role}` (see `CHANNEL_PROBLEM`).
636
+ */
637
+ exports.ProblemChannelRole = void 0;
638
+ (function (ProblemChannelRole) {
639
+ ProblemChannelRole["ADMINISTRATORS"] = "administrators";
640
+ ProblemChannelRole["MANAGERS"] = "managers";
641
+ ProblemChannelRole["SPECTATORS"] = "spectators";
642
+ })(exports.ProblemChannelRole || (exports.ProblemChannelRole = {}));
643
+ /**
644
+ * Roles of the public channels of a user: `user:{userId}:{role}` (see `CHANNEL_USER_ROLE`).
645
+ * There are no per-user permissions: `spectators` is everyone who can list the submissions of the user.
646
+ * The private channel of the user is `user:{userId}` (see `CHANNEL_USER`).
647
+ */
648
+ exports.UserChannelRole = void 0;
649
+ (function (UserChannelRole) {
650
+ UserChannelRole["SPECTATORS"] = "spectators";
651
+ })(exports.UserChannelRole || (exports.UserChannelRole = {}));
630
652
 
631
653
  exports.DataViewMode = void 0;
632
654
  (function (DataViewMode) {
@@ -2234,12 +2256,14 @@ const EMPTY_USER_PERMISSIONS = {
2234
2256
  const segment = (value) => encodeURIComponent(value);
2235
2257
  /** private: events of one session (code runs, requested data) */
2236
2258
  const CHANNEL_SESSION = (sessionId) => `session:${segment(sessionId)}`;
2237
- /** private: events of one user (own submissions while the contest is frozen, notifications) */
2259
+ /** private: events of one user (own submissions while the contest is frozen, notifications, own problem crawls) */
2238
2260
  const CHANNEL_USER = (userId) => `user:${segment(userId)}`;
2261
+ /** per role: public events of one user (judging of its submissions visible to everyone, see `UserChannelRole`) */
2262
+ const CHANNEL_USER_ROLE = (userId, role) => `user:${segment(userId)}:${role}`;
2239
2263
  /** public: status of a submission when it is visible to everyone */
2240
2264
  const CHANNEL_SUBMISSION = (submitId) => `submission:${segment(submitId)}`;
2241
- /** public: changes of a problem */
2242
- const CHANNEL_PROBLEM = (problemKey) => `problem:${segment(problemKey)}`;
2265
+ /** per role: events of a problem for one role (judging of its submissions, see `ProblemChannelRole`) */
2266
+ const CHANNEL_PROBLEM = (problemKey, role) => `problem:${segment(problemKey)}:${role}`;
2243
2267
  /** per role: events of a contest for one role (and the roles above it, see `ContestChannelRole`) */
2244
2268
  const CHANNEL_CONTEST = (contestKey, role) => `contest:${segment(contestKey)}:${role}`;
2245
2269
  /**
@@ -2250,9 +2274,12 @@ const CHANNEL_CONTEST = (contestKey, role) => `contest:${segment(contestKey)}:${
2250
2274
  * The identity of a member is its `clientId` (`sessionId|uiId`, verified by Ably), never its data.
2251
2275
  */
2252
2276
  const CHANNEL_ORGANIZATION_PRESENCE = (organizationKey) => `presence:organization:${segment(organizationKey)}`;
2253
- // capability resources (wildcards) of the public channels
2277
+ // capability resources (wildcards) of the public channels.
2278
+ // Ably: a trailing `*` replaces one or more segments, any other `*` exactly one segment. So `user:*:spectators`
2279
+ // matches every `user:{userId}:spectators` but never the private `user:{userId}`.
2280
+ // There is no public `problem:*`: it would also match the role channels `problem:{problemKey}:{role}`.
2254
2281
  const CHANNEL_SUBMISSION_ALL = 'submission:*';
2255
- const CHANNEL_PROBLEM_ALL = 'problem:*';
2282
+ const CHANNEL_USER_SPECTATORS_ALL = `user:*:${exports.UserChannelRole.SPECTATORS}`;
2256
2283
  /**
2257
2284
  * Returns the contest and role of a `CHANNEL_CONTEST` channel name, or null if it is not one.
2258
2285
  */
@@ -2263,6 +2290,16 @@ const getContestChannelParams = (channel) => {
2263
2290
  }
2264
2291
  return { contestKey: decodeURIComponent(contestKey), role: role };
2265
2292
  };
2293
+ /**
2294
+ * Returns the problem and role of a `CHANNEL_PROBLEM` channel name, or null if it is not one.
2295
+ */
2296
+ const getProblemChannelParams = (channel) => {
2297
+ const [namespace, problemKey, role, ...rest] = channel.split(':');
2298
+ if (namespace !== 'problem' || !problemKey || rest.length || !Object.values(exports.ProblemChannelRole).includes(role)) {
2299
+ return null;
2300
+ }
2301
+ return { problemKey: decodeURIComponent(problemKey), role: role };
2302
+ };
2266
2303
 
2267
2304
  const NEW_PAGE_SHEET = () => ({
2268
2305
  id: uuid.v4(),
@@ -2974,6 +3011,23 @@ const isUnsubscribeUserNotificationWebSocketEventDto = (event) => {
2974
3011
  return isWebsocketSubscription(event)
2975
3012
  && event?.event === exports.WebSocketSubscriptionEvent.UNSUBSCRIBE_USER_NOTIFICATION;
2976
3013
  };
3014
+ const isNonEmptyString = (value) => typeof value === 'string' && !!value;
3015
+ /**
3016
+ * Exactly one of `problemKey` / `contestKey` / `userId` is set.
3017
+ */
3018
+ const isSubmissionsJudgingScope = (scope) => {
3019
+ return [scope?.problemKey, scope?.contestKey, scope?.userId].filter(isNonEmptyString).length === 1;
3020
+ };
3021
+ const isSubscribeSubmissionsJudgingWebSocketEventDto = (event) => {
3022
+ return isWebsocketSubscription(event)
3023
+ && event?.event === exports.WebSocketSubscriptionEvent.SUBSCRIBE_SUBMISSIONS_JUDGING
3024
+ && isSubmissionsJudgingScope(event);
3025
+ };
3026
+ const isUnsubscribeSubmissionsJudgingWebSocketEventDto = (event) => {
3027
+ return isWebsocketSubscription(event)
3028
+ && event?.event === exports.WebSocketSubscriptionEvent.UNSUBSCRIBE_SUBMISSIONS_JUDGING
3029
+ && isSubmissionsJudgingScope(event);
3030
+ };
2977
3031
  // is WebSocketResponseEventDto, CHANNEL_SUBSCRIBE_CLIENT
2978
3032
  const isWebSocketResponseEventDto = (event) => {
2979
3033
  return Object.values(exports.WebSocketResponseEvent).includes(event?.event)
@@ -2995,6 +3049,18 @@ const isSubmissionRunStatusMessageWebSocketResponseEventDto = (event) => {
2995
3049
  && event?.verdict in exports.ProblemVerdict
2996
3050
  && typeof event?.points === 'number';
2997
3051
  };
3052
+ const isSubmissionJudgingWebSocketResponseEventDto = (event) => {
3053
+ return isWebSocketResponseEventDto(event)
3054
+ && event?.event === exports.WebSocketResponseEvent.SUBMISSION_JUDGING
3055
+ && isNonEmptyString(event?.submitId)
3056
+ && isNonEmptyString(event?.problemKey)
3057
+ && (event?.contestKey === null || isNonEmptyString(event?.contestKey))
3058
+ && isNonEmptyString(event?.userId)
3059
+ && typeof event?.judging === 'boolean'
3060
+ && event?.status in exports.SubmissionRunStatus
3061
+ && event?.verdict in exports.ProblemVerdict
3062
+ && typeof event?.points === 'number';
3063
+ };
2998
3064
  const isUserMessageWebSocketResponseEventDto = (event) => {
2999
3065
  return isWebSocketResponseEventDto(event)
3000
3066
  && event?.event === exports.WebSocketResponseEvent.USER_MESSAGE
@@ -3100,6 +3166,11 @@ const getWebSocketResponseEventKeyOfResponse = (response, clientId) => {
3100
3166
  if (isUserNotificationWebSocketResponseEventDto(response)) {
3101
3167
  return getWebSocketResponseEventKey(exports.WebSocketResponseEvent.USER_NOTIFICATION_, clientId, '*');
3102
3168
  }
3169
+ if (isSubmissionJudgingWebSocketResponseEventDto(response)) {
3170
+ // one key for every table: the same event is published on several channels (problem / contest / user) and the
3171
+ // client deduplicates it, so it is dispatched once; each table filters by the submissions it lists
3172
+ return getWebSocketResponseEventKey(exports.WebSocketResponseEvent.SUBMISSION_JUDGING, clientId, '*');
3173
+ }
3103
3174
  return null;
3104
3175
  };
3105
3176
  /**
@@ -3111,12 +3182,23 @@ const getChannelOfWebSocketSubscribeEvent = (event) => {
3111
3182
  if (isSubscribeSubmissionRunStatusWebSocketEventDto(event)) {
3112
3183
  return CHANNEL_SUBMISSION(event.submitId);
3113
3184
  }
3114
- if (isSubscribeProblemCrawledWebSocketEventDto(event)) {
3115
- return CHANNEL_PROBLEM(event.problemKey);
3116
- }
3185
+ // SUBSCRIBE_PROBLEM_CRAWLED: the event arrives by the private user channel of who requested the crawl
3117
3186
  if (isSubscribeContestChangesWebSocketEventDto(event) || isSubscribeSubmissionsCrawlWebSocketEventDto(event)) {
3118
3187
  return CHANNEL_CONTEST(event.contestKey, exports.ContestChannelRole.SPECTATORS);
3119
3188
  }
3189
+ if (isSubscribeSubmissionsJudgingWebSocketEventDto(event)) {
3190
+ // the judging start/end is published on the spectators channel when it is visible to everyone; the hidden ones
3191
+ // (frozen / quiet contests) arrive by the role channels of the contest (managers) and the private user channel
3192
+ if ('problemKey' in event && event.problemKey) {
3193
+ return CHANNEL_PROBLEM(event.problemKey, exports.ProblemChannelRole.SPECTATORS);
3194
+ }
3195
+ if ('contestKey' in event && event.contestKey) {
3196
+ return CHANNEL_CONTEST(event.contestKey, exports.ContestChannelRole.SPECTATORS);
3197
+ }
3198
+ if ('userId' in event && event.userId) {
3199
+ return CHANNEL_USER_ROLE(event.userId, exports.UserChannelRole.SPECTATORS);
3200
+ }
3201
+ }
3120
3202
  return null;
3121
3203
  };
3122
3204
  /**
@@ -3126,6 +3208,18 @@ const getContestKeyOfWebSocketSubscribeEvent = (event) => {
3126
3208
  if (isSubscribeContestChangesWebSocketEventDto(event) || isSubscribeSubmissionsCrawlWebSocketEventDto(event)) {
3127
3209
  return event.contestKey;
3128
3210
  }
3211
+ if (isSubscribeSubmissionsJudgingWebSocketEventDto(event) && 'contestKey' in event && event.contestKey) {
3212
+ return event.contestKey;
3213
+ }
3214
+ return null;
3215
+ };
3216
+ /**
3217
+ * Problem whose role channels the client needs (the API grants them when the token is requested with this problem).
3218
+ */
3219
+ const getProblemKeyOfWebSocketSubscribeEvent = (event) => {
3220
+ if (isSubscribeSubmissionsJudgingWebSocketEventDto(event) && 'problemKey' in event && event.problemKey) {
3221
+ return event.problemKey;
3222
+ }
3129
3223
  return null;
3130
3224
  };
3131
3225
 
@@ -3327,11 +3421,12 @@ exports.AssignmentType = AssignmentType;
3327
3421
  exports.CHANNEL_CONTEST = CHANNEL_CONTEST;
3328
3422
  exports.CHANNEL_ORGANIZATION_PRESENCE = CHANNEL_ORGANIZATION_PRESENCE;
3329
3423
  exports.CHANNEL_PROBLEM = CHANNEL_PROBLEM;
3330
- exports.CHANNEL_PROBLEM_ALL = CHANNEL_PROBLEM_ALL;
3331
3424
  exports.CHANNEL_SESSION = CHANNEL_SESSION;
3332
3425
  exports.CHANNEL_SUBMISSION = CHANNEL_SUBMISSION;
3333
3426
  exports.CHANNEL_SUBMISSION_ALL = CHANNEL_SUBMISSION_ALL;
3334
3427
  exports.CHANNEL_USER = CHANNEL_USER;
3428
+ exports.CHANNEL_USER_ROLE = CHANNEL_USER_ROLE;
3429
+ exports.CHANNEL_USER_SPECTATORS_ALL = CHANNEL_USER_SPECTATORS_ALL;
3335
3430
  exports.CODE_LANGUAGE = CODE_LANGUAGE;
3336
3431
  exports.CONTEST_ROLE = CONTEST_ROLE;
3337
3432
  exports.CONTEST_STATUS = CONTEST_STATUS;
@@ -3470,8 +3565,10 @@ exports.getMonths = getMonths;
3470
3565
  exports.getParamsOfClientId = getParamsOfClientId;
3471
3566
  exports.getParamsOfUserKey = getParamsOfUserKey;
3472
3567
  exports.getPlainText = getPlainText;
3568
+ exports.getProblemChannelParams = getProblemChannelParams;
3473
3569
  exports.getProblemCommentKey = getProblemCommentKey;
3474
3570
  exports.getProblemJudgeKey = getProblemJudgeKey;
3571
+ exports.getProblemKeyOfWebSocketSubscribeEvent = getProblemKeyOfWebSocketSubscribeEvent;
3475
3572
  exports.getRandomString = getRandomString;
3476
3573
  exports.getSeconds = getSeconds;
3477
3574
  exports.getSummaryWorksheetsInPages = getSummaryWorksheetsInPages;
@@ -3532,14 +3629,17 @@ exports.isSendDataRunCommandWebSocketResponseEventDto = isSendDataRunCommandWebS
3532
3629
  exports.isSendDataSsmSessionsListWebSocketResponseEventDto = isSendDataSsmSessionsListWebSocketResponseEventDto;
3533
3630
  exports.isSendDataWebSocketResponseEventDto = isSendDataWebSocketResponseEventDto;
3534
3631
  exports.isStringJson = isStringJson;
3632
+ exports.isSubmissionJudgingWebSocketResponseEventDto = isSubmissionJudgingWebSocketResponseEventDto;
3535
3633
  exports.isSubmissionRunStatusMessageWebSocketResponseEventDto = isSubmissionRunStatusMessageWebSocketResponseEventDto;
3536
3634
  exports.isSubmissionsCrawlWebSocketResponseEventDto = isSubmissionsCrawlWebSocketResponseEventDto;
3635
+ exports.isSubmissionsJudgingScope = isSubmissionsJudgingScope;
3537
3636
  exports.isSubscribeCodeRunStatusWebSocketEventDto = isSubscribeCodeRunStatusWebSocketEventDto;
3538
3637
  exports.isSubscribeContestChangesWebSocketEventDto = isSubscribeContestChangesWebSocketEventDto;
3539
3638
  exports.isSubscribeGetDataWebSocketEventDto = isSubscribeGetDataWebSocketEventDto;
3540
3639
  exports.isSubscribeProblemCrawledWebSocketEventDto = isSubscribeProblemCrawledWebSocketEventDto;
3541
3640
  exports.isSubscribeSubmissionRunStatusWebSocketEventDto = isSubscribeSubmissionRunStatusWebSocketEventDto;
3542
3641
  exports.isSubscribeSubmissionsCrawlWebSocketEventDto = isSubscribeSubmissionsCrawlWebSocketEventDto;
3642
+ exports.isSubscribeSubmissionsJudgingWebSocketEventDto = isSubscribeSubmissionsJudgingWebSocketEventDto;
3543
3643
  exports.isSubscribeUserNotificationWebSocketEventDto = isSubscribeUserNotificationWebSocketEventDto;
3544
3644
  exports.isTeamMember = isTeamMember;
3545
3645
  exports.isToday = isToday;
@@ -3549,6 +3649,7 @@ exports.isUnsubscribeGetDataWebSocketEventDto = isUnsubscribeGetDataWebSocketEve
3549
3649
  exports.isUnsubscribeProblemCrawledWebSocketEventDto = isUnsubscribeProblemCrawledWebSocketEventDto;
3550
3650
  exports.isUnsubscribeSubmissionRunStatusWebSocketEventDto = isUnsubscribeSubmissionRunStatusWebSocketEventDto;
3551
3651
  exports.isUnsubscribeSubmissionsCrawlWebSocketEventDto = isUnsubscribeSubmissionsCrawlWebSocketEventDto;
3652
+ exports.isUnsubscribeSubmissionsJudgingWebSocketEventDto = isUnsubscribeSubmissionsJudgingWebSocketEventDto;
3552
3653
  exports.isUnsubscribeUserNotificationWebSocketEventDto = isUnsubscribeUserNotificationWebSocketEventDto;
3553
3654
  exports.isUserMember = isUserMember;
3554
3655
  exports.isUserMessageWebSocketResponseEventDto = isUserMessageWebSocketResponseEventDto;
package/dist/esm/index.js CHANGED
@@ -592,6 +592,8 @@ var WebSocketSubscriptionEvent;
592
592
  WebSocketSubscriptionEvent["UNSUBSCRIBE_CONTEST_CHANGES"] = "UNSUBSCRIBE_CONTEST_CHANGES";
593
593
  WebSocketSubscriptionEvent["SUBSCRIBE_USER_NOTIFICATION"] = "SUBSCRIBE_USER_NOTIFICATION";
594
594
  WebSocketSubscriptionEvent["UNSUBSCRIBE_USER_NOTIFICATION"] = "UNSUBSCRIBE_USER_NOTIFICATION";
595
+ WebSocketSubscriptionEvent["SUBSCRIBE_SUBMISSIONS_JUDGING"] = "SUBSCRIBE_SUBMISSIONS_JUDGING";
596
+ WebSocketSubscriptionEvent["UNSUBSCRIBE_SUBMISSIONS_JUDGING"] = "UNSUBSCRIBE_SUBMISSIONS_JUDGING";
595
597
  })(WebSocketSubscriptionEvent || (WebSocketSubscriptionEvent = {}));
596
598
  var WebSocketResponseEvent;
597
599
  (function (WebSocketResponseEvent) {
@@ -610,6 +612,7 @@ var WebSocketResponseEvent;
610
612
  WebSocketResponseEvent["USER_NOTIFICATION_"] = "USER_NOTIFICATION_";
611
613
  WebSocketResponseEvent["USER_NOTIFICATION_SUBMISSION"] = "USER_NOTIFICATION_SUBMISSION";
612
614
  WebSocketResponseEvent["USER_NOTIFICATION_CONTEST_CLARIFICATION"] = "USER_NOTIFICATION_CONTEST_CLARIFICATION";
615
+ WebSocketResponseEvent["SUBMISSION_JUDGING"] = "SUBMISSION_JUDGING";
613
616
  })(WebSocketResponseEvent || (WebSocketResponseEvent = {}));
614
617
  /**
615
618
  * Roles of a contest, with the same names as the contest members in the DB.
@@ -625,6 +628,25 @@ var ContestChannelRole;
625
628
  ContestChannelRole["GUESTS"] = "guests";
626
629
  ContestChannelRole["SPECTATORS"] = "spectators";
627
630
  })(ContestChannelRole || (ContestChannelRole = {}));
631
+ /**
632
+ * Roles of a problem, the ones computed by `getProblemAccessPermissions` (backend).
633
+ * Each role has its own Ably channel: `problem:{problemKey}:{role}` (see `CHANNEL_PROBLEM`).
634
+ */
635
+ var ProblemChannelRole;
636
+ (function (ProblemChannelRole) {
637
+ ProblemChannelRole["ADMINISTRATORS"] = "administrators";
638
+ ProblemChannelRole["MANAGERS"] = "managers";
639
+ ProblemChannelRole["SPECTATORS"] = "spectators";
640
+ })(ProblemChannelRole || (ProblemChannelRole = {}));
641
+ /**
642
+ * Roles of the public channels of a user: `user:{userId}:{role}` (see `CHANNEL_USER_ROLE`).
643
+ * There are no per-user permissions: `spectators` is everyone who can list the submissions of the user.
644
+ * The private channel of the user is `user:{userId}` (see `CHANNEL_USER`).
645
+ */
646
+ var UserChannelRole;
647
+ (function (UserChannelRole) {
648
+ UserChannelRole["SPECTATORS"] = "spectators";
649
+ })(UserChannelRole || (UserChannelRole = {}));
628
650
 
629
651
  var DataViewMode;
630
652
  (function (DataViewMode) {
@@ -2232,12 +2254,14 @@ const EMPTY_USER_PERMISSIONS = {
2232
2254
  const segment = (value) => encodeURIComponent(value);
2233
2255
  /** private: events of one session (code runs, requested data) */
2234
2256
  const CHANNEL_SESSION = (sessionId) => `session:${segment(sessionId)}`;
2235
- /** private: events of one user (own submissions while the contest is frozen, notifications) */
2257
+ /** private: events of one user (own submissions while the contest is frozen, notifications, own problem crawls) */
2236
2258
  const CHANNEL_USER = (userId) => `user:${segment(userId)}`;
2259
+ /** per role: public events of one user (judging of its submissions visible to everyone, see `UserChannelRole`) */
2260
+ const CHANNEL_USER_ROLE = (userId, role) => `user:${segment(userId)}:${role}`;
2237
2261
  /** public: status of a submission when it is visible to everyone */
2238
2262
  const CHANNEL_SUBMISSION = (submitId) => `submission:${segment(submitId)}`;
2239
- /** public: changes of a problem */
2240
- const CHANNEL_PROBLEM = (problemKey) => `problem:${segment(problemKey)}`;
2263
+ /** per role: events of a problem for one role (judging of its submissions, see `ProblemChannelRole`) */
2264
+ const CHANNEL_PROBLEM = (problemKey, role) => `problem:${segment(problemKey)}:${role}`;
2241
2265
  /** per role: events of a contest for one role (and the roles above it, see `ContestChannelRole`) */
2242
2266
  const CHANNEL_CONTEST = (contestKey, role) => `contest:${segment(contestKey)}:${role}`;
2243
2267
  /**
@@ -2248,9 +2272,12 @@ const CHANNEL_CONTEST = (contestKey, role) => `contest:${segment(contestKey)}:${
2248
2272
  * The identity of a member is its `clientId` (`sessionId|uiId`, verified by Ably), never its data.
2249
2273
  */
2250
2274
  const CHANNEL_ORGANIZATION_PRESENCE = (organizationKey) => `presence:organization:${segment(organizationKey)}`;
2251
- // capability resources (wildcards) of the public channels
2275
+ // capability resources (wildcards) of the public channels.
2276
+ // Ably: a trailing `*` replaces one or more segments, any other `*` exactly one segment. So `user:*:spectators`
2277
+ // matches every `user:{userId}:spectators` but never the private `user:{userId}`.
2278
+ // There is no public `problem:*`: it would also match the role channels `problem:{problemKey}:{role}`.
2252
2279
  const CHANNEL_SUBMISSION_ALL = 'submission:*';
2253
- const CHANNEL_PROBLEM_ALL = 'problem:*';
2280
+ const CHANNEL_USER_SPECTATORS_ALL = `user:*:${UserChannelRole.SPECTATORS}`;
2254
2281
  /**
2255
2282
  * Returns the contest and role of a `CHANNEL_CONTEST` channel name, or null if it is not one.
2256
2283
  */
@@ -2261,6 +2288,16 @@ const getContestChannelParams = (channel) => {
2261
2288
  }
2262
2289
  return { contestKey: decodeURIComponent(contestKey), role: role };
2263
2290
  };
2291
+ /**
2292
+ * Returns the problem and role of a `CHANNEL_PROBLEM` channel name, or null if it is not one.
2293
+ */
2294
+ const getProblemChannelParams = (channel) => {
2295
+ const [namespace, problemKey, role, ...rest] = channel.split(':');
2296
+ if (namespace !== 'problem' || !problemKey || rest.length || !Object.values(ProblemChannelRole).includes(role)) {
2297
+ return null;
2298
+ }
2299
+ return { problemKey: decodeURIComponent(problemKey), role: role };
2300
+ };
2264
2301
 
2265
2302
  const NEW_PAGE_SHEET = () => ({
2266
2303
  id: v4(),
@@ -2972,6 +3009,23 @@ const isUnsubscribeUserNotificationWebSocketEventDto = (event) => {
2972
3009
  return isWebsocketSubscription(event)
2973
3010
  && event?.event === WebSocketSubscriptionEvent.UNSUBSCRIBE_USER_NOTIFICATION;
2974
3011
  };
3012
+ const isNonEmptyString = (value) => typeof value === 'string' && !!value;
3013
+ /**
3014
+ * Exactly one of `problemKey` / `contestKey` / `userId` is set.
3015
+ */
3016
+ const isSubmissionsJudgingScope = (scope) => {
3017
+ return [scope?.problemKey, scope?.contestKey, scope?.userId].filter(isNonEmptyString).length === 1;
3018
+ };
3019
+ const isSubscribeSubmissionsJudgingWebSocketEventDto = (event) => {
3020
+ return isWebsocketSubscription(event)
3021
+ && event?.event === WebSocketSubscriptionEvent.SUBSCRIBE_SUBMISSIONS_JUDGING
3022
+ && isSubmissionsJudgingScope(event);
3023
+ };
3024
+ const isUnsubscribeSubmissionsJudgingWebSocketEventDto = (event) => {
3025
+ return isWebsocketSubscription(event)
3026
+ && event?.event === WebSocketSubscriptionEvent.UNSUBSCRIBE_SUBMISSIONS_JUDGING
3027
+ && isSubmissionsJudgingScope(event);
3028
+ };
2975
3029
  // is WebSocketResponseEventDto, CHANNEL_SUBSCRIBE_CLIENT
2976
3030
  const isWebSocketResponseEventDto = (event) => {
2977
3031
  return Object.values(WebSocketResponseEvent).includes(event?.event)
@@ -2993,6 +3047,18 @@ const isSubmissionRunStatusMessageWebSocketResponseEventDto = (event) => {
2993
3047
  && event?.verdict in ProblemVerdict
2994
3048
  && typeof event?.points === 'number';
2995
3049
  };
3050
+ const isSubmissionJudgingWebSocketResponseEventDto = (event) => {
3051
+ return isWebSocketResponseEventDto(event)
3052
+ && event?.event === WebSocketResponseEvent.SUBMISSION_JUDGING
3053
+ && isNonEmptyString(event?.submitId)
3054
+ && isNonEmptyString(event?.problemKey)
3055
+ && (event?.contestKey === null || isNonEmptyString(event?.contestKey))
3056
+ && isNonEmptyString(event?.userId)
3057
+ && typeof event?.judging === 'boolean'
3058
+ && event?.status in SubmissionRunStatus
3059
+ && event?.verdict in ProblemVerdict
3060
+ && typeof event?.points === 'number';
3061
+ };
2996
3062
  const isUserMessageWebSocketResponseEventDto = (event) => {
2997
3063
  return isWebSocketResponseEventDto(event)
2998
3064
  && event?.event === WebSocketResponseEvent.USER_MESSAGE
@@ -3098,6 +3164,11 @@ const getWebSocketResponseEventKeyOfResponse = (response, clientId) => {
3098
3164
  if (isUserNotificationWebSocketResponseEventDto(response)) {
3099
3165
  return getWebSocketResponseEventKey(WebSocketResponseEvent.USER_NOTIFICATION_, clientId, '*');
3100
3166
  }
3167
+ if (isSubmissionJudgingWebSocketResponseEventDto(response)) {
3168
+ // one key for every table: the same event is published on several channels (problem / contest / user) and the
3169
+ // client deduplicates it, so it is dispatched once; each table filters by the submissions it lists
3170
+ return getWebSocketResponseEventKey(WebSocketResponseEvent.SUBMISSION_JUDGING, clientId, '*');
3171
+ }
3101
3172
  return null;
3102
3173
  };
3103
3174
  /**
@@ -3109,12 +3180,23 @@ const getChannelOfWebSocketSubscribeEvent = (event) => {
3109
3180
  if (isSubscribeSubmissionRunStatusWebSocketEventDto(event)) {
3110
3181
  return CHANNEL_SUBMISSION(event.submitId);
3111
3182
  }
3112
- if (isSubscribeProblemCrawledWebSocketEventDto(event)) {
3113
- return CHANNEL_PROBLEM(event.problemKey);
3114
- }
3183
+ // SUBSCRIBE_PROBLEM_CRAWLED: the event arrives by the private user channel of who requested the crawl
3115
3184
  if (isSubscribeContestChangesWebSocketEventDto(event) || isSubscribeSubmissionsCrawlWebSocketEventDto(event)) {
3116
3185
  return CHANNEL_CONTEST(event.contestKey, ContestChannelRole.SPECTATORS);
3117
3186
  }
3187
+ if (isSubscribeSubmissionsJudgingWebSocketEventDto(event)) {
3188
+ // the judging start/end is published on the spectators channel when it is visible to everyone; the hidden ones
3189
+ // (frozen / quiet contests) arrive by the role channels of the contest (managers) and the private user channel
3190
+ if ('problemKey' in event && event.problemKey) {
3191
+ return CHANNEL_PROBLEM(event.problemKey, ProblemChannelRole.SPECTATORS);
3192
+ }
3193
+ if ('contestKey' in event && event.contestKey) {
3194
+ return CHANNEL_CONTEST(event.contestKey, ContestChannelRole.SPECTATORS);
3195
+ }
3196
+ if ('userId' in event && event.userId) {
3197
+ return CHANNEL_USER_ROLE(event.userId, UserChannelRole.SPECTATORS);
3198
+ }
3199
+ }
3118
3200
  return null;
3119
3201
  };
3120
3202
  /**
@@ -3124,6 +3206,18 @@ const getContestKeyOfWebSocketSubscribeEvent = (event) => {
3124
3206
  if (isSubscribeContestChangesWebSocketEventDto(event) || isSubscribeSubmissionsCrawlWebSocketEventDto(event)) {
3125
3207
  return event.contestKey;
3126
3208
  }
3209
+ if (isSubscribeSubmissionsJudgingWebSocketEventDto(event) && 'contestKey' in event && event.contestKey) {
3210
+ return event.contestKey;
3211
+ }
3212
+ return null;
3213
+ };
3214
+ /**
3215
+ * Problem whose role channels the client needs (the API grants them when the token is requested with this problem).
3216
+ */
3217
+ const getProblemKeyOfWebSocketSubscribeEvent = (event) => {
3218
+ if (isSubscribeSubmissionsJudgingWebSocketEventDto(event) && 'problemKey' in event && event.problemKey) {
3219
+ return event.problemKey;
3220
+ }
3127
3221
  return null;
3128
3222
  };
3129
3223
 
@@ -3318,4 +3412,4 @@ const isQuizTextSheet = (value) => {
3318
3412
  && ['text', 'number', 'textarea'].includes(value?.inputType);
3319
3413
  };
3320
3414
 
3321
- export { ACCEPTED_PROGRAMMING_LANGUAGES, ASPECT_RATIO, AspectRatio, AssignmentClass, AssignmentState, AssignmentType, CHANNEL_CONTEST, CHANNEL_ORGANIZATION_PRESENCE, CHANNEL_PROBLEM, CHANNEL_PROBLEM_ALL, CHANNEL_SESSION, CHANNEL_SUBMISSION, CHANNEL_SUBMISSION_ALL, CHANNEL_USER, CODE_LANGUAGE, CONTEST_ROLE, CONTEST_STATUS, COOKIE_JUKI_SESSION_ID, COURSE_ROLE, CodeLanguage, CollectionKey, ContestChannelRole, ContestEventAction, ContestProblemBlockedByType, ContestProblemPrerequisiteType, ContestStatus, ContestTimeStatus, CourseRole, CourseStatus, CourseType, DAY_NAMES, DEFAULT_JUKI_PROFILE_IMAGE, DataViewMode, EMPTY_DOCUMENT_MEMBERS, EMPTY_ENTITY_MEMBERS, EMPTY_USER_PERMISSIONS, ENTITY_ACCESS, ENTITY_STATE, ERROR, EmailSubscription, EmailValidationState, EntityAccess, EntityCollection, EntityMembersRank, EntityState, EntityStatus, ErrorCode, FILE_ROLE, FILE_TYPE, FileMemberRole, FileType, FilesJukiPub, GroupType, HEADER_JUKI_FORWARDED_HOST, HEADER_JUKI_INTERNAL_API_KEY, HEADER_JUKI_METADATA, HEADER_JUKI_SECRET_TOKEN, HEADER_JUKI_VISITOR_SESSION_ID, HOURS, HTTPMethod, JUDGE, JUKI_APP_EMAIL_TEMPLATE, JUKI_APP_ORGANIZATION_KEY, JkError, Judge, LANGUAGE, LOWERCASE_LETTERS, Language, LogLevel, LogOperation, MAX_DATE, MEMBER_ROLE, MILLISECONDS, MINUTES, MIN_DATE, MONTH_NAMES, MemberRole, MemberType, MenuViewMode, NEW_PAGE_SHEET, NUMBERS, NotificationType, ONE_DAY, ONE_HOUR, ONE_MINUTE, ONE_SECOND, ONE_WEEK, ORGANIZATION_PLAN, OrganizationLogoType, OrganizationPlan, PAGE, PAGE_SIZE, PAGE_SIZES, PALLETE, PROBLEM_INPUT, PROBLEM_MODE, PROBLEM_MODES, PROBLEM_OUTPUT, PROBLEM_ROLE, PROBLEM_TYPE, PROBLEM_TYPES, PROBLEM_VERDICT, PROBLEM_VERDICTS, ProblemInput, ProblemOutput, ProblemScoringMode, ProblemType, ProblemVerdict, ProfileSetting, QuizScoringMode, RUNNER_ACCEPTED_PROBLEM_MODES, RUNNER_ACCEPTED_PROBLEM_TYPES, RUNNER_ACCEPTED_PROGRAMMING_LANGUAGES, RUNNER_TYPE, Role, RunnerType, SECONDS, SEC_120, SEC_30, SEC_60, SEC_90, SEPARATOR_TOKEN, SUBMISSION_RUN_STATUS, SUPPORT_EMAIL, SYSTEM_ROLE, ScopeData, Status, SubmissionRunStatus, SystemRole, TEAM_ROLE, THEME, TeamRole, TeamStatus, Theme, TypeTestCase, UPPERCASE_LETTERS, USER_ROLE, USER_STATUS, UserRole, UserStatus, UserTeamStatus, WebSocketResponseEvent, WebSocketSubscriptionEvent, WorkingIn, WorksheetType, YEARS, _KEY, _SUB_SUB_TAB, _SUB_TAB, _TAB, _TYPE, areArraysDifferent, changeDay, changeHours, changeMilliseconds, changeMinutes, changeMonth, changeSeconds, changeYear, chunkString, cleanRequest, clearAsyncInterval, consoleError, consoleInfo, consoleWarn, contentResponse, contentsResponse, decreaseDay, decreaseMonth, decreaseYear, endOfDay, endOfHour, endOfMinute, endOfMonth, endOfSecond, endOfWeek, endOfYear, errorsResponse, getAssignmentCommentKey, getAssignmentUserCommentKey, getChannelOfWebSocketSubscribeEvent, getClientId, getContestChannelParams, getContestCommentKey, getContestKeyOfWebSocketSubscribeEvent, getDataLog, getDataOfTestCase, getDays, getDefaultMeta, getDocumentAccess, getHours, getMilliseconds, getMinutes, getMonths, getParamsOfClientId, getParamsOfUserKey, getPlainText, getProblemCommentKey, getProblemJudgeKey, getRandomString, getSeconds, getSummaryWorksheetsInPages, getTotalExercisesOfSummaryWorksheetsInPages, getUserKey, getVerdictFromTestCase, getWebSocketResponseEventKey, getWebSocketResponseEventKeyOfResponse, getWorksheetsInPages, getYears, humanFileSize, increaseDay, increaseMonth, increaseYear, indexToLetters, isAfter, isBasicWorksheet, isBefore, isCodeEditorSheet, isCodeRunStatusMessageWebSocketResponseEventDto, isContestAssignment, isContestChangesWebSocketResponseEventDto, isCourseAssignment, isDayAfter, isDayBefore, isEndlessContest, isEqual, isFutureContest, isGlobalContest, isHoursAfter, isHoursBefore, isJkmdSheet, isMillisecondsAfter, isMillisecondsBefore, isMinutesAfter, isMinutesBefore, isMonthAfter, isMonthBefore, isObjectJson, isPastContest, isProblemCrawledWebSocketResponseEventDto, isQuizOptionsSheet, isQuizProblemSheet, isQuizTextSheet, isSameDay, isSameHour, isSameMillisecond, isSameMinute, isSameMonth, isSameSecond, isSameYear, isSecondsAfter, isSecondsBefore, isSendDataEc2InstancesListWebSocketResponseEventDto, isSendDataEcsTaskDefinitionListWebSocketResponseEventDto, isSendDataEcsTasksListWebSocketResponseEventDto, isSendDataRunCommandWebSocketResponseEventDto, isSendDataSsmSessionsListWebSocketResponseEventDto, isSendDataWebSocketResponseEventDto, isStringJson, isSubmissionRunStatusMessageWebSocketResponseEventDto, isSubmissionsCrawlWebSocketResponseEventDto, isSubscribeCodeRunStatusWebSocketEventDto, isSubscribeContestChangesWebSocketEventDto, isSubscribeGetDataWebSocketEventDto, isSubscribeProblemCrawledWebSocketEventDto, isSubscribeSubmissionRunStatusWebSocketEventDto, isSubscribeSubmissionsCrawlWebSocketEventDto, isSubscribeUserNotificationWebSocketEventDto, isTeamMember, isToday, isUnsubscribeCodeRunStatusWebSocketEventDto, isUnsubscribeContestChangesWebSocketEventDto, isUnsubscribeGetDataWebSocketEventDto, isUnsubscribeProblemCrawledWebSocketEventDto, isUnsubscribeSubmissionRunStatusWebSocketEventDto, isUnsubscribeSubmissionsCrawlWebSocketEventDto, isUnsubscribeUserNotificationWebSocketEventDto, isUserMember, isUserMessageWebSocketResponseEventDto, isUserNotificationContestClarificationWebSocketResponseEventDto, isUserNotificationSubmissionWebSocketResponseEventDto, isUserNotificationWebSocketResponseEventDto, isValidDate, isWebSocketChannelsMessageDto, isWebSocketResponseEventDto, isWebsocketSubscription, isWithinInterval, isWorksheetAssignment, isYearAfter, isYearBefore, join, lettersToIndex, memorySizeOf, mergeVerdicts, mex, normalizeToLocalStartOfDay, normalizeToLocalStartOfHour, normalizeToLocalStartOfMinutes, normalizeToLocalStartOfSeconds, objectUpdate, objectsUpdate, padEnd, padStart, range, removeExtension, safeJsonParse, setAsyncInterval, split, splitTime, startOfDay, startOfHour, startOfMinute, startOfMonth, startOfSecond, startOfWeek, startOfYear, stringToArrayBuffer, toEntityMembersDto, toJkError };
3415
+ export { ACCEPTED_PROGRAMMING_LANGUAGES, ASPECT_RATIO, AspectRatio, AssignmentClass, AssignmentState, AssignmentType, CHANNEL_CONTEST, CHANNEL_ORGANIZATION_PRESENCE, CHANNEL_PROBLEM, CHANNEL_SESSION, CHANNEL_SUBMISSION, CHANNEL_SUBMISSION_ALL, CHANNEL_USER, CHANNEL_USER_ROLE, CHANNEL_USER_SPECTATORS_ALL, CODE_LANGUAGE, CONTEST_ROLE, CONTEST_STATUS, COOKIE_JUKI_SESSION_ID, COURSE_ROLE, CodeLanguage, CollectionKey, ContestChannelRole, ContestEventAction, ContestProblemBlockedByType, ContestProblemPrerequisiteType, ContestStatus, ContestTimeStatus, CourseRole, CourseStatus, CourseType, DAY_NAMES, DEFAULT_JUKI_PROFILE_IMAGE, DataViewMode, EMPTY_DOCUMENT_MEMBERS, EMPTY_ENTITY_MEMBERS, EMPTY_USER_PERMISSIONS, ENTITY_ACCESS, ENTITY_STATE, ERROR, EmailSubscription, EmailValidationState, EntityAccess, EntityCollection, EntityMembersRank, EntityState, EntityStatus, ErrorCode, FILE_ROLE, FILE_TYPE, FileMemberRole, FileType, FilesJukiPub, GroupType, HEADER_JUKI_FORWARDED_HOST, HEADER_JUKI_INTERNAL_API_KEY, HEADER_JUKI_METADATA, HEADER_JUKI_SECRET_TOKEN, HEADER_JUKI_VISITOR_SESSION_ID, HOURS, HTTPMethod, JUDGE, JUKI_APP_EMAIL_TEMPLATE, JUKI_APP_ORGANIZATION_KEY, JkError, Judge, LANGUAGE, LOWERCASE_LETTERS, Language, LogLevel, LogOperation, MAX_DATE, MEMBER_ROLE, MILLISECONDS, MINUTES, MIN_DATE, MONTH_NAMES, MemberRole, MemberType, MenuViewMode, NEW_PAGE_SHEET, NUMBERS, NotificationType, ONE_DAY, ONE_HOUR, ONE_MINUTE, ONE_SECOND, ONE_WEEK, ORGANIZATION_PLAN, OrganizationLogoType, OrganizationPlan, PAGE, PAGE_SIZE, PAGE_SIZES, PALLETE, PROBLEM_INPUT, PROBLEM_MODE, PROBLEM_MODES, PROBLEM_OUTPUT, PROBLEM_ROLE, PROBLEM_TYPE, PROBLEM_TYPES, PROBLEM_VERDICT, PROBLEM_VERDICTS, ProblemChannelRole, ProblemInput, ProblemOutput, ProblemScoringMode, ProblemType, ProblemVerdict, ProfileSetting, QuizScoringMode, RUNNER_ACCEPTED_PROBLEM_MODES, RUNNER_ACCEPTED_PROBLEM_TYPES, RUNNER_ACCEPTED_PROGRAMMING_LANGUAGES, RUNNER_TYPE, Role, RunnerType, SECONDS, SEC_120, SEC_30, SEC_60, SEC_90, SEPARATOR_TOKEN, SUBMISSION_RUN_STATUS, SUPPORT_EMAIL, SYSTEM_ROLE, ScopeData, Status, SubmissionRunStatus, SystemRole, TEAM_ROLE, THEME, TeamRole, TeamStatus, Theme, TypeTestCase, UPPERCASE_LETTERS, USER_ROLE, USER_STATUS, UserChannelRole, UserRole, UserStatus, UserTeamStatus, WebSocketResponseEvent, WebSocketSubscriptionEvent, WorkingIn, WorksheetType, YEARS, _KEY, _SUB_SUB_TAB, _SUB_TAB, _TAB, _TYPE, areArraysDifferent, changeDay, changeHours, changeMilliseconds, changeMinutes, changeMonth, changeSeconds, changeYear, chunkString, cleanRequest, clearAsyncInterval, consoleError, consoleInfo, consoleWarn, contentResponse, contentsResponse, decreaseDay, decreaseMonth, decreaseYear, endOfDay, endOfHour, endOfMinute, endOfMonth, endOfSecond, endOfWeek, endOfYear, errorsResponse, getAssignmentCommentKey, getAssignmentUserCommentKey, getChannelOfWebSocketSubscribeEvent, getClientId, getContestChannelParams, getContestCommentKey, getContestKeyOfWebSocketSubscribeEvent, getDataLog, getDataOfTestCase, getDays, getDefaultMeta, getDocumentAccess, getHours, getMilliseconds, getMinutes, getMonths, getParamsOfClientId, getParamsOfUserKey, getPlainText, getProblemChannelParams, getProblemCommentKey, getProblemJudgeKey, getProblemKeyOfWebSocketSubscribeEvent, getRandomString, getSeconds, getSummaryWorksheetsInPages, getTotalExercisesOfSummaryWorksheetsInPages, getUserKey, getVerdictFromTestCase, getWebSocketResponseEventKey, getWebSocketResponseEventKeyOfResponse, getWorksheetsInPages, getYears, humanFileSize, increaseDay, increaseMonth, increaseYear, indexToLetters, isAfter, isBasicWorksheet, isBefore, isCodeEditorSheet, isCodeRunStatusMessageWebSocketResponseEventDto, isContestAssignment, isContestChangesWebSocketResponseEventDto, isCourseAssignment, isDayAfter, isDayBefore, isEndlessContest, isEqual, isFutureContest, isGlobalContest, isHoursAfter, isHoursBefore, isJkmdSheet, isMillisecondsAfter, isMillisecondsBefore, isMinutesAfter, isMinutesBefore, isMonthAfter, isMonthBefore, isObjectJson, isPastContest, isProblemCrawledWebSocketResponseEventDto, isQuizOptionsSheet, isQuizProblemSheet, isQuizTextSheet, isSameDay, isSameHour, isSameMillisecond, isSameMinute, isSameMonth, isSameSecond, isSameYear, isSecondsAfter, isSecondsBefore, isSendDataEc2InstancesListWebSocketResponseEventDto, isSendDataEcsTaskDefinitionListWebSocketResponseEventDto, isSendDataEcsTasksListWebSocketResponseEventDto, isSendDataRunCommandWebSocketResponseEventDto, isSendDataSsmSessionsListWebSocketResponseEventDto, isSendDataWebSocketResponseEventDto, isStringJson, isSubmissionJudgingWebSocketResponseEventDto, isSubmissionRunStatusMessageWebSocketResponseEventDto, isSubmissionsCrawlWebSocketResponseEventDto, isSubmissionsJudgingScope, isSubscribeCodeRunStatusWebSocketEventDto, isSubscribeContestChangesWebSocketEventDto, isSubscribeGetDataWebSocketEventDto, isSubscribeProblemCrawledWebSocketEventDto, isSubscribeSubmissionRunStatusWebSocketEventDto, isSubscribeSubmissionsCrawlWebSocketEventDto, isSubscribeSubmissionsJudgingWebSocketEventDto, isSubscribeUserNotificationWebSocketEventDto, isTeamMember, isToday, isUnsubscribeCodeRunStatusWebSocketEventDto, isUnsubscribeContestChangesWebSocketEventDto, isUnsubscribeGetDataWebSocketEventDto, isUnsubscribeProblemCrawledWebSocketEventDto, isUnsubscribeSubmissionRunStatusWebSocketEventDto, isUnsubscribeSubmissionsCrawlWebSocketEventDto, isUnsubscribeSubmissionsJudgingWebSocketEventDto, isUnsubscribeUserNotificationWebSocketEventDto, isUserMember, isUserMessageWebSocketResponseEventDto, isUserNotificationContestClarificationWebSocketResponseEventDto, isUserNotificationSubmissionWebSocketResponseEventDto, isUserNotificationWebSocketResponseEventDto, isValidDate, isWebSocketChannelsMessageDto, isWebSocketResponseEventDto, isWebsocketSubscription, isWithinInterval, isWorksheetAssignment, isYearAfter, isYearBefore, join, lettersToIndex, memorySizeOf, mergeVerdicts, mex, normalizeToLocalStartOfDay, normalizeToLocalStartOfHour, normalizeToLocalStartOfMinutes, normalizeToLocalStartOfSeconds, objectUpdate, objectsUpdate, padEnd, padStart, range, removeExtension, safeJsonParse, setAsyncInterval, split, splitTime, startOfDay, startOfHour, startOfMinute, startOfMonth, startOfSecond, startOfWeek, startOfYear, stringToArrayBuffer, toEntityMembersDto, toJkError };
@@ -1,12 +1,14 @@
1
- import { ContestChannelRole } from '../types';
1
+ import { ContestChannelRole, ProblemChannelRole, UserChannelRole } from '../types';
2
2
  /** private: events of one session (code runs, requested data) */
3
3
  export declare const CHANNEL_SESSION: (sessionId: string) => string;
4
- /** private: events of one user (own submissions while the contest is frozen, notifications) */
4
+ /** private: events of one user (own submissions while the contest is frozen, notifications, own problem crawls) */
5
5
  export declare const CHANNEL_USER: (userId: string) => string;
6
+ /** per role: public events of one user (judging of its submissions visible to everyone, see `UserChannelRole`) */
7
+ export declare const CHANNEL_USER_ROLE: (userId: string, role: UserChannelRole) => string;
6
8
  /** public: status of a submission when it is visible to everyone */
7
9
  export declare const CHANNEL_SUBMISSION: (submitId: string) => string;
8
- /** public: changes of a problem */
9
- export declare const CHANNEL_PROBLEM: (problemKey: string) => string;
10
+ /** per role: events of a problem for one role (judging of its submissions, see `ProblemChannelRole`) */
11
+ export declare const CHANNEL_PROBLEM: (problemKey: string, role: ProblemChannelRole) => string;
10
12
  /** per role: events of a contest for one role (and the roles above it, see `ContestChannelRole`) */
11
13
  export declare const CHANNEL_CONTEST: (contestKey: string, role: ContestChannelRole) => string;
12
14
  /**
@@ -18,7 +20,7 @@ export declare const CHANNEL_CONTEST: (contestKey: string, role: ContestChannelR
18
20
  */
19
21
  export declare const CHANNEL_ORGANIZATION_PRESENCE: (organizationKey: string) => string;
20
22
  export declare const CHANNEL_SUBMISSION_ALL = "submission:*";
21
- export declare const CHANNEL_PROBLEM_ALL = "problem:*";
23
+ export declare const CHANNEL_USER_SPECTATORS_ALL = "user:*:spectators";
22
24
  /**
23
25
  * Returns the contest and role of a `CHANNEL_CONTEST` channel name, or null if it is not one.
24
26
  */
@@ -26,3 +28,10 @@ export declare const getContestChannelParams: (channel: string) => {
26
28
  contestKey: string;
27
29
  role: ContestChannelRole;
28
30
  } | null;
31
+ /**
32
+ * Returns the problem and role of a `CHANNEL_PROBLEM` channel name, or null if it is not one.
33
+ */
34
+ export declare const getProblemChannelParams: (channel: string) => {
35
+ problemKey: string;
36
+ role: ProblemChannelRole;
37
+ } | null;
@@ -63,8 +63,25 @@ export interface UnsubscribeUserNotificationWebSocketEventDto extends WebsocketS
63
63
  event: WebSocketSubscriptionEvent.UNSUBSCRIBE_USER_NOTIFICATION;
64
64
  userKey: string;
65
65
  }
66
- export type WebSocketSubscribeEventDto = SubscribeCodeRunStatusWebSocketEventDto | SubscribeSubmissionRunStatusWebSocketEventDto | SubscribeGetDataWebSocketEventDto | SubscribeProblemCrawledWebSocketEventDto | SubscribeSubmissionsCrawlWebSocketEventDto | SubscribeContestChangesWebSocketEventDto | SubscribeUserNotificationWebsocketEventDto;
67
- export type WebSocketUnsubscribeEventDto = UnsubscribeCodeRunStatusWebSocketEventDto | UnsubscribeSubmissionRunStatusWebSocketEventDto | UnsubscribeGetDataWebSocketEventDto | UnsubscribeProblemCrawledWebSocketEventDto | UnsubscribeSubmissionsCrawlWebSocketEventDto | UnsubscribeContestChangesWebSocketEventDto | UnsubscribeUserNotificationWebSocketEventDto;
66
+ /**
67
+ * Scope of a submissions table: the judging start/end of the submissions of one problem, one contest or one user.
68
+ * Exactly one of the keys is set.
69
+ */
70
+ export type SubmissionsJudgingScope = {
71
+ problemKey: string;
72
+ } | {
73
+ contestKey: string;
74
+ } | {
75
+ userId: string;
76
+ };
77
+ export type SubscribeSubmissionsJudgingWebSocketEventDto = WebsocketSubscription & {
78
+ event: WebSocketSubscriptionEvent.SUBSCRIBE_SUBMISSIONS_JUDGING;
79
+ } & SubmissionsJudgingScope;
80
+ export type UnsubscribeSubmissionsJudgingWebSocketEventDto = WebsocketSubscription & {
81
+ event: WebSocketSubscriptionEvent.UNSUBSCRIBE_SUBMISSIONS_JUDGING;
82
+ } & SubmissionsJudgingScope;
83
+ export type WebSocketSubscribeEventDto = SubscribeCodeRunStatusWebSocketEventDto | SubscribeSubmissionRunStatusWebSocketEventDto | SubscribeGetDataWebSocketEventDto | SubscribeProblemCrawledWebSocketEventDto | SubscribeSubmissionsCrawlWebSocketEventDto | SubscribeContestChangesWebSocketEventDto | SubscribeUserNotificationWebsocketEventDto | SubscribeSubmissionsJudgingWebSocketEventDto;
84
+ export type WebSocketUnsubscribeEventDto = UnsubscribeCodeRunStatusWebSocketEventDto | UnsubscribeSubmissionRunStatusWebSocketEventDto | UnsubscribeGetDataWebSocketEventDto | UnsubscribeProblemCrawledWebSocketEventDto | UnsubscribeSubmissionsCrawlWebSocketEventDto | UnsubscribeContestChangesWebSocketEventDto | UnsubscribeUserNotificationWebSocketEventDto | UnsubscribeSubmissionsJudgingWebSocketEventDto;
68
85
  export interface WebSocketResponse {
69
86
  event: WebSocketResponseEvent;
70
87
  key: WebSocketResponseEventKey;
@@ -86,6 +103,23 @@ export interface SubmissionRunStatusWebSocketResponseEventDto extends WebSocketR
86
103
  testInfo?: TestInfo;
87
104
  shouldValidateContest: boolean;
88
105
  }
106
+ /**
107
+ * Start (`judging: true`, status RECEIVED) and end (`judging: false`, status COMPLETED / FAILED) of the judging of a
108
+ * submission, also on rejudges. Published on the channels of the tables that list it (problem / contest / user), so a
109
+ * table listens to one channel and only attaches `CHANNEL_SUBMISSION` (the case by case progress) of the submissions
110
+ * on screen that are being judged. The case by case events keep arriving as `SubmissionRunStatusWebSocketResponseEventDto`.
111
+ */
112
+ export interface SubmissionJudgingWebSocketResponseEventDto extends WebSocketResponse {
113
+ event: WebSocketResponseEvent.SUBMISSION_JUDGING;
114
+ submitId: string;
115
+ problemKey: string;
116
+ contestKey: string | null;
117
+ userId: string;
118
+ judging: boolean;
119
+ status: SubmissionRunStatus;
120
+ verdict: ProblemVerdict;
121
+ points: number;
122
+ }
89
123
  export interface UserMessageWebSocketResponseEventDto extends WebSocketResponse {
90
124
  event: WebSocketResponseEvent.USER_MESSAGE;
91
125
  user: UserOrganizationBasicInfoResponseDto;
@@ -179,7 +213,7 @@ export interface UserNotificationContestClarificationWebSocketResponseEventDto e
179
213
  contestKey: string;
180
214
  };
181
215
  }
182
- export type WebSocketResponseEventDto = CodeRunStatusWebSocketResponseEventDto | SubmissionRunStatusWebSocketResponseEventDto | UserMessageWebSocketResponseEventDto | SendDataEcsTaskDefinitionListWebSocketResponseEventDto | SendDataEcsTasksListWebSocketResponseEventDto | SendDataEc2InstancesListWebSocketResponseEventDto | SendDataSsmSessionsListWebSocketResponseEventDto | ProblemCrawledWebSocketResponseEventDto | SubmissionsCrawlWebSocketResponseEventDto | ContestChangesWebSocketResponseEventDto | SendDataRunCommandWebSocketResponseEventDto | UserNotificationSubmissionWebSocketResponseEventDto | UserNotificationContestClarificationWebSocketResponseEventDto;
216
+ export type WebSocketResponseEventDto = CodeRunStatusWebSocketResponseEventDto | SubmissionRunStatusWebSocketResponseEventDto | UserMessageWebSocketResponseEventDto | SendDataEcsTaskDefinitionListWebSocketResponseEventDto | SendDataEcsTasksListWebSocketResponseEventDto | SendDataEc2InstancesListWebSocketResponseEventDto | SendDataSsmSessionsListWebSocketResponseEventDto | ProblemCrawledWebSocketResponseEventDto | SubmissionsCrawlWebSocketResponseEventDto | ContestChangesWebSocketResponseEventDto | SendDataRunCommandWebSocketResponseEventDto | UserNotificationSubmissionWebSocketResponseEventDto | UserNotificationContestClarificationWebSocketResponseEventDto | SubmissionJudgingWebSocketResponseEventDto;
183
217
  /**
184
218
  * Message sent by the backend producers to the websocket SQS queue:
185
219
  * the producer (trusted, with access to the DB) decides the Ably channels where the event is published.
@@ -1,4 +1,4 @@
1
- import { CodeRunStatusWebSocketResponseEventDto, ContestChangesWebSocketResponseEventDto, ProblemCrawledWebSocketResponseEventDto, SendDataEc2InstancesListWebSocketResponseEventDto, SendDataEcsTaskDefinitionListWebSocketResponseEventDto, SendDataEcsTasksListWebSocketResponseEventDto, SendDataRunCommandWebSocketResponseEventDto, SendDataSsmSessionsListWebSocketResponseEventDto, SendDataWebSocketResponseEventDto, SubmissionRunStatusWebSocketResponseEventDto, SubmissionsCrawlWebSocketResponseEventDto, SubscribeCodeRunStatusWebSocketEventDto, SubscribeContestChangesWebSocketEventDto, SubscribeGetDataWebSocketEventDto, SubscribeProblemCrawledWebSocketEventDto, SubscribeSubmissionRunStatusWebSocketEventDto, SubscribeSubmissionsCrawlWebSocketEventDto, SubscribeUserNotificationWebsocketEventDto, UnsubscribeCodeRunStatusWebSocketEventDto, UnsubscribeContestChangesWebSocketEventDto, UnsubscribeGetDataWebSocketEventDto, UnsubscribeProblemCrawledWebSocketEventDto, UnsubscribeSubmissionRunStatusWebSocketEventDto, UnsubscribeSubmissionsCrawlWebSocketEventDto, UnsubscribeUserNotificationWebSocketEventDto, UserMessageWebSocketResponseEventDto, UserNotificationContestClarificationWebSocketResponseEventDto, UserNotificationSubmissionWebSocketResponseEventDto, UserNotificationWebSocketResponseEventDto, WebSocketChannelsMessageDto, WebSocketResponseEventDto, WebSocketSubscribeEventDto, WebSocketUnsubscribeEventDto } from '../dto';
1
+ import { CodeRunStatusWebSocketResponseEventDto, ContestChangesWebSocketResponseEventDto, ProblemCrawledWebSocketResponseEventDto, SendDataEc2InstancesListWebSocketResponseEventDto, SendDataEcsTaskDefinitionListWebSocketResponseEventDto, SendDataEcsTasksListWebSocketResponseEventDto, SendDataRunCommandWebSocketResponseEventDto, SendDataSsmSessionsListWebSocketResponseEventDto, SendDataWebSocketResponseEventDto, SubmissionJudgingWebSocketResponseEventDto, SubmissionRunStatusWebSocketResponseEventDto, SubmissionsCrawlWebSocketResponseEventDto, SubmissionsJudgingScope, SubscribeCodeRunStatusWebSocketEventDto, SubscribeContestChangesWebSocketEventDto, SubscribeGetDataWebSocketEventDto, SubscribeProblemCrawledWebSocketEventDto, SubscribeSubmissionRunStatusWebSocketEventDto, SubscribeSubmissionsCrawlWebSocketEventDto, SubscribeSubmissionsJudgingWebSocketEventDto, SubscribeUserNotificationWebsocketEventDto, UnsubscribeCodeRunStatusWebSocketEventDto, UnsubscribeContestChangesWebSocketEventDto, UnsubscribeGetDataWebSocketEventDto, UnsubscribeProblemCrawledWebSocketEventDto, UnsubscribeSubmissionRunStatusWebSocketEventDto, UnsubscribeSubmissionsCrawlWebSocketEventDto, UnsubscribeSubmissionsJudgingWebSocketEventDto, UnsubscribeUserNotificationWebSocketEventDto, UserMessageWebSocketResponseEventDto, UserNotificationContestClarificationWebSocketResponseEventDto, UserNotificationSubmissionWebSocketResponseEventDto, UserNotificationWebSocketResponseEventDto, WebSocketChannelsMessageDto, WebSocketResponseEventDto, WebSocketSubscribeEventDto, WebSocketUnsubscribeEventDto } from '../dto';
2
2
  import { ClientId, WebSocketResponseEvent, WebSocketResponseEventKey } from '../types';
3
3
  export declare const isWebsocketSubscription: (event: any) => event is WebSocketSubscribeEventDto | WebSocketUnsubscribeEventDto;
4
4
  export declare const isSubscribeCodeRunStatusWebSocketEventDto: (event: any) => event is SubscribeCodeRunStatusWebSocketEventDto;
@@ -15,9 +15,16 @@ export declare const isSubscribeContestChangesWebSocketEventDto: (event: any) =>
15
15
  export declare const isUnsubscribeContestChangesWebSocketEventDto: (event: any) => event is UnsubscribeContestChangesWebSocketEventDto;
16
16
  export declare const isSubscribeUserNotificationWebSocketEventDto: (event: any) => event is SubscribeUserNotificationWebsocketEventDto;
17
17
  export declare const isUnsubscribeUserNotificationWebSocketEventDto: (event: any) => event is UnsubscribeUserNotificationWebSocketEventDto;
18
+ /**
19
+ * Exactly one of `problemKey` / `contestKey` / `userId` is set.
20
+ */
21
+ export declare const isSubmissionsJudgingScope: (scope: any) => scope is SubmissionsJudgingScope;
22
+ export declare const isSubscribeSubmissionsJudgingWebSocketEventDto: (event: any) => event is SubscribeSubmissionsJudgingWebSocketEventDto;
23
+ export declare const isUnsubscribeSubmissionsJudgingWebSocketEventDto: (event: any) => event is UnsubscribeSubmissionsJudgingWebSocketEventDto;
18
24
  export declare const isWebSocketResponseEventDto: (event: any) => event is WebSocketResponseEventDto;
19
25
  export declare const isCodeRunStatusMessageWebSocketResponseEventDto: (event: any) => event is CodeRunStatusWebSocketResponseEventDto;
20
26
  export declare const isSubmissionRunStatusMessageWebSocketResponseEventDto: (event: any) => event is SubmissionRunStatusWebSocketResponseEventDto;
27
+ export declare const isSubmissionJudgingWebSocketResponseEventDto: (event: any) => event is SubmissionJudgingWebSocketResponseEventDto;
21
28
  export declare const isUserMessageWebSocketResponseEventDto: (event: any) => event is UserMessageWebSocketResponseEventDto;
22
29
  export declare const isSendDataWebSocketResponseEventDto: (event: any) => event is SendDataWebSocketResponseEventDto;
23
30
  export declare const isSendDataEcsTaskDefinitionListWebSocketResponseEventDto: (event: any) => event is SendDataEcsTaskDefinitionListWebSocketResponseEventDto;
@@ -49,3 +56,7 @@ export declare const getChannelOfWebSocketSubscribeEvent: (event: WebSocketSubsc
49
56
  * Contest whose role channels the client needs (the API grants them when the token is requested with this contest).
50
57
  */
51
58
  export declare const getContestKeyOfWebSocketSubscribeEvent: (event: WebSocketSubscribeEventDto) => string | null;
59
+ /**
60
+ * Problem whose role channels the client needs (the API grants them when the token is requested with this problem).
61
+ */
62
+ export declare const getProblemKeyOfWebSocketSubscribeEvent: (event: WebSocketSubscribeEventDto) => string | null;
@@ -17,7 +17,9 @@ export declare enum WebSocketSubscriptionEvent {
17
17
  SUBSCRIBE_CONTEST_CHANGES = "SUBSCRIBE_CONTEST_CHANGES",
18
18
  UNSUBSCRIBE_CONTEST_CHANGES = "UNSUBSCRIBE_CONTEST_CHANGES",
19
19
  SUBSCRIBE_USER_NOTIFICATION = "SUBSCRIBE_USER_NOTIFICATION",
20
- UNSUBSCRIBE_USER_NOTIFICATION = "UNSUBSCRIBE_USER_NOTIFICATION"
20
+ UNSUBSCRIBE_USER_NOTIFICATION = "UNSUBSCRIBE_USER_NOTIFICATION",
21
+ SUBSCRIBE_SUBMISSIONS_JUDGING = "SUBSCRIBE_SUBMISSIONS_JUDGING",
22
+ UNSUBSCRIBE_SUBMISSIONS_JUDGING = "UNSUBSCRIBE_SUBMISSIONS_JUDGING"
21
23
  }
22
24
  export declare enum WebSocketResponseEvent {
23
25
  CODE_RUN_STATUS = "CODE_RUN_STATUS",
@@ -34,7 +36,8 @@ export declare enum WebSocketResponseEvent {
34
36
  CONTEST_CHANGES = "CONTEST_CHANGES",
35
37
  USER_NOTIFICATION_ = "USER_NOTIFICATION_",
36
38
  USER_NOTIFICATION_SUBMISSION = "USER_NOTIFICATION_SUBMISSION",
37
- USER_NOTIFICATION_CONTEST_CLARIFICATION = "USER_NOTIFICATION_CONTEST_CLARIFICATION"
39
+ USER_NOTIFICATION_CONTEST_CLARIFICATION = "USER_NOTIFICATION_CONTEST_CLARIFICATION",
40
+ SUBMISSION_JUDGING = "SUBMISSION_JUDGING"
38
41
  }
39
42
  export type ClientId = `${RecordId}|${string}`;
40
43
  export type WebSocketResponseEventKey = `${WebSocketResponseEvent}-${ClientId}-${string}`;
@@ -51,6 +54,23 @@ export declare enum ContestChannelRole {
51
54
  GUESTS = "guests",
52
55
  SPECTATORS = "spectators"
53
56
  }
57
+ /**
58
+ * Roles of a problem, the ones computed by `getProblemAccessPermissions` (backend).
59
+ * Each role has its own Ably channel: `problem:{problemKey}:{role}` (see `CHANNEL_PROBLEM`).
60
+ */
61
+ export declare enum ProblemChannelRole {
62
+ ADMINISTRATORS = "administrators",
63
+ MANAGERS = "managers",
64
+ SPECTATORS = "spectators"
65
+ }
66
+ /**
67
+ * Roles of the public channels of a user: `user:{userId}:{role}` (see `CHANNEL_USER_ROLE`).
68
+ * There are no per-user permissions: `spectators` is everyone who can list the submissions of the user.
69
+ * The private channel of the user is `user:{userId}` (see `CHANNEL_USER`).
70
+ */
71
+ export declare enum UserChannelRole {
72
+ SPECTATORS = "spectators"
73
+ }
54
74
  export type InfoLogCaseStatus = {
55
75
  inputKey: string;
56
76
  out: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juki-team/commons",
3
- "version": "0.1.45",
3
+ "version": "0.1.46",
4
4
  "author": "juki-team",
5
5
  "license": "MIT",
6
6
  "type": "module",