Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,22 @@ export function initializeSearchSessionManager(
dashboardInternalApi: DashboardInternalApi
) {
const searchSessionId$ = new BehaviorSubject<string | undefined>(undefined);
const searchSessionGenerationInProgress$ = new BehaviorSubject<boolean>(false);

let stopSearchSessionIntegration: (() => void) | undefined;
let requestSearchSessionId: (() => Promise<string | undefined>) | undefined;
if (searchSessionSettings) {
stopSearchSessionIntegration = startDashboardSearchSessionIntegration(
{
...dashboardApi,
searchSessionId$,
},
dashboardInternalApi,
searchSessionSettings,
(searchSessionId: string) => searchSessionId$.next(searchSessionId),
searchSessionGenerationInProgress$
);
Comment on lines +30 to +39
Copy link
Copy Markdown
Contributor Author

@AlexGPlay AlexGPlay Dec 11, 2025

Choose a reason for hiding this comment

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

moved this before the continue/start - when the session id changes we kick in some polling to keep searches alive while others are still running, but for that we need to have called enableStorage before changing the id.

It was happening the other way around here, this call that I moved is the one that calls enableStorage, since we were calling it after start that meant that we never polled for embeddables, now we do

TL;DR
Before: 1. Start, 2. Enable storage (We don't poll, we don't have storage enabled)
After: 1. Enable storage, 2. Start (We poll, we have storage enabled)


const { sessionIdToRestore } = searchSessionSettings;

// if this incoming embeddable has a session, continue it.
Expand All @@ -47,7 +59,6 @@ export function initializeSearchSessionManager(
searchSessionId$.next(initialSearchSessionId);

// `requestSearchSessionId` should be used when you need to ensure that you have the up-to-date search session ID
const searchSessionGenerationInProgress$ = new BehaviorSubject<boolean>(false);
requestSearchSessionId = async () => {
if (!searchSessionGenerationInProgress$.getValue()) return searchSessionId$.getValue();
return new Promise((resolve) => {
Expand All @@ -59,17 +70,6 @@ export function initializeSearchSessionManager(
});
});
};

stopSearchSessionIntegration = startDashboardSearchSessionIntegration(
{
...dashboardApi,
searchSessionId$,
},
dashboardInternalApi,
searchSessionSettings,
(searchSessionId: string) => searchSessionId$.next(searchSessionId),
searchSessionGenerationInProgress$
);
}
return {
api: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
getStartServices,
this.searchSessionEBTManager,
this.sessionsClient,
nowProvider,
this.usageCollector
nowProvider
);
/**
* A global object that intercepts all searches and provides convenience methods for cancelling
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { BehaviorSubject, of } from 'rxjs';
import { BehaviorSubject } from 'rxjs';
import type { ISessionsClient } from './sessions_client';
import type { ISessionService } from './session_service';
import { SearchSessionState } from './search_session_state';
Expand Down Expand Up @@ -42,7 +42,6 @@ export function getSessionServiceMock(
state: SearchSessionState.None,
isContinued: false,
}).asObservable(),
disableSaveAfterSearchesExpire$: of(false),
renameCurrentSession: jest.fn(),
trackSearch: jest.fn((searchDescriptor) => ({
complete: jest.fn(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,7 @@ let nowProvider: jest.Mocked<NowProviderInternalContract>;
let currentAppId$: BehaviorSubject<string>;

beforeEach(() => {
const initializerContext = coreMock.createPluginInitializerContext({
search: {
sessions: {
notTouchedTimeout: '5m',
},
},
});
const initializerContext = coreMock.createPluginInitializerContext();
const startService = coreMock.createSetup().getStartServices;
nowProvider = createNowProviderMock();
currentAppId$ = new BehaviorSubject('app');
Expand Down Expand Up @@ -59,7 +53,6 @@ beforeEach(() => {
getSearchSessionEBTManagerMock(),
getSessionsClientMock(),
nowProvider,
undefined,
{ freezeState: false } // needed to use mocks inside state container
);
state$ = new BehaviorSubject<SearchSessionState>(SearchSessionState.None);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ import type { NowProviderInternalContract } from '../../now_provider';
import { SEARCH_SESSIONS_MANAGEMENT_ID } from './constants';
import type { ISessionsClient, SearchSessionSavedObject } from './sessions_client';
import type { CoreStart } from '@kbn/core/public';
import type { SearchUsageCollector } from '../..';
import { createSearchUsageCollectorMock } from '../collectors/mocks';

const mockSavedObject: SearchSessionSavedObject = {
id: 'd7170a35-7e2c-48d6-8dec-9a056721b489',
Expand All @@ -46,16 +44,9 @@ describe('Session service', () => {
let currentAppId$: BehaviorSubject<string>;
let toastService: jest.Mocked<CoreStart['notifications']['toasts']>;
let sessionsClient: jest.Mocked<ISessionsClient>;
let usageCollector: jest.Mocked<SearchUsageCollector>;

beforeEach(() => {
const initializerContext = coreMock.createPluginInitializerContext({
search: {
sessions: {
notTouchedTimeout: 5 * 60 * 1000,
},
},
});
const initializerContext = coreMock.createPluginInitializerContext();
const startService = coreMock.createSetup().getStartServices;
const startServicesMock = coreMock.createStart();
toastService = startServicesMock.notifications.toasts;
Expand All @@ -67,7 +58,6 @@ describe('Session service', () => {
id,
attributes: { ...mockSavedObject.attributes, sessionId: id },
}));
usageCollector = createSearchUsageCollectorMock();
sessionService = new SessionService(
initializerContext,
() =>
Expand All @@ -92,7 +82,6 @@ describe('Session service', () => {
getSearchSessionEBTManagerMock(),
sessionsClient,
nowProvider,
usageCollector,
{ freezeState: false } // needed to use mocks inside state container
);
state$ = new BehaviorSubject<SearchSessionState>(SearchSessionState.None);
Expand Down Expand Up @@ -180,46 +169,105 @@ describe('Session service', () => {

expect(abort).toBeCalledTimes(3);
});
});

describe('Keeping searches alive', () => {
let dateNowSpy: jest.SpyInstance;
let now = Date.now();
const advanceTimersBy = (by: number) => {
now = now + by;
jest.advanceTimersByTime(by);
};
beforeEach(() => {
dateNowSpy = jest.spyOn(Date, 'now').mockImplementation(() => now);
now = Date.now();
jest.useFakeTimers();
});
afterEach(() => {
dateNowSpy.mockRestore();
jest.useRealTimers();
describe('Keeping searches alive', () => {
let dateNowSpy: jest.SpyInstance;
let now = Date.now();
const advanceTimersBy = (by: number) => {
now = now + by;
jest.advanceTimersByTime(by);
};
beforeEach(() => {
dateNowSpy = jest.spyOn(Date, 'now').mockImplementation(() => now);
now = Date.now();

sessionService.enableStorage({
getName: async () => 'Name',
getLocatorData: async () => ({
id: 'id',
initialState: {},
restoreState: {},
}),
});

it('Polls all completed searches to keep them alive', async () => {
const abort = jest.fn();
const poll = jest.fn(() => Promise.resolve());
jest.useFakeTimers();
});
afterEach(() => {
dateNowSpy.mockRestore();
jest.useRealTimers();
});

sessionService.enableStorage({
getName: async () => 'Name',
getLocatorData: async () => ({
id: 'id',
initialState: {},
restoreState: {},
}),
describe('when there is only 1 search', () => {
describe('when it finishes', () => {
it('should NOT poll the search', () => {
const abort = jest.fn();
const poll = jest.fn(() => Promise.resolve());

sessionService.start();

const searchTracker = sessionService.trackSearch({ abort, poll });
searchTracker.complete();

expect(poll).toHaveBeenCalledTimes(0);
advanceTimersBy(35_000);
expect(poll).toHaveBeenCalledTimes(0);
});
sessionService.start();
});
});

const searchTracker = sessionService.trackSearch({ abort, poll });
searchTracker.complete();
describe('when there are multiple searches', () => {
describe('when not all of them are is finished', () => {
it('should poll the finished searches', () => {
const search1 = {
poll: jest.fn(() => Promise.resolve()),
abort: jest.fn(),
};
const search2 = {
poll: jest.fn(() => Promise.resolve()),
abort: jest.fn(),
};

expect(poll).toHaveBeenCalledTimes(0);
sessionService.start();

advanceTimersBy(30000);
const searchTracker1 = sessionService.trackSearch(search1);
sessionService.trackSearch(search2);

expect(poll).toHaveBeenCalledTimes(1);
searchTracker1.complete();

expect(search1.poll).toHaveBeenCalledTimes(0);
expect(search2.poll).toHaveBeenCalledTimes(0);
advanceTimersBy(35_000);
expect(search1.poll).toHaveBeenCalledTimes(1);
expect(search2.poll).toHaveBeenCalledTimes(0);
});
});

describe('when all of them are is finished', () => {
it('should not poll anything', () => {
const search1 = {
poll: jest.fn(() => Promise.resolve()),
abort: jest.fn(),
};
const search2 = {
poll: jest.fn(() => Promise.resolve()),
abort: jest.fn(),
};

sessionService.start();

const searchTracker1 = sessionService.trackSearch(search1);
const searchTracker2 = sessionService.trackSearch(search2);

searchTracker1.complete();
searchTracker2.complete();

expect(search1.poll).toHaveBeenCalledTimes(0);
expect(search2.poll).toHaveBeenCalledTimes(0);
advanceTimersBy(35_000);
expect(search1.poll).toHaveBeenCalledTimes(0);
expect(search2.poll).toHaveBeenCalledTimes(0);
});
});
});
});
Expand Down Expand Up @@ -534,98 +582,6 @@ describe('Session service', () => {
);
});

describe('disableSaveAfterSearchesExpire$', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});

test('disables save after session completes on timeout', async () => {
const emitResult: boolean[] = [];
sessionService.disableSaveAfterSearchesExpire$.subscribe((result) => {
emitResult.push(result);
});

sessionService.start();
const complete = sessionService.trackSearch({
abort: () => {},
poll: async () => {},
}).complete;

complete();

expect(emitResult).toEqual([false]);

jest.advanceTimersByTime(2 * 60 * 1000); // 2 minutes

expect(emitResult).toEqual([false]);

jest.advanceTimersByTime(3 * 60 * 1000); // 3 minutes

expect(emitResult).toEqual([false, true]);

sessionService.start();

expect(emitResult).toEqual([false, true, false]);
});

test('disables save for continued from different app sessions', async () => {
const emitResult: boolean[] = [];
sessionService.disableSaveAfterSearchesExpire$.subscribe((result) => {
emitResult.push(result);
});

const sessionId = sessionService.start();

const complete = sessionService.trackSearch({
abort: () => {},
poll: async () => {},
}).complete;

complete();

expect(emitResult).toEqual([false]);

sessionService.clear();

sessionService.continue(sessionId);

expect(emitResult).toEqual([false, true]);

sessionService.start();

expect(emitResult).toEqual([false, true, false]);
});

test('emits usage once', async () => {
const emitResult: boolean[] = [];
sessionService.disableSaveAfterSearchesExpire$.subscribe((result) => {
emitResult.push(result);
});
sessionService.disableSaveAfterSearchesExpire$.subscribe(); // testing that source is shared

sessionService.start();
const complete = sessionService.trackSearch({
abort: () => {},
poll: async () => {},
}).complete;

expect(usageCollector.trackSessionIndicatorSaveDisabled).toHaveBeenCalledTimes(0);

complete();

jest.advanceTimersByTime(5 * 60 * 1000); // 5 minutes

expect(usageCollector.trackSessionIndicatorSaveDisabled).toHaveBeenCalledTimes(1);

sessionService.start();

expect(usageCollector.trackSessionIndicatorSaveDisabled).toHaveBeenCalledTimes(1);
});
});

it('can continue an old session', () => {
const firstSessionId = sessionService.start();

Expand Down
Loading