Skip to content

Commit eee4495

Browse files
committed
fixup! Push SSE history updates to non-owner viewers
1 parent 4505c3a commit eee4495

4 files changed

Lines changed: 132 additions & 14 deletions

File tree

client/src/api/schema/schema.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1292,6 +1292,31 @@ export interface paths {
12921292
patch?: never;
12931293
trace?: never;
12941294
};
1295+
"/api/events/history-subscriptions": {
1296+
parameters: {
1297+
query?: never;
1298+
header?: never;
1299+
path?: never;
1300+
cookie?: never;
1301+
};
1302+
get?: never;
1303+
put?: never;
1304+
/**
1305+
* Subscribe to history_update SSE events for histories you don't own.
1306+
* @description Asks every webapp worker to start routing ``history_update`` events
1307+
* for these histories to the requesting user/session, in addition to the
1308+
* default owner-routing. Idempotent: re-subscribing to the same id is a
1309+
* no-op. Clients re-send the full set after each ``EventSource.onopen``
1310+
* so reconnects don't drop subscriptions.
1311+
*/
1312+
post: operations["subscribe_history_viewer_api_events_history_subscriptions_post"];
1313+
/** Cancel viewer subscriptions for these histories. */
1314+
delete: operations["unsubscribe_history_viewer_api_events_history_subscriptions_delete"];
1315+
options?: never;
1316+
head?: never;
1317+
patch?: never;
1318+
trace?: never;
1319+
};
12951320
"/api/events/stream": {
12961321
parameters: {
12971322
query?: never;
@@ -15433,6 +15458,14 @@ export interface components {
1543315458
*/
1543415459
url: string;
1543515460
};
15461+
/**
15462+
* HistoryViewerSubscriptionPayload
15463+
* @description REST payload for ``/api/events/history-subscriptions`` endpoints.
15464+
*/
15465+
HistoryViewerSubscriptionPayload: {
15466+
/** History Ids */
15467+
history_ids: string[];
15468+
};
1543615469
/**
1543715470
* Hyperlink
1543815471
* @description Represents some text with an Hyperlink.
@@ -33478,6 +33511,92 @@ export interface operations {
3347833511
};
3347933512
};
3348033513
};
33514+
subscribe_history_viewer_api_events_history_subscriptions_post: {
33515+
parameters: {
33516+
query?: never;
33517+
header?: {
33518+
/** @description The user ID that will be used to effectively make this API call. Only admins and designated users can make API calls on behalf of other users. */
33519+
"run-as"?: string | null;
33520+
};
33521+
path?: never;
33522+
cookie?: never;
33523+
};
33524+
requestBody: {
33525+
content: {
33526+
"application/json": components["schemas"]["HistoryViewerSubscriptionPayload"];
33527+
};
33528+
};
33529+
responses: {
33530+
/** @description Successful Response */
33531+
204: {
33532+
headers: {
33533+
[name: string]: unknown;
33534+
};
33535+
content?: never;
33536+
};
33537+
/** @description Request Error */
33538+
"4XX": {
33539+
headers: {
33540+
[name: string]: unknown;
33541+
};
33542+
content: {
33543+
"application/json": components["schemas"]["MessageExceptionModel"];
33544+
};
33545+
};
33546+
/** @description Server Error */
33547+
"5XX": {
33548+
headers: {
33549+
[name: string]: unknown;
33550+
};
33551+
content: {
33552+
"application/json": components["schemas"]["MessageExceptionModel"];
33553+
};
33554+
};
33555+
};
33556+
};
33557+
unsubscribe_history_viewer_api_events_history_subscriptions_delete: {
33558+
parameters: {
33559+
query?: never;
33560+
header?: {
33561+
/** @description The user ID that will be used to effectively make this API call. Only admins and designated users can make API calls on behalf of other users. */
33562+
"run-as"?: string | null;
33563+
};
33564+
path?: never;
33565+
cookie?: never;
33566+
};
33567+
requestBody: {
33568+
content: {
33569+
"application/json": components["schemas"]["HistoryViewerSubscriptionPayload"];
33570+
};
33571+
};
33572+
responses: {
33573+
/** @description Successful Response */
33574+
204: {
33575+
headers: {
33576+
[name: string]: unknown;
33577+
};
33578+
content?: never;
33579+
};
33580+
/** @description Request Error */
33581+
"4XX": {
33582+
headers: {
33583+
[name: string]: unknown;
33584+
};
33585+
content: {
33586+
"application/json": components["schemas"]["MessageExceptionModel"];
33587+
};
33588+
};
33589+
/** @description Server Error */
33590+
"5XX": {
33591+
headers: {
33592+
[name: string]: unknown;
33593+
};
33594+
content: {
33595+
"application/json": components["schemas"]["MessageExceptionModel"];
33596+
};
33597+
};
33598+
};
33599+
};
3348133600
stream_events_api_events_stream_get: {
3348233601
parameters: {
3348333602
query?: never;

client/src/components/History/Multiple/MultipleViewItem.vue

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,9 @@ import { storeToRefs } from "pinia";
66
import { computed, ref, watch } from "vue";
77
88
import { userOwnsHistory } from "@/api";
9+
import { useConfig } from "@/composables/config";
910
import { useExtendedHistory } from "@/composables/detailedHistory";
10-
import {
11-
addHistoryViewerSubscription,
12-
removeHistoryViewerSubscription,
13-
} from "@/composables/useNotificationSSE";
14-
import { useConfigStore } from "@/stores/configurationStore";
11+
import { addHistoryViewerSubscription, removeHistoryViewerSubscription } from "@/composables/useNotificationSSE";
1512
import { useHistoryStore } from "@/stores/historyStore";
1613
import { useUserStore } from "@/stores/userStore";
1714
@@ -32,7 +29,7 @@ const props = defineProps<Props>();
3229
const historyStore = useHistoryStore();
3330
const { currentHistoryId, pinnedHistories } = storeToRefs(historyStore);
3431
const { currentUser } = storeToRefs(useUserStore());
35-
const configStore = useConfigStore();
32+
const { config } = useConfig();
3633
3734
const { history } = useExtendedHistory(props.source.id);
3835
@@ -46,7 +43,7 @@ const sameToCurrent = computed(() => {
4643
// histories need a viewer subscription. Skip in polling mode — there's no
4744
// SSE channel to push events to and the REST call would still hit the queue.
4845
const needsViewerSubscription = computed(() => {
49-
if (!configStore.config?.enable_sse_updates) {
46+
if (!config.value?.enable_sse_updates) {
5047
return false;
5148
}
5249
if (!history.value || !currentUser.value) {

client/src/composables/useNotificationSSE.test.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* into the (test-only) onopen replay path.
88
*/
99

10+
import flushPromises from "flush-promises";
1011
import { http, HttpResponse } from "msw";
1112
import { afterEach, beforeEach, describe, expect, it } from "vitest";
1213

@@ -51,7 +52,7 @@ describe("useNotificationSSE viewer subscriptions", () => {
5152

5253
it("POSTs once per first subscriber for a given history id", async () => {
5354
addHistoryViewerSubscription("hist-A");
54-
await Promise.resolve();
55+
await flushPromises();
5556
expect(requests).toHaveLength(1);
5657
expect(requests[0]?.method).toBe("POST");
5758
expect(requests[0]?.history_ids).toEqual(["hist-A"]);
@@ -60,39 +61,39 @@ describe("useNotificationSSE viewer subscriptions", () => {
6061
it("refcounts duplicate subscriptions — second add is a no-op on the wire", async () => {
6162
addHistoryViewerSubscription("hist-A");
6263
addHistoryViewerSubscription("hist-A");
63-
await Promise.resolve();
64+
await flushPromises();
6465
expect(requests.filter((r) => r.method === "POST")).toHaveLength(1);
6566
});
6667

6768
it("only DELETEs when the last subscriber for an id releases", async () => {
6869
addHistoryViewerSubscription("hist-A");
6970
addHistoryViewerSubscription("hist-A");
70-
await Promise.resolve();
71+
await flushPromises();
7172
const postCount = requests.filter((r) => r.method === "POST").length;
7273

7374
removeHistoryViewerSubscription("hist-A");
74-
await Promise.resolve();
75+
await flushPromises();
7576
// First remove still has one outstanding refcount — must not DELETE yet.
7677
expect(requests.filter((r) => r.method === "DELETE")).toHaveLength(0);
7778
expect(requests.filter((r) => r.method === "POST")).toHaveLength(postCount);
7879

7980
removeHistoryViewerSubscription("hist-A");
80-
await Promise.resolve();
81+
await flushPromises();
8182
const deletes = requests.filter((r) => r.method === "DELETE");
8283
expect(deletes).toHaveLength(1);
8384
expect(deletes[0]?.history_ids).toEqual(["hist-A"]);
8485
});
8586

8687
it("ignores unsubscribes for ids that were never subscribed", async () => {
8788
removeHistoryViewerSubscription("hist-never");
88-
await Promise.resolve();
89+
await flushPromises();
8990
expect(requests).toHaveLength(0);
9091
});
9192

9293
it("tracks distinct history ids independently", async () => {
9394
addHistoryViewerSubscription("hist-A");
9495
addHistoryViewerSubscription("hist-B");
95-
await Promise.resolve();
96+
await flushPromises();
9697
const ids = requests.filter((r) => r.method === "POST").map((r) => r.history_ids[0]);
9798
expect(new Set(ids)).toEqual(new Set(["hist-A", "hist-B"]));
9899
});

lib/galaxy/webapps/galaxy/api/events.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class HistoryViewerSubscriptionPayload(BaseModel):
3131

3232
history_ids: list[str]
3333

34+
3435
log = logging.getLogger(__name__)
3536

3637
router = Router(tags=["events"])

0 commit comments

Comments
 (0)