mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Merge pull request #17355 from davelopez/unify_continuous_polling
Unify continuous polling with composable
This commit is contained in:
@@ -9,7 +9,7 @@ import SelectedItems from "@/components/History/Content/SelectedItems";
|
||||
import { HistoryFilters } from "@/components/History/HistoryFilters";
|
||||
import { deleteContent, updateContentFields } from "@/components/History/model/queries";
|
||||
import { Toast } from "@/composables/toast";
|
||||
import { rewatchHistory } from "@/store/historyStore/model/watchHistory";
|
||||
import { startWatchingHistory } from "@/store/historyStore/model/watchHistory";
|
||||
import { type HistoryItem, useHistoryItemsStore } from "@/stores/historyItemsStore";
|
||||
import { useHistoryStore } from "@/stores/historyStore";
|
||||
import { type Alias, getOperatorForAlias } from "@/utils/filtering";
|
||||
@@ -277,7 +277,7 @@ async function onUnhide(item: HistoryItem) {
|
||||
}
|
||||
|
||||
function reloadContents() {
|
||||
rewatchHistory();
|
||||
startWatchingHistory();
|
||||
}
|
||||
|
||||
function setInvisible(item: HistoryItem) {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
import Backbone from "backbone";
|
||||
import { createDatasetCollection } from "components/History/model/queries";
|
||||
import { watchHistory } from "store/historyStore/model/watchHistory";
|
||||
import { startWatchingHistory } from "store/historyStore/model/watchHistory";
|
||||
import { useHistoryItemsStore } from "stores/historyItemsStore";
|
||||
import { useHistoryStore } from "stores/historyStore";
|
||||
|
||||
@@ -26,7 +26,7 @@ export class HistoryPanelProxy {
|
||||
};
|
||||
|
||||
// start watching the history with continuous queries
|
||||
watchHistory();
|
||||
startWatchingHistory();
|
||||
}
|
||||
|
||||
syncCurrentHistoryModel(currentHistory) {
|
||||
|
||||
@@ -134,9 +134,8 @@ export default {
|
||||
this.load();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useEntryPointStore, ["ensurePollingEntryPoints", "removeEntryPoint"]),
|
||||
...mapActions(useEntryPointStore, ["removeEntryPoint"]),
|
||||
load() {
|
||||
this.ensurePollingEntryPoints();
|
||||
this.filter = "";
|
||||
},
|
||||
filtered: function (items) {
|
||||
|
||||
@@ -89,7 +89,7 @@ watch(
|
||||
/* lifecyle */
|
||||
onBeforeMount(() => {
|
||||
entryPointStore = useEntryPointStore();
|
||||
entryPointStore.ensurePollingEntryPoints();
|
||||
entryPointStore.startWatchingEntryPoints();
|
||||
entryPointStore.$subscribe((mutation, state) => {
|
||||
updateVisibility(state.entryPoints.length > 0);
|
||||
});
|
||||
|
||||
@@ -41,7 +41,7 @@ import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import { mapActions, mapState } from "pinia";
|
||||
import { mapState } from "pinia";
|
||||
import { useEntryPointStore } from "stores/entryPointStore";
|
||||
|
||||
library.add(faExternalLinkAlt);
|
||||
@@ -62,11 +62,5 @@ export default {
|
||||
return getAppRoot() + "interactivetool_entry_points/list";
|
||||
},
|
||||
},
|
||||
created: function () {
|
||||
this.ensurePollingEntryPoints();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useEntryPointStore, ["ensurePollingEntryPoints"]),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
export type WatchResourceHandler = () => Promise<void>;
|
||||
|
||||
export interface WatchOptions {
|
||||
/**
|
||||
* Polling interval in milliseconds when the app is active (in the current tab).
|
||||
*/
|
||||
shortPollingInterval?: number;
|
||||
/**
|
||||
* Polling interval in milliseconds when the app is in the background (not in the current tab).
|
||||
*/
|
||||
longPollingInterval?: number;
|
||||
/**
|
||||
* If true, the resource is watched in the background even when the app is not active (in the current tab).
|
||||
*/
|
||||
enableBackgroundPolling?: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_WATCH_OPTIONS: WatchOptions = {
|
||||
shortPollingInterval: 3000,
|
||||
longPollingInterval: 10000,
|
||||
enableBackgroundPolling: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a composable that watches a resource by polling the server continuously.
|
||||
* By default, the polling interval is 'short' when the app is active (in the current tab) and 'long'
|
||||
* when the app is in the background (not in the current tab).
|
||||
* You can also completely disable background polling by setting `enableBackgroundPolling` to false in the options.
|
||||
* @param watchHandler The handler function that watches the resource by querying the server.
|
||||
* @param options Options to customize the polling interval.
|
||||
*/
|
||||
export function useResourceWatcher(watchHandler: WatchResourceHandler, options: WatchOptions = DEFAULT_WATCH_OPTIONS) {
|
||||
const { shortPollingInterval, longPollingInterval, enableBackgroundPolling } = {
|
||||
...DEFAULT_WATCH_OPTIONS,
|
||||
...options,
|
||||
};
|
||||
let currentPollingInterval = shortPollingInterval;
|
||||
let watchTimeout: NodeJS.Timeout | null = null;
|
||||
let isEventSetup = false;
|
||||
|
||||
/**
|
||||
* Starts watching the resource by polling the server continuously.
|
||||
*/
|
||||
function startWatchingResource() {
|
||||
stopWatchingResource();
|
||||
tryWatchResource();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops continuously watching the resource.
|
||||
*/
|
||||
function stopWatchingResource() {
|
||||
if (watchTimeout) {
|
||||
clearTimeout(watchTimeout);
|
||||
watchTimeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function tryWatchResource() {
|
||||
try {
|
||||
await watchHandler();
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
} finally {
|
||||
if (currentPollingInterval) {
|
||||
watchTimeout = setTimeout(() => {
|
||||
tryWatchResource();
|
||||
}, currentPollingInterval);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setupVisibilityListeners() {
|
||||
if (!isEventSetup) {
|
||||
isEventSetup = true;
|
||||
document.addEventListener("visibilitychange", updateThrottle);
|
||||
}
|
||||
}
|
||||
|
||||
function updateThrottle() {
|
||||
if (document.visibilityState === "visible") {
|
||||
currentPollingInterval = shortPollingInterval;
|
||||
startWatchingResource();
|
||||
} else {
|
||||
currentPollingInterval = enableBackgroundPolling ? longPollingInterval : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
setupVisibilityListeners();
|
||||
|
||||
return {
|
||||
startWatchingResource,
|
||||
};
|
||||
}
|
||||
@@ -173,7 +173,7 @@ export default {
|
||||
this.Galaxy.modal = new Modal.View();
|
||||
this.Galaxy.frame = this.windowManager;
|
||||
if (this.Galaxy.config.enable_notification_system) {
|
||||
this.startNotificationsPolling();
|
||||
this.startWatchingNotifications();
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -189,9 +189,9 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
startNotificationsPolling() {
|
||||
startWatchingNotifications() {
|
||||
const notificationsStore = useNotificationsStore();
|
||||
notificationsStore.startPollingNotifications();
|
||||
notificationsStore.startWatchingNotifications();
|
||||
},
|
||||
openUrl(urlObj) {
|
||||
if (!urlObj.target) {
|
||||
|
||||
@@ -13,13 +13,14 @@ import { getCurrentHistoryFromServer } from "stores/services/history.services";
|
||||
import { loadSet } from "utils/setCache";
|
||||
import { urlData } from "utils/url";
|
||||
|
||||
import { useResourceWatcher } from "@/composables/resourceWatcher";
|
||||
import { useCollectionElementsStore } from "@/stores/collectionElementsStore";
|
||||
import { useDatasetStore } from "@/stores/datasetStore";
|
||||
|
||||
const limit = 1000;
|
||||
|
||||
let throttlePeriod = 3000;
|
||||
let watchTimeout = null;
|
||||
const ACTIVE_POLLING_INTERVAL = 3000;
|
||||
const INACTIVE_POLLING_INTERVAL = 60000;
|
||||
|
||||
// last time the history has changed
|
||||
let lastUpdateTime = null;
|
||||
@@ -27,17 +28,22 @@ let lastUpdateTime = null;
|
||||
// last time changed history items have been requested
|
||||
let lastRequestDate = new Date();
|
||||
|
||||
// We only want to kick this off once we're actively watching history
|
||||
let watchingVisibility = false;
|
||||
const { startWatchingResource: startWatchingHistory } = useResourceWatcher(watchHistory, {
|
||||
shortPollingInterval: ACTIVE_POLLING_INTERVAL,
|
||||
longPollingInterval: INACTIVE_POLLING_INTERVAL,
|
||||
});
|
||||
|
||||
function setVisibilityThrottle() {
|
||||
if (document.visibilityState === "visible") {
|
||||
// Poll every 3 seconds when visible
|
||||
throttlePeriod = 3000;
|
||||
rewatchHistory();
|
||||
} else {
|
||||
// Poll every 60 seconds when hidden/backgrounded
|
||||
throttlePeriod = 60000;
|
||||
export { startWatchingHistory };
|
||||
|
||||
async function watchHistory() {
|
||||
const { isWatching } = storeToRefs(useHistoryItemsStore());
|
||||
try {
|
||||
isWatching.value = true;
|
||||
await watchHistoryOnce();
|
||||
} catch (error) {
|
||||
// error alerting the user that watch history failed
|
||||
console.warn(error);
|
||||
isWatching.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,8 +52,7 @@ export async function watchHistoryOnce() {
|
||||
const historyItemsStore = useHistoryItemsStore();
|
||||
const datasetStore = useDatasetStore();
|
||||
const collectionElementsStore = useCollectionElementsStore();
|
||||
// "Reset" watchTimeout so we don't queue up watchHistory calls in rewatchHistory.
|
||||
watchTimeout = null;
|
||||
|
||||
// get current history
|
||||
const checkForUpdate = new Date();
|
||||
const history = await getCurrentHistoryFromServer(lastUpdateTime);
|
||||
@@ -96,35 +101,6 @@ export async function watchHistoryOnce() {
|
||||
}
|
||||
}
|
||||
|
||||
export async function watchHistory() {
|
||||
const { isWatching } = storeToRefs(useHistoryItemsStore());
|
||||
// Only set up visibility listeners once, whenever a watch is first started
|
||||
if (watchingVisibility === false) {
|
||||
watchingVisibility = true;
|
||||
isWatching.value = watchingVisibility;
|
||||
document.addEventListener("visibilitychange", setVisibilityThrottle);
|
||||
}
|
||||
try {
|
||||
await watchHistoryOnce();
|
||||
} catch (error) {
|
||||
// error alerting the user that watch history failed
|
||||
console.warn(error);
|
||||
watchingVisibility = false;
|
||||
isWatching.value = watchingVisibility;
|
||||
} finally {
|
||||
watchTimeout = setTimeout(() => {
|
||||
watchHistory();
|
||||
}, throttlePeriod);
|
||||
}
|
||||
}
|
||||
|
||||
export function rewatchHistory() {
|
||||
if (watchTimeout) {
|
||||
clearTimeout(watchTimeout);
|
||||
watchHistory();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the set of history item IDs that are currently expanded in the history panel from the cache.
|
||||
* These content items need to retrieve detailed information when updated.
|
||||
|
||||
@@ -70,7 +70,7 @@ describe("watchHistory", () => {
|
||||
.replyOnce(200, historyData)
|
||||
.onGet(/api\/histories\/history-id\/contents?.*/)
|
||||
.replyOnce(200, historyItems);
|
||||
await watchHistoryOnce(wrapper.vm.$store);
|
||||
await watchHistoryOnce();
|
||||
expect(wrapper.vm.getHistoryItems("history-id", "").length).toBe(2);
|
||||
expect(wrapper.vm.getHistoryItems("history-id", "second")[0].hid).toBe(2);
|
||||
expect(wrapper.vm.getHistoryItems("history-id", "state:ok")[0].hid).toBe(1);
|
||||
@@ -86,11 +86,11 @@ describe("watchHistory", () => {
|
||||
.onGet(`/history/current_history_json`)
|
||||
.replyOnce(500);
|
||||
|
||||
await watchHistoryOnce(wrapper.vm.$store);
|
||||
await watchHistoryOnce();
|
||||
expect(wrapper.vm.currentHistoryId).toBe("history-id");
|
||||
expect(wrapper.vm.getHistoryItems("history-id", "").length).toBe(2);
|
||||
try {
|
||||
await watchHistoryOnce(wrapper.vm.$store);
|
||||
await watchHistoryOnce();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
expect(error.response.status).toBe(500);
|
||||
@@ -113,7 +113,7 @@ describe("watchHistory", () => {
|
||||
history_id: "history-id",
|
||||
},
|
||||
]);
|
||||
await watchHistoryOnce(wrapper.vm.$store);
|
||||
await watchHistoryOnce();
|
||||
// We should have received the update and have 3 items in the history
|
||||
expect(wrapper.vm.getHistoryItems("history-id", "").length).toBe(3);
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ describe("stores/EntryPointStore", () => {
|
||||
setActivePinia(createPinia());
|
||||
axiosMock.onGet("/api/entry_points", { params: { running: true } }).reply(200, testInteractiveToolsResponse);
|
||||
store = useEntryPointStore();
|
||||
store.ensurePollingEntryPoints();
|
||||
await store.fetchEntryPoints();
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
@@ -23,16 +23,7 @@ describe("stores/EntryPointStore", () => {
|
||||
axiosMock.restore();
|
||||
});
|
||||
|
||||
it("polls", async () => {
|
||||
expect(store.entryPoints.length).toBe(2);
|
||||
});
|
||||
it("stops polling", async () => {
|
||||
expect(store.pollTimeout !== undefined).toBeTruthy();
|
||||
store.stopPollingEntryPoints();
|
||||
expect(store.pollTimeout === undefined).toBeTruthy();
|
||||
});
|
||||
it("performs a partial update", async () => {
|
||||
store.stopPollingEntryPoints();
|
||||
const updateData = [
|
||||
{
|
||||
model_class: "InteractiveToolEntryPoint",
|
||||
|
||||
@@ -3,9 +3,12 @@ import isEqual from "lodash.isequal";
|
||||
import { defineStore } from "pinia";
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
import { useResourceWatcher } from "@/composables/resourceWatcher";
|
||||
import { getAppRoot } from "@/onload/loadConfig";
|
||||
import { rethrowSimple } from "@/utils/simple-error";
|
||||
|
||||
const ACTIVE_POLLING_INTERVAL = 10000;
|
||||
|
||||
// TODO: replace with the corresponding autogenerated model when ready
|
||||
interface EntryPoint {
|
||||
model_class: "InteractiveToolEntryPoint";
|
||||
@@ -20,7 +23,11 @@ interface EntryPoint {
|
||||
}
|
||||
|
||||
export const useEntryPointStore = defineStore("entryPointStore", () => {
|
||||
const pollTimeout = ref<NodeJS.Timeout | undefined>(undefined);
|
||||
const { startWatchingResource: startWatchingEntryPoints } = useResourceWatcher(fetchEntryPoints, {
|
||||
shortPollingInterval: ACTIVE_POLLING_INTERVAL,
|
||||
enableBackgroundPolling: false, // No need to poll in the background
|
||||
});
|
||||
|
||||
const entryPoints = ref<EntryPoint[]>([]);
|
||||
|
||||
const entryPointsForJob = computed(() => {
|
||||
@@ -32,18 +39,6 @@ export const useEntryPointStore = defineStore("entryPointStore", () => {
|
||||
entryPoints.value.filter((entryPoint) => entryPoint["output_datasets_ids"].includes(hdaId));
|
||||
});
|
||||
|
||||
async function ensurePollingEntryPoints() {
|
||||
await fetchEntryPoints();
|
||||
pollTimeout.value = setTimeout(() => {
|
||||
ensurePollingEntryPoints();
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
function stopPollingEntryPoints() {
|
||||
clearTimeout(pollTimeout.value);
|
||||
pollTimeout.value = undefined;
|
||||
}
|
||||
|
||||
async function fetchEntryPoints() {
|
||||
const url = `${getAppRoot()}api/entry_points`;
|
||||
const params = { running: true };
|
||||
@@ -96,8 +91,6 @@ export const useEntryPointStore = defineStore("entryPointStore", () => {
|
||||
fetchEntryPoints,
|
||||
updateEntryPoints,
|
||||
removeEntryPoint,
|
||||
pollTimeout,
|
||||
ensurePollingEntryPoints,
|
||||
stopPollingEntryPoints,
|
||||
startWatchingEntryPoints,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -7,19 +7,24 @@ import {
|
||||
loadNotificationsStatus,
|
||||
updateBatchNotificationsOnServer,
|
||||
} from "@/api/notifications";
|
||||
import { useResourceWatcher } from "@/composables/resourceWatcher";
|
||||
import { mergeObjectListsById } from "@/utils/utils";
|
||||
|
||||
import { useBroadcastsStore } from "./broadcastsStore";
|
||||
|
||||
const STATUS_POLLING_DELAY = 5000;
|
||||
const ACTIVE_POLLING_INTERVAL = 5000;
|
||||
const INACTIVE_POLLING_INTERVAL = 30000;
|
||||
|
||||
export const useNotificationsStore = defineStore("notificationsStore", () => {
|
||||
const { startWatchingResource: startWatchingNotifications } = useResourceWatcher(getNotificationStatus, {
|
||||
shortPollingInterval: ACTIVE_POLLING_INTERVAL,
|
||||
longPollingInterval: INACTIVE_POLLING_INTERVAL,
|
||||
});
|
||||
const broadcastsStore = useBroadcastsStore();
|
||||
|
||||
const totalUnreadCount = ref<number>(0);
|
||||
const notifications = ref<UserNotification[]>([]);
|
||||
|
||||
const pollId = ref<NodeJS.Timeout | undefined>(undefined);
|
||||
const loadingNotifications = ref<boolean>(false);
|
||||
const lastNotificationUpdate = ref<Date | null>(null);
|
||||
|
||||
@@ -31,7 +36,6 @@ export const useNotificationsStore = defineStore("notificationsStore", () => {
|
||||
}
|
||||
|
||||
async function getNotificationStatus() {
|
||||
stopPollingNotifications();
|
||||
try {
|
||||
if (!lastNotificationUpdate.value) {
|
||||
loadingNotifications.value = true;
|
||||
@@ -56,22 +60,12 @@ export const useNotificationsStore = defineStore("notificationsStore", () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function startPollingNotifications() {
|
||||
await getNotificationStatus();
|
||||
pollId.value = setTimeout(() => startPollingNotifications(), STATUS_POLLING_DELAY);
|
||||
}
|
||||
|
||||
function stopPollingNotifications() {
|
||||
clearTimeout(pollId.value);
|
||||
pollId.value = undefined;
|
||||
}
|
||||
|
||||
async function updateBatchNotification(request: UserNotificationsBatchUpdateRequest) {
|
||||
await updateBatchNotificationsOnServer(request);
|
||||
if (request.changes.deleted) {
|
||||
notifications.value = notifications.value.filter((n) => !request.notification_ids.includes(n.id));
|
||||
}
|
||||
await startPollingNotifications();
|
||||
startWatchingNotifications();
|
||||
}
|
||||
|
||||
async function updateNotification(notification: UserNotification, changes: NotificationChanges) {
|
||||
@@ -85,6 +79,6 @@ export const useNotificationsStore = defineStore("notificationsStore", () => {
|
||||
loadingNotifications,
|
||||
updateNotification,
|
||||
updateBatchNotification,
|
||||
startPollingNotifications,
|
||||
startWatchingNotifications,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import { FilesDialog } from "components/FilesDialog";
|
||||
import { useGlobalUploadModal } from "composables/globalUploadModal";
|
||||
import $ from "jquery";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import { rewatchHistory } from "store/historyStore/model/watchHistory";
|
||||
import { startWatchingHistory } from "store/historyStore/model/watchHistory";
|
||||
import Vue from "vue";
|
||||
|
||||
import { uploadPayload } from "@/utils/upload-payload.js";
|
||||
@@ -116,5 +116,5 @@ export function refreshContentsWrapper() {
|
||||
// Legacy Panel Interface. no-op if using new history
|
||||
Galaxy?.currHistoryPanel?.refreshContents();
|
||||
// Will not do anything in legacy interface
|
||||
rewatchHistory();
|
||||
startWatchingHistory();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user