From 58dab5bf7036b850642fb35b1cc57700c08cea4e Mon Sep 17 00:00:00 2001 From: Gani Georgiev Date: Thu, 6 Mar 2025 21:26:57 +0200 Subject: [PATCH] restored DynamicModel types cache --- plugins/jsvm/binds.go | 87 +++++++++++++++++++++++--------------- plugins/jsvm/binds_test.go | 25 +++++++++-- 2 files changed, 75 insertions(+), 37 deletions(-) diff --git a/plugins/jsvm/binds.go b/plugins/jsvm/binds.go index d63a8e08..ee4f1d0e 100644 --- a/plugins/jsvm/binds.go +++ b/plugins/jsvm/binds.go @@ -13,6 +13,7 @@ import ( "path/filepath" "reflect" "slices" + "sort" "strings" "time" @@ -1020,9 +1021,19 @@ func structConstructorUnmarshal(vm *goja.Runtime, call goja.ConstructorCall, ins return instanceValue } +var cachedDynamicModelStructs = store.New[string, reflect.Type](nil) + // newDynamicModel creates a new dynamic struct with fields based // on the specified "shape". // +// The "shape" values are used as defaults and could be of type: +// - int (ex. 0) +// - float (ex. -0) +// - string (ex. "") +// - bool (ex. false) +// - slice (ex. []) +// - map (ex. map[string]any{}) +// // Example: // // m := newDynamicModel(map[string]any{ @@ -1030,37 +1041,21 @@ func structConstructorUnmarshal(vm *goja.Runtime, call goja.ConstructorCall, ins // "total": 0, // }) func newDynamicModel(shape map[string]any) any { - modelType := getDynamicModelStruct(shape) + info := make([]*shapeFieldInfo, 0, len(shape)) - rvShapeValues := make([]reflect.Value, len(modelType.shapeValues)) - for i, v := range modelType.shapeValues { - rvShapeValues[i] = reflect.ValueOf(v) + var hash strings.Builder + + sortedKeys := make([]string, 0, len(shape)) + for k := range shape { + sortedKeys = append(sortedKeys, k) } + sort.Strings(sortedKeys) - elem := reflect.New(modelType.structType).Elem() - - for i, v := range rvShapeValues { - elem.Field(i).Set(v) - } - - return elem.Addr().Interface() -} - -type dynamicModelType struct { - structType reflect.Type - shapeValues []any -} - -func getDynamicModelStruct(shape map[string]any) *dynamicModelType { - result := new(dynamicModelType) - result.shapeValues = make([]any, 0, len(shape)) - - structFields := make([]reflect.StructField, 0, len(shape)) - - for k, v := range shape { + for _, k := range sortedKeys { + v := shape[k] vt := reflect.TypeOf(v) - switch kind := vt.Kind(); kind { + switch vt.Kind() { case reflect.Map: raw, _ := json.Marshal(v) newV := types.JSONMap[any]{} @@ -1075,16 +1070,40 @@ func getDynamicModelStruct(shape map[string]any) *dynamicModelType { vt = reflect.TypeOf(newV) } - result.shapeValues = append(result.shapeValues, v) + hash.WriteString(k) + hash.WriteString(":") + hash.WriteString(vt.String()) // it doesn't guarantee to be unique across all types but it should be fine with the primitive types DynamicModel is used + hash.WriteString("|") - structFields = append(structFields, reflect.StructField{ - Name: inflector.UcFirst(k), // ensures that the field is exportable - Type: vt, - Tag: reflect.StructTag(`db:"` + k + `" json:"` + k + `" form:"` + k + `"`), - }) + info = append(info, &shapeFieldInfo{key: k, value: v, valueType: vt}) } - result.structType = reflect.StructOf(structFields) + st := cachedDynamicModelStructs.GetOrSet(hash.String(), func() reflect.Type { + structFields := make([]reflect.StructField, len(info)) - return result + for i, item := range info { + structFields[i] = reflect.StructField{ + Name: inflector.UcFirst(item.key), // ensures that the field is exportable + Type: item.valueType, + Tag: reflect.StructTag(`db:"` + item.key + `" json:"` + item.key + `" form:"` + item.key + `"`), + } + } + + return reflect.StructOf(structFields) + }) + + elem := reflect.New(st).Elem() + + // load default values into the new model + for i, item := range info { + elem.Field(i).Set(reflect.ValueOf(item.value)) + } + + return elem.Addr().Interface() +} + +type shapeFieldInfo struct { + value any + valueType reflect.Type + key string } diff --git a/plugins/jsvm/binds_test.go b/plugins/jsvm/binds_test.go index e4f23f69..9c59ffdf 100644 --- a/plugins/jsvm/binds_test.go +++ b/plugins/jsvm/binds_test.go @@ -1137,7 +1137,6 @@ func TestLoadingDynamicModel(t *testing.T) { } } -// @todo revert the reflect caching and check other types func TestDynamicModelMapFieldCaching(t *testing.T) { app, _ := tests.NewTestApp() defer app.Cleanup() @@ -1149,24 +1148,44 @@ func TestDynamicModelMapFieldCaching(t *testing.T) { _, err := vm.RunString(` let m1 = new DynamicModel({ + int: 0, + float: -0, + text: "", + bool: false, obj: {}, + arr: [], }) let m2 = new DynamicModel({ + int: 0, + float: -0, + text: "", + bool: false, obj: {}, + arr: [], }) + m1.int = 1 + m1.float = 1.5 + m1.text = "a" + m1.bool = true m1.obj.set("a", 1) + m1.arr.push(1) + m2.int = 2 + m2.float = 2.5 + m2.text = "b" + m2.bool = false m2.obj.set("b", 1) + m2.arr.push(2) - let m1Expected = '{"obj":{"a":1}}'; + let m1Expected = '{"arr":[1],"bool":true,"float":1.5,"int":1,"obj":{"a":1},"text":"a"}'; let m1Serialized = JSON.stringify(m1); if (m1Serialized != m1Expected) { throw new Error("Expected m1 \n" + m1Expected + "\ngot\n" + m1Serialized); } - let m2Expected = '{"obj":{"b":1}}'; + let m2Expected = '{"arr":[2],"bool":false,"float":2.5,"int":2,"obj":{"b":1},"text":"b"}'; let m2Serialized = JSON.stringify(m2); if (m2Serialized != m2Expected) { throw new Error("Expected m2 \n" + m2Expected + "\ngot\n" + m2Serialized);