@zeniai/client-epic-state 5.1.60-betaDI0 → 5.1.60-betaDI1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,10 @@
1
1
  import { NotificationRegistryPayload } from './notificationRegistryPayload';
2
2
  import { NotificationRegistryState } from './notificationRegistryState';
3
3
  export declare const initialState: NotificationRegistryState;
4
- export declare const clearAllNotificationRegistry: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"notificationRegistry/clearAllNotificationRegistry">, updateNotificationRegistry: import("@reduxjs/toolkit").ActionCreatorWithPayload<NotificationRegistryPayload, "notificationRegistry/updateNotificationRegistry">;
4
+ export interface UpdateNotificationRegistryPayload {
5
+ registry: NotificationRegistryPayload | undefined;
6
+ registryEpochAtDispatch: number;
7
+ }
8
+ export declare const clearAllNotificationRegistry: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"notificationRegistry/clearAllNotificationRegistry">, updateNotificationRegistry: import("@reduxjs/toolkit").ActionCreatorWithPayload<UpdateNotificationRegistryPayload, "notificationRegistry/updateNotificationRegistry">;
5
9
  declare const _default: import("redux").Reducer<NotificationRegistryState>;
6
10
  export default _default;
@@ -4,21 +4,45 @@ Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.updateNotificationRegistry = exports.clearAllNotificationRegistry = exports.initialState = void 0;
5
5
  const toolkit_1 = require("@reduxjs/toolkit");
6
6
  const notificationRegistryPayload_1 = require("./notificationRegistryPayload");
7
- exports.initialState = {};
7
+ exports.initialState = { registryEpoch: 0 };
8
8
  const notificationRegistry = (0, toolkit_1.createSlice)({
9
9
  name: 'notificationRegistry',
10
10
  initialState: exports.initialState,
11
11
  reducers: {
12
- clearAllNotificationRegistry() {
13
- return exports.initialState;
12
+ clearAllNotificationRegistry(draft) {
13
+ // Bump `registryEpoch` (don't rewind to 0) so any settings-envelope
14
+ // GET that was already in flight when the tenant switched is treated
15
+ // as stale: its captured `registryEpochAtDispatch` is now lower than
16
+ // the current epoch, so `updateNotificationRegistry` skips its write.
17
+ // Mirrors the `saveEpoch` bump in `clearAllNotificationPreferencesView`
18
+ // — Cursor Bugbot 67d6918e. Returning `initialState` verbatim would
19
+ // rewind the counter and let a stale in-flight GET restore A's
20
+ // registry over B's.
21
+ return { registryEpoch: draft.registryEpoch + 1 };
14
22
  },
15
23
  updateNotificationRegistry(draft, action) {
24
+ // Stale-response guard (Cursor Bugbot 67d6918e): if the fetch epic
25
+ // captured a `registryEpoch` lower than the current one, a clear
26
+ // (tenant switch) has landed since this GET dispatched. The response
27
+ // carries the prior tenant's snapshot and must not restore it — nor,
28
+ // when it omits the registry, wipe the fresh one. Skip entirely.
29
+ if (action.payload.registryEpochAtDispatch < draft.registryEpoch) {
30
+ return;
31
+ }
32
+ // Gate-off / absent-registry case: an envelope that omits
33
+ // `task_checklist_registry` CLEARS so tenant B doesn't inherit A's
34
+ // Task Checklist section. Guarded by the epoch check above so a stale
35
+ // absent-response can't wipe a fresh registry.
36
+ if (action.payload.registry == null) {
37
+ draft.registry = undefined;
38
+ return;
39
+ }
16
40
  // Full-replace contract: the registry payload is atomic — every fetch
17
41
  // returns the complete group/event tree. If the backend ever ships a
18
42
  // partial registry response, this write MUST switch to a merge (mirror
19
43
  // the preferences slice's `mergePreferences`). Do not assume the
20
44
  // current payload is partial without also updating this reducer.
21
- draft.registry = (0, notificationRegistryPayload_1.mapNotificationRegistryPayloadToRegistry)(action.payload);
45
+ draft.registry = (0, notificationRegistryPayload_1.mapNotificationRegistryPayloadToRegistry)(action.payload.registry);
22
46
  },
23
47
  },
24
48
  });
@@ -27,5 +27,6 @@ export interface NotificationRegistry {
27
27
  * `clearAllNotificationRegistry` from taking effect.
28
28
  */
29
29
  export interface NotificationRegistryState {
30
+ registryEpoch: number;
30
31
  registry?: NotificationRegistry;
31
32
  }
@@ -1,20 +1,44 @@
1
1
  import { createSlice } from '@reduxjs/toolkit';
2
2
  import { mapNotificationRegistryPayloadToRegistry, } from './notificationRegistryPayload';
3
- export const initialState = {};
3
+ export const initialState = { registryEpoch: 0 };
4
4
  const notificationRegistry = createSlice({
5
5
  name: 'notificationRegistry',
6
6
  initialState,
7
7
  reducers: {
8
- clearAllNotificationRegistry() {
9
- return initialState;
8
+ clearAllNotificationRegistry(draft) {
9
+ // Bump `registryEpoch` (don't rewind to 0) so any settings-envelope
10
+ // GET that was already in flight when the tenant switched is treated
11
+ // as stale: its captured `registryEpochAtDispatch` is now lower than
12
+ // the current epoch, so `updateNotificationRegistry` skips its write.
13
+ // Mirrors the `saveEpoch` bump in `clearAllNotificationPreferencesView`
14
+ // — Cursor Bugbot 67d6918e. Returning `initialState` verbatim would
15
+ // rewind the counter and let a stale in-flight GET restore A's
16
+ // registry over B's.
17
+ return { registryEpoch: draft.registryEpoch + 1 };
10
18
  },
11
19
  updateNotificationRegistry(draft, action) {
20
+ // Stale-response guard (Cursor Bugbot 67d6918e): if the fetch epic
21
+ // captured a `registryEpoch` lower than the current one, a clear
22
+ // (tenant switch) has landed since this GET dispatched. The response
23
+ // carries the prior tenant's snapshot and must not restore it — nor,
24
+ // when it omits the registry, wipe the fresh one. Skip entirely.
25
+ if (action.payload.registryEpochAtDispatch < draft.registryEpoch) {
26
+ return;
27
+ }
28
+ // Gate-off / absent-registry case: an envelope that omits
29
+ // `task_checklist_registry` CLEARS so tenant B doesn't inherit A's
30
+ // Task Checklist section. Guarded by the epoch check above so a stale
31
+ // absent-response can't wipe a fresh registry.
32
+ if (action.payload.registry == null) {
33
+ draft.registry = undefined;
34
+ return;
35
+ }
12
36
  // Full-replace contract: the registry payload is atomic — every fetch
13
37
  // returns the complete group/event tree. If the backend ever ships a
14
38
  // partial registry response, this write MUST switch to a merge (mirror
15
39
  // the preferences slice's `mergePreferences`). Do not assume the
16
40
  // current payload is partial without also updating this reducer.
17
- draft.registry = mapNotificationRegistryPayloadToRegistry(action.payload);
41
+ draft.registry = mapNotificationRegistryPayloadToRegistry(action.payload.registry);
18
42
  },
19
43
  },
20
44
  });
@@ -17,6 +17,12 @@ export const fetchNotificationSettingsEpic = (actions$, state$, zeniAPI) => acti
17
17
  // meantime — the GET is carrying the pre-PUT snapshot and would
18
18
  // otherwise clobber the fresh save. Cursor Bugbot 3632510237.
19
19
  const saveEpochAtDispatch = state.notificationPreferencesViewState.saveEpoch;
20
+ // Snapshot the registry clear epoch at GET-dispatch time (same idiom as
21
+ // `saveEpochAtDispatch` above). The registry reducer compares it against
22
+ // the current `registryEpoch` and skips its write when a
23
+ // `clearAllNotificationRegistry` (tenant switch) has landed since — the
24
+ // GET is carrying the prior tenant's snapshot. Cursor Bugbot 67d6918e.
25
+ const registryEpochAtDispatch = state.notificationRegistryState.registryEpoch;
20
26
  return zeniAPI
21
27
  .getJSON(notificationPreferencesUrl(zeniAPI))
22
28
  .pipe(mergeMap((response) => {
@@ -24,9 +30,16 @@ export const fetchNotificationSettingsEpic = (actions$, state$, zeniAPI) => acti
24
30
  const updateActions = [
25
31
  updateNotificationSettingsOnSuccess(response.data, userEmail),
26
32
  ];
27
- if (response.data.task_checklist_registry != null) {
28
- updateActions.push(updateNotificationRegistry(response.data.task_checklist_registry));
29
- }
33
+ // Always dispatch an absent `task_checklist_registry` (gate
34
+ // off) must CLEAR the prior tenant's registry, not leave it in
35
+ // place. The reducer clears on `undefined` and full-replaces
36
+ // otherwise, both guarded by the registry epoch so a stale GET
37
+ // that lands after a tenant switch is skipped. Cursor Bugbot
38
+ // 67d6918e.
39
+ updateActions.push(updateNotificationRegistry({
40
+ registry: response.data.task_checklist_registry ?? undefined,
41
+ registryEpochAtDispatch,
42
+ }));
30
43
  if (response.data.task_checklist_preferences != null) {
31
44
  updateActions.push(fetchNotificationPreferencesSuccess({
32
45
  preferences: response.data.task_checklist_preferences,
@@ -18,7 +18,7 @@ export declare const fetchNotificationSettingsEpic: (actions$: ActionsObservable
18
18
  payload: import("../../../responsePayload").ZeniAPIStatus<Record<string, unknown>>;
19
19
  type: "settings/updateNotificationSettingsOnFailure";
20
20
  } | {
21
- payload: import("../../../entity/notificationRegistry/notificationRegistryPayload").NotificationRegistryPayload;
21
+ payload: import("../../../entity/notificationRegistry/notificationRegistryReducer").UpdateNotificationRegistryPayload;
22
22
  type: "notificationRegistry/updateNotificationRegistry";
23
23
  } | {
24
24
  payload: import("../../notificationPreferencesView/notificationPreferencesViewReducer").FetchNotificationPreferencesSuccessPayload;
@@ -20,6 +20,12 @@ const fetchNotificationSettingsEpic = (actions$, state$, zeniAPI) => actions$.pi
20
20
  // meantime — the GET is carrying the pre-PUT snapshot and would
21
21
  // otherwise clobber the fresh save. Cursor Bugbot 3632510237.
22
22
  const saveEpochAtDispatch = state.notificationPreferencesViewState.saveEpoch;
23
+ // Snapshot the registry clear epoch at GET-dispatch time (same idiom as
24
+ // `saveEpochAtDispatch` above). The registry reducer compares it against
25
+ // the current `registryEpoch` and skips its write when a
26
+ // `clearAllNotificationRegistry` (tenant switch) has landed since — the
27
+ // GET is carrying the prior tenant's snapshot. Cursor Bugbot 67d6918e.
28
+ const registryEpochAtDispatch = state.notificationRegistryState.registryEpoch;
23
29
  return zeniAPI
24
30
  .getJSON((0, notificationPreferencesEndpoint_1.notificationPreferencesUrl)(zeniAPI))
25
31
  .pipe((0, operators_1.mergeMap)((response) => {
@@ -27,9 +33,16 @@ const fetchNotificationSettingsEpic = (actions$, state$, zeniAPI) => actions$.pi
27
33
  const updateActions = [
28
34
  (0, settingsViewReducer_1.updateNotificationSettingsOnSuccess)(response.data, userEmail),
29
35
  ];
30
- if (response.data.task_checklist_registry != null) {
31
- updateActions.push((0, notificationRegistryReducer_1.updateNotificationRegistry)(response.data.task_checklist_registry));
32
- }
36
+ // Always dispatch an absent `task_checklist_registry` (gate
37
+ // off) must CLEAR the prior tenant's registry, not leave it in
38
+ // place. The reducer clears on `undefined` and full-replaces
39
+ // otherwise, both guarded by the registry epoch so a stale GET
40
+ // that lands after a tenant switch is skipped. Cursor Bugbot
41
+ // 67d6918e.
42
+ updateActions.push((0, notificationRegistryReducer_1.updateNotificationRegistry)({
43
+ registry: response.data.task_checklist_registry ?? undefined,
44
+ registryEpochAtDispatch,
45
+ }));
33
46
  if (response.data.task_checklist_preferences != null) {
34
47
  updateActions.push((0, notificationPreferencesViewReducer_1.fetchNotificationPreferencesSuccess)({
35
48
  preferences: response.data.task_checklist_preferences,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeniai/client-epic-state",
3
- "version": "5.1.60-betaDI0",
3
+ "version": "5.1.60-betaDI1",
4
4
  "description": "Shared module between Web & Mobile containing required abstractions for state management, async network communication. ",
5
5
  "main": "lib/index.js",
6
6
  "module": "lib/esm/index.js",