@applicaster/quick-brick-core 16.0.0-rc.126 → 16.0.0-rc.128

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.
@@ -4,8 +4,8 @@ import { allSettled } from "promise";
4
4
  import { useActions } from "@applicaster/zapp-react-native-utils/reactHooks/actions";
5
5
 
6
6
  import * as React from "react";
7
- import { coreAppLogger } from "../logger";
8
- const logger = coreAppLogger.addSubsystem("AppLifeCycleManager");
7
+ import { subsystemLogger } from "@applicaster/zapp-react-native-utils/logger";
8
+ const logger = subsystemLogger("app/lifecycle", "AppLifeCycleManager");
9
9
 
10
10
  const debugActions = (actions) =>
11
11
  logger.debug({
@@ -8,7 +8,7 @@ import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
8
8
  import { LegacyActionsRegistryAdapter } from "./LegacyActionsRegistryAdapter";
9
9
 
10
10
  const { log_debug } = createLogger({
11
- subsystem: "ActionsProvider",
11
+ subsystem: "core/actions",
12
12
  category: "ActionRegistration",
13
13
  });
14
14
 
@@ -5,7 +5,7 @@ import { uiActionsRegistry } from "@applicaster/zapp-react-native-utils/uiAction
5
5
  import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
6
6
 
7
7
  const { log_debug, log_warning } = createLogger({
8
- subsystem: "LegacyActionsRegistryAdapter",
8
+ subsystem: "core/actions/legacy_registry",
9
9
  category: "ActionRegistration",
10
10
  });
11
11
 
@@ -5,6 +5,6 @@ import { coreAppLogger } from "../logger";
5
5
  export const { log_verbose, log_debug, log_warning, log_info, log_error } =
6
6
  createLogger({
7
7
  category: "AssetCache",
8
- subsystem: "quick-brick-core",
8
+ subsystem: "storage/asset_cache",
9
9
  parent: coreAppLogger,
10
10
  });
@@ -3,6 +3,7 @@ import React, {
3
3
  createContext,
4
4
  useEffect,
5
5
  useCallback,
6
+ useMemo,
6
7
  ComponentType,
7
8
  ReactChild,
8
9
  } from "react";
@@ -196,9 +197,11 @@ function URLSchemeContextProvider(props: Props) {
196
197
  };
197
198
  }, []);
198
199
 
200
+ const contextValue = useMemo(() => ({ url: state, done }), [state, done]);
201
+
199
202
  return (
200
203
  <View style={fillStyles.container}>
201
- <URLSchemeContext.Provider value={{ url: state, done }}>
204
+ <URLSchemeContext.Provider value={contextValue}>
202
205
  {props.children}
203
206
  </URLSchemeContext.Provider>
204
207
  </View>
@@ -5,6 +5,6 @@ import { coreAppLogger } from "../logger";
5
5
  export const { log_verbose, log_debug, log_warning, log_info, log_error } =
6
6
  createLogger({
7
7
  category: "DeepLinking",
8
- subsystem: "General",
8
+ subsystem: "app/deep_linking",
9
9
  parent: coreAppLogger,
10
10
  });
@@ -1,13 +1,16 @@
1
1
  import * as React from "react";
2
2
  import pick from "lodash/pick";
3
3
 
4
- import { coreLogger } from "@applicaster/zapp-react-native-utils/logger";
4
+ import { subsystemLogger } from "@applicaster/zapp-react-native-utils/logger";
5
5
  import { ErrorScreen } from "@applicaster/zapp-react-native-ui-components/Components/ErrorScreen";
6
6
 
7
7
  import { useErrorStore } from "./store";
8
8
  import * as QuickBrickManager from "@applicaster/zapp-react-native-bridge/QuickBrick";
9
9
 
10
- const errorBoundaryLogger = coreLogger.addSubsystem("react_error-boundary");
10
+ const errorBoundaryLogger = subsystemLogger(
11
+ "crash/error_boundary",
12
+ "ErrorBoundary"
13
+ );
11
14
 
12
15
  type Props = {
13
16
  children: React.ReactNode;
@@ -45,7 +45,6 @@ import {
45
45
  startUpHookPluginIdentifiersSelector,
46
46
  } from "./navigator/selectors";
47
47
 
48
- import { coreAppLogger } from "../logger";
49
48
  import {
50
49
  getPlatform,
51
50
  isTV,
@@ -89,8 +88,9 @@ import {
89
88
  useLayoutVersion,
90
89
  useRivers,
91
90
  } from "@applicaster/zapp-react-native-utils/reactHooks";
91
+ import { subsystemLogger } from "@applicaster/zapp-react-native-utils/logger";
92
92
 
93
- const logger = coreAppLogger.addSubsystem("Navigator");
93
+ const logger = subsystemLogger("ui/navigation", "Navigator");
94
94
 
95
95
  type Props = {
96
96
  children?: React.ReactChild;
@@ -7,16 +7,16 @@ import {
7
7
  } from "@applicaster/zapp-react-native-utils/navigationUtils";
8
8
  import { findPluginByType } from "@applicaster/zapp-react-native-utils/pluginUtils";
9
9
 
10
- import { coreAppLogger } from "../logger";
11
10
  import { isString } from "@applicaster/zapp-react-native-utils/stringUtils";
12
11
  import { SCREEN_TYPES } from "@applicaster/zapp-react-native-utils/navigationUtils/itemTypes";
13
12
  import { isNilOrEmpty } from "@applicaster/zapp-react-native-utils/reactUtils/helpers";
13
+ import { subsystemLogger } from "@applicaster/zapp-react-native-utils/logger";
14
14
 
15
15
  const EXTERNAL_URL_TYPE = "external_url";
16
16
  const EXTERNAL_URL_FLAG = "open_external_url";
17
17
  const WEBVIEW_SCREEN_IDENTIFIER = "webview_screen_qb";
18
18
 
19
- const logger = coreAppLogger.addSubsystem("Navigator");
19
+ const logger = subsystemLogger("ui/navigation", "Navigator");
20
20
 
21
21
  /**
22
22
  * Drops a leading `/hooks/<screen_id>` segment from a path.
@@ -12,11 +12,12 @@ import {
12
12
  unsubscribeToDeviceNetworkChanges,
13
13
  } from "./utils";
14
14
  import { usePrevious } from "@applicaster/zapp-react-native-utils/reactHooks/utils";
15
- import { coreAppLogger } from "../logger";
15
+
16
16
  import { useResumeVerifiedNetInfo } from "./useResumeVerifiedNetInfo";
17
17
  import { describeNetInfo } from "./resumeVerification";
18
+ import { subsystemLogger } from "@applicaster/zapp-react-native-utils/logger";
18
19
 
19
- const logger = coreAppLogger.addSubsystem("NetworkStatusProvider");
20
+ const logger = subsystemLogger("network/reachability", "NetworkStatusProvider");
20
21
 
21
22
  // Use default netInfo config for all platforms (important for web platform which otherwise
22
23
  // will use backend root which is not responding for HEAD request)
@@ -54,10 +55,17 @@ export const NetworkStatusProvider = ({
54
55
  const { netInfo: verifiedNetInfo, stillOfflineAfterResumeCount } =
55
56
  useResumeVerifiedNetInfo(liveNetInfo);
56
57
 
57
- const combinedNetInfo: NetInfoState & {
58
- deviceStatus?: NetStatus;
59
- stillOfflineAfterResumeCount: number;
60
- } = { ...verifiedNetInfo, stillOfflineAfterResumeCount };
58
+ // Memoized so consumers only re-render when the published status changes,
59
+ // not on every render of this provider
60
+ const combinedNetInfo = React.useMemo<
61
+ NetInfoState & {
62
+ deviceStatus?: NetStatus;
63
+ stillOfflineAfterResumeCount: number;
64
+ }
65
+ >(
66
+ () => ({ ...verifiedNetInfo, stillOfflineAfterResumeCount }),
67
+ [verifiedNetInfo, stillOfflineAfterResumeCount]
68
+ );
61
69
 
62
70
  const { onConnectionTypeChanged, onConnectionLost, onConnectionRestored } =
63
71
  React.useMemo<NetworkHooks>(() => getNetworkHooks(plugins), []);
@@ -1,7 +1,6 @@
1
1
  import { AppState, AppStateStatus } from "react-native";
2
2
  import { NetInfoState } from "@react-native-community/netinfo";
3
3
 
4
- import { coreAppLogger } from "../logger";
5
4
  import { isOnline } from "./utils";
6
5
  import {
7
6
  RESUME_VERIFICATION_DELAY_MS,
@@ -10,8 +9,9 @@ import {
10
9
  isBackgroundAppState,
11
10
  isResumeVerificationEnabled,
12
11
  } from "./resumeVerification";
12
+ import { subsystemLogger } from "@applicaster/zapp-react-native-utils/logger";
13
13
 
14
- const logger = coreAppLogger.addSubsystem("NetworkStatusProvider");
14
+ const logger = subsystemLogger("network/reachability", "NetworkStatusProvider");
15
15
 
16
16
  export type ConnectionInfo = NetInfoState & { deviceStatus?: NetStatus };
17
17
 
@@ -16,18 +16,33 @@ const mockLogger = {
16
16
  error: jest.fn(),
17
17
  };
18
18
 
19
- // addSubsystem runs at module load, before mockLogger is initialised: delegate lazily
20
- jest.mock("../../logger", () => ({
21
- coreAppLogger: {
22
- addSubsystem: () => ({
23
- warning: (event) => mockLogger.warning(event),
24
- debug: (event) => mockLogger.debug(event),
25
- info: (event) => mockLogger.info(event),
26
- log: (event) => mockLogger.log(event),
27
- error: (event) => mockLogger.error(event),
28
- }),
29
- },
30
- }));
19
+ // subsystemLogger runs at module load, before mockLogger is initialised: delegate lazily
20
+ jest.mock("@applicaster/zapp-react-native-utils/logger", () => {
21
+ const actual = jest.requireActual(
22
+ "@applicaster/zapp-react-native-utils/logger"
23
+ );
24
+
25
+ return {
26
+ ...actual,
27
+ // Only the logger of this subsystem is replaced; every other module keeps its real one.
28
+ subsystemLogger: (subsystem: string) => {
29
+ if (subsystem !== "network/reachability") {
30
+ return actual.subsystemLogger(subsystem);
31
+ }
32
+
33
+ const logger = {
34
+ warning: (event) => mockLogger.warning(event),
35
+ debug: (event) => mockLogger.debug(event),
36
+ info: (event) => mockLogger.info(event),
37
+ log: (event) => mockLogger.log(event),
38
+ error: (event) => mockLogger.error(event),
39
+ addSubsystem: () => logger,
40
+ };
41
+
42
+ return logger;
43
+ },
44
+ };
45
+ });
31
46
 
32
47
  jest.mock("react-native/Libraries/AppState/AppState", () => {
33
48
  const { EventEmitter } = require("events");
@@ -12,18 +12,33 @@ const mockLogger = {
12
12
  error: jest.fn(),
13
13
  };
14
14
 
15
- // addSubsystem runs at module load, before mockLogger is initialised: delegate lazily
16
- jest.mock("../../logger", () => ({
17
- coreAppLogger: {
18
- addSubsystem: () => ({
19
- warning: (event) => mockLogger.warning(event),
20
- debug: (event) => mockLogger.debug(event),
21
- info: (event) => mockLogger.info(event),
22
- log: (event) => mockLogger.log(event),
23
- error: (event) => mockLogger.error(event),
24
- }),
25
- },
26
- }));
15
+ // subsystemLogger runs at module load, before mockLogger is initialised: delegate lazily
16
+ jest.mock("@applicaster/zapp-react-native-utils/logger", () => {
17
+ const actual = jest.requireActual(
18
+ "@applicaster/zapp-react-native-utils/logger"
19
+ );
20
+
21
+ return {
22
+ ...actual,
23
+ // Only the logger of this subsystem is replaced; every other module keeps its real one.
24
+ subsystemLogger: (subsystem: string) => {
25
+ if (subsystem !== "network/reachability") {
26
+ return actual.subsystemLogger(subsystem);
27
+ }
28
+
29
+ const logger = {
30
+ warning: (event) => mockLogger.warning(event),
31
+ debug: (event) => mockLogger.debug(event),
32
+ info: (event) => mockLogger.info(event),
33
+ log: (event) => mockLogger.log(event),
34
+ error: (event) => mockLogger.error(event),
35
+ addSubsystem: () => logger,
36
+ };
37
+
38
+ return logger;
39
+ },
40
+ };
41
+ });
27
42
 
28
43
  jest.mock("react-native/Libraries/AppState/AppState", () => {
29
44
  const { EventEmitter } = require("events");
@@ -14,18 +14,33 @@ const mockLogger = {
14
14
  error: jest.fn(),
15
15
  };
16
16
 
17
- // addSubsystem runs at module load, before mockLogger is initialised: delegate lazily
18
- jest.mock("../../logger", () => ({
19
- coreAppLogger: {
20
- addSubsystem: () => ({
21
- warning: (event) => mockLogger.warning(event),
22
- debug: (event) => mockLogger.debug(event),
23
- info: (event) => mockLogger.info(event),
24
- log: (event) => mockLogger.log(event),
25
- error: (event) => mockLogger.error(event),
26
- }),
27
- },
28
- }));
17
+ // subsystemLogger runs at module load, before mockLogger is initialised: delegate lazily
18
+ jest.mock("@applicaster/zapp-react-native-utils/logger", () => {
19
+ const actual = jest.requireActual(
20
+ "@applicaster/zapp-react-native-utils/logger"
21
+ );
22
+
23
+ return {
24
+ ...actual,
25
+ // Only the logger of this subsystem is replaced; every other module keeps its real one.
26
+ subsystemLogger: (subsystem: string) => {
27
+ if (subsystem !== "network/reachability") {
28
+ return actual.subsystemLogger(subsystem);
29
+ }
30
+
31
+ const logger = {
32
+ warning: (event) => mockLogger.warning(event),
33
+ debug: (event) => mockLogger.debug(event),
34
+ info: (event) => mockLogger.info(event),
35
+ log: (event) => mockLogger.log(event),
36
+ error: (event) => mockLogger.error(event),
37
+ addSubsystem: () => logger,
38
+ };
39
+
40
+ return logger;
41
+ },
42
+ };
43
+ });
29
44
 
30
45
  jest.mock("react-native/Libraries/AppState/AppState", () => {
31
46
  const { EventEmitter } = require("events");
@@ -100,7 +100,12 @@ const MemoizedThemeContextProvider = React.memo(
100
100
  function ThemeManagerComponent({ children, plugins }: Props) {
101
101
  const isTablet = useIsTablet();
102
102
 
103
- const themes = selectThemes(isTablet, plugins);
103
+ // Stable reference keeps MemoizedThemeContextProvider's byReference check
104
+ // effective, so theme consumers don't re-render on every parent render
105
+ const themes = React.useMemo(
106
+ () => selectThemes(isTablet, plugins),
107
+ [isTablet, plugins]
108
+ );
104
109
 
105
110
  const [selectedThemeId, setSelectedThemeId] =
106
111
  React.useState(DEFAULT_THEME_ID);
@@ -12,13 +12,13 @@ import { isAndroidPlatform } from "@applicaster/zapp-react-native-utils/reactUti
12
12
  import { networkStatusReady } from "../NetworkStatusProvider/networkStatusReady";
13
13
  import { updateSessionNumber } from "../SessionCounter/sessionCounter";
14
14
 
15
- import { coreAppLogger } from "../logger";
16
15
  import { sessionStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/SessionStorage";
17
16
  import { isEmptyOrNil } from "@applicaster/zapp-react-native-utils/cellUtils";
18
17
  import { requiredSessionKeys } from "../../const";
19
18
  import { initializeActionSetters, useActionSetters } from "../ActionSetters";
19
+ import { subsystemLogger } from "@applicaster/zapp-react-native-utils/logger";
20
20
 
21
- const logger = coreAppLogger.addSubsystem("AppLifeCycleManager");
21
+ const logger = subsystemLogger("app/lifecycle", "AppLifeCycleManager");
22
22
 
23
23
  const isAndroid = isAndroidPlatform();
24
24
 
@@ -2,11 +2,11 @@ import * as React from "react";
2
2
 
3
3
  import { getRemoteContextData } from "../remoteContextReloader/getRemoteContextData";
4
4
 
5
- import { coreAppLogger } from "../logger";
6
5
  import { UnknownAction, Dispatch } from "@reduxjs/toolkit";
7
6
  import { isAndroidPlatform } from "@applicaster/zapp-react-native-utils/reactUtils";
7
+ import { subsystemLogger } from "@applicaster/zapp-react-native-utils/logger";
8
8
 
9
- const logger = coreAppLogger.addSubsystem("AppRemoteDataLoader");
9
+ const logger = subsystemLogger("app/loading", "AppRemoteDataLoader");
10
10
 
11
11
  /**
12
12
  * adds a call to the QuickBrickManager to tell the app is ready, reloads remote
@@ -5,8 +5,8 @@ import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
5
5
  import { coreAppLogger } from "../logger";
6
6
 
7
7
  const logger = createLogger({
8
- category: "PlayerController",
9
- subsystem: "RemoteDataLoader",
8
+ category: "RemoteDataLoader",
9
+ subsystem: "app/loading",
10
10
  parent: coreAppLogger,
11
11
  });
12
12
 
package/App/logger.js CHANGED
@@ -1,3 +1,3 @@
1
- import { coreLogger } from "@applicaster/zapp-react-native-utils/logger";
1
+ import { subsystemLogger } from "@applicaster/zapp-react-native-utils/logger";
2
2
 
3
- export const coreAppLogger = coreLogger.addSubsystem("app");
3
+ export const coreAppLogger = subsystemLogger("app");
@@ -0,0 +1,148 @@
1
+ import { loadAppContextData } from "@applicaster/zapp-react-native-redux";
2
+ import { getPromiseForType } from "../helpers";
3
+ import {
4
+ applyPersistedLayoutPreview,
5
+ restoreInvalidPreview,
6
+ } from "../../layoutPreview/persistence";
7
+ import { getNativeRemoteContextData } from "../getRemoteContextData/getNativeRemoteContextData";
8
+
9
+ jest.mock("@applicaster/zapp-react-native-redux", () => ({
10
+ ...jest.requireActual("@applicaster/zapp-react-native-redux"),
11
+ loadAppContextData: jest.fn(),
12
+ }));
13
+
14
+ jest.mock("../helpers", () => ({
15
+ getPromiseForType: jest.fn(),
16
+ fetchPluginConfiguration: () => (plugin) => plugin,
17
+ }));
18
+
19
+ jest.mock("../../AssetCache", () => ({
20
+ cacheAssets: jest.fn(async (data) => data),
21
+ }));
22
+
23
+ jest.mock("../../layoutPreview/persistence", () => ({
24
+ applyPersistedLayoutPreview: jest.fn(),
25
+ restoreInvalidPreview: jest.fn(),
26
+ }));
27
+
28
+ const mockedGetPromiseForType = getPromiseForType as jest.Mock;
29
+
30
+ const FILES = {
31
+ remoteConfigurations: { remoteConfigurations: {} },
32
+ pluginConfigurations: { pluginConfigurations: [] },
33
+ pipesEndpoints: { pipesEndpoints: {} },
34
+ layout: { layout: { api_version: "v2", content_types: {} } },
35
+ cellStyles: { cellStyles: { style: {} } },
36
+ presetsMapping: { presetsMapping: { preset: {} } },
37
+ };
38
+
39
+ const LAYOUT_FILES = ["layout", "cellStyles", "presetsMapping"];
40
+
41
+ /** Holds every request open until the test releases it. */
42
+ function deferredFiles(files = FILES) {
43
+ const pending: Record<string, () => void> = {};
44
+
45
+ mockedGetPromiseForType.mockImplementation(
46
+ (key: string) =>
47
+ new Promise((resolve, reject) => {
48
+ pending[key] = () =>
49
+ files[key] ? resolve(files[key]) : reject(new Error(key));
50
+ })
51
+ );
52
+
53
+ return pending;
54
+ }
55
+
56
+ /** Keys of the app context handed to the store on the final load. */
57
+ const loadedKeys = () =>
58
+ Object.keys((loadAppContextData as jest.Mock).mock.calls.at(-1)[1]);
59
+
60
+ const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
61
+
62
+ describe("getNativeRemoteContextData", () => {
63
+ beforeEach(() => {
64
+ jest.clearAllMocks();
65
+ (applyPersistedLayoutPreview as jest.Mock).mockResolvedValue("absent");
66
+ });
67
+
68
+ it("requests the layout files together, not one after another", async () => {
69
+ const pending = deferredFiles();
70
+ const dispatch = jest.fn();
71
+
72
+ const done = getNativeRemoteContextData(dispatch, {}, []);
73
+
74
+ await flush();
75
+
76
+ ["remoteConfigurations", "pluginConfigurations", "pipesEndpoints"].forEach(
77
+ (key) => pending[key]()
78
+ );
79
+
80
+ await flush();
81
+
82
+ expect(Object.keys(pending)).toEqual(expect.arrayContaining(LAYOUT_FILES));
83
+
84
+ LAYOUT_FILES.forEach((key) => pending[key]());
85
+ await done;
86
+
87
+ expect(loadedKeys()).toEqual(
88
+ expect.arrayContaining(["rivers", "cellStyles", "presetsMapping"])
89
+ );
90
+ });
91
+
92
+ it("tolerates a missing presetsMapping", async () => {
93
+ const { presetsMapping: _presetsMapping, ...withoutPresets } = FILES;
94
+
95
+ mockedGetPromiseForType.mockImplementation(async (key: string) => {
96
+ if (!withoutPresets[key]) throw new Error(key);
97
+
98
+ return withoutPresets[key];
99
+ });
100
+
101
+ const dispatch = jest.fn();
102
+
103
+ await expect(
104
+ getNativeRemoteContextData(dispatch, {}, [])
105
+ ).resolves.toBeUndefined();
106
+
107
+ expect(loadedKeys()).toContain("rivers");
108
+ expect(loadedKeys()).not.toContain("presetsMapping");
109
+ });
110
+
111
+ it("rejects when cellStyles cannot be loaded", async () => {
112
+ const { cellStyles: _cellStyles, ...withoutCellStyles } = FILES;
113
+
114
+ mockedGetPromiseForType.mockImplementation(async (key: string) => {
115
+ if (!withoutCellStyles[key]) throw new Error(key);
116
+
117
+ return withoutCellStyles[key];
118
+ });
119
+
120
+ await expect(getNativeRemoteContextData(jest.fn(), {}, [])).rejects.toThrow(
121
+ "cellStyles"
122
+ );
123
+ });
124
+
125
+ it("refetches every layout file after restoring a broken preview", async () => {
126
+ (applyPersistedLayoutPreview as jest.Mock).mockResolvedValue("applied");
127
+
128
+ let restored = false;
129
+
130
+ (restoreInvalidPreview as jest.Mock).mockImplementation(async () => {
131
+ restored = true;
132
+ });
133
+
134
+ mockedGetPromiseForType.mockImplementation(async (key: string) => {
135
+ if (key === "layout" && !restored) throw new Error("preview layout");
136
+
137
+ return FILES[key];
138
+ });
139
+
140
+ await getNativeRemoteContextData(jest.fn(), {}, []);
141
+
142
+ const requests = mockedGetPromiseForType.mock.calls.map(([key]) => key);
143
+
144
+ LAYOUT_FILES.forEach((key) => {
145
+ expect(requests.filter((request) => request === key)).toHaveLength(2);
146
+ });
147
+ });
148
+ });
@@ -0,0 +1,31 @@
1
+ const { NativeModules } = require("react-native");
2
+
3
+ NativeModules.AppLoaderBridge = { getFile: jest.fn() };
4
+
5
+ const { getPromiseForType } = require("../helpers");
6
+
7
+ const layout = { id: "layout", content_types: {} };
8
+
9
+ describe("getPromiseForType", () => {
10
+ it("parses the JSON string native bridges resolve", async () => {
11
+ NativeModules.AppLoaderBridge.getFile.mockResolvedValue(
12
+ JSON.stringify(layout)
13
+ );
14
+
15
+ await expect(getPromiseForType("layout")).resolves.toEqual({ layout });
16
+ });
17
+
18
+ it("passes through the parsed file the web bridge resolves", async () => {
19
+ NativeModules.AppLoaderBridge.getFile.mockResolvedValue(layout);
20
+
21
+ await expect(getPromiseForType("layout")).resolves.toEqual({ layout });
22
+ });
23
+
24
+ it("rejects when the bridge fails", async () => {
25
+ NativeModules.AppLoaderBridge.getFile.mockRejectedValue(new Error("404"));
26
+
27
+ await expect(getPromiseForType("layout")).rejects.toThrow(
28
+ "cannot retrieve configuration data for layout"
29
+ );
30
+ });
31
+ });
@@ -1,5 +1,4 @@
1
1
  import * as R from "ramda";
2
- import isEmpty from "lodash/isEmpty";
3
2
  import { Dimensions, NativeModules } from "react-native";
4
3
  import {
5
4
  loadAppContextData,
@@ -7,6 +6,7 @@ import {
7
6
  } from "@applicaster/zapp-react-native-redux";
8
7
  import { isTablet } from "@applicaster/zapp-react-native-utils/reactHooks";
9
8
  import { mapPromises } from "@applicaster/zapp-react-native-utils/arrayUtils";
9
+ import { isEmpty } from "@applicaster/zapp-react-native-utils/utils";
10
10
  import { fetchPluginConfiguration, getPromiseForType } from "../helpers";
11
11
  import { cacheAssets } from "../../AssetCache";
12
12
  import { logger } from "@applicaster/zapp-react-native-utils/logger";
@@ -65,6 +65,33 @@ async function selectActiveLayout(
65
65
  return preview;
66
66
  }
67
67
 
68
+ /**
69
+ * Fetches the files that belong to the selected layout. They all derive from
70
+ * the same layout id, so they are requested together rather than one round
71
+ * trip after another. A missing presetsMapping is tolerated; a failing layout
72
+ * or cellStyles request rejects, as it did when they were fetched in sequence.
73
+ */
74
+ type Settled<T> =
75
+ | { status: "fulfilled"; value: T }
76
+ | { status: "rejected"; reason: unknown };
77
+
78
+ // Promise.allSettled is not guaranteed on older TV engines, so settle by hand
79
+ const settle = <T>(promise: Promise<T>): Promise<Settled<T>> =>
80
+ promise.then(
81
+ (value) => ({ status: "fulfilled", value }),
82
+ (reason) => ({ status: "rejected", reason })
83
+ );
84
+
85
+ async function fetchLayoutFiles() {
86
+ const [layout, cellStyles, presetsMapping] = await Promise.all([
87
+ settle(getPromiseForType<any>("layout")),
88
+ settle(getPromiseForType<any>("cellStyles")),
89
+ settle(getPromiseForType<any>("presetsMapping")),
90
+ ]);
91
+
92
+ return { layout, cellStyles, presetsMapping };
93
+ }
94
+
68
95
  export async function getNativeRemoteContextData(
69
96
  dispatch,
70
97
  runtimeConfigurationUrls,
@@ -91,22 +118,27 @@ export async function getNativeRemoteContextData(
91
118
  isTabletDevice
92
119
  );
93
120
 
94
- let layout;
121
+ let layoutFiles = await fetchLayoutFiles();
95
122
 
96
- try {
97
- layout = await getPromiseForType("layout");
98
- } catch (error) {
99
- if (preview !== "applied") throw error;
123
+ if (layoutFiles.layout.status === "rejected") {
124
+ if (preview !== "applied") throw layoutFiles.layout.reason;
100
125
 
101
126
  logger.error({
102
127
  message: "Persisted preview layout failed to load",
103
- data: error,
128
+ data: layoutFiles.layout.reason,
104
129
  });
105
130
 
131
+ // The files fetched so far belong to the broken preview layout
106
132
  await restoreInvalidPreview();
107
- layout = await getPromiseForType("layout");
133
+ layoutFiles = await fetchLayoutFiles();
134
+ }
135
+
136
+ if (layoutFiles.layout.status === "rejected") {
137
+ throw layoutFiles.layout.reason;
108
138
  }
109
139
 
140
+ const layout = layoutFiles.layout.value;
141
+
110
142
  const layoutLoadingErrorMessage = "Failed to load layout data";
111
143
 
112
144
  if (isEmpty(layout)) {
@@ -123,16 +155,17 @@ export async function getNativeRemoteContextData(
123
155
 
124
156
  dispatch(AppData.actions.merge({ layoutVersion: apiVersion || "v1" }));
125
157
 
126
- const resolvedPromises = await mapPromises(getPromiseForType, ["cellStyles"]);
158
+ if (layoutFiles.cellStyles.status === "rejected") {
159
+ throw layoutFiles.cellStyles.reason;
160
+ }
127
161
 
128
- let optionalResolvedPromise;
162
+ const resolvedPromises = [layoutFiles.cellStyles.value];
129
163
 
130
- try {
131
- optionalResolvedPromise = await getPromiseForType("presetsMapping");
132
- } catch (e) {
133
- // don't break loading sequence if presetsMapping does not exist
134
- // only general error could be intercepted here so no way divide flow
135
- }
164
+ // don't break loading sequence if presetsMapping does not exist
165
+ const optionalResolvedPromise =
166
+ layoutFiles.presetsMapping.status === "fulfilled"
167
+ ? layoutFiles.presetsMapping.value
168
+ : undefined;
136
169
 
137
170
  const remoteData = R.mergeAll([
138
171
  ...resolvedPromises,
@@ -119,8 +119,9 @@ export async function getPromiseForType<T = FileResponse>(
119
119
  try {
120
120
  const response = await AppLoaderBridge?.getFile(key, null);
121
121
 
122
+ // Native bridges resolve a JSON string; the web bridge resolves the parsed file.
122
123
  return {
123
- [key]: JSON.parse(response),
124
+ [key]: typeof response === "string" ? JSON.parse(response) : response,
124
125
  } as T;
125
126
  } catch (e) {
126
127
  throw new Error("cannot retrieve configuration data for " + key);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/quick-brick-core",
3
- "version": "16.0.0-rc.126",
3
+ "version": "16.0.0-rc.128",
4
4
  "description": "Core package for Applicaster's Quick Brick App",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -28,13 +28,13 @@
28
28
  },
29
29
  "homepage": "https://github.com/applicaster/quickbrick#readme",
30
30
  "dependencies": {
31
- "@applicaster/applicaster-types": "16.0.0-rc.126",
32
- "@applicaster/quick-brick-core-plugins": "16.0.0-rc.126",
33
- "@applicaster/zapp-pipes-v2-client": "16.0.0-rc.126",
34
- "@applicaster/zapp-react-native-bridge": "16.0.0-rc.126",
35
- "@applicaster/zapp-react-native-redux": "16.0.0-rc.126",
36
- "@applicaster/zapp-react-native-ui-components": "16.0.0-rc.126",
37
- "@applicaster/zapp-react-native-utils": "16.0.0-rc.126",
31
+ "@applicaster/applicaster-types": "16.0.0-rc.128",
32
+ "@applicaster/quick-brick-core-plugins": "16.0.0-rc.128",
33
+ "@applicaster/zapp-pipes-v2-client": "16.0.0-rc.128",
34
+ "@applicaster/zapp-react-native-bridge": "16.0.0-rc.128",
35
+ "@applicaster/zapp-react-native-redux": "16.0.0-rc.128",
36
+ "@applicaster/zapp-react-native-ui-components": "16.0.0-rc.128",
37
+ "@applicaster/zapp-react-native-utils": "16.0.0-rc.128",
38
38
  "atob": "^2.1.2",
39
39
  "axios": "^0.28.0",
40
40
  "btoa": "^1.2.1",