Skip to content
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: Expand the type for ServiceSchema to allow for typed lifecycle handlers #1272

Merged
merged 5 commits into from
Apr 27, 2024

Conversation

marceliwac
Copy link
Contributor

@marceliwac marceliwac commented Feb 5, 2024

This change expands the typings for the ServiceSchema, allowing it to accept the "service-this" type used by the lifecycle handlers in the typescript template. Please see this issue for a more complete explanation.

📝 Description

Change the type of the ServiceSchema to accept two generic types, latter of which is new and used to provide types for the lifecycle methods (created, started, stopped).

The typescript template will require a minor change if this change is accepted.

🎯 Relevant issues

moleculer-template-project-typescript #70

💎 Type of change

  • New feature (non-breaking change which adds functionality)

🚦 How Has This Been Tested?

The example code to verify that this change works is outlined in moleculer-template-project-typescript #70.

🏁 Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have commented my code, particularly in hard-to-understand areas

The tests use the latest template from https://github.com/moleculerjs/moleculer-template-project-typescript
The service also provides the types for the lifecycle methods which now call the logger to demostrate that correct object is passed.
@marceliwac
Copy link
Contributor Author

@icebob , Apologies for any confusion!

I updated the test/typescript/hello-world/greeter.service.ts (i.e. service schema) to the one used in the typescript template repository, since I couldn't find any in-code uses of the hooks or any tests related to that schema. I must be missing something, most likely the actual test that use this code. Could you please point me to the tests you're referring to?

I tried running all the tests specified in package.json which all pass fine (with the exception of memory leak tests, which I am guessing are unrelated).

@icebob
Copy link
Member

icebob commented Apr 21, 2024

I've checked again the code and it looks the test can be fine, but you removed a lot of things from the original greeter service, e.g. the hooks which uppercaseing the context params. it would be good if you keep the same logic in the service file.

If you should modify the test files, it means the changes contains breaking changes.

@marceliwac
Copy link
Contributor Author

@icebob You're absolutely right, sorry for that!

I reverted the deletions and tweaked them to comply with the service definition. I also added the implementation for the uppercase() method (which was declared as existing on the service but didn't) and replaced the direct call to toUpperCase() to a call to that method. Finally, I added the missing type delcaration for this where appropriate.

@icebob
Copy link
Member

icebob commented Apr 27, 2024

Great, thanks!

@icebob icebob merged commit 2bfa49a into moleculerjs:master Apr 27, 2024
176 of 177 checks passed
@marceliwac marceliwac deleted the typed-lifecycle-handlers branch July 10, 2024 08:37
@shawnmcknight
Copy link
Member

@marceliwac I've just updated moleculer versions and I'm encountering type errors as a result of this change.

Can you clarify why the ServiceSchema generic has a default of void while the lifecycle handlers have a default of Service<S>? This causes a conflict in scenarios where both types are being used along with their defaults. Is there a reason why ServiceSchema didn't have the same default as the lifecycle handlers?

jellydn added a commit to jellydn/moleculer-typescript-template that referenced this pull request Jul 31, 2024
@marceliwac
Copy link
Contributor Author

marceliwac commented Aug 16, 2024

@shawnmcknight

Apologies for the late reply and thanks for bringing this up!

You're right, these should in fact use Service<S> rather than void. The S type also seems to be redundant:

type ServiceSyncLifecycleHandler<T> = (this: T) => void;
type ServiceAsyncLifecycleHandler<T> = (this: T) => void | Promise<void>;

interface ServiceSchema<S = ServiceSettingSchema, T = Service<S>> {
	name: string;
	version?: string | number;
	settings?: S;
	dependencies?: string | ServiceDependency | (string | ServiceDependency)[];
	metadata?: any;
	actions?: ServiceActionsSchema;
	mixins?: Partial<ServiceSchema>[];
	methods?: ServiceMethods;
	hooks?: ServiceHooks;

	events?: ServiceEvents;
	created?: ServiceSyncLifecycleHandler<T> | ServiceSyncLifecycleHandler<T>[];
	started?: ServiceAsyncLifecycleHandler<T> | ServiceAsyncLifecycleHandler<T>[];
	stopped?: ServiceAsyncLifecycleHandler<T> | ServiceAsyncLifecycleHandler<T>[];

	[name: string]: any;
}

Ideally, if lifecycle handlers were nested within a separate property there would be a much cleaner solution that uses ThisType<T> utility like so:

type ServiceSyncLifecycleHandler = () => void;
type ServiceAsyncLifecycleHandler = () => void | Promise<void>;

type ServiceLifecycle<T> = {
	created?: ServiceSyncLifecycleHandler | ServiceSyncLifecycleHandler[];
	started?: ServiceAsyncLifecycleHandler | ServiceAsyncLifecycleHandler[];
	stopped?: ServiceAsyncLifecycleHandler | ServiceAsyncLifecycleHandler[];
};

interface ServiceSchema<S = ServiceSettingSchema, T = Service<S>> {
	name: string;
	version?: string | number;
	settings?: S;
	dependencies?: string | ServiceDependency | (string | ServiceDependency)[];
	metadata?: any;
	actions?: ServiceActionsSchema;
	mixins?: Partial<ServiceSchema>[];
	methods?: ServiceMethods;
	hooks?: ServiceHooks;

	events?: ServiceEvents;
        lifecycle?: ServiceLifecycle & ThisType<T>;

	[name: string]: any;
}

But because the lifecycle handlers are direct properties of the service, this makes things slightly messier. ServiceSchema is an interface and extending it with a type of LifecycleHandlers<T> (which is legal in TS >= 2.2) doesn't seem to apply the ThisType<T> correctly:

type ServiceSyncLifecycleHandler = () => void;
type ServiceAsyncLifecycleHandler = () => void | Promise<void>;

type ServiceLifecycle<T> = {
	created?: ServiceSyncLifecycleHandler | ServiceSyncLifecycleHandler[];
	started?: ServiceAsyncLifecycleHandler | ServiceAsyncLifecycleHandler[];
	stopped?: ServiceAsyncLifecycleHandler | ServiceAsyncLifecycleHandler[];
} & ThisType<T>;

interface ServiceSchema<S = ServiceSettingSchema, T = Service<S>> extends ServiceLifecycle<T> {
	name: string;
	version?: string | number;
	settings?: S;
	dependencies?: string | ServiceDependency | (string | ServiceDependency)[];
	metadata?: any;
	actions?: ServiceActionsSchema;
	mixins?: Partial<ServiceSchema>[];
	methods?: ServiceMethods;
	hooks?: ServiceHooks;

        // `created`, `started` and `stopped` are defined, but don't have the correct type for `this`

	events?: ServiceEvents;
	[name: string]: any;
}

On a side note, I also think that the generic T should be passed to the methods, events and handlers so we don't have to explicitly set this in each of the handlers. That matters less, because it still needs to be done if the function is defined outside of schema definition.

@icebob Any thoughts on this? Should I submit a PR?

@icebob
Copy link
Member

icebob commented Aug 25, 2024

I have no idea due to lack of TS experience. @shawnmcknight ?

@MichaelErmer
Copy link
Contributor

what @marceliwac suggested would resolve our issues with the current version

@marceliwac
Copy link
Contributor Author

@shawnmcknight, @icebob, @MichaelErmer I've pushed the PR to address this. I would appreciate a second pair of eyes sanity checking it if you could find some time. Hopefully, the added TS tests should prevent similar issues in the future.

I'm not 100% sure how to verify that this type of the lifecycle handlers (created, started and stopped) actually poitns to the instance of Service type, hence the use surrogate types (ServiceSyncLifecycleHandler and ServiceAsyncLifecycleHandler) in the tests. If you have better ideas, I'm open to suggestions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants