@applicaster/zapp-react-native-utils 13.0.0-alpha.7120331332 → 13.0.0-alpha.7192404241

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 (40) hide show
  1. package/actionsExecutor/ActionExecutor.ts +16 -9
  2. package/actionsExecutor/ActionExecutorContext.tsx +59 -8
  3. package/alertUtils/alert.ts +148 -0
  4. package/alertUtils/index.ts +1 -148
  5. package/analyticsUtils/AnalyticPlayerListener.ts +18 -1
  6. package/appUtils/HooksManager/types.ts +3 -0
  7. package/appUtils/accessibilityManager/index.ts +49 -1
  8. package/appUtils/contextKeysManager/contextResolver.ts +73 -0
  9. package/appUtils/focusManager/index.ts +92 -64
  10. package/appUtils/focusManager/treeDataStructure/Tree/index.js +23 -0
  11. package/appUtils/focusManager/utils/enums.ts +4 -0
  12. package/appUtils/focusManagerAux/utils/index.ts +4 -3
  13. package/appUtils/keyCodes/keys/keysByPlatform.ts +4 -0
  14. package/appUtils/keyInputHandler/KeyInputHandler.ts +68 -0
  15. package/appUtils/platform/const.ts +14 -0
  16. package/appUtils/platform/platformUtils.ts +17 -1
  17. package/appUtils/playerManager/player.ts +17 -5
  18. package/appUtils/playerManager/playerNative.ts +2 -1
  19. package/appUtils/playerManager/usePlayerControllerSetup.tsx +3 -3
  20. package/idleUtils/__tests__/withTimeout$.test.ts +37 -0
  21. package/idleUtils/index.ts +1 -1
  22. package/idleUtils/withTimeout.ts +15 -2
  23. package/logger/const.ts +9 -67
  24. package/package.json +3 -2
  25. package/reactHooks/cell-click/__tests__/index.test.js +5 -0
  26. package/reactHooks/cell-click/index.ts +7 -1
  27. package/reactHooks/feed/useFeedLoader.tsx +1 -1
  28. package/reactHooks/player/TVSeekControlller/ISeekController.ts +32 -0
  29. package/reactHooks/player/TVSeekControlller/LongPressSeekController.ts +71 -0
  30. package/reactHooks/player/TVSeekControlller/TVSeekController.ts +258 -0
  31. package/reactHooks/player/TVSeekControlller/TapSeekController.ts +53 -0
  32. package/reactHooks/player/const.ts +0 -4
  33. package/reactHooks/player/index.ts +0 -6
  34. package/time/Timer.ts +1 -1
  35. package/types.ts +1 -0
  36. package/reactHooks/player/useAutoSeek.tsx +0 -87
  37. package/reactHooks/player/useSeek.tsx +0 -66
  38. package/reactHooks/player/useTapSeek.tsx +0 -58
  39. /package/reactHooks/player/__tests__/{useAutoSeek.test.tsx → useAutoSeek._test.tsx} +0 -0
  40. /package/reactHooks/player/__tests__/{useTapSeek.test.ts → useTapSeek._test.ts} +0 -0
@@ -11,8 +11,9 @@ import {
11
11
  haveSameParentBeforeRoot,
12
12
  } from "./treeDataStructure/Utils";
13
13
  import { subscriber } from "../../functionUtils";
14
- import * as FOCUS_EVENTS from "./events";
15
14
  import { coreLogger } from "../../logger";
15
+ import { ACTION } from "./utils/enums";
16
+
16
17
  const logger = coreLogger.addSubsystem("focusManager");
17
18
 
18
19
  const isFocusEnabled = (focusableItem): boolean => {
@@ -167,13 +168,82 @@ export const focusManager = (function () {
167
168
  }
168
169
 
169
170
  /**
170
- * returns the current focusable's group
171
- * @private
172
- * @returns {React.Component} the current focus
171
+ * Computes the difference in parent groups between two nodes.
172
+ * It returns the IDs of parents that are present in the current node but not in the previous node,
173
+ * as well as the IDs of parents that are present in the previous node but not in the current node.
174
+ *
175
+ * @param {string} currentNodeID - The ID of the current node.
176
+ * @param {string} previousNodeID - The ID of the previous node.
177
+ * @returns {Object} An object containing the differences in parent IDs:
178
+ * - {Array<string>} currentNodeParentsIDs: Parent IDs present in the current node but not in the previous node.
179
+ * - {Array<string>} previousNodeParentsIDs: Parent IDs present in the previous node but not in the current node.
173
180
  */
174
- function getCurrentGroup() {
175
- return getGroupById(R.path(["props", "groupId"], getCurrentFocus()));
176
- }
181
+ const getParentDiffs = (currentNodeID, previousNodeID) => {
182
+ const currentNodeParentsIDs =
183
+ focusableTree.getParentGroupIds(currentNodeID);
184
+
185
+ const previousNodeParentsIDs =
186
+ focusableTree.getParentGroupIds(previousNodeID);
187
+
188
+ return {
189
+ currentNodeParentsIDs: R.difference(
190
+ currentNodeParentsIDs,
191
+ previousNodeParentsIDs
192
+ ),
193
+ previousNodeParentsIDs: R.difference(
194
+ previousNodeParentsIDs,
195
+ currentNodeParentsIDs
196
+ ),
197
+ };
198
+ };
199
+
200
+ /**
201
+ * Updates the focus state of nodes identified by their IDs.
202
+ * For each ID, if `setFocus` is true, the node will receive focus;
203
+ * otherwise, it will lose focus.
204
+ *
205
+ * @param {Array<string>} ids - An array of node IDs to update.
206
+ * @param {boolean} setFocus - A flag indicating whether to set focus (true) or blur (false) on the nodes.
207
+ */
208
+ const updateNodeFocus = (ids, action) => {
209
+ if (!ids || ids.length === 0) {
210
+ return; // Nothing to do
211
+ }
212
+
213
+ const startNode = focusableTree.findInTree(ids[0]);
214
+
215
+ if (!startNode) {
216
+ return; // Start node not found
217
+ }
218
+
219
+ const endNodeId = ids[ids.length - 1]; // Get the ID of the end node
220
+
221
+ let currentNode = startNode;
222
+
223
+ // Function to apply the action (focus or blur)
224
+ const applyAction = (node) => {
225
+ if (node && node.component) {
226
+ if (action === "focus") {
227
+ node.component.setFocus();
228
+ } else if (action === "blur") {
229
+ node.component.setBlur();
230
+ }
231
+ }
232
+ };
233
+
234
+ // Apply action to the starting node
235
+ applyAction(currentNode);
236
+
237
+ // Traverse upwards
238
+ while (currentNode && currentNode.id !== endNodeId) {
239
+ currentNode = currentNode.parent;
240
+
241
+ if (currentNode) {
242
+ // check if current node exists, as parent might be root
243
+ applyAction(currentNode);
244
+ }
245
+ }
246
+ };
177
247
 
178
248
  /**
179
249
  * Sets the current focus to the provided id. If there is an existing focus, it will
@@ -181,34 +251,29 @@ export const focusManager = (function () {
181
251
  * @private
182
252
  * @param {String} id of the focusable to set as currentFocus
183
253
  * @param {Object} direction of the navigation, which led to this focus change
184
- * @param {Object} options
185
- * @param {Boolean} options.groupFocusedChanged tells wether the focus has moved
186
254
  * to another group or not. defaults to false
187
255
  */
188
- function setFocus(
189
- id: string,
190
- direction?: FocusManager.Web.Direction,
191
- options?: Partial<{ groupFocusedChanged: boolean }>
192
- ) {
256
+ function setFocus(id: string, direction?: FocusManager.Web.Direction) {
193
257
  if (focusDisabled) return false;
194
258
 
195
- const { groupFocusedChanged = false } = options || {};
196
-
197
259
  // due to optimisiation it's recommanded to set currentFocusNode before setFocus
198
260
  // is called, so there will be no delays on tree search.
199
261
  // If currentFocusNode is null or undefined we still want it to findInTree
200
262
  if (currentFocusNode?.id !== id) {
263
+ const { currentNodeParentsIDs, previousNodeParentsIDs } = getParentDiffs(
264
+ id,
265
+ currentFocusNode?.id
266
+ );
267
+
268
+ // Set focus on current node parents and blur on previous node parents
269
+ updateNodeFocus(currentNodeParentsIDs, ACTION.FOCUS);
270
+ updateNodeFocus(previousNodeParentsIDs, ACTION.BLUR);
271
+
201
272
  currentFocusNode = focusableTree.findInTree(id);
202
273
  }
203
274
 
204
275
  setLastFocusOnParentNode(currentFocusNode);
205
276
 
206
- const currentGroup = getCurrentGroup();
207
-
208
- if (groupFocusedChanged && currentGroup) {
209
- currentGroup.hasLostFocus(currentGroup, direction);
210
- }
211
-
212
277
  focus(direction);
213
278
  }
214
279
 
@@ -251,6 +316,7 @@ export const focusManager = (function () {
251
316
  !focusableItem.isGroup &&
252
317
  isFocusEnabled(focusableItem)
253
318
  ) {
319
+ // TODO: Move this to setFocus method
254
320
  blur();
255
321
 
256
322
  logger.log({
@@ -261,7 +327,7 @@ export const focusManager = (function () {
261
327
  });
262
328
 
263
329
  currentFocusNode = focusCandidate;
264
- focusableItem.willReceiveFocus(focusableItem, null);
330
+
265
331
  focusableItem && setFocus(focusCandidate.id, null);
266
332
 
267
333
  return { success: true };
@@ -412,7 +478,7 @@ export const focusManager = (function () {
412
478
  function isCurrentFocusOnTheTopScreen() {
413
479
  const routes = R.pathOr([], ["root", "children"], focusableTree);
414
480
 
415
- if (routes.length <= 1) {
481
+ if (currentFocusNode && routes.length <= 1) {
416
482
  // there is only one or zero routes
417
483
  return true;
418
484
  }
@@ -448,20 +514,10 @@ export const focusManager = (function () {
448
514
  const nextRect = nextFocusable.component.getRect();
449
515
 
450
516
  if (nextRect && nextRect.width > 0) {
451
- nextFocusable.component.willReceiveFocus(
452
- nextFocusable.component,
453
- direction
454
- );
455
-
517
+ // TODO: Move this to setFocus method
456
518
  blur(direction);
457
519
 
458
- const groupChanged =
459
- nextFocusable.component.props.groupId !==
460
- currentFocusNode?.component?.props?.groupId;
461
-
462
- setFocus(nextFocusable.id, direction, {
463
- groupFocusedChanged: groupChanged,
464
- });
520
+ setFocus(nextFocusable.id, direction);
465
521
  }
466
522
  } else {
467
523
  currentFocusNode.component.failedLostFocus(currentFocusNode, direction);
@@ -481,34 +537,6 @@ export const focusManager = (function () {
481
537
 
482
538
  const { on, invokeHandler, removeHandler } = eventHandler;
483
539
 
484
- on(FOCUS_EVENTS.FOCUS, (focusable, direction) => {
485
- if (focusable.props.groupId) {
486
- const parent = focusableTree.findInTree(focusable.props.groupId);
487
- parent?.component?.onFocus(focusable.component, direction);
488
- }
489
- });
490
-
491
- on(FOCUS_EVENTS.BLUR, (focusable, direction) => {
492
- if (focusable.props.groupId) {
493
- const parent = focusableTree.findInTree(focusable.props.groupId);
494
- parent?.component?.onBlur(focusable.component, direction);
495
- }
496
- });
497
-
498
- on(FOCUS_EVENTS.HAS_LOST_FOCUS, (focusable, direction) => {
499
- if (focusable.props.groupId) {
500
- const parent = focusableTree.findInTree(focusable.props.groupId);
501
- parent?.component?.hasLostFocus(parent.component, direction);
502
- }
503
- });
504
-
505
- on(FOCUS_EVENTS.WILL_RECEIVE_FOCUS, (focusable, direction) => {
506
- if (focusable.props.groupId) {
507
- const parent = focusableTree.findInTree(focusable.props.groupId);
508
- parent?.component?.willReceiveFocus(focusable.component, direction);
509
- }
510
- });
511
-
512
540
  /**
513
541
  * this is the list of the functions available externally
514
542
  * when importing the focus manager
@@ -231,4 +231,27 @@ export class Tree {
231
231
 
232
232
  return retVal;
233
233
  }
234
+
235
+ /**
236
+ * Gets the IDs of all parent groups for a given node id.
237
+ * @public
238
+ * @param {String} nodeId - The id of the node to get parent IDs for.
239
+ * @returns {Array} An array of parent group IDs or an empty array if the node is not found.
240
+ */
241
+ getParentGroupIds(nodeId) {
242
+ const parentIds = [];
243
+ let currentNode = this.findInTree(nodeId);
244
+
245
+ if (!currentNode) {
246
+ return []; // Node not found, return an empty array
247
+ }
248
+
249
+ // Traverse up the tree, collecting parent IDs
250
+ while (currentNode.parent && currentNode.parent.id !== "root") {
251
+ parentIds.push(currentNode.parent.id);
252
+ currentNode = currentNode.parent;
253
+ }
254
+
255
+ return parentIds;
256
+ }
234
257
  }
@@ -0,0 +1,4 @@
1
+ export enum ACTION {
2
+ FOCUS = "focus",
3
+ BLUR = "blur",
4
+ }
@@ -1,8 +1,8 @@
1
1
  import { isNotNil } from "@applicaster/zapp-react-native-utils/reactUtils/helpers";
2
- import { find, last, startsWith, pathOr } from "ramda";
2
+ import { find, last, pathOr, startsWith } from "ramda";
3
3
  import {
4
- QUICK_BRICK_NAVBAR,
5
4
  QUICK_BRICK_CONTENT,
5
+ QUICK_BRICK_NAVBAR,
6
6
  } from "@applicaster/quick-brick-core/const";
7
7
 
8
8
  // run check each 300 ms
@@ -66,7 +66,8 @@ export const getContentNode = (focusableTree) => {
66
66
  const content = find(
67
67
  (child) =>
68
68
  startsWith(QUICK_BRICK_CONTENT, child.id) ||
69
- startsWith("player", child.id),
69
+ startsWith("player", child.id) ||
70
+ startsWith("/hooks", child?.parent?.id), // For startup hooks case
70
71
  getActiveScreenChildren(focusableTree)
71
72
  );
72
73
 
@@ -77,6 +77,10 @@ const web = {
77
77
  Exit: { code: "Exit", keyCode: null },
78
78
  Escape: { code: "Escape", keyCode: 27 },
79
79
  EscapeDevice: { code: "Escape", keyCode: 27 },
80
+ // For Debugging
81
+ Z: { code: "KeyZ", keyCode: 90 }, // Hardware Backward Button
82
+ X: { code: "KeyX", keyCode: 88 }, // Hardware Enter Button
83
+ C: { code: "KeyC", keyCode: 67 }, // Hardware Forward Button
80
84
  };
81
85
 
82
86
  const android_tv = {
@@ -0,0 +1,68 @@
1
+ export type SeekKeyHandlerListener = {
2
+ onRewindPress: () => boolean;
3
+ onForwardPress: () => boolean;
4
+ onRewindPressOut: () => void;
5
+ onForwardPressOut: () => void;
6
+ };
7
+
8
+ export class KeyInputHandler {
9
+ private static instance: KeyInputHandler;
10
+
11
+ public static getInstance() {
12
+ if (!KeyInputHandler.instance) {
13
+ KeyInputHandler.instance = new KeyInputHandler();
14
+ }
15
+
16
+ return KeyInputHandler.instance;
17
+ }
18
+
19
+ constructor() {
20
+ this.listeners = [];
21
+ }
22
+
23
+ private listeners: SeekKeyHandlerListener[] = [];
24
+
25
+ addListener = (listener: SeekKeyHandlerListener) => {
26
+ this.listeners.splice(0, 0, listener);
27
+
28
+ return () => this.removeListener(listener);
29
+ };
30
+
31
+ removeListener = (listener: SeekKeyHandlerListener) => {
32
+ this.listeners = this.listeners.filter((l) => l !== listener);
33
+ };
34
+
35
+ onRewindPress = () => {
36
+ for (const element of this.listeners) {
37
+ if (element.onRewindPress()) {
38
+ break;
39
+ }
40
+ }
41
+ };
42
+
43
+ onForwardPress = () => {
44
+ for (const element of this.listeners) {
45
+ if (element.onForwardPress()) {
46
+ break;
47
+ }
48
+ }
49
+ };
50
+
51
+ // TODO: Event should be send to listeners who handled down event
52
+ onRewindPressOut = () => {
53
+ this.listeners.forEach((listener) => listener.onRewindPressOut());
54
+ };
55
+
56
+ onForwardPressOut = () => {
57
+ this.listeners.forEach((listener) => listener.onForwardPressOut());
58
+ };
59
+
60
+ getSeekMethods = () => {
61
+ return {
62
+ onForwardPress: this.onForwardPress,
63
+ onRewindPress: this.onRewindPress,
64
+ onForwardPressOut: this.onForwardPressOut,
65
+ onRewindPressOut: this.onRewindPressOut,
66
+ };
67
+ };
68
+ }
@@ -23,3 +23,17 @@ export const PLATFORMS = {
23
23
  export const PLATFORM_KEYS = {
24
24
  deviceClosedCaptioningEnabled: "device_closed_captioning_enabled",
25
25
  };
26
+
27
+ export enum ZappPlatform {
28
+ Android = "android",
29
+ AndroidTV = "android_tv",
30
+ AmazonFireTV = "amazon",
31
+ iOS = "ios",
32
+ tvOS = "tvos",
33
+ Roku = "roku",
34
+ Tizen = "tizen",
35
+ Web = "web",
36
+ WebOS = "webos",
37
+ Vizio = "vizio",
38
+ Unknown = "unknown",
39
+ }
@@ -6,8 +6,10 @@ import {
6
6
  isSamsungPlatform,
7
7
  } from "@applicaster/zapp-react-dom-app/App/Loader/utils/platform";
8
8
 
9
- import { PLATFORM_KEYS, PLATFORMS } from "./const";
9
+ import { PLATFORM_KEYS, PLATFORMS, ZappPlatform } from "./const";
10
10
  import { createLogger, utilsLogger } from "../../logger";
11
+ import { getPlatform } from "../../reactUtils";
12
+ import { appStore } from "@applicaster/zapp-react-native-redux/AppStore";
11
13
 
12
14
  const { log_debug } = createLogger({
13
15
  category: "General",
@@ -15,6 +17,20 @@ const { log_debug } = createLogger({
15
17
  parent: utilsLogger,
16
18
  });
17
19
 
20
+ export const getZappPlatform = (): ZappPlatform => {
21
+ const platform = appStore.get("appData")?.platform;
22
+
23
+ switch (platform) {
24
+ case "amazon_fire_tv":
25
+ return ZappPlatform.AmazonFireTV;
26
+ case ZappPlatform.Vizio:
27
+ return ZappPlatform.Vizio;
28
+ default:
29
+ // TODO: Use only ZappPlatform enum
30
+ return getPlatform() as ZappPlatform;
31
+ }
32
+ };
33
+
18
34
  export const getUserAgent = () => window?.navigator?.userAgent;
19
35
 
20
36
  /** VIZIO UTILS */
@@ -5,6 +5,7 @@ import { createLogger, utilsLogger } from "../../logger";
5
5
  import { getBoolFromConfigValue } from "../../configurationUtils";
6
6
  import { PlayerRole, SharedPlayerCallBacksKeys } from "./conts";
7
7
  import { OverlaysObserver } from "./OverlayObserver/OverlaysObserver";
8
+ import { TVSeekController } from "../../reactHooks/player/TVSeekControlller/TVSeekController";
8
9
 
9
10
  const logger = createLogger({
10
11
  category: "PlayerController",
@@ -49,6 +50,8 @@ export class Player {
49
50
  [key: string]: QuickBrickPlayer.SharedPlayerCallBacks;
50
51
  } = {};
51
52
 
53
+ public seekController?: TVSeekController | null;
54
+
52
55
  get entry(): ZappEntry | null {
53
56
  return this._entry;
54
57
  }
@@ -59,6 +62,8 @@ export class Player {
59
62
  }
60
63
 
61
64
  constructor(props) {
65
+ this.seekController = null;
66
+
62
67
  this.playerState = {
63
68
  // We want isLive null by default to be able not to show controls if we do not know type
64
69
  isLive: null,
@@ -91,6 +96,8 @@ export class Player {
91
96
  this.onVideoProgress = this.onVideoProgress.bind(this);
92
97
  this.onVideoLoad = this.onVideoLoad.bind(this);
93
98
  this.isFullScreenSupported = this.isFullScreenSupported.bind(this);
99
+ this.onPlayerSeekComplete = this.onPlayerSeekComplete.bind(this);
100
+
94
101
  this.entryObservable = null;
95
102
  }
96
103
 
@@ -321,7 +328,7 @@ export class Player {
321
328
  * You should use dispatchPlayerEvent at PlayerContainer or call playerManager.invokeHandler directly
322
329
  * This allows you to use one method for everything.
323
330
  */
324
- onSeekStart = (event: QuickBrickPlayer.OnSeekStartEvent) => {
331
+ onPlayerSeekStart = (event: QuickBrickPlayer.OnSeekStartEvent) => {
325
332
  const { isLive, seekableDuration, currentTime, duration, seekTime } = event;
326
333
 
327
334
  this.setPlaybackState({ currentTime, duration, seekableDuration, isLive });
@@ -336,10 +343,11 @@ export class Player {
336
343
  });
337
344
  };
338
345
 
339
- onSeekComplete = (event: QuickBrickPlayer.OnSeekCompleteEvent) => {
346
+ onPlayerSeekComplete = (event: QuickBrickPlayer.OnSeekCompleteEvent) => {
340
347
  const { isLive, seekableDuration, currentTime, duration } = event;
341
348
 
342
349
  this.setPlaybackState({ currentTime, duration, seekableDuration, isLive });
350
+
343
351
  this.playerState.seekPosition = null;
344
352
 
345
353
  if (!isLive) {
@@ -620,14 +628,18 @@ export class Player {
620
628
  selectTrack = (_track) => {};
621
629
 
622
630
  protected notifyPlayHeadPositionUpdate = () => {
631
+ const currentTime = this.getPosition();
632
+
623
633
  const event = {
624
- currentTime: this.getPosition(),
634
+ currentTime,
625
635
  duration: this.getDuration(),
626
636
  item: this.getEntry(),
627
637
  seekableDuration: this.playerState.seekableDuration,
628
638
  isLive: this.isLive(),
629
639
  };
630
640
 
641
+ this.seekController?.onVideoProgress(currentTime);
642
+
631
643
  this.invokeListenersUpdate({
632
644
  callbackName: SharedPlayerCallBacksKeys.OnVideoProgress,
633
645
  event,
@@ -653,8 +665,8 @@ export class Player {
653
665
  };
654
666
 
655
667
  public getListener = (): QuickBrickPlayer.SharedPlayerCallBacks => ({
656
- onPlayerSeekStart: this.onSeekStart,
657
- onPlayerSeekComplete: this.onSeekComplete,
668
+ onPlayerSeekStart: this.onPlayerSeekStart,
669
+ onPlayerSeekComplete: this.onPlayerSeekComplete,
658
670
  onVideoEnd: this.onVideoEnd,
659
671
  onError: this.onError,
660
672
  onVideoError: this.onVideoError,
@@ -222,9 +222,10 @@ export class PlayerNative extends Player {
222
222
 
223
223
  onVideoLoad = (event) => {
224
224
  if (this.startPosition) {
225
- super.onVideoLoad({ ...event, ignoreContinueWatching: true });
226
225
  this.seekTo(this.startPosition);
227
226
  this.startPosition = null;
227
+
228
+ super.onVideoLoad({ ...event, ignoreContinueWatching: true });
228
229
  } else {
229
230
  super.onVideoLoad(event);
230
231
  }
@@ -39,7 +39,7 @@ export const usePlayerControllerSetup = ({
39
39
  ? { supports_native_controls: true, ...config }
40
40
  : config;
41
41
 
42
- const newPlayerController = new ControllerType({
42
+ const combinedProps = {
43
43
  player: ref,
44
44
  listener: props as any,
45
45
  config: newConfig,
@@ -47,9 +47,9 @@ export const usePlayerControllerSetup = ({
47
47
  entry,
48
48
  playerPluginId: pluginId,
49
49
  playerRole: PlayerRole.Primary,
50
- });
50
+ };
51
51
 
52
- setPlayerController(newPlayerController);
52
+ setPlayerController(new ControllerType(combinedProps));
53
53
  }, [playerId, entry?.id]);
54
54
 
55
55
  React.useEffect(() => {
@@ -0,0 +1,37 @@
1
+ import { TestScheduler } from "rxjs/testing";
2
+
3
+ import { withTimeout$ } from "..";
4
+
5
+ describe("withTimeout$", () => {
6
+ let testScheduler: TestScheduler;
7
+
8
+ beforeEach(() => {
9
+ testScheduler = new TestScheduler((actual, expected) => {
10
+ expect(actual).toEqual(expected);
11
+ });
12
+ });
13
+
14
+ it("should emit undefined after the specified timeout", () => {
15
+ testScheduler.run(({ expectObservable }) => {
16
+ const timeout = 1000;
17
+ const expectedMarble = "1s (a|)";
18
+ const expectedValues = { a: undefined };
19
+
20
+ const result$ = withTimeout$(timeout);
21
+
22
+ expectObservable(result$).toBe(expectedMarble, expectedValues);
23
+ });
24
+ });
25
+
26
+ it("should complete after emitting undefined", () => {
27
+ testScheduler.run(({ expectObservable }) => {
28
+ const timeout = 500;
29
+ const expectedMarble = "500ms (a|)";
30
+ const expectedValues = { a: undefined };
31
+
32
+ const result$ = withTimeout$(timeout);
33
+
34
+ expectObservable(result$).toBe(expectedMarble, expectedValues);
35
+ });
36
+ });
37
+ });
@@ -1,4 +1,4 @@
1
- export { withTimeout } from "./withTimeout";
1
+ export { withTimeout, withTimeout$ } from "./withTimeout";
2
2
 
3
3
  export { onIdle } from "./onIdle";
4
4
 
@@ -1,7 +1,20 @@
1
- export const withTimeout = (timeout: number) => {
2
- return new Promise((resolve) => {
1
+ import { of, Observable } from "rxjs";
2
+ import { delay } from "rxjs/operators";
3
+
4
+ export const withTimeout = (timeout: number): Promise<void> => {
5
+ return new Promise<void>((resolve) => {
3
6
  setTimeout(() => {
4
7
  resolve(undefined);
5
8
  }, timeout);
6
9
  });
7
10
  };
11
+
12
+ /**
13
+ * Creates an observable that emits a single value after a specified timeout.
14
+ *
15
+ * @param timeout - The delay duration in milliseconds before emitting the value.
16
+ * @returns An observable that emits `undefined` after the specified timeout.
17
+ */
18
+ export const withTimeout$ = (timeout: number): Observable<void> => {
19
+ return of(undefined).pipe(delay(timeout));
20
+ };