composition-go/vm_v8.go (272 lines of code) (raw):

//go:build wg_composition_v8 package composition import ( "fmt" "os" "reflect" v8 "rogchap.com/v8go" ) func deserializeArray[T any](arrayObject *v8.Object, elementDecoder func(*v8.Object) (T, error)) ([]T, error) { lengthValue, err := arrayObject.Get("length") if err != nil { return nil, err } length := lengthValue.Integer() result := make([]T, length) for ii := 0; ii < int(length); ii++ { elementValue, err := arrayObject.GetIdx(uint32(ii)) if err != nil { return nil, err } elementObject := elementValue.Object() element, err := elementDecoder(elementObject) if err != nil { return nil, err } result[ii] = element } return result, nil } // serializes an object for v8 using reflect and the goja // tags for field names func serializeGojaObject(ctx *v8.Context, v any) (*v8.Value, error) { rv := reflect.ValueOf(v) for rv.Kind() == reflect.Ptr { rv = rv.Elem() } rt := rv.Type() switch rt.Kind() { case reflect.Struct: tmpl := v8.NewObjectTemplate(ctx.Isolate()) obj, err := tmpl.NewInstance(ctx) if err != nil { return nil, err } for ii := 0; ii < rt.NumField(); ii++ { field := rt.Field(ii) tag := field.Tag.Get("goja") if tag == "" { return nil, fmt.Errorf("no goja field tag in field %s", field.Name) } value := rv.Field(ii).Interface() valueObject, err := serializeGojaObject(ctx, value) if err != nil { return nil, err } obj.Set(tag, valueObject) } return obj.Value, nil case reflect.String: return v8.NewValue(ctx.Isolate(), rv.String()) default: return nil, fmt.Errorf("cannot serialize type kind %s", rt.Kind()) } } type v8Vm struct { isolate *v8.Isolate ctx *v8.Context null v8.Valuer federateSubgraphs *v8.Function buildRouterConfiguration *v8.Function } func (m *v8Vm) Dispose() { m.isolate.Dispose() } func (m *v8Vm) subgraphsToJS(subgraphs []*Subgraph) (*v8.Object, error) { tmpl := v8.NewObjectTemplate(m.ctx.Isolate()) arrayValue, err := m.ctx.RunScript("[]", "array") if err != nil { return nil, err } arrayObject := arrayValue.Object() arrayObject.Set("length", len(subgraphs)) for ii := range subgraphs { obj, err := tmpl.NewInstance(m.ctx) if err != nil { return nil, err } obj.Set("name", subgraphs[ii].Name) obj.Set("url", subgraphs[ii].URL) obj.Set("schema", subgraphs[ii].Schema) arrayObject.SetIdx(uint32(ii), obj) } return arrayObject, nil } func (m *v8Vm) FederateSubgraphs(subgraphs []*Subgraph) (*FederatedGraph, error) { arrayObject, err := m.subgraphsToJS(subgraphs) if err != nil { return nil, err } result, err := m.federateSubgraphs.Call(m.null, arrayObject) if err != nil { return nil, err } resultObject := result.Object() fieldConfigurationsValue, err := resultObject.Get("fieldConfigurations") if err != nil { return nil, err } fieldConfigurationsObject := fieldConfigurationsValue.Object() fieldConfigurations, err := deserializeArray(fieldConfigurationsObject, func(o *v8.Object) (*FieldConfiguration, error) { argumentNamesValue, err := o.Get("argumentNames") if err != nil { return nil, err } argumentNames, err := deserializeArray(argumentNamesValue.Object(), func(o *v8.Object) (string, error) { return o.String(), nil }) if err != nil { return nil, err } fieldNameValue, err := o.Get("fieldName") if err != nil { return nil, err } typeNameValue, err := o.Get("typeName") if err != nil { return nil, err } requiresAuthenticationValue, _ := o.Get("requiresAuthentication") requiredScopesValue, _ := o.Get("requiredScopes") fieldConfiguration := &FieldConfiguration{ ArgumentNames: argumentNames, FieldName: fieldNameValue.String(), TypeName: typeNameValue.String(), } if requiresAuthenticationValue.IsBoolean() { fieldConfiguration.RequiresAuthentication = requiresAuthenticationValue.Boolean() } if requiredScopesValue.IsArray() { requiredScopes, err := deserializeArray(requiredScopesValue.Object(), func(orScopes *v8.Object) ([]string, error) { deserializedAndScopes, err := deserializeArray(orScopes, func(andScopes *v8.Object) (string, error) { return andScopes.String(), nil }) if err != nil { return nil, err } return deserializedAndScopes, nil }) if err != nil { return nil, err } fieldConfiguration.RequiredScopes = requiredScopes } return fieldConfiguration, nil }) if err != nil { return nil, err } sdl, err := resultObject.Get("sdl") if err != nil { return nil, err } return &FederatedGraph{ FieldConfigurations: fieldConfigurations, SDL: sdl.String(), }, nil } func (m *v8Vm) BuildRouterConfiguration(subgraphs []*Subgraph) (string, error) { arrayObject, err := m.subgraphsToJS(subgraphs) if err != nil { return "", err } result, err := m.buildRouterConfiguration.Call(m.null, arrayObject) if err != nil { return "", err } return result.String(), nil } func debugErr(err error) error { if os.Getenv("DEBUG_V8_ERRORS") != "" { if e, ok := err.(*v8.JSError); ok { fmt.Fprintln(os.Stderr, e.Message) fmt.Fprintln(os.Stderr, e.Location) fmt.Fprintln(os.Stderr, e.StackTrace) } } return err } func v8Exception(isolate *v8.Isolate, err error) { exc, err := v8.NewValue(isolate, err.Error()) if err != nil { panic(err) } isolate.ThrowException(exc) } func stringHashV8(info *v8.FunctionCallbackInfo) *v8.Value { isolate := info.Context().Isolate() input := info.Args()[0].String() result, err := stringHash(input) if err != nil { v8Exception(isolate, err) return nil } resultValue, err := v8.NewValue(isolate, result) if err != nil { panic(err) } return resultValue } func urlParseV8(info *v8.FunctionCallbackInfo) *v8.Value { isolate := info.Context().Isolate() args := info.Args() url := args[0].String() base := args[1].String() result, err := urlParse(url, base) if err != nil { v8Exception(isolate, err) return nil } value, err := serializeGojaObject(info.Context(), result) if err != nil { panic(err) } return value } func newVM() (*v8Vm, error) { isolate := v8.NewIsolate() global := v8.NewObjectTemplate(isolate) stringHash := v8.NewFunctionTemplate(isolate, stringHashV8) if err := global.Set("stringHash", stringHash, v8.ReadOnly); err != nil { return nil, err } urlParse := v8.NewFunctionTemplate(isolate, urlParseV8) if err := global.Set("urlParse", urlParse, v8.ReadOnly); err != nil { return nil, err } ctx := v8.NewContext(isolate, global) if _, err := ctx.RunScript(jsPrelude, "prelude.js"); err != nil { return nil, fmt.Errorf("error running prelude: %w", debugErr(err)) } if _, err := ctx.RunScript(indexJs, "shim.js"); err != nil { return nil, fmt.Errorf("error loading shim: %w", debugErr(err)) } shim, err := ctx.Global().Get("shim") if err != nil { return nil, fmt.Errorf("error retrieving shim: %w", debugErr(err)) } shimFunc := func(name string) (*v8.Function, error) { fpv, err := shim.Object().Get(name) if err != nil { return nil, fmt.Errorf("error retrieving shim function %s: %w", name, debugErr(err)) } fp, err := fpv.AsFunction() if err != nil { return nil, fmt.Errorf("error converting shim function to JS function %s: %w", name, debugErr(err)) } return fp, nil } federateSubgraphs, err := shimFunc("federateSubgraphs") if err != nil { return nil, err } buildRouterConfiguration, err := shimFunc("buildRouterConfiguration") if err != nil { return nil, err } return &v8Vm{ isolate: isolate, ctx: ctx, null: v8.Null(ctx.Isolate()), federateSubgraphs: federateSubgraphs, buildRouterConfiguration: buildRouterConfiguration, }, nil } type vm = v8Vm