-
Notifications
You must be signed in to change notification settings - Fork 55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(project): services modularization #363
Conversation
Visit the preview URL for this PR (updated for commit e73e05c): https://ottwebapp--pr363-feature-services-mod-smampy9r.web.app (expires Sat, 16 Dec 2023 07:59:09 GMT) 🔥 via Firebase Hosting GitHub Action 🌎 Sign: c198f8a3a199ba8747819f7f1e45cf602b777529 |
c267293
to
781c80c
Compare
781c80c
to
e737b44
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried to comment on the major suggestions for this refactor, but 1 other point I want to make as well is I think it's best if we don't do file moving / renaming with this change. There is already going to be enough churn to cause merge conflicts without also trying to trace conflicts across file paths. Then we can follow up this PR with a big move / rename that doesn't have any logic changes. I think that will make merges much less painful.
Nice work @AntonLantukh, this is huge! This is a great step forward! This will make it easier to separate the logic from the UI and deal with the multiple integrations better. I do have one generic comment regarding the actual separation of integrations. The UI is mainly based on the data/logic from Cleeng APIs. This requires the JWP integration to transform all data to the Cleeng format. I would like proof that the UI works using OTT app-only models and types. The services would need to transform the data into an OTT app model used by UI, controllers and stores. For example; type User = {
id: string;
email: string;
profile: {
firstName?: string;
lastName?: string;
}
// ...
};
export default interface AccountService {
// ...
getUser: ({ config }: { config: Config }) => Promise<User>;
updateUser: ({ config, user }: { config: Config, user: User }) => Promise<User>;
// ...
} |
I just created a PR on your PR with a working version of the reflect stuff for using classes as identifiers w/o the need for If you move all the files back to match the structure currently in |
3949e7a
to
6cdf0ed
Compare
Some draft comments:
Doesn't work:
Works:
|
780d105
to
7b6e030
Compare
7b6e030
to
4120a32
Compare
4120a32
to
2459e9b
Compare
Nice changes @AntonLantukh! Have you also considered factories or dynamic values to achieve dynamic integrations? For example, you can use named factories like so: container.bind<AccountService>('AccountService').to(CleengAccountService).whenTargetNamed('cleeng');
container.bind<AccountService>('AccountService').to(InPlayerAccountService).whenTargetNamed('inplayer');
// Factories
container.bind(AccountServiceFactoryId).toFactory((context) => {
return (integration: string) => integration ? context.container.getNamed<AccountService>('AccountService', integration) : undefined;
});
// usage
class AccountController {
constructor(
@inject(AccountServiceFactoryId) accountServiceFactory: AccountServiceFactory,
) {
const { getAuthProviderName } = useConfigStore.getState();
this.accountService = accountServiceFactory(getAuthProviderName()); // optional
}
} Or using a dynamic value that resolves the integration: container.bind('CleengAccountService').to(CleengAccountService);
container.bind('InPlayerAccountService').to(InPlayerAccountService);
container.bind(AccountService).toDynamicValue((context: interfaces.Context) => {
const config = context.resolve(ConfigService);
const authProvider = config.getAuthProviderName();
if (authProvider === 'jwp') return context.resolve('InPlayerAccountService');
if (authProvider === 'cleeng') return context.resolve('CleengAccountService');
}); |
@ChristiaanScheermeijer Thanks! It looks like it may add some complexity in the init logic (dynamic value approach). For AccountService and CheckoutService we may also need CleengService. So it would result in something like this (we also add unnecessary service-specific injectables):
The approach with factories requires additional factory types like 'AccountServiceFactory'. We removed string and symbol based types in favour of native classes / types (though we may reconsider it). Cleeng also needs to be initialized somehow (additional condition?): init.ts:
AccountController:
|
f163312
to
4cbbab0
Compare
Ah, I hoped we could move the DI init logic to the This means:
We may want to do code splitting based on the configured integration. E.g., we don't want to load the InPlayer SDK for AVOD or when Cleeng is used (and vice-versa). But we must first lazy import SDKs in the initialize function of the service instead of importing them directly. Factories seem the logical feature for this, but I also don't like injecting factories/symbols instead of the class directly. That's why I hoped the dynamic value could be used similarly to regular classes. I experimented with this branch and refactored a few things to make this work (I used factories here):
|
@ChristiaanScheermeijer @dbudzins I merged latest changes from
Thoughts? |
Nice work @AntonLantukh! I think the PR is finished and can be merged 🎉 One last question; do you know why the two e2e tests are failing, is that caused by this PR or were they already broken? |
const RootLoader = ({ onReady }: { onReady: () => void }) => { | ||
const query = useBootstrapApp(onReady); | ||
|
||
return IS_PROD_MODE ? <ProdContentLoader query={query} /> : <DemoContentLoader query={query} />; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you need different components here? Won't IS_DEMO_OR_PREVIEW
take care of all the differences?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I prefer to understand which 'side components' our main components will have depending on the environment. Here we can clearly differentiate between prod and other envs and can probably also easily remove the second part when not needed.
@@ -3,6 +3,8 @@ import { createRoot } from 'react-dom/client'; | |||
import 'wicg-inert'; | |||
import { registerSW } from 'virtual:pwa-register'; | |||
|
|||
import '#src/modules/register'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would rather export a default method and actually call it, instead of relying on side effects of an import.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should 'import' and bind modules before App is imported (and our AppController is invoked). If we invoke it here, then it starts to fail, as Import statements are always hoisted to the very top.
settings: Settings; | ||
}; | ||
|
||
export const useBootstrapApp = (onReady: () => void) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still don't think this needs to be a separate hook instead of just being inline in Root.tsx.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It also helps to separate typings from the hook's response in reuse them in DemoConfigDialog
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few small suggestions, but otherwise it looks GREAT!
7ed9b43
to
c4b583a
Compare
Fixed tests, asked Cleeng about the coupon changes. |
Controllers and Services
Controllers and Services can both be used to provide services (objects) that can be injectable into parts of the application.
Services - domain related entities. Business logic is stored there. We use services to communicate with the back-end, to process the data we receive. Services help to manage different dependencies. For example, we can use them to support several integration providers or streaming schedule providers. If this is the case we should also create a common interface and make dependant entities use the interface instead of the actual implementation. This is how inversion of control principle can be respected. Then when we inject services into controllers we use interfaces types instead of the implementation classes.
Controllers - binding different parts of the application, using services, store and providing methods to operate with business logic in the UI and in the App. If we need to share code across controllers then it is better to promote the code to the next level. Then it is possible to modify both controllers to call the same (now shared) code. We should try to avoid controllers calling each other because it leads to circular dependencies and makes the code messy. However now they do it sometimes.
Store - If the code is related to storage/retrieval, it should go in the Store. Both controllers and UI / View can use Store in accordance with their needs. Something to think about: use classes and inject them into controllers.
InversifyJS
InversifyJS library is used to provide IOC container and to perform dependency injection for both services and controllers. Injection happens automatically with the help of the reflect-metadata package.
initDefaultServices
function is used to init default services and controllers which don't depend on any integration provider.In the
initIOCData
function we initialise controllers based on the selected integration provider and inject necessary services.Steps completed:
According to our definition of done, I have completed the following steps: