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

Lazyloading properties with es6 proxy #149

Closed
tarb opened this issue May 3, 2020 · 3 comments
Closed

Lazyloading properties with es6 proxy #149

tarb opened this issue May 3, 2020 · 3 comments

Comments

@tarb
Copy link

tarb commented May 3, 2020

Im having trouble getting lazyloading working with the new es6 proxy, encountering the TypeError: get needs to be called with target and receiver as Object and property as string at stringify (native) error.

Below is a mini example showing the approach im taking. I have partial functionality, but cant get nested values to work.

package main

import (
	"fmt"
	"sort"

	"github.com/dop251/goja"
)

//
type Lazy struct {
	f1 float64
	f2 float64
}

func main() {
	rt := goja.New()
	rt.Set("lazy", newObj1(rt, &Lazy{5.0, 13.0}))

	v, err := rt.RunString(`
		JSON.stringify(lazy.f1);
	`)
	fmt.Println(v, err) // 5 <nil>

	v, err = rt.RunString(`
		JSON.stringify(lazy); 
	`)
	fmt.Println(v, err) // <nil> TypeError: get needs to be called with target and receiver as Object and property as string at stringify (native)

	v, err = rt.RunString(`
		lazy.nested; 
	`)
	fmt.Println(v, err) // %!v(PANIC=String method: TypeError: get needs to be called with target and receiver as Object and property as string) <nil>

}

// obj1
func newObj1(rt *goja.Runtime, l *Lazy) goja.Value {
	keys := []string{"add", "f1", "f2", "nested"}
	fields := rt.ToValue(keys).ToObject(rt)

	pcfg := goja.ProxyTrapConfig{
		OwnKeys: func(target *goja.Object) *goja.Object { return fields },
		Has:     func(target *goja.Object, prop string) (available bool) { return sort.SearchStrings(keys, prop) != -1 },
		Get: func(target *goja.Object, prop string, r *goja.Object) goja.Value {
			return obj1Vals(rt, l, target, prop)
		},
		GetOwnPropertyDescriptor: func(target *goja.Object, prop string) (desc goja.PropertyDescriptor) {
			desc.Enumerable, desc.Configurable = goja.FLAG_TRUE, goja.FLAG_TRUE
			desc.Value = obj1Vals(rt, l, target, prop)
			return desc
		},
	}

	return rt.ToValue(rt.NewProxy(rt.NewObject(), &pcfg))
}

func obj1Vals(rt *goja.Runtime, l *Lazy, target *goja.Object, prop string) goja.Value {
	v := target.Get(prop)
	if v != nil {
		return v
	}

	switch prop {
	case "f1":
		v = rt.ToValue(l.f1)
	case "f2":
		v = rt.ToValue(l.f2)
	case "add":
		v = rt.ToValue(func(call goja.FunctionCall) goja.Value {
			if len(call.Arguments) == 0 {
				return goja.Undefined()
			}
			return rt.ToValue(call.Arguments[0].ToInteger() + call.Arguments[1].ToInteger())
		})
	case "nested":
		v = newObj2(rt, l)
	default:
		return goja.Undefined()
	}

	target.Set(prop, v)
	return v
}

// obj 2 - the same as above without the nesting
func newObj2(rt *goja.Runtime, l *Lazy) goja.Value {
	keys := []string{"add", "f1", "f2"}
	fields := rt.ToValue(keys).ToObject(rt)

	pcfg := goja.ProxyTrapConfig{
		OwnKeys: func(target *goja.Object) *goja.Object { return fields },
		Has:     func(target *goja.Object, prop string) (available bool) { return sort.SearchStrings(keys, prop) != -1 },
		Get: func(target *goja.Object, prop string, r *goja.Object) goja.Value {
			return obj2Vals(rt, l, target, prop)
		},
		GetOwnPropertyDescriptor: func(target *goja.Object, prop string) (desc goja.PropertyDescriptor) {
			desc.Enumerable, desc.Configurable = goja.FLAG_TRUE, goja.FLAG_TRUE
			desc.Value = obj2Vals(rt, l, target, prop)
			return desc
		},
	}

	return rt.ToValue(rt.NewProxy(rt.NewObject(), &pcfg))
}

func obj2Vals(rt *goja.Runtime, l *Lazy, target *goja.Object, prop string) goja.Value {
	v := target.Get(prop)
	if v != nil {
		return v
	}

	switch prop {
	case "f1":
		v = rt.ToValue(l.f1)
	case "f2":
		v = rt.ToValue(l.f2)
	case "add":
		v = rt.ToValue(func(call goja.FunctionCall) goja.Value {
			if len(call.Arguments) == 0 {
				return goja.Undefined()
			}
			return rt.ToValue(call.Arguments[0].ToInteger() + call.Arguments[1].ToInteger())
		})
	default:
		return goja.Undefined()
	}

	target.Set(prop, v)
	return v
}
@tarb
Copy link
Author

tarb commented May 4, 2020

Thanks for the fix, JSON.stringify works now, but still having problems with accessing nested properties which are proxies,

v, err = rt.RunString(`
	lazy.nested;
`)
fmt.Println(v, err) // %!v(PANIC=String method: TypeError: get needs to be called with target and receiver as Object and property as string) <nil>

still fails with the same error, hopefully another quick fix

@dop251
Copy link
Owner

dop251 commented May 4, 2020

This time it's a bit more complicated. It's to do with Symbol properties. Println() calls String() which tries to get Symbol.toPrimitive. I can't think of a better solution other than disabling Symbol properties support for 'native' proxy handlers. Otherwise not only I'd have to expose Symbol (which I'll probably do at some point anyway), but also to change signatures of the 'native' trap functions which makes their implementation more complicated.

By 'disabling' I mean get* would always return undefined and has would always return false for Symbol properties. set and defineOwnProperty would throw TypeError.

@tarb
Copy link
Author

tarb commented May 5, 2020

Thanks mate, this is all fixed up for me. Appreciate the hard work creating and maintaining this. Im seeing the expected performance improvements from es6-proxy that I was hoping for

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

No branches or pull requests

2 participants