@rancher/shell 3.0.8 → 3.0.9-rc.2

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.
Files changed (192) hide show
  1. package/apis/intf/modal.ts +38 -0
  2. package/apis/intf/slide-in.ts +3 -1
  3. package/apis/shell/__tests__/slide-in.test.ts +36 -0
  4. package/apis/shell/slide-in.ts +5 -1
  5. package/assets/styles/base/_color.scss +1 -0
  6. package/assets/styles/base/_typography.scss +14 -5
  7. package/assets/styles/themes/_light.scss +1 -1
  8. package/assets/styles/themes/_modern.scss +1 -1
  9. package/assets/translations/en-us.yaml +94 -33
  10. package/assets/translations/zh-hans.yaml +0 -2
  11. package/components/ActionMenuShell.vue +4 -4
  12. package/components/CodeMirror.vue +4 -3
  13. package/components/DetailText.vue +54 -7
  14. package/components/Drawer/Chrome.vue +11 -4
  15. package/components/Drawer/DrawerCard.vue +19 -0
  16. package/components/Drawer/ResourceDetailDrawer/ConfigTab.vue +3 -11
  17. package/components/Drawer/ResourceDetailDrawer/__tests__/ConfigTab.test.ts +2 -2
  18. package/components/Drawer/ResourceDetailDrawer/index.vue +3 -20
  19. package/components/Drawer/types.ts +1 -0
  20. package/components/DynamicContent/DynamicContentCloseButton.vue +2 -2
  21. package/components/LocaleSelector.vue +1 -1
  22. package/components/Markdown.vue +1 -1
  23. package/components/PopoverCard.vue +3 -3
  24. package/components/Resource/Detail/Card/ExtrasCard.vue +39 -0
  25. package/components/Resource/Detail/Card/StateCard/__tests__/composables.test.ts +142 -0
  26. package/components/Resource/Detail/Card/StateCard/composables.ts +41 -11
  27. package/components/Resource/Detail/Card/StateCard/index.vue +3 -9
  28. package/components/Resource/Detail/Card/StateCard/types.ts +6 -0
  29. package/components/Resource/Detail/Card/{PodsCard → StatusCard}/index.vue +11 -10
  30. package/components/Resource/Detail/Card/__tests__/PodsCard.test.ts +24 -25
  31. package/components/Resource/Detail/Cards.vue +27 -0
  32. package/components/Resource/Detail/Masthead/__tests__/index.test.ts +70 -0
  33. package/components/Resource/Detail/Masthead/index.vue +5 -0
  34. package/components/Resource/Detail/Metadata/KeyValueRow.vue +4 -2
  35. package/components/Resource/Detail/ResourcePopover/ResourcePopoverCard.vue +2 -2
  36. package/components/Resource/Detail/ResourceRow.types.ts +14 -0
  37. package/components/Resource/Detail/ResourceRow.vue +23 -35
  38. package/components/Resource/Detail/StatusRow.vue +5 -2
  39. package/components/Resource/Detail/TitleBar/__tests__/composables.test.ts +38 -7
  40. package/components/Resource/Detail/TitleBar/__tests__/index.test.ts +106 -2
  41. package/components/Resource/Detail/TitleBar/composables.ts +2 -1
  42. package/components/Resource/Detail/TitleBar/index.vue +41 -6
  43. package/components/ResourceDetail/Masthead/__tests__/index.test.ts +49 -1
  44. package/components/ResourceDetail/Masthead/__tests__/latest.test.ts +85 -0
  45. package/components/ResourceDetail/Masthead/index.vue +1 -0
  46. package/components/ResourceDetail/Masthead/latest.vue +8 -1
  47. package/components/ResourceDetail/Masthead/legacy.vue +1 -1
  48. package/components/Setting.vue +1 -1
  49. package/components/SortableTable/index.vue +25 -0
  50. package/components/SortableTable/selection.js +25 -12
  51. package/components/SortableTable/sorting.js +1 -1
  52. package/components/Tabbed/Tab.vue +1 -0
  53. package/components/Tabbed/index.vue +29 -6
  54. package/components/Window/ContainerShell.vue +10 -13
  55. package/components/fleet/FleetClusterTargets/TargetsList.vue +47 -29
  56. package/components/fleet/FleetClusterTargets/index.vue +82 -29
  57. package/components/fleet/FleetClusters.vue +26 -12
  58. package/components/fleet/FleetGitRepoPaths.vue +2 -2
  59. package/components/fleet/FleetResources.vue +14 -0
  60. package/components/fleet/FleetValuesFrom.vue +2 -2
  61. package/components/fleet/__tests__/FleetClusterTargets.test.ts +531 -0
  62. package/components/fleet/__tests__/FleetClusters.test.ts +576 -0
  63. package/components/fleet/dashboard/ResourceDetails.vue +96 -123
  64. package/components/form/Conditions.vue +1 -15
  65. package/components/form/HookOption.vue +5 -0
  66. package/components/form/LabeledSelect.vue +1 -1
  67. package/components/form/LifecycleHooks.vue +2 -6
  68. package/components/form/ResourceLabeledSelect.vue +12 -1
  69. package/components/form/SeccompProfile.vue +113 -0
  70. package/components/form/Security.vue +244 -133
  71. package/components/form/__tests__/LabeledSelect.test.ts +1 -1
  72. package/components/form/__tests__/SeccompProfile.test.js +124 -0
  73. package/components/form/__tests__/Security.test.ts +125 -37
  74. package/components/formatter/Autoscaler.vue +2 -2
  75. package/components/formatter/FleetSummaryGraph.vue +4 -1
  76. package/components/nav/Group.vue +5 -0
  77. package/components/nav/Header.vue +3 -3
  78. package/components/nav/HeaderPageActionMenu.vue +1 -1
  79. package/components/nav/NamespaceFilter.vue +6 -6
  80. package/components/nav/NotificationCenter/index.vue +1 -1
  81. package/components/nav/TopLevelMenu.helper.ts +41 -16
  82. package/components/nav/TopLevelMenu.vue +45 -25
  83. package/components/nav/WorkspaceSwitcher.vue +1 -1
  84. package/components/nav/__tests__/TopLevelMenu.helper.test.ts +277 -0
  85. package/components/nav/__tests__/TopLevelMenu.test.ts +160 -4
  86. package/components/templates/default.vue +0 -3
  87. package/components/templates/home.vue +0 -3
  88. package/components/templates/plain.vue +0 -3
  89. package/composables/useClickOutside.ts +1 -1
  90. package/config/product/explorer.js +1 -2
  91. package/config/types.js +41 -8
  92. package/detail/__tests__/workload.test.ts +8 -16
  93. package/detail/catalog.cattle.io.app.vue +6 -0
  94. package/detail/fleet.cattle.io.cluster.vue +6 -0
  95. package/detail/workload/index.vue +7 -109
  96. package/edit/__tests__/projectsecret.test.ts +42 -0
  97. package/edit/auth/__tests__/oidc.test.ts +50 -0
  98. package/edit/auth/oidc.vue +68 -44
  99. package/edit/autoscaling.horizontalpodautoscaler/index.vue +140 -59
  100. package/edit/autoscaling.horizontalpodautoscaler/metrics-row.vue +41 -5
  101. package/edit/projectsecret.vue +29 -0
  102. package/edit/provisioning.cattle.io.cluster/__tests__/Basics.test.ts +89 -200
  103. package/edit/provisioning.cattle.io.cluster/__tests__/Networking.test.ts +58 -17
  104. package/edit/provisioning.cattle.io.cluster/rke2.vue +11 -0
  105. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +3 -63
  106. package/edit/provisioning.cattle.io.cluster/tabs/networking/index.vue +82 -14
  107. package/edit/workload/__tests__/index.test.ts +122 -85
  108. package/edit/workload/index.vue +48 -29
  109. package/edit/workload/mixins/workload.js +85 -32
  110. package/list/catalog.cattle.io.clusterrepo.vue +1 -1
  111. package/list/projectsecret.vue +2 -2
  112. package/machine-config/__tests__/vmwarevsphere.test.ts +64 -0
  113. package/machine-config/amazonec2.vue +2 -2
  114. package/machine-config/vmwarevsphere.vue +58 -4
  115. package/mixins/__tests__/brand.spec.ts +18 -13
  116. package/mixins/__tests__/chart.test.ts +63 -0
  117. package/mixins/chart.js +56 -51
  118. package/models/__tests__/catalog.cattle.io.app.test.ts +33 -0
  119. package/models/__tests__/workload.test.ts +333 -0
  120. package/models/catalog.cattle.io.app.js +8 -0
  121. package/models/pod.js +14 -0
  122. package/models/secret.js +1 -1
  123. package/models/workload.js +93 -27
  124. package/package.json +4 -4
  125. package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +91 -0
  126. package/pages/c/_cluster/apps/charts/install.vue +4 -4
  127. package/pages/c/_cluster/explorer/EventsTable.vue +2 -2
  128. package/pages/c/_cluster/fleet/index.vue +18 -12
  129. package/pages/c/_cluster/manager/hostedprovider/index.vue +1 -19
  130. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +1 -1
  131. package/pages/c/_cluster/uiplugins/index.vue +1 -1
  132. package/plugins/dashboard-store/__tests__/resource-class.test.ts +234 -0
  133. package/plugins/dashboard-store/actions.js +9 -8
  134. package/plugins/dashboard-store/resource-class.js +97 -1
  135. package/plugins/steve/__tests__/revision.test.ts +84 -0
  136. package/plugins/steve/__tests__/steve-pagination-utils.test.ts +30 -0
  137. package/plugins/steve/__tests__/subscribe.spec.ts +134 -0
  138. package/plugins/steve/mutations.js +9 -0
  139. package/plugins/steve/revision.ts +26 -0
  140. package/plugins/steve/steve-pagination-utils.ts +6 -5
  141. package/plugins/steve/subscribe.js +211 -51
  142. package/plugins/subscribe-events.ts +2 -2
  143. package/rancher-components/Form/Checkbox/Checkbox.vue +13 -0
  144. package/rancher-components/LabeledTooltip/LabeledTooltip.vue +1 -1
  145. package/rancher-components/Pill/RcCounterBadge/RcCounterBadge.vue +1 -1
  146. package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +3 -1
  147. package/rancher-components/Pill/RcStatusIndicator/RcStatusIndicator.vue +3 -1
  148. package/rancher-components/Pill/RcTag/RcTag.vue +1 -1
  149. package/rancher-components/Pill/index.ts +4 -0
  150. package/rancher-components/RcButton/RcButton.test.ts +53 -9
  151. package/rancher-components/RcButton/RcButton.vue +217 -25
  152. package/rancher-components/RcButton/types.ts +27 -1
  153. package/rancher-components/RcDropdown/RcDropdownMenu.vue +4 -4
  154. package/rancher-components/RcDropdown/types.ts +3 -3
  155. package/rancher-components/RcIcon/RcIcon.test.ts +42 -0
  156. package/rancher-components/RcIcon/RcIcon.vue +9 -6
  157. package/rancher-components/RcIcon/types.ts +13 -9
  158. package/rancher-components/utils/status.test.ts +10 -15
  159. package/rancher-components/utils/status.ts +5 -6
  160. package/store/aws.js +18 -12
  161. package/store/index.js +4 -8
  162. package/store/type-map.utils.ts +1 -1
  163. package/types/kube/kube-api.ts +29 -3
  164. package/types/rancher/steve.api.ts +40 -0
  165. package/types/shell/index.d.ts +99 -0
  166. package/types/store/dashboard-store.types.ts +29 -7
  167. package/types/store/pagination.types.ts +1 -0
  168. package/types/store/subscribe-events.types.ts +1 -0
  169. package/utils/__tests__/azure.test.ts +56 -0
  170. package/utils/__tests__/back-off.test.ts +364 -245
  171. package/utils/__tests__/error.test.ts +44 -0
  172. package/utils/__tests__/fleet.test.ts +8 -1
  173. package/utils/__tests__/pagination-wrapper.test.ts +167 -0
  174. package/utils/__tests__/version.test.ts +55 -1
  175. package/utils/azure.js +12 -0
  176. package/utils/back-off.ts +302 -69
  177. package/utils/cspAdaptor.ts +32 -14
  178. package/utils/dynamic-content/__tests__/index.test.ts +1 -1
  179. package/utils/dynamic-content/__tests__/new-release.test.ts +48 -7
  180. package/utils/dynamic-content/__tests__/support-notice.test.ts +1 -4
  181. package/utils/dynamic-content/index.ts +1 -6
  182. package/utils/dynamic-content/new-release.ts +5 -3
  183. package/utils/dynamic-content/types.d.ts +0 -1
  184. package/utils/error.js +9 -0
  185. package/utils/fleet.ts +2 -2
  186. package/utils/inactivity.ts +2 -3
  187. package/utils/pagination-wrapper.ts +101 -17
  188. package/utils/validators/formRules/index.ts +3 -0
  189. package/utils/version.js +38 -0
  190. package/components/auth/AzureWarning.vue +0 -77
  191. /package/components/Resource/Detail/{Card/PodsCard/Bubble.vue → Bubble.vue} +0 -0
  192. /package/components/Resource/Detail/Card/{PodsCard → StatusCard}/composable.ts +0 -0
@@ -1,5 +1,3 @@
1
- // Import the BackOff instance from your code.
2
- // Assuming the file is named `backoff.ts` and the default export is the BackOff instance.
3
1
  import backOff from '../back-off';
4
2
 
5
3
  describe('backOff', () => {
@@ -10,345 +8,466 @@ describe('backOff', () => {
10
8
  let consoleLogMock: jest.SpyInstance;
11
9
  let consoleErrorMock: jest.SpyInstance;
12
10
 
13
- beforeEach(() => {
11
+ const beforeEachFn = () => {
14
12
  // Before each test, reset the BackOff state.
15
13
  backOff.resetAll();
16
14
 
17
15
  // Create new mocks for each test to ensure a clean slate.
18
16
  consoleLogMock = jest.spyOn(console, 'log').mockImplementation(() => {});
19
17
  consoleErrorMock = jest.spyOn(console, 'error').mockImplementation(() => {});
20
- });
18
+ };
21
19
 
22
- afterEach(() => {
20
+ const afterEachFn = () => {
23
21
  // Restore the original console functions after each test.
24
22
  consoleLogMock.mockRestore();
25
23
  consoleErrorMock.mockRestore();
26
- });
24
+ };
25
+
26
+ describe('execute', () => {
27
+ beforeEach(beforeEachFn);
28
+
29
+ afterEach(afterEachFn);
27
30
 
28
- // --- Test Suite for `execute` method ---
31
+ it('should execute the function immediately the first time without a delay', async() => {
32
+ const delayedFn = jest.fn();
29
33
 
30
- it('should execute the function immediately the first time without a delay', async() => {
31
- const delayedFn = jest.fn();
34
+ // Call the function for the first time.
35
+ await backOff.execute({
36
+ id: 'test1', description: 'Test 1', delayedFn
37
+ });
32
38
 
33
- // Call the function for the first time.
34
- await backOff.execute({
35
- id: 'test1', description: 'Test 1', delayedFn
39
+ // The function should have been called immediately, since the fake timer hasn't advanced.
40
+ expect(delayedFn).toHaveBeenCalledTimes(0);
41
+ expect(backOff.getBackOff('test1')).toBeDefined();
36
42
  });
37
43
 
38
- // The function should have been called immediately, since the fake timer hasn't advanced.
39
- expect(delayedFn).toHaveBeenCalledTimes(0);
40
- expect(backOff.getBackOff('test1')).toBeDefined();
41
- });
44
+ it('should back off and delay the second execution', async() => {
45
+ const delayedFn = jest.fn();
46
+ const id = 'backoff-test';
42
47
 
43
- it('should back off and delay the second execution', async() => {
44
- const delayedFn = jest.fn();
45
- const id = 'backoff-test';
48
+ // First call, which should run immediately.
49
+ await backOff.execute({
50
+ id, description: 'Backoff Test', delayedFn,
51
+ });
46
52
 
47
- // First call, which should run immediately.
48
- await backOff.execute({
49
- id, description: 'Backoff Test', delayedFn,
50
- });
53
+ jest.advanceTimersByTime(1);
54
+
55
+ // Expect the first call to be immediate.
56
+ expect(delayedFn).toHaveBeenCalledTimes(1);
51
57
 
52
- jest.advanceTimersByTime(1);
58
+ // Call it a second time. This should initiate a backoff delay.
59
+ await backOff.execute({
60
+ id, description: 'Backoff Test', delayedFn
61
+ });
53
62
 
54
- // Expect the first call to be immediate.
55
- expect(delayedFn).toHaveBeenCalledTimes(1);
63
+ // The function should not have been called a second time yet.
64
+ expect(delayedFn).toHaveBeenCalledTimes(1);
56
65
 
57
- // Call it a second time. This should initiate a backoff delay.
58
- await backOff.execute({
59
- id, description: 'Backoff Test', delayedFn
66
+ // Advance the timer by less than the first backoff delay (250ms).
67
+ jest.advanceTimersByTime(200);
68
+ expect(delayedFn).toHaveBeenCalledTimes(1);
69
+
70
+ // Now, advance the timer by the required delay to trigger the second call.
71
+ jest.advanceTimersByTime(50);
72
+
73
+ // The function should have been called a second time.
74
+ await Promise.resolve();
75
+ expect(delayedFn).toHaveBeenCalledTimes(2);
76
+
77
+ // Verify the backoff entry was created correctly.
78
+ const backOffEntry = backOff.getBackOff(id);
79
+
80
+ expect(backOffEntry.try).toBe(2);
60
81
  });
61
82
 
62
- // The function should not have been called a second time yet.
63
- expect(delayedFn).toHaveBeenCalledTimes(1);
83
+ it('should implement exponential backoff on subsequent calls', async() => {
84
+ const delayedFn = jest.fn();
85
+ const id = 'exp-backoff';
86
+
87
+ // First call (immediate)
88
+ await backOff.execute({
89
+ id, description: 'Exponential Backoff Test', delayedFn
90
+ });
91
+
92
+ jest.advanceTimersByTime(1);
93
+ expect(delayedFn).toHaveBeenCalledTimes(1);
94
+
95
+ // Second call (should have a delay of 1^2 * 250 = 250ms)
96
+ await backOff.execute({
97
+ id, description: 'Exponential Backoff Test', delayedFn
98
+ });
99
+ jest.advanceTimersByTime(250);
100
+ await Promise.resolve();
101
+ expect(delayedFn).toHaveBeenCalledTimes(2);
102
+
103
+ // Third call (should have a delay of 2^2 * 250 = 1000ms)
104
+ await backOff.execute({
105
+ id, description: 'Exponential Backoff Test', delayedFn
106
+ });
107
+ jest.advanceTimersByTime(1000);
108
+ await Promise.resolve();
109
+ expect(delayedFn).toHaveBeenCalledTimes(3);
110
+
111
+ // Fourth call (should have a delay of 3^2 * 250 = 2250ms)
112
+ await backOff.execute({
113
+ id, description: 'Exponential Backoff Test', delayedFn
114
+ });
115
+ jest.advanceTimersByTime(2250);
116
+ await Promise.resolve();
117
+ expect(delayedFn).toHaveBeenCalledTimes(4);
118
+ });
64
119
 
65
- // Advance the timer by less than the first backoff delay (250ms).
66
- jest.advanceTimersByTime(200);
67
- expect(delayedFn).toHaveBeenCalledTimes(1);
120
+ it('should skip execution if a previous backoff process is already running', async() => {
121
+ const delayedFn = jest.fn();
122
+ const id = 'skip-test';
68
123
 
69
- // Now, advance the timer by the required delay to trigger the second call.
70
- jest.advanceTimersByTime(50);
124
+ await backOff.execute({
125
+ id, description: 'Skip Test', delayedFn
126
+ });
127
+ expect(delayedFn).toHaveBeenCalledTimes(0);
71
128
 
72
- // The function should have been called a second time.
73
- await Promise.resolve();
74
- expect(delayedFn).toHaveBeenCalledTimes(2);
129
+ // Second call, will be ignored
130
+ await backOff.execute({
131
+ id, description: 'Skip Test', delayedFn
132
+ });
75
133
 
76
- // Verify the backoff entry was created correctly.
77
- const backOffEntry = backOff.getBackOff(id);
134
+ expect(delayedFn).toHaveBeenCalledTimes(0);
78
135
 
79
- expect(backOffEntry.try).toBe(2);
80
- });
136
+ // We should only have 1 call so far.
137
+ jest.advanceTimersByTime(1);
138
+ await Promise.resolve();
139
+ expect(delayedFn).toHaveBeenCalledTimes(1);
81
140
 
82
- it('should implement exponential backoff on subsequent calls', async() => {
83
- const delayedFn = jest.fn();
84
- const id = 'exp-backoff';
141
+ // A third call while the first is still pending.
142
+ // This call should be ignored and the delayedFn should not be executed.
143
+ await backOff.execute({
144
+ id, description: 'Skip Test', delayedFn
145
+ });
85
146
 
86
- // First call (immediate)
87
- await backOff.execute({
88
- id, description: 'Exponential Backoff Test', delayedFn
89
- });
147
+ expect(delayedFn).toHaveBeenCalledTimes(1);
90
148
 
91
- jest.advanceTimersByTime(1);
92
- expect(delayedFn).toHaveBeenCalledTimes(1);
149
+ jest.advanceTimersByTime(300);
150
+ await Promise.resolve();
151
+ expect(delayedFn).toHaveBeenCalledTimes(2);
93
152
 
94
- // Second call (should have a delay of 1^2 * 250 = 250ms)
95
- await backOff.execute({
96
- id, description: 'Exponential Backoff Test', delayedFn
153
+ // Advance timers to complete the pending second call.
154
+ jest.advanceTimersByTime(1000);
155
+ await Promise.resolve();
156
+ // Now there should be 2 calls, not 3.
157
+ expect(delayedFn).toHaveBeenCalledTimes(2);
97
158
  });
98
- jest.advanceTimersByTime(250);
99
- await Promise.resolve();
100
- expect(delayedFn).toHaveBeenCalledTimes(2);
101
159
 
102
- // Third call (should have a delay of 2^2 * 250 = 1000ms)
103
- await backOff.execute({
104
- id, description: 'Exponential Backoff Test', delayedFn
160
+ it('should not execute if the number of retries is exceeded', async() => {
161
+ const delayedFn = jest.fn();
162
+ const id = 'retries-test';
163
+
164
+ // Set retries to 2.
165
+ const retries = 2;
166
+
167
+ // Call 1 (immediate)
168
+ await backOff.execute({
169
+ id, description: 'Retries Test', retries, delayedFn
170
+ });
171
+ jest.advanceTimersByTime(1);
172
+ await Promise.resolve();
173
+ expect(delayedFn).toHaveBeenCalledTimes(1);
174
+
175
+ // Call 2 (after 250ms delay)
176
+ await backOff.execute({
177
+ id, description: 'Retries Test', retries, delayedFn
178
+ });
179
+ jest.advanceTimersByTime(250);
180
+ await Promise.resolve();
181
+ expect(delayedFn).toHaveBeenCalledTimes(2);
182
+
183
+ // Call 3 (should be ignored because it exceeds the `retries` limit of 2)
184
+ await backOff.execute({
185
+ id, description: 'Retries Test', retries, delayedFn
186
+ });
187
+ jest.advanceTimersByTime(250);
188
+ await Promise.resolve();
189
+
190
+ expect(delayedFn).toHaveBeenCalledTimes(2);
105
191
  });
106
- jest.advanceTimersByTime(1000);
107
- await Promise.resolve();
108
- expect(delayedFn).toHaveBeenCalledTimes(3);
109
192
 
110
- // Fourth call (should have a delay of 3^2 * 250 = 2250ms)
111
- await backOff.execute({
112
- id, description: 'Exponential Backoff Test', delayedFn
113
- });
114
- jest.advanceTimersByTime(2250);
115
- await Promise.resolve();
116
- expect(delayedFn).toHaveBeenCalledTimes(4);
117
- });
193
+ it('should skip execution if `canFn` returns false', async() => {
194
+ const delayedFn = jest.fn();
195
+ const canFn = jest.fn().mockResolvedValue(false);
118
196
 
119
- it('should skip execution if a previous backoff process is already running', async() => {
120
- const delayedFn = jest.fn();
121
- const id = 'skip-test';
197
+ await backOff.execute({
198
+ id: 'canfn-test', description: 'canFn Test', canFn, delayedFn
199
+ });
122
200
 
123
- await backOff.execute({
124
- id, description: 'Skip Test', delayedFn
125
- });
126
- expect(delayedFn).toHaveBeenCalledTimes(0);
201
+ jest.advanceTimersByTime(250);
202
+ await Promise.resolve();
127
203
 
128
- // Second call, will be ignored
129
- await backOff.execute({
130
- id, description: 'Skip Test', delayedFn
204
+ expect(delayedFn).not.toHaveBeenCalled();
131
205
  });
132
206
 
133
- expect(delayedFn).toHaveBeenCalledTimes(0);
207
+ it('should not clear backoff entry if the delayedFn throws an error', async() => {
208
+ const id = 'error-test';
209
+ const delayedFn = jest.fn().mockRejectedValue(new Error('Test Error'));
134
210
 
135
- // We should only have 1 call so far.
136
- jest.advanceTimersByTime(1);
137
- await Promise.resolve();
138
- expect(delayedFn).toHaveBeenCalledTimes(1);
211
+ // Call the function for the first time.
212
+ await backOff.execute({
213
+ id, description: 'Error Test', delayedFn
214
+ });
139
215
 
140
- // A third call while the first is still pending.
141
- // This call should be ignored and the delayedFn should not be executed.
142
- await backOff.execute({
143
- id, description: 'Skip Test', delayedFn
144
- });
216
+ expect(backOff.getBackOff(id).execute?.timeoutId).toBeDefined();
145
217
 
146
- expect(delayedFn).toHaveBeenCalledTimes(1);
218
+ // Wait for the immediate call to finish.
219
+ jest.advanceTimersByTime(1);
220
+ await Promise.resolve();
221
+ await Promise.resolve();
222
+ await Promise.resolve();
147
223
 
148
- jest.advanceTimersByTime(300);
149
- await Promise.resolve();
150
- expect(delayedFn).toHaveBeenCalledTimes(2);
224
+ // The entry should be removed after success/failure.
225
+ expect(backOff.getBackOff(id).execute?.timeoutId).toBeUndefined();
151
226
 
152
- // Advance timers to complete the pending second call.
153
- jest.advanceTimersByTime(1000);
154
- await Promise.resolve();
155
- // Now there should be 2 calls, not 3.
156
- expect(delayedFn).toHaveBeenCalledTimes(2);
157
- });
227
+ // Call again to trigger a backoff delay and an error.
228
+ await backOff.execute({
229
+ id, description: 'Error Test', delayedFn
230
+ });
158
231
 
159
- it('should not execute if the number of retries is exceeded', async() => {
160
- const delayedFn = jest.fn();
161
- const id = 'retries-test';
232
+ expect(backOff.getBackOff(id).execute?.timeoutId).toBeDefined();
162
233
 
163
- // Set retries to 2.
164
- const retries = 2;
234
+ // Advance timers to trigger the delayed function.
235
+ jest.advanceTimersByTime(250);
236
+ await Promise.resolve();
237
+ await Promise.resolve();
238
+ await Promise.resolve();
165
239
 
166
- // Call 1 (immediate)
167
- await backOff.execute({
168
- id, description: 'Retries Test', retries, delayedFn
169
- });
170
- jest.advanceTimersByTime(1);
171
- await Promise.resolve();
172
- expect(delayedFn).toHaveBeenCalledTimes(1);
240
+ // The timeoutId should be cleared, but the `try` count should be preserved on the next call.
241
+ expect(backOff.getBackOff(id).execute?.timeoutId).toBeUndefined();
173
242
 
174
- // Call 2 (after 250ms delay)
175
- await backOff.execute({
176
- id, description: 'Retries Test', retries, delayedFn
177
- });
178
- jest.advanceTimersByTime(250);
179
- await Promise.resolve();
180
- expect(delayedFn).toHaveBeenCalledTimes(2);
243
+ // Check if the next call will still back off.
244
+ const newDelayedFn = jest.fn();
181
245
 
182
- // Call 3 (should be ignored because it exceeds the `retries` limit of 2)
183
- await backOff.execute({
184
- id, description: 'Retries Test', retries, delayedFn
246
+ await backOff.execute({
247
+ id, description: 'Error Test', delayedFn: newDelayedFn
248
+ });
249
+ expect(newDelayedFn).not.toHaveBeenCalled(); // The next call should still be delayed
185
250
  });
186
- jest.advanceTimersByTime(250);
187
- await Promise.resolve();
188
251
 
189
- expect(delayedFn).toHaveBeenCalledTimes(2);
190
- });
252
+ it('should save metadata', async() => {
253
+ const delayedFn = jest.fn();
254
+ const id = 'exp-backoff';
255
+ const metadata = { a: true };
191
256
 
192
- it('should skip execution if `canFn` returns false', async() => {
193
- const delayedFn = jest.fn();
194
- const canFn = jest.fn().mockResolvedValue(false);
257
+ // First call (immediate)
258
+ await backOff.execute({
259
+ id, description: 'Exponential Backoff Test', delayedFn, metadata
260
+ });
195
261
 
196
- await backOff.execute({
197
- id: 'canfn-test', description: 'canFn Test', canFn, delayedFn
262
+ expect(backOff.getBackOff(id)).toBeDefined();
263
+ expect(backOff.getBackOff(id).metadata).toStrictEqual(metadata);
198
264
  });
265
+ });
199
266
 
200
- jest.advanceTimersByTime(250);
201
- await Promise.resolve();
267
+ describe('reset', () => {
268
+ beforeEach(beforeEachFn);
202
269
 
203
- expect(delayedFn).not.toHaveBeenCalled();
204
- });
270
+ afterEach(afterEachFn);
205
271
 
206
- it('should not clear backoff entry if the delayedFn throws an error', async() => {
207
- const id = 'error-test';
208
- const delayedFn = jest.fn().mockRejectedValue(new Error('Test Error'));
272
+ it('should reset a specific backoff process', async() => {
273
+ const delayedFn = jest.fn();
274
+ const id = 'reset-test';
209
275
 
210
- // Call the function for the first time.
211
- await backOff.execute({
212
- id, description: 'Error Test', delayedFn
213
- });
276
+ // Start a backoff process.
277
+ await backOff.execute({
278
+ id, description: 'Reset Test', delayedFn
279
+ });
280
+
281
+ expect(backOff.getBackOff(id)).toBeDefined();
214
282
 
215
- // Wait for the immediate call to finish.
216
- jest.advanceTimersByTime(1);
217
- await Promise.resolve();
218
- await Promise.resolve();
219
- await Promise.resolve();
283
+ // Reset the process.
284
+ backOff.reset(id);
220
285
 
221
- // The entry should be removed after success/failure.
222
- expect(backOff.getBackOff(id).timeoutId).toBeUndefined();
286
+ // The entry should be deleted from the map.
287
+ expect(backOff.getBackOff(id)).toBeUndefined();
223
288
 
224
- // Call again to trigger a backoff delay and an error.
225
- await backOff.execute({
226
- id, description: 'Error Test', delayedFn
289
+ // Now, a new execution should not be delayed.
290
+ await backOff.execute({
291
+ id, description: 'Reset Test', delayedFn
292
+ });
293
+
294
+ jest.advanceTimersByTime(250);
295
+ await Promise.resolve();
296
+ expect(delayedFn).toHaveBeenCalledTimes(1);
227
297
  });
228
298
 
229
- // Advance timers to trigger the delayed function.
230
- jest.advanceTimersByTime(250);
231
- await Promise.resolve();
232
- await Promise.resolve();
233
- await Promise.resolve();
299
+ it('should reset all backoff processes', async() => {
300
+ const delayedFn1 = jest.fn();
301
+ const delayedFn2 = jest.fn();
234
302
 
235
- // The timeoutId should be cleared, but the `try` count should be preserved on the next call.
236
- expect(backOff.getBackOff(id).timeoutId).toBeUndefined();
303
+ await backOff.execute({
304
+ id: 'all-1', description: 'All 1', delayedFn: delayedFn1
305
+ });
306
+ await backOff.execute({
307
+ id: 'all-1', description: 'All 1', delayedFn: delayedFn1
308
+ });
237
309
 
238
- // Check if the next call will still back off.
239
- const newDelayedFn = jest.fn();
310
+ await backOff.execute({
311
+ id: 'all-2', description: 'All 2', delayedFn: delayedFn2
312
+ });
313
+ await backOff.execute({
314
+ id: 'all-2', description: 'All 2', delayedFn: delayedFn2
315
+ });
240
316
 
241
- await backOff.execute({
242
- id, description: 'Error Test', delayedFn: newDelayedFn
243
- });
244
- expect(newDelayedFn).not.toHaveBeenCalled(); // The next call should still be delayed
245
- });
317
+ expect(backOff.getBackOff('all-1')).toBeDefined();
318
+ expect(backOff.getBackOff('all-2')).toBeDefined();
246
319
 
247
- it('should save metadata', async() => {
248
- const delayedFn = jest.fn();
249
- const id = 'exp-backoff';
250
- const metadata = { a: true };
320
+ // Reset all processes.
321
+ backOff.resetAll();
251
322
 
252
- // First call (immediate)
253
- await backOff.execute({
254
- id, description: 'Exponential Backoff Test', delayedFn, metadata
323
+ expect(backOff.getBackOff('all-1')).toBeUndefined();
324
+ expect(backOff.getBackOff('all-2')).toBeUndefined();
255
325
  });
256
326
 
257
- expect(backOff.getBackOff(id)).toBeDefined();
258
- expect(backOff.getBackOff(id).metadata).toStrictEqual(metadata);
327
+ it('should reset only processes with a specific prefix', async() => {
328
+ const delayedFn = jest.fn();
329
+
330
+ await backOff.execute({
331
+ id: 'prefix-test-1', description: 'Prefix Test 1', delayedFn
332
+ });
333
+ await backOff.execute({
334
+ id: 'prefix-test-1', description: 'Prefix Test 1', delayedFn
335
+ });
336
+
337
+ await backOff.execute({
338
+ id: 'prefix-test-2', description: 'Prefix Test 2', delayedFn
339
+ });
340
+ await backOff.execute({
341
+ id: 'prefix-test-2', description: 'Prefix Test 2', delayedFn
342
+ });
343
+
344
+ await backOff.execute({
345
+ id: 'other-test-1', description: 'Other Test 1', delayedFn
346
+ });
347
+ await backOff.execute({
348
+ id: 'other-test-1', description: 'Other Test 1', delayedFn
349
+ });
350
+
351
+ expect(backOff.getBackOff('prefix-test-1')).toBeDefined();
352
+ expect(backOff.getBackOff('prefix-test-2')).toBeDefined();
353
+ expect(backOff.getBackOff('other-test-1')).toBeDefined();
354
+
355
+ // Reset only the "prefix-test" processes.
356
+ backOff.resetPrefix('prefix-test');
357
+
358
+ expect(backOff.getBackOff('prefix-test-1')).toBeUndefined();
359
+ expect(backOff.getBackOff('prefix-test-2')).toBeUndefined();
360
+ // The other process should still exist.
361
+ expect(backOff.getBackOff('other-test-1')).toBeDefined();
362
+ });
259
363
  });
260
364
 
261
- // --- Test Suite for Reset methods ---
365
+ describe('recurse', () => {
366
+ beforeEach(beforeEachFn);
262
367
 
263
- it('should reset a specific backoff process', async() => {
264
- const delayedFn = jest.fn();
265
- const id = 'reset-test';
368
+ afterEach(afterEachFn);
266
369
 
267
- // Start a backoff process.
268
- await backOff.execute({
269
- id, description: 'Reset Test', delayedFn
270
- });
370
+ const handlePromises = async() => {
371
+ for (let i = 0; i < 10; i++) {
372
+ await Promise.resolve();
373
+ }
374
+ };
271
375
 
272
- expect(backOff.getBackOff(id)).toBeDefined();
376
+ const iterationLoop = async(wait: number) => {
377
+ await handlePromises();
273
378
 
274
- // Reset the process.
275
- backOff.reset(id);
379
+ jest.advanceTimersByTime(wait);
276
380
 
277
- // The entry should be deleted from the map.
278
- expect(backOff.getBackOff(id)).toBeUndefined();
381
+ await handlePromises();
382
+ };
279
383
 
280
- // Now, a new execution should not be delayed.
281
- await backOff.execute({
282
- id, description: 'Reset Test', delayedFn
283
- });
384
+ it('should execute the function immediately', async() => {
385
+ const delayedFn = jest.fn().mockResolvedValue('success');
386
+ const id = 'recurse-test';
284
387
 
285
- jest.advanceTimersByTime(250);
286
- await Promise.resolve();
287
- expect(delayedFn).toHaveBeenCalledTimes(1);
288
- });
388
+ const promise = backOff.recurse({
389
+ id, description: 'Recurse Test', delayedFn, continueOnError: async() => false
390
+ });
289
391
 
290
- it('should reset all backoff processes', async() => {
291
- const delayedFn1 = jest.fn();
292
- const delayedFn2 = jest.fn();
392
+ await iterationLoop(1);
293
393
 
294
- await backOff.execute({
295
- id: 'all-1', description: 'All 1', delayedFn: delayedFn1
296
- });
297
- await backOff.execute({
298
- id: 'all-1', description: 'All 1', delayedFn: delayedFn1
299
- });
394
+ const result = await promise;
300
395
 
301
- await backOff.execute({
302
- id: 'all-2', description: 'All 2', delayedFn: delayedFn2
303
- });
304
- await backOff.execute({
305
- id: 'all-2', description: 'All 2', delayedFn: delayedFn2
396
+ expect(delayedFn).toHaveBeenCalledTimes(1);
397
+ expect(result).toBe('success');
306
398
  });
307
399
 
308
- expect(backOff.getBackOff('all-1')).toBeDefined();
309
- expect(backOff.getBackOff('all-2')).toBeDefined();
400
+ it('should retry on failure if continueOnError returns true', async() => {
401
+ const delayedFn = jest.fn()
402
+ .mockRejectedValueOnce(new Error('Fail 1'))
403
+ .mockResolvedValue('success');
404
+ const id = 'recurse-retry';
405
+ const continueOnError = jest.fn().mockResolvedValue(true);
310
406
 
311
- // Reset all processes.
312
- backOff.resetAll();
407
+ const promise = backOff.recurse({
408
+ id, description: 'Recurse Retry', delayedFn, continueOnError
409
+ });
313
410
 
314
- expect(backOff.getBackOff('all-1')).toBeUndefined();
315
- expect(backOff.getBackOff('all-2')).toBeUndefined();
316
- });
411
+ // First attempt (i=0)
412
+ await iterationLoop(1);
317
413
 
318
- it('should reset only processes with a specific prefix', async() => {
319
- const delayedFn = jest.fn();
414
+ // Second attempt (i=1), delay 250ms
415
+ await iterationLoop(250);
320
416
 
321
- await backOff.execute({
322
- id: 'prefix-test-1', description: 'Prefix Test 1', delayedFn
323
- });
324
- await backOff.execute({
325
- id: 'prefix-test-1', description: 'Prefix Test 1', delayedFn
326
- });
417
+ const result = await promise;
327
418
 
328
- await backOff.execute({
329
- id: 'prefix-test-2', description: 'Prefix Test 2', delayedFn
330
- });
331
- await backOff.execute({
332
- id: 'prefix-test-2', description: 'Prefix Test 2', delayedFn
419
+ expect(delayedFn).toHaveBeenCalledTimes(2);
420
+ expect(result).toBe('success');
333
421
  });
334
422
 
335
- await backOff.execute({
336
- id: 'other-test-1', description: 'Other Test 1', delayedFn
423
+ it('should stop retrying if continueOnError returns false', async() => {
424
+ const delayedFn = jest.fn().mockRejectedValue(new Error('Fail'));
425
+ const id = 'recurse-stop';
426
+ const continueOnError = jest.fn().mockResolvedValue(false);
427
+
428
+ const promise = backOff.recurse({
429
+ id, description: 'Recurse Stop', delayedFn, continueOnError
430
+ });
431
+
432
+ await iterationLoop(1);
433
+
434
+ await expect(promise).rejects.toThrow('Failed call');
435
+ expect(delayedFn).toHaveBeenCalledTimes(1);
337
436
  });
338
- await backOff.execute({
339
- id: 'other-test-1', description: 'Other Test 1', delayedFn
437
+
438
+ it('should stop retrying if max retries reached', async() => {
439
+ const delayedFn = jest.fn().mockRejectedValue(new Error('Fail'));
440
+ const id = 'recurse-max';
441
+ const continueOnError = jest.fn().mockResolvedValue(true);
442
+ const retries = 2;
443
+
444
+ const promise = backOff.recurse({
445
+ id, description: 'Recurse Max', delayedFn, continueOnError, retries
446
+ });
447
+
448
+ // Try 1
449
+ await iterationLoop(1);
450
+
451
+ // Try 2
452
+ await iterationLoop(250);
453
+
454
+ const result = await promise;
455
+
456
+ expect(result).toBeUndefined();
457
+ expect(delayedFn).toHaveBeenCalledTimes(2);
340
458
  });
341
459
 
342
- expect(backOff.getBackOff('prefix-test-1')).toBeDefined();
343
- expect(backOff.getBackOff('prefix-test-2')).toBeDefined();
344
- expect(backOff.getBackOff('other-test-1')).toBeDefined();
460
+ it('should skip if canFn returns false', async() => {
461
+ const delayedFn = jest.fn();
462
+ const id = 'recurse-canfn';
463
+ const canFn = jest.fn().mockResolvedValue(false);
345
464
 
346
- // Reset only the "prefix-test" processes.
347
- backOff.resetPrefix('prefix-test');
465
+ const promise = backOff.recurse({
466
+ id, description: 'Recurse CanFn', delayedFn, continueOnError: async() => false, canFn
467
+ });
348
468
 
349
- expect(backOff.getBackOff('prefix-test-1')).toBeUndefined();
350
- expect(backOff.getBackOff('prefix-test-2')).toBeUndefined();
351
- // The other process should still exist.
352
- expect(backOff.getBackOff('other-test-1')).toBeDefined();
469
+ await expect(promise).rejects.toThrow('Skipping (canFn test failed)');
470
+ expect(delayedFn).not.toHaveBeenCalled();
471
+ });
353
472
  });
354
473
  });