From bccb8e7a6195da67ac423bca06274ab11c5fa26e Mon Sep 17 00:00:00 2001 From: Simon Esposito Date: Mon, 6 Jan 2025 14:39:43 +0000 Subject: [PATCH 1/3] JavaScript runtime improvements. Allow match state to be a class instance with functions - previously the object would be stripped of it's methods. Use JS runtime JSON unmarshal instead of Go to prevent arrays to behave in unexpected ways when modified - this is a goja quirk, see: https://github.com/dop251/goja/blob/master/runtime.go#L1529 (above ToValue definition). --- server/runtime_javascript.go | 45 +- server/runtime_javascript_match_core.go | 256 ++++------ server/runtime_javascript_nakama.go | 628 +++++++++++------------- 3 files changed, 412 insertions(+), 517 deletions(-) diff --git a/server/runtime_javascript.go b/server/runtime_javascript.go index 94cfaf0dd..913f9af92 100644 --- a/server/runtime_javascript.go +++ b/server/runtime_javascript.go @@ -1949,14 +1949,12 @@ func (rp *RuntimeProviderJS) TournamentEnd(ctx context.Context, tournament *api. _ = tournamentObj.Set("nextReset", tournament.NextReset) } _ = tournamentObj.Set("operator", strings.ToLower(tournament.Operator.String())) - metadataMap := make(map[string]interface{}) - err = json.Unmarshal([]byte(tournament.Metadata), &metadataMap) + metadata, err := jsJsonParse(r.vm, tournament.Metadata) if err != nil { rp.Put(r) return fmt.Errorf("failed to convert metadata to json: %s", err.Error()) } - pointerizeSlices(metadataMap) - _ = tournamentObj.Set("metadata", metadataMap) + _ = tournamentObj.Set("metadata", metadata) _ = tournamentObj.Set("createTime", tournament.CreateTime.Seconds) _ = tournamentObj.Set("startTime", tournament.StartTime.Seconds) if tournament.EndTime == nil { @@ -2026,14 +2024,13 @@ func (rp *RuntimeProviderJS) TournamentReset(ctx context.Context, tournament *ap _ = tournamentObj.Set("nextReset", tournament.NextReset) } _ = tournamentObj.Set("operator", strings.ToLower(tournament.Operator.String())) - metadataMap := make(map[string]interface{}) - err = json.Unmarshal([]byte(tournament.Metadata), &metadataMap) + metadata, err := jsJsonParse(r.vm, tournament.Metadata) if err != nil { rp.Put(r) return fmt.Errorf("failed to convert metadata to json: %s", err.Error()) } - pointerizeSlices(metadataMap) - _ = tournamentObj.Set("metadata", metadataMap) + + _ = tournamentObj.Set("metadata", metadata) _ = tournamentObj.Set("createTime", tournament.CreateTime.Seconds) _ = tournamentObj.Set("startTime", tournament.StartTime.Seconds) if tournament.EndTime == nil { @@ -2094,14 +2091,13 @@ func (rp *RuntimeProviderJS) LeaderboardReset(ctx context.Context, leaderboard * if leaderboard.NextReset != 0 { _ = leaderboardObj.Set("nextReset", leaderboard.NextReset) } - metadataMap := make(map[string]interface{}) - err = json.Unmarshal([]byte(leaderboard.Metadata), &metadataMap) + metadata, err := jsJsonParse(r.vm, leaderboard.Metadata) if err != nil { rp.Put(r) return fmt.Errorf("failed to convert metadata to json: %s", err.Error()) } - pointerizeSlices(metadataMap) - _ = leaderboardObj.Set("metadata", metadataMap) + + _ = leaderboardObj.Set("metadata", metadata) _ = leaderboardObj.Set("createTime", leaderboard.CreateTime) fn, ok := goja.AssertFunction(r.vm.Get(jsFn)) @@ -2369,29 +2365,28 @@ func (rp *RuntimeProviderJS) StorageIndexFilter(ctx context.Context, indexName s return false, errors.New("Could not run Storage Index Filter hook.") } - objectMap := make(map[string]interface{}, 7) - objectMap["key"] = storageWrite.Object.Key - objectMap["collection"] = storageWrite.Object.Collection + object := r.vm.NewObject() + _ = object.Set("key", storageWrite.Object.Key) + _ = object.Set("collection", storageWrite.Object.Collection) if storageWrite.OwnerID != "" { - objectMap["userId"] = storageWrite.OwnerID + _ = object.Set("userId", storageWrite.OwnerID) } else { - objectMap["userId"] = nil + _ = object.Set("userId", goja.Null()) } - objectMap["version"] = storageWrite.Object.Version - objectMap["permissionRead"] = storageWrite.Object.PermissionRead - objectMap["permissionWrite"] = storageWrite.Object.PermissionWrite + _ = object.Set("version", storageWrite.Object.Version) + _ = object.Set("permissionRead", storageWrite.Object.PermissionRead) + _ = object.Set("permissionWrite", storageWrite.Object.PermissionWrite) - valueMap := make(map[string]interface{}) - err = json.Unmarshal([]byte(storageWrite.Object.Value), &valueMap) + value, err := jsJsonParse(r.vm, storageWrite.Object.Value) if err != nil { return false, fmt.Errorf("Error running runtime Storage Index Filter hook for %q index: %v", indexName, err.Error()) } - pointerizeSlices(valueMap) - objectMap["value"] = valueMap + + _ = object.Set("value", value) ctx = NewRuntimeGoContext(ctx, r.node, r.version, r.envMap, RuntimeExecutionModeStorageIndexFilter, nil, nil, 0, "", "", nil, "", "", "", "") r.SetContext(ctx) - retValue, err, _ := r.InvokeFunction(RuntimeExecutionModeStorageIndexFilter, "storageIndexFilter", fn, jsLogger, nil, nil, "", "", nil, 0, "", "", "", "", r.vm.ToValue(objectMap)) + retValue, err, _ := r.InvokeFunction(RuntimeExecutionModeStorageIndexFilter, "storageIndexFilter", fn, jsLogger, nil, nil, "", "", nil, 0, "", "", "", "", object) r.SetContext(context.Background()) rp.Put(r) if err != nil { diff --git a/server/runtime_javascript_match_core.go b/server/runtime_javascript_match_core.go index 768839172..480360e77 100644 --- a/server/runtime_javascript_match_core.go +++ b/server/runtime_javascript_match_core.go @@ -197,21 +197,18 @@ func NewRuntimeJavascriptMatchCore(logger *zap.Logger, module string, db *sql.DB func (rm *RuntimeJavaScriptMatchCore) MatchInit(presenceList *MatchPresenceList, deferMessageFn RuntimeMatchDeferMessageFunction, params map[string]interface{}) (interface{}, int, error) { args := []goja.Value{rm.ctx, rm.loggerModule, rm.nakamaModule, rm.vm.ToValue(params)} - retVal, err := rm.initFn(goja.Null(), args...) + retVal, err := rm.initFn(goja.Undefined(), args...) if err != nil { return nil, 0, err } - retMap, ok := retVal.Export().(map[string]interface{}) - if !ok { - return nil, 0, errors.New("matchInit is expected to return an object with 'state', 'tickRate' and 'label' properties") - } - tickRateRet, ok := retMap["tickRate"] - if !ok { + retObj := retVal.ToObject(rm.vm) + tickRateRet := retObj.Get("tickRate") + if tickRateRet == nil { return nil, 0, errors.New("matchInit return value has no 'tickRate' property") } - rate, ok := tickRateRet.(int64) - if !ok { + rate := tickRateRet.ToInteger() + if rate == 0 { return nil, 0, errors.New("matchInit 'tickRate' must be a number between 1 and 60") } if rate < 1 || rate > 60 { @@ -220,20 +217,17 @@ func (rm *RuntimeJavaScriptMatchCore) MatchInit(presenceList *MatchPresenceList, rm.tickRate = int(rate) var label string - labelRet, ok := retMap["label"] - if ok { - label, ok = labelRet.(string) - if !ok { - return nil, 0, errors.New("matchInit 'label' value must be a string") + labelRet := retObj.Get("label") + if labelRet != nil { + label = labelRet.String() + if label == "" { + return nil, 0, errors.New("matchInit 'label' value must be a non-empty string") } } - state, ok := retMap["state"] - if !ok { - return nil, 0, errors.New("matchInit is expected to return an object with a 'state' property") - } + state := retObj.Get("state") if state == nil { - return nil, 0, ErrMatchInitStateNil + return nil, 0, errors.New("matchInit is expected to return an object with a 'state' property") } if err := rm.matchRegistry.UpdateMatchLabel(rm.id, rm.tickRate, rm.module, label, rm.createTime); err != nil { @@ -277,13 +271,9 @@ func (rm *RuntimeJavaScriptMatchCore) MatchJoinAttempt(tick int64, state interfa _ = ctxObj.Set(__RUNTIME_JAVASCRIPT_CTX_CLIENT_PORT, clientPort) } - pointerizeSlices(state) - stateObject := rm.vm.NewObject() - for k, v := range state.(map[string]any) { - _ = stateObject.Set(k, v) - } - args := []goja.Value{ctxObj, rm.loggerModule, rm.nakamaModule, rm.dispatcher, rm.vm.ToValue(tick), rm.vm.ToValue(stateObject), presenceObj, rm.vm.ToValue(metadata)} - retVal, err := rm.joinAttemptFn(goja.Null(), args...) + stateObj := state.(*goja.Object) + args := []goja.Value{ctxObj, rm.loggerModule, rm.nakamaModule, rm.dispatcher, rm.vm.ToValue(tick), stateObj, presenceObj, rm.vm.ToValue(metadata)} + retVal, err := rm.joinAttemptFn(goja.Undefined(), args...) if err != nil { return nil, false, "", err } @@ -292,36 +282,27 @@ func (rm *RuntimeJavaScriptMatchCore) MatchJoinAttempt(tick int64, state interfa return nil, false, "", nil } - retMap, ok := retVal.Export().(map[string]interface{}) - if !ok { + retObj := retVal.ToObject(rm.vm) + if retObj == nil { return nil, false, "", errors.New("matchJoinAttempt is expected to return an object with 'state' and 'accept' properties") } - allowRet, ok := retMap["accept"] - if !ok { + acceptVal := retObj.Get("accept") + if acceptVal == nil { return nil, false, "", errors.New("matchJoinAttempt return value has an 'accept' property") } - allow, ok := allowRet.(bool) - if !ok { - return nil, false, "", errors.New("matchJoinAttempt 'accept' property must be a boolean") - } var rejectMsg string + allow := acceptVal.ToBoolean() if !allow { - rejectMsgRet, ok := retMap["rejectMessage"] - if ok { - rejectMsg, ok = rejectMsgRet.(string) - if !ok { - return nil, false, "", errors.New("matchJoinAttempt 'rejectMessage' property must be a string") - } + rejectMsgRet := retObj.Get("rejectMessage") + if rejectMsgRet != nil { + rejectMsg = rejectMsgRet.String() } } - newState, ok := retMap["state"] - if !ok { - return nil, false, "", errors.New("matchJoinAttempt is expected to return an object with 'state' object property") - } - if _, ok = newState.(map[string]any); !ok { + newState := retObj.Get("state") + if newState == nil { return nil, false, "", errors.New("matchJoinAttempt is expected to return an object with 'state' object property") } @@ -329,25 +310,21 @@ func (rm *RuntimeJavaScriptMatchCore) MatchJoinAttempt(tick int64, state interfa } func (rm *RuntimeJavaScriptMatchCore) MatchJoin(tick int64, state interface{}, joins []*MatchPresence) (interface{}, error) { - presences := make([]interface{}, 0, len(joins)) + presences := make([]any, 0, len(joins)) for _, p := range joins { - presenceMap := make(map[string]interface{}, 5) - presenceMap["userId"] = p.UserID.String() - presenceMap["sessionId"] = p.SessionID.String() - presenceMap["username"] = p.Username - presenceMap["node"] = p.Node - presenceMap["reason"] = p.Reason + presenceObj := rm.vm.NewObject() + _ = presenceObj.Set("userId", p.UserID.String()) + _ = presenceObj.Set("sessionId", p.SessionID.String()) + _ = presenceObj.Set("username", p.Username) + _ = presenceObj.Set("node", p.Node) + _ = presenceObj.Set("reason", p.Reason) - presences = append(presences, presenceMap) + presences = append(presences, presenceObj) } - pointerizeSlices(state) - stateObject := rm.vm.NewObject() - for k, v := range state.(map[string]any) { - _ = stateObject.Set(k, v) - } - args := []goja.Value{rm.ctx, rm.loggerModule, rm.nakamaModule, rm.dispatcher, rm.vm.ToValue(tick), rm.vm.ToValue(stateObject), rm.vm.ToValue(presences)} - retVal, err := rm.joinFn(goja.Null(), args...) + stateObj := state.(*goja.Object) + args := []goja.Value{rm.ctx, rm.loggerModule, rm.nakamaModule, rm.dispatcher, rm.vm.ToValue(tick), stateObj, rm.vm.NewArray(presences...)} + retVal, err := rm.joinFn(goja.Undefined(), args...) if err != nil { return nil, err } @@ -356,16 +333,13 @@ func (rm *RuntimeJavaScriptMatchCore) MatchJoin(tick int64, state interface{}, j return nil, nil } - retMap, ok := retVal.Export().(map[string]interface{}) - if !ok { + retObj := retVal.ToObject(rm.vm) + if retObj == nil { return nil, errors.New("matchJoin is expected to return an object with 'state' property") } - newState, ok := retMap["state"] - if !ok { - return nil, errors.New("matchJoin is expected to return an object with 'state' object property") - } - if _, ok = newState.(map[string]any); !ok { + newState := retObj.Get("state") + if newState == nil { return nil, errors.New("matchJoin is expected to return an object with 'state' object property") } @@ -375,24 +349,19 @@ func (rm *RuntimeJavaScriptMatchCore) MatchJoin(tick int64, state interface{}, j func (rm *RuntimeJavaScriptMatchCore) MatchLeave(tick int64, state interface{}, leaves []*MatchPresence) (interface{}, error) { presences := make([]interface{}, 0, len(leaves)) for _, p := range leaves { - presenceMap := make(map[string]interface{}, 5) - presenceMap["userId"] = p.UserID.String() - presenceMap["sessionId"] = p.SessionID.String() - presenceMap["username"] = p.Username - presenceMap["node"] = p.Node - presenceMap["reason"] = p.Reason + presenceObj := rm.vm.NewObject() + _ = presenceObj.Set("userId", p.UserID.String()) + _ = presenceObj.Set("sessionId", p.SessionID.String()) + _ = presenceObj.Set("username", p.Username) + _ = presenceObj.Set("node", p.Node) + _ = presenceObj.Set("reason", p.Reason) - presences = append(presences, presenceMap) + presences = append(presences, presenceObj) } - pointerizeSlices(state) - s := state.(map[string]any) - o := rm.vm.NewObject() - for k, v := range s { - _ = o.Set(k, v) - } - args := []goja.Value{rm.ctx, rm.loggerModule, rm.nakamaModule, rm.dispatcher, rm.vm.ToValue(tick), rm.vm.ToValue(o), rm.vm.ToValue(presences)} - retVal, err := rm.leaveFn(goja.Null(), args...) + stateObj := state.(*goja.Object) + args := []goja.Value{rm.ctx, rm.loggerModule, rm.nakamaModule, rm.dispatcher, rm.vm.ToValue(tick), stateObj, rm.vm.NewArray(presences...)} + retVal, err := rm.leaveFn(goja.Undefined(), args...) if err != nil { return nil, err } @@ -401,17 +370,14 @@ func (rm *RuntimeJavaScriptMatchCore) MatchLeave(tick int64, state interface{}, return nil, nil } - retMap, ok := retVal.Export().(map[string]interface{}) - if !ok { - return nil, errors.New("matchLeave is expected to return an object with 'state' property") + retObj := retVal.ToObject(rm.vm) + if retObj == nil { + return nil, errors.New("matchLoop is expected to return an object with 'state' property") } - newState, ok := retMap["state"] - if !ok { - return nil, errors.New("matchLeave is expected to return an object with 'state' object property") - } - if _, ok = newState.(map[string]any); !ok { - return nil, errors.New("matchLeave is expected to return an object with 'state' object property") + newState := retObj.Get("state") + if newState == nil { + return nil, errors.New("matchLoop is expected to return an object with 'state' object property") } return newState, nil @@ -419,37 +385,33 @@ func (rm *RuntimeJavaScriptMatchCore) MatchLeave(tick int64, state interface{}, func (rm *RuntimeJavaScriptMatchCore) MatchLoop(tick int64, state interface{}, inputCh <-chan *MatchDataMessage) (interface{}, error) { size := len(inputCh) - inputs := make([]interface{}, 0, size) + inputs := make([]any, 0, size) for i := 0; i < size; i++ { msg := <-inputCh - presenceMap := make(map[string]interface{}, 5) - presenceMap["userId"] = msg.UserID.String() - presenceMap["sessionId"] = msg.SessionID.String() - presenceMap["username"] = msg.Username - presenceMap["node"] = msg.Node + presenceObj := rm.vm.NewObject() + _ = presenceObj.Set("userId", msg.UserID.String()) + _ = presenceObj.Set("sessionId", msg.SessionID.String()) + _ = presenceObj.Set("username", msg.Username) + _ = presenceObj.Set("node", msg.Node) - msgMap := make(map[string]interface{}, 5) - msgMap["sender"] = presenceMap - msgMap["opCode"] = msg.OpCode + msgObj := rm.vm.NewObject() + _ = msgObj.Set("sender", presenceObj) + _ = msgObj.Set("opCode", msg.OpCode) if msg.Data == nil { - msgMap["data"] = goja.Null() + _ = msgObj.Set("data", goja.Null()) } else { - msgMap["data"] = rm.vm.NewArrayBuffer(msg.Data) + _ = msgObj.Set("data", rm.vm.NewArrayBuffer(msg.Data)) } - msgMap["reliable"] = msg.Reliable - msgMap["receiveTimeMs"] = msg.ReceiveTime + _ = msgObj.Set("reliable", msg.Reliable) + _ = msgObj.Set("receiveTimeMs", msg.ReceiveTime) - inputs = append(inputs, msgMap) + inputs = append(inputs, msgObj) } - pointerizeSlices(state) - stateObject := rm.vm.NewObject() - for k, v := range state.(map[string]any) { - _ = stateObject.Set(k, v) - } - args := []goja.Value{rm.ctx, rm.loggerModule, rm.nakamaModule, rm.dispatcher, rm.vm.ToValue(tick), rm.vm.ToValue(stateObject), rm.vm.ToValue(inputs)} - retVal, err := rm.loopFn(goja.Null(), args...) + stateObj := state.(*goja.Object) + args := []goja.Value{rm.ctx, rm.loggerModule, rm.nakamaModule, rm.dispatcher, rm.vm.ToValue(tick), stateObj, rm.vm.NewArray(inputs...)} + retVal, err := rm.loopFn(goja.Undefined(), args...) if err != nil { return nil, err } @@ -458,48 +420,42 @@ func (rm *RuntimeJavaScriptMatchCore) MatchLoop(tick int64, state interface{}, i return nil, nil } - retMap, ok := retVal.Export().(map[string]interface{}) - if !ok { + retObj := retVal.ToObject(rm.vm) + if retObj == nil { return nil, errors.New("matchLoop is expected to return an object with 'state' property") } - newState, ok := retMap["state"] - if !ok { + newState := retObj.Get("state") + if newState == nil { return nil, errors.New("matchLoop is expected to return an object with 'state' object property") } - if _, ok = newState.(map[string]any); !ok { - return nil, errors.New("matchLeave is expected to return an object with 'state' object property") - } return newState, nil } func (rm *RuntimeJavaScriptMatchCore) MatchTerminate(tick int64, state interface{}, graceSeconds int) (interface{}, error) { - pointerizeSlices(state) - stateObject := rm.vm.NewObject() - for k, v := range state.(map[string]any) { - _ = stateObject.Set(k, v) + stateObj := state.(*goja.Object) + if stateObj == nil { + return nil, errors.New("matchTerminate is expected to return an object with 'state' object property") } - args := []goja.Value{rm.ctx, rm.loggerModule, rm.nakamaModule, rm.dispatcher, rm.vm.ToValue(tick), rm.vm.ToValue(stateObject), rm.vm.ToValue(graceSeconds)} - retVal, err := rm.terminateFn(goja.Null(), args...) + + args := []goja.Value{rm.ctx, rm.loggerModule, rm.nakamaModule, rm.dispatcher, rm.vm.ToValue(tick), stateObj, rm.vm.ToValue(graceSeconds)} + retVal, err := rm.terminateFn(goja.Undefined(), args...) if err != nil { return nil, err } - retMap, ok := retVal.Export().(map[string]interface{}) - if !ok { - return nil, errors.New("matchTerminate is expected to return an object with 'state' property") - } - if goja.IsNull(retVal) || goja.IsUndefined(retVal) { return nil, nil } - newState, ok := retMap["state"] - if !ok { + retObj := retVal.ToObject(rm.vm) + if retObj == nil { return nil, errors.New("matchTerminate is expected to return an object with 'state' object property") } - if _, ok = newState.(map[string]any); !ok { + + newState := retObj.Get("state") + if newState == nil { return nil, errors.New("matchTerminate is expected to return an object with 'state' object property") } @@ -507,41 +463,31 @@ func (rm *RuntimeJavaScriptMatchCore) MatchTerminate(tick int64, state interface } func (rm *RuntimeJavaScriptMatchCore) MatchSignal(tick int64, state interface{}, data string) (interface{}, string, error) { - pointerizeSlices(state) - stateObject := rm.vm.NewObject() - for k, v := range state.(map[string]any) { - _ = stateObject.Set(k, v) - } - args := []goja.Value{rm.ctx, rm.loggerModule, rm.nakamaModule, rm.dispatcher, rm.vm.ToValue(tick), rm.vm.ToValue(stateObject), rm.vm.ToValue(data)} - retVal, err := rm.signalFn(goja.Null(), args...) + stateObj := state.(*goja.Object) + args := []goja.Value{rm.ctx, rm.loggerModule, rm.nakamaModule, rm.dispatcher, rm.vm.ToValue(tick), stateObj, rm.vm.ToValue(data)} + retVal, err := rm.signalFn(goja.Undefined(), args...) if err != nil { return nil, "", err } - retMap, ok := retVal.Export().(map[string]interface{}) - if !ok { - return nil, "", errors.New("matchSignal is expected to return an object with 'state' property") - } - if goja.IsNull(retVal) || goja.IsUndefined(retVal) { return nil, "", nil } - newState, ok := retMap["state"] - if !ok { + retObj := retVal.ToObject(rm.vm) + if retObj == nil { return nil, "", errors.New("matchSignal is expected to return an object with 'state' property") } - if _, ok = newState.(map[string]any); !ok { - return nil, "", errors.New("matchSignal is expected to return an object with 'state' object property") + + newState := retObj.Get("state") + if newState == nil { + return nil, "", errors.New("matchSignal is expected to return an object with 'state' property") } - responseDataRet, ok := retMap["data"] var responseData string - if ok { - responseData, ok = responseDataRet.(string) - if !ok { - return nil, "", errors.New("matchSignal 'data' property must be a string") - } + responseDataVal := retObj.Get("responseData") + if responseDataVal != nil { + responseData = responseDataVal.String() } return newState, responseData, nil diff --git a/server/runtime_javascript_nakama.go b/server/runtime_javascript_nakama.go index 56075cbc5..510c3bca2 100644 --- a/server/runtime_javascript_nakama.go +++ b/server/runtime_javascript_nakama.go @@ -421,14 +421,11 @@ func (n *RuntimeJavascriptNakamaModule) storageIndexList(r *goja.Runtime) func(g _ = obj.Set("permissionWrite", o.PermissionWrite) _ = obj.Set("createTime", o.CreateTime.Seconds) _ = obj.Set("updateTime", o.UpdateTime.Seconds) - - valueMap := make(map[string]interface{}) - err = json.Unmarshal([]byte(o.Value), &valueMap) + value, err := jsJsonParse(r, o.Value) if err != nil { panic(r.NewGoError(fmt.Errorf("failed to convert value to json: %s", err.Error()))) } - pointerizeSlices(valueMap) - _ = obj.Set("value", valueMap) + _ = obj.Set("value", value) objects = append(objects, obj) } @@ -1782,7 +1779,7 @@ func (n *RuntimeJavascriptNakamaModule) authenticateGoogle(r *goja.Runtime) func func (n *RuntimeJavascriptNakamaModule) authenticateSteam(r *goja.Runtime) func(goja.FunctionCall) goja.Value { return func(f goja.FunctionCall) goja.Value { if n.config.GetSocial().Steam.PublisherKey == "" || n.config.GetSocial().Steam.AppID == 0 { - panic(r.NewGoError(errors.New("Steam authentication is not configured"))) + panic(r.NewGoError(errors.New("steam authentication is not configured"))) } token := getJsString(r, f.Argument(0)) @@ -1905,12 +1902,12 @@ func (n *RuntimeJavascriptNakamaModule) accountGetId(r *goja.Runtime) func(goja. panic(r.NewGoError(fmt.Errorf("error getting account: %v", err.Error()))) } - accountData, err := accountToJsObject(account) + accountData, err := accountToJsObject(r, account) if err != nil { panic(r.NewGoError(err)) } - return r.ToValue(accountData) + return accountData } } @@ -1941,16 +1938,16 @@ func (n *RuntimeJavascriptNakamaModule) accountsGetId(r *goja.Runtime) func(goja panic(r.NewGoError(fmt.Errorf("failed to get accounts: %s", err.Error()))) } - accountsData := make([]map[string]interface{}, 0, len(accounts)) + accountsData := make([]any, 0, len(accounts)) for _, account := range accounts { - accountData, err := accountToJsObject(account) + accountData, err := accountToJsObject(r, account) if err != nil { panic(r.NewGoError(err)) } accountsData = append(accountsData, accountData) } - return r.ToValue(accountsData) + return r.NewArray(accountsData...) } } @@ -2124,16 +2121,16 @@ func (n *RuntimeJavascriptNakamaModule) usersGetId(r *goja.Runtime) func(goja.Fu panic(r.NewGoError(fmt.Errorf("failed to get users: %s", err.Error()))) } - usersData := make([]map[string]any, 0, len(users.Users)) + usersData := make([]any, 0, len(users.Users)) for _, user := range users.Users { - userData, err := userToJsObject(user) + userData, err := userToJsObject(r, user) if err != nil { panic(r.NewGoError(err)) } usersData = append(usersData, userData) } - return r.ToValue(usersData) + return r.NewArray(usersData...) } } @@ -2159,16 +2156,16 @@ func (n *RuntimeJavascriptNakamaModule) usersGetUsername(r *goja.Runtime) func(g panic(r.NewGoError(fmt.Errorf("failed to get users: %s", err.Error()))) } - usersData := make([]map[string]interface{}, 0, len(users.Users)) + usersData := make([]any, 0, len(users.Users)) for _, user := range users.Users { - userData, err := userToJsObject(user) + userData, err := userToJsObject(r, user) if err != nil { panic(r.NewGoError(err)) } usersData = append(usersData, userData) } - return r.ToValue(usersData) + return r.NewArray(usersData...) } } @@ -2210,26 +2207,25 @@ func (n *RuntimeJavascriptNakamaModule) usersGetFriendStatus(r *goja.Runtime) fu userFriends := make([]interface{}, 0, len(friends)) for _, f := range friends { - fum, err := userToJsObject(f.User) + fum, err := userToJsObject(r, f.User) if err != nil { panic(r.NewGoError(err)) } - fm := make(map[string]interface{}, 4) - fm["state"] = f.State.Value - fm["updateTime"] = f.UpdateTime.Seconds - fm["user"] = fum - metadata := make(map[string]interface{}) - if err = json.Unmarshal([]byte(f.Metadata), &metadata); err != nil { + fm := r.NewObject() + _ = fm.Set("state", f.State.Value) + _ = fm.Set("updateTime", f.UpdateTime.Seconds) + _ = fm.Set("user", fum) + metadata, err := jsJsonParse(r, f.Metadata) + if err != nil { panic(r.NewGoError(fmt.Errorf("error while trying to unmarshal friend metadata: %v", err.Error()))) } - pointerizeSlices(metadata) - fm["metadata"] = metadata + _ = fm.Set("metadata", metadata) userFriends = append(userFriends, fm) } - return r.ToValue(userFriends) + return r.NewArray(userFriends...) } } @@ -2251,16 +2247,16 @@ func (n *RuntimeJavascriptNakamaModule) usersGetRandom(r *goja.Runtime) func(goj panic(r.NewGoError(fmt.Errorf("failed to get users: %s", err.Error()))) } - usersData := make([]map[string]interface{}, 0, len(users)) + usersData := make([]any, 0, len(users)) for _, user := range users { - userData, err := userToJsObject(user) + userData, err := userToJsObject(r, user) if err != nil { panic(r.NewGoError(err)) } usersData = append(usersData, userData) } - return r.ToValue(usersData) + return r.NewArray(usersData...) } } @@ -4651,38 +4647,36 @@ func (n *RuntimeJavascriptNakamaModule) storageList(r *goja.Runtime) func(goja.F objects := make([]interface{}, 0, len(objectList.Objects)) for _, o := range objectList.Objects { - objectMap := make(map[string]interface{}, 9) - objectMap["key"] = o.Key - objectMap["collection"] = o.Collection + objectMap := r.NewObject() + _ = objectMap.Set("key", o.Key) + _ = objectMap.Set("collection", o.Collection) if o.UserId != "" { - objectMap["userId"] = o.UserId + _ = objectMap.Set("userId", o.UserId) } else { - objectMap["userId"] = nil + _ = objectMap.Set("userId", nil) } - objectMap["version"] = o.Version - objectMap["permissionRead"] = o.PermissionRead - objectMap["permissionWrite"] = o.PermissionWrite - objectMap["createTime"] = o.CreateTime.Seconds - objectMap["updateTime"] = o.UpdateTime.Seconds + _ = objectMap.Set("version", o.Version) + _ = objectMap.Set("permissionRead", o.PermissionRead) + _ = objectMap.Set("permissionWrite", o.PermissionWrite) + _ = objectMap.Set("createTime", o.CreateTime.Seconds) + _ = objectMap.Set("updateTime", o.UpdateTime.Seconds) - valueMap := make(map[string]interface{}) - err = json.Unmarshal([]byte(o.Value), &valueMap) + value, err := jsJsonParse(r, o.Value) if err != nil { panic(r.NewGoError(fmt.Errorf("failed to convert value to json: %s", err.Error()))) } - pointerizeSlices(valueMap) - objectMap["value"] = valueMap + _ = objectMap.Set("value", value) objects = append(objects, objectMap) } - returnObj := map[string]interface{}{ - "objects": objects, - } + returnObj := r.NewObject() + _ = returnObj.Set("objects", r.NewArray(objects...)) + if objectList.Cursor == "" { - returnObj["cursor"] = nil + _ = returnObj.Set("cursor", goja.Null()) } else { - returnObj["cursor"] = objectList.Cursor + _ = returnObj.Set("cursor", objectList.Cursor) } return r.ToValue(returnObj) @@ -4760,33 +4754,31 @@ func (n *RuntimeJavascriptNakamaModule) storageRead(r *goja.Runtime) func(goja.F results := make([]interface{}, 0, len(objects.Objects)) for _, o := range objects.GetObjects() { - oMap := make(map[string]interface{}) + obj := r.NewObject() - oMap["key"] = o.Key - oMap["collection"] = o.Collection + _ = obj.Set("key", o.Key) + _ = obj.Set("collection", o.Collection) if o.UserId != "" { - oMap["userId"] = o.UserId + _ = obj.Set("userId", o.UserId) } else { - oMap["userId"] = nil + _ = obj.Set("userId", nil) } - oMap["version"] = o.Version - oMap["permissionRead"] = o.PermissionRead - oMap["permissionWrite"] = o.PermissionWrite - oMap["createTime"] = o.CreateTime.Seconds - oMap["updateTime"] = o.UpdateTime.Seconds + _ = obj.Set("version", o.Version) + _ = obj.Set("permissionRead", o.PermissionRead) + _ = obj.Set("permissionWrite", o.PermissionWrite) + _ = obj.Set("createTime", o.CreateTime.Seconds) + _ = obj.Set("updateTime", o.UpdateTime.Seconds) - valueMap := make(map[string]interface{}) - err = json.Unmarshal([]byte(o.Value), &valueMap) + value, err := jsJsonParse(r, o.Value) if err != nil { panic(r.NewGoError(fmt.Errorf("failed to convert value to json: %s", err.Error()))) } - pointerizeSlices(valueMap) - oMap["value"] = valueMap + _ = obj.Set("value", value) - results = append(results, oMap) + results = append(results, obj) } - return r.ToValue(results) + return r.NewArray(results...) } } @@ -4819,16 +4811,16 @@ func (n *RuntimeJavascriptNakamaModule) storageWrite(r *goja.Runtime) func(goja. results := make([]interface{}, 0, len(acks.Acks)) for _, ack := range acks.Acks { - result := make(map[string]interface{}, 4) - result["key"] = ack.Key - result["collection"] = ack.Collection - result["userId"] = ack.UserId - result["version"] = ack.Version + result := r.NewObject() + _ = result.Set("key", ack.Key) + _ = result.Set("collection", ack.Collection) + _ = result.Set("userId", ack.UserId) + _ = result.Set("version", ack.Version) results = append(results, result) } - return r.ToValue(results) + return r.NewArray(results...) } } @@ -5557,7 +5549,7 @@ func (n *RuntimeJavascriptNakamaModule) leaderboardList(r *goja.Runtime) func(go results := make([]interface{}, 0, len(list.Leaderboards)) for _, leaderboard := range list.Leaderboards { - t, err := leaderboardToJsObject(leaderboard) + t, err := leaderboardToJsObject(r, leaderboard) if err != nil { panic(r.NewGoError(err)) } @@ -5565,17 +5557,16 @@ func (n *RuntimeJavascriptNakamaModule) leaderboardList(r *goja.Runtime) func(go results = append(results, t) } - resultMap := make(map[string]interface{}, 2) - + result := r.NewObject() if list.Cursor == "" { - resultMap["cursor"] = nil + _ = result.Set("cursor", goja.Null) } else { - resultMap["cursor"] = list.Cursor + _ = result.Set("cursor", list.Cursor) } - resultMap["leaderboards"] = results + _ = result.Set("leaderboards", r.NewArray(results...)) - return r.ToValue(resultMap) + return result } } @@ -5797,7 +5788,7 @@ func (n *RuntimeJavascriptNakamaModule) leaderboardRecordWrite(r *goja.Runtime) panic(r.NewGoError(fmt.Errorf("error writing leaderboard record: %v", err.Error()))) } - return r.ToValue(leaderboardRecordToJsMap(r, record)) + return leaderboardRecordToJsValue(r, record) } } @@ -5846,7 +5837,7 @@ func (n *RuntimeJavascriptNakamaModule) leaderboardsGetId(r *goja.Runtime) func( leaderboardsSlice := make([]interface{}, 0, len(leaderboards)) for _, l := range leaderboards { - leaderboardMap, err := leaderboardToJsObject(l) + leaderboardMap, err := leaderboardToJsObject(r, l) if err != nil { panic(r.NewGoError(err)) } @@ -5854,7 +5845,7 @@ func (n *RuntimeJavascriptNakamaModule) leaderboardsGetId(r *goja.Runtime) func( leaderboardsSlice = append(leaderboardsSlice, leaderboardMap) } - return r.ToValue(leaderboardsSlice) + return r.NewArray(leaderboardsSlice...) } } @@ -6654,7 +6645,7 @@ func (n *RuntimeJavascriptNakamaModule) tournamentsGetId(r *goja.Runtime) func(g results := make([]interface{}, 0, len(list)) for _, tournament := range list { - tournament, err := tournamentToJsObject(tournament) + tournament, err := tournamentToJsObject(r, tournament) if err != nil { panic(r.NewGoError(err)) } @@ -6662,7 +6653,7 @@ func (n *RuntimeJavascriptNakamaModule) tournamentsGetId(r *goja.Runtime) func(g results = append(results, tournament) } - return r.ToValue(results) + return r.NewArray(results...) } } @@ -6732,66 +6723,63 @@ func (n *RuntimeJavascriptNakamaModule) tournamentRecordsList(r *goja.Runtime) f func leaderboardRecordsListToJs(r *goja.Runtime, records []*api.LeaderboardRecord, ownerRecords []*api.LeaderboardRecord, prevCursor, nextCursor string, rankCount int64) goja.Value { recordsSlice := make([]interface{}, 0, len(records)) for _, record := range records { - recordsSlice = append(recordsSlice, leaderboardRecordToJsMap(r, record)) + recordsSlice = append(recordsSlice, leaderboardRecordToJsValue(r, record)) } ownerRecordsSlice := make([]interface{}, 0, len(ownerRecords)) for _, ownerRecord := range ownerRecords { - ownerRecordsSlice = append(ownerRecordsSlice, leaderboardRecordToJsMap(r, ownerRecord)) + ownerRecordsSlice = append(ownerRecordsSlice, leaderboardRecordToJsValue(r, ownerRecord)) } - resultMap := make(map[string]interface{}, 5) - - resultMap["records"] = recordsSlice - resultMap["ownerRecords"] = ownerRecordsSlice + resultObj := r.NewObject() + _ = resultObj.Set("records", r.NewArray(recordsSlice...)) + _ = resultObj.Set("ownerRecords", r.NewArray(ownerRecordsSlice...)) if nextCursor != "" { - resultMap["nextCursor"] = nextCursor + _ = resultObj.Set("nextCursor", nextCursor) } else { - resultMap["nextCursor"] = nil + _ = resultObj.Set("nextCursor", goja.Null()) } if prevCursor != "" { - resultMap["prevCursor"] = prevCursor + _ = resultObj.Set("prevCursor", prevCursor) } else { - resultMap["prevCursor"] = nil + _ = resultObj.Set("prevCursor", goja.Null()) } + _ = resultObj.Set("rankCount", rankCount) - resultMap["rankCount"] = rankCount - - return r.ToValue(resultMap) + return resultObj } -func leaderboardRecordToJsMap(r *goja.Runtime, record *api.LeaderboardRecord) map[string]interface{} { - recordMap := make(map[string]interface{}, 12) - recordMap["leaderboardId"] = record.LeaderboardId - recordMap["ownerId"] = record.OwnerId +func leaderboardRecordToJsValue(r *goja.Runtime, record *api.LeaderboardRecord) goja.Value { + recordObj := r.NewObject() + _ = recordObj.Set("leaderboardId", record.LeaderboardId) + _ = recordObj.Set("ownerId", record.OwnerId) if record.Username != nil { - recordMap["username"] = record.Username.Value + _ = recordObj.Set("username", record.Username.Value) } else { - recordMap["username"] = nil - } - recordMap["score"] = record.Score - recordMap["subscore"] = record.Subscore - recordMap["numScore"] = record.NumScore - recordMap["maxNumScore"] = record.MaxNumScore - metadataMap := make(map[string]interface{}) - err := json.Unmarshal([]byte(record.Metadata), &metadataMap) + _ = recordObj.Set("username", goja.Null()) + } + _ = recordObj.Set("score", record.Score) + _ = recordObj.Set("subscore", record.Subscore) + _ = recordObj.Set("numScore", record.NumScore) + _ = recordObj.Set("maxNumScore", record.MaxNumScore) + + metadata, err := jsJsonParse(r, record.Metadata) if err != nil { panic(r.NewGoError(fmt.Errorf("failed to convert metadata to json: %s", err.Error()))) } - pointerizeSlices(metadataMap) - recordMap["metadata"] = metadataMap - recordMap["createTime"] = record.CreateTime.Seconds - recordMap["updateTime"] = record.UpdateTime.Seconds + _ = recordObj.Set("metadata", metadata) + _ = recordObj.Set("createTime", record.CreateTime.Seconds) + _ = recordObj.Set("updateTime", record.UpdateTime.Seconds) if record.ExpiryTime != nil { - recordMap["expiryTime"] = record.ExpiryTime.Seconds + _ = recordObj.Set("expiryTime", record.ExpiryTime.Seconds) } else { - recordMap["expiryTime"] = nil + _ = recordObj.Set("expiryTime", goja.Null()) } - recordMap["rank"] = record.Rank + _ = recordObj.Set("rank", record.Rank) - return recordMap + return recordObj } // @group tournaments @@ -6874,7 +6862,7 @@ func (n *RuntimeJavascriptNakamaModule) tournamentList(r *goja.Runtime) func(goj results := make([]interface{}, 0, len(list.Tournaments)) for _, tournament := range list.Tournaments { - t, err := tournamentToJsObject(tournament) + t, err := tournamentToJsObject(r, tournament) if err != nil { panic(r.NewGoError(err)) } @@ -6882,17 +6870,17 @@ func (n *RuntimeJavascriptNakamaModule) tournamentList(r *goja.Runtime) func(goj results = append(results, t) } - resultMap := make(map[string]interface{}, 2) + result := r.NewObject() if list.Cursor == "" { - resultMap["cursor"] = nil + _ = result.Set("cursor", goja.Null()) } else { - resultMap["cursor"] = list.Cursor + _ = result.Set("cursor", list.Cursor) } - resultMap["tournaments"] = results + _ = result.Set("tournaments", r.NewArray(results...)) - return r.ToValue(resultMap) + return result } } @@ -6978,7 +6966,7 @@ func (n *RuntimeJavascriptNakamaModule) tournamentRecordWrite(r *goja.Runtime) f panic(r.NewGoError(fmt.Errorf("error writing tournament record: %v", err.Error()))) } - return r.ToValue(leaderboardRecordToJsMap(r, record)) + return r.ToValue(leaderboardRecordToJsValue(r, record)) } } @@ -7435,62 +7423,59 @@ func (n *RuntimeJavascriptNakamaModule) groupUsersList(r *goja.Runtime) func(goj for _, gu := range res.GroupUsers { u := gu.User - guMap := make(map[string]interface{}, 18) - - guMap["userId"] = u.Id - guMap["username"] = u.Username - guMap["displayName"] = u.DisplayName - guMap["avatarUrl"] = u.AvatarUrl - guMap["langTag"] = u.LangTag - guMap["location"] = u.Location - guMap["timezone"] = u.Timezone + guObj := r.NewObject() + _ = guObj.Set("userId", u.Id) + _ = guObj.Set("username", u.Username) + _ = guObj.Set("displayName", u.DisplayName) + _ = guObj.Set("avatarUrl", u.AvatarUrl) + _ = guObj.Set("langTag", u.LangTag) + _ = guObj.Set("location", u.Location) + _ = guObj.Set("timezone", u.Timezone) if u.AppleId != "" { - guMap["appleId"] = u.AppleId + _ = guObj.Set("appleId", u.AppleId) } if u.FacebookId != "" { - guMap["facebookId"] = u.FacebookId + _ = guObj.Set("facebookId", u.FacebookId) } if u.FacebookInstantGameId != "" { - guMap["facebookInstantGameId"] = u.FacebookInstantGameId + _ = guObj.Set("facebookInstantGameId", u.FacebookInstantGameId) } if u.GoogleId != "" { - guMap["googleId"] = u.GoogleId + _ = guObj.Set("googleId", u.GoogleId) } if u.GamecenterId != "" { - guMap["gamecenterId"] = u.GamecenterId + _ = guObj.Set("gamecenterId", u.GamecenterId) } if u.SteamId != "" { - guMap["steamId"] = u.SteamId + _ = guObj.Set("steamId", u.SteamId) } - guMap["online"] = u.Online - guMap["edgeCount"] = u.EdgeCount - guMap["createTime"] = u.CreateTime.Seconds - guMap["updateTime"] = u.UpdateTime.Seconds + _ = guObj.Set("online", u.Online) + _ = guObj.Set("edgeCount", u.EdgeCount) + _ = guObj.Set("createTime", u.CreateTime.Seconds) + _ = guObj.Set("updateTime", u.UpdateTime.Seconds) - metadataMap := make(map[string]interface{}) - err = json.Unmarshal([]byte(u.Metadata), &metadataMap) + metadata, err := jsJsonParse(r, u.Metadata) if err != nil { panic(r.NewGoError(fmt.Errorf("failed to convert metadata to json: %s", err.Error()))) } - pointerizeSlices(metadataMap) - guMap["metadata"] = metadataMap + _ = guObj.Set("metadata", metadata) - groupUsers = append(groupUsers, map[string]interface{}{ - "user": guMap, - "state": gu.State.Value, - }) + gsObj := r.NewObject() + _ = gsObj.Set("user", guObj) + _ = gsObj.Set("state", gu.State.Value) + groupUsers = append(groupUsers, gsObj) } - result := make(map[string]interface{}, 2) - result["groupUsers"] = groupUsers + result := r.NewObject() + _ = result.Set("groupUsers", r.NewArray(groupUsers...)) if res.Cursor == "" { - result["cursor"] = nil + _ = result.Set("cursor", goja.Null()) } else { - result["cursor"] = res.Cursor + _ = result.Set("cursor", res.Cursor) } - return r.ToValue(result) + return result } } @@ -7541,44 +7526,41 @@ func (n *RuntimeJavascriptNakamaModule) userGroupsList(r *goja.Runtime) func(goj for _, ug := range res.UserGroups { g := ug.Group - ugMap := make(map[string]interface{}, 12) - - ugMap["id"] = g.Id - ugMap["creatorId"] = g.CreatorId - ugMap["name"] = g.Name - ugMap["description"] = g.Description - ugMap["avatarUrl"] = g.AvatarUrl - ugMap["langTag"] = g.LangTag - ugMap["open"] = g.Open.Value - ugMap["edgeCount"] = g.EdgeCount - ugMap["maxCount"] = g.MaxCount - ugMap["createTime"] = g.CreateTime.Seconds - ugMap["updateTime"] = g.UpdateTime.Seconds - - metadataMap := make(map[string]interface{}) - err = json.Unmarshal([]byte(g.Metadata), &metadataMap) + ugObj := r.NewObject() + _ = ugObj.Set("id", g.Id) + _ = ugObj.Set("creatorId", g.CreatorId) + _ = ugObj.Set("name", g.Name) + _ = ugObj.Set("description", g.Description) + _ = ugObj.Set("avatarUrl", g.AvatarUrl) + _ = ugObj.Set("langTag", g.LangTag) + _ = ugObj.Set("open", g.Open.Value) + _ = ugObj.Set("edgeCount", g.EdgeCount) + _ = ugObj.Set("maxCount", g.MaxCount) + _ = ugObj.Set("createTime", g.CreateTime.Seconds) + _ = ugObj.Set("updateTime", g.UpdateTime.Seconds) + + metadata, err := jsJsonParse(r, g.Metadata) if err != nil { panic(r.NewGoError(fmt.Errorf("failed to convert metadata to json: %s", err.Error()))) } - pointerizeSlices(metadataMap) - ugMap["metadata"] = metadataMap + _ = ugObj.Set("metadata", metadata) - userGroups = append(userGroups, map[string]interface{}{ - "group": ugMap, - "state": ug.State.Value, - }) + uStateObj := r.NewObject() + _ = uStateObj.Set("group", ugObj) + _ = uStateObj.Set("state", ug.State.Value) + userGroups = append(userGroups, uStateObj) } - result := make(map[string]interface{}, 2) - result["userGroups"] = userGroups + result := r.NewObject() + _ = result.Set("userGroups", r.NewArray(userGroups...)) if res.Cursor == "" { - result["cursor"] = nil + _ = result.Set("cursor", goja.Null()) } else { - result["cursor"] = res.Cursor + _ = result.Set("cursor", res.Cursor) } - return r.ToValue(result) + return result } } @@ -7633,33 +7615,31 @@ func (n *RuntimeJavascriptNakamaModule) friendsList(r *goja.Runtime) func(goja.F userFriends := make([]interface{}, 0, len(friends.Friends)) for _, f := range friends.Friends { - fum, err := userToJsObject(f.User) + fum, err := userToJsObject(r, f.User) if err != nil { panic(r.NewGoError(err)) } - fm := make(map[string]interface{}, 4) - fm["state"] = f.State.Value - fm["updateTime"] = f.UpdateTime.Seconds - fm["user"] = fum - metadata := make(map[string]interface{}) - if err = json.Unmarshal([]byte(f.Metadata), &metadata); err != nil { - panic(r.NewGoError(fmt.Errorf("error while trying to unmarshal friend metadata: %v", err.Error()))) + fm := r.NewObject() + _ = fm.Set("state", f.State.Value) + _ = fm.Set("updateTime", f.UpdateTime.Seconds) + _ = fm.Set("user", fum) + metadata, err := jsJsonParse(r, f.Metadata) + if err != nil { + panic(r.NewGoError(fmt.Errorf("failed to convert metadata to json: %s", err.Error()))) } - pointerizeSlices(metadata) - fm["metadata"] = metadata + _ = fm.Set("metadata", metadata) userFriends = append(userFriends, fm) } - result := map[string]interface{}{ - "friends": userFriends, - } + result := r.NewObject() + _ = result.Set("friends", r.NewArray(userFriends...)) if friends.Cursor != "" { - result["cursor"] = friends.Cursor + _ = result.Set("cursor", friends.Cursor) } - return r.ToValue(result) + return result } } @@ -7700,28 +7680,27 @@ func (n *RuntimeJavascriptNakamaModule) friendsOfFriendsList(r *goja.Runtime) fu panic(r.NewGoError(fmt.Errorf("error while trying to list friends for a user: %v", err.Error()))) } - userFriendsOfFriends := make([]interface{}, 0, len(friends.FriendsOfFriends)) + userFriendsOfFriends := make([]any, 0, len(friends.FriendsOfFriends)) for _, f := range friends.FriendsOfFriends { - fum, err := userToJsObject(f.User) + fum, err := userToJsObject(r, f.User) if err != nil { panic(r.NewGoError(err)) } - fm := make(map[string]interface{}, 3) - fm["referrer"] = f.Referrer - fm["user"] = fum + fm := r.NewObject() + _ = fm.Set("referrer", f.Referrer) + _ = fm.Set("user", fum) userFriendsOfFriends = append(userFriendsOfFriends, fm) } - result := map[string]interface{}{ - "friendsOfFriends": userFriendsOfFriends, - } + result := r.NewObject() + _ = result.Set("friendsOfFriends", r.NewArray(userFriendsOfFriends...)) if friends.Cursor != "" { - result["cursor"] = friends.Cursor + _ = result.Set("cursor", friends.Cursor) } - return r.ToValue(result) + return result } } @@ -8376,9 +8355,9 @@ func (n *RuntimeJavascriptNakamaModule) groupsList(r *goja.Runtime) func(goja.Fu panic(r.NewGoError(fmt.Errorf("error listing groups: %s", err.Error()))) } - groupsSlice := make([]interface{}, 0, len(groups.Groups)) + groupsSlice := make([]any, 0, len(groups.Groups)) for _, g := range groups.Groups { - groupData, err := groupToJsObject(g) + groupData, err := groupToJsObject(r, g) if err != nil { panic(r.NewGoError(err)) } @@ -8417,16 +8396,16 @@ func (n *RuntimeJavascriptNakamaModule) groupsGetRandom(r *goja.Runtime) func(go panic(r.NewGoError(fmt.Errorf("failed to get groups: %s", err.Error()))) } - groupsData := make([]map[string]interface{}, 0, len(groups)) + groupsData := make([]any, 0, len(groups)) for _, group := range groups { - userData, err := groupToJsObject(group) + userData, err := groupToJsObject(r, group) if err != nil { panic(r.NewGoError(err)) } groupsData = append(groupsData, userData) } - return r.ToValue(groupsData) + return r.NewArray(groupsData...) } } @@ -9381,178 +9360,168 @@ func getJsBool(r *goja.Runtime, v goja.Value) bool { return b } -func accountToJsObject(account *api.Account) (map[string]interface{}, error) { - accountData := make(map[string]interface{}) - userData, err := userToJsObject(account.User) +func accountToJsObject(r *goja.Runtime, account *api.Account) (goja.Value, error) { + accountData := r.NewObject() + userData, err := userToJsObject(r, account.User) if err != nil { return nil, err } - accountData["user"] = userData + _ = accountData.Set("user", userData) walletData := make(map[string]int64) err = json.Unmarshal([]byte(account.Wallet), &walletData) if err != nil { return nil, fmt.Errorf("failed to convert wallet to json: %s", err.Error()) } - accountData["wallet"] = walletData + _ = accountData.Set("wallet", walletData) if account.Email != "" { - accountData["email"] = account.Email + _ = accountData.Set("email", account.Email) } if len(account.Devices) != 0 { - devices := make([]map[string]string, 0, len(account.Devices)) + devices := make([]any, 0, len(account.Devices)) for _, device := range account.Devices { deviceData := make(map[string]string) deviceData["id"] = device.Id devices = append(devices, deviceData) } - accountData["devices"] = devices + _ = accountData.Set("devices", r.NewArray(devices...)) } if account.CustomId != "" { - accountData["customId"] = account.CustomId + _ = accountData.Set("customId", account.CustomId) } if account.VerifyTime != nil { - accountData["verifyTime"] = account.VerifyTime.Seconds + _ = accountData.Set("verifyTime", account.VerifyTime.Seconds) } if account.DisableTime != nil { - accountData["disableTime"] = account.DisableTime.Seconds + _ = accountData.Set("disableTime", account.DisableTime.Seconds) } return accountData, nil } -func userToJsObject(user *api.User) (map[string]interface{}, error) { - userData := make(map[string]interface{}, 18) - userData["userId"] = user.Id - userData["username"] = user.Username - userData["displayName"] = user.DisplayName - userData["avatarUrl"] = user.AvatarUrl - userData["langTag"] = user.LangTag - userData["location"] = user.Location - userData["timezone"] = user.Timezone +func userToJsObject(r *goja.Runtime, user *api.User) (goja.Value, error) { + userObj := r.NewObject() + _ = userObj.Set("userId", user.Id) + _ = userObj.Set("username", user.Username) + _ = userObj.Set("displayName", user.DisplayName) + _ = userObj.Set("avatarUrl", user.AvatarUrl) + _ = userObj.Set("langTag", user.LangTag) + _ = userObj.Set("location", user.Location) + _ = userObj.Set("timezone", user.Timezone) if user.AppleId != "" { - userData["appleId"] = user.AppleId + _ = userObj.Set("appleId", user.AppleId) } if user.FacebookId != "" { - userData["facebookId"] = user.FacebookId + _ = userObj.Set("facebookId", user.FacebookId) } if user.FacebookInstantGameId != "" { - userData["facebookInstantGameId"] = user.FacebookInstantGameId + _ = userObj.Set("facebookInstantGameId", user.FacebookInstantGameId) } if user.GoogleId != "" { - userData["googleId"] = user.GoogleId + _ = userObj.Set("googleId", user.GoogleId) } if user.GamecenterId != "" { - userData["gamecenterId"] = user.GamecenterId + _ = userObj.Set("gamecenterId", user.GamecenterId) } if user.SteamId != "" { - userData["steamId"] = user.SteamId + _ = userObj.Set("steamId", user.SteamId) } - userData["online"] = user.Online - userData["edgeCount"] = user.EdgeCount - userData["createTime"] = user.CreateTime.Seconds - userData["updateTime"] = user.UpdateTime.Seconds + _ = userObj.Set("online", user.Online) + _ = userObj.Set("edgeCount", user.EdgeCount) + _ = userObj.Set("createTime", user.CreateTime.Seconds) + _ = userObj.Set("updateTime", user.UpdateTime.Seconds) - metadata := make(map[string]interface{}) - err := json.Unmarshal([]byte(user.Metadata), &metadata) + metadata, err := jsJsonParse(r, user.Metadata) if err != nil { return nil, fmt.Errorf("failed to convert metadata to json: %s", err.Error()) } - pointerizeSlices(metadata) - userData["metadata"] = metadata + _ = userObj.Set("metadata", metadata) - return userData, nil + return userObj, nil } -func groupToJsObject(group *api.Group) (map[string]interface{}, error) { - groupMap := make(map[string]interface{}, 12) +func groupToJsObject(r *goja.Runtime, group *api.Group) (goja.Value, error) { + groupObj := r.NewObject() + _ = groupObj.Set("id", group.Id) + _ = groupObj.Set("creatorId", group.CreatorId) + _ = groupObj.Set("name", group.Name) + _ = groupObj.Set("description", group.Description) + _ = groupObj.Set("avatarUrl", group.AvatarUrl) + _ = groupObj.Set("langTag", group.LangTag) + _ = groupObj.Set("open", group.Open.Value) + _ = groupObj.Set("edgeCount", group.EdgeCount) + _ = groupObj.Set("maxCount", group.MaxCount) + _ = groupObj.Set("createTime", group.CreateTime.Seconds) + _ = groupObj.Set("updateTime", group.UpdateTime.Seconds) - groupMap["id"] = group.Id - groupMap["creatorId"] = group.CreatorId - groupMap["name"] = group.Name - groupMap["description"] = group.Description - groupMap["avatarUrl"] = group.AvatarUrl - groupMap["langTag"] = group.LangTag - groupMap["open"] = group.Open.Value - groupMap["edgeCount"] = group.EdgeCount - groupMap["maxCount"] = group.MaxCount - groupMap["createTime"] = group.CreateTime.Seconds - groupMap["updateTime"] = group.UpdateTime.Seconds - - metadataMap := make(map[string]interface{}) - err := json.Unmarshal([]byte(group.Metadata), &metadataMap) + metadata, err := jsJsonParse(r, group.Metadata) if err != nil { return nil, fmt.Errorf("failed to convert group metadata to json: %s", err.Error()) } - pointerizeSlices(metadataMap) - groupMap["metadata"] = metadataMap + _ = groupObj.Set("metadata", metadata) - return groupMap, nil + return groupObj, nil } -func leaderboardToJsObject(leaderboard *api.Leaderboard) (map[string]interface{}, error) { - leaderboardMap := make(map[string]interface{}, 11) - leaderboardMap["id"] = leaderboard.Id - leaderboardMap["operator"] = strings.ToLower(leaderboard.Operator.String()) - leaderboardMap["sortOrder"] = leaderboard.SortOrder - metadataMap := make(map[string]interface{}) - err := json.Unmarshal([]byte(leaderboard.Metadata), &metadataMap) +func leaderboardToJsObject(r *goja.Runtime, leaderboard *api.Leaderboard) (goja.Value, error) { + leaderboardObj := r.NewObject() + _ = leaderboardObj.Set("id", leaderboard.Id) + _ = leaderboardObj.Set("operator", strings.ToLower(leaderboard.Operator.String())) + _ = leaderboardObj.Set("sortOrder", leaderboard.SortOrder) + metadata, err := jsJsonParse(r, leaderboard.Metadata) if err != nil { return nil, fmt.Errorf("failed to convert metadata to json: %s", err.Error()) } - pointerizeSlices(metadataMap) - leaderboardMap["metadata"] = metadataMap - leaderboardMap["createTime"] = leaderboard.CreateTime.Seconds + _ = leaderboardObj.Set("metadata", metadata) + _ = leaderboardObj.Set("createTime", leaderboard.CreateTime.Seconds) if leaderboard.PrevReset != 0 { - leaderboardMap["prevReset"] = leaderboard.PrevReset + _ = leaderboardObj.Set("prevReset", leaderboard.PrevReset) } if leaderboard.NextReset != 0 { - leaderboardMap["nextReset"] = leaderboard.NextReset - } - leaderboardMap["authoritative"] = leaderboard.Authoritative - - return leaderboardMap, nil -} - -func tournamentToJsObject(tournament *api.Tournament) (map[string]interface{}, error) { - tournamentMap := make(map[string]interface{}, 18) - - tournamentMap["id"] = tournament.Id - tournamentMap["title"] = tournament.Title - tournamentMap["description"] = tournament.Description - tournamentMap["category"] = tournament.Category - tournamentMap["sortOrder"] = tournament.SortOrder - tournamentMap["size"] = tournament.Size - tournamentMap["maxSize"] = tournament.MaxSize - tournamentMap["maxNumScore"] = tournament.MaxNumScore - tournamentMap["duration"] = tournament.Duration - tournamentMap["startActive"] = tournament.StartActive - tournamentMap["endActive"] = tournament.EndActive - tournamentMap["canEnter"] = tournament.CanEnter + _ = leaderboardObj.Set("nextReset", leaderboard.NextReset) + } + _ = leaderboardObj.Set("authoritative", leaderboard.Authoritative) + + return leaderboardObj, nil +} + +func tournamentToJsObject(r *goja.Runtime, tournament *api.Tournament) (goja.Value, error) { + tournamentObj := r.NewObject() + _ = tournamentObj.Set("id", tournament.Id) + _ = tournamentObj.Set("title", tournament.Title) + _ = tournamentObj.Set("description", tournament.Description) + _ = tournamentObj.Set("category", tournament.Category) + _ = tournamentObj.Set("sortOrder", tournament.SortOrder) + _ = tournamentObj.Set("size", tournament.Size) + _ = tournamentObj.Set("maxSize", tournament.MaxSize) + _ = tournamentObj.Set("maxNumScore", tournament.MaxNumScore) + _ = tournamentObj.Set("duration", tournament.Duration) + _ = tournamentObj.Set("startActive", tournament.StartActive) + _ = tournamentObj.Set("endActive", tournament.EndActive) + _ = tournamentObj.Set("canEnter", tournament.CanEnter) if tournament.PrevReset != 0 { - tournamentMap["prevReset"] = tournament.PrevReset + _ = tournamentObj.Set("prevReset", tournament.PrevReset) } if tournament.NextReset != 0 { - tournamentMap["nextReset"] = tournament.NextReset + _ = tournamentObj.Set("nextReset", tournament.NextReset) } - metadataMap := make(map[string]interface{}) - err := json.Unmarshal([]byte(tournament.Metadata), &metadataMap) + metadata, err := jsJsonParse(r, tournament.Metadata) if err != nil { return nil, fmt.Errorf("failed to convert metadata to json: %s", err.Error()) } - pointerizeSlices(metadataMap) - tournamentMap["metadata"] = metadataMap - tournamentMap["createTime"] = tournament.CreateTime.Seconds - tournamentMap["startTime"] = tournament.StartTime.Seconds + _ = tournamentObj.Set("metadata", metadata) + _ = tournamentObj.Set("createTime", tournament.CreateTime.Seconds) + _ = tournamentObj.Set("startTime", tournament.StartTime.Seconds) if tournament.EndTime == nil { - tournamentMap["endTime"] = nil + _ = tournamentObj.Set("endTime", nil) } else { - tournamentMap["endTime"] = tournament.EndTime.Seconds + _ = tournamentObj.Set("endTime", tournament.EndTime.Seconds) } - tournamentMap["operator"] = strings.ToLower(tournament.Operator.String()) + _ = tournamentObj.Set("operator", strings.ToLower(tournament.Operator.String())) - return tournamentMap, nil + return tournamentObj, nil } func purchaseResponseToJsObject(validation *api.ValidatePurchaseResponse) map[string]interface{} { @@ -9673,32 +9642,17 @@ func jsObjectToPresenceStream(r *goja.Runtime, streamObj map[string]interface{}) return stream } -// pointerizeSlices recursively walks a map[string]interface{} and replaces any []interface{} references for *[]interface{}. -// This is needed to allow goja operations that resize a JS wrapped Go slice to work as expected, otherwise -// such operations won't reflect on the original slice as it would be passed by value and not by reference. -func pointerizeSlices(m interface{}) { - switch i := m.(type) { - case map[string]interface{}: - for k, v := range i { - if s, ok := v.([]interface{}); ok { - i[k] = &s - pointerizeSlices(&s) - } - if mi, ok := v.(map[string]interface{}); ok { - pointerizeSlices(mi) - } - } - case *[]interface{}: - for idx, v := range *i { - if s, ok := v.([]interface{}); ok { - (*i)[idx] = &s - pointerizeSlices(&s) - } - if mi, ok := v.(map[string]interface{}); ok { - pointerizeSlices(mi) - } - } +func jsJsonParse(vm *goja.Runtime, s string) (goja.Value, error) { + jsonObj := vm.Get("JSON") + parse, ok := goja.AssertFunction(jsonObj.ToObject(vm).Get("parse")) + if !ok { + return nil, errors.New("failed to get js vm json parse method") + } + val, err := parse(jsonObj, vm.ToValue(s)) + if err != nil { + return nil, err } + return val, nil } func exportToSlice[S ~[]E, E any](v goja.Value) (S, error) { From 10550596eb1cd2e6b99f3ba43a8d71d31f901b79 Mon Sep 17 00:00:00 2001 From: Simon Esposito Date: Mon, 10 Feb 2025 17:33:55 +0000 Subject: [PATCH 2/3] Add benchmarks --- server/runtime_javascript_test.go | 64 +++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 3 deletions(-) diff --git a/server/runtime_javascript_test.go b/server/runtime_javascript_test.go index 2dcf9f1a0..071da2b52 100644 --- a/server/runtime_javascript_test.go +++ b/server/runtime_javascript_test.go @@ -15,12 +15,12 @@ package server import ( - "strings" - "testing" - + "encoding/json" "github.com/dop251/goja" "go.uber.org/zap" "go.uber.org/zap/zaptest/observer" + "strings" + "testing" ) func TestJsObjectFreeze(t *testing.T) { @@ -112,3 +112,61 @@ m.get('a'); } }) } + +const data = `{"title_data":{"ads_config": [1,2,3]}}` + +// go test -run=XXX -bench=BenchmarkParse ./... +// go test -run=XXX -bench=BenchmarkParse ./... -benchmem +func BenchmarkParseJsonGo(b *testing.B) { + vm := goja.New() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v, err := jsJsonParse(vm, data) + if err != nil { + b.Fatal(err) + } + vm.Set("data", v) + vm.RunString(`data.foo = []; data.foo.push(3)`) + } +} +func BenchmarkParseJsonGoja(b *testing.B) { + vm := goja.New() + b.ResetTimer() + for i := 0; i < b.N; i++ { + var out map[string]any + if err := json.Unmarshal([]byte(data), &out); err != nil { + b.Fatal(err) + } + pointerizeSlices(out) + vm.Set("data", out) + vm.RunString(`data.foo = []; data.foo.push(3)`) + } +} + +// pointerizeSlices recursively walks a map[string]interface{} and replaces any []interface{} references for *[]interface{}. +// This is needed to allow goja operations that resize a JS wrapped Go slice to work as expected, otherwise +// such operations won't reflect on the original slice as it would be passed by value and not by reference. +func pointerizeSlices(m interface{}) { + switch i := m.(type) { + case map[string]interface{}: + for k, v := range i { + if s, ok := v.([]interface{}); ok { + i[k] = &s + pointerizeSlices(&s) + } + if mi, ok := v.(map[string]interface{}); ok { + pointerizeSlices(mi) + } + } + case *[]interface{}: + for idx, v := range *i { + if s, ok := v.([]interface{}); ok { + (*i)[idx] = &s + pointerizeSlices(&s) + } + if mi, ok := v.(map[string]interface{}); ok { + pointerizeSlices(mi) + } + } + } +} From b6c9a97687954558263dc7fca52ab70093f4b895 Mon Sep 17 00:00:00 2001 From: Simon Esposito Date: Mon, 10 Feb 2025 17:43:34 +0000 Subject: [PATCH 3/3] Linting fix --- server/runtime_javascript_test.go | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/server/runtime_javascript_test.go b/server/runtime_javascript_test.go index 071da2b52..2978ad28d 100644 --- a/server/runtime_javascript_test.go +++ b/server/runtime_javascript_test.go @@ -125,8 +125,11 @@ func BenchmarkParseJsonGo(b *testing.B) { if err != nil { b.Fatal(err) } - vm.Set("data", v) - vm.RunString(`data.foo = []; data.foo.push(3)`) + _ = vm.Set("data", v) + _, err = vm.RunString(`data.foo = []; data.foo.push(3)`) + if err != nil { + b.Fatal(err) + } } } func BenchmarkParseJsonGoja(b *testing.B) { @@ -134,12 +137,16 @@ func BenchmarkParseJsonGoja(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { var out map[string]any - if err := json.Unmarshal([]byte(data), &out); err != nil { + err := json.Unmarshal([]byte(data), &out) + if err != nil { b.Fatal(err) } pointerizeSlices(out) - vm.Set("data", out) - vm.RunString(`data.foo = []; data.foo.push(3)`) + _ = vm.Set("data", out) + _, err = vm.RunString(`data.foo = []; data.foo.push(3)`) + if err != nil { + b.Fatal(err) + } } }