1 /* eslint-disable @typescript-eslint/no-throw-literal, curly */
2 import { all, fork, put, select, takeEvery } from 'redux-saga/effects';
4 import { type EventManagerEvent, NOOP_EVENT } from '@proton/pass/lib/events/manager';
5 import { requestItemsForShareId } from '@proton/pass/lib/items/item.requests';
6 import { parseShareResponse } from '@proton/pass/lib/shares/share.parser';
7 import { hasShareAccessChanged } from '@proton/pass/lib/shares/share.predicates';
8 import { shareAccessChange, sharedVaultCreated, sharesSync, vaultCreationSuccess } from '@proton/pass/store/actions';
9 import type { ItemsByShareId } from '@proton/pass/store/reducers';
10 import { selectAllShares } from '@proton/pass/store/selectors';
11 import type { RootSagaOptions } from '@proton/pass/store/types';
12 import type { Api, Maybe, Share, ShareRole, SharesGetResponse } from '@proton/pass/types';
13 import { ShareType } from '@proton/pass/types';
14 import { or, truthy } from '@proton/pass/utils/fp/predicates';
15 import { diadic } from '@proton/pass/utils/fp/variadics';
16 import { logger } from '@proton/pass/utils/logger';
17 import { merge } from '@proton/pass/utils/object/merge';
18 import { toMap } from '@proton/shared/lib/helpers/object';
20 import { eventChannelFactory } from './channel.factory';
21 import { getShareChannelForks } from './channel.share';
22 import { channelEventsWorker, channelInitWorker } from './channel.worker';
23 import type { EventChannel } from './types';
25 /** We're only interested in new shares in this effect : Deleted shares will
26 * be handled by the share's EventChannel error handling. see `channel.share.ts`
27 * code `PassErrorCode.DISABLED_SHARE`. FIXME: handle ItemShares */
28 function* onSharesEvent(
29 event: EventManagerEvent<SharesGetResponse>,
30 { api }: EventChannel<any>,
31 options: RootSagaOptions
33 if ('error' in event) throw event.error;
35 const localShares: Share[] = yield select(selectAllShares);
36 const localShareIds = localShares.map(({ shareId }) => shareId);
38 const remoteShares = event.Shares;
40 const newShares = remoteShares.filter((share) => !localShareIds.includes(share.ShareID));
42 if (newShares.length) {
43 logger.info(`[ServerEvents::Shares]`, `${newShares.length} remote share(s) not in cache`);
45 const activeNewShares = (
48 .filter((share) => share.TargetType === ShareType.Vault)
49 .map((encryptedShare) => parseShareResponse(encryptedShare))
53 if (activeNewShares.length > 0) {
56 activeNewShares.map(async ({ shareId }) => ({
57 [shareId]: toMap(await requestItemsForShareId(shareId), 'itemId'),
59 )) as ItemsByShareId[]
60 ).reduce(diadic(merge));
62 yield put(sharesSync({ shares: toMap(activeNewShares, 'shareId'), items }));
63 yield all(activeNewShares.map(getShareChannelForks(api, options)).flat());
67 yield fork(function* () {
68 for (const share of remoteShares) {
69 const shareId = share.ShareID;
70 const localShare = localShares.find((localShare) => localShare.shareId === shareId);
72 if (localShare && hasShareAccessChanged(localShare, share)) {
75 newUserInvitesReady: share.NewUserInvitesReady,
79 shareRoleId: share.ShareRoleID as ShareRole,
80 targetMaxMembers: share.TargetMaxMembers,
81 targetMembers: share.TargetMembers,
89 /* The event-manager can be used to implement
90 * a polling mechanism if we conform to the data
91 * structure it is expecting. In order to poll for
92 * new shares, set the query accordingly & use a
93 * non-existing eventID */
94 export const createSharesChannel = (api: Api) =>
95 eventChannelFactory<SharesGetResponse>({
98 initialEventID: NOOP_EVENT,
99 getCursor: () => ({ EventID: NOOP_EVENT, More: false }),
100 onClose: () => logger.info(`[ServerEvents::Shares] closing channel`),
101 onEvent: onSharesEvent,
102 query: () => ({ url: 'pass/v1/share', method: 'get' }),
105 /* when a vault is created : recreate all the necessary
106 * channels to start polling for this new share's events */
107 function* onNewShare(api: Api, options: RootSagaOptions) {
108 yield takeEvery(or(vaultCreationSuccess.match, sharedVaultCreated.match), function* ({ payload: { share } }) {
109 yield all(getShareChannelForks(api, options)(share));
113 export function* sharesChannel(api: Api, options: RootSagaOptions) {
114 logger.info(`[ServerEvents::Shares] start polling for new shares`);
115 const eventsChannel = createSharesChannel(api);
116 const events = fork(channelEventsWorker<SharesGetResponse>, eventsChannel, options);
117 const wakeup = fork(channelInitWorker<SharesGetResponse>, eventsChannel, options);
118 const newVault = fork(onNewShare, api, options);
120 yield all([events, wakeup, newVault]);