ECMAScript icon indicating copy to clipboard operation
ECMAScript copied to clipboard

generate strong types for engine signals and connections

Open MichaelBelousov opened this issue 4 years ago • 4 comments

is there any interest in generating strong types for connections such as:

(this is probably a poor example, I'd need to dig in and make a PR to do it correctly)

declare module godot {
  interface SceneTree implements MainLoop {
    //overload 1 for network_peer_connected signal
    function connect<T extends Object>(
      signal: "network_peer_connected",
      target: T,
      method: Extract<T, Function> | (...args: Parameters<onNetworkPeerConnected>) => void,  //could use e.g. MethodOf<T>,
      binds: Tail<Parameters<onNetworkPeerConnected>>,
      flags: number = 0,
    ): number;
    // more overloads, one per signal for discriminating union
  }
}

MichaelBelousov avatar Jul 18 '20 20:07 MichaelBelousov

another place that stronger inference would generate useful tooling: although I would need to research if it's even possible.

function rpc_id<T extends {} = {}, MethodKey extends key of T, Method = T[]>(id: number, method: MethodKey, ...args: Parameters<Method>): any;

MichaelBelousov avatar Jul 18 '20 20:07 MichaelBelousov

In godot signal just a string constant. That seems a lot of work to do but gains no difference in JavaScript.

Geequlim avatar Jul 19 '20 03:07 Geequlim

I'm just personally a big fan of strong typing in typescript, and it offers better intellisense/tooling, such as having a type error if you pass arguments that don't correspond to the connected signal. I can look into it and offer a PR and better proof of concept

i.e.

this.get_tree().connect("network_peer_connected", (id: string, otherArg: number) => {}, "other");

that will be a type error because the id argument should be a number, and a string was passed to the otherArg when a number is required.

MichaelBelousov avatar Jul 19 '20 03:07 MichaelBelousov

This is an interesting and useful suggestion. I was able to implement such a thing for C# using godot4-like signal declaration. For now, I am on the way to implementing this for our project (and it goes much much simpler). There is a code snippet of how this can work in typescript:


// This part should be defined somewhere in the ECMAScript
// The implementation of `emit` and `connect` is omitted to keep things simple
// In real Life each signal instance should keep owenr_id and name (which is set by
// ECMAscript when object instantiating) and bypass connection to singleton proxy Object
// ProxyObject well receive all Godot signals and additional argument bypassed
// by Signal instance - `callback_id` and then calls exact function.

// Godot signal are generated as properties which create instances of Signal
// on demand.
type VarArgs = readonly unknown[];
type Action<T extends VarArgs> = (...args: T) => void; 
class Signal<T extends VarArgs> implements PromiseLike<T> {
    object_id: Number = 0
    name: String = ""

    // showcase
    callbacks:Action<T>[] = []

    public emit(...args: T) {
        // something from real world:
        // godot.instance_from_id(this.instance_id).emit(this.name, ...args)
        

        // just for showcase
        for (const cb of this.callbacks) {
            cb(...args)
        }
    }

    public connect(cb: Action<T>) {
        // real world:
        // let obj = godot.instance_from_id(this.instance_id)
        // let callback_id = Proxy.get_instance().register_callback(cb)
        // obj.connect(this.name, Proxy.get_instance(), "handle_signal", [callback_id])

        // just for showcase
        this.callbacks.push(cb);
    }

    // this is required for awaiting directly for signals
    public then<TResult1 = T>(onfulfilled?: ((value: T) => (PromiseLike<TResult1> | TResult1)) | undefined | null): PromiseLike<TResult1> {
        return new Promise<TResult1>( (resolve, reject) => {
            this.connect((...args: T) => {
                if (typeof onfulfilled === 'function') {
                    resolve(onfulfilled(args))
                } else {
                    // exception?
                    reject("Don't know how to complete without onfulfilled");
                }
            })
        })
    }
}

// the rest part is the usage example
class Obj  /* extends godot.Object */ {

    // @godot.typedsignal - for registering signal nad providing name and owner_id (or maybe generating property)
    pressed: Signal<[]> = new Signal();
    input: Signal<[name: String]> = new Signal();
    selected: Signal<[name: String, age: Number]> = new Signal();

    public async ready() {
        console.log("in ready")
        this.input.connect(this.handle_input) // <= everything typed as excpected
        this.pressed.connect(() => console.log("pressed"))
        this.input.connect((name) => console.log("input", name))
        this.selected.connect((name, age) => console.log("selected", name, age))
        
        // faking signals for showcase
        setTimeout(() => this.pressed.emit(), 100)
        setTimeout(() => this.input.emit("jhj"), 200);
        setTimeout(() => this.selected.emit("vasia", 21), 300);

        console.log("before handle")
        await this.handle()
        console.log("after handle")
    }

    public handle_input(name: String) {
        console.log("Handling input with handle_input method", name)
    }

    public async handle(a: String = "" ) {
        console.log("in handle")
        await this.pressed
        console.log("after pressed")
        let [key] = await this.input; // key is str
        console.log("after input", key)
        let [name, age] = await this.selected; // name is str, age is num
        console.log("after selected", name, age)
    }
}

let n = new Obj()
n.ready()

/* Output:
[LOG]: "in ready" 
[LOG]: "before handle" 
[LOG]: "in handle" 
[LOG]: "pressed" 
[LOG]: "after pressed" 
[LOG]: "Handling input with handle_input method",  "jhj" 
[LOG]: "input",  "jhj" 
[LOG]: "after input",  "jhj" 
[LOG]: "selected",  "vasia",  21 
[LOG]: "after selected",  "vasia",  21 
[LOG]: "after handle" 
*/

This approach has some memory overhead but adding a lot of impact and stability. It is also fully compatible with Godot API, e.g. I am able to emit a signal from the engine using old good emit_signal and so on.

There is Playground Link

jkb0o avatar May 06 '21 13:05 jkb0o