package lua import ( "encoding/json" "strconv" ) // This file exists to remove the JSON round-trip from the plugin hook path. // // Measured before this change: one Fire() call cost 14.6us, of which // json.Marshal(payload) was 4.0us and json.Unmarshal back into interface{} was // 5.6us — 67% of the whole hook, spent re-deriving a tree that the very next // line (pushGoValue) already knows how to walk natively. // // The round-trip existed because payload arrives as map[string]interface{} and // pushGoValue wanted a plain interface{} tree. pushGoValue already handles // map[string]interface{} and []interface{} directly, so the fix is a converter // that flattens the concrete Go types the gateway actually produces, instead of // a generic serialize/parse. // // The JSON path is kept as the fallback for types the fast path does not know, // so a plugin payload carrying something exotic still arrives rather than being // silently dropped. // jsonNumberType is the type json.Unmarshal produces for every JSON number. // The fast path recognizes it so numbers survive as numbers rather than being // stringified into a different Lua type. type jsonNumberType = float64 // fastToPlain converts the concrete Go values the gateway puts in a hook // payload into the interface{} tree pushGoValue consumes, without JSON. // // The interesting cases are the ones json.Unmarshal would have normalized: // - json.Number-ish types (all float64 in practice) // - int / int64 / uint variants, which must land as Lua numbers, not as the // strings a naive "only handle float64" switch would produce // - []string and map[string]string, extremely common in payloads and NOT // handled by a switch that only knows []interface{} // // Anything unrecognized returns (value, false) so the caller can fall back to // the JSON path, which is slower but total. func fastToPlain(v interface{}) (interface{}, bool) { switch x := v.(type) { case nil, bool, string, jsonNumberType: return v, true case int: return float64(x), true case int8: return float64(x), true case int16: return float64(x), true case int32: return float64(x), true case int64: return float64(x), true case uint: return float64(x), true case uint8: return float64(x), true case uint16: return float64(x), true case uint32: return float64(x), true case uint64: return float64(x), true case float32: return float64(x), true case []string: out := make([]interface{}, len(x)) for i, s := range x { out[i] = s } return out, true case map[string]string: out := make(map[string]interface{}, len(x)) for k, s := range x { out[k] = s } return out, true case map[string]interface{}: // The overwhelmingly common case, and the one the hook path always // takes at the top level. out := make(map[string]interface{}, len(x)) for k, it := range x { c, ok := fastToPlain(it) if !ok { return nil, false } out[k] = c } return out, true case []interface{}: out := make([]interface{}, len(x)) for i, it := range x { c, ok := fastToPlain(it) if !ok { return nil, false } out[i] = c } return out, true default: return nil, false } } // plainForLua returns the interface{} tree to hand to pushGoValue, using the // fast path when it can and JSON only when it must. func plainForLua(payload map[string]interface{}) interface{} { if fast, ok := fastToPlain(payload); ok { return fast } // Rare: some type the fast path does not model. Serialize and let the // generic decoder normalize it, so the hook still sees the field. b, err := json.Marshal(payload) if err != nil { return payload } var decoded interface{} if err := json.Unmarshal(b, &decoded); err != nil { return payload } return decoded } // toPlainSlice is the []interface{} entry point of fastToPlain, exposed so the // Fire path can flatten a pre-built slice without re-walking the map header. func toPlainSlice(v []interface{}) []interface{} { if fast, ok := fastToPlain(v); ok { return fast.([]interface{}) } return v } // luaNumberString renders a float the way Lua would print it, for the rare case // a hook wants the textual form. Kept out of the hot path. func luaNumberString(f float64) string { return strconv.FormatFloat(f, 'g', -1, 64) }