Skip to content

Commit 44266d5

Browse files
authored
[MM-69724] Allow WS updates to update session attribute manifest rather than rely on refetch every time (#3890)
* [MM-69724] Allow WS updates to update session attribute manifest rather than rely on refetch every time * Update src/main/sessionAttributes/sessionAttributesManager.ts
1 parent ca1a39d commit 44266d5

10 files changed

Lines changed: 204 additions & 4 deletions

File tree

api-types/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,16 @@ export type PopoutViewProps = {
4343
titleTemplate?: string;
4444
isRHS?: boolean;
4545
};
46+
export type SessionAttributeField = {
47+
name: string;
48+
type: string;
49+
attrs: {
50+
enabled: boolean;
51+
ttl_seconds: number;
52+
grace_period_seconds: number;
53+
platforms: string[];
54+
};
55+
};
4656

4757
export type DesktopAPI = {
4858

@@ -62,6 +72,7 @@ export type DesktopAPI = {
6272
onLogout: () => void;
6373
invalidateSessionAttributeManifest: () => void;
6474
resendSessionAttributes: () => void;
75+
updateSessionAttribute: (field: SessionAttributeField) => void;
6576

6677
// Unreads/mentions/notifications
6778
sendNotification: (title: string, body: string, channelId: string, teamId: string, url: string, silent: boolean, soundName: string) => Promise<{status: string; reason?: string; data?: string}>;

api-types/lib/index.d.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,16 @@ export type PopoutViewProps = {
4242
titleTemplate?: string;
4343
isRHS?: boolean;
4444
};
45+
export type SessionAttributeField = {
46+
name: string;
47+
type: string;
48+
attrs: {
49+
enabled: boolean;
50+
ttl_seconds: number;
51+
grace_period_seconds: number;
52+
platforms: string[];
53+
};
54+
};
4555
export type DesktopAPI = {
4656
isDev: () => Promise<boolean>;
4757
getAppInfo: () => Promise<{
@@ -55,6 +65,7 @@ export type DesktopAPI = {
5565
onLogout: () => void;
5666
invalidateSessionAttributeManifest: () => void;
5767
resendSessionAttributes: () => void;
68+
updateSessionAttribute: (field: SessionAttributeField) => void;
5869
sendNotification: (title: string, body: string, channelId: string, teamId: string, url: string, silent: boolean, soundName: string) => Promise<{
5970
status: string;
6071
reason?: string;

api-types/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api-types/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@mattermost/desktop-api",
3-
"version": "6.3.0-1",
3+
"version": "6.3.0-2",
44
"description": "Shared types for the Desktop App API provided to the Web App",
55
"keywords": [
66
"mattermost"

src/app/preload/externalAPI.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
TAB_LOGIN_CHANGED,
3838
SESSION_ATTRIBUTES_MANIFEST_INVALIDATED,
3939
SESSION_ATTRIBUTES_RESEND_REQUESTED,
40+
SESSION_ATTRIBUTES_FIELD_UPDATED,
4041
METRICS_SEND,
4142
METRICS_REQUEST,
4243
METRICS_RECEIVE,
@@ -85,6 +86,7 @@ const desktopAPI: DesktopAPI = {
8586
onLogout: () => ipcRenderer.send(TAB_LOGIN_CHANGED, false),
8687
invalidateSessionAttributeManifest: () => ipcRenderer.send(SESSION_ATTRIBUTES_MANIFEST_INVALIDATED),
8788
resendSessionAttributes: () => ipcRenderer.send(SESSION_ATTRIBUTES_RESEND_REQUESTED),
89+
updateSessionAttribute: (field) => ipcRenderer.send(SESSION_ATTRIBUTES_FIELD_UPDATED, field),
8890

8991
// Unreads/mentions/notifications
9092
sendNotification: (title, body, channelId, teamId, url, silent, soundName) =>

src/common/Validator.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,3 +418,14 @@ export const desktopSourcesOptsSchema = Joi.object({
418418
}),
419419
fetchWindowIcons: Joi.boolean(),
420420
});
421+
422+
export const sessionAttributeFieldSchema = Joi.object({
423+
name: Joi.string().required(),
424+
type: Joi.string().required(),
425+
attrs: Joi.object({
426+
enabled: Joi.boolean().required(),
427+
ttl_seconds: Joi.number().required(),
428+
grace_period_seconds: Joi.number().required(),
429+
platforms: Joi.array().items(Joi.string()).required(),
430+
}).unknown(true).required(),
431+
}).unknown(true);

src/common/communication.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,3 +263,4 @@ export const RESET_THEME = 'reset-theme';
263263
// Session attributes events
264264
export const SESSION_ATTRIBUTES_MANIFEST_INVALIDATED = 'session-attributes-manifest-invalidated';
265265
export const SESSION_ATTRIBUTES_RESEND_REQUESTED = 'session-attributes-resend-requested';
266+
export const SESSION_ATTRIBUTES_FIELD_UPDATED = 'session-attributes-field-updated';

src/main/sessionAttributes/sessionAttributesManager.test.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {ipcMain} from 'electron';
77
import WebContentsManager from 'app/views/webContentsManager';
88
import {
99
SERVER_REMOVED,
10+
SESSION_ATTRIBUTES_FIELD_UPDATED,
1011
SESSION_ATTRIBUTES_MANIFEST_INVALIDATED,
1112
SESSION_ATTRIBUTES_RESEND_REQUESTED,
1213
} from 'common/communication';
@@ -47,6 +48,7 @@ jest.mock('common/servers/serverManager', () => ({
4748
getServer: jest.fn(),
4849
lookupServerByURL: jest.fn(),
4950
getRemoteInfo: jest.fn(),
51+
updateRemoteInfo: jest.fn(),
5052
},
5153
}));
5254

@@ -84,6 +86,7 @@ describe('main/sessionAttributes/sessionAttributesManager', () => {
8486
let serverRemovedHandler: (srv: typeof server) => void;
8587
let manifestInvalidatedHandler: (event: IpcMainEvent) => void;
8688
let resendRequestedHandler: (event: IpcMainEvent) => void;
89+
let fieldUpdatedHandler: (event: IpcMainEvent, field: unknown) => void;
8790

8891
beforeEach(() => {
8992
jest.clearAllMocks();
@@ -101,6 +104,9 @@ describe('main/sessionAttributes/sessionAttributesManager', () => {
101104
if (channel === SESSION_ATTRIBUTES_RESEND_REQUESTED) {
102105
resendRequestedHandler = handler;
103106
}
107+
if (channel === SESSION_ATTRIBUTES_FIELD_UPDATED) {
108+
fieldUpdatedHandler = handler;
109+
}
104110
return ipcMainMock;
105111
});
106112
ServerManagerMock.getServer.mockReturnValue(server as never);
@@ -231,4 +237,116 @@ describe('main/sessionAttributes/sessionAttributesManager', () => {
231237
expect(updateServerInfos).not.toHaveBeenCalled();
232238
expect((manager as unknown as {lastSentAt: Map<string, Map<string, number>>}).lastSentAt.has(server.id)).toBe(true);
233239
});
240+
241+
it('appends a new field to the manifest on SESSION_ATTRIBUTES_FIELD_UPDATED', () => {
242+
ServerManagerMock.getRemoteInfo.mockReturnValue({
243+
sessionAttributesManifest: [
244+
{name: 'os_platform', type: 'text', ttl_seconds: 0, grace_period_seconds: 0, platforms: ['desktop']},
245+
],
246+
});
247+
WebContentsManagerMock.getViewByWebContentsId.mockReturnValue({serverId: server.id} as never);
248+
249+
fieldUpdatedHandler({sender: {id: 42}} as IpcMainEvent, {
250+
name: 'hardware_id',
251+
type: 'text',
252+
attrs: {enabled: true, ttl_seconds: 300, grace_period_seconds: 300, platforms: ['desktop']},
253+
});
254+
255+
expect(ServerManagerMock.updateRemoteInfo).toHaveBeenCalledWith(server.id, {
256+
sessionAttributesManifest: [
257+
{name: 'os_platform', type: 'text', ttl_seconds: 0, grace_period_seconds: 0, platforms: ['desktop']},
258+
{name: 'hardware_id', type: 'text', ttl_seconds: 300, grace_period_seconds: 300, platforms: ['desktop']},
259+
],
260+
});
261+
});
262+
263+
it('replaces an existing field in the manifest on SESSION_ATTRIBUTES_FIELD_UPDATED', () => {
264+
ServerManagerMock.getRemoteInfo.mockReturnValue({
265+
sessionAttributesManifest: [
266+
{name: 'hardware_id', type: 'text', ttl_seconds: 60, grace_period_seconds: 60, platforms: ['desktop']},
267+
],
268+
});
269+
WebContentsManagerMock.getViewByWebContentsId.mockReturnValue({serverId: server.id} as never);
270+
271+
fieldUpdatedHandler({sender: {id: 42}} as IpcMainEvent, {
272+
name: 'hardware_id',
273+
type: 'text',
274+
attrs: {enabled: true, ttl_seconds: 300, grace_period_seconds: 300, platforms: ['desktop']},
275+
});
276+
277+
expect(ServerManagerMock.updateRemoteInfo).toHaveBeenCalledWith(server.id, {
278+
sessionAttributesManifest: [
279+
{name: 'hardware_id', type: 'text', ttl_seconds: 300, grace_period_seconds: 300, platforms: ['desktop']},
280+
],
281+
});
282+
});
283+
284+
it('removes a field from the manifest when it is disabled', () => {
285+
ServerManagerMock.getRemoteInfo.mockReturnValue({
286+
sessionAttributesManifest: [
287+
{name: 'hardware_id', type: 'text', ttl_seconds: 300, grace_period_seconds: 300, platforms: ['desktop']},
288+
],
289+
});
290+
WebContentsManagerMock.getViewByWebContentsId.mockReturnValue({serverId: server.id} as never);
291+
292+
fieldUpdatedHandler({sender: {id: 42}} as IpcMainEvent, {
293+
name: 'hardware_id',
294+
type: 'text',
295+
attrs: {enabled: false, ttl_seconds: 300, grace_period_seconds: 300, platforms: ['desktop']},
296+
});
297+
298+
expect(ServerManagerMock.updateRemoteInfo).toHaveBeenCalledWith(server.id, {
299+
sessionAttributesManifest: [],
300+
});
301+
});
302+
303+
it('does not track a field that no longer targets the desktop', () => {
304+
ServerManagerMock.getRemoteInfo.mockReturnValue({
305+
sessionAttributesManifest: [
306+
{name: 'hardware_id', type: 'text', ttl_seconds: 300, grace_period_seconds: 300, platforms: ['desktop']},
307+
],
308+
});
309+
WebContentsManagerMock.getViewByWebContentsId.mockReturnValue({serverId: server.id} as never);
310+
311+
fieldUpdatedHandler({sender: {id: 42}} as IpcMainEvent, {
312+
name: 'hardware_id',
313+
type: 'text',
314+
attrs: {enabled: true, ttl_seconds: 300, grace_period_seconds: 300, platforms: ['mobile']},
315+
});
316+
317+
expect(ServerManagerMock.updateRemoteInfo).toHaveBeenCalledWith(server.id, {
318+
sessionAttributesManifest: [],
319+
});
320+
});
321+
322+
it('clears the lastSentAt entry for the updated field so it is resent', () => {
323+
ServerManagerMock.getRemoteInfo.mockReturnValue({
324+
sessionAttributesManifest: [
325+
{name: 'hardware_id', type: 'text', ttl_seconds: 300, grace_period_seconds: 300, platforms: ['desktop']},
326+
],
327+
});
328+
(manager as unknown as {lastSentAt: Map<string, Map<string, number>>}).lastSentAt.set(server.id, new Map([['hardware_id', Date.now()]]));
329+
WebContentsManagerMock.getViewByWebContentsId.mockReturnValue({serverId: server.id} as never);
330+
331+
fieldUpdatedHandler({sender: {id: 42}} as IpcMainEvent, {
332+
name: 'hardware_id',
333+
type: 'text',
334+
attrs: {enabled: true, ttl_seconds: 600, grace_period_seconds: 300, platforms: ['desktop']},
335+
});
336+
337+
expect((manager as unknown as {lastSentAt: Map<string, Map<string, number>>}).lastSentAt.get(server.id)?.has('hardware_id')).toBe(false);
338+
});
339+
340+
it('ignores an invalid field payload on SESSION_ATTRIBUTES_FIELD_UPDATED', () => {
341+
ServerManagerMock.getRemoteInfo.mockReturnValue({
342+
sessionAttributesManifest: [
343+
{name: 'hardware_id', type: 'text', ttl_seconds: 300, grace_period_seconds: 300, platforms: ['desktop']},
344+
],
345+
});
346+
WebContentsManagerMock.getViewByWebContentsId.mockReturnValue({serverId: server.id} as never);
347+
348+
fieldUpdatedHandler({sender: {id: 42}} as IpcMainEvent, {name: 'hardware_id'});
349+
350+
expect(ServerManagerMock.updateRemoteInfo).not.toHaveBeenCalled();
351+
});
234352
});

src/main/sessionAttributes/sessionAttributesManager.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
SERVER_PRE_AUTH_SECRET_CHANGED,
1010
SERVER_REMOVED,
1111
SERVER_URL_CHANGED,
12+
SESSION_ATTRIBUTES_FIELD_UPDATED,
1213
SESSION_ATTRIBUTES_MANIFEST_INVALIDATED,
1314
SESSION_ATTRIBUTES_RESEND_REQUESTED,
1415
} from 'common/communication';
@@ -18,9 +19,10 @@ import {Logger} from 'common/log';
1819
import type {MattermostServer} from 'common/servers/MattermostServer';
1920
import ServerManager from 'common/servers/serverManager';
2021
import {parseURL} from 'common/utils/url';
22+
import {ipcValidate, sessionAttributeFieldSchema} from 'common/Validator';
2123
import {updateServerInfos} from 'main/app/utils';
2224

23-
import type {SAField} from 'types/sessionAttributes';
25+
import type {SAField, SAPropertyField} from 'types/sessionAttributes';
2426

2527
import SessionAttributeCollector from './collector';
2628

@@ -36,6 +38,7 @@ export class SessionAttributesManager {
3638

3739
ipcMain.on(SESSION_ATTRIBUTES_MANIFEST_INVALIDATED, this.handleManifestInvalidated);
3840
ipcMain.on(SESSION_ATTRIBUTES_RESEND_REQUESTED, this.handleResendRequested);
41+
ipcMain.on(SESSION_ATTRIBUTES_FIELD_UPDATED, ipcValidate(this.handleFieldUpdated, [sessionAttributeFieldSchema]));
3942
}
4043

4144
getHeaderForRequest = (
@@ -197,6 +200,38 @@ export class SessionAttributesManager {
197200
}
198201
this.lastSentAt.delete(serverId);
199202
};
203+
204+
private handleFieldUpdated = (event: IpcMainEvent, field: SAPropertyField) => {
205+
log.debug('handleFieldUpdated', {name: field.name});
206+
207+
const serverId = WebContentsManager.getViewByWebContentsId(event.sender.id)?.serverId;
208+
if (!serverId) {
209+
return;
210+
}
211+
212+
const remoteInfo = ServerManager.getRemoteInfo(serverId);
213+
if (!remoteInfo || !remoteInfo.sessionAttributesManifest) {
214+
return;
215+
}
216+
217+
const manifest = remoteInfo.sessionAttributesManifest.filter((f) => f.name !== field.name);
218+
219+
// Only track enabled fields that target the desktop; disabled fields are dropped entirely
220+
if (field.attrs.enabled && field.attrs.platforms.includes('desktop')) {
221+
manifest.push({
222+
name: field.name,
223+
type: field.type,
224+
ttl_seconds: field.attrs.ttl_seconds,
225+
grace_period_seconds: field.attrs.grace_period_seconds,
226+
platforms: field.attrs.platforms,
227+
});
228+
}
229+
230+
ServerManager.updateRemoteInfo(serverId, {...remoteInfo, sessionAttributesManifest: manifest});
231+
232+
// Force the changed field to be re-sent on the next request
233+
this.lastSentAt.get(serverId)?.delete(field.name);
234+
};
200235
}
201236

202237
const sessionAttributesManager = new SessionAttributesManager();

src/types/sessionAttributes.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,15 @@ export type SAField = {
99
platforms: string[];
1010
};
1111

12+
export type SAPropertyField = {
13+
name: string;
14+
type: string;
15+
attrs: {
16+
enabled: boolean;
17+
ttl_seconds: number;
18+
grace_period_seconds: number;
19+
platforms: string[];
20+
};
21+
};
22+
1223
export type InterfaceType = 'wifi' | 'ethernet' | 'cellular' | 'vpn' | 'other';

0 commit comments

Comments
 (0)