@juki-team/commons 0.1.45 → 0.1.47

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,26 @@ 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:{userKey}:{role}` (see `CHANNEL_USER_ROLE`), by its public key
645
+ * (`getUserKey(nickname, organizationKey)`) like problems and contests.
646
+ * There are no per-user permissions: `spectators` is everyone who can list the submissions of the user.
647
+ * The private channel of the user is `user:{userId}` (see `CHANNEL_USER`).
648
+ */
649
+ exports.UserChannelRole = void 0;
650
+ (function (UserChannelRole) {
651
+ UserChannelRole["SPECTATORS"] = "spectators";
652
+ })(exports.UserChannelRole || (exports.UserChannelRole = {}));
630
653
 
631
654
  exports.DataViewMode = void 0;
632
655
  (function (DataViewMode) {
@@ -2234,12 +2257,18 @@ const EMPTY_USER_PERMISSIONS = {
2234
2257
  const segment = (value) => encodeURIComponent(value);
2235
2258
  /** private: events of one session (code runs, requested data) */
2236
2259
  const CHANNEL_SESSION = (sessionId) => `session:${segment(sessionId)}`;
2237
- /** private: events of one user (own submissions while the contest is frozen, notifications) */
2260
+ /** private: events of one user (own submissions while the contest is frozen, notifications, own problem crawls) */
2238
2261
  const CHANNEL_USER = (userId) => `user:${segment(userId)}`;
2262
+ /**
2263
+ * per role: public events of one user (judging of its submissions visible to everyone, see `UserChannelRole`).
2264
+ * By its public key (`getUserKey(nickname, organizationKey)`), the one the clients know; the private channel
2265
+ * (`CHANNEL_USER`) is by id. They never collide: the role channel has one more segment.
2266
+ */
2267
+ const CHANNEL_USER_ROLE = (userKey, role) => `user:${segment(userKey)}:${role}`;
2239
2268
  /** public: status of a submission when it is visible to everyone */
2240
2269
  const CHANNEL_SUBMISSION = (submitId) => `submission:${segment(submitId)}`;
2241
- /** public: changes of a problem */
2242
- const CHANNEL_PROBLEM = (problemKey) => `problem:${segment(problemKey)}`;
2270
+ /** per role: events of a problem for one role (judging of its submissions, see `ProblemChannelRole`) */
2271
+ const CHANNEL_PROBLEM = (problemKey, role) => `problem:${segment(problemKey)}:${role}`;
2243
2272
  /** per role: events of a contest for one role (and the roles above it, see `ContestChannelRole`) */
2244
2273
  const CHANNEL_CONTEST = (contestKey, role) => `contest:${segment(contestKey)}:${role}`;
2245
2274
  /**
@@ -2250,9 +2279,12 @@ const CHANNEL_CONTEST = (contestKey, role) => `contest:${segment(contestKey)}:${
2250
2279
  * The identity of a member is its `clientId` (`sessionId|uiId`, verified by Ably), never its data.
2251
2280
  */
2252
2281
  const CHANNEL_ORGANIZATION_PRESENCE = (organizationKey) => `presence:organization:${segment(organizationKey)}`;
2253
- // capability resources (wildcards) of the public channels
2282
+ // capability resources (wildcards) of the public channels.
2283
+ // Ably: a trailing `*` replaces one or more segments, any other `*` exactly one segment. So `user:*:spectators`
2284
+ // matches every `user:{userKey}:spectators` but never the private `user:{userId}`.
2285
+ // There is no public `problem:*`: it would also match the role channels `problem:{problemKey}:{role}`.
2254
2286
  const CHANNEL_SUBMISSION_ALL = 'submission:*';
2255
- const CHANNEL_PROBLEM_ALL = 'problem:*';
2287
+ const CHANNEL_USER_SPECTATORS_ALL = `user:*:${exports.UserChannelRole.SPECTATORS}`;
2256
2288
  /**
2257
2289
  * Returns the contest and role of a `CHANNEL_CONTEST` channel name, or null if it is not one.
2258
2290
  */
@@ -2263,6 +2295,16 @@ const getContestChannelParams = (channel) => {
2263
2295
  }
2264
2296
  return { contestKey: decodeURIComponent(contestKey), role: role };
2265
2297
  };
2298
+ /**
2299
+ * Returns the problem and role of a `CHANNEL_PROBLEM` channel name, or null if it is not one.
2300
+ */
2301
+ const getProblemChannelParams = (channel) => {
2302
+ const [namespace, problemKey, role, ...rest] = channel.split(':');
2303
+ if (namespace !== 'problem' || !problemKey || rest.length || !Object.values(exports.ProblemChannelRole).includes(role)) {
2304
+ return null;
2305
+ }
2306
+ return { problemKey: decodeURIComponent(problemKey), role: role };
2307
+ };
2266
2308
 
2267
2309
  const NEW_PAGE_SHEET = () => ({
2268
2310
  id: uuid.v4(),
@@ -2974,6 +3016,23 @@ const isUnsubscribeUserNotificationWebSocketEventDto = (event) => {
2974
3016
  return isWebsocketSubscription(event)
2975
3017
  && event?.event === exports.WebSocketSubscriptionEvent.UNSUBSCRIBE_USER_NOTIFICATION;
2976
3018
  };
3019
+ const isNonEmptyString = (value) => typeof value === 'string' && !!value;
3020
+ /**
3021
+ * Exactly one of `problemKey` / `contestKey` / `userKey` is set.
3022
+ */
3023
+ const isSubmissionsJudgingScope = (scope) => {
3024
+ return [scope?.problemKey, scope?.contestKey, scope?.userKey].filter(isNonEmptyString).length === 1;
3025
+ };
3026
+ const isSubscribeSubmissionsJudgingWebSocketEventDto = (event) => {
3027
+ return isWebsocketSubscription(event)
3028
+ && event?.event === exports.WebSocketSubscriptionEvent.SUBSCRIBE_SUBMISSIONS_JUDGING
3029
+ && isSubmissionsJudgingScope(event);
3030
+ };
3031
+ const isUnsubscribeSubmissionsJudgingWebSocketEventDto = (event) => {
3032
+ return isWebsocketSubscription(event)
3033
+ && event?.event === exports.WebSocketSubscriptionEvent.UNSUBSCRIBE_SUBMISSIONS_JUDGING
3034
+ && isSubmissionsJudgingScope(event);
3035
+ };
2977
3036
  // is WebSocketResponseEventDto, CHANNEL_SUBSCRIBE_CLIENT
2978
3037
  const isWebSocketResponseEventDto = (event) => {
2979
3038
  return Object.values(exports.WebSocketResponseEvent).includes(event?.event)
@@ -2995,6 +3054,18 @@ const isSubmissionRunStatusMessageWebSocketResponseEventDto = (event) => {
2995
3054
  && event?.verdict in exports.ProblemVerdict
2996
3055
  && typeof event?.points === 'number';
2997
3056
  };
3057
+ const isSubmissionJudgingWebSocketResponseEventDto = (event) => {
3058
+ return isWebSocketResponseEventDto(event)
3059
+ && event?.event === exports.WebSocketResponseEvent.SUBMISSION_JUDGING
3060
+ && isNonEmptyString(event?.submitId)
3061
+ && isNonEmptyString(event?.problemKey)
3062
+ && (event?.contestKey === null || isNonEmptyString(event?.contestKey))
3063
+ && isNonEmptyString(event?.userKey)
3064
+ && typeof event?.judging === 'boolean'
3065
+ && event?.status in exports.SubmissionRunStatus
3066
+ && event?.verdict in exports.ProblemVerdict
3067
+ && typeof event?.points === 'number';
3068
+ };
2998
3069
  const isUserMessageWebSocketResponseEventDto = (event) => {
2999
3070
  return isWebSocketResponseEventDto(event)
3000
3071
  && event?.event === exports.WebSocketResponseEvent.USER_MESSAGE
@@ -3100,6 +3171,11 @@ const getWebSocketResponseEventKeyOfResponse = (response, clientId) => {
3100
3171
  if (isUserNotificationWebSocketResponseEventDto(response)) {
3101
3172
  return getWebSocketResponseEventKey(exports.WebSocketResponseEvent.USER_NOTIFICATION_, clientId, '*');
3102
3173
  }
3174
+ if (isSubmissionJudgingWebSocketResponseEventDto(response)) {
3175
+ // one key for every table: the same event is published on several channels (problem / contest / user) and the
3176
+ // client deduplicates it, so it is dispatched once; each table filters by the submissions it lists
3177
+ return getWebSocketResponseEventKey(exports.WebSocketResponseEvent.SUBMISSION_JUDGING, clientId, '*');
3178
+ }
3103
3179
  return null;
3104
3180
  };
3105
3181
  /**
@@ -3111,12 +3187,23 @@ const getChannelOfWebSocketSubscribeEvent = (event) => {
3111
3187
  if (isSubscribeSubmissionRunStatusWebSocketEventDto(event)) {
3112
3188
  return CHANNEL_SUBMISSION(event.submitId);
3113
3189
  }
3114
- if (isSubscribeProblemCrawledWebSocketEventDto(event)) {
3115
- return CHANNEL_PROBLEM(event.problemKey);
3116
- }
3190
+ // SUBSCRIBE_PROBLEM_CRAWLED: the event arrives by the private user channel of who requested the crawl
3117
3191
  if (isSubscribeContestChangesWebSocketEventDto(event) || isSubscribeSubmissionsCrawlWebSocketEventDto(event)) {
3118
3192
  return CHANNEL_CONTEST(event.contestKey, exports.ContestChannelRole.SPECTATORS);
3119
3193
  }
3194
+ if (isSubscribeSubmissionsJudgingWebSocketEventDto(event)) {
3195
+ // the judging start/end is published on the spectators channel when it is visible to everyone; the hidden ones
3196
+ // (frozen / quiet contests) arrive by the role channels of the contest (managers) and the private user channel
3197
+ if ('problemKey' in event && event.problemKey) {
3198
+ return CHANNEL_PROBLEM(event.problemKey, exports.ProblemChannelRole.SPECTATORS);
3199
+ }
3200
+ if ('contestKey' in event && event.contestKey) {
3201
+ return CHANNEL_CONTEST(event.contestKey, exports.ContestChannelRole.SPECTATORS);
3202
+ }
3203
+ if ('userKey' in event && event.userKey) {
3204
+ return CHANNEL_USER_ROLE(event.userKey, exports.UserChannelRole.SPECTATORS);
3205
+ }
3206
+ }
3120
3207
  return null;
3121
3208
  };
3122
3209
  /**
@@ -3126,6 +3213,18 @@ const getContestKeyOfWebSocketSubscribeEvent = (event) => {
3126
3213
  if (isSubscribeContestChangesWebSocketEventDto(event) || isSubscribeSubmissionsCrawlWebSocketEventDto(event)) {
3127
3214
  return event.contestKey;
3128
3215
  }
3216
+ if (isSubscribeSubmissionsJudgingWebSocketEventDto(event) && 'contestKey' in event && event.contestKey) {
3217
+ return event.contestKey;
3218
+ }
3219
+ return null;
3220
+ };
3221
+ /**
3222
+ * Problem whose role channels the client needs (the API grants them when the token is requested with this problem).
3223
+ */
3224
+ const getProblemKeyOfWebSocketSubscribeEvent = (event) => {
3225
+ if (isSubscribeSubmissionsJudgingWebSocketEventDto(event) && 'problemKey' in event && event.problemKey) {
3226
+ return event.problemKey;
3227
+ }
3129
3228
  return null;
3130
3229
  };
3131
3230
 
@@ -3327,11 +3426,12 @@ exports.AssignmentType = AssignmentType;
3327
3426
  exports.CHANNEL_CONTEST = CHANNEL_CONTEST;
3328
3427
  exports.CHANNEL_ORGANIZATION_PRESENCE = CHANNEL_ORGANIZATION_PRESENCE;
3329
3428
  exports.CHANNEL_PROBLEM = CHANNEL_PROBLEM;
3330
- exports.CHANNEL_PROBLEM_ALL = CHANNEL_PROBLEM_ALL;
3331
3429
  exports.CHANNEL_SESSION = CHANNEL_SESSION;
3332
3430
  exports.CHANNEL_SUBMISSION = CHANNEL_SUBMISSION;
3333
3431
  exports.CHANNEL_SUBMISSION_ALL = CHANNEL_SUBMISSION_ALL;
3334
3432
  exports.CHANNEL_USER = CHANNEL_USER;
3433
+ exports.CHANNEL_USER_ROLE = CHANNEL_USER_ROLE;
3434
+ exports.CHANNEL_USER_SPECTATORS_ALL = CHANNEL_USER_SPECTATORS_ALL;
3335
3435
  exports.CODE_LANGUAGE = CODE_LANGUAGE;
3336
3436
  exports.CONTEST_ROLE = CONTEST_ROLE;
3337
3437
  exports.CONTEST_STATUS = CONTEST_STATUS;
@@ -3470,8 +3570,10 @@ exports.getMonths = getMonths;
3470
3570
  exports.getParamsOfClientId = getParamsOfClientId;
3471
3571
  exports.getParamsOfUserKey = getParamsOfUserKey;
3472
3572
  exports.getPlainText = getPlainText;
3573
+ exports.getProblemChannelParams = getProblemChannelParams;
3473
3574
  exports.getProblemCommentKey = getProblemCommentKey;
3474
3575
  exports.getProblemJudgeKey = getProblemJudgeKey;
3576
+ exports.getProblemKeyOfWebSocketSubscribeEvent = getProblemKeyOfWebSocketSubscribeEvent;
3475
3577
  exports.getRandomString = getRandomString;
3476
3578
  exports.getSeconds = getSeconds;
3477
3579
  exports.getSummaryWorksheetsInPages = getSummaryWorksheetsInPages;
@@ -3532,14 +3634,17 @@ exports.isSendDataRunCommandWebSocketResponseEventDto = isSendDataRunCommandWebS
3532
3634
  exports.isSendDataSsmSessionsListWebSocketResponseEventDto = isSendDataSsmSessionsListWebSocketResponseEventDto;
3533
3635
  exports.isSendDataWebSocketResponseEventDto = isSendDataWebSocketResponseEventDto;
3534
3636
  exports.isStringJson = isStringJson;
3637
+ exports.isSubmissionJudgingWebSocketResponseEventDto = isSubmissionJudgingWebSocketResponseEventDto;
3535
3638
  exports.isSubmissionRunStatusMessageWebSocketResponseEventDto = isSubmissionRunStatusMessageWebSocketResponseEventDto;
3536
3639
  exports.isSubmissionsCrawlWebSocketResponseEventDto = isSubmissionsCrawlWebSocketResponseEventDto;
3640
+ exports.isSubmissionsJudgingScope = isSubmissionsJudgingScope;
3537
3641
  exports.isSubscribeCodeRunStatusWebSocketEventDto = isSubscribeCodeRunStatusWebSocketEventDto;
3538
3642
  exports.isSubscribeContestChangesWebSocketEventDto = isSubscribeContestChangesWebSocketEventDto;
3539
3643
  exports.isSubscribeGetDataWebSocketEventDto = isSubscribeGetDataWebSocketEventDto;
3540
3644
  exports.isSubscribeProblemCrawledWebSocketEventDto = isSubscribeProblemCrawledWebSocketEventDto;
3541
3645
  exports.isSubscribeSubmissionRunStatusWebSocketEventDto = isSubscribeSubmissionRunStatusWebSocketEventDto;
3542
3646
  exports.isSubscribeSubmissionsCrawlWebSocketEventDto = isSubscribeSubmissionsCrawlWebSocketEventDto;
3647
+ exports.isSubscribeSubmissionsJudgingWebSocketEventDto = isSubscribeSubmissionsJudgingWebSocketEventDto;
3543
3648
  exports.isSubscribeUserNotificationWebSocketEventDto = isSubscribeUserNotificationWebSocketEventDto;
3544
3649
  exports.isTeamMember = isTeamMember;
3545
3650
  exports.isToday = isToday;
@@ -3549,6 +3654,7 @@ exports.isUnsubscribeGetDataWebSocketEventDto = isUnsubscribeGetDataWebSocketEve
3549
3654
  exports.isUnsubscribeProblemCrawledWebSocketEventDto = isUnsubscribeProblemCrawledWebSocketEventDto;
3550
3655
  exports.isUnsubscribeSubmissionRunStatusWebSocketEventDto = isUnsubscribeSubmissionRunStatusWebSocketEventDto;
3551
3656
  exports.isUnsubscribeSubmissionsCrawlWebSocketEventDto = isUnsubscribeSubmissionsCrawlWebSocketEventDto;
3657
+ exports.isUnsubscribeSubmissionsJudgingWebSocketEventDto = isUnsubscribeSubmissionsJudgingWebSocketEventDto;
3552
3658
  exports.isUnsubscribeUserNotificationWebSocketEventDto = isUnsubscribeUserNotificationWebSocketEventDto;
3553
3659
  exports.isUserMember = isUserMember;
3554
3660
  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,26 @@ 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:{userKey}:{role}` (see `CHANNEL_USER_ROLE`), by its public key
643
+ * (`getUserKey(nickname, organizationKey)`) like problems and contests.
644
+ * There are no per-user permissions: `spectators` is everyone who can list the submissions of the user.
645
+ * The private channel of the user is `user:{userId}` (see `CHANNEL_USER`).
646
+ */
647
+ var UserChannelRole;
648
+ (function (UserChannelRole) {
649
+ UserChannelRole["SPECTATORS"] = "spectators";
650
+ })(UserChannelRole || (UserChannelRole = {}));
628
651
 
629
652
  var DataViewMode;
630
653
  (function (DataViewMode) {
@@ -2232,12 +2255,18 @@ const EMPTY_USER_PERMISSIONS = {
2232
2255
  const segment = (value) => encodeURIComponent(value);
2233
2256
  /** private: events of one session (code runs, requested data) */
2234
2257
  const CHANNEL_SESSION = (sessionId) => `session:${segment(sessionId)}`;
2235
- /** private: events of one user (own submissions while the contest is frozen, notifications) */
2258
+ /** private: events of one user (own submissions while the contest is frozen, notifications, own problem crawls) */
2236
2259
  const CHANNEL_USER = (userId) => `user:${segment(userId)}`;
2260
+ /**
2261
+ * per role: public events of one user (judging of its submissions visible to everyone, see `UserChannelRole`).
2262
+ * By its public key (`getUserKey(nickname, organizationKey)`), the one the clients know; the private channel
2263
+ * (`CHANNEL_USER`) is by id. They never collide: the role channel has one more segment.
2264
+ */
2265
+ const CHANNEL_USER_ROLE = (userKey, role) => `user:${segment(userKey)}:${role}`;
2237
2266
  /** public: status of a submission when it is visible to everyone */
2238
2267
  const CHANNEL_SUBMISSION = (submitId) => `submission:${segment(submitId)}`;
2239
- /** public: changes of a problem */
2240
- const CHANNEL_PROBLEM = (problemKey) => `problem:${segment(problemKey)}`;
2268
+ /** per role: events of a problem for one role (judging of its submissions, see `ProblemChannelRole`) */
2269
+ const CHANNEL_PROBLEM = (problemKey, role) => `problem:${segment(problemKey)}:${role}`;
2241
2270
  /** per role: events of a contest for one role (and the roles above it, see `ContestChannelRole`) */
2242
2271
  const CHANNEL_CONTEST = (contestKey, role) => `contest:${segment(contestKey)}:${role}`;
2243
2272
  /**
@@ -2248,9 +2277,12 @@ const CHANNEL_CONTEST = (contestKey, role) => `contest:${segment(contestKey)}:${
2248
2277
  * The identity of a member is its `clientId` (`sessionId|uiId`, verified by Ably), never its data.
2249
2278
  */
2250
2279
  const CHANNEL_ORGANIZATION_PRESENCE = (organizationKey) => `presence:organization:${segment(organizationKey)}`;
2251
- // capability resources (wildcards) of the public channels
2280
+ // capability resources (wildcards) of the public channels.
2281
+ // Ably: a trailing `*` replaces one or more segments, any other `*` exactly one segment. So `user:*:spectators`
2282
+ // matches every `user:{userKey}:spectators` but never the private `user:{userId}`.
2283
+ // There is no public `problem:*`: it would also match the role channels `problem:{problemKey}:{role}`.
2252
2284
  const CHANNEL_SUBMISSION_ALL = 'submission:*';
2253
- const CHANNEL_PROBLEM_ALL = 'problem:*';
2285
+ const CHANNEL_USER_SPECTATORS_ALL = `user:*:${UserChannelRole.SPECTATORS}`;
2254
2286
  /**
2255
2287
  * Returns the contest and role of a `CHANNEL_CONTEST` channel name, or null if it is not one.
2256
2288
  */
@@ -2261,6 +2293,16 @@ const getContestChannelParams = (channel) => {
2261
2293
  }
2262
2294
  return { contestKey: decodeURIComponent(contestKey), role: role };
2263
2295
  };
2296
+ /**
2297
+ * Returns the problem and role of a `CHANNEL_PROBLEM` channel name, or null if it is not one.
2298
+ */
2299
+ const getProblemChannelParams = (channel) => {
2300
+ const [namespace, problemKey, role, ...rest] = channel.split(':');
2301
+ if (namespace !== 'problem' || !problemKey || rest.length || !Object.values(ProblemChannelRole).includes(role)) {
2302
+ return null;
2303
+ }
2304
+ return { problemKey: decodeURIComponent(problemKey), role: role };
2305
+ };
2264
2306
 
2265
2307
  const NEW_PAGE_SHEET = () => ({
2266
2308
  id: v4(),
@@ -2972,6 +3014,23 @@ const isUnsubscribeUserNotificationWebSocketEventDto = (event) => {
2972
3014
  return isWebsocketSubscription(event)
2973
3015
  && event?.event === WebSocketSubscriptionEvent.UNSUBSCRIBE_USER_NOTIFICATION;
2974
3016
  };
3017
+ const isNonEmptyString = (value) => typeof value === 'string' && !!value;
3018
+ /**
3019
+ * Exactly one of `problemKey` / `contestKey` / `userKey` is set.
3020
+ */
3021
+ const isSubmissionsJudgingScope = (scope) => {
3022
+ return [scope?.problemKey, scope?.contestKey, scope?.userKey].filter(isNonEmptyString).length === 1;
3023
+ };
3024
+ const isSubscribeSubmissionsJudgingWebSocketEventDto = (event) => {
3025
+ return isWebsocketSubscription(event)
3026
+ && event?.event === WebSocketSubscriptionEvent.SUBSCRIBE_SUBMISSIONS_JUDGING
3027
+ && isSubmissionsJudgingScope(event);
3028
+ };
3029
+ const isUnsubscribeSubmissionsJudgingWebSocketEventDto = (event) => {
3030
+ return isWebsocketSubscription(event)
3031
+ && event?.event === WebSocketSubscriptionEvent.UNSUBSCRIBE_SUBMISSIONS_JUDGING
3032
+ && isSubmissionsJudgingScope(event);
3033
+ };
2975
3034
  // is WebSocketResponseEventDto, CHANNEL_SUBSCRIBE_CLIENT
2976
3035
  const isWebSocketResponseEventDto = (event) => {
2977
3036
  return Object.values(WebSocketResponseEvent).includes(event?.event)
@@ -2993,6 +3052,18 @@ const isSubmissionRunStatusMessageWebSocketResponseEventDto = (event) => {
2993
3052
  && event?.verdict in ProblemVerdict
2994
3053
  && typeof event?.points === 'number';
2995
3054
  };
3055
+ const isSubmissionJudgingWebSocketResponseEventDto = (event) => {
3056
+ return isWebSocketResponseEventDto(event)
3057
+ && event?.event === WebSocketResponseEvent.SUBMISSION_JUDGING
3058
+ && isNonEmptyString(event?.submitId)
3059
+ && isNonEmptyString(event?.problemKey)
3060
+ && (event?.contestKey === null || isNonEmptyString(event?.contestKey))
3061
+ && isNonEmptyString(event?.userKey)
3062
+ && typeof event?.judging === 'boolean'
3063
+ && event?.status in SubmissionRunStatus
3064
+ && event?.verdict in ProblemVerdict
3065
+ && typeof event?.points === 'number';
3066
+ };
2996
3067
  const isUserMessageWebSocketResponseEventDto = (event) => {
2997
3068
  return isWebSocketResponseEventDto(event)
2998
3069
  && event?.event === WebSocketResponseEvent.USER_MESSAGE
@@ -3098,6 +3169,11 @@ const getWebSocketResponseEventKeyOfResponse = (response, clientId) => {
3098
3169
  if (isUserNotificationWebSocketResponseEventDto(response)) {
3099
3170
  return getWebSocketResponseEventKey(WebSocketResponseEvent.USER_NOTIFICATION_, clientId, '*');
3100
3171
  }
3172
+ if (isSubmissionJudgingWebSocketResponseEventDto(response)) {
3173
+ // one key for every table: the same event is published on several channels (problem / contest / user) and the
3174
+ // client deduplicates it, so it is dispatched once; each table filters by the submissions it lists
3175
+ return getWebSocketResponseEventKey(WebSocketResponseEvent.SUBMISSION_JUDGING, clientId, '*');
3176
+ }
3101
3177
  return null;
3102
3178
  };
3103
3179
  /**
@@ -3109,12 +3185,23 @@ const getChannelOfWebSocketSubscribeEvent = (event) => {
3109
3185
  if (isSubscribeSubmissionRunStatusWebSocketEventDto(event)) {
3110
3186
  return CHANNEL_SUBMISSION(event.submitId);
3111
3187
  }
3112
- if (isSubscribeProblemCrawledWebSocketEventDto(event)) {
3113
- return CHANNEL_PROBLEM(event.problemKey);
3114
- }
3188
+ // SUBSCRIBE_PROBLEM_CRAWLED: the event arrives by the private user channel of who requested the crawl
3115
3189
  if (isSubscribeContestChangesWebSocketEventDto(event) || isSubscribeSubmissionsCrawlWebSocketEventDto(event)) {
3116
3190
  return CHANNEL_CONTEST(event.contestKey, ContestChannelRole.SPECTATORS);
3117
3191
  }
3192
+ if (isSubscribeSubmissionsJudgingWebSocketEventDto(event)) {
3193
+ // the judging start/end is published on the spectators channel when it is visible to everyone; the hidden ones
3194
+ // (frozen / quiet contests) arrive by the role channels of the contest (managers) and the private user channel
3195
+ if ('problemKey' in event && event.problemKey) {
3196
+ return CHANNEL_PROBLEM(event.problemKey, ProblemChannelRole.SPECTATORS);
3197
+ }
3198
+ if ('contestKey' in event && event.contestKey) {
3199
+ return CHANNEL_CONTEST(event.contestKey, ContestChannelRole.SPECTATORS);
3200
+ }
3201
+ if ('userKey' in event && event.userKey) {
3202
+ return CHANNEL_USER_ROLE(event.userKey, UserChannelRole.SPECTATORS);
3203
+ }
3204
+ }
3118
3205
  return null;
3119
3206
  };
3120
3207
  /**
@@ -3124,6 +3211,18 @@ const getContestKeyOfWebSocketSubscribeEvent = (event) => {
3124
3211
  if (isSubscribeContestChangesWebSocketEventDto(event) || isSubscribeSubmissionsCrawlWebSocketEventDto(event)) {
3125
3212
  return event.contestKey;
3126
3213
  }
3214
+ if (isSubscribeSubmissionsJudgingWebSocketEventDto(event) && 'contestKey' in event && event.contestKey) {
3215
+ return event.contestKey;
3216
+ }
3217
+ return null;
3218
+ };
3219
+ /**
3220
+ * Problem whose role channels the client needs (the API grants them when the token is requested with this problem).
3221
+ */
3222
+ const getProblemKeyOfWebSocketSubscribeEvent = (event) => {
3223
+ if (isSubscribeSubmissionsJudgingWebSocketEventDto(event) && 'problemKey' in event && event.problemKey) {
3224
+ return event.problemKey;
3225
+ }
3127
3226
  return null;
3128
3227
  };
3129
3228
 
@@ -3318,4 +3417,4 @@ const isQuizTextSheet = (value) => {
3318
3417
  && ['text', 'number', 'textarea'].includes(value?.inputType);
3319
3418
  };
3320
3419
 
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 };
3420
+ 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,18 @@
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
+ /**
7
+ * per role: public events of one user (judging of its submissions visible to everyone, see `UserChannelRole`).
8
+ * By its public key (`getUserKey(nickname, organizationKey)`), the one the clients know; the private channel
9
+ * (`CHANNEL_USER`) is by id. They never collide: the role channel has one more segment.
10
+ */
11
+ export declare const CHANNEL_USER_ROLE: (userKey: string, role: UserChannelRole) => string;
6
12
  /** public: status of a submission when it is visible to everyone */
7
13
  export declare const CHANNEL_SUBMISSION: (submitId: string) => string;
8
- /** public: changes of a problem */
9
- export declare const CHANNEL_PROBLEM: (problemKey: string) => string;
14
+ /** per role: events of a problem for one role (judging of its submissions, see `ProblemChannelRole`) */
15
+ export declare const CHANNEL_PROBLEM: (problemKey: string, role: ProblemChannelRole) => string;
10
16
  /** per role: events of a contest for one role (and the roles above it, see `ContestChannelRole`) */
11
17
  export declare const CHANNEL_CONTEST: (contestKey: string, role: ContestChannelRole) => string;
12
18
  /**
@@ -18,7 +24,7 @@ export declare const CHANNEL_CONTEST: (contestKey: string, role: ContestChannelR
18
24
  */
19
25
  export declare const CHANNEL_ORGANIZATION_PRESENCE: (organizationKey: string) => string;
20
26
  export declare const CHANNEL_SUBMISSION_ALL = "submission:*";
21
- export declare const CHANNEL_PROBLEM_ALL = "problem:*";
27
+ export declare const CHANNEL_USER_SPECTATORS_ALL = "user:*:spectators";
22
28
  /**
23
29
  * Returns the contest and role of a `CHANNEL_CONTEST` channel name, or null if it is not one.
24
30
  */
@@ -26,3 +32,10 @@ export declare const getContestChannelParams: (channel: string) => {
26
32
  contestKey: string;
27
33
  role: ContestChannelRole;
28
34
  } | null;
35
+ /**
36
+ * Returns the problem and role of a `CHANNEL_PROBLEM` channel name, or null if it is not one.
37
+ */
38
+ export declare const getProblemChannelParams: (channel: string) => {
39
+ problemKey: string;
40
+ role: ProblemChannelRole;
41
+ } | 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
+ userKey: 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
+ userKey: 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` / `userKey` 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,24 @@ 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:{userKey}:{role}` (see `CHANNEL_USER_ROLE`), by its public key
68
+ * (`getUserKey(nickname, organizationKey)`) like problems and contests.
69
+ * There are no per-user permissions: `spectators` is everyone who can list the submissions of the user.
70
+ * The private channel of the user is `user:{userId}` (see `CHANNEL_USER`).
71
+ */
72
+ export declare enum UserChannelRole {
73
+ SPECTATORS = "spectators"
74
+ }
54
75
  export type InfoLogCaseStatus = {
55
76
  inputKey: string;
56
77
  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.47",
4
4
  "author": "juki-team",
5
5
  "license": "MIT",
6
6
  "type": "module",