@applicaster/zapp-react-native-utils 15.0.0-rc.14 → 15.0.0-rc.140
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.
- package/README.md +0 -6
- package/actionsExecutor/ActionExecutorContext.tsx +86 -12
- package/actionsExecutor/feedDecorator.ts +6 -6
- package/adsUtils/__tests__/createVMAP.test.ts +419 -0
- package/adsUtils/index.ts +2 -2
- package/analyticsUtils/README.md +1 -1
- package/analyticsUtils/analyticsMapper.ts +10 -2
- package/appDataUtils/__tests__/urlScheme.test.ts +678 -0
- package/appUtils/HooksManager/__tests__/__snapshots__/hooksManager.test.js.snap +0 -188
- package/appUtils/HooksManager/__tests__/hooksManager.test.js +16 -2
- package/appUtils/HooksManager/index.ts +45 -10
- package/appUtils/RiverFocusManager/{index.js → index.ts} +25 -18
- package/appUtils/accessibilityManager/__tests__/utils.test.ts +360 -0
- package/appUtils/accessibilityManager/const.ts +4 -0
- package/appUtils/accessibilityManager/hooks.ts +20 -13
- package/appUtils/accessibilityManager/index.ts +28 -1
- package/appUtils/accessibilityManager/utils.ts +59 -8
- package/appUtils/contextKeysManager/__tests__/getKeys/failure.test.ts +7 -2
- package/appUtils/contextKeysManager/__tests__/getKeys/success.test.ts +48 -0
- package/appUtils/contextKeysManager/contextResolver.ts +51 -22
- package/appUtils/contextKeysManager/index.ts +65 -10
- package/appUtils/focusManager/__tests__/__snapshots__/focusManager.test.js.snap +3 -0
- package/appUtils/focusManager/index.ios.ts +43 -4
- package/appUtils/focusManager/treeDataStructure/Tree/__tests__/Tree.test.js +46 -0
- package/appUtils/focusManager/treeDataStructure/Tree/index.js +18 -18
- package/appUtils/focusManagerAux/utils/index.ios.ts +122 -0
- package/appUtils/focusManagerAux/utils/index.ts +13 -7
- package/appUtils/focusManagerAux/utils/utils.ios.ts +202 -3
- package/appUtils/keyCodes/keys/keys.web.ts +1 -4
- package/appUtils/localizationsHelper.ts +4 -0
- package/appUtils/orientationHelper.ts +2 -4
- package/appUtils/platform/platformUtils.ts +117 -18
- package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +94 -4
- package/appUtils/playerManager/OverlayObserver/utils.ts +32 -20
- package/appUtils/playerManager/index.ts +9 -0
- package/appUtils/playerManager/player.ts +5 -1
- package/appUtils/playerManager/playerNative.ts +31 -17
- package/appUtils/playerManager/usePlayer.tsx +5 -3
- package/appUtils/playerManager/usePlayerState.tsx +14 -2
- package/arrayUtils/__tests__/allTruthy.test.ts +24 -0
- package/arrayUtils/__tests__/anyThruthy.test.ts +24 -0
- package/arrayUtils/index.ts +5 -0
- package/cellUtils/__tests__/cellUtils.test.ts +39 -0
- package/cellUtils/index.ts +43 -1
- package/cloudEventsUtils/__tests__/index.test.ts +529 -0
- package/cloudEventsUtils/index.ts +65 -1
- package/componentsUtils/index.ts +8 -0
- package/configurationUtils/__tests__/imageSrcFromMediaItem.test.ts +38 -0
- package/configurationUtils/__tests__/manifestKeyParser.test.ts +26 -26
- package/configurationUtils/index.ts +17 -11
- package/dateUtils/__tests__/dayjs.test.ts +327 -0
- package/dateUtils/index.ts +2 -0
- package/enumUtils/__tests__/getEnumKeyByEnumValue.test.ts +207 -0
- package/errorUtils/__tests__/GeneralError.test.ts +97 -0
- package/errorUtils/__tests__/HttpStatusCode.test.ts +344 -0
- package/errorUtils/__tests__/MissingPluginError.test.ts +113 -0
- package/errorUtils/__tests__/NetworkError.test.ts +202 -0
- package/errorUtils/__tests__/getParsedResponse.test.ts +188 -0
- package/errorUtils/__tests__/invariant.test.ts +112 -0
- package/focusManager/aux/index.ts +1 -1
- package/headersUtils/__tests__/headersUtils.test.js +11 -1
- package/headersUtils/index.ts +2 -1
- package/manifestUtils/_internals/__tests__/index.test.js +41 -0
- package/manifestUtils/_internals/index.js +33 -0
- package/manifestUtils/defaultManifestConfigurations/player.js +115 -11
- package/manifestUtils/fieldUtils/__tests__/fieldUtils.test.js +49 -0
- package/manifestUtils/fieldUtils/index.js +54 -0
- package/manifestUtils/index.js +2 -0
- package/manifestUtils/keys.js +249 -0
- package/manifestUtils/mobileAction/button/__tests__/mobileActionButton.test.js +168 -0
- package/manifestUtils/mobileAction/button/index.js +140 -0
- package/manifestUtils/mobileAction/container/__tests__/mobileActionButtonsContainer.test.js +102 -0
- package/manifestUtils/mobileAction/container/index.js +73 -0
- package/manifestUtils/mobileAction/groups/__tests__/buildMobileActionButtonGroups.test.js +127 -0
- package/manifestUtils/mobileAction/groups/defaults.js +76 -0
- package/manifestUtils/mobileAction/groups/index.js +80 -0
- package/manifestUtils/platformIsTV.js +13 -0
- package/manifestUtils/sharedConfiguration/screenPicker/utils.js +1 -0
- package/manifestUtils/tvAction/container/index.js +1 -1
- package/navigationUtils/index.ts +15 -5
- package/numberUtils/__tests__/toNumber.test.ts +27 -0
- package/numberUtils/__tests__/toPositiveNumber.test.ts +193 -0
- package/numberUtils/index.ts +23 -1
- package/package.json +4 -4
- package/playerUtils/usePlayerTTS.ts +8 -3
- package/pluginUtils/index.ts +4 -0
- package/reactHooks/advertising/index.ts +2 -2
- package/reactHooks/analytics/__tests__/useSendAnalyticsOnPress.test.ts +537 -0
- package/reactHooks/app/__tests__/useAppState.test.ts +1 -1
- package/reactHooks/autoscrolling/__tests__/useTrackCurrentAutoScrollingElement.test.ts +1 -1
- package/reactHooks/autoscrolling/__tests__/useTrackedView.test.tsx +1 -2
- package/reactHooks/cell-click/__tests__/index.test.js +1 -3
- package/reactHooks/cell-click/index.ts +2 -1
- package/reactHooks/configuration/__tests__/index.test.tsx +1 -1
- package/reactHooks/connection/__tests__/index.test.js +1 -1
- package/reactHooks/debugging/__tests__/index.test.js +4 -4
- package/reactHooks/dev/__tests__/useReRenderLog.test.ts +188 -0
- package/reactHooks/device/useIsTablet.tsx +14 -19
- package/reactHooks/device/useMemoizedIsTablet.ts +3 -3
- package/reactHooks/events/index.ts +20 -0
- package/reactHooks/feed/__tests__/useBatchLoading.test.tsx +32 -23
- package/reactHooks/feed/__tests__/useBuildPipesUrl.test.tsx +19 -19
- package/reactHooks/feed/__tests__/useEntryScreenId.test.tsx +4 -1
- package/reactHooks/feed/__tests__/useFeedLoader.test.tsx +42 -30
- package/reactHooks/feed/__tests__/{useInflatedUrl.test.ts → useInflatedUrl.test.tsx} +62 -7
- package/reactHooks/feed/index.ts +0 -2
- package/reactHooks/feed/useBatchLoading.ts +7 -1
- package/reactHooks/feed/useEntryScreenId.ts +2 -2
- package/reactHooks/feed/useInflatedUrl.ts +44 -18
- package/reactHooks/feed/usePipesCacheReset.ts +3 -1
- package/reactHooks/flatList/useLoadNextPageIfNeeded.ts +13 -16
- package/reactHooks/hookModal/hooks/useHookModalScreenData.ts +12 -8
- package/reactHooks/index.ts +2 -0
- package/reactHooks/layout/__tests__/index.test.tsx +1 -1
- package/reactHooks/layout/__tests__/useLayoutVersion.test.tsx +1 -1
- package/reactHooks/layout/index.ts +1 -1
- package/reactHooks/layout/useDimensions/__tests__/{useDimensions.test.ts → useDimensions.test.tsx} +105 -25
- package/reactHooks/layout/useDimensions/useDimensions.ts +2 -2
- package/reactHooks/navigation/__tests__/index.test.tsx +40 -9
- package/reactHooks/navigation/index.ts +27 -11
- package/reactHooks/navigation/useRoute.ts +11 -7
- package/reactHooks/player/TVSeekControlller/TVSeekController.ts +27 -10
- package/reactHooks/player/__tests__/useAutoSeek._test.tsx +1 -1
- package/reactHooks/player/__tests__/useTapSeek._test.ts +1 -1
- package/reactHooks/resolvers/__tests__/useCellResolver.test.tsx +1 -1
- package/reactHooks/resolvers/__tests__/useComponentResolver.test.tsx +1 -1
- package/reactHooks/resolvers/useCellResolver.ts +6 -2
- package/reactHooks/resolvers/useComponentResolver.ts +19 -3
- package/reactHooks/screen/__tests__/useCurrentScreenData.test.tsx +2 -2
- package/reactHooks/screen/__tests__/useScreenBackgroundColor.test.tsx +1 -1
- package/reactHooks/screen/__tests__/useScreenData.test.tsx +1 -1
- package/reactHooks/screen/__tests__/useTargetScreenData.test.tsx +12 -4
- package/reactHooks/screen/useTargetScreenData.ts +4 -2
- package/reactHooks/state/__tests__/useComponentScreenState.test.ts +246 -0
- package/reactHooks/state/index.ts +2 -0
- package/reactHooks/state/useComponentScreenState.ts +45 -0
- package/reactHooks/state/useRefWithInitialValue.ts +10 -0
- package/reactHooks/state/useRivers.ts +1 -1
- package/reactHooks/ui/__tests__/useFadeOutWhenBlurred.test.ts +580 -0
- package/reactHooks/usePluginConfiguration.ts +2 -2
- package/reactHooks/utils/__tests__/index.test.js +1 -1
- package/rectUtils/__tests__/index.test.ts +549 -0
- package/rectUtils/index.ts +2 -2
- package/refreshUtils/RefreshCoordinator/__tests__/refreshCoordinator.test.ts +206 -0
- package/refreshUtils/RefreshCoordinator/index.ts +245 -0
- package/refreshUtils/RefreshCoordinator/utils/__tests__/getDataRefreshConfig.test.ts +104 -0
- package/refreshUtils/RefreshCoordinator/utils/index.ts +29 -0
- package/screenPickerUtils/__tests__/index.test.ts +333 -0
- package/screenPickerUtils/index.ts +5 -0
- package/screenState/__tests__/index.test.ts +1 -1
- package/screenUtils/index.ts +3 -0
- package/searchUtils/const.ts +7 -0
- package/searchUtils/index.ts +3 -0
- package/services/storageServiceSync.web.ts +1 -1
- package/stringUtils/index.ts +1 -1
- package/testUtils/index.tsx +30 -21
- package/time/__tests__/BackgroundTimer.test.ts +156 -0
- package/time/__tests__/Timer.test.ts +236 -0
- package/typeGuards/__tests__/isString.test.ts +21 -0
- package/typeGuards/index.ts +4 -0
- package/utils/__tests__/clone.test.ts +158 -0
- package/utils/__tests__/mapAccum.test.ts +73 -0
- package/utils/__tests__/mergeRight.test.ts +48 -0
- package/utils/__tests__/path.test.ts +7 -0
- package/utils/__tests__/selectors.test.ts +124 -0
- package/utils/clone.ts +7 -0
- package/utils/index.ts +21 -1
- package/utils/mapAccum.ts +23 -0
- package/utils/mergeRight.ts +5 -0
- package/utils/path.ts +6 -3
- package/utils/pathOr.ts +5 -1
- package/utils/selectors.ts +46 -0
- package/zappFrameworkUtils/HookCallback/callbackNavigationAction.ts +49 -12
- package/zappFrameworkUtils/HookCallback/hookCallbackManifestExtensions.config.js +1 -1
- package/reactHooks/componentsMap/index.ts +0 -55
- package/reactHooks/feed/__tests__/useFeedRefresh.test.tsx +0 -75
- package/reactHooks/feed/useFeedRefresh.tsx +0 -65
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { refreshCoordinator } from "..";
|
|
2
|
+
import { getDataRefreshConfig } from "../utils";
|
|
3
|
+
import { Subscription } from "rxjs";
|
|
4
|
+
|
|
5
|
+
jest.mock("../utils", () => ({
|
|
6
|
+
getDataRefreshConfig: jest.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
describe("RefreshCoordinator", () => {
|
|
10
|
+
const component = { id: "c1" } as ZappUIComponent;
|
|
11
|
+
const screenId = "screen-1";
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
jest.clearAllMocks();
|
|
15
|
+
refreshCoordinator.clear();
|
|
16
|
+
jest.useFakeTimers();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
jest.runOnlyPendingTimers();
|
|
21
|
+
jest.useRealTimers();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// ----------------------------------------
|
|
25
|
+
// Singleton
|
|
26
|
+
// ----------------------------------------
|
|
27
|
+
it("returns the same instance via getInstance", () => {
|
|
28
|
+
const instance1 = refreshCoordinator;
|
|
29
|
+
const instance2 = (refreshCoordinator as any).constructor.getInstance();
|
|
30
|
+
expect(instance1).toBe(instance2);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// ----------------------------------------
|
|
34
|
+
// register & unregister
|
|
35
|
+
// ----------------------------------------
|
|
36
|
+
it("register starts a timer and returns an unregister function", () => {
|
|
37
|
+
(getDataRefreshConfig as jest.Mock).mockReturnValue({
|
|
38
|
+
refreshIntervalMs: 1000,
|
|
39
|
+
isRefreshingEnabled: true,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const unregister = refreshCoordinator.register(component, screenId);
|
|
43
|
+
|
|
44
|
+
expect(typeof unregister).toBe("function");
|
|
45
|
+
expect((refreshCoordinator as any).timers.size).toBe(1);
|
|
46
|
+
|
|
47
|
+
// Calling unregister should remove timer
|
|
48
|
+
unregister();
|
|
49
|
+
expect((refreshCoordinator as any).timers.size).toBe(0);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("does not register if refreshing is disabled", () => {
|
|
53
|
+
(getDataRefreshConfig as jest.Mock).mockReturnValue({
|
|
54
|
+
refreshIntervalMs: 1000,
|
|
55
|
+
isRefreshingEnabled: false,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const unregister = refreshCoordinator.register(component, screenId);
|
|
59
|
+
|
|
60
|
+
expect(unregister).toBeInstanceOf(Function);
|
|
61
|
+
expect((refreshCoordinator as any).timers.size).toBe(0);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("replaces existing timer if component is re-registered", () => {
|
|
65
|
+
(getDataRefreshConfig as jest.Mock).mockReturnValue({
|
|
66
|
+
refreshIntervalMs: 1000,
|
|
67
|
+
isRefreshingEnabled: true,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const unregister1 = refreshCoordinator.register(component, screenId);
|
|
71
|
+
const unregister2 = refreshCoordinator.register(component, screenId);
|
|
72
|
+
|
|
73
|
+
expect((refreshCoordinator as any).timers.size).toBe(1);
|
|
74
|
+
expect(unregister1).toBeInstanceOf(Function);
|
|
75
|
+
expect(unregister2).toBeInstanceOf(Function);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ----------------------------------------
|
|
79
|
+
// subscribeTo
|
|
80
|
+
// ----------------------------------------
|
|
81
|
+
it("subscribeTo only triggers listener for matching component & screen", () => {
|
|
82
|
+
(getDataRefreshConfig as jest.Mock).mockReturnValue({
|
|
83
|
+
refreshIntervalMs: 1000,
|
|
84
|
+
isRefreshingEnabled: true,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const listener = jest.fn();
|
|
88
|
+
refreshCoordinator.register(component, screenId);
|
|
89
|
+
const sub = refreshCoordinator.subscribeTo(component, screenId, listener);
|
|
90
|
+
|
|
91
|
+
// Emit a refresh manually via private Subject
|
|
92
|
+
(refreshCoordinator as any).refresh$.next({ component, screenId });
|
|
93
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
94
|
+
|
|
95
|
+
// Non-matching component should not call listener
|
|
96
|
+
(refreshCoordinator as any).refresh$.next({
|
|
97
|
+
component: { id: "other" } as ZappUIComponent,
|
|
98
|
+
screenId,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
102
|
+
|
|
103
|
+
sub.unsubscribe();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ----------------------------------------
|
|
107
|
+
// subscribeToAny
|
|
108
|
+
// ----------------------------------------
|
|
109
|
+
it("subscribeToAny triggers listener with throttling and replaces previous listener", () => {
|
|
110
|
+
const listener1 = jest.fn();
|
|
111
|
+
const listener2 = jest.fn();
|
|
112
|
+
|
|
113
|
+
// First subscription
|
|
114
|
+
const sub1: Subscription = refreshCoordinator.subscribeToAny(listener1);
|
|
115
|
+
expect(sub1).toBeInstanceOf(Subscription);
|
|
116
|
+
|
|
117
|
+
// Second subscription replaces first
|
|
118
|
+
const sub2: Subscription = refreshCoordinator.subscribeToAny(listener2);
|
|
119
|
+
expect(sub2).toBeInstanceOf(Subscription);
|
|
120
|
+
|
|
121
|
+
// Emit multiple events quickly
|
|
122
|
+
(refreshCoordinator as any).refresh$.next({ component, screenId });
|
|
123
|
+
(refreshCoordinator as any).refresh$.next({ component, screenId });
|
|
124
|
+
|
|
125
|
+
// Because of throttleTime(500ms), fast events are ignored
|
|
126
|
+
jest.advanceTimersByTime(500);
|
|
127
|
+
expect(listener1).not.toHaveBeenCalled();
|
|
128
|
+
expect(listener2).toHaveBeenCalledTimes(1);
|
|
129
|
+
|
|
130
|
+
sub2.unsubscribe();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// ----------------------------------------
|
|
134
|
+
// triggerRefresh
|
|
135
|
+
// ----------------------------------------
|
|
136
|
+
it("triggerRefresh emits refresh events for each component", () => {
|
|
137
|
+
const component1 = { id: "c1" } as ZappUIComponent;
|
|
138
|
+
const component2 = { id: "c2" } as ZappUIComponent;
|
|
139
|
+
const listener = jest.fn();
|
|
140
|
+
|
|
141
|
+
// Subscribe to all events
|
|
142
|
+
(refreshCoordinator as any).refresh$.subscribe(listener);
|
|
143
|
+
|
|
144
|
+
refreshCoordinator.triggerRefresh([component1, component2], screenId);
|
|
145
|
+
|
|
146
|
+
expect(listener).toHaveBeenCalledTimes(2);
|
|
147
|
+
|
|
148
|
+
expect(listener).toHaveBeenCalledWith({
|
|
149
|
+
component: component1,
|
|
150
|
+
screenId,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
expect(listener).toHaveBeenCalledWith({
|
|
154
|
+
component: component2,
|
|
155
|
+
screenId,
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("triggerRefresh with empty components does nothing", () => {
|
|
160
|
+
const listener = jest.fn();
|
|
161
|
+
|
|
162
|
+
(refreshCoordinator as any).refresh$.subscribe(listener);
|
|
163
|
+
|
|
164
|
+
refreshCoordinator.triggerRefresh([], screenId);
|
|
165
|
+
|
|
166
|
+
expect(listener).not.toHaveBeenCalled();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("triggerRefresh events are picked up by subscribeTo", () => {
|
|
170
|
+
const listener = jest.fn();
|
|
171
|
+
|
|
172
|
+
refreshCoordinator.subscribeTo(component, screenId, listener);
|
|
173
|
+
refreshCoordinator.triggerRefresh([component], screenId);
|
|
174
|
+
|
|
175
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ----------------------------------------
|
|
179
|
+
// clear
|
|
180
|
+
// ----------------------------------------
|
|
181
|
+
it("clear stops all timers, anyListener, and recreates subject", () => {
|
|
182
|
+
(getDataRefreshConfig as jest.Mock).mockReturnValue({
|
|
183
|
+
refreshIntervalMs: 1000,
|
|
184
|
+
isRefreshingEnabled: true,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const listener = jest.fn();
|
|
188
|
+
refreshCoordinator.register(component, screenId);
|
|
189
|
+
refreshCoordinator.subscribeToAny(listener);
|
|
190
|
+
|
|
191
|
+
expect((refreshCoordinator as any).timers.size).toBe(1);
|
|
192
|
+
expect((refreshCoordinator as any).anyListenerSubscription).toBeDefined();
|
|
193
|
+
|
|
194
|
+
refreshCoordinator.clear();
|
|
195
|
+
|
|
196
|
+
expect((refreshCoordinator as any).timers.size).toBe(0);
|
|
197
|
+
expect((refreshCoordinator as any).anyListenerSubscription).toBeUndefined();
|
|
198
|
+
|
|
199
|
+
// New events on new Subject still work
|
|
200
|
+
const newListener = jest.fn();
|
|
201
|
+
refreshCoordinator.subscribeToAny(newListener);
|
|
202
|
+
(refreshCoordinator as any).refresh$.next({ component, screenId });
|
|
203
|
+
jest.advanceTimersByTime(500);
|
|
204
|
+
expect(newListener).toHaveBeenCalled();
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { noop } from "@applicaster/zapp-react-native-utils/functionUtils";
|
|
2
|
+
import { interval, Subject, Subscription } from "rxjs";
|
|
3
|
+
import { map, filter, throttleTime } from "rxjs/operators";
|
|
4
|
+
|
|
5
|
+
import { getDataRefreshConfig } from "./utils";
|
|
6
|
+
|
|
7
|
+
export { getDataRefreshConfig, type RefreshConfig } from "./utils";
|
|
8
|
+
|
|
9
|
+
const THROTTLE_TIMEOUT = 500; // ms
|
|
10
|
+
|
|
11
|
+
type ComponentToRefresh = {
|
|
12
|
+
component: ZappUIComponent;
|
|
13
|
+
screenId: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Public contract for refresh coordination.
|
|
18
|
+
*
|
|
19
|
+
* The coordinator serves as an event bus for component refresh events.
|
|
20
|
+
* It supports two types of producers and any number of consumers:
|
|
21
|
+
*
|
|
22
|
+
* Producers (push events into refresh$):
|
|
23
|
+
* register() → starts a periodic timer that emits refresh events
|
|
24
|
+
* triggerRefresh() → imperatively emits refresh events (e.g. pull-to-refresh)
|
|
25
|
+
*
|
|
26
|
+
* Consumers (react to refresh events):
|
|
27
|
+
* subscribeTo() → listen to refresh events for a specific component/screen
|
|
28
|
+
* subscribeToAny() → listen to all refresh events with throttling
|
|
29
|
+
*
|
|
30
|
+
* Note: subscribeTo() works independently of register(). A component can
|
|
31
|
+
* subscribe to refresh events without having auto-refresh enabled.
|
|
32
|
+
* This is how pull-to-refresh works — triggerRefresh() pushes events that
|
|
33
|
+
* are picked up by existing subscribeTo() listeners in UrlFeedResolver,
|
|
34
|
+
* even for components that don't have periodic auto-refresh timers.
|
|
35
|
+
*/
|
|
36
|
+
interface IRefreshCoordinator {
|
|
37
|
+
register(component: ZappUIComponent, screenId: string): () => void;
|
|
38
|
+
triggerRefresh(components: ZappUIComponent[], screenId: string): void;
|
|
39
|
+
subscribeTo(
|
|
40
|
+
component: ZappUIComponent,
|
|
41
|
+
screenId: string,
|
|
42
|
+
listener: () => void
|
|
43
|
+
): Subscription;
|
|
44
|
+
subscribeToAny(
|
|
45
|
+
listener: (componentToRefresh: ComponentToRefresh) => void
|
|
46
|
+
): Subscription;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Singleton implementation of RefreshCoordinator.
|
|
51
|
+
*
|
|
52
|
+
* Design goals:
|
|
53
|
+
* - One shared coordinator across the app
|
|
54
|
+
* - Central refresh$ event bus for all refresh events
|
|
55
|
+
* - Multiple producers: periodic timers (register) and imperative triggers (triggerRefresh)
|
|
56
|
+
* - Multiple consumers: per-component (subscribeTo) and global (subscribeToAny)
|
|
57
|
+
* - Producers and consumers are independent — subscribeTo works without register
|
|
58
|
+
* - Safe cleanup of timers and subscriptions
|
|
59
|
+
*/
|
|
60
|
+
class RefreshCoordinator implements IRefreshCoordinator {
|
|
61
|
+
private static instance: RefreshCoordinator;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Active timers per component/screen pair ((componentId, screenId) → RxJS Subscription).
|
|
65
|
+
* Each entry is keyed by a composite of componentId and screenId (via getKey),
|
|
66
|
+
* and gets its own interval stream for periodic refresh.
|
|
67
|
+
*/
|
|
68
|
+
private timers: Map<string, Subscription> = new Map();
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Central event stream emitting components that need refresh.
|
|
72
|
+
* All component interval streams push into this Subject.
|
|
73
|
+
* Multiple listeners can subscribe to receive refresh events for specific components.
|
|
74
|
+
* This is the core stream that coordinates all refresh operations.
|
|
75
|
+
*/
|
|
76
|
+
private refresh$ = new Subject<ComponentToRefresh>();
|
|
77
|
+
|
|
78
|
+
// Store the current single subscriber for subscribeToAny
|
|
79
|
+
private anyListenerSubscription?: Subscription;
|
|
80
|
+
|
|
81
|
+
private constructor() {}
|
|
82
|
+
|
|
83
|
+
public static getInstance(): RefreshCoordinator {
|
|
84
|
+
if (!RefreshCoordinator.instance) {
|
|
85
|
+
RefreshCoordinator.instance = new RefreshCoordinator();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return RefreshCoordinator.instance;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Registers a component for periodic refresh.
|
|
93
|
+
*
|
|
94
|
+
* Behavior:
|
|
95
|
+
* - If the component was previously registered, its timer is replaced.
|
|
96
|
+
* - If refreshing is disabled, nothing is scheduled.
|
|
97
|
+
* - Returns an unregister function for manual cleanup.
|
|
98
|
+
* - Each component gets its own interval stream for periodic refresh.
|
|
99
|
+
*/
|
|
100
|
+
public register(component: ZappUIComponent, screenId: string): () => void {
|
|
101
|
+
const componentId: ZappUIComponent["id"] = component.id;
|
|
102
|
+
|
|
103
|
+
// Ensure no duplicate timer exists for this component
|
|
104
|
+
this.unregister(componentId, screenId);
|
|
105
|
+
|
|
106
|
+
const refreshConfig = getDataRefreshConfig(component);
|
|
107
|
+
|
|
108
|
+
// If refreshing disabled → do nothing
|
|
109
|
+
if (!refreshConfig.isRefreshingEnabled) {
|
|
110
|
+
return noop;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Create interval stream:
|
|
115
|
+
* - Emits every refreshIntervalMs
|
|
116
|
+
* - Maps emission to the ComponentToRefresh object
|
|
117
|
+
* - Pushes component into central refresh$ stream
|
|
118
|
+
*/
|
|
119
|
+
const subscription: Subscription = interval(refreshConfig.refreshIntervalMs)
|
|
120
|
+
.pipe(
|
|
121
|
+
map(() => ({
|
|
122
|
+
component,
|
|
123
|
+
screenId,
|
|
124
|
+
}))
|
|
125
|
+
)
|
|
126
|
+
.subscribe(this.refresh$);
|
|
127
|
+
|
|
128
|
+
this.timers.set(this.getKey(componentId, screenId), subscription);
|
|
129
|
+
|
|
130
|
+
return (): void => this.unregister(componentId, screenId);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Imperatively triggers refresh for the given components on a screen.
|
|
135
|
+
*
|
|
136
|
+
* Pushes each component into the central refresh$ stream so that
|
|
137
|
+
* existing subscribers (e.g. UrlFeedResolver's reloadData) react
|
|
138
|
+
* to the event. Used by pull-to-refresh and programmatic refresh.
|
|
139
|
+
*/
|
|
140
|
+
public triggerRefresh(components: ZappUIComponent[], screenId: string): void {
|
|
141
|
+
components.forEach((component) => {
|
|
142
|
+
this.refresh$.next({ component, screenId });
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Stops and removes timer for a specific component.
|
|
148
|
+
* Cleans up the RxJS subscription for that component's interval stream.
|
|
149
|
+
*/
|
|
150
|
+
private unregister(
|
|
151
|
+
componentId: ZappUIComponent["id"],
|
|
152
|
+
screenId: string
|
|
153
|
+
): void {
|
|
154
|
+
const key = this.getKey(componentId, screenId);
|
|
155
|
+
const subscription: Subscription | undefined = this.timers.get(key);
|
|
156
|
+
|
|
157
|
+
if (subscription) {
|
|
158
|
+
subscription.unsubscribe();
|
|
159
|
+
this.timers.delete(key);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private getKey(componentId: ZappUIComponent["id"], screenId: string): string {
|
|
164
|
+
return `componentId:${componentId}___screenId:${screenId}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Subscribes to refresh events for a specific component.
|
|
169
|
+
*
|
|
170
|
+
* Each subscription is independent - multiple listeners can subscribe
|
|
171
|
+
* to the same component's refresh events.
|
|
172
|
+
*/
|
|
173
|
+
public subscribeTo(
|
|
174
|
+
component: ZappUIComponent,
|
|
175
|
+
screenId: string,
|
|
176
|
+
listener: () => void
|
|
177
|
+
): Subscription {
|
|
178
|
+
return this.refresh$
|
|
179
|
+
.pipe(
|
|
180
|
+
filter(
|
|
181
|
+
(componentToRefresh: ComponentToRefresh) =>
|
|
182
|
+
componentToRefresh.component.id === component.id &&
|
|
183
|
+
componentToRefresh.screenId === screenId
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
.subscribe(listener);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Subscribes a single global listener to all refresh events.
|
|
191
|
+
*
|
|
192
|
+
* Behavior:
|
|
193
|
+
* - Only one active listener exists at a time.
|
|
194
|
+
* - If a previous listener was already subscribed, it is automatically unsubscribed.
|
|
195
|
+
* - Throttles refresh events to emit at most once per THROTTLE_TIMEOUT interval.
|
|
196
|
+
* - Useful for scenarios where you want a single handler to react
|
|
197
|
+
* to any component refresh without having multiple active callbacks.
|
|
198
|
+
*
|
|
199
|
+
* @param listener - Callback to invoke whenever any component triggers a refresh
|
|
200
|
+
* @returns The RxJS Subscription object for this listener
|
|
201
|
+
*/
|
|
202
|
+
public subscribeToAny(
|
|
203
|
+
listener: (componentToRefresh: ComponentToRefresh) => void
|
|
204
|
+
): Subscription {
|
|
205
|
+
// Unsubscribe previous listener if present to enforce "single active listener" policy
|
|
206
|
+
if (this.anyListenerSubscription) {
|
|
207
|
+
this.anyListenerSubscription.unsubscribe();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Create a new throttled subscription
|
|
211
|
+
this.anyListenerSubscription = this.refresh$
|
|
212
|
+
.pipe(throttleTime(THROTTLE_TIMEOUT))
|
|
213
|
+
.subscribe(listener);
|
|
214
|
+
|
|
215
|
+
return this.anyListenerSubscription;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Fully resets coordinator state.
|
|
220
|
+
*
|
|
221
|
+
* Clears:
|
|
222
|
+
* - All component timers
|
|
223
|
+
* - Any active "subscribeToAny" listener
|
|
224
|
+
* - Recreates internal Subject to allow fresh subscriptions
|
|
225
|
+
*/
|
|
226
|
+
public clear(): void {
|
|
227
|
+
// Stop and remove all active timers
|
|
228
|
+
this.timers.forEach((sub: Subscription) => sub.unsubscribe());
|
|
229
|
+
this.timers.clear();
|
|
230
|
+
|
|
231
|
+
// Stop any active "subscribeToAny" subscription
|
|
232
|
+
if (this.anyListenerSubscription) {
|
|
233
|
+
this.anyListenerSubscription.unsubscribe();
|
|
234
|
+
this.anyListenerSubscription = undefined;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Complete old Subject to release subscribers
|
|
238
|
+
this.refresh$.complete();
|
|
239
|
+
|
|
240
|
+
// Recreate Subject so coordinator can be reused
|
|
241
|
+
this.refresh$ = new Subject<ComponentToRefresh>();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export const refreshCoordinator = RefreshCoordinator.getInstance();
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getDataRefreshConfig,
|
|
3
|
+
MINIMUM_REFRESHING_INTERVAL_IN_SECONDS,
|
|
4
|
+
DEFAULT_REFRESHING_INTERVAL_IN_SECONDS,
|
|
5
|
+
} from "..";
|
|
6
|
+
|
|
7
|
+
describe("getDataRefreshConfig", () => {
|
|
8
|
+
it("returns correct values when rules are defined", () => {
|
|
9
|
+
const component = {
|
|
10
|
+
rules: {
|
|
11
|
+
enable_data_refreshing: true,
|
|
12
|
+
refreshing_interval: 30,
|
|
13
|
+
},
|
|
14
|
+
} as any;
|
|
15
|
+
|
|
16
|
+
const result = getDataRefreshConfig(component);
|
|
17
|
+
|
|
18
|
+
expect(result).toEqual({
|
|
19
|
+
isRefreshingEnabled: true,
|
|
20
|
+
refreshIntervalMs: 30 * 1000,
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("returns false for isRefreshingEnabled if rules flag is false", () => {
|
|
25
|
+
const component = {
|
|
26
|
+
rules: {
|
|
27
|
+
enable_data_refreshing: false,
|
|
28
|
+
refreshing_interval: 45,
|
|
29
|
+
},
|
|
30
|
+
} as any;
|
|
31
|
+
|
|
32
|
+
const result = getDataRefreshConfig(component);
|
|
33
|
+
|
|
34
|
+
expect(result).toEqual({
|
|
35
|
+
isRefreshingEnabled: false,
|
|
36
|
+
refreshIntervalMs: 45 * 1000,
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("applies minimum interval if given interval is too low", () => {
|
|
41
|
+
const component = {
|
|
42
|
+
rules: {
|
|
43
|
+
enable_data_refreshing: true,
|
|
44
|
+
refreshing_interval: 2,
|
|
45
|
+
},
|
|
46
|
+
} as any;
|
|
47
|
+
|
|
48
|
+
const result = getDataRefreshConfig(component);
|
|
49
|
+
|
|
50
|
+
expect(result.refreshIntervalMs).toBe(
|
|
51
|
+
MINIMUM_REFRESHING_INTERVAL_IN_SECONDS * 1000
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("uses default interval when rules.refreshing_interval is undefined", () => {
|
|
56
|
+
const component = {
|
|
57
|
+
rules: {
|
|
58
|
+
enable_data_refreshing: true,
|
|
59
|
+
},
|
|
60
|
+
} as any;
|
|
61
|
+
|
|
62
|
+
const result = getDataRefreshConfig(component);
|
|
63
|
+
|
|
64
|
+
expect(result.refreshIntervalMs).toBe(
|
|
65
|
+
DEFAULT_REFRESHING_INTERVAL_IN_SECONDS * 1000
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("handles missing rules gracefully", () => {
|
|
70
|
+
const component = {} as any;
|
|
71
|
+
|
|
72
|
+
const result = getDataRefreshConfig(component);
|
|
73
|
+
|
|
74
|
+
expect(result).toEqual({
|
|
75
|
+
isRefreshingEnabled: false,
|
|
76
|
+
refreshIntervalMs: DEFAULT_REFRESHING_INTERVAL_IN_SECONDS * 1000,
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("handles null component gracefully", () => {
|
|
81
|
+
const result = getDataRefreshConfig(null as any);
|
|
82
|
+
|
|
83
|
+
expect(result).toEqual({
|
|
84
|
+
isRefreshingEnabled: false,
|
|
85
|
+
refreshIntervalMs: DEFAULT_REFRESHING_INTERVAL_IN_SECONDS * 1000,
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("handles string values for rules correctly", () => {
|
|
90
|
+
const component = {
|
|
91
|
+
rules: {
|
|
92
|
+
enable_data_refreshing: "true",
|
|
93
|
+
refreshing_interval: "25",
|
|
94
|
+
},
|
|
95
|
+
} as any;
|
|
96
|
+
|
|
97
|
+
const result = getDataRefreshConfig(component);
|
|
98
|
+
|
|
99
|
+
expect(result).toEqual({
|
|
100
|
+
isRefreshingEnabled: true,
|
|
101
|
+
refreshIntervalMs: 25 * 1000,
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { isTrue } from "@applicaster/zapp-react-native-utils/booleanUtils";
|
|
2
|
+
import { toNumberWithDefault } from "@applicaster/zapp-react-native-utils/numberUtils";
|
|
3
|
+
|
|
4
|
+
export const MINIMUM_REFRESHING_INTERVAL_IN_SECONDS = 5; // 5 seconds
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_REFRESHING_INTERVAL_IN_SECONDS = 60; // 1 minute
|
|
7
|
+
|
|
8
|
+
export type RefreshConfig = {
|
|
9
|
+
isRefreshingEnabled: boolean;
|
|
10
|
+
refreshIntervalMs: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const getDataRefreshConfig = (
|
|
14
|
+
component: ZappUIComponent
|
|
15
|
+
): RefreshConfig => {
|
|
16
|
+
const isRefreshingEnabled = isTrue(component?.rules?.enable_data_refreshing);
|
|
17
|
+
|
|
18
|
+
const refreshing_interval = toNumberWithDefault(
|
|
19
|
+
DEFAULT_REFRESHING_INTERVAL_IN_SECONDS,
|
|
20
|
+
component?.rules?.refreshing_interval
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
isRefreshingEnabled,
|
|
25
|
+
refreshIntervalMs:
|
|
26
|
+
Math.max(refreshing_interval, MINIMUM_REFRESHING_INTERVAL_IN_SECONDS) *
|
|
27
|
+
1000,
|
|
28
|
+
};
|
|
29
|
+
};
|