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

Decorator cookbook #231

Open
littledan opened this issue Jan 22, 2019 · 54 comments
Open

Decorator cookbook #231

littledan opened this issue Jan 22, 2019 · 54 comments
Labels

Comments

@littledan
Copy link
Member

littledan commented Jan 22, 2019

Please post your use cases for decorators as an issue comment, and we'll see if we can document them in the cookbook

Let's write a how-to guide about how to achieve various things with decorators, including:

  • A class decorator to "register" a class once it's created
  • A field decorator to create an accessor and underlying field
  • ...
@ljharb
Copy link
Member

ljharb commented Jan 22, 2019

  • Making a private field conditionally public based on NODE_ENV, for testing
  • Making a public field with an arrow function into a field-bound instance method
  • Similarly, field-binding instance methods
  • making a method throw when it's passed the wrong number of arguments, or the wrong type of arguments

@littledan
Copy link
Member Author

More examples welcome!

@b-strauss
Copy link

  • A method that logs all its parameters on execution
  • A method that is throttled via requestAnimationFrame
  • A method that caches its result based on primitive param values

@littledan
Copy link
Member Author

To generalize @b-strauss , wrapping a method, or wrapping a field initializer.

@rgolea
Copy link

rgolea commented Jan 23, 2019

I generally use them for argument validation or permissions and also, response/return sanitization. Not sure if it would help but I’m sure everyone does this...

@bathos-wistia
Copy link

Documentation/annotation. Not all use cases need to be stuff that makes it all the way to runtime, I think — even if stripped during a build step for prod, it’s an improvement over comment-based doc generation because they’ll be first-class AST nodes associated with the correct context.

@Lodin
Copy link
Contributor

Lodin commented Jan 23, 2019

  • Extending method in ClassDecorator that could be defined by user or exist on prototype chain. If it is not defined at all, fallback can be used.
  • Viewing private names added by a ClassDecorator in a coupled PropertyDecorator.

@bathos-wistia
Copy link

bathos-wistia commented Jan 23, 2019

  • async methods that return custom thenables

  • more generally, generator methods with custom “drivers”

  • or generator methods that implement custom prototypes (awkward currently, but can be a useful part of the generator model)

  • branding

  • web idl mixins — e.g. @maplike implementing the common methods

  • emitting one-time deprecation notices on access or invocation

  • declarative async dep graphs “for free” if you combine async fns + a @once caching decorator. this is hard to explain w/o an illustration. I did something like this a few years back in the context of an angular 1 app that made a lot of interrelated API calls using the OG decorators proposal and I think it can be a very useful pattern:

    class Foo {
      @once
      async getCats() {
        return api.getCats();
      }
    
      @once
      async getDogs() {
        return api.getDogs();
      }
    
      @once
      async getPetOwners() {
        const [ cats, dogs ] = await Promise.all([ this.getCats(), this.getDogs() ]);
        const ownerIDs = new Set([ ...cats, ...dogs ].flatMap(pet => pet.ownerIDs));
        return api.getPeopleByIDs(ownerIDs);
      }
    
      @once
      async getQuadrupedCount() {
        const [ cats, dogs ] = await Promise.all([ this.getCats(), this.getDogs() ]);
        return cats.length + dogs.length;
      }
    
      @once
      async getResources() {
        const [ cats, dogs, petOwners, quadupedCount ] = await Promise.all([
          this.getCats(),
          this.getDogs(),
          this.getPetOwners(),
          this.getQuadrupedCount()
        ]);
    
        return { cats, dogs, petOwners, quadupedCount };
      }
    }
    
    // maybe sometimes you need all resources, sometimes you just need the quad count, etc.
    // no matter what you request, though, you’re only making the minimum number of api calls
    // per instance.
    

@evanplaice
Copy link

Route Authentication

Wrap a route with a function that verifies that the user is authenticated with the correct role, reject the request with the proper HTTP response if otherwise.

I have implemented Basic-Auth using this approach in Python. It made auth soooo much easier to apply authentication to routes on a case-by-case basis.

Debugging: Count # of executions

Add a stateful counter decorator that increments every time a function is called and logs it to the console. Can be useful for tracking down obscure bugs.

Debugging: Measure execution time

Add a timer method that marks the time before, after, calculates the difference and logs it to the console. Useful for pinpointing hotspots in the code for optimization.

Apologies, the sample is in Typescript.

export function Timer(target: object, propertyKey: string, descriptor: TypedPropertyDescriptor<any>) { 
  const originalMethod = descriptor.value; 
 
  descriptor.value = function(...args: any[]) { 
    const start = performance.now(); 
    const result = originalMethod.apply(this, args); 
    const stop = performance.now(); 
    const elapsed = (stop - start).toFixed(2); 
    console.info(`%c[Timer]%c`, `color: blue`, `color: black`, `${target.constructor.name}.${propertyKey}() - ${elapsed} milliseconds`); 
 
    return result; 
  }; 
 
  return descriptor; 
}

Augmenting Classes

Sometimes it's useful to augment classes with additional behavior without adding a ton of boilerplate. For example, on the last big project I worked on we used custom scrollbars on a significant number of UI elements that were provided by an external lib.

To use the scrollbar required adding an instance prop to the class, and attaching it to the inner HTMLElement during construction. It could also be configured to use either fat/thin variants.

This was very easy to accomplish using a ClassDecorator. Unfortunately, we didn't use it because Typescript's type system couldn't see props that get dynamically added to a class during runtime. The only way to make it work was to pre-define the prop on the class.

Logging class/method/function Params

Somebody already mentioned this but you could write a function that collects all of the params attached to a class/method/function and logs them to the console and/or attach an event that can do the logging when it's fired.

@isidrok
Copy link

isidrok commented Jan 23, 2019

  • Writing metadata to later generate documentation: for example for an OpenAPI schema.

  • Changing a class definition at runtime / injecting arguments into its constructor:

    @ inject example

    function inject(...providers) {
      return function (classDescriptor) {
        return {
          ...classDescriptor,
          finisher(klass) {
            class Injectable {
              constructor(...args) {
                return Reflect.construct(klass, [...container.getAll(providers), ...args], Injectable);
              }
            }
            Reflect.setPrototypeOf(Injectable, klass);
            Reflect.setPrototypeOf(Injectable.prototype, klass.prototype);
            return Injectable;
          }
        }
      }
    }

@goodforenergy
Copy link

goodforenergy commented Jan 23, 2019

Mixins!

We migrated our codebase from an old pre ES6 inheritance pattern that looked like this:

const MySpectacularPage = BasePage.extend(SparklesMixin, SprinklesMixin, {});

to this:

class MySpectacularPage extends SprinklesMixin(SparklesMixin(BasePage)) {}

to snaz up our mixin pattern we used mix / with:

class MySpectacularPage extends mix(BasePage).with(SparklesMixin, SprinklesMixin) {}

but we did consider (and I would have preferred) decorators:

@SparklesMixin
@SprinklesMixin
class MySpectacularPage extends BasePage {}

@gitowiec
Copy link

gitowiec commented Jan 23, 2019

I would use decorators for Inversify, Tsoa and Nest.js!😀

@sithmel
Copy link

sithmel commented Jan 24, 2019

In this library you can see many examples of decorators used with asynchronous functions. https://github.com/sithmel/async-deco

@littledan
Copy link
Member Author

@ljharb To clarify #231 (comment) , what do you mean exactly by a field-bound instance method? And what is the motivation for using this?

@ljharb
Copy link
Member

ljharb commented Feb 1, 2019

foo = foo.bind(this);
foo() { }

The motivation is that arrows in class fields make it very hard to test react components, because you can’t mock/stub foo prior to instantiating the class (when testing a react component, one generally creates the element and passes it into enzyme in one swoop, which doesnt give you an opportunity to intercept the instance before references to the instance arrow functions are passed into the render tree). By converting to a field-bound instance method, tests can spy on the prototype prior to creating the instance. This problem is the source of dozens of bugs filed on enzyme, and the reason the advice i give is to never put functions in class fields. This decorator could serve as an alternative for those who find the syntax sugar more compelling than being able to test their code.

@SanderElias
Copy link

  • class decorators for adding metadata to a class (somewhat like static fields but more private)
  • property decorators that can read/mutate the above metadata
  • property decorators that can gather/inspect runtime artefacts. (as a sample, a litElement can have internal HTML-CSS that is changed at runtime, sometimes you want to extract things there) (this one will probably slow path) and expose those as a property.

@pabloalmunia
Copy link
Contributor

pabloalmunia commented Feb 15, 2019

You can use decorators for define other decorators. It's is posible by a simple replace hook than return a function, and it's very useful because you can define an class extensions as other class and use this extensions as a decorator.

@Decorator
class Onoff {

  @Override
  on() {
    /... 
  }
  
  @Override 
  off() { 
    /... 
  }
}

@Onoff  // This decorator is defined as a class (specially decorated) 
class MyClass {
}

const m = new MyClass();
m.on();

@czewail
Copy link

czewail commented Feb 16, 2019

decorate a class method, do some parameter manipulation, such as injection, want to use a parameter decorator, sad for not supported now

@kt3k
Copy link
Contributor

kt3k commented Feb 16, 2019

(Is it ok to provide the example use cases of a specific library?)
In capsid.js, you can define (DOM) event listeners by decorators, and can make a field into a query selector by decorators:

const { on, wired, component } = require("capsid");

@component("mirroring") // This registeres the class as a capsid component
class Mirroring {
  @wired(".dest") // This decorator makes the field equivalent of `get dest () { return this.el.querySelector('.dest'); }`
  dest;
  @wired(".src")
  src;

  @on("input") // This decorator makes the method into an event listener on the mounted element. In this case, onReceiveData is bound to `input` event of the mounted element
  onReceiveData() {
    this.dest.textContent = this.src.value;
  }
}

Here is the working demo at codesandbox

@littledan
Copy link
Member Author

I very much encourage giving practical examples from specific libraries! If you can explain why this is useful for you, even better.

@pabloalmunia
Copy link
Contributor

pabloalmunia commented Feb 16, 2019

Exist a group of constructor's intersection patterns very useful. For example, with a simple @singleton decorator we can define an object shared between all class users.

function Singleton (descriptor) {
  return {
    ...descriptor,
    finisher (Cls) {
      let ref;
      return function () {
        if (ref) return ref;
        return ref = new Cls ();
      }
    }
  }
}

@Singleton
class M {
}

const m1 = new M();
const m2 = new M();
console.assert(m1 === m2);

@pabloalmunia
Copy link
Contributor

pabloalmunia commented Feb 16, 2019

Decorators and Proxies together are an extreme powerful combination. For example, we can define a method as Default with a decorator and rewrite the constructor for return a proxy. As a result, if we call an unknow member, the default method is called with this value.

function Default (descriptor) {
  return {
    ...descriptor,
    finisher (Cls) {
      return function (...args) {
        const result = new Cls (...args);
        return new Proxy (result, {
          get (target, prop, receiver) {
            if (prop in target) {
              return Reflect.get (target, prop, receiver);
            }
            const origin = Reflect.get (target, descriptor.key, receiver);
            return origin (prop);
          }
        });
      };
    }
  };
}

class Database {
  open  () {}
  close () {}
  
  @Default
  table (name) {
    return {
      find   () {},
      add    () {},
      remove () {},
      edit   () {}
    }
  }
}

const db = new Database ();
db.table ('users').find ();
db.users.find ();  // .users isn't a member, but the default method is called with this value

@pabloalmunia
Copy link
Contributor

A very complete collection of method decoration is https://github.com/steelsojka/lodash-decorators

This library is a Decorators version of Lodash functions.

@MaxGraey
Copy link

AssemblyScript which superset of JavaScript using function level decorators a lot, but only for built-in decorators so no hoisting problems. Is it possible use function level decorator with this proposal for non-runtime decorators in future?

@littledan
Copy link
Member Author

@MaxGraey Interesting, can you say more about what you use AssemblyScript decorators for?

@MaxGraey
Copy link

MaxGraey commented Feb 17, 2019

Ok couple useful examples:

/* [built-in] hint for compiler which should always inline this func. 
 * Simpilar to `inline` in other langs
 */
@inline
function toDeg(x: number): number {
   return x * (180 / Math.PI);
}

example below just proof of concept. Not supported yet by AS:

/* [built-in] similar to `constexpr` in C++. 
 * Force expression evaluation during compilation. Valid only for constant arguments.
 */
@precompute // or @const ?
function fib(x: number): number {
   if (n <= 1) return n;
   return fib(n - 1) + fib(n - 2);
}
/* [built-in] indicate that function pure and could be potentially optimized for 
 * high ordered functions and lambda calculus
 */
@pure
function action(type: string, payload: object): object {
   return {
      type,
      ...payload
   };
}
/* [built-in] 
 * Same hint for TCO like in Kotlin
 */
@tailrec
function findFixPoint(x = 1.0): number {
  return Math.abs(x - Math.cos(x)) < eps ? x : findFixPoint(Math.cos(x));
}

Javascript world more and more shifted to functional paradigm and in this case functional level decorators is very necessary in my opinion)

@mikepii
Copy link

mikepii commented Feb 18, 2019

Decorators would be great for applying React higher-order components which inject React props. Here's a JSX example of what I was trying to get working to inject a prop from a React Context.

import React from 'react';
import update from "immutability-helper";

const userCtxDefaults = {name: 'J Doe'};
const UserContext = React.createContext(userCtxDefaults);

/**
 * Injects the userCtx prop from UserContext
 */
function withUserContext(Component) {
  return class extends React.Component {
    static displayName = `withUserContext(${Component.displayName || Component.name})`;

    render() {
      return (
        <UserContext.Consumer>
          {userCtx => {
            const props = update(this.props, {userCtx: {$set: userCtx}});
            return <Component {...props} />;
          }}
        </UserContext.Consumer>
      );
    }
  };
}

@withUserContext
class MyComponent extends React.Component {
  render() {
    return `name: ${this.props.userCtx.name}`
  }
}

It doesn't work in Typescript yet because they're waiting on this to reach stage 3 or 4 microsoft/TypeScript#4881 .

@pabloalmunia
Copy link
Contributor

pabloalmunia commented Feb 19, 2019

In VUE ecosystem we can found:

They are an example about Vue components, Vuex and decorators.

@mweststrate
Copy link

Made an overview of the most important use cases in MobX including some comments. (Sorry, noticed there was this issue for it only afterwards, I can bring the stuff here if needed). https://gist.github.com/mweststrate/8a4d12db0e11ca536c9ff3b6ba754243

Abstract summary:

  1. manipulate a constructor / prototype (no problems in stage 0)
  2. transform a descriptor (e.g. wrap a function) on the prototype (no problems in stage 0)
  3. transform a value based descriptor to getter / setter on the prototype (no problems in stage 0)
  4. transform a value based descriptor + field initializer into a descriptor on the instance. Lot's of trouble here, mostly: initializer cannot be run before the first access to the field (on the prototype). Which makes the field go missing in reflection. Can be fixed by either [[set]] for fields (e.g. TS doesn't give an problems here) or, by running the decorators for fields with initializer as part of the constructor (so that fields can be created eagerly)

@kasperpeulen
Copy link

I tried the babel plugin, but I could not get decorators for top level functions to work.
Is this proposal not about top level functions?
If so, I would love to have them added to this proposal, or that there would come another proposal that covers decorators for top level functions.

In the React ecosystem there would be a lot of usecases:

@connect(mapStateToProps, mapDispatchToProps)
@memo
@withTranslation 
@withStyles(styles, { name: 'button' })
export const MyComponent = props => (
  <div>
    <Button kind="primary" onClick={() => console.log('clicked!')}>
      Hello World!
    </Button>
  </div>
);

Without decorators, you will end up with code like this:

let MyComponent = props => (
  <div>
    <Button kind="primary" onClick={() => console.log('clicked!')}>
      Hello World!
    </Button>
  </div>
);
MyComponent = connect(
  mapStateToProps,
  mapDispatchToProps,
)(MyComponent);
MyComponent = memo(MyComponent);
MyComponent = withStyles(styles, { name: 'button' });
export { MyComponent };

Or you could write like this:

export const MyComponent = withStyles(styles, { name: 'button' })(
  memo(
    connect(
      mapStateToProps,
      mapDispatchToProps,
    )(props => (
      <div>
        <Button kind="primary" onClick={() => console.log('clicked!')}>
          Hello World!
        </Button>
      </div>
    )),
  ),
);

Of course there are many other possible usecases, Iike @MaxGraey examples.
I would probably use decorators like this:

@curry // curry all arguments so that they can be partially applied
export const every = (predicate, iterable) => {
  for (const item of iterable) {
    if (!predicate(item)) return false;
  }
  return true;
};
@memoize // caches the function results in a ES2015 Map as the function is pure 
export const isPrime = p =>
  pipe(
    naturals(2),
    takeWhile(n => n ** 2 <= p),
    every(n => p % n !== 0),
  );

@FireyFly
Copy link

@kasperpeulen you can get reasonably far with a compose-based approach (which is what we tend to use currently):

const MyComponent = props => (
  <div>
    <Button kind="primary" onClick={() => console.log('clicked!')}>
      Hello World!
    </Button>
  </div>
);
export default compose(
  connect(mapStateToProps, mapDispatchToProps),
  memo,
  withTranslation,
  withStyles(styles, { name: 'button' }),
)(MyComponent);

That said, I agree with your point, and would definitely like to see function-level decorators as well. I think it's important for consistency (e.g. in a React context you'd want to decorate class and function components the same way), and I think things like @memoize are good examples of the merits of function-level decorators more generally. 👍

@rdking
Copy link

rdking commented Feb 22, 2019

Support for

  • protected members
  • prototype properties
  • immutable prototypes (through the instance)
  • delegate members (instance-bound functions)
  • constant members (non-writable)
  • final members (non-writable, non-configurable: cannot be overridden in derived class)
  • abstract classes
  • final classes

There are others, but that depends on whether or not decorators will be allowed to inject new private names into the class's private name scope.

@dbartholomae
Copy link

dbartholomae commented Mar 9, 2019

+1 for the React examples, this is our most used use case so far. I would also like to use them for Lambda Middleware, looking like this:

function Authenticate (target, propertyType, descriptor) {
  const originalHandler = descriptor.value
  descriptor.value = (event) => {
    const userName = getUserNameFromAuthorizationHeader(event.headers.Authorization)
    return originalHandler({...event, auth: { userName }})
  }
  return descriptor
}

class Controller {
  @Authenticate
  public greetUser (event) {
    return {
      status: 200,
      body: `Hello ${event.auth.userName}`
    }
  }
}

new Controller.greetUser({
  headers: {
    Authorization: 'Bearer TOKEN'
  }
})

I don't know of any library implementing this yet for lambdas, though, but might work on one in the near future. There are multiple libraries with this goal for other server frameworks like express, though.
The general usage here is to augment behavior of methods for (Aspect-oriented programming)[https://en.wikipedia.org/wiki/Aspect-oriented_programming].

@Mansi1
Copy link

Mansi1 commented Mar 11, 2019

  • using them for object validation
  • using it to declare an testcase output
  • dependency injection
  • logging
  • authentication
  • routing

and a lot more....

@matthewadams
Copy link

Decorator-based aspect-oriented programming (AOP). See https://www.npmjs.com/package/@scispike/aspectify & https://github.com/SciSpike/aspectify.

@CyanSalt
Copy link

Some Node.js modules like TypeORM or routing-controllers use decorators to define and collect metadata of classes, methods and properties, similar to @interface annotation in Java.

@Entity()
export class User {
    @PrimaryColumn()
    id: number;
    @Column()
    name: string;
}
@JsonController()
export class UserController {
    @Get("/users")
    getAll() {
       return userRepository.findAll();
    }
}

@bodograumann
Copy link

Decorators are used in vuex-class-modules to define vuex modules as typescript classes. Example from the readme:

import { VuexModule, Module, Mutation, Action } from "vuex-class-modules";

@Module
class UserModule extends VuexModule {
  // state
  firstName = "Foo"
  lastName = "Bar"

  // getters
  get fullName() {
    return this.firstName + " " + this.lastName
  }

  // mutations
  @Mutation
  setFirstName(firstName: string) {
    this.firstName = firstName
  }
  @Mutation
  setLastName(lastName: string) {
    this.lastName = lastName
  }

  // actions
  @Action
  async loadUser() {
    const user = await fetchUser()
    this.setFirstName(user.firstName)
    this.setLastName(user.lastName)
  }
}

This uses a two step process of defining metadata via method decorators and then creating a proxy class with the @Module class decorator.

@hbroer
Copy link

hbroer commented Jun 1, 2019

I use decorators for a kind of dependency injection. The decorator is build like this:

function inject(type: symbol, container: Container, args: symbol[]) {
    return function(target: object, property: string): void {
        Object.defineProperty(target, property, {
            get: function() {
                const value = container.get<any>(type);
                if (args.indexOf(NOCACHE) === -1) {
                    Object.defineProperty(this, property, {
                        value,
                        enumerable: true,
                    });
                }
                return value;
            },
            configurable: true,
            enumerable: true,
        });
    };
}

export function createDecorator(container: Container) {
    return function(type: symbol, ...args: symbol[]) {
        return inject(type, container, args);
    };
}

The decorator is used like this:

class Example {
    @inject(symbol)
    readonly service!: Interface;
}

Source: https://github.com/owja/ioc/blob/master/src/ioc/inject.ts

@littledan
Copy link
Member Author

These use cases are great! Keep them coming!

If someone wants to try to write up these decorators in terms of the new decorators proposal, I would be very grateful.

@pzuraq pzuraq added the info label Mar 27, 2022
@trusktr
Copy link
Contributor

trusktr commented Sep 16, 2022

One of my main use cases is reactive properties (classy-solid which uses the reactive primitives from Solid.js).

The only way I can think of so far to intercept class fields (f.e. to make them "reactive" which involves needing to make a getters and setters for them), without using accessor, is with a two step process (similar to what @bodograumann mentioned above) involving both a field decorator, and a class decorator.

Something like this (EDIT: the following is broken, not working with more than one class, see fixed version below):

const propsToSignalify = new Map()

export function signal(...args) {
	const [_, {kind, name}] = args

	if (kind !== 'field') new TypeError('not field')

	return function (initialValue) {
		propsToSignalify.set(name, {name, initialValue})
		return initialValue
	}
}

export function reactive(...args) {
	const [value, {kind, name}] = args

	if (kind !== 'class') throw new TypeError('not class')
	
	return class Reactive extends value {
		constructor(...args) {
			super(...args)

			for (const [name, {initialValue}] of propsToSignalify) {
				// This replaces the class field descriptor with a new one.
				makePropertyReactiveWithGetterSetter(this, name, initialValue)
			}

			propsToSignalify.clear()
		}
	}
}
import {reactive, signal, effect} from 'reactive-lib'

@reactive
class Foo {
  @signal foo = 123
}

@reactive
class Bar extends Foo {
  @signal bar = 456
}

const b = new Bar

setInterval(() => {
  b.foo++
  b.bar++
})

// Each of these logs re-run any time b.foo or b.bar change.
effect(() => console.log(b.foo))
effect(() => console.log(b.bar))

where imagination has to be used for the makePropertyReactiveWithGetterSetter function that will create the new getters/setters for the properties.

The main problem with this approach is that the user must not forget to use @reactive on the class that will have reactive properties. Otherwise, signalProps will have invalid outdated information for the next class that is decorated with @reactive.

In previous decorators spec, a class finisher along with access to the class in field decorators prevented the need for the extra signalProps that needs to be cleared for each class.

Is there a better way?


Here's what it looks like with several properties in one class:

@reactive
class Ipsum extends Lorem {
  @signal foo = 456
  @signal bar = 456
  @signal baz = 456
}

Using accessor would prevent the need for the class decoration, but then each field is more verbose and less semantic to achieve the same:

// ... re-make function reactive to work on accessor fields...

class Ipsum extends Lorem {
  @signal accessor foo = 456
  @signal accessor bar = 456
  @signal accessor baz = 456
}

But apart from being human-forgettable, I like the class decorator more than accessor. From the end user perspective, why should they need to think about an accessor. They just want a reactive property.

Or maybe "signal accessor" is actually more semantic in this particular naming scheme, because the properties "access signals" underneath. I still think I like the more concise one with class decoration (it makes a difference with even more props).

@pzuraq
Copy link
Collaborator

pzuraq commented Sep 16, 2022

@trusktr there is not another way, and this is by design. Using accessor is the correct and only way to intercept access to fields with the current proposal. This design is the culmination of over a year of deliberation and exploration, in which we explored all the possible alternatives. This is the best solution which solves all of the various constraints.

@trusktr
Copy link
Contributor

trusktr commented Sep 17, 2022

What I'm doing works, but I suppose it means it creates field descriptors twice, so less instantiation performance. I think I'll support both, so people can pick to use accessor without the class decorator if they wish.

Nvm, my approach above totally fell apart with more than one class (brain fart, I thought the Reactive subclass happened at class define time, like a class finisher, doh). Hmmm.

@trusktr
Copy link
Contributor

trusktr commented Sep 17, 2022

This uses a two step process of defining metadata via method decorators and then creating a proxy class with the @Module class decorator.

It looks like this is no longer possible with Stage 3 decorators. Have you given it a try yet?

@trusktr
Copy link
Contributor

trusktr commented Sep 17, 2022

Turns out it is possible with a little dancing🕺.

Here's my previous example fixed, with helpful errors in case the class decorator is forgotten:

const propsToSignalify = new Map()
const classFinishers = []

export function signal(...args) {
	const [_, {kind, name}] = args

	let props = propsToSignalify
	classFinishers.push(propsToSignalify => (props = propsToSignalify))

	if (kind !== 'field') new TypeError('not field')

	props.set(name, {initialValue: undefined})

	return function (initialValue) {
		props.get(name)!.initialValue = initialValue
		return initialValue
	}

	queueReactiveDecoratorChecker()
}

const hasOwnProperty = Object.prototype.hasOwnProperty

export function reactive(...args) {
	const [value, {kind, name}] = args

	if (kind !== 'class') throw new TypeError('not class')

	const props = new Map(propsToSignalify)
	propsToSignalify.clear()

	for (const finisher of classFinishers) finisher(props)
	classFinishers.length = 0
	
	return class Reactive extends value {
		constructor(...args) {
			super(...args)

			for (const [name, {initialValue}] of propsToSignalify) {
				if (!(hasOwnProperty.call(this, prop) || hasOwnProperty.call((value as Constructor).prototype, prop))) {
					throw new Error(
						`Property "${prop.toString()}" not found on object. Did you forget to use the \`@reactive\` decorator on a class that has properties decorated with \`@signal\`?`,
					)
				}

				// This replaces the class field descriptor with a new one using a Solid signal.
				makePropertyReactive(this, name, initialValue)
			}
		}
	}
}

let checkerQueued = false

function queueReactiveDecoratorChecker() {
	if (checkerQueued) return
	checkerQueued = true

	queueMicrotask(() => {
		checkerQueued = false

		if (propsToSignalify.size) {
			throw new Error(
				`Stray @signal-decorated properties detected: ${[...propsToSignalify.keys()].join(
					', ',
				)}. Did you forget to use the \`@reactive\` decorator on a class that has properties decorated with \`@signal\`?`,
			)
		}
	})
}

And we can further add support for accessor and getter/setters, and private/static.

Usage:

@reactive
class Foo {
  @signal foo = 123
}

// oops, no @reactive, will throw an error
class Bar extends Foo {
  @signal bar = 456
}

This sort of decoration will cost the extra descriptor defines, and is maybe only ever-so-slightly slower than a pattern like:

class Foo {
  foo = 1
  bar = 2
  constructor() {
    signalify(this, 'foo', 'bar')
    // or signalify(this) // signalify all properties
  }
}

@pzuraq
Copy link
Collaborator

pzuraq commented Sep 17, 2022

To clarify, I meant that accessor is the only way to intercept access with a single decorator.

I would avoid the pattern you have here of defining getters and setters on the instance. This was actually considered as possible behavior for class field decorators, but the issue is that accessors on the instance can never be inlined, that can only happen to accessors defined on a prototype. At scale, this will result in significantly worse performance compared to using accessor.

@trusktr
Copy link
Contributor

trusktr commented Oct 7, 2022

I think it will be fine though, the performance may be worse, but in most cases not human-time perceivable. I'm using this pattern with 3D rendering and it has been fine (typically not creating objects over and over, but keeping a set of objects alive over time and toggling features like visibility instead, etc).

I think this,

@reactive class Foo {
  @signal foo = 1
  @signal bar = 1
  @signal baz = 1
  @signal lorem = 1
  @signal ipsum = 1
}

is cleaner than

class Foo {
  @signal accessor foo = 1
  @signal accessor bar = 1
  @signal accessor baz = 1
  @signal accessor lorem = 1
  @signal accessor ipsum = 1
}

because the accessor sort of detracts from the stated intent (we just want a signal property, we don't necessarily care about the propertys' implementation details).

I mean, yeah, implementation wise, for someone who knows how the implementation of the decorators work, the accessor implementation is cleaner, but for an end user who doesn't care and just wants to write final code, the usage without accessor is cleaner, I think.

@justinfagnani
Copy link

I think it's probably bad to train users that this works and confuse them as to when they need to use accessor or not.

@pzuraq
Copy link
Collaborator

pzuraq commented Oct 7, 2022

@trusktr

but in most cases not human-time perceivable

I have been able to shave multiple seconds off of initial render time with these types of optimizations in complex applications at scale. For small apps it likely will not have a major impact, but anything beyond a trivial size will benefit significantly from inlining.

@trusktr
Copy link
Contributor

trusktr commented Nov 12, 2022

I think it's probably bad to train users that this works and confuse them as to when they need to use accessor or not.

It is not necessarily bad: the lib's documentation is responsible for telling users how to use the decorators. If the lib has bad docs, then yeah.

@trusktr
Copy link
Contributor

trusktr commented Nov 12, 2022

@trusktr

but in most cases not human-time perceivable

I have been able to shave multiple seconds off of initial render time with these types of optimizations in complex applications at scale. For small apps it likely will not have a major impact, but anything beyond a trivial size will benefit significantly from inlining.

It would be interesting to see an example. You must be talking about a big app with tons of decorators.

@pzuraq
Copy link
Collaborator

pzuraq commented Nov 12, 2022

The app was linkedin.com. It was written in Ember.js, which was undergoing a rewrite to its main rendering engine, Glimmer. The rewrite was initially a significant regression, but we were able to optimize until it was about the same as the original.

The single biggest optimization we did was monomorphising core classes (e.g. taking an interface that was implemented as many classes and turning it into a single class with a shape that was predictable to the engine). Monomorphic classes are very useful to the engine because it allows them to inline calls to methods on those classes, which means calling the method requires far less CPU time. This optimization saved several seconds on the initial render of linkedin.com (p90), according to our benchmarks.

Relevant PR with benchmarks for a much smaller and significantly less complicated opensource app (emberobserver.com): emberjs/ember.js#18223

When an getter/setter is defined directly on an instance of a class, rather than the prototype, it can never benefit from this type of optimization. This was a key discussion point in designs for decorators. At one point, I suggested that field decorators could intercept the [[Define]] call during initialization and install accessors on the instance. This was rejected for this reason around performance.

@endel
Copy link

endel commented Jan 30, 2024

I'm having a hard time now that class field decorators (and field access decorators) don't have access to the class reference anymore. I used to keep track of the "index" of each decorated field on a custom serializer, e.g.:

export class Player extends Schema {
  @type("string") name: string; // index 0
  @type("number") x: number;  // index 1
  @type("number") y: number;  // index 2
}

Using legacy decorators, it was possible to keep track of field indexes by doing this: (this is a simplification of what I have on colyseus/schema for demonstration)

export function type (type) {
    return function (target, field) {
      const ctor = target.constructor;
      if (!ctor._definition) { 
        ctor._definition = {
          fields: [],
          descriptors: {},
        }; 
      }

      ctor._fields.push(field);

      const fieldCached = `_${field}`;

      // define property descriptors (they're attached at the `constructor` of the base Schema class)
      ctor._definition.descriptors[field] = {
        get: function () { return this[fieldCached]; },
        set: function (value) {
          const index = ctor._fields.indexOf(field)
          // perform action with "index"
          this[fieldCached] = value;
        },
        enumerable: true,
        configurable: true
      };
    }
}

This might not be the right place to look for guidance, but I'd appreciate it if any of you could suggest an approach using the new decorators API.

@littledan
Copy link
Member Author

@endel Maybe you could store this counter in the metadata?

@endel
Copy link

endel commented Jan 30, 2024

Thank you for your swift response @littledan. Indeed metadata is exactly what I need 🙃... Just realized context.metadata is undefined by default on TypeScript unless Symbol.metadata is defined/polyfilled 🙌

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

No branches or pull requests